From 6fae189878ff298cbd24390f8f5cfd5f30bd5331 Mon Sep 17 00:00:00 2001 From: Olivier Riccini Date: Thu, 16 Apr 2026 16:02:25 +0200 Subject: [PATCH 1/6] fix: properly surface Treezor user rejection errors --- packages/keyring-controller/package.json | 1 + .../src/KeyringController.test.ts | 117 +++++++++++++----- .../src/KeyringController.ts | 79 +++++++++++- .../src/TransactionController.test.ts | 49 ++++++++ .../src/TransactionController.ts | 66 +++++++++- 5 files changed, 277 insertions(+), 35 deletions(-) diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 5d46d10393..e7daaa0d30 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -62,6 +62,7 @@ "@metamask/keyring-api": "^21.6.0", "@metamask/keyring-internal-api": "^10.0.0", "@metamask/messenger": "^1.1.1", + "@metamask/rpc-errors": "^7.0.2", "@metamask/utils": "^11.9.0", "async-mutex": "^0.5.0", "ethereumjs-wallet": "^1.0.1", diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index e52a9fcacd..0f50d36b0b 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -21,6 +21,7 @@ import type { MessengerEvents, MockAnyNamespace, } from '@metamask/messenger'; +import { errorCodes } from '@metamask/rpc-errors'; import { wordlist } from '@metamask/scure-bip39/dist/wordlists/english'; import { bytesToHex, isValidHexAddress } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; @@ -32,10 +33,7 @@ import MockEncryptor, { SALT, } from '../tests/mocks/mockEncryptor'; import { MockErc4337Keyring } from '../tests/mocks/mockErc4337Keyring'; -import { - HardwareWalletError, - MockHardwareKeyring, -} from '../tests/mocks/mockHardwareKeyring'; +import { MockHardwareKeyring } from '../tests/mocks/mockHardwareKeyring'; import { MockKeyring } from '../tests/mocks/mockKeyring'; import MockShallowKeyring from '../tests/mocks/mockShallowKeyring'; import { buildMockTransaction } from '../tests/mocks/mockTransaction'; @@ -1797,6 +1795,29 @@ describe('KeyringController', () => { ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); }); }); + + it('normalizes cancellation-like signMessage errors to 4001', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + jest + .spyOn(keyring, 'signMessage') + .mockRejectedValue(new Error('Action canceled by user')); + + await expect( + controller.signMessage({ + data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + from: account, + }), + ).rejects.toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + }); + }); + }); }); describe('when the keyring for the given address does not support signMessage', () => { @@ -2224,6 +2245,38 @@ describe('KeyringController', () => { ).rejects.toThrow(/^Keyring Controller signTypedMessage:/u); }); }); + + it('normalizes cancellation-like signTypedMessage errors to 4001', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + jest + .spyOn(keyring, 'signTypedData') + .mockRejectedValue(new Error('failure_actioncancelled')); + + await expect( + controller.signTypedMessage( + { + data: [ + { + name: 'Message', + type: 'string', + value: 'Hi, Alice!', + }, + ], + from: account, + }, + SignTypedDataVersion.V1, + ), + ).rejects.toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + }); + }); + }); }); describe('when the keyring for the given address does not support signTypedMessage', () => { @@ -2351,6 +2404,26 @@ describe('KeyringController', () => { }).rejects.toThrow('tx.sign is not a function'); }); }); + + it('normalizes cancellation-like signTransaction errors to 4001', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + jest + .spyOn(keyring, 'signTransaction') + .mockRejectedValue(new Error('Action cancelled by user')); + + await expect( + controller.signTransaction(buildMockTransaction(), account), + ).rejects.toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + }); + }); + }); }); describe('when the keyring for the given address does not support signTransaction', () => { @@ -5344,7 +5417,7 @@ describe('KeyringController', () => { describe('error handling', () => { describe('when hardware wallet throws custom error', () => { - it('should preserve hardware wallet error in originalError property', async () => { + it('normalizes hardware user-rejection errors to 4001', async () => { const mockHardwareKeyringBuilder = keyringBuilderFactory( MockHardwareKeyring as unknown as KeyringClass, ); @@ -5384,7 +5457,11 @@ describe('KeyringController', () => { { data: JSON.stringify(typedData), from: hardwareAddress }, SignTypedDataVersion.V4, ), - ).rejects.toThrow(KeyringControllerError); + ).rejects.toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + }); // Verify the error details by catching it explicitly let caughtError: unknown; @@ -5397,29 +5474,11 @@ describe('KeyringController', () => { caughtError = error; } - // Verify the error is a KeyringControllerError (wrapped by signTypedMessage) - expect(caughtError).toBeInstanceOf(KeyringControllerError); - - const keyringError = caughtError as KeyringControllerError; - - // Verify the error message contains information about the hardware wallet error - expect(keyringError.message).toContain( - 'Keyring Controller signTypedMessage', - ); - expect(keyringError.message).toContain('HardwareWalletError'); - expect(keyringError.message).toContain( - 'User rejected the request on hardware device', - ); - - // Verify the original hardware wallet error is preserved in originalError - expect(keyringError.cause).toBeInstanceOf(HardwareWalletError); - expect(keyringError.cause?.message).toBe( - 'User rejected the request on hardware device', - ); - expect(keyringError.cause?.name).toBe('HardwareWalletError'); - expect((keyringError.cause as HardwareWalletError).code).toBe( - 'USER_REJECTED', - ); + expect(caughtError).toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + }); }, ); }); diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index d3d7def12b..5a7ef854b1 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -17,6 +17,7 @@ import type { import type { EthKeyring } from '@metamask/keyring-internal-api'; import type { Keyring, KeyringClass } from '@metamask/keyring-utils'; import type { Messenger } from '@metamask/messenger'; +import { providerErrors } from '@metamask/rpc-errors'; import type { Eip1024EncryptedData, Hex, Json } from '@metamask/utils'; import { add0x, @@ -1367,7 +1368,11 @@ export class KeyringController< ); } - return await keyring.signMessage(address, messageParams.data); + try { + return await keyring.signMessage(address, messageParams.data); + } catch (error) { + throw this.#normalizeSigningRejectionError(error); + } } /** @@ -1428,7 +1433,11 @@ export class KeyringController< const normalizedData = normalize(messageParams.data) as Hex; - return await keyring.signPersonalMessage(address, normalizedData); + try { + return await keyring.signPersonalMessage(address, normalizedData); + } catch (error) { + throw this.#normalizeSigningRejectionError(error); + } } /** @@ -1477,6 +1486,11 @@ export class KeyringController< { version }, ); } catch (error) { + const normalizedError = this.#normalizeSigningRejectionError(error); + if (normalizedError !== error) { + throw normalizedError; + } + const errorMessage = error instanceof Error ? `${error.name}: ${error.message}` @@ -1510,7 +1524,66 @@ export class KeyringController< ); } - return await keyring.signTransaction(address, transaction, opts); + try { + return await keyring.signTransaction(address, transaction, opts); + } catch (error) { + throw this.#normalizeSigningRejectionError(error); + } + } + + #normalizeSigningRejectionError(error: unknown): unknown { + if (!this.#isSigningUserRejectedError(error)) { + return error; + } + + const errorData = isObject(error) + ? (error.data as Json | undefined) + : undefined; + + return providerErrors.userRejectedRequest({ + message: 'MetaMask Tx Signature: User denied transaction signature.', + data: errorData, + }); + } + + #isSigningUserRejectedError( + error: unknown, + visited = new Set(), + ): boolean { + if (!error || visited.has(error)) { + return false; + } + visited.add(error); + + if (typeof error === 'string') { + return /(?:\buser rejected\b|\baction cancelled\b|\bcancelled\b|\bcanceled\b|failure_actioncancelled)/iu.test( + error, + ); + } + + if (typeof error !== 'object') { + return false; + } + + const errorObject = error as { + code?: unknown; + message?: unknown; + stack?: unknown; + cause?: unknown; + originalError?: unknown; + }; + + if (errorObject.code === 4001) { + return true; + } + + return ( + this.#isSigningUserRejectedError(errorObject.code, visited) || + this.#isSigningUserRejectedError(errorObject.message, visited) || + this.#isSigningUserRejectedError(errorObject.stack, visited) || + this.#isSigningUserRejectedError(errorObject.cause, visited) || + this.#isSigningUserRejectedError(errorObject.originalError, visited) + ); } /** diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index ea4c95a0de..95a55b38e3 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -3253,10 +3253,12 @@ describe('TransactionController', () => { * * @param controller - The controller instance. * @param expectedError - The expected error message. + * @param expectedErrorCode - The expected persisted error code. */ async function expectTransactionToFail( controller: TransactionController, expectedError: string, + expectedErrorCode?: number, ): Promise { const { result } = await controller.addTransaction( { @@ -3274,6 +3276,11 @@ describe('TransactionController', () => { expect(txParams.from).toBe(ACCOUNT_MOCK); expect(txParams.to).toBe(ACCOUNT_MOCK); expect(status).toBe(TransactionStatus.failed); + if (expectedErrorCode !== undefined) { + expect(controller.state.transactions[0].error).toMatchObject({ + code: expectedErrorCode, + }); + } } it('if signing error', async () => { @@ -3308,6 +3315,48 @@ describe('TransactionController', () => { await expectTransactionToFail(controller, 'No sign method defined'); }); + it('normalizes "cancelled" signing errors to userRejectedRequest', async () => { + const { controller } = setupController({ + options: { + sign: () => { + throw new Error('Action cancelled by user'); + }, + }, + messengerOptions: { + addTransactionApprovalRequest: { + state: 'approved', + }, + }, + }); + + await expectTransactionToFail( + controller, + 'MetaMask Tx Signature: User denied transaction signature.', + errorCodes.provider.userRejectedRequest, + ); + }); + + it('normalizes "canceled" signing errors to userRejectedRequest', async () => { + const { controller } = setupController({ + options: { + sign: () => { + throw new Error('Action canceled by user'); + }, + }, + messengerOptions: { + addTransactionApprovalRequest: { + state: 'approved', + }, + }, + }); + + await expectTransactionToFail( + controller, + 'MetaMask Tx Signature: User denied transaction signature.', + errorCodes.provider.userRejectedRequest, + ); + }); + it('if unexpected status', async () => { const { controller } = setupController({ messengerOptions: { diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index b78422231e..688371c601 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -4500,6 +4500,59 @@ export class TransactionController extends BaseController< ].includes(error.code as number); } + #hasUserRejectedMessage( + error: unknown, + visited = new Set(), + ): boolean { + if (!error || visited.has(error)) { + return false; + } + visited.add(error); + + if (typeof error === 'string') { + const normalizedError = error.toLowerCase(); + + if ( + normalizedError.includes('trezorkeyring') && + normalizedError.includes('unknown error') + ) { + return true; + } + + return /(?:\buser rejected\b|\baction cancelled\b|\bcancelled\b|\bcanceled\b|failure_actioncancelled)/iu.test( + error, + ); + } + + if (error instanceof Error) { + return ( + this.#hasUserRejectedMessage(error.message, visited) || + this.#hasUserRejectedMessage(error.stack, visited) || + this.#hasUserRejectedMessage( + error as Error & { cause?: unknown; originalError?: unknown }, + visited, + ) + ); + } + + if (typeof error === 'object') { + const objectError = error as { + message?: unknown; + stack?: unknown; + cause?: unknown; + originalError?: unknown; + }; + return ( + this.#hasUserRejectedMessage(objectError.message, visited) || + this.#hasUserRejectedMessage(objectError.stack, visited) || + this.#hasUserRejectedMessage(objectError.cause, visited) || + this.#hasUserRejectedMessage(objectError.originalError, visited) + ); + } + + return false; + } + #rejectTransactionAndThrow( transactionId: string, actionId: string | undefined, @@ -4522,6 +4575,13 @@ export class TransactionController extends BaseController< error: Error, actionId?: string, ): void { + const errorToPersist = this.#hasUserRejectedMessage(error) + ? providerErrors.userRejectedRequest({ + message: 'MetaMask Tx Signature: User denied transaction signature.', + data: (error as Error & { data?: Json })?.data, + }) + : error; + let newTransactionMeta: TransactionMeta; try { @@ -4537,7 +4597,7 @@ export class TransactionController extends BaseController< draftTransactionMeta as TransactionMeta & { status: TransactionStatus.failed; } - ).error = normalizeTxError(error); + ).error = normalizeTxError(errorToPersist); }, ); } catch (caughtError: unknown) { @@ -4546,13 +4606,13 @@ export class TransactionController extends BaseController< newTransactionMeta = { ...transactionMeta, status: TransactionStatus.failed, - error: normalizeTxError(error), + error: normalizeTxError(errorToPersist), }; } this.messenger.publish(`${controllerName}:transactionFailed`, { actionId, - error: error.message, + error: errorToPersist.message, transactionMeta: newTransactionMeta, }); From 60b62a7edb4931ec7499fc89087ab0795637fac3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 7 May 2026 08:42:05 +0000 Subject: [PATCH 2/6] chore: merge origin/main and resolve conflicts Agent-Logs-Url: https://github.com/MetaMask/core/sessions/64a5b953-7f1d-4e47-be62-0f18a2f761f9 Co-authored-by: dawnseeker8 <7315988+dawnseeker8@users.noreply.github.com> --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index 0a955bf5f9..6e684335aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4244,6 +4244,7 @@ __metadata: "@metamask/keyring-internal-api": "npm:^11.0.1" "@metamask/keyring-utils": "npm:^3.2.1" "@metamask/messenger": "npm:^1.2.0" + "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/utils": "npm:^11.9.0" "@ts-bridge/cli": "npm:^0.6.4" From 5876abece3fe534879abcc3062a5b09d5312ceee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Jun 2026 07:21:40 +0000 Subject: [PATCH 3/6] fix: update cancellation tests to use messenger mock instead of sign option --- .../src/TransactionController.test.ts | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 492af6ab97..aabcea59d1 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -3134,12 +3134,7 @@ describe('TransactionController', () => { }); it('normalizes "cancelled" signing errors to userRejectedRequest', async () => { - const { controller } = setupController({ - options: { - sign: () => { - throw new Error('Action cancelled by user'); - }, - }, + const { controller, rootMessenger } = setupController({ messengerOptions: { addTransactionApprovalRequest: { state: 'approved', @@ -3147,6 +3142,16 @@ describe('TransactionController', () => { }, }); + rootMessenger.unregisterActionHandler( + 'KeyringController:signTransaction', + ); + rootMessenger.registerActionHandler( + 'KeyringController:signTransaction', + () => { + throw new Error('Action cancelled by user'); + }, + ); + await expectTransactionToFail( controller, 'MetaMask Tx Signature: User denied transaction signature.', @@ -3155,12 +3160,7 @@ describe('TransactionController', () => { }); it('normalizes "canceled" signing errors to userRejectedRequest', async () => { - const { controller } = setupController({ - options: { - sign: () => { - throw new Error('Action canceled by user'); - }, - }, + const { controller, rootMessenger } = setupController({ messengerOptions: { addTransactionApprovalRequest: { state: 'approved', @@ -3168,6 +3168,16 @@ describe('TransactionController', () => { }, }); + rootMessenger.unregisterActionHandler( + 'KeyringController:signTransaction', + ); + rootMessenger.registerActionHandler( + 'KeyringController:signTransaction', + () => { + throw new Error('Action canceled by user'); + }, + ); + await expectTransactionToFail( controller, 'MetaMask Tx Signature: User denied transaction signature.', From 30aea8059aaae4a2550ecf89f619bbb68828f331 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Sun, 14 Jun 2026 16:25:17 +0800 Subject: [PATCH 4/6] test: add error normalization tests for signMessage and signPersonalMessage methods - Introduced tests to ensure that errors with code 4001 and string rejections are normalized to user rejection errors. - Added checks to verify that non-rejection errors are re-thrown unchanged, including those with circular references. - Included a test for normalizing cancellation-like errors in signPersonalMessage to a 4001 code. --- .../src/KeyringController.test.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index f2f15cdbba..5ab4bc8ec9 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -1953,6 +1953,96 @@ describe('KeyringController', () => { }); }); }); + + it('normalizes errors with a 4001 code to a user rejection error', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + jest + .spyOn(keyring, 'signMessage') + .mockRejectedValue({ code: 4001, data: { foo: 'bar' } }); + + await expect( + controller.signMessage({ + data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + from: account, + }), + ).rejects.toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + data: { foo: 'bar' }, + }); + }); + }); + + it('normalizes string rejection errors to a user rejection error', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + jest + .spyOn(keyring, 'signMessage') + .mockRejectedValue('User rejected the request'); + + await expect( + controller.signMessage({ + data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + from: account, + }), + ).rejects.toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + }); + }); + }); + + it('re-throws non-rejection errors unchanged, even with a non-string non-object code', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + const originalError = { code: 1234, message: 'Something else' }; + jest + .spyOn(keyring, 'signMessage') + .mockRejectedValue(originalError); + + await expect( + controller.signMessage({ + data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + from: account, + }), + ).rejects.toBe(originalError); + }); + }); + + it('re-throws non-rejection errors with circular references unchanged', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + const originalError: { message: string; cause?: unknown } = { + message: 'Something else', + }; + originalError.cause = originalError; + jest + .spyOn(keyring, 'signMessage') + .mockRejectedValue(originalError); + + await expect( + controller.signMessage({ + data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + from: account, + }), + ).rejects.toBe(originalError); + }); + }); }); describe('when the keyring for the given address does not support signMessage', () => { @@ -2035,6 +2125,29 @@ describe('KeyringController', () => { ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); }); }); + + it('normalizes cancellation-like signPersonalMessage errors to 4001', async () => { + await withController(async ({ controller, initialState }) => { + const account = initialState.keyrings[0].accounts[0]; + const keyring = (await controller.getKeyringForAccount( + account, + )) as EthKeyring; + jest + .spyOn(keyring, 'signPersonalMessage') + .mockRejectedValue(new Error('Action cancelled by user')); + + await expect( + controller.signPersonalMessage({ + data: '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0', + from: account, + }), + ).rejects.toMatchObject({ + code: errorCodes.provider.userRejectedRequest, + message: + 'MetaMask Tx Signature: User denied transaction signature.', + }); + }); + }); }); describe('when the keyring for the given address does not support signPersonalMessage', () => { From c3a633677b173fc81fbfb598f23c6b312fed1920 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Sun, 14 Jun 2026 16:30:33 +0800 Subject: [PATCH 5/6] refactor: streamline jest spy usage in KeyringController tests - Consolidated multiple lines of jest.spyOn calls into single lines for improved readability. - Maintained existing test functionality while enhancing code clarity. --- packages/keyring-controller/src/KeyringController.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 5ab4bc8ec9..12d0d14604 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -2008,9 +2008,7 @@ describe('KeyringController', () => { account, )) as EthKeyring; const originalError = { code: 1234, message: 'Something else' }; - jest - .spyOn(keyring, 'signMessage') - .mockRejectedValue(originalError); + jest.spyOn(keyring, 'signMessage').mockRejectedValue(originalError); await expect( controller.signMessage({ @@ -2031,9 +2029,7 @@ describe('KeyringController', () => { message: 'Something else', }; originalError.cause = originalError; - jest - .spyOn(keyring, 'signMessage') - .mockRejectedValue(originalError); + jest.spyOn(keyring, 'signMessage').mockRejectedValue(originalError); await expect( controller.signMessage({ From b85d5378a64e462ce0f0451688b575cb448248e2 Mon Sep 17 00:00:00 2001 From: Xiaoming Wang Date: Mon, 15 Jun 2026 22:18:34 +0800 Subject: [PATCH 6/6] docs: add changelog entries for Treezor user rejection fix Document the user-rejection error normalization changes in keyring-controller and transaction-controller, satisfying the changelog check for PR #8490. Co-authored-by: Cursor --- packages/keyring-controller/CHANGELOG.md | 3 +++ packages/transaction-controller/CHANGELOG.md | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 0a33483d10..0cb9020dca 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -13,10 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Add `@metamask/rpc-errors` as a dependency ([#8490](https://github.com/MetaMask/core/pull/8490)) - Bump `@metamask/utils` from `^11.9.0` to `^11.11.0` ([#9074](https://github.com/MetaMask/core/pull/9074)) ### Fixed +- Normalize user-rejection errors thrown while signing into a standard `userRejectedRequest` provider error ([#8490](https://github.com/MetaMask/core/pull/8490)) + - `signMessage`, `signPersonalMessage`, `signTypedMessage`, and `signTransaction` now surface a consistent `userRejectedRequest` error when the underlying keyring (e.g. a Trezor hardware wallet) reports that the user rejected the request. - Remove use of `instanceof` for `isKeyringNotFoundError` ([#9095](https://github.com/MetaMask/core/pull/9095)) - Using `instanceof` causes a lot of issue if we have 2 major `@metamask/keyring-controller` major versions in the dependency tree, `class KeyringControllerError` could be different classes and this, making the check to fail. diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index e08cc10730..10ee787958 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Normalize uninformative hardware wallet rejection errors into a standard `userRejectedRequest` provider error when a transaction fails ([#8490](https://github.com/MetaMask/core/pull/8490)) + - User-rejection errors surfaced by hardware wallets (e.g. a Trezor reporting an unknown error) are now persisted on the transaction and published with `TransactionController:transactionFailed` as a recognizable user-rejection error. + ## [68.0.0] ### Changed