From ae8c396cc6e8cee1c8ac9d26460ba72d28b37cee Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 10 Apr 2026 16:17:52 +0800 Subject: [PATCH 1/8] fix: add dynamic nft and erc20 check --- packages/keyring-eth-hd/package.json | 2 +- .../keyring-eth-ledger-bridge/CHANGELOG.md | 10 +- .../keyring-eth-ledger-bridge/jest.config.js | 8 +- .../keyring-eth-ledger-bridge/package.json | 1 + .../src/constants.test.ts | 50 +++++++ .../src/constants.ts | 23 +++ .../keyring-eth-ledger-bridge/src/index.ts | 2 + .../src/ledger-mobile-bridge.test.ts | 66 ++++++++- .../src/ledger-mobile-bridge.ts | 10 +- .../src/utils.test.ts | 139 ++++++++++++++++++ .../keyring-eth-ledger-bridge/src/utils.ts | 24 +++ packages/keyring-eth-simple/package.json | 2 +- packages/keyring-sdk/package.json | 2 +- yarn.lock | 7 +- 14 files changed, 330 insertions(+), 16 deletions(-) create mode 100644 packages/keyring-eth-ledger-bridge/src/constants.test.ts create mode 100644 packages/keyring-eth-ledger-bridge/src/constants.ts create mode 100644 packages/keyring-eth-ledger-bridge/src/utils.test.ts create mode 100644 packages/keyring-eth-ledger-bridge/src/utils.ts diff --git a/packages/keyring-eth-hd/package.json b/packages/keyring-eth-hd/package.json index a4a24e506..d9be92868 100644 --- a/packages/keyring-eth-hd/package.json +++ b/packages/keyring-eth-hd/package.json @@ -53,7 +53,7 @@ "@metamask/scure-bip39": "^2.1.1", "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.10.0", - "ethereum-cryptography": "^2.1.2" + "ethereum-cryptography": "^2.2.1" }, "devDependencies": { "@lavamoat/allow-scripts": "^3.2.1", diff --git a/packages/keyring-eth-ledger-bridge/CHANGELOG.md b/packages/keyring-eth-ledger-bridge/CHANGELOG.md index cc334c5c1..5dd6a09f5 100644 --- a/packages/keyring-eth-ledger-bridge/CHANGELOG.md +++ b/packages/keyring-eth-ledger-bridge/CHANGELOG.md @@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `getTransactionSelector` to read the 4-byte calldata selector from serialized transaction hex (legacy and typed txs) ([#TODO](https://github.com/MetaMask/accounts/pull/TODO)) + - Ledger mobile bridge passes `nft: true` to `clearSignTransaction` when that selector is NFT-only (ERC-721 / ERC-1155). + - Add `ERC20_WRITE_SELECTORS` for the three EIP-20 write functions (`transfer`, `transferFrom`, `approve`). + - Add unit tests for selector constants, `getTransactionSelector`, and Ledger mobile `clearSignTransaction` clear-sign flags ([#TODO](https://github.com/MetaMask/accounts/pull/TODO)). + ### Changed -- Add new dependency `@metamask/keyring-sdk@1.1.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)) - - This package now contains the keyring v2 wrapper helpers (`EthKeyringWrapper`). +- This package now contains the keyring v2 wrapper helpers (`EthKeyringWrapper`). - Bump `@metamask/hw-wallet-sdk` from `^0.6.0` to `^0.7.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) - Bump `@metamask/keyring-api` from `^21.6.0` to `^22.0.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) - Bump `@metamask/account-api` from `^1.0.0` to `^1.0.1` ([#487](https://github.com/MetaMask/accounts/pull/487)) diff --git a/packages/keyring-eth-ledger-bridge/jest.config.js b/packages/keyring-eth-ledger-bridge/jest.config.js index 48b8bcb0b..a5fe6f2e3 100644 --- a/packages/keyring-eth-ledger-bridge/jest.config.js +++ b/packages/keyring-eth-ledger-bridge/jest.config.js @@ -26,10 +26,10 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.36, - functions: 98.29, - lines: 97.86, - statements: 97.88, + branches: 93.59, + functions: 98.3, + lines: 97.94, + statements: 97.95, }, }, }); diff --git a/packages/keyring-eth-ledger-bridge/package.json b/packages/keyring-eth-ledger-bridge/package.json index b3a761f6b..0bab60403 100644 --- a/packages/keyring-eth-ledger-bridge/package.json +++ b/packages/keyring-eth-ledger-bridge/package.json @@ -77,6 +77,7 @@ "@types/web": "^0.0.69", "deepmerge": "^4.2.2", "depcheck": "^1.4.7", + "ethereum-cryptography": "^2.2.1", "ethereumjs-tx": "^1.3.7", "jest": "^29.5.0", "jest-it-up": "^3.1.0", diff --git a/packages/keyring-eth-ledger-bridge/src/constants.test.ts b/packages/keyring-eth-ledger-bridge/src/constants.test.ts new file mode 100644 index 000000000..9dc51eaa4 --- /dev/null +++ b/packages/keyring-eth-ledger-bridge/src/constants.test.ts @@ -0,0 +1,50 @@ +import { keccak256 } from 'ethereum-cryptography/keccak'; + +import { ERC20_WRITE_SELECTORS, NFT_ONLY_SELECTORS } from './constants'; + +/** + * Computes the four-byte function selector for a canonical Solidity signature. + * + * @param signature - Canonical Solidity ABI function signature. + * @returns Lowercase `0x` + 8-hex-digit selector. + */ +function selectorFromSignature(signature: string): string { + const hash = keccak256(Buffer.from(signature, 'utf8')); + return `0x${Buffer.from(hash).subarray(0, 4).toString('hex')}`; +} + +describe('NFT_ONLY_SELECTORS', () => { + const signatures: readonly string[] = [ + 'setApprovalForAll(address,bool)', + 'safeTransferFrom(address,address,uint256)', + 'safeTransferFrom(address,address,uint256,bytes)', + 'safeTransferFrom(address,address,uint256,uint256,bytes)', + 'safeBatchTransferFrom(address,address,uint256[],uint256[],bytes)', + ]; + + it('contains exactly one entry per canonical NFT-related signature', () => { + expect(NFT_ONLY_SELECTORS.size).toBe(signatures.length); + for (const signature of signatures) { + expect(NFT_ONLY_SELECTORS.has(selectorFromSignature(signature))).toBe( + true, + ); + } + }); +}); + +describe('ERC20_WRITE_SELECTORS', () => { + const signatures: readonly string[] = [ + 'transfer(address,uint256)', + 'transferFrom(address,address,uint256)', + 'approve(address,uint256)', + ]; + + it('contains exactly the three EIP-20 write function selectors', () => { + expect(ERC20_WRITE_SELECTORS.size).toBe(signatures.length); + for (const signature of signatures) { + expect(ERC20_WRITE_SELECTORS.has(selectorFromSignature(signature))).toBe( + true, + ); + } + }); +}); diff --git a/packages/keyring-eth-ledger-bridge/src/constants.ts b/packages/keyring-eth-ledger-bridge/src/constants.ts new file mode 100644 index 000000000..fad1e6ec1 --- /dev/null +++ b/packages/keyring-eth-ledger-bridge/src/constants.ts @@ -0,0 +1,23 @@ +/** + * Selectors that are used only by NFT standards (ERC721/ERC1155), not by ERC20. + * When the tx uses one of these, we enable Ledger NFT clear signing. + * approve(0x095ea7b3) is shared by ERC20 and ERC721 so we do NOT include it here. + */ +export const NFT_ONLY_SELECTORS = new Set([ + '0xa22cb465', // setApprovalForAll (ERC721 + ERC1155) + '0x42842e0e', // safeTransferFrom (ERC721) + '0xb88d4fde', // safeTransferFrom(address,address,uint256,bytes) (ERC721) + '0xf242432a', // safeTransferFrom (ERC1155) + '0x2eb2c2d6', // safeBatchTransferFrom (ERC1155) +]); + +/** + * Four-byte selectors for the three state-changing functions defined in EIP-20. + * + * @see https://eips.ethereum.org/EIPS/eip-20 + */ +export const ERC20_WRITE_SELECTORS = new Set([ + '0xa9059cbb', // transfer(address,uint256) + '0x23b872dd', // transferFrom(address,address,uint256) + '0x095ea7b3', // approve(address,uint256) +]); diff --git a/packages/keyring-eth-ledger-bridge/src/index.ts b/packages/keyring-eth-ledger-bridge/src/index.ts index 46951e098..90b53a3a0 100644 --- a/packages/keyring-eth-ledger-bridge/src/index.ts +++ b/packages/keyring-eth-ledger-bridge/src/index.ts @@ -8,3 +8,5 @@ export type * from './type'; export * from './ledger-hw-app'; export * from './errors'; export * from './ledger-error-handler'; +export * from './constants'; +export * from './utils'; diff --git a/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.test.ts b/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.test.ts index bd1b45f30..dea82502c 100644 --- a/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.test.ts +++ b/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.test.ts @@ -1,5 +1,9 @@ +import { Common, Chain, Hardfork } from '@ethereumjs/common'; +import { TransactionFactory } from '@ethereumjs/tx'; +import { bytesToHex } from '@ethereumjs/util'; import Transport from '@ledgerhq/hw-transport'; import { EIP712Message } from '@ledgerhq/types-live'; +import { remove0x } from '@metamask/utils'; import { MetaMaskLedgerHwAppEth } from './ledger-hw-app'; import { LedgerMobileBridge } from './ledger-mobile-bridge'; @@ -162,9 +166,67 @@ describe('LedgerMobileBridge', function () { }); expect(transportMiddlewareGetEthAppSpy).toHaveBeenCalledTimes(1); expect(mockEthApp.clearSignTransaction).toHaveBeenCalledTimes(1); + expect(mockEthApp.clearSignTransaction).toHaveBeenCalledWith(hdPath, tx, { + externalPlugins: true, + erc20: false, + nft: false, + }); + }); + + it('sets erc20 when calldata uses an EIP-20 write selector', async function () { + const hdPath = "m/44'/60'/0'/0/0"; + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Berlin, + }); + const erc20Tx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: '0xa9059cbb0000000000000000000000000000000000000000000000000000000000000000', + }, + { common }, + ); + const tx = remove0x(bytesToHex(erc20Tx.serialize())); + await bridge.deviceSignTransaction({ + hdPath, + tx, + }); expect(mockEthApp.clearSignTransaction).toHaveBeenCalledWith(hdPath, tx, { externalPlugins: true, erc20: true, + nft: false, + }); + }); + + it('sets nft when calldata uses an NFT-only selector', async function () { + const hdPath = "m/44'/60'/0'/0/0"; + const common = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Berlin, + }); + const nftTx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: '0xa22cb46500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', + }, + { common }, + ); + const tx = remove0x(bytesToHex(nftTx.serialize())); + await bridge.deviceSignTransaction({ + hdPath, + tx, + }); + expect(mockEthApp.clearSignTransaction).toHaveBeenCalledWith(hdPath, tx, { + externalPlugins: true, + erc20: false, nft: true, }); }); @@ -182,8 +244,8 @@ describe('LedgerMobileBridge', function () { expect(mockEthApp.clearSignTransaction).toHaveBeenCalledTimes(1); expect(mockEthApp.clearSignTransaction).toHaveBeenCalledWith(hdPath, tx, { externalPlugins: true, - erc20: true, - nft: true, + erc20: false, + nft: false, }); }); }); diff --git a/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.ts b/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.ts index 3eb8ff13c..7a90a7ed2 100644 --- a/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.ts +++ b/packages/keyring-eth-ledger-bridge/src/ledger-mobile-bridge.ts @@ -1,5 +1,6 @@ import type Transport from '@ledgerhq/hw-transport'; +import { ERC20_WRITE_SELECTORS, NFT_ONLY_SELECTORS } from './constants'; import { AppConfigurationResponse, GetAppNameAndVersionResponse, @@ -16,6 +17,7 @@ import { import { MetaMaskLedgerHwAppEth } from './ledger-hw-app'; import { TransportMiddleware } from './ledger-transport-middleware'; import { LedgerMobileBridgeOptions } from './type'; +import { getTransactionSelector } from './utils'; // MobileBridge Type will always use LedgerBridge with LedgerMobileBridgeOptions export type MobileBridge = LedgerBridge & { @@ -110,10 +112,14 @@ export class LedgerMobileBridge implements MobileBridge { tx, hdPath, }: LedgerSignTransactionParams): Promise { + const selector = getTransactionSelector(tx); + const nft = Boolean(selector && NFT_ONLY_SELECTORS.has(selector)); + const erc20 = Boolean(selector && ERC20_WRITE_SELECTORS.has(selector)); + return this.#getEthApp().clearSignTransaction(hdPath, tx, { externalPlugins: true, - erc20: true, - nft: true, + erc20, + nft, }); } diff --git a/packages/keyring-eth-ledger-bridge/src/utils.test.ts b/packages/keyring-eth-ledger-bridge/src/utils.test.ts new file mode 100644 index 000000000..3dfe58479 --- /dev/null +++ b/packages/keyring-eth-ledger-bridge/src/utils.test.ts @@ -0,0 +1,139 @@ +import { Common, Chain, Hardfork } from '@ethereumjs/common'; +import { TransactionFactory } from '@ethereumjs/tx'; +import { bytesToHex } from '@ethereumjs/util'; +import { remove0x } from '@metamask/utils'; + +import { getTransactionSelector } from './utils'; + +const TRANSFER_SELECTOR = '0xa9059cbb'; +const TRANSFER_FROM_SELECTOR = '0x23b872dd'; +const APPROVE_SELECTOR = '0x095ea7b3'; + +describe('getTransactionSelector', () => { + const commonLegacy = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.Berlin, + }); + const common1559 = new Common({ + chain: Chain.Mainnet, + hardfork: Hardfork.London, + }); + + it('returns the first four bytes of calldata for a legacy serialized tx', () => { + const tx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: `${TRANSFER_SELECTOR}00`, + }, + { common: commonLegacy }, + ); + const serializedHex = bytesToHex(tx.serialize()); + expect(getTransactionSelector(serializedHex)).toBe(TRANSFER_SELECTOR); + }); + + it('returns the selector for an EIP-1559 serialized tx', () => { + const tx = TransactionFactory.fromTxData( + { + type: 2, + nonce: '0x00', + maxFeePerGas: '0x01', + maxPriorityFeePerGas: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: `${TRANSFER_SELECTOR}deadbeef`, + }, + { common: common1559 }, + ); + const serializedHex = bytesToHex(tx.serialize()); + expect(getTransactionSelector(serializedHex)).toBe(TRANSFER_SELECTOR); + }); + + it('accepts hex without a 0x prefix', () => { + const tx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: `${TRANSFER_SELECTOR}00`, + }, + { common: commonLegacy }, + ); + const serializedHex = remove0x(bytesToHex(tx.serialize())); + expect(getTransactionSelector(serializedHex)).toBe(TRANSFER_SELECTOR); + }); + + it('returns undefined when calldata is empty', () => { + const tx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: '0x', + }, + { common: commonLegacy }, + ); + expect(getTransactionSelector(bytesToHex(tx.serialize()))).toBeUndefined(); + }); + + it('returns undefined for invalid serialized hex', () => { + expect(getTransactionSelector('0xnothex')).toBeUndefined(); + }); + + it('returns transferFrom selector when calldata uses transferFrom', () => { + const tx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: `${TRANSFER_FROM_SELECTOR}00`, + }, + { common: commonLegacy }, + ); + expect(getTransactionSelector(bytesToHex(tx.serialize()))).toBe( + TRANSFER_FROM_SELECTOR, + ); + }); + + it('returns approve selector when calldata uses approve', () => { + const tx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: `${APPROVE_SELECTOR}00`, + }, + { common: commonLegacy }, + ); + expect(getTransactionSelector(bytesToHex(tx.serialize()))).toBe( + APPROVE_SELECTOR, + ); + }); + + it('returns undefined when calldata is shorter than four bytes', () => { + const tx = TransactionFactory.fromTxData( + { + nonce: '0x00', + gasPrice: '0x01', + gasLimit: '0x5208', + to: '0x0000000000000000000000000000000000000000', + value: '0x00', + data: '0xabcd', + }, + { common: commonLegacy }, + ); + expect(getTransactionSelector(bytesToHex(tx.serialize()))).toBeUndefined(); + }); +}); diff --git a/packages/keyring-eth-ledger-bridge/src/utils.ts b/packages/keyring-eth-ledger-bridge/src/utils.ts new file mode 100644 index 000000000..c158ead94 --- /dev/null +++ b/packages/keyring-eth-ledger-bridge/src/utils.ts @@ -0,0 +1,24 @@ +import { TransactionFactory } from '@ethereumjs/tx'; +import { bytesToHex, hexToBytes } from '@ethereumjs/util'; +import { add0x } from '@metamask/utils'; + +/** + * Returns the 4-byte selector from raw serialized transaction hex or undefined if not present. + * Supports legacy RLP and EIP-2718 typed transactions (via `@ethereumjs/tx`). + * + * @param rawTxHex - Raw serialized transaction hex (with or without `0x` prefix). + * @returns The selector (`0x` + 8 hex digits, lowercased) or undefined if parsing fails or no calldata. + */ +export function getTransactionSelector(rawTxHex: string): string | undefined { + try { + const prefixedHex = rawTxHex.startsWith('0x') ? rawTxHex : add0x(rawTxHex); + const tx = TransactionFactory.fromSerializedData(hexToBytes(prefixedHex)); + const dataHex = bytesToHex(tx.data); + if (dataHex.length >= 10) { + return dataHex.slice(0, 10).toLowerCase(); + } + } catch { + // ignore parse errors + } + return undefined; +} diff --git a/packages/keyring-eth-simple/package.json b/packages/keyring-eth-simple/package.json index 148d20d94..1cb117e9d 100644 --- a/packages/keyring-eth-simple/package.json +++ b/packages/keyring-eth-simple/package.json @@ -49,7 +49,7 @@ "@metamask/keyring-api": "^22.0.0", "@metamask/keyring-sdk": "^1.1.0", "@metamask/utils": "^11.10.0", - "ethereum-cryptography": "^2.1.2", + "ethereum-cryptography": "^2.2.1", "randombytes": "^2.1.0" }, "devDependencies": { diff --git a/packages/keyring-sdk/package.json b/packages/keyring-sdk/package.json index 79f1a9cd2..e8f7a6c0d 100644 --- a/packages/keyring-sdk/package.json +++ b/packages/keyring-sdk/package.json @@ -53,7 +53,7 @@ "@metamask/superstruct": "^3.1.0", "@metamask/utils": "^11.10.0", "async-mutex": "^0.5.0", - "ethereum-cryptography": "^2.1.2", + "ethereum-cryptography": "^2.2.1", "uuid": "^9.0.1" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index dfab5a700..ec5bdf9df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1700,7 +1700,7 @@ __metadata: "@ts-bridge/cli": "npm:^0.6.3" "@types/jest": "npm:^29.5.12" deepmerge: "npm:^4.2.2" - ethereum-cryptography: "npm:^2.1.2" + ethereum-cryptography: "npm:^2.2.1" jest: "npm:^29.5.0" old-hd-keyring: "npm:@metamask/eth-hd-keyring@^4.0.1" languageName: unknown @@ -1737,6 +1737,7 @@ __metadata: "@types/web": "npm:^0.0.69" deepmerge: "npm:^4.2.2" depcheck: "npm:^1.4.7" + ethereum-cryptography: "npm:^2.2.1" ethereumjs-tx: "npm:^1.3.7" hdkey: "npm:^2.1.0" jest: "npm:^29.5.0" @@ -1863,7 +1864,7 @@ __metadata: "@types/randombytes": "npm:^2.0.0" deepmerge: "npm:^4.2.2" depcheck: "npm:^1.4.7" - ethereum-cryptography: "npm:^2.1.2" + ethereum-cryptography: "npm:^2.2.1" ethereumjs-tx: "npm:^1.3.7" jest: "npm:^29.5.0" randombytes: "npm:^2.1.0" @@ -2136,7 +2137,7 @@ __metadata: async-mutex: "npm:^0.5.0" deepmerge: "npm:^4.2.2" depcheck: "npm:^1.4.7" - ethereum-cryptography: "npm:^2.1.2" + ethereum-cryptography: "npm:^2.2.1" jest: "npm:^29.5.0" jest-it-up: "npm:^3.1.0" rimraf: "npm:^5.0.7" From c26e419c5afdd6aa865de7004d9f8697ed8ee142 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 10 Apr 2026 16:22:54 +0800 Subject: [PATCH 2/8] chore: update pr number --- packages/keyring-eth-ledger-bridge/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/keyring-eth-ledger-bridge/CHANGELOG.md b/packages/keyring-eth-ledger-bridge/CHANGELOG.md index 5dd6a09f5..8ec0d9f06 100644 --- a/packages/keyring-eth-ledger-bridge/CHANGELOG.md +++ b/packages/keyring-eth-ledger-bridge/CHANGELOG.md @@ -9,10 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `getTransactionSelector` to read the 4-byte calldata selector from serialized transaction hex (legacy and typed txs) ([#TODO](https://github.com/MetaMask/accounts/pull/TODO)) +- Add `getTransactionSelector` to read the 4-byte calldata selector from serialized transaction hex (legacy and typed txs) ([#506](https://github.com/MetaMask/accounts/pull/506)) - Ledger mobile bridge passes `nft: true` to `clearSignTransaction` when that selector is NFT-only (ERC-721 / ERC-1155). - Add `ERC20_WRITE_SELECTORS` for the three EIP-20 write functions (`transfer`, `transferFrom`, `approve`). - - Add unit tests for selector constants, `getTransactionSelector`, and Ledger mobile `clearSignTransaction` clear-sign flags ([#TODO](https://github.com/MetaMask/accounts/pull/TODO)). + - Add unit tests for selector constants, `getTransactionSelector`, and Ledger mobile `clearSignTransaction` clear-sign flags ([#506](https://github.com/MetaMask/accounts/pull/506)). ### Changed From 1fa71bd0b1e2dcd79a6383e6c0b20e245d65d6ef Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 10 Apr 2026 16:28:00 +0800 Subject: [PATCH 3/8] fix: changelog --- packages/keyring-eth-ledger-bridge/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/keyring-eth-ledger-bridge/CHANGELOG.md b/packages/keyring-eth-ledger-bridge/CHANGELOG.md index 8ec0d9f06..7cbd130cb 100644 --- a/packages/keyring-eth-ledger-bridge/CHANGELOG.md +++ b/packages/keyring-eth-ledger-bridge/CHANGELOG.md @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- This package now contains the keyring v2 wrapper helpers (`EthKeyringWrapper`). +- Add new dependency `@metamask/keyring-sdk@1.1.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)) + - This package now contains the keyring v2 wrapper helpers (`EthKeyringWrapper`). - Bump `@metamask/hw-wallet-sdk` from `^0.6.0` to `^0.7.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) - Bump `@metamask/keyring-api` from `^21.6.0` to `^22.0.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) - Bump `@metamask/account-api` from `^1.0.0` to `^1.0.1` ([#487](https://github.com/MetaMask/accounts/pull/487)) From a7eb11d38b01f0a0ed7050d73b4485c48306e2ee Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 10 Apr 2026 18:44:50 +0800 Subject: [PATCH 4/8] Apply suggestions from code review Co-authored-by: Charly Chevalier --- packages/keyring-eth-ledger-bridge/CHANGELOG.md | 7 +++---- packages/keyring-eth-ledger-bridge/src/utils.ts | 7 ++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/keyring-eth-ledger-bridge/CHANGELOG.md b/packages/keyring-eth-ledger-bridge/CHANGELOG.md index 7cbd130cb..57d4bf9a8 100644 --- a/packages/keyring-eth-ledger-bridge/CHANGELOG.md +++ b/packages/keyring-eth-ledger-bridge/CHANGELOG.md @@ -9,10 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `getTransactionSelector` to read the 4-byte calldata selector from serialized transaction hex (legacy and typed txs) ([#506](https://github.com/MetaMask/accounts/pull/506)) - - Ledger mobile bridge passes `nft: true` to `clearSignTransaction` when that selector is NFT-only (ERC-721 / ERC-1155). - - Add `ERC20_WRITE_SELECTORS` for the three EIP-20 write functions (`transfer`, `transferFrom`, `approve`). - - Add unit tests for selector constants, `getTransactionSelector`, and Ledger mobile `clearSignTransaction` clear-sign flags ([#506](https://github.com/MetaMask/accounts/pull/506)). +- Add `getTransactionSelector` to read the 4-byte calldata selector from serialized transaction hex (legacy and typed txs) ([#506](https://github.com/MetaMask/accounts/pull/506)) + - Ledger mobile bridge passes `nft: true` to `clearSignTransaction` when that selector is NFT-only (ERC-721 / ERC-1155). +- Add `ERC20_WRITE_SELECTORS` for the three EIP-20 write functions (`transfer`, `transferFrom`, `approve`) ([#506](https://github.com/MetaMask/accounts/pull/506)) ### Changed diff --git a/packages/keyring-eth-ledger-bridge/src/utils.ts b/packages/keyring-eth-ledger-bridge/src/utils.ts index c158ead94..8a2d55fa7 100644 --- a/packages/keyring-eth-ledger-bridge/src/utils.ts +++ b/packages/keyring-eth-ledger-bridge/src/utils.ts @@ -11,11 +11,12 @@ import { add0x } from '@metamask/utils'; */ export function getTransactionSelector(rawTxHex: string): string | undefined { try { - const prefixedHex = rawTxHex.startsWith('0x') ? rawTxHex : add0x(rawTxHex); + const prefixedHex = add0x(rawTxHex); const tx = TransactionFactory.fromSerializedData(hexToBytes(prefixedHex)); const dataHex = bytesToHex(tx.data); - if (dataHex.length >= 10) { - return dataHex.slice(0, 10).toLowerCase(); + const selectorSize = 2 /* 0x */ + (4 * 2) /* 4 bytes (hex) */; + if (dataHex.length >= selectorSize) { + return dataHex.slice(0, selectorSize).toLowerCase(); } } catch { // ignore parse errors From f979cbab314a8f38f35f4c63e7909f63b2175c87 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 10 Apr 2026 10:36:53 +0200 Subject: [PATCH 5/8] feat: add `KeyringClientV2` support (#408) Similar implementation than `KeyringClient` but using the new unified keyring API (keyring v2) methods. --- > [!NOTE] > **Medium Risk** > Introduces a parallel v2 JSON-RPC surface (including `exportAccount`) across API, clients, and snap dispatch logic; mistakes could break RPC compatibility or unintentionally affect account export/request routing. > > **Overview** > **Adds keyring v2 RPC support end-to-end.** `@metamask/keyring-api` now exports `KeyringRpcV2Method` plus superstruct-validated v2 request/response types and `isKeyringRpcV2Method`, and refines `isKeyringRpcMethod` to be a type predicate. > > **Introduces v2 client and dispatcher.** `@metamask/keyring-snap-client` adds `KeyringClientV2` (UUID ids, strict response masking) and `@metamask/keyring-internal-snap-client` adds `KeyringInternalSnapClientV2` (Messenger-backed). `@metamask/keyring-snap-sdk` adds `handleKeyringRequestV2` to validate and route v2 JSON-RPC calls, including guarding optional `exportAccount`. Tests and package exports/changelogs are updated accordingly. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit cc83c1787ee92dbd29842ae666dff797c65bf66a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Mathieu Artu --- packages/keyring-api/CHANGELOG.md | 5 + packages/keyring-api/src/api/v2/index.ts | 1 + .../src/api/v2/keyring-rpc.test.ts | 14 + .../keyring-api/src/api/v2/keyring-rpc.ts | 194 +++++++++++++ packages/keyring-api/src/rpc.test.ts | 2 +- packages/keyring-api/src/rpc.ts | 2 +- .../keyring-internal-snap-client/CHANGELOG.md | 4 + .../src/KeyringInternalSnapClient.ts | 9 +- .../keyring-internal-snap-client/src/index.ts | 1 + .../v2/KeyringInternalSnapClientV2.test.ts | 82 ++++++ .../src/v2/KeyringInternalSnapClientV2.ts | 59 ++++ .../src/v2/index.ts | 1 + packages/keyring-snap-client/CHANGELOG.md | 4 + packages/keyring-snap-client/src/index.ts | 1 + .../src/v2/KeyringClientV2.test.ts | 193 +++++++++++++ .../src/v2/KeyringClientV2.ts | 128 +++++++++ packages/keyring-snap-client/src/v2/index.ts | 1 + packages/keyring-snap-sdk/CHANGELOG.md | 4 + packages/keyring-snap-sdk/src/index.ts | 1 + packages/keyring-snap-sdk/src/v2/index.ts | 1 + .../src/v2/rpc-handler.test.ts | 264 ++++++++++++++++++ .../keyring-snap-sdk/src/v2/rpc-handler.ts | 115 ++++++++ 22 files changed, 1080 insertions(+), 6 deletions(-) create mode 100644 packages/keyring-api/src/api/v2/keyring-rpc.test.ts create mode 100644 packages/keyring-api/src/api/v2/keyring-rpc.ts create mode 100644 packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts create mode 100644 packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts create mode 100644 packages/keyring-internal-snap-client/src/v2/index.ts create mode 100644 packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts create mode 100644 packages/keyring-snap-client/src/v2/KeyringClientV2.ts create mode 100644 packages/keyring-snap-client/src/v2/index.ts create mode 100644 packages/keyring-snap-sdk/src/v2/index.ts create mode 100644 packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts create mode 100644 packages/keyring-snap-sdk/src/v2/rpc-handler.ts diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 163b29a0a..2a055bd18 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add keyring v2 RPC types and structs (`KeyringRpcV2`, `KeyringRpcV2Method`, `isKeyringRpcV2Method`, and request/response structs) ([#408](https://github.com/MetaMask/accounts/pull/408)) + ### Changed +- Improve return type of `isKeyringRpcMethod` to use type predicate `method is KeyringRpcMethod` ([#408](https://github.com/MetaMask/accounts/pull/408)) - Bump `@metamask/utils` from `^11.1.0` to `^11.10.0` ([#489](https://github.com/MetaMask/accounts/pull/489)) ## [22.0.0] diff --git a/packages/keyring-api/src/api/v2/index.ts b/packages/keyring-api/src/api/v2/index.ts index f9ce241b6..30482b992 100644 --- a/packages/keyring-api/src/api/v2/index.ts +++ b/packages/keyring-api/src/api/v2/index.ts @@ -1,6 +1,7 @@ export type * from './keyring'; export * from './keyring-capabilities'; export * from './keyring-type'; +export * from './keyring-rpc'; export * from './create-account'; export * from './export-account'; export * from './private-key'; diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.test.ts b/packages/keyring-api/src/api/v2/keyring-rpc.test.ts new file mode 100644 index 000000000..bac57e900 --- /dev/null +++ b/packages/keyring-api/src/api/v2/keyring-rpc.test.ts @@ -0,0 +1,14 @@ +import { KeyringRpcV2Method, isKeyringRpcV2Method } from './keyring-rpc'; + +describe('isKeyringRpcV2Method', () => { + it.each(Object.values(KeyringRpcV2Method))( + 'returns true for: "%s"', + (method) => { + expect(isKeyringRpcV2Method(method)).toBe(true); + }, + ); + + it('returns false for unknown method', () => { + expect(isKeyringRpcV2Method('keyring_unknownMethod')).toBe(false); + }); +}); diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts new file mode 100644 index 000000000..c453192ec --- /dev/null +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -0,0 +1,194 @@ +import { object, exactOptional, UuidStruct } from '@metamask/keyring-utils'; +import type { Infer } from '@metamask/superstruct'; +import { array, literal, number, string, union } from '@metamask/superstruct'; +import { JsonStruct } from '@metamask/utils'; + +import { CreateAccountOptionsStruct } from './create-account'; +import { + ExportAccountOptionsStruct, + PrivateKeyExportedAccountStruct, +} from './export-account'; +import type { KeyringV2 } from './keyring'; +import { KeyringAccountStruct } from '../account'; +import { KeyringRequestStruct } from '../request'; + +/** + * Keyring interface for keyring methods that can be invoked through + * RPC calls. + */ +export type KeyringRpcV2 = { + getAccount: KeyringV2['getAccount']; + getAccounts: KeyringV2['getAccounts']; + createAccounts: KeyringV2['createAccounts']; + deleteAccount: KeyringV2['deleteAccount']; + submitRequest: KeyringV2['submitRequest']; + exportAccount?: KeyringV2['exportAccount']; +}; + +/** + * Keyring RPC methods used by the API. + */ +export const KeyringRpcV2Method = { + GetAccounts: 'keyring_getAccounts', + CreateAccounts: 'keyring_createAccounts', + // Inherited from v1 (but method signatures may differ...): + // NOTE: We use literals here to avoid circular dependencies. + GetAccount: 'keyring_getAccount', + DeleteAccount: 'keyring_deleteAccount', + ExportAccount: 'keyring_exportAccount', + SubmitRequest: 'keyring_submitRequest', +} as const; + +/** + * Keyring RPC methods used by the API. + */ +export type KeyringRpcV2Method = + (typeof KeyringRpcV2Method)[keyof typeof KeyringRpcV2Method]; + +/** + * Check if a method is a keyring RPC method (v2). + * + * @param method - Method to check. + * @returns Whether the method is a keyring RPC method (v2). + */ +export function isKeyringRpcV2Method( + method: string, +): method is KeyringRpcV2Method { + return Object.values(KeyringRpcV2Method).includes( + method as KeyringRpcV2Method, + ); +} + +// ---------------------------------------------------------------------------- + +const CommonHeader = { + jsonrpc: literal('2.0'), + id: union([string(), number(), literal(null)]), +}; + +// ---------------------------------------------------------------------------- +// Get accounts + +export const GetAccountsV2RequestStruct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.GetAccounts}`), +}); + +export type GetAccountsV2Request = Infer; + +export const GetAccountsV2ResponseStruct = array(KeyringAccountStruct); + +export type GetAccountsV2Response = Infer; + +// ---------------------------------------------------------------------------- +// Get account + +export const GetAccountV2RequestStruct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.GetAccount}`), + params: object({ + id: UuidStruct, + }), +}); + +export type GetAccountV2Request = Infer; + +export const GetAccountV2ResponseStruct = KeyringAccountStruct; + +export type GetAccountV2Response = Infer; + +// ---------------------------------------------------------------------------- +// Create accounts + +export const CreateAccountsV2RequestStruct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.CreateAccounts}`), + params: CreateAccountOptionsStruct, +}); + +export type CreateAccountsV2Request = Infer< + typeof CreateAccountsV2RequestStruct +>; + +export const CreateAccountsV2ResponseStruct = array(KeyringAccountStruct); + +export type CreateAccountsV2Response = Infer< + typeof CreateAccountsV2ResponseStruct +>; + +// ---------------------------------------------------------------------------- +// Delete account + +export const DeleteAccountV2RequestStruct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.DeleteAccount}`), + params: object({ + id: UuidStruct, + }), +}); + +export type DeleteAccountV2Request = Infer; + +export const DeleteAccountV2ResponseStruct = literal(null); + +export type DeleteAccountV2Response = Infer< + typeof DeleteAccountV2ResponseStruct +>; + +// ---------------------------------------------------------------------------- +// Export account + +export const ExportAccountV2RequestStruct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.ExportAccount}`), + params: object({ + id: UuidStruct, + options: exactOptional(ExportAccountOptionsStruct), + }), +}); + +export type ExportAccountV2Request = Infer; + +export const ExportAccountV2ResponseStruct = PrivateKeyExportedAccountStruct; + +export type ExportAccountV2Response = Infer< + typeof ExportAccountV2ResponseStruct +>; + +// ---------------------------------------------------------------------------- +// Submit request + +export const SubmitRequestV2RequestStruct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.SubmitRequest}`), + params: KeyringRequestStruct, +}); + +export type SubmitRequestV2Request = Infer; + +export const SubmitRequestV2ResponseStruct = JsonStruct; + +export type SubmitRequestV2Response = Infer< + typeof SubmitRequestV2ResponseStruct +>; + +// ---------------------------------------------------------------------------- + +/** + * Keyring RPC requests. + */ +export type KeyringRpcV2Requests = + | GetAccountsV2Request + | GetAccountV2Request + | CreateAccountsV2Request + | DeleteAccountV2Request + | ExportAccountV2Request + | SubmitRequestV2Request; + +/** + * Extract the proper request type for a given `KeyringRpcV2Method`. + */ +export type KeyringRpcV2Request = Extract< + KeyringRpcV2Requests, + { method: `${RpcMethod}` } +>; diff --git a/packages/keyring-api/src/rpc.test.ts b/packages/keyring-api/src/rpc.test.ts index e2f0afde7..ba007bb1b 100644 --- a/packages/keyring-api/src/rpc.test.ts +++ b/packages/keyring-api/src/rpc.test.ts @@ -2,7 +2,7 @@ import { KeyringRpcMethod, isKeyringRpcMethod } from './rpc'; describe('isKeyringRpcMethod', () => { it.each(Object.values(KeyringRpcMethod))( - 'returns true for: KeyringRpcMethod.$s', + 'returns true for: "%s"', (method) => { expect(isKeyringRpcMethod(method)).toBe(true); }, diff --git a/packages/keyring-api/src/rpc.ts b/packages/keyring-api/src/rpc.ts index 6f444c442..07d45da19 100644 --- a/packages/keyring-api/src/rpc.ts +++ b/packages/keyring-api/src/rpc.ts @@ -69,7 +69,7 @@ export enum KeyringRpcMethod { * @param method - Method to check. * @returns Whether the method is a keyring RPC method. */ -export function isKeyringRpcMethod(method: string): boolean { +export function isKeyringRpcMethod(method: string): method is KeyringRpcMethod { return Object.values(KeyringRpcMethod).includes(method as KeyringRpcMethod); } diff --git a/packages/keyring-internal-snap-client/CHANGELOG.md b/packages/keyring-internal-snap-client/CHANGELOG.md index 7dbbb7c59..02cf9d742 100644 --- a/packages/keyring-internal-snap-client/CHANGELOG.md +++ b/packages/keyring-internal-snap-client/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `KeyringInternalSnapClientV2` class for communicating with a Snap using the keyring v2 RPC protocol ([#408](https://github.com/MetaMask/accounts/pull/408)) + ### Changed - Bump `@metamask/messenger` from `^0.3.0` to `^1.1.1` ([#489](https://github.com/MetaMask/accounts/pull/489), [#500](https://github.com/MetaMask/accounts/pull/500)) diff --git a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts index 5aa48797d..a1d3e9238 100644 --- a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts +++ b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts @@ -4,11 +4,12 @@ import type { KeyringResponseWithoutOrigin, } from '@metamask/keyring-internal-api'; import { SubmitRequestResponseV1Struct } from '@metamask/keyring-internal-api'; -import { KeyringClient, type Sender } from '@metamask/keyring-snap-client'; -import { strictMask, type JsonRpcRequest } from '@metamask/keyring-utils'; +import type { Sender } from '@metamask/keyring-snap-client'; +import { KeyringClient } from '@metamask/keyring-snap-client'; +import { strictMask } from '@metamask/keyring-utils'; import type { Messenger } from '@metamask/messenger'; import type { SnapControllerHandleRequestAction } from '@metamask/snaps-controllers'; -import type { SnapId } from '@metamask/snaps-sdk'; +import type { JsonRpcRequest, SnapId } from '@metamask/snaps-sdk'; import type { HandlerType } from '@metamask/snaps-utils'; import type { Json } from '@metamask/utils'; @@ -28,7 +29,7 @@ export type KeyringInternalSnapClientMessenger = Messenger< * Implementation of the `Sender` interface that can be used to send requests * to a Snap through a `Messenger`. */ -class SnapControllerMessengerSender implements Sender { +export class SnapControllerMessengerSender implements Sender { readonly #snapId: SnapId; readonly #origin: string; diff --git a/packages/keyring-internal-snap-client/src/index.ts b/packages/keyring-internal-snap-client/src/index.ts index cbd731726..d30021c91 100644 --- a/packages/keyring-internal-snap-client/src/index.ts +++ b/packages/keyring-internal-snap-client/src/index.ts @@ -1 +1,2 @@ export * from './KeyringInternalSnapClient'; +export * from './v2'; diff --git a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts new file mode 100644 index 000000000..9dcda80a7 --- /dev/null +++ b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts @@ -0,0 +1,82 @@ +import { KeyringRpcV2Method, type KeyringAccount } from '@metamask/keyring-api'; +import type { SnapId } from '@metamask/snaps-sdk'; + +import { KeyringInternalSnapClientV2 } from './KeyringInternalSnapClientV2'; +import type { KeyringInternalSnapClientMessenger } from '../KeyringInternalSnapClient'; + +const MOCK_ACCOUNT: KeyringAccount = { + id: '13f94041-6ae6-451f-a0fe-afdd2fda18a7', + address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae', + options: {}, + methods: [], + scopes: ['eip155:0'], + type: 'eip155:eoa', +}; + +describe('KeyringInternalSnapClientV2', () => { + const snapId = 'local:localhost:3000' as SnapId; + + const accountsList: KeyringAccount[] = [MOCK_ACCOUNT]; + + const messenger = { + call: jest.fn(), + }; + + describe('getAccounts', () => { + const request = { + snapId, + origin: 'metamask', + handler: 'onKeyringRequest', + request: { + id: expect.any(String), + jsonrpc: '2.0', + method: KeyringRpcV2Method.GetAccounts, + }, + }; + + it('calls the getAccounts method and return the result', async () => { + const client = new KeyringInternalSnapClientV2({ + messenger: messenger as unknown as KeyringInternalSnapClientMessenger, + snapId, + }); + + messenger.call.mockResolvedValue(accountsList); + const accounts = await client.getAccounts(); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:handleRequest', + request, + ); + expect(accounts).toStrictEqual(accountsList); + }); + + it('calls the getAccounts method and return the result (withSnapId)', async () => { + const client = new KeyringInternalSnapClientV2({ + messenger: messenger as unknown as KeyringInternalSnapClientMessenger, + }); + + messenger.call.mockResolvedValue(accountsList); + const accounts = await client.withSnapId(snapId).getAccounts(); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:handleRequest', + request, + ); + expect(accounts).toStrictEqual(accountsList); + }); + + it('calls the default snapId value ("undefined")', async () => { + const client = new KeyringInternalSnapClientV2({ + messenger: messenger as unknown as KeyringInternalSnapClientMessenger, + }); + + messenger.call.mockResolvedValue(accountsList); + await client.getAccounts(); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:handleRequest', + { + ...request, + snapId: 'undefined', + }, + ); + }); + }); +}); diff --git a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts new file mode 100644 index 000000000..43f0f0294 --- /dev/null +++ b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts @@ -0,0 +1,59 @@ +import { KeyringClientV2 } from '@metamask/keyring-snap-client'; +import type { SnapId } from '@metamask/snaps-sdk'; +import type { HandlerType } from '@metamask/snaps-utils'; + +import type { KeyringInternalSnapClientMessenger } from '../KeyringInternalSnapClient'; +import { SnapControllerMessengerSender } from '../KeyringInternalSnapClient'; + +/** + * A `KeyringClient` that allows the communication with a Snap through a + * `Messenger`. + */ +export class KeyringInternalSnapClientV2 extends KeyringClientV2 { + readonly #messenger: KeyringInternalSnapClientMessenger; + + /** + * Create a new instance of `KeyringInternalSnapClientV2`. + * + * The `handlerType` argument has a hard-coded default `string` value instead + * of a `HandlerType` value to prevent the `@metamask/snaps-utils` module + * from being required at runtime. + * + * @param args - Constructor arguments. + * @param args.messenger - The `KeyringInternalSnapClientMessenger` instance to use. + * @param args.snapId - The ID of the Snap to use (default: `'undefined'`). + * @param args.origin - The sender's origin (default: `'metamask'`). + * @param args.handler - The handler type (default: `'onKeyringRequest'`). + */ + constructor({ + messenger, + snapId = 'undefined' as SnapId, + origin = 'metamask', + handler = 'onKeyringRequest' as HandlerType, + }: { + messenger: KeyringInternalSnapClientMessenger; + snapId?: SnapId; + origin?: string; + handler?: HandlerType; + }) { + super( + new SnapControllerMessengerSender(messenger, snapId, origin, handler), + ); + this.#messenger = messenger; + } + + /** + * Create a new instance of `KeyringInternalSnapClientV2` with the specified + * `snapId`. + * + * @param snapId - The ID of the Snap to use in the new instance. + * @returns A new instance of `KeyringInternalSnapClientV2` with the + * specified Snap ID. + */ + withSnapId(snapId: SnapId): KeyringInternalSnapClientV2 { + return new KeyringInternalSnapClientV2({ + messenger: this.#messenger, + snapId, + }); + } +} diff --git a/packages/keyring-internal-snap-client/src/v2/index.ts b/packages/keyring-internal-snap-client/src/v2/index.ts new file mode 100644 index 000000000..15970e561 --- /dev/null +++ b/packages/keyring-internal-snap-client/src/v2/index.ts @@ -0,0 +1 @@ +export * from './KeyringInternalSnapClientV2'; diff --git a/packages/keyring-snap-client/CHANGELOG.md b/packages/keyring-snap-client/CHANGELOG.md index 6aa4a9022..bceba0dfd 100644 --- a/packages/keyring-snap-client/CHANGELOG.md +++ b/packages/keyring-snap-client/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `KeyringClientV2` class implementing the keyring v2 RPC client ([#408](https://github.com/MetaMask/accounts/pull/408)) + ## [8.2.1] ### Changed diff --git a/packages/keyring-snap-client/src/index.ts b/packages/keyring-snap-client/src/index.ts index 3e845b705..289955e51 100644 --- a/packages/keyring-snap-client/src/index.ts +++ b/packages/keyring-snap-client/src/index.ts @@ -1,3 +1,4 @@ export * from './KeyringClient'; export * from './KeyringSnapRpcClient'; export * from './KeyringPublicClient'; +export * from './v2'; diff --git a/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts new file mode 100644 index 000000000..50b38f191 --- /dev/null +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts @@ -0,0 +1,193 @@ +import { + KeyringRpcV2Method, + PrivateKeyEncoding, + type KeyringAccount, + type KeyringRequest, +} from '@metamask/keyring-api'; +import type { Json } from '@metamask/utils'; + +import { KeyringClientV2 } from './KeyringClientV2'; + +describe('KeyringClientV2', () => { + const mockSender = { + send: jest.fn(), + }; + + beforeEach(() => { + mockSender.send.mockClear(); + }); + + describe('KeyringClientV2', () => { + const client = new KeyringClientV2(mockSender); + + describe('getAccounts', () => { + it('sends a request to get accounts and return the response', async () => { + const expectedResponse: KeyringAccount[] = [ + { + id: '49116980-0712-4fa5-b045-e4294f1d440e', + address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae', + options: {}, + methods: [], + scopes: ['eip155:0'], + type: 'eip155:eoa', + }, + ]; + + mockSender.send.mockResolvedValue(expectedResponse); + const accounts = await client.getAccounts(); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.GetAccounts}`, + }); + expect(accounts).toStrictEqual(expectedResponse); + }); + }); + + describe('getAccount', () => { + it('sends a request to get an account by ID and return the response', async () => { + const id = '49116980-0712-4fa5-b045-e4294f1d440e'; + const expectedResponse: KeyringAccount = { + id: '49116980-0712-4fa5-b045-e4294f1d440e', + address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae', + options: {}, + methods: [], + scopes: ['eip155:0'], + type: 'eip155:eoa', + }; + + mockSender.send.mockResolvedValue(expectedResponse); + const account = await client.getAccount(id); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.GetAccount}`, + params: { id }, + }); + expect(account).toStrictEqual(expectedResponse); + }); + }); + + describe('createAccounts', () => { + it('sends a request to create an account and return the response', async () => { + const expectedResponse: KeyringAccount[] = [ + { + id: '49116980-0712-4fa5-b045-e4294f1d440e', + address: '0xE9A74AACd7df8112911ca93260fC5a046f8a64Ae', + options: {}, + methods: [], + scopes: ['eip155:0'], + type: 'eip155:eoa', + }, + ]; + + const createAccountOptions = { + type: 'bip44:derive-index', + entropySource: 'mock-entropy-source', + groupIndex: 0, + } as const; + + mockSender.send.mockResolvedValue(expectedResponse); + const account = await client.createAccounts(createAccountOptions); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.CreateAccounts}`, + params: createAccountOptions, + }); + expect(account).toStrictEqual(expectedResponse); + }); + }); + + describe('deleteAccount', () => { + it('sends a request to delete an account', async () => { + const id = '49116980-0712-4fa5-b045-e4294f1d440e'; + + mockSender.send.mockResolvedValue(null); + const response = await client.deleteAccount(id); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.DeleteAccount}`, + params: { id }, + }); + expect(response).toBeUndefined(); + }); + }); + + describe('exportAccount', () => { + it('sends a request to export an account', async () => { + const id = '49116980-0712-4fa5-b045-e4294f1d440e'; + const expectedResponse = { + type: 'private-key', + privateKey: '0x000000000', + encoding: 'hexadecimal', + }; + + mockSender.send.mockResolvedValue(expectedResponse); + const response = await client.exportAccount(id); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.ExportAccount}`, + params: { id }, + }); + expect(response).toStrictEqual(expectedResponse); + }); + + it('sends a request to export an account with options', async () => { + const id = '49116980-0712-4fa5-b045-e4294f1d440e'; + const expectedResponse = { + type: 'private-key', + privateKey: '0x000000000', + encoding: 'hexadecimal', + }; + const options = { + type: 'private-key' as const, + encoding: PrivateKeyEncoding.Hexadecimal, + }; + + mockSender.send.mockResolvedValue(expectedResponse); + const response = await client.exportAccount(id, options); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.ExportAccount}`, + params: { + id, + options, + }, + }); + expect(response).toStrictEqual(expectedResponse); + }); + }); + + describe('submitRequest', () => { + it('sends a request to submit a request', async () => { + const request: KeyringRequest = { + id: '71621d8d-62a4-4bf4-97cc-fb8f243679b0', + scope: 'eip155:1', + origin: 'test', + account: '46b5ccd3-4786-427c-89d2-cef626dffe9b', + request: { + method: 'personal_sign', + params: ['0xe9a74aacd7df8112911ca93260fc5a046f8a64ae', '0x0'], + }, + }; + const expectedResponse: Json = { + result: 'success', + }; + + mockSender.send.mockResolvedValue(expectedResponse); + const response = await client.submitRequest(request); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.SubmitRequest}`, + params: request, + }); + expect(response).toStrictEqual(expectedResponse); + }); + }); + }); +}); diff --git a/packages/keyring-snap-client/src/v2/KeyringClientV2.ts b/packages/keyring-snap-client/src/v2/KeyringClientV2.ts new file mode 100644 index 000000000..7b2447498 --- /dev/null +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.ts @@ -0,0 +1,128 @@ +import { + CreateAccountsV2ResponseStruct, + DeleteAccountV2ResponseStruct, + GetAccountV2ResponseStruct, + GetAccountsV2ResponseStruct, + SubmitRequestV2ResponseStruct, + KeyringRpcV2Method, + ExportAccountV2ResponseStruct, +} from '@metamask/keyring-api'; +import type { + CreateAccountOptions, + ExportAccountOptions, + ExportedAccount, + KeyringAccount, + KeyringRequest, + KeyringRpcV2, + KeyringRpcV2Request, +} from '@metamask/keyring-api'; +import type { AccountId } from '@metamask/keyring-utils'; +import { strictMask } from '@metamask/keyring-utils'; +import { assert } from '@metamask/superstruct'; +import type { Json } from '@metamask/utils'; +import { v4 as uuid } from 'uuid'; + +import type { Sender } from '../KeyringClient'; + +export class KeyringClientV2 implements KeyringRpcV2 { + readonly #sender: Sender; + + /** + * Create a new instance of `KeyringClient`. + * + * @param sender - The `Sender` instance to use to send requests to the snap. + */ + constructor(sender: Sender) { + this.#sender = sender; + } + + /** + * Send a request to the Snap and return the response. + * + * @param request - A partial JSON-RPC request (method and params). + * @returns A promise that resolves to the response to the request. + */ + protected async send( + request: KeyringRpcV2Request, + ): Promise { + return this.#sender.send({ + ...request, + }); + } + + async getAccounts(): Promise { + return strictMask( + await this.send({ + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcV2Method.GetAccounts, + }), + GetAccountsV2ResponseStruct, + ); + } + + async getAccount(id: string): Promise { + return strictMask( + await this.send({ + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcV2Method.GetAccount, + params: { id }, + }), + GetAccountV2ResponseStruct, + ); + } + + async createAccounts( + params: CreateAccountOptions, + ): Promise { + return strictMask( + await this.send({ + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcV2Method.CreateAccounts, + params, + }), + CreateAccountsV2ResponseStruct, + ); + } + + async exportAccount( + id: AccountId, + options?: ExportAccountOptions, + ): Promise { + return strictMask( + await this.send({ + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcV2Method.ExportAccount, + params: { id, ...(options && { options }) }, + }), + ExportAccountV2ResponseStruct, + ); + } + + async deleteAccount(id: AccountId): Promise { + assert( + await this.send({ + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcV2Method.DeleteAccount, + params: { id }, + }), + DeleteAccountV2ResponseStruct, + ); + } + + async submitRequest(request: KeyringRequest): Promise { + return strictMask( + await this.send({ + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcV2Method.SubmitRequest, + params: request, + }), + SubmitRequestV2ResponseStruct, + ); + } +} diff --git a/packages/keyring-snap-client/src/v2/index.ts b/packages/keyring-snap-client/src/v2/index.ts new file mode 100644 index 000000000..d272e687d --- /dev/null +++ b/packages/keyring-snap-client/src/v2/index.ts @@ -0,0 +1 @@ +export * from './KeyringClientV2'; diff --git a/packages/keyring-snap-sdk/CHANGELOG.md b/packages/keyring-snap-sdk/CHANGELOG.md index 7105519e4..8f8f1a001 100644 --- a/packages/keyring-snap-sdk/CHANGELOG.md +++ b/packages/keyring-snap-sdk/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `handleKeyringRequestV2` function for dispatching keyring v2 JSON-RPC requests ([#408](https://github.com/MetaMask/accounts/pull/408)) + ### Changed - Bump `@metamask/utils` from `^11.1.0` to `^11.10.0` ([#489](https://github.com/MetaMask/accounts/pull/489)) diff --git a/packages/keyring-snap-sdk/src/index.ts b/packages/keyring-snap-sdk/src/index.ts index b4ca8ca6c..b4a56df57 100644 --- a/packages/keyring-snap-sdk/src/index.ts +++ b/packages/keyring-snap-sdk/src/index.ts @@ -2,3 +2,4 @@ export * from './rpc-handler'; export * from './snap-utils'; export * from './time'; export * from './methods'; +export * from './v2'; diff --git a/packages/keyring-snap-sdk/src/v2/index.ts b/packages/keyring-snap-sdk/src/v2/index.ts new file mode 100644 index 000000000..7b53817fb --- /dev/null +++ b/packages/keyring-snap-sdk/src/v2/index.ts @@ -0,0 +1 @@ +export * from './rpc-handler'; diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts new file mode 100644 index 000000000..a2a640968 --- /dev/null +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts @@ -0,0 +1,264 @@ +import { KeyringRpcV2Method, PrivateKeyEncoding } from '@metamask/keyring-api'; +import type { + KeyringType, + CreateAccountsV2Request, + GetAccountV2Request, + GetAccountsV2Request, + DeleteAccountV2Request, + KeyringV2, + ExportAccountV2Request, + SubmitRequestV2Request, +} from '@metamask/keyring-api'; +import type { JsonRpcRequest } from '@metamask/keyring-utils'; + +import { handleKeyringRequestV2 } from './rpc-handler'; + +describe('handleKeyringRequestV2', () => { + const keyring = { + getAccounts: jest.fn(), + getAccount: jest.fn(), + createAccounts: jest.fn(), + deleteAccount: jest.fn(), + exportAccount: jest.fn(), + submitRequest: jest.fn(), + // Not required by this test. + type: 'Mocked Keyring' as KeyringType, + capabilities: { + scopes: [], + }, + serialize: jest.fn(), + deserialize: jest.fn(), + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('fails to execute an mal-formatted JSON-RPC request', async () => { + const request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + // Missing method name. + }; + + await expect( + handleKeyringRequestV2(keyring, request as unknown as JsonRpcRequest), + ).rejects.toThrow( + 'At path: method -- Expected a string, but received: undefined', + ); + }); + + it('calls `keyring_v2_getAccounts`', async () => { + const request: GetAccountsV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.GetAccounts}`, + }; + + const mockedResult = 'GetAccounts result'; + keyring.getAccounts.mockResolvedValue(mockedResult); + const result = await handleKeyringRequestV2(keyring, request); + + expect(keyring.getAccounts).toHaveBeenCalled(); + expect(result).toBe(mockedResult); + }); + + it('calls `keyring_v2_getAccount`', async () => { + const request: GetAccountV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.GetAccount}`, + params: { id: '4f983fa2-4f53-4c63-a7c2-f9a5ed750041' }, + }; + + const mockedResult = 'GetAccount result'; + keyring.getAccount.mockResolvedValue(mockedResult); + const result = await handleKeyringRequestV2(keyring, request); + + expect(keyring.getAccount).toHaveBeenCalledWith(request.params.id); + expect(result).toBe(mockedResult); + }); + + it('fails to call `keyring_v2_getAccount` without providing an account ID', async () => { + const request: GetAccountV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.GetAccount}`, + // @ts-expect-error - Testing error case. + params: {}, // Missing account ID. + }; + + await expect(handleKeyringRequestV2(keyring, request)).rejects.toThrow( + 'At path: params.id -- Expected a value of type `UuidV4`, but received: `undefined`', + ); + }); + + it('fails to call `keyring_v2_getAccount` when the `params` is not provided', async () => { + // @ts-expect-error - Testing error case. + const request: GetAccountV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.GetAccount}`, + }; + + await expect(handleKeyringRequestV2(keyring, request)).rejects.toThrow( + 'At path: params -- Expected an object, but received: undefined', + ); + }); + + it('calls `keyring_v2_createAccounts`', async () => { + const request: CreateAccountsV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.CreateAccounts}`, + params: { + type: 'bip44:derive-index', + groupIndex: 0, + entropySource: 'mock-entropy-source', + }, + }; + + const mockedResult = 'CreateAccounts result'; + keyring.createAccounts.mockResolvedValue(mockedResult); + const result = await handleKeyringRequestV2(keyring, request); + + expect(keyring.createAccounts).toHaveBeenCalledWith(request.params); + expect(result).toBe(mockedResult); + }); + + it('calls `keyring_v2_deleteAccount`', async () => { + const request: DeleteAccountV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.DeleteAccount}`, + params: { id: '4f983fa2-4f53-4c63-a7c2-f9a5ed750041' }, + }; + + keyring.deleteAccount.mockResolvedValue(undefined); + await handleKeyringRequestV2(keyring, request); + + expect(keyring.deleteAccount).toHaveBeenCalledWith(request.params.id); + }); + + it('calls `keyring_v2_exportAccount` (without options)', async () => { + const request: ExportAccountV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.ExportAccount}`, + params: { id: '4f983fa2-4f53-4c63-a7c2-f9a5ed750041' }, + }; + + const mockedResult = { + privateKey: '0x0123', + }; + keyring.exportAccount.mockResolvedValue(mockedResult); + const result = await handleKeyringRequestV2(keyring, request); + + expect(keyring.exportAccount).toHaveBeenCalledWith( + request.params.id, + undefined, + ); + expect(result).toStrictEqual(mockedResult); + }); + + it('calls `keyring_v2_exportAccount` (with options)', async () => { + const request: ExportAccountV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.ExportAccount}`, + params: { + id: '4f983fa2-4f53-4c63-a7c2-f9a5ed750041', + options: { + type: 'private-key', + encoding: PrivateKeyEncoding.Hexadecimal, + }, + }, + }; + + const mockedResult = { + privateKey: '0x0123', + }; + keyring.exportAccount.mockResolvedValue(mockedResult); + const result = await handleKeyringRequestV2(keyring, request); + + expect(keyring.exportAccount).toHaveBeenCalledWith( + request.params.id, + request.params.options, + ); + expect(result).toStrictEqual(mockedResult); + }); + + it('throws an error if `keyring_v2_exportAccount` is not implemented', async () => { + const request: ExportAccountV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.ExportAccount}`, + params: { id: '4f983fa2-4f53-4c63-a7c2-f9a5ed750041' }, + }; + + const partialKeyring: KeyringV2 = { + ...keyring, + }; + delete partialKeyring.exportAccount; + + await expect( + handleKeyringRequestV2(partialKeyring, request), + ).rejects.toThrow( + `Method not supported: ${KeyringRpcV2Method.ExportAccount}`, + ); + }); + + it('calls `keyring_v2_submitRequest`', async () => { + const dappRequest = { + id: 'c555de37-cf4b-4ff2-8273-39db7fb58f1c', + scope: 'eip155:1', + account: '4abdd17e-8b0f-4d06-a017-947a64823b3d', + origin: 'metamask', + request: { + method: 'eth_method', + params: [1, 2, 3], + }, + }; + + const request: SubmitRequestV2Request = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: `${KeyringRpcV2Method.SubmitRequest}`, + params: dappRequest, + }; + + const mockedResult = 'SubmitRequest result'; + keyring.submitRequest.mockResolvedValue(mockedResult); + const result = await handleKeyringRequestV2(keyring, request); + + expect(keyring.submitRequest).toHaveBeenCalledWith(dappRequest); + expect(result).toBe(mockedResult); + }); + + it('throws an error if an unknown method is called', async () => { + const request: JsonRpcRequest = { + jsonrpc: '2.0', + id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', + method: 'unknown_method', + }; + + await expect(handleKeyringRequestV2(keyring, request)).rejects.toThrow( + 'Method not supported: unknown_method', + ); + }); + + it('throws an "unknown error" if the error message is not a string', async () => { + const request: JsonRpcRequest = { + jsonrpc: '2.0', + id: '80c25a6b-4a76-44f4-88c5-7b3b76f72a74', + method: `${KeyringRpcV2Method.GetAccounts}`, + }; + + const error = new Error(); + error.message = 1 as unknown as string; + keyring.getAccounts.mockRejectedValue(error); + await expect(handleKeyringRequestV2(keyring, request)).rejects.toThrow( + 'An unknown error occurred while handling the keyring (v2) request', + ); + }); +}); diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts new file mode 100644 index 000000000..e246a4e26 --- /dev/null +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts @@ -0,0 +1,115 @@ +import type { KeyringV2 } from '@metamask/keyring-api'; +import { + KeyringRpcV2Method, + GetAccountsV2RequestStruct, + GetAccountV2RequestStruct, + CreateAccountsV2RequestStruct, + DeleteAccountV2RequestStruct, + ExportAccountV2RequestStruct, + SubmitRequestV2RequestStruct, +} from '@metamask/keyring-api'; +import type { JsonRpcRequest } from '@metamask/keyring-utils'; +import { JsonRpcRequestStruct } from '@metamask/keyring-utils'; +import { assert } from '@metamask/superstruct'; +import type { Json } from '@metamask/utils'; + +import { isSnapError } from '../errors'; +import { MethodNotSupportedError } from '../rpc-handler'; + +// ESLint does not like our custom error classes in this repo for some reason, they do extend Error, so unsure why. +/* eslint-disable @typescript-eslint/only-throw-error */ + +/** + * Inner function that dispatches JSON-RPC request to the associated Keyring + * methods. + * + * @param keyring - Keyring instance. + * @param request - Keyring JSON-RPC request. + * @returns A promise that resolves to the keyring response. + */ +async function dispatchKeyringRequestV2( + keyring: KeyringV2, + request: JsonRpcRequest, +): Promise { + // We first have to make sure that the request is a valid JSON-RPC request so + // we can check its method name. + assert(request, JsonRpcRequestStruct); + + switch (request.method) { + case `${KeyringRpcV2Method.GetAccounts}`: { + assert(request, GetAccountsV2RequestStruct); + return keyring.getAccounts(); + } + + case `${KeyringRpcV2Method.GetAccount}`: { + assert(request, GetAccountV2RequestStruct); + return keyring.getAccount(request.params.id); + } + + case `${KeyringRpcV2Method.CreateAccounts}`: { + assert(request, CreateAccountsV2RequestStruct); + return keyring.createAccounts(request.params); + } + + case `${KeyringRpcV2Method.DeleteAccount}`: { + assert(request, DeleteAccountV2RequestStruct); + return keyring.deleteAccount(request.params.id); + } + + case `${KeyringRpcV2Method.ExportAccount}`: { + if (keyring.exportAccount === undefined) { + throw new MethodNotSupportedError(request.method); + } + assert(request, ExportAccountV2RequestStruct); + return keyring.exportAccount(request.params.id, request.params.options); + } + + case `${KeyringRpcV2Method.SubmitRequest}`: { + assert(request, SubmitRequestV2RequestStruct); + return keyring.submitRequest(request.params); + } + + default: { + throw new MethodNotSupportedError(request.method); + } + } +} + +/** + * Handles a keyring (v2) JSON-RPC request. + * + * This function is meant to be used as a handler for Keyring (v2) JSON-RPC requests + * in an Accounts Snap. + * + * @param keyring - Keyring instance. + * @param request - Keyring JSON-RPC request. + * @returns A promise that resolves to the keyring response. + * @example + * ```ts + * export const onKeyringRequest: OnKeyringRequestHandler = async ({ + * origin, + * request, + * }) => { + * return await handleKeyringRequestV2(keyring, request); + * }; + * ``` + */ +export async function handleKeyringRequestV2( + keyring: KeyringV2, + request: JsonRpcRequest, +): Promise { + try { + return await dispatchKeyringRequestV2(keyring, request); + } catch (error) { + if (isSnapError(error)) { + throw error; + } + + const message = + error instanceof Error && typeof error.message === 'string' + ? error.message + : 'An unknown error occurred while handling the keyring (v2) request'; + + throw new Error(message); + } +} From 62146e237c5d58e9ca8cf0a53e4fad72cd5d96ca Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 10 Apr 2026 13:40:17 +0200 Subject: [PATCH 6/8] release: 99.0.0 (#509) This is the release candidate for version 99.0.0. See the changelogs for more details. --- > [!NOTE] > **Medium Risk** > Primarily a release/version bump, but it upgrades `@metamask/keyring-sdk` to `1.2.0` across multiple keyrings, which may change generated account IDs (now deterministic for EVM addresses) for downstream consumers. > > **Overview** > **Release 99.0.0**: bumps the monorepo version and publishes patch releases for several keyring packages (HD, Ledger Bridge, Trezor, Simple), updating their changelogs accordingly. > > Updates keyring dependencies to `@metamask/keyring-sdk@^1.2.0` (and bumps `@metamask/eth-hd-keyring` in `@metamask/eth-money-keyring`), with `yarn.lock` refreshed to match. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2a589339b754b3fe28600c8c07c0a14ecff1f3e0. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- package.json | 2 +- packages/keyring-eth-hd/CHANGELOG.md | 8 ++++++-- packages/keyring-eth-hd/package.json | 4 ++-- packages/keyring-eth-ledger-bridge/CHANGELOG.md | 17 ++++++++++------- packages/keyring-eth-ledger-bridge/package.json | 4 ++-- packages/keyring-eth-money/CHANGELOG.md | 4 ++++ packages/keyring-eth-money/package.json | 2 +- packages/keyring-eth-qr/CHANGELOG.md | 4 ++-- packages/keyring-eth-qr/package.json | 2 +- packages/keyring-eth-simple/CHANGELOG.md | 10 +++++++++- packages/keyring-eth-simple/package.json | 4 ++-- packages/keyring-eth-trezor/CHANGELOG.md | 11 +++++++---- packages/keyring-eth-trezor/package.json | 4 ++-- packages/keyring-sdk/CHANGELOG.md | 5 ++++- packages/keyring-sdk/package.json | 2 +- yarn.lock | 16 ++++++++-------- 16 files changed, 62 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 271082f6f..9c6b860ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/accounts-monorepo", - "version": "98.0.0", + "version": "99.0.0", "private": true, "description": "Monorepo for MetaMask accounts related packages", "repository": { diff --git a/packages/keyring-eth-hd/CHANGELOG.md b/packages/keyring-eth-hd/CHANGELOG.md index 499d40f63..a36de55ad 100644 --- a/packages/keyring-eth-hd/CHANGELOG.md +++ b/packages/keyring-eth-hd/CHANGELOG.md @@ -7,10 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [13.1.1] + ### Changed -- Add new dependency `@metamask/keyring-sdk@1.1.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)) +- Add new dependency `@metamask/keyring-sdk@1.2.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)), ([#509](https://github.com/MetaMask/accounts/pull/509)) - This package now contains the keyring v2 wrapper helpers (`EthKeyringWrapper`, `EthKeyringMethod`). + - The account ID (generated by `KeyringAccountRegistry`) are now deterministic for EVM addresses. - Bump `@metamask/keyring-api` from `^21.6.0` to `^22.0.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) - Bump `@metamask/account-api` from `^1.0.0` to `^1.0.1` ([#487](https://github.com/MetaMask/accounts/pull/487)) - Bump `@metamask/utils` from `^11.1.0` to `^11.10.0` ([#489](https://github.com/MetaMask/accounts/pull/489)) @@ -243,7 +246,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Deserialize method (and `HdKeyring` constructor by extension) can no longer be passed an options object containing a value for `numberOfAccounts` if it is not also containing a value for `mnemonic`. - Package name changed from `eth-hd-keyring` to `@metamask/eth-hd-keyring`. -[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-hd-keyring@13.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-hd-keyring@13.1.1...HEAD +[13.1.1]: https://github.com/MetaMask/accounts/compare/@metamask/eth-hd-keyring@13.1.0...@metamask/eth-hd-keyring@13.1.1 [13.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-hd-keyring@13.0.0...@metamask/eth-hd-keyring@13.1.0 [13.0.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-hd-keyring@12.1.0...@metamask/eth-hd-keyring@13.0.0 [12.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-hd-keyring@12.0.0...@metamask/eth-hd-keyring@12.1.0 diff --git a/packages/keyring-eth-hd/package.json b/packages/keyring-eth-hd/package.json index d9be92868..56a13cdaf 100644 --- a/packages/keyring-eth-hd/package.json +++ b/packages/keyring-eth-hd/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/eth-hd-keyring", - "version": "13.1.0", + "version": "13.1.1", "description": "A simple standard interface for a seed phrase generated set of Ethereum accounts.", "keywords": [ "ethereum", @@ -48,7 +48,7 @@ "@metamask/eth-sig-util": "^8.2.0", "@metamask/key-tree": "^10.0.2", "@metamask/keyring-api": "^22.0.0", - "@metamask/keyring-sdk": "^1.1.0", + "@metamask/keyring-sdk": "^1.2.0", "@metamask/keyring-utils": "^3.2.0", "@metamask/scure-bip39": "^2.1.1", "@metamask/superstruct": "^3.1.0", diff --git a/packages/keyring-eth-ledger-bridge/CHANGELOG.md b/packages/keyring-eth-ledger-bridge/CHANGELOG.md index 57d4bf9a8..21c2801fd 100644 --- a/packages/keyring-eth-ledger-bridge/CHANGELOG.md +++ b/packages/keyring-eth-ledger-bridge/CHANGELOG.md @@ -9,18 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `getTransactionSelector` to read the 4-byte calldata selector from serialized transaction hex (legacy and typed txs) ([#506](https://github.com/MetaMask/accounts/pull/506)) - - Ledger mobile bridge passes `nft: true` to `clearSignTransaction` when that selector is NFT-only (ERC-721 / ERC-1155). -- Add `ERC20_WRITE_SELECTORS` for the three EIP-20 write functions (`transfer`, `transferFrom`, `approve`) ([#506](https://github.com/MetaMask/accounts/pull/506)) +- Add `getTransactionSelector` to read the 4-byte calldata selector from serialized transaction hex (legacy and typed txs) ([#506](https://github.com/MetaMask/accounts/pull/506)) + - Ledger mobile bridge passes `nft: true` to `clearSignTransaction` when that selector is NFT-only (ERC-721 / ERC-1155). +- Add `ERC20_WRITE_SELECTORS` for the three EIP-20 write functions (`transfer`, `transferFrom`, `approve`) ([#506](https://github.com/MetaMask/accounts/pull/506)) + +## [11.3.1] ### Changed -- Add new dependency `@metamask/keyring-sdk@1.1.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)) +- Add new dependency `@metamask/keyring-sdk@1.2.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)), ([#509](https://github.com/MetaMask/accounts/pull/509)) - This package now contains the keyring v2 wrapper helpers (`EthKeyringWrapper`). -- Bump `@metamask/hw-wallet-sdk` from `^0.6.0` to `^0.7.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) + - The account ID (generated by `KeyringAccountRegistry`) are now deterministic for EVM addresses. +- Bump `@metamask/hw-wallet-sdk` from `^0.6.0` to `^0.8.0` ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#497](https://github.com/MetaMask/accounts/pull/497)) - Bump `@metamask/keyring-api` from `^21.6.0` to `^22.0.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) - Bump `@metamask/account-api` from `^1.0.0` to `^1.0.1` ([#487](https://github.com/MetaMask/accounts/pull/487)) -- Bump `@metamask/hw-wallet-sdk` from `^0.7.0` to `^0.8.0` ([#497](https://github.com/MetaMask/accounts/pull/497)) ## [11.3.0] @@ -380,7 +382,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support new versions of ethereumjs/tx ([#68](https://github.com/MetaMask/eth-ledger-bridge-keyring/pull/68)) -[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-ledger-bridge-keyring@11.3.0...HEAD +[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-ledger-bridge-keyring@11.3.1...HEAD +[11.3.1]: https://github.com/MetaMask/accounts/compare/@metamask/eth-ledger-bridge-keyring@11.3.0...@metamask/eth-ledger-bridge-keyring@11.3.1 [11.3.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-ledger-bridge-keyring@11.2.0...@metamask/eth-ledger-bridge-keyring@11.3.0 [11.2.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-ledger-bridge-keyring@11.1.2...@metamask/eth-ledger-bridge-keyring@11.2.0 [11.1.2]: https://github.com/MetaMask/accounts/compare/@metamask/eth-ledger-bridge-keyring@11.1.1...@metamask/eth-ledger-bridge-keyring@11.1.2 diff --git a/packages/keyring-eth-ledger-bridge/package.json b/packages/keyring-eth-ledger-bridge/package.json index 0bab60403..b20188be2 100644 --- a/packages/keyring-eth-ledger-bridge/package.json +++ b/packages/keyring-eth-ledger-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/eth-ledger-bridge-keyring", - "version": "11.3.0", + "version": "11.3.1", "description": "A MetaMask compatible keyring, for ledger hardware wallets", "keywords": [ "ethereum", @@ -53,7 +53,7 @@ "@metamask/eth-sig-util": "^8.2.0", "@metamask/hw-wallet-sdk": "^0.8.0", "@metamask/keyring-api": "^22.0.0", - "@metamask/keyring-sdk": "^1.1.0", + "@metamask/keyring-sdk": "^1.2.0", "@metamask/keyring-utils": "^3.2.0", "hdkey": "^2.1.0" }, diff --git a/packages/keyring-eth-money/CHANGELOG.md b/packages/keyring-eth-money/CHANGELOG.md index 52ddd2b56..986d55779 100644 --- a/packages/keyring-eth-money/CHANGELOG.md +++ b/packages/keyring-eth-money/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Bump `@metamask/eth-hd-keyring` from `^13.1.0` to `^13.1.1` ([#509](https://github.com/MetaMask/accounts/pull/509)) + ## [2.0.0] ### Added diff --git a/packages/keyring-eth-money/package.json b/packages/keyring-eth-money/package.json index e57d56a15..77e58f159 100644 --- a/packages/keyring-eth-money/package.json +++ b/packages/keyring-eth-money/package.json @@ -43,7 +43,7 @@ "test:clean": "jest --clearCache" }, "dependencies": { - "@metamask/eth-hd-keyring": "^13.1.0", + "@metamask/eth-hd-keyring": "^13.1.1", "@metamask/keyring-api": "^22.0.0", "@metamask/keyring-utils": "^3.2.0", "@metamask/superstruct": "^3.1.0", diff --git a/packages/keyring-eth-qr/CHANGELOG.md b/packages/keyring-eth-qr/CHANGELOG.md index 228f62169..0cca38400 100644 --- a/packages/keyring-eth-qr/CHANGELOG.md +++ b/packages/keyring-eth-qr/CHANGELOG.md @@ -9,9 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `QrKeyringV2` class implementing `KeyringV2` interface ([#411](https://github.com/MetaMask/accounts/pull/411)), ([#447](https://github.com/MetaMask/accounts/pull/447)), ([#451](https://github.com/MetaMask/accounts/pull/451)), ([#453](https://github.com/MetaMask/accounts/pull/453)), ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#487](https://github.com/MetaMask/accounts/pull/487)), ([#496](https://github.com/MetaMask/accounts/pull/496)) +- Add `QrKeyringV2` class implementing `KeyringV2` interface ([#411](https://github.com/MetaMask/accounts/pull/411)), ([#447](https://github.com/MetaMask/accounts/pull/447)), ([#451](https://github.com/MetaMask/accounts/pull/451)), ([#453](https://github.com/MetaMask/accounts/pull/453)), ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#487](https://github.com/MetaMask/accounts/pull/487)), ([#496](https://github.com/MetaMask/accounts/pull/496)), ([#509](https://github.com/MetaMask/accounts/pull/509)) - Add new dependency `@metamask/keyring-api@22.0.0`. - - Add new dependency `@metamask/keyring-sdk@1.1.0`. + - Add new dependency `@metamask/keyring-sdk@1.2.0`. - Add new dependency `@metamask/account-api@1.0.1`. - Wraps legacy `QrKeyring` to expose accounts via the unified `KeyringV2` API and the `KeyringAccount` type. - Extends `EthKeyringWrapper` for common Ethereum logic. diff --git a/packages/keyring-eth-qr/package.json b/packages/keyring-eth-qr/package.json index ae0fe224c..e5037d85a 100644 --- a/packages/keyring-eth-qr/package.json +++ b/packages/keyring-eth-qr/package.json @@ -54,7 +54,7 @@ "@keystonehq/bc-ur-registry-eth": "^0.19.1", "@metamask/eth-sig-util": "^8.2.0", "@metamask/keyring-api": "^22.0.0", - "@metamask/keyring-sdk": "^1.1.0", + "@metamask/keyring-sdk": "^1.2.0", "@metamask/keyring-utils": "^3.2.0", "@metamask/utils": "^11.10.0", "async-mutex": "^0.5.0", diff --git a/packages/keyring-eth-simple/CHANGELOG.md b/packages/keyring-eth-simple/CHANGELOG.md index 549810949..f10a8bf93 100644 --- a/packages/keyring-eth-simple/CHANGELOG.md +++ b/packages/keyring-eth-simple/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [11.1.2] + +### Changed + +- Bump `@metamask/keyring-sdk` from `^1.0.0` to `^1.1.0` ([#509](https://github.com/MetaMask/accounts/pull/509)) + - The account ID (generated by `KeyringAccountRegistry`) are now deterministic for EVM addresses. + ## [11.1.1] ### Changed @@ -171,7 +178,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Remove redundant `newGethSignMessage` method ([#72](https://github.com/MetaMask/eth-simple-keyring/pull/72)) - Consumers can use `signPersonalMessage` method as a replacement for `newGethSignMessage`. -[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-simple-keyring@11.1.1...HEAD +[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-simple-keyring@11.1.2...HEAD +[11.1.2]: https://github.com/MetaMask/accounts/compare/@metamask/eth-simple-keyring@11.1.1...@metamask/eth-simple-keyring@11.1.2 [11.1.1]: https://github.com/MetaMask/accounts/compare/@metamask/eth-simple-keyring@11.1.0...@metamask/eth-simple-keyring@11.1.1 [11.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-simple-keyring@11.0.0...@metamask/eth-simple-keyring@11.1.0 [11.0.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-simple-keyring@10.0.0...@metamask/eth-simple-keyring@11.0.0 diff --git a/packages/keyring-eth-simple/package.json b/packages/keyring-eth-simple/package.json index 1cb117e9d..eb8acaea5 100644 --- a/packages/keyring-eth-simple/package.json +++ b/packages/keyring-eth-simple/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/eth-simple-keyring", - "version": "11.1.1", + "version": "11.1.2", "description": "A simple standard interface for a series of Ethereum private keys.", "keywords": [ "ethereum", @@ -47,7 +47,7 @@ "@ethereumjs/util": "^9.1.0", "@metamask/eth-sig-util": "^8.2.0", "@metamask/keyring-api": "^22.0.0", - "@metamask/keyring-sdk": "^1.1.0", + "@metamask/keyring-sdk": "^1.2.0", "@metamask/utils": "^11.10.0", "ethereum-cryptography": "^2.2.1", "randombytes": "^2.1.0" diff --git a/packages/keyring-eth-trezor/CHANGELOG.md b/packages/keyring-eth-trezor/CHANGELOG.md index 3a9fa941b..68bceeb94 100644 --- a/packages/keyring-eth-trezor/CHANGELOG.md +++ b/packages/keyring-eth-trezor/CHANGELOG.md @@ -7,15 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [9.1.1] + ### Changed -- Add new dependency `@metamask/keyring-sdk@1.1.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)) +- Add new dependency `@metamask/keyring-sdk@1.2.0` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#496](https://github.com/MetaMask/accounts/pull/496)), ([#509](https://github.com/MetaMask/accounts/pull/509)) - This package now contains the keyring v2 wrapper helpers (`EthKeyringWrapper`). -- Bump `@metamask/hw-wallet-sdk` from `^0.6.0` to `^0.7.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) + - The account ID (generated by `KeyringAccountRegistry`) are now deterministic for EVM addresses. +- Bump `@metamask/hw-wallet-sdk` from `^0.6.0` to `^0.8.0` ([#482](https://github.com/MetaMask/accounts/pull/482)), ([#497](https://github.com/MetaMask/accounts/pull/497)) - Bump `@metamask/keyring-api` from `^21.6.0` to `^22.0.0` ([#482](https://github.com/MetaMask/accounts/pull/482)) - Bump `@metamask/account-api` from `^1.0.0` to `^1.0.1` ([#487](https://github.com/MetaMask/accounts/pull/487)) - Bump `@metamask/utils` from `^11.1.0` to `^11.10.0` ([#489](https://github.com/MetaMask/accounts/pull/489)) -- Bump `@metamask/hw-wallet-sdk` from `^0.7.0` to `^0.8.0` ([#497](https://github.com/MetaMask/accounts/pull/497)) ## [9.1.0] @@ -236,7 +238,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support new versions of ethereumjs/tx ([#88](https://github.com/metamask/eth-trezor-keyring/pull/88)) -[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-trezor-keyring@9.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/eth-trezor-keyring@9.1.1...HEAD +[9.1.1]: https://github.com/MetaMask/accounts/compare/@metamask/eth-trezor-keyring@9.1.0...@metamask/eth-trezor-keyring@9.1.1 [9.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-trezor-keyring@9.0.0...@metamask/eth-trezor-keyring@9.1.0 [9.0.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-trezor-keyring@8.0.0...@metamask/eth-trezor-keyring@9.0.0 [8.0.0]: https://github.com/MetaMask/accounts/compare/@metamask/eth-trezor-keyring@7.0.0...@metamask/eth-trezor-keyring@8.0.0 diff --git a/packages/keyring-eth-trezor/package.json b/packages/keyring-eth-trezor/package.json index 88dffd307..5c643f748 100644 --- a/packages/keyring-eth-trezor/package.json +++ b/packages/keyring-eth-trezor/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/eth-trezor-keyring", - "version": "9.1.0", + "version": "9.1.1", "description": "A MetaMask compatible keyring, for trezor hardware wallets", "keywords": [ "ethereum", @@ -51,7 +51,7 @@ "@metamask/eth-sig-util": "^8.2.0", "@metamask/hw-wallet-sdk": "^0.8.0", "@metamask/keyring-api": "^22.0.0", - "@metamask/keyring-sdk": "^1.1.0", + "@metamask/keyring-sdk": "^1.2.0", "@metamask/keyring-utils": "^3.2.0", "@metamask/utils": "^11.10.0", "@trezor/connect-plugin-ethereum": "^9.0.5", diff --git a/packages/keyring-sdk/CHANGELOG.md b/packages/keyring-sdk/CHANGELOG.md index f7efcd966..d269cadaa 100644 --- a/packages/keyring-sdk/CHANGELOG.md +++ b/packages/keyring-sdk/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] + ### Added - Add `generateId` option to `KeyringAccountRegistry` ([#503](https://github.com/MetaMask/accounts/pull/503)) @@ -28,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release, extracted from `@metamask/keyring-api` ([#478](https://github.com/MetaMask/accounts/pull/478)), ([#482](https://github.com/MetaMask/accounts/pull/482)) -[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@1.1.0...HEAD +[Unreleased]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@1.2.0...HEAD +[1.2.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@1.1.0...@metamask/keyring-sdk@1.2.0 [1.1.0]: https://github.com/MetaMask/accounts/compare/@metamask/keyring-sdk@1.0.0...@metamask/keyring-sdk@1.1.0 [1.0.0]: https://github.com/MetaMask/accounts/releases/tag/@metamask/keyring-sdk@1.0.0 diff --git a/packages/keyring-sdk/package.json b/packages/keyring-sdk/package.json index e8f7a6c0d..7ff6ced74 100644 --- a/packages/keyring-sdk/package.json +++ b/packages/keyring-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@metamask/keyring-sdk", - "version": "1.1.0", + "version": "1.2.0", "description": "MetaMask Keyring SDK", "keywords": [ "metamask", diff --git a/yarn.lock b/yarn.lock index ec5bdf9df..024c282be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1678,7 +1678,7 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-hd-keyring@npm:^13.1.0, @metamask/eth-hd-keyring@workspace:packages/keyring-eth-hd": +"@metamask/eth-hd-keyring@npm:^13.1.1, @metamask/eth-hd-keyring@workspace:packages/keyring-eth-hd": version: 0.0.0-use.local resolution: "@metamask/eth-hd-keyring@workspace:packages/keyring-eth-hd" dependencies: @@ -1692,7 +1692,7 @@ __metadata: "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/key-tree": "npm:^10.0.2" "@metamask/keyring-api": "npm:^22.0.0" - "@metamask/keyring-sdk": "npm:^1.1.0" + "@metamask/keyring-sdk": "npm:^1.2.0" "@metamask/keyring-utils": "npm:^3.2.0" "@metamask/scure-bip39": "npm:^2.1.1" "@metamask/superstruct": "npm:^3.1.0" @@ -1726,7 +1726,7 @@ __metadata: "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/hw-wallet-sdk": "npm:^0.8.0" "@metamask/keyring-api": "npm:^22.0.0" - "@metamask/keyring-sdk": "npm:^1.1.0" + "@metamask/keyring-sdk": "npm:^1.2.0" "@metamask/keyring-utils": "npm:^3.2.0" "@metamask/utils": "npm:^11.10.0" "@ts-bridge/cli": "npm:^0.6.3" @@ -1756,7 +1756,7 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.2.1" "@lavamoat/preinstall-always-fail": "npm:^2.1.0" "@metamask/auto-changelog": "npm:^3.4.4" - "@metamask/eth-hd-keyring": "npm:^13.1.0" + "@metamask/eth-hd-keyring": "npm:^13.1.1" "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/key-tree": "npm:^10.0.2" "@metamask/keyring-api": "npm:^22.0.0" @@ -1786,7 +1786,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/keyring-api": "npm:^22.0.0" - "@metamask/keyring-sdk": "npm:^1.1.0" + "@metamask/keyring-sdk": "npm:^1.2.0" "@metamask/keyring-utils": "npm:^3.2.0" "@metamask/utils": "npm:^11.10.0" "@types/hdkey": "npm:^2.0.1" @@ -1854,7 +1854,7 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/keyring-api": "npm:^22.0.0" - "@metamask/keyring-sdk": "npm:^1.1.0" + "@metamask/keyring-sdk": "npm:^1.2.0" "@metamask/keyring-utils": "npm:^3.2.0" "@metamask/utils": "npm:^11.10.0" "@ts-bridge/cli": "npm:^0.6.3" @@ -1929,7 +1929,7 @@ __metadata: "@metamask/eth-sig-util": "npm:^8.2.0" "@metamask/hw-wallet-sdk": "npm:^0.8.0" "@metamask/keyring-api": "npm:^22.0.0" - "@metamask/keyring-sdk": "npm:^1.1.0" + "@metamask/keyring-sdk": "npm:^1.2.0" "@metamask/keyring-utils": "npm:^3.2.0" "@metamask/utils": "npm:^11.10.0" "@trezor/connect-plugin-ethereum": "npm:^9.0.5" @@ -2116,7 +2116,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/keyring-sdk@npm:^1.1.0, @metamask/keyring-sdk@workspace:packages/keyring-sdk": +"@metamask/keyring-sdk@npm:^1.2.0, @metamask/keyring-sdk@workspace:packages/keyring-sdk": version: 0.0.0-use.local resolution: "@metamask/keyring-sdk@workspace:packages/keyring-sdk" dependencies: From 6dfcb37aa133255b2c8b1d65f29009ff81c052a6 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 10 Apr 2026 21:41:47 +0800 Subject: [PATCH 7/8] fix: lint --- packages/keyring-eth-ledger-bridge/src/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/keyring-eth-ledger-bridge/src/utils.ts b/packages/keyring-eth-ledger-bridge/src/utils.ts index 8a2d55fa7..6b1fc7432 100644 --- a/packages/keyring-eth-ledger-bridge/src/utils.ts +++ b/packages/keyring-eth-ledger-bridge/src/utils.ts @@ -11,12 +11,12 @@ import { add0x } from '@metamask/utils'; */ export function getTransactionSelector(rawTxHex: string): string | undefined { try { - const prefixedHex = add0x(rawTxHex); + const prefixedHex = add0x(rawTxHex); const tx = TransactionFactory.fromSerializedData(hexToBytes(prefixedHex)); const dataHex = bytesToHex(tx.data); - const selectorSize = 2 /* 0x */ + (4 * 2) /* 4 bytes (hex) */; - if (dataHex.length >= selectorSize) { - return dataHex.slice(0, selectorSize).toLowerCase(); + const selectorSize = 2 /* 0x */ + 4 * 2; /* 4 bytes (hex) */ + if (dataHex.length >= selectorSize) { + return dataHex.slice(0, selectorSize).toLowerCase(); } } catch { // ignore parse errors From 51e56bb735ba127a24ba367bd435e514319b3da5 Mon Sep 17 00:00:00 2001 From: Monte Lai Date: Fri, 10 Apr 2026 21:51:18 +0800 Subject: [PATCH 8/8] fix: update jest --- packages/keyring-eth-ledger-bridge/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-eth-ledger-bridge/jest.config.js b/packages/keyring-eth-ledger-bridge/jest.config.js index a5fe6f2e3..c60d853b2 100644 --- a/packages/keyring-eth-ledger-bridge/jest.config.js +++ b/packages/keyring-eth-ledger-bridge/jest.config.js @@ -26,7 +26,7 @@ module.exports = merge(baseConfig, { // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { - branches: 93.59, + branches: 93.53, functions: 98.3, lines: 97.94, statements: 97.95,