diff --git a/packages/keyring-eth-ledger-bridge/jest.config.js b/packages/keyring-eth-ledger-bridge/jest.config.js index de4929243..a6c5d8576 100644 --- a/packages/keyring-eth-ledger-bridge/jest.config.js +++ b/packages/keyring-eth-ledger-bridge/jest.config.js @@ -23,10 +23,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 91.89, + branches: 92.77, functions: 97.87, - lines: 97.16, - statements: 97.19, + lines: 97.28, + statements: 97.31, }, }, }); diff --git a/packages/keyring-eth-ledger-bridge/src/ledger-keyring.test.ts b/packages/keyring-eth-ledger-bridge/src/ledger-keyring.test.ts index 694ffb536..29f660dfd 100644 --- a/packages/keyring-eth-ledger-bridge/src/ledger-keyring.test.ts +++ b/packages/keyring-eth-ledger-bridge/src/ledger-keyring.test.ts @@ -2,6 +2,7 @@ import { Common, Chain, Hardfork } from '@ethereumjs/common'; import { RLP } from '@ethereumjs/rlp'; import { TransactionFactory } from '@ethereumjs/tx'; import * as ethUtil from '@ethereumjs/util'; +import { TransportStatusError } from '@ledgerhq/hw-transport'; import * as sigUtil from '@metamask/eth-sig-util'; import { bytesToHex, Hex, remove0x } from '@metamask/utils'; import EthereumTx from 'ethereumjs-tx'; @@ -784,6 +785,60 @@ describe('LedgerKeyring', function () { keyring.signTransaction(fakeAccounts[0], fakeTx), ).rejects.toThrow('Ledger: The transaction signature is not valid'); }); + + it('throws user rejection error when TransportStatusError with code 27013 is thrown', async function () { + await basicSetupToUnlockOneAccount(); + const transportError = { + statusCode: 27013, + message: 'Ledger device: (denied by the user?) (0x6985)', + name: 'TransportStatusError', + }; + + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignTransaction') + .mockRejectedValue(transportError); + + await expect( + keyring.signTransaction(fakeAccounts[0], fakeTx), + ).rejects.toThrow('Ledger: User rejected the transaction'); + }); + + it('throws blind signing error when TransportStatusError with code 27264 is thrown', async function () { + await basicSetupToUnlockOneAccount(); + const transportError = { + statusCode: 27264, + message: 'Ledger device: Invalid data received (0x6a80)', + name: 'TransportStatusError', + }; + + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignTransaction') + .mockRejectedValue(transportError); + + await expect( + keyring.signTransaction(fakeAccounts[0], fakeTx), + ).rejects.toThrow('Ledger: Blind signing must be enabled'); + }); + + it('re-throws TransportStatusError with unknown status code', async function () { + await basicSetupToUnlockOneAccount(); + const transportError = { + statusCode: 12345, + message: 'Some other transport error', + name: 'TransportStatusError', + }; + + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignTransaction') + .mockRejectedValue(transportError); + + await expect( + keyring.signTransaction(fakeAccounts[0], fakeTx), + ).rejects.toThrow(transportError); + }); }); describe('signPersonalMessage', function () { @@ -858,6 +913,40 @@ describe('LedgerKeyring', function () { 'Ledger: The signature doesnt match the right address', ); }); + + it('throws user rejection error when TransportStatusError with code 27013 is thrown in signPersonalMessage', async function () { + await basicSetupToUnlockOneAccount(); + const transportError = { + statusCode: 27013, + message: 'Ledger device: (denied by the user?) (0x6985)', + name: 'TransportStatusError', + }; + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignMessage') + .mockRejectedValue(transportError); + + await expect( + keyring.signPersonalMessage(fakeAccounts[0], 'some message'), + ).rejects.toThrow('Ledger: User rejected the transaction'); + }); + + it('re-throws TransportStatusError with unknown status code in signPersonalMessage', async function () { + await basicSetupToUnlockOneAccount(); + const transportError = { + statusCode: 12345, + message: 'Some other transport error', + name: 'TransportStatusError', + }; + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignMessage') + .mockRejectedValue(transportError); + + await expect( + keyring.signPersonalMessage(fakeAccounts[0], 'some message'), + ).rejects.toThrow(transportError); + }); }); describe('signMessage', function () { @@ -1207,6 +1296,60 @@ describe('LedgerKeyring', function () { '0x72d4e38a0e582e09a620fd38e236fe687a1ec782206b56d576f579c026a7e5b946759735981cd0c3efb02d36df28bb2feedfec3d90e408efc93f45b894946e3200', ); }); + + it('throws user rejection error when TransportStatusError with code 27013 is thrown in signTypedData', async function () { + const transportError = { + statusCode: 27013, + message: 'Ledger device: (denied by the user?) (0x6985)', + name: 'TransportStatusError', + }; + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignTypedData') + .mockRejectedValue(transportError); + + await expect( + keyring.signTypedData(fakeAccounts[15], fixtureData, { + version: sigUtil.SignTypedDataVersion.V4, + }), + ).rejects.toThrow('Ledger: User rejected the transaction'); + }); + + it('throws blind signing error when TransportStatusError with code 27264 is thrown in signTypedData', async function () { + const transportError = { + statusCode: 27264, + message: 'Ledger device: Invalid data received (0x6a80)', + name: 'TransportStatusError', + }; + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignTypedData') + .mockRejectedValue(transportError); + + await expect( + keyring.signTypedData(fakeAccounts[15], fixtureData, { + version: sigUtil.SignTypedDataVersion.V4, + }), + ).rejects.toThrow('Ledger: Blind signing must be enabled'); + }); + + it('re-throws TransportStatusError with unknown status code in signTypedData', async function () { + const transportError = { + statusCode: 12345, + message: 'Some other transport error', + name: 'TransportStatusError', + }; + Object.setPrototypeOf(transportError, TransportStatusError.prototype); + jest + .spyOn(keyring.bridge, 'deviceSignTypedData') + .mockRejectedValue(transportError); + + await expect( + keyring.signTypedData(fakeAccounts[15], fixtureData, { + version: sigUtil.SignTypedDataVersion.V4, + }), + ).rejects.toThrow(transportError); + }); }); describe('destroy', function () { diff --git a/packages/keyring-eth-ledger-bridge/src/ledger-keyring.ts b/packages/keyring-eth-ledger-bridge/src/ledger-keyring.ts index 89fb52f02..bd51a0e9b 100644 --- a/packages/keyring-eth-ledger-bridge/src/ledger-keyring.ts +++ b/packages/keyring-eth-ledger-bridge/src/ledger-keyring.ts @@ -5,6 +5,7 @@ import { type TypedTransaction, } from '@ethereumjs/tx'; import { publicToAddress } from '@ethereumjs/util'; +import { TransportStatusError } from '@ledgerhq/hw-transport'; import type { MessageTypes, TypedMessage } from '@metamask/eth-sig-util'; import { recoverPersonalSignature, @@ -413,7 +414,26 @@ export class LedgerKeyring implements Keyring { tx: remove0x(rawTxHex), hdPath, }); - } catch (error) { + } catch (error: unknown) { + // check whether error is TransportError + if (error instanceof TransportStatusError) { + // cast error to TransportError + const transportError: TransportStatusError = error; + if ( + transportError.statusCode === 27013 && + transportError.message.includes('(denied by the user?) (0x6985)') + ) { + throw new Error('Ledger: User rejected the transaction'); + } + + if ( + transportError.statusCode === 27264 && + transportError.message.includes('Invalid data received (0x6a80)') + ) { + throw new Error('Ledger: Blind signing must be enabled'); + } + } + throw error instanceof Error ? error : new Error('Ledger: Unknown error while signing transaction'); @@ -448,7 +468,20 @@ export class LedgerKeyring implements Keyring { hdPath, message: remove0x(message), }); - } catch (error) { + } catch (error: unknown) { + if (error instanceof TransportStatusError) { + const transportError: TransportStatusError = error; + /** + * for user rejected the transaction error + */ + if ( + transportError.statusCode === 27013 && + transportError.message.includes('(denied by the user?) (0x6985)') + ) { + throw new Error('Ledger: User rejected the transaction'); + } + } + throw error instanceof Error ? error : new Error('Ledger: Unknown error while signing message'); @@ -540,7 +573,24 @@ export class LedgerKeyring implements Keyring { message, }, }); - } catch (error) { + } catch (error: unknown) { + if (error instanceof TransportStatusError) { + // cast error to TransportError + const transportError: TransportStatusError = error; + if ( + transportError.statusCode === 27013 && + transportError.message.includes('(denied by the user?) (0x6985)') + ) { + throw new Error('Ledger: User rejected the transaction'); + } + + if ( + transportError.statusCode === 27264 && + transportError.message.includes('Invalid data received (0x6a80)') + ) { + throw new Error('Ledger: Blind signing must be enabled'); + } + } throw error instanceof Error ? error : new Error('Ledger: Unknown error while signing message');