diff --git a/.changelog/2201.trivial.md b/.changelog/2201.trivial.md new file mode 100644 index 0000000000..1f1565907e --- /dev/null +++ b/.changelog/2201.trivial.md @@ -0,0 +1 @@ +Deduplicate logic in TransportWebUSB.create and requestDevice diff --git a/extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup.tsx b/extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup.tsx index bd47f3f99c..3f0427cea8 100644 --- a/extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup.tsx +++ b/extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup.tsx @@ -10,10 +10,10 @@ import { Header } from 'app/components/Header' import { ErrorFormatter } from 'app/components/ErrorFormatter' import { AlertBox } from 'app/components/AlertBox' import { WalletErrors } from 'types/errors' -import { requestDevice } from 'app/lib/ledger' import logotype from '../../../public/Icon Blue 192.png' import { CountdownButton } from 'app/components/CountdownButton' -import TransportWebUSB from '@ledgerhq/hw-transport-webusb' +import { getUSBTransport } from '../../../src/app/state/importaccounts/saga' +import { runSaga } from 'redux-saga' type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error' type ConnectionStatusIconPros = { @@ -49,14 +49,11 @@ export function ExtLedgerAccessPopup() { const handleConnect = async () => { setConnection('connecting') try { - const device = await requestDevice() - const transport = await TransportWebUSB.create() - if (device && transport) { - setConnection('connected') - // Used to redirect after reopening wallet - window.localStorage.setItem('oasis_wallet_granted_usb_ledger_timestamp', Date.now().toString()) - setTimeout(() => window.close(), 5_000) - } + await (await runSaga({}, getUSBTransport).toPromise()).close() // Get access permissions + setConnection('connected') + // Used to redirect after reopening wallet + window.localStorage.setItem('oasis_wallet_granted_usb_ledger_timestamp', Date.now().toString()) + setTimeout(() => window.close(), 5_000) } catch { setConnection('error') } diff --git a/extension/src/ExtLedgerAccessPopup/__tests__/index.test.tsx b/extension/src/ExtLedgerAccessPopup/__tests__/index.test.tsx index 905c79db0e..3b0e4daa18 100644 --- a/extension/src/ExtLedgerAccessPopup/__tests__/index.test.tsx +++ b/extension/src/ExtLedgerAccessPopup/__tests__/index.test.tsx @@ -1,11 +1,9 @@ import React from 'react' import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { requestDevice } from 'app/lib/ledger' import { ExtLedgerAccessPopup } from '../ExtLedgerAccessPopup' import TransportWebUSB from '@ledgerhq/hw-transport-webusb' -jest.mock('app/lib/ledger') jest.mock('@ledgerhq/hw-transport') describe('', () => { @@ -16,7 +14,6 @@ describe('', () => { }) it('should render success state', async () => { - jest.mocked(requestDevice).mockResolvedValue({} as USBDevice) jest.mocked(TransportWebUSB.create).mockResolvedValue({} as TransportWebUSB) render() @@ -28,7 +25,6 @@ describe('', () => { }) it('should render error state', async () => { - jest.mocked(requestDevice).mockRejectedValue(new Error('error')) jest.mocked(TransportWebUSB.create).mockRejectedValue(new Error('error')) render() diff --git a/jest.config.js b/jest.config.js index dc8d9deff9..9afad85b11 100644 --- a/jest.config.js +++ b/jest.config.js @@ -24,9 +24,7 @@ const config = { }, ], }, - transformIgnorePatterns: [ - '/node_modules/(?!(@ledgerhq/hw-transport-webusb|cborg|grommet/es6|grommet-icons/es6)/)', - ], + transformIgnorePatterns: ['/node_modules/(?!(cborg|grommet/es6|grommet-icons/es6)/)'], testMatch: [ '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', '/src/**/*.{spec,test}.{js,jsx,ts,tsx}', diff --git a/package.json b/package.json index 14c2177764..881fd5b0c1 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,9 @@ "@capacitor/ios": "6.0.0", "@capawesome/capacitor-app-update": "6.0.0", "@ethereumjs/util": "9.0.3", - "@ledgerhq/hw-transport-webusb": "6.29.4", + "@ledgerhq/hw-transport-web-ble": "6.29.8", + "@ledgerhq/hw-transport-webhid": "6.30.4", + "@ledgerhq/hw-transport-webusb": "6.29.8", "@metamask/browser-passworder": "=3.0.0", "@metamask/jazzicon": "2.0.0", "@oasisprotocol/client": "1.2.0", diff --git a/src/app/lib/__tests__/ledger.test.ts b/src/app/lib/__tests__/ledger.test.ts index 9824cb496b..32dcb92357 100644 --- a/src/app/lib/__tests__/ledger.test.ts +++ b/src/app/lib/__tests__/ledger.test.ts @@ -1,11 +1,9 @@ -import { canAccessBle, Ledger, LedgerSigner, requestDevice } from '../ledger' +import { canAccessBle, Ledger, LedgerSigner } from '../ledger' import OasisApp from '@oasisprotocol/ledger' import { WalletError, WalletErrors } from 'types/errors' import { Wallet, WalletType } from 'app/state/wallet/types' -import { isSupported, requestLedgerDevice } from '@ledgerhq/hw-transport-webusb/lib-es/webusb' import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib' -jest.mock('@ledgerhq/hw-transport-webusb/lib-es/webusb') jest.mock('@oasisprotocol/ionic-ledger-hw-transport-ble/lib', () => { return { isEnabled: jest.fn(), @@ -22,16 +20,6 @@ function mockAppIsOpen(appName: string) { appInfo.mockResolvedValueOnce({ appName: appName, return_code: 0x9000, error_message: '' }) } -describe('Extension access', () => { - it('should return a ledger device when web usb is supported', async () => { - const device = {} as USBDevice - jest.mocked(isSupported).mockResolvedValue(true) - jest.mocked(requestLedgerDevice).mockResolvedValue(device) - const result = await requestDevice() - expect(result).toBe(device) - }) -}) - describe('Ledger Library', () => { afterEach(() => { jest.resetAllMocks() diff --git a/src/app/lib/ledger.ts b/src/app/lib/ledger.ts index 6dbc2f0bdd..bfb3208f2c 100644 --- a/src/app/lib/ledger.ts +++ b/src/app/lib/ledger.ts @@ -5,7 +5,7 @@ import { LedgerWalletType, Wallet, WalletType } from 'app/state/wallet/types' import { WalletError, WalletErrors } from 'types/errors' import { hex2uint, publicKeyToAddress } from './helpers' import type Transport from '@ledgerhq/hw-transport' -import { isSupported, requestLedgerDevice } from '@ledgerhq/hw-transport-webusb/lib-es/webusb' +import TransportWebUSB from '@ledgerhq/hw-transport-web-ble' import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib' import { runtimeIs } from 'app/lib/runtimeIs' @@ -16,7 +16,7 @@ interface LedgerAccount { } export async function canAccessNavigatorUsb(): Promise { - return await isSupported() + return await TransportWebUSB.isSupported() } export async function canAccessBle(): Promise { @@ -26,12 +26,6 @@ export async function canAccessBle(): Promise { return hasBLE && hasLEScan } -export async function requestDevice(): Promise { - if (await isSupported()) { - return await requestLedgerDevice() - } -} - function successOrThrowWalletError(response: Response, message: string) { try { return successOrThrow(response) diff --git a/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx b/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx index 416e1f2328..40072965c9 100644 --- a/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx +++ b/src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx @@ -6,8 +6,9 @@ import { Button } from 'grommet/es6/components/Button' import { Text } from 'grommet/es6/components/Text' import { canAccessBle, canAccessNavigatorUsb } from '../../../../lib/ledger' import { useTranslation } from 'react-i18next' -import TransportWebUSB from '@ledgerhq/hw-transport-webusb' import { runtimeIs } from 'app/lib/runtimeIs' +import { runSaga } from 'redux-saga' +import { getUSBTransport } from '../../../../state/importaccounts/saga' type SelectOpenMethodProps = { openLedgerAccessPopup?: () => void @@ -35,11 +36,12 @@ export function FromLedger({ openLedgerAccessPopup }: SelectOpenMethodProps) { useEffect(() => { if (openLedgerAccessPopup) { // In default ext popup this gets auto-accepted / auto-rejected. In a tab or persistent popup it would - // prompt user to select a ledger device. TransportWebUSB.create seems to match requestDevice called in - // openLedgerAccessPopup. - // If TransportWebUSB.create() is rejected then call openLedgerAccessPopup and requestDevice. When user + // prompt user to select a ledger device. + // If TransportWebUSB.create() is rejected then call openLedgerAccessPopup. When user // confirms the prompt tell them to come back here. TransportWebUSB.create() will resolve. - TransportWebUSB.create() + runSaga({}, getUSBTransport) + .toPromise() + .then(t => t.close()) .then(() => setHasUsbLedgerAccess(true)) .catch(() => setHasUsbLedgerAccess(false)) } else { diff --git a/src/app/state/importaccounts/saga.ts b/src/app/state/importaccounts/saga.ts index 568053041e..ea6c5d118f 100644 --- a/src/app/state/importaccounts/saga.ts +++ b/src/app/state/importaccounts/saga.ts @@ -1,5 +1,5 @@ import { PayloadAction } from '@reduxjs/toolkit' -import TransportWebUSB from '@ledgerhq/hw-transport-webusb' +import TransportWebUSB from '@ledgerhq/hw-transport-web-ble' import * as oasis from '@oasisprotocol/client' import { publicKeyToAddress, uint2hex } from 'app/lib/helpers' import { Ledger, LedgerSigner } from 'app/lib/ledger' @@ -59,7 +59,7 @@ function* getBluetoothTransport(device?: ScanResult) { } } -function* getUSBTransport() { +export function* getUSBTransport() { const isSupported = yield* call([TransportWebUSB, TransportWebUSB.isSupported]) if (!isSupported) { throw new WalletError(WalletErrors.USBTransportNotSupported, 'TransportWebUSB unsupported') diff --git a/src/types/typed-redux-runSaga.d.ts b/src/types/typed-redux-runSaga.d.ts new file mode 100644 index 0000000000..4fbf245709 --- /dev/null +++ b/src/types/typed-redux-runSaga.d.ts @@ -0,0 +1,24 @@ +/** Improve types - based on typed-redux-saga's fork() */ +import { RunSagaOptions, Task } from 'redux-saga' +// eslint-disable-next-line no-restricted-imports +import { SagaReturnType } from 'redux-saga/effects' + +interface FixedTask extends Task { + result: () => T | undefined + toPromise: () => Promise +} + +declare module 'redux-saga' { + /** + * Allows starting sagas outside the Redux middleware environment. Useful if you + * want to connect a Saga to external input/output, other than store actions. + * + * `runSaga` returns a Task object. Just like the one returned from a `fork` + * effect. + */ + export function runSaga any>( + options: RunSagaOptions, + fn: Fn, + ...args: Args + ): FixedTask> +} diff --git a/yarn.lock b/yarn.lock index 80b5b8b32d..2b46e757de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1706,6 +1706,16 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@ledgerhq/devices@8.4.8": + version "8.4.8" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.8.tgz#0eee2e6b7f946dedd7adebb10dcb2dc54c1aaf87" + integrity sha512-joodpi1lTIoPswpH6DBtlAieEfP0xB1NlM8RY3xj82EtO8eU1IRqTQz7wtsbstXJ1FdLRg/CJ5OL41omdBsrZQ== + dependencies: + "@ledgerhq/errors" "^6.23.0" + "@ledgerhq/logs" "^6.13.0" + rxjs "^7.8.1" + semver "^7.3.5" + "@ledgerhq/devices@^8.4.4": version "8.4.4" resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.4.4.tgz#0d195c1650fe57da2fad7f0d9074a0190947cd6f" @@ -1721,17 +1731,43 @@ resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.19.1.tgz#d9ac45ad4ff839e468b8f63766e665537aaede58" integrity sha512-75yK7Nnit/Gp7gdrJAz0ipp31CCgncRp+evWt6QawQEtQKYEDfGo10QywgrrBBixeRxwnMy1DP6g2oCWRf1bjw== -"@ledgerhq/hw-transport-webusb@6.29.4": - version "6.29.4" - resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.29.4.tgz#5926421b8db4b474c7aba4851f21ddd4ad4bcc70" - integrity sha512-HoGF1LlBT9HEGBQy2XeCHrFdv/FEOZU0+J+yfKcgAQIAiASr2MLvdzwoJbUS8h6Gn+vc+/BjzBSO3JNn7Loqbg== +"@ledgerhq/errors@^6.23.0": + version "6.23.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.23.0.tgz#fae7b61095ce4c033394d16085b5f14ca9f9d738" + integrity sha512-bM7tPYShPThtBy1Y+9D28iquOeP5W5s4p7KKD/cUQoaVaPibrtC7Mm4u+IeSlH4WGvFJkTmv0vmZJajuZtM79A== + +"@ledgerhq/hw-transport-web-ble@6.29.8": + version "6.29.8" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-web-ble/-/hw-transport-web-ble-6.29.8.tgz#57d3baa2f4e3cf265b0cd539dcefebb51a667453" + integrity sha512-xDcgH0avx1rQyUqIbVVyCTM8LEoEC+ASniTg6lR+Qvfwuks3f7wQEyEhpmrvZxwKpuATPpGDr54DYq3JRDH6Tg== dependencies: - "@ledgerhq/devices" "^8.4.4" - "@ledgerhq/errors" "^6.19.1" - "@ledgerhq/hw-transport" "^6.31.4" - "@ledgerhq/logs" "^6.12.0" + "@ledgerhq/devices" "8.4.8" + "@ledgerhq/errors" "^6.23.0" + "@ledgerhq/hw-transport" "^6.31.8" + "@ledgerhq/logs" "^6.13.0" + rxjs "^7.8.1" + +"@ledgerhq/hw-transport-webhid@6.30.4": + version "6.30.4" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.30.4.tgz#2f3b3039e7c4fa56692ba606412827c958874dea" + integrity sha512-A13E89U1wfWx8reA9ciwm/+SsbGM163yl1UXuWUQ521hIDAAwVbpnRhwaQT4XjTmDbWhvtT2RYRCoOWWO7N6GA== + dependencies: + "@ledgerhq/devices" "8.4.8" + "@ledgerhq/errors" "^6.23.0" + "@ledgerhq/hw-transport" "^6.31.8" + "@ledgerhq/logs" "^6.13.0" -"@ledgerhq/hw-transport@^6.1.0", "@ledgerhq/hw-transport@^6.28.8", "@ledgerhq/hw-transport@^6.31.4": +"@ledgerhq/hw-transport-webusb@6.29.8": + version "6.29.8" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.29.8.tgz#8dbda97cc124c4b3efc570bb04767558c499c460" + integrity sha512-hAFTnJYB++Ey8qlpGBL2ZHBX4ULQOcBzYzNBcrKUNXqlDaWODtc5uyPCyfGmfUjLnVvZoarDa3h+zDVVQCUHVA== + dependencies: + "@ledgerhq/devices" "8.4.8" + "@ledgerhq/errors" "^6.23.0" + "@ledgerhq/hw-transport" "^6.31.8" + "@ledgerhq/logs" "^6.13.0" + +"@ledgerhq/hw-transport@^6.1.0", "@ledgerhq/hw-transport@^6.28.8", "@ledgerhq/hw-transport@^6.31.4", "@ledgerhq/hw-transport@^6.31.8": version "6.31.4" resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.31.4.tgz#9b23a6de4a4caaa5c24b149c2dea8adde46f0eb1" integrity sha512-6c1ir/cXWJm5dCWdq55NPgCJ3UuKuuxRvf//Xs36Bq9BwkV2YaRQhZITAkads83l07NAdR16hkTWqqpwFMaI6A== @@ -1746,6 +1782,11 @@ resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.12.0.tgz#ad903528bf3687a44da435d7b2479d724d374f5d" integrity sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA== +"@ledgerhq/logs@^6.13.0": + version "6.13.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.13.0.tgz#0b083af64b6b85db0630c7b940a0ed74ff6262b6" + integrity sha512-4+qRW2Pc8V+btL0QEmdB2X+uyx0kOWMWE1/LWsq5sZy3Q5tpi4eItJS6mB0XL3wGW59RQ+8bchNQQ1OW/va8Og== + "@lezer/common@^0.15.0", "@lezer/common@^0.15.7": version "0.15.12" resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.12.tgz#2f21aec551dd5fd7d24eb069f90f54d5bc6ee5e9"