Skip to content

Commit 51f86a8

Browse files
authored
Merge pull request #2205 from oasisprotocol/lw/detect-faulty-firmware
Detect faulty Ledger firmware
2 parents 052be53 + e3317e5 commit 51f86a8

File tree

8 files changed

+77
-13
lines changed

8 files changed

+77
-13
lines changed

.changelog/2205.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Detect faulty ledger firmware

extension/src/ExtLedgerAccessPopup/ExtLedgerAccessPopup.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import { StatusGood } from 'grommet-icons/es6/icons/StatusGood'
99
import { Header } from 'app/components/Header'
1010
import { ErrorFormatter } from 'app/components/ErrorFormatter'
1111
import { AlertBox } from 'app/components/AlertBox'
12-
import { WalletErrors } from 'types/errors'
12+
import { WalletError } from 'types/errors'
1313
import logotype from '../../../public/Icon Blue 192.png'
1414
import { CountdownButton } from 'app/components/CountdownButton'
15-
import TransportWebUSB from '@ledgerhq/hw-transport-webusb'
15+
import { getUSBTransport } from '../../../src/app/state/importaccounts/saga'
16+
import { runSaga } from 'redux-saga'
1617

17-
type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error'
18+
type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | WalletError
1819
type ConnectionStatusIconPros = {
1920
success?: boolean
2021
label: string
@@ -48,13 +49,13 @@ export function ExtLedgerAccessPopup() {
4849
const handleConnect = async () => {
4950
setConnection('connecting')
5051
try {
51-
await (await TransportWebUSB.create()).close() // Get access permissions
52+
await (await runSaga({}, getUSBTransport).toPromise()).close() // Get access permissions
5253
setConnection('connected')
5354
// Used to redirect after reopening wallet
5455
window.localStorage.setItem('oasis_wallet_granted_usb_ledger_timestamp', Date.now().toString())
5556
setTimeout(() => window.close(), 5_000)
56-
} catch {
57-
setConnection('error')
57+
} catch (e) {
58+
setConnection(e as WalletError)
5859
}
5960
}
6061

@@ -105,15 +106,15 @@ export function ExtLedgerAccessPopup() {
105106
/>
106107
</Box>
107108
)}
108-
{connection === 'error' && (
109+
{connection instanceof WalletError && (
109110
<Box margin={{ bottom: 'medium' }}>
110111
<ConnectionStatusIcon
111112
success={false}
112113
label={t('ledger.extension.failed', 'Connection failed')}
113114
withMargin
114115
/>
115116
<AlertBox status="error">
116-
<ErrorFormatter code={WalletErrors.LedgerNoDeviceSelected} />
117+
<ErrorFormatter code={connection.type} message={connection.message} />
117118
</AlertBox>
118119
</Box>
119120
)}

src/app/components/ErrorFormatter/index.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,23 @@ export function ErrorFormatter(props: Props) {
9393
'errors.LedgerDerivedDifferentAccount',
9494
'This account does not belong to the currently connected Ledger.',
9595
),
96+
[WalletErrors.LedgerLikelyFaultyFirmware]: (
97+
<Trans
98+
i18nKey="errors.LedgerLikelyFaultyFirmware"
99+
t={t}
100+
defaults="Connecting to Ledger timed out. This is likely due to faulty Ledger firmware: Nano X 2.5.0, Nano S Plus 1.4.0, Stax 1.8.0, Flex 1.4.0. We suggest <WorkaroundsHelp>some workarounds here</WorkaroundsHelp>."
101+
components={{
102+
WorkaroundsHelp: (
103+
<Anchor
104+
href="https://github.com/oasisprotocol/wallet/issues/2198#issuecomment-3177012146"
105+
target="_blank"
106+
rel="noopener"
107+
style={{ display: 'inline' }}
108+
/>
109+
),
110+
}}
111+
/>
112+
),
96113
[WalletErrors.LedgerUnknownError]: t('errors.unknownLedgerError', 'Unknown ledger error: {{message}}', {
97114
message,
98115
}),

src/app/pages/OpenWalletPage/Features/FromLedger/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { Button } from 'grommet/es6/components/Button'
66
import { Text } from 'grommet/es6/components/Text'
77
import { canAccessBle, canAccessNavigatorUsb } from '../../../../lib/ledger'
88
import { useTranslation } from 'react-i18next'
9-
import TransportWebUSB from '@ledgerhq/hw-transport-webusb'
109
import { runtimeIs } from 'app/lib/runtimeIs'
10+
import { runSaga } from 'redux-saga'
11+
import { getUSBTransport } from '../../../../state/importaccounts/saga'
1112

1213
type SelectOpenMethodProps = {
1314
openLedgerAccessPopup?: () => void
@@ -38,7 +39,8 @@ export function FromLedger({ openLedgerAccessPopup }: SelectOpenMethodProps) {
3839
// prompt user to select a ledger device.
3940
// If TransportWebUSB.create() is rejected then call openLedgerAccessPopup. When user
4041
// confirms the prompt tell them to come back here. TransportWebUSB.create() will resolve.
41-
TransportWebUSB.create()
42+
runSaga({}, getUSBTransport)
43+
.toPromise()
4244
.then(t => t.close())
4345
.then(() => setHasUsbLedgerAccess(true))
4446
.catch(() => setHasUsbLedgerAccess(false))

src/app/state/importaccounts/saga.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ import BleTransport from '@oasisprotocol/ionic-ledger-hw-transport-ble/lib'
2323
import { ScanResult } from '@capacitor-community/bluetooth-le'
2424
import { getChainContext } from '../network/saga'
2525

26+
class TransportWebUSBDetectLedgerLikelyFaultyFirmware extends TransportWebUSB {
27+
static async open(a: any) {
28+
const response = await Promise.race([
29+
super.open(a),
30+
new Promise<never>((resolve, reject) =>
31+
setTimeout(() => reject(new WalletError(WalletErrors.LedgerLikelyFaultyFirmware, 'timeout')), 8_000),
32+
),
33+
])
34+
return response
35+
}
36+
}
37+
2638
function* setStep(step: ImportAccountsStep) {
2739
yield* put(importAccountsActions.setStep(step))
2840
}
@@ -59,15 +71,20 @@ function* getBluetoothTransport(device?: ScanResult) {
5971
}
6072
}
6173

62-
function* getUSBTransport() {
74+
export function* getUSBTransport() {
6375
const isSupported = yield* call([TransportWebUSB, TransportWebUSB.isSupported])
6476
if (!isSupported) {
6577
throw new WalletError(WalletErrors.USBTransportNotSupported, 'TransportWebUSB unsupported')
6678
}
6779
try {
68-
return yield* call([TransportWebUSB, TransportWebUSB.create])
80+
return yield* call([
81+
TransportWebUSBDetectLedgerLikelyFaultyFirmware,
82+
TransportWebUSBDetectLedgerLikelyFaultyFirmware.create,
83+
])
6984
} catch (e: any) {
70-
if (e.message.match(/No device selected/)) {
85+
if (e instanceof WalletError) {
86+
throw e
87+
} else if (e.message.match(/No device selected/)) {
7188
throw new WalletError(WalletErrors.LedgerNoDeviceSelected, e.message)
7289
} else {
7390
throw new WalletError(WalletErrors.USBTransportError, e.message)

src/locales/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
},
153153
"errors": {
154154
"LedgerDerivedDifferentAccount": "This account does not belong to the currently connected Ledger.",
155+
"LedgerLikelyFaultyFirmware": "Connecting to Ledger timed out. This is likely due to faulty Ledger firmware: Nano X 2.5.0, Nano S Plus 1.4.0, Stax 1.8.0, Flex 1.4.0. We suggest <WorkaroundsHelp>some workarounds here</WorkaroundsHelp>.",
155156
"LedgerOasisAppIsNotOpen": "Oasis App on Ledger is closed.",
156157
"bluetoothTransportNotSupported": "Bluetooth may be turned off or your current platform does not support Bluetooth capability.",
157158
"cannotSendToSelf": "Cannot send to yourself",

src/types/errors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export enum WalletErrors {
2828
LedgerTransactionRejected = 'transaction_rejected',
2929
LedgerAppVersionNotSupported = 'ledger_version_not_supported',
3030
LedgerDerivedDifferentAccount = 'ledger_derived_different_account',
31+
LedgerLikelyFaultyFirmware = 'ledger_likely_faulty_firmware',
3132
IndexerAPIError = 'indexer_api_error',
3233
DisconnectedError = 'disconnected_error',
3334
ParaTimesUnknownError = 'para_times_unknown_error',

src/types/typed-redux-runSaga.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/** Improve types - based on typed-redux-saga's fork() */
2+
import { RunSagaOptions, Task } from 'redux-saga'
3+
// eslint-disable-next-line no-restricted-imports
4+
import { SagaReturnType } from 'redux-saga/effects'
5+
6+
interface FixedTask<A> extends Task {
7+
result: <T = A>() => T | undefined
8+
toPromise: <T = A>() => Promise<T>
9+
}
10+
11+
declare module 'redux-saga' {
12+
/**
13+
* Allows starting sagas outside the Redux middleware environment. Useful if you
14+
* want to connect a Saga to external input/output, other than store actions.
15+
*
16+
* `runSaga` returns a Task object. Just like the one returned from a `fork`
17+
* effect.
18+
*/
19+
export function runSaga<Action, State, Args extends any[], Fn extends (...args: Args) => any>(
20+
options: RunSagaOptions<Action, State>,
21+
fn: Fn,
22+
...args: Args
23+
): FixedTask<SagaReturnType<Fn>>
24+
}

0 commit comments

Comments
 (0)