From 5622413f3408c1f3dc257a360a2d5a14bc99c3e3 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 4 Dec 2025 15:48:21 +0100 Subject: [PATCH 01/25] feat: add KeyringClientV2 support --- packages/keyring-api/src/api/v2/index.ts | 1 + .../keyring-api/src/api/v2/keyring-rpc.ts | 183 +++++++++++++++++ .../src/v2/KeyringClientV2.test.ts | 187 ++++++++++++++++++ .../src/v2/KeyringClientV2.ts | 129 ++++++++++++ 4 files changed, 500 insertions(+) create mode 100644 packages/keyring-api/src/api/v2/keyring-rpc.ts create mode 100644 packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts create mode 100644 packages/keyring-snap-client/src/v2/KeyringClientV2.ts diff --git a/packages/keyring-api/src/api/v2/index.ts b/packages/keyring-api/src/api/v2/index.ts index cc137c238..0f450dcad 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.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts new file mode 100644 index 000000000..e309a276d --- /dev/null +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -0,0 +1,183 @@ +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 { + 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 = { + getAccounts: KeyringV2['getAccounts']; + getAccount: KeyringV2['getAccount']; + createAccounts: KeyringV2['createAccounts']; + deleteAccount: KeyringV2['deleteAccount']; + exportAccount: KeyringV2['exportAccount']; + submitRequest: KeyringV2['submitRequest']; +}; + +/** + * Keyring RPC methods used by the API. + */ +export enum KeyringRpcV2Method { + GetAccounts = 'keyring_v2_getAccounts', + GetAccount = 'keyring_v2_getAccount', + CreateAccounts = 'keyring_v2_createAccounts', + DeleteAccount = 'keyring_v2_deleteAccount', + ExportAccount = 'keyring_v2_exportAccount', + SubmitRequest = 'keyring_v2_submitRequest', +} + +/** + * Check if a method is a keyring RPC method. + * + * @param method - Method to check. + * @returns Whether the method is a keyring RPC method. + */ +export function isKeyringRpcV2Method(method: string): boolean { + return Object.values(KeyringRpcV2Method).includes( + method as KeyringRpcV2Method, + ); +} + +// ---------------------------------------------------------------------------- + +const CommonHeader = { + jsonrpc: literal('2.0'), + id: union([string(), number(), literal(null)]), +}; + +// ---------------------------------------------------------------------------- +// Get accounts + +export const GetAccountsV2Struct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.GetAccounts}`), +}); + +export type GetAccountsV2Request = Infer; + +export const GetAccountsV2ResponseStruct = array(KeyringAccountStruct); + +export type GetAccountsV2Response = Infer; + +// ---------------------------------------------------------------------------- +// Get account + +export const GetAccountV2Struct = 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 CreateAccountsV2Struct = object({ + ...CommonHeader, + method: literal(`${KeyringRpcV2Method.CreateAccounts}`), + params: object({ + // TODO + }), +}); + +export type CreateAccountsV2Request = Infer; + +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-snap-client/src/v2/KeyringClientV2.test.ts b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts new file mode 100644 index 000000000..32d0b7e1b --- /dev/null +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts @@ -0,0 +1,187 @@ +import { + KeyringRpcV2Method, + PrivateKeyEncoding, + type KeyringAccount, + type KeyringRequest, +} from '@metamask/keyring-api'; +import type { Json } from '@metamask/utils'; + +import { KeyringClientV2 } from './KeyringClientV2'; + +describe('KeyringClient', () => { + 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', + }, + ]; + + mockSender.send.mockResolvedValue(expectedResponse); + const account = await client.createAccounts(); + expect(mockSender.send).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: expect.any(String), + method: `${KeyringRpcV2Method.CreateAccounts}`, + params: { options: {} }, + }); + 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..8386476e7 --- /dev/null +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.ts @@ -0,0 +1,129 @@ +/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ + +import { + CreateAccountsV2ResponseStruct, + DeleteAccountV2ResponseStruct, + GetAccountV2ResponseStruct, + GetAccountsV2ResponseStruct, + SubmitRequestV2ResponseStruct, + KeyringRpcV2Method, + ExportAccountV2ResponseStruct, +} from '@metamask/keyring-api'; +import type { + 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( + options: Record = {}, + ): Promise { + return strictMask( + await this.send({ + jsonrpc: '2.0', + id: uuid(), + method: KeyringRpcV2Method.CreateAccounts, + params: { options }, + }), + 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, + ); + } +} From 0ed48121d9c412cd4d8826f6db5eaba65e1d6c63 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 4 Dec 2025 17:37:28 +0100 Subject: [PATCH 02/25] fix: remove unused function --- packages/keyring-api/src/api/v2/keyring-rpc.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts index e309a276d..932c68558 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -36,18 +36,6 @@ export enum KeyringRpcV2Method { SubmitRequest = 'keyring_v2_submitRequest', } -/** - * Check if a method is a keyring RPC method. - * - * @param method - Method to check. - * @returns Whether the method is a keyring RPC method. - */ -export function isKeyringRpcV2Method(method: string): boolean { - return Object.values(KeyringRpcV2Method).includes( - method as KeyringRpcV2Method, - ); -} - // ---------------------------------------------------------------------------- const CommonHeader = { From e59cd8fa6bd53e8a17903419cc640a3b50b56af1 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 4 Dec 2025 17:37:39 +0100 Subject: [PATCH 03/25] fix: remove unused eslint directive --- packages/keyring-snap-client/src/v2/KeyringClientV2.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/keyring-snap-client/src/v2/KeyringClientV2.ts b/packages/keyring-snap-client/src/v2/KeyringClientV2.ts index 8386476e7..cd340ae17 100644 --- a/packages/keyring-snap-client/src/v2/KeyringClientV2.ts +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-redundant-type-constituents */ - import { CreateAccountsV2ResponseStruct, DeleteAccountV2ResponseStruct, From f52e5b6185f9b68f86efd4c4ce76de212bd5550e Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 4 Dec 2025 21:21:38 +0100 Subject: [PATCH 04/25] fix: fix createAccounts params --- packages/keyring-api/src/api/v2/keyring-rpc.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts index 932c68558..5d9299454 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -3,6 +3,7 @@ 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, @@ -80,9 +81,7 @@ export type GetAccountV2Response = Infer; export const CreateAccountsV2Struct = object({ ...CommonHeader, method: literal(`${KeyringRpcV2Method.CreateAccounts}`), - params: object({ - // TODO - }), + params: CreateAccountOptionsStruct, }); export type CreateAccountsV2Request = Infer; From 085b7f9854b26a85013d63d0341b941829a79e5f Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 4 Dec 2025 21:21:55 +0100 Subject: [PATCH 05/25] fix: properly name request structs --- packages/keyring-api/src/api/v2/keyring-rpc.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts index 5d9299454..815bac303 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -47,12 +47,12 @@ const CommonHeader = { // ---------------------------------------------------------------------------- // Get accounts -export const GetAccountsV2Struct = object({ +export const GetAccountsV2RequestStruct = object({ ...CommonHeader, method: literal(`${KeyringRpcV2Method.GetAccounts}`), }); -export type GetAccountsV2Request = Infer; +export type GetAccountsV2Request = Infer; export const GetAccountsV2ResponseStruct = array(KeyringAccountStruct); @@ -61,7 +61,7 @@ export type GetAccountsV2Response = Infer; // ---------------------------------------------------------------------------- // Get account -export const GetAccountV2Struct = object({ +export const GetAccountV2RequestStruct = object({ ...CommonHeader, method: literal(`${KeyringRpcV2Method.GetAccount}`), params: object({ @@ -69,7 +69,7 @@ export const GetAccountV2Struct = object({ }), }); -export type GetAccountV2Request = Infer; +export type GetAccountV2Request = Infer; export const GetAccountV2ResponseStruct = KeyringAccountStruct; @@ -78,13 +78,15 @@ export type GetAccountV2Response = Infer; // ---------------------------------------------------------------------------- // Create accounts -export const CreateAccountsV2Struct = object({ +export const CreateAccountsV2RequestStruct = object({ ...CommonHeader, method: literal(`${KeyringRpcV2Method.CreateAccounts}`), params: CreateAccountOptionsStruct, }); -export type CreateAccountsV2Request = Infer; +export type CreateAccountsV2Request = Infer< + typeof CreateAccountsV2RequestStruct +>; export const CreateAccountsV2ResponseStruct = array(KeyringAccountStruct); From 058543e78d7cec9d4435a9e13ffbe1c8612655c6 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 4 Dec 2025 21:22:13 +0100 Subject: [PATCH 06/25] feat: add rpc-handler for v2 --- .../src/v2/rpc-handler.test.ts | 242 ++++++++++++++++++ .../keyring-snap-sdk/src/v2/rpc-handler.ts | 115 +++++++++ 2 files changed, 357 insertions(+) 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-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..80475f019 --- /dev/null +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts @@ -0,0 +1,242 @@ +import { KeyringRpcV2Method } 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( + '4f983fa2-4f53-4c63-a7c2-f9a5ed750041', + ); + 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' }, + }; + + const mockedResult = 'DeleteAccount result'; + keyring.deleteAccount.mockResolvedValue(mockedResult); + const result = await handleKeyringRequestV2(keyring, request); + + expect(keyring.deleteAccount).toHaveBeenCalledWith( + '4f983fa2-4f53-4c63-a7c2-f9a5ed750041', + ); + expect(result).toBe(mockedResult); + }); + + it('calls `keyring_v2_exportAccount`', 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( + '4f983fa2-4f53-4c63-a7c2-f9a5ed750041', + ); + 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 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..68ef415f9 --- /dev/null +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts @@ -0,0 +1,115 @@ +import type { KeyringV2 } from '@metamask/keyring-api'; +import { + KeyringRpcMethod, + 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'; + +/** + * Error thrown when a keyring JSON-RPC method is not supported. + */ +export class MethodNotSupportedError extends Error { + constructor(method: string) { + super(`Method not supported: ${method}`); + } +} + +/** + * 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); + } + + case `${KeyringRpcV2Method.SubmitRequest}`: { + assert(request, SubmitRequestV2RequestStruct); + return keyring.submitRequest(request.params); + } + + default: { + throw new MethodNotSupportedError(request.method); + } + } +} + +/** + * Handles a keyring JSON-RPC request. + * + * This function is meant to be used as a handler for Keyring 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 handleKeyringRequest(keyring, request); + * }; + * ``` + */ +export async function handleKeyringRequestV2( + keyring: KeyringV2, + request: JsonRpcRequest, +): Promise { + try { + return await dispatchKeyringRequestV2(keyring, request); + } catch (error) { + const message = + error instanceof Error && typeof error.message === 'string' + ? error.message + : 'An unknown error occurred while handling the keyring request (v2)'; + + throw new Error(message); + } +} From 45e3ed55cabcf1950407896dbb21acd4a0a36635 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 12:20:45 +0100 Subject: [PATCH 07/25] feat: better typing for isKeyringRpcMethod --- packages/keyring-api/src/rpc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-api/src/rpc.ts b/packages/keyring-api/src/rpc.ts index c3895674c..c62b7daad 100644 --- a/packages/keyring-api/src/rpc.ts +++ b/packages/keyring-api/src/rpc.ts @@ -60,7 +60,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); } From 84b6e955c4ac03b6d74dcbf613733b3dae7adef5 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 12:21:26 +0100 Subject: [PATCH 08/25] feat: add isKeyringRpcV2Method --- .../keyring-api/src/api/v2/keyring-rpc.test.ts | 14 ++++++++++++++ packages/keyring-api/src/api/v2/keyring-rpc.ts | 14 ++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 packages/keyring-api/src/api/v2/keyring-rpc.test.ts 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..f98c2c27b --- /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: KeyringRpcV2Method.$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 index 815bac303..d4aa11e2c 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -37,6 +37,20 @@ export enum KeyringRpcV2Method { SubmitRequest = 'keyring_v2_submitRequest', } +/** + * 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 = { From 6b260f0640c5a54a88a2d16ca6f51399a15029e9 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 12:22:21 +0100 Subject: [PATCH 09/25] feat: fix createAccounts for client v2 --- .../keyring-snap-client/src/v2/KeyringClientV2.test.ts | 10 ++++++++-- packages/keyring-snap-client/src/v2/KeyringClientV2.ts | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts index 32d0b7e1b..593eea936 100644 --- a/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts @@ -81,13 +81,19 @@ describe('KeyringClient', () => { }, ]; + const createAccountOptions = { + type: 'bip44:derive-index', + entropySource: 'mock-entropy-source', + groupIndex: 0, + } as const; + mockSender.send.mockResolvedValue(expectedResponse); - const account = await client.createAccounts(); + const account = await client.createAccounts(createAccountOptions); expect(mockSender.send).toHaveBeenCalledWith({ jsonrpc: '2.0', id: expect.any(String), method: `${KeyringRpcV2Method.CreateAccounts}`, - params: { options: {} }, + params: createAccountOptions, }); expect(account).toStrictEqual(expectedResponse); }); diff --git a/packages/keyring-snap-client/src/v2/KeyringClientV2.ts b/packages/keyring-snap-client/src/v2/KeyringClientV2.ts index cd340ae17..dc9bd33dc 100644 --- a/packages/keyring-snap-client/src/v2/KeyringClientV2.ts +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.ts @@ -8,6 +8,7 @@ import { ExportAccountV2ResponseStruct, } from '@metamask/keyring-api'; import type { + CreateAccountOptions, ExportAccountOptions, ExportedAccount, KeyringAccount, @@ -73,14 +74,14 @@ export class KeyringClientV2 implements KeyringRpcV2 { } async createAccounts( - options: Record = {}, + params: CreateAccountOptions, ): Promise { return strictMask( await this.send({ jsonrpc: '2.0', id: uuid(), method: KeyringRpcV2Method.CreateAccounts, - params: { options }, + params, }), CreateAccountsV2ResponseStruct, ); From fac383efac9dbdc2c9ffb76a29ef6f5b2279d3cf Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 12:26:04 +0100 Subject: [PATCH 10/25] chore: lint --- packages/keyring-snap-sdk/src/v2/rpc-handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts index 68ef415f9..271123afa 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts @@ -1,6 +1,5 @@ import type { KeyringV2 } from '@metamask/keyring-api'; import { - KeyringRpcMethod, KeyringRpcV2Method, GetAccountsV2RequestStruct, GetAccountV2RequestStruct, From 0d94203c7b8e3007140de768be83255f0dd4502f Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 12:27:48 +0100 Subject: [PATCH 11/25] fix: fix jsdocs --- packages/keyring-snap-sdk/src/v2/rpc-handler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts index 271123afa..96cf9d36f 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts @@ -79,9 +79,9 @@ async function dispatchKeyringRequestV2( } /** - * Handles a keyring JSON-RPC request. + * Handles a keyring (v2) JSON-RPC request. * - * This function is meant to be used as a handler for Keyring JSON-RPC requests + * This function is meant to be used as a handler for Keyring (v2) JSON-RPC requests * in an Accounts Snap. * * @param keyring - Keyring instance. @@ -93,7 +93,7 @@ async function dispatchKeyringRequestV2( * origin, * request, * }) => { - * return await handleKeyringRequest(keyring, request); + * return await handleKeyringRequestV2(keyring, request); * }; * ``` */ @@ -107,7 +107,7 @@ export async function handleKeyringRequestV2( const message = error instanceof Error && typeof error.message === 'string' ? error.message - : 'An unknown error occurred while handling the keyring request (v2)'; + : 'An unknown error occurred while handling the keyring (v2) request'; throw new Error(message); } From 4b72e8b47c48243ba2bc9f2dcd9e64dd4e7ed2a0 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 13:13:16 +0100 Subject: [PATCH 12/25] feat: add KeyringInternalSnapClientV2 + missing v2 exports --- .../src/KeyringInternalSnapClient.ts | 69 +--------------- .../src/KeyringInternalSnapClientMessenger.ts | 0 .../src/SnapControllerMessengerSender.ts | 48 +++++++++++ .../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/src/index.ts | 1 + packages/keyring-snap-client/src/v2/index.ts | 1 + 9 files changed, 197 insertions(+), 65 deletions(-) create mode 100644 packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts create mode 100644 packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.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/index.ts diff --git a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts index 49b2c7917..ea9a5f516 100644 --- a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts +++ b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts @@ -4,74 +4,13 @@ import type { KeyringResponseV1, } 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 { Messenger } from '@metamask/messenger'; -import type { HandleSnapRequest } from '@metamask/snaps-controllers'; +import { KeyringClient } from '@metamask/keyring-snap-client'; +import { strictMask } from '@metamask/keyring-utils'; import type { SnapId } from '@metamask/snaps-sdk'; import type { HandlerType } from '@metamask/snaps-utils'; -import type { Json } from '@metamask/utils'; -// We only need to dispatch Snap request to the Snaps controller for now. -type AllowedActions = HandleSnapRequest; - -/** - * A restricted-`Messenger` used by `KeyringInternalSnapClient` to dispatch - * internal Snap requests. - */ -export type KeyringInternalSnapClientMessenger = Messenger< - 'KeyringInternalSnapClient', - AllowedActions ->; - -/** - * Implementation of the `Sender` interface that can be used to send requests - * to a Snap through a `Messenger`. - */ -class SnapControllerMessengerSender implements Sender { - readonly #snapId: SnapId; - - readonly #origin: string; - - readonly #messenger: KeyringInternalSnapClientMessenger; - - readonly #handler: HandlerType; - - /** - * Create a new instance of `SnapControllerSender`. - * - * @param messenger - The `Messenger` instance used when dispatching controllers actions. - * @param snapId - The ID of the Snap to use. - * @param origin - The sender's origin. - * @param handler - The handler type. - */ - constructor( - messenger: KeyringInternalSnapClientMessenger, - snapId: SnapId, - origin: string, - handler: HandlerType, - ) { - this.#messenger = messenger; - this.#snapId = snapId; - this.#origin = origin; - this.#handler = handler; - } - - /** - * Send a request to the Snap and return the response. - * - * @param request - JSON-RPC request to send to the Snap. - * @returns A promise that resolves to the response of the request. - */ - async send(request: JsonRpcRequest): Promise { - return this.#messenger.call('SnapController:handleRequest', { - snapId: this.#snapId, - origin: this.#origin, - handler: this.#handler, - request, - }) as Promise; - } -} +import type { KeyringInternalSnapClientMessenger } from './KeyringInternalSnapClientMessenger'; +import { SnapControllerMessengerSender } from './SnapControllerMessengerSender'; /** * A `KeyringClient` that allows the communication with a Snap through a diff --git a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts b/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts new file mode 100644 index 000000000..67e2cd409 --- /dev/null +++ b/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts @@ -0,0 +1,48 @@ +/** + * Implementation of the `Sender` interface that can be used to send requests + * to a Snap through a `Messenger`. + */ +export class SnapControllerMessengerSender implements Sender { + readonly #snapId: SnapId; + + readonly #origin: string; + + readonly #messenger: KeyringInternalSnapClientMessenger; + + readonly #handler: HandlerType; + + /** + * Create a new instance of `SnapControllerSender`. + * + * @param messenger - The `Messenger` instance used when dispatching controllers actions. + * @param snapId - The ID of the Snap to use. + * @param origin - The sender's origin. + * @param handler - The handler type. + */ + constructor( + messenger: KeyringInternalSnapClientMessenger, + snapId: SnapId, + origin: string, + handler: HandlerType, + ) { + this.#messenger = messenger; + this.#snapId = snapId; + this.#origin = origin; + this.#handler = handler; + } + + /** + * Send a request to the Snap and return the response. + * + * @param request - JSON-RPC request to send to the Snap. + * @returns A promise that resolves to the response of the request. + */ + async send(request: JsonRpcRequest): Promise { + return this.#messenger.call('SnapController:handleRequest', { + snapId: this.#snapId, + origin: this.#origin, + handler: this.#handler, + request, + }) as Promise; + } +} 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..0bb8e5fbc --- /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 '../KeyringInternalSnapClientMessenger'; + +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..6ea934cfc --- /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 '../KeyringInternalSnapClientMessenger'; +import { SnapControllerMessengerSender } from '../SnapControllerMessengerSender'; + +/** + * 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 `KeyringInternalSnapClient`. + * + * 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 `KeyringInternalSnapClient` with the specified + * `snapId`. + * + * @param snapId - The ID of the Snap to use in the new instance. + * @returns A new instance of `KeyringInternalSnapClient` 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/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/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'; From 2ae5c150184a8ca5c94b7c1e0610eb5014be272d Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 13:20:27 +0100 Subject: [PATCH 13/25] fix: add missing code --- .../src/KeyringInternalSnapClientMessenger.ts | 14 ++++++++++++++ .../src/SnapControllerMessengerSender.ts | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts index e69de29bb..3ab27d56b 100644 --- a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts +++ b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts @@ -0,0 +1,14 @@ +import type { Messenger } from '@metamask/messenger'; +import type { HandleSnapRequest } from '@metamask/snaps-controllers'; + +// We only need to dispatch Snap request to the Snaps controller for now. +type AllowedActions = HandleSnapRequest; + +/** + * A restricted-`Messenger` used by `KeyringInternalSnapClient` to dispatch + * internal Snap requests. + */ +export type KeyringInternalSnapClientMessenger = Messenger< + 'KeyringInternalSnapClient', + AllowedActions +>; diff --git a/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts b/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts index 67e2cd409..e130f085a 100644 --- a/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts +++ b/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts @@ -1,3 +1,9 @@ +import type { Sender } from '@metamask/keyring-snap-client'; +import type { Json, JsonRpcRequest, SnapId } from '@metamask/snaps-sdk'; +import type { HandlerType } from '@metamask/snaps-utils'; + +import type { KeyringInternalSnapClientMessenger } from './KeyringInternalSnapClientMessenger'; + /** * Implementation of the `Sender` interface that can be used to send requests * to a Snap through a `Messenger`. From 7f68ec45909f5f8b51e2b653c718c2577b04d04f Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 13:27:55 +0100 Subject: [PATCH 14/25] refactor: revert class split --- .../src/KeyringInternalSnapClient.ts | 68 ++++++++++++++++++- .../src/KeyringInternalSnapClientMessenger.ts | 14 ---- .../src/SnapControllerMessengerSender.ts | 54 --------------- .../v2/KeyringInternalSnapClientV2.test.ts | 2 +- .../src/v2/KeyringInternalSnapClientV2.ts | 4 +- 5 files changed, 68 insertions(+), 74 deletions(-) delete mode 100644 packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts delete mode 100644 packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts diff --git a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts index ea9a5f516..9f59cadaf 100644 --- a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts +++ b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClient.ts @@ -4,13 +4,75 @@ import type { KeyringResponseV1, } from '@metamask/keyring-internal-api'; import { SubmitRequestResponseV1Struct } from '@metamask/keyring-internal-api'; +import type { Sender } from '@metamask/keyring-snap-client'; import { KeyringClient } from '@metamask/keyring-snap-client'; import { strictMask } from '@metamask/keyring-utils'; -import type { SnapId } from '@metamask/snaps-sdk'; +import type { Messenger } from '@metamask/messenger'; +import type { HandleSnapRequest } from '@metamask/snaps-controllers'; +import type { JsonRpcRequest, SnapId } from '@metamask/snaps-sdk'; import type { HandlerType } from '@metamask/snaps-utils'; +import type { Json } from '@metamask/utils'; -import type { KeyringInternalSnapClientMessenger } from './KeyringInternalSnapClientMessenger'; -import { SnapControllerMessengerSender } from './SnapControllerMessengerSender'; +// We only need to dispatch Snap request to the Snaps controller for now. +type AllowedActions = HandleSnapRequest; + +/** + * A restricted-`Messenger` used by `KeyringInternalSnapClient` to dispatch + * internal Snap requests. + */ +export type KeyringInternalSnapClientMessenger = Messenger< + 'KeyringInternalSnapClient', + AllowedActions +>; + +/** + * Implementation of the `Sender` interface that can be used to send requests + * to a Snap through a `Messenger`. + */ +export class SnapControllerMessengerSender implements Sender { + readonly #snapId: SnapId; + + readonly #origin: string; + + readonly #messenger: KeyringInternalSnapClientMessenger; + + readonly #handler: HandlerType; + + /** + * Create a new instance of `SnapControllerSender`. + * + * @param messenger - The `Messenger` instance used when dispatching controllers actions. + * @param snapId - The ID of the Snap to use. + * @param origin - The sender's origin. + * @param handler - The handler type. + */ + constructor( + messenger: KeyringInternalSnapClientMessenger, + snapId: SnapId, + origin: string, + handler: HandlerType, + ) { + this.#messenger = messenger; + this.#snapId = snapId; + this.#origin = origin; + this.#handler = handler; + } + + /** + * Send a request to the Snap and return the response. + * + * @param request - JSON-RPC request to send to the Snap. + * @returns A promise that resolves to the response of the request. + */ + async send(request: JsonRpcRequest): Promise { + return this.#messenger.call('SnapController:handleRequest', { + snapId: this.#snapId, + origin: this.#origin, + handler: this.#handler, + request, + }) as Promise; + } +} /** * A `KeyringClient` that allows the communication with a Snap through a diff --git a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts b/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts deleted file mode 100644 index 3ab27d56b..000000000 --- a/packages/keyring-internal-snap-client/src/KeyringInternalSnapClientMessenger.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Messenger } from '@metamask/messenger'; -import type { HandleSnapRequest } from '@metamask/snaps-controllers'; - -// We only need to dispatch Snap request to the Snaps controller for now. -type AllowedActions = HandleSnapRequest; - -/** - * A restricted-`Messenger` used by `KeyringInternalSnapClient` to dispatch - * internal Snap requests. - */ -export type KeyringInternalSnapClientMessenger = Messenger< - 'KeyringInternalSnapClient', - AllowedActions ->; diff --git a/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts b/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts deleted file mode 100644 index e130f085a..000000000 --- a/packages/keyring-internal-snap-client/src/SnapControllerMessengerSender.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { Sender } from '@metamask/keyring-snap-client'; -import type { Json, JsonRpcRequest, SnapId } from '@metamask/snaps-sdk'; -import type { HandlerType } from '@metamask/snaps-utils'; - -import type { KeyringInternalSnapClientMessenger } from './KeyringInternalSnapClientMessenger'; - -/** - * Implementation of the `Sender` interface that can be used to send requests - * to a Snap through a `Messenger`. - */ -export class SnapControllerMessengerSender implements Sender { - readonly #snapId: SnapId; - - readonly #origin: string; - - readonly #messenger: KeyringInternalSnapClientMessenger; - - readonly #handler: HandlerType; - - /** - * Create a new instance of `SnapControllerSender`. - * - * @param messenger - The `Messenger` instance used when dispatching controllers actions. - * @param snapId - The ID of the Snap to use. - * @param origin - The sender's origin. - * @param handler - The handler type. - */ - constructor( - messenger: KeyringInternalSnapClientMessenger, - snapId: SnapId, - origin: string, - handler: HandlerType, - ) { - this.#messenger = messenger; - this.#snapId = snapId; - this.#origin = origin; - this.#handler = handler; - } - - /** - * Send a request to the Snap and return the response. - * - * @param request - JSON-RPC request to send to the Snap. - * @returns A promise that resolves to the response of the request. - */ - async send(request: JsonRpcRequest): Promise { - return this.#messenger.call('SnapController:handleRequest', { - snapId: this.#snapId, - origin: this.#origin, - handler: this.#handler, - request, - }) as Promise; - } -} diff --git a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts index 0bb8e5fbc..9dcda80a7 100644 --- a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts +++ b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.test.ts @@ -2,7 +2,7 @@ import { KeyringRpcV2Method, type KeyringAccount } from '@metamask/keyring-api'; import type { SnapId } from '@metamask/snaps-sdk'; import { KeyringInternalSnapClientV2 } from './KeyringInternalSnapClientV2'; -import type { KeyringInternalSnapClientMessenger } from '../KeyringInternalSnapClientMessenger'; +import type { KeyringInternalSnapClientMessenger } from '../KeyringInternalSnapClient'; const MOCK_ACCOUNT: KeyringAccount = { id: '13f94041-6ae6-451f-a0fe-afdd2fda18a7', diff --git a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts index 6ea934cfc..36112f078 100644 --- a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts +++ b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts @@ -2,8 +2,8 @@ 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 '../KeyringInternalSnapClientMessenger'; -import { SnapControllerMessengerSender } from '../SnapControllerMessengerSender'; +import type { KeyringInternalSnapClientMessenger } from '../KeyringInternalSnapClient'; +import { SnapControllerMessengerSender } from '../KeyringInternalSnapClient'; /** * A `KeyringClient` that allows the communication with a Snap through a From 273e431104b24be04787427aa0550e18bdbcfe1a Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Fri, 5 Dec 2025 15:04:28 +0100 Subject: [PATCH 15/25] fix: fix test --- packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts index 80475f019..dd763d705 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts @@ -236,7 +236,7 @@ describe('handleKeyringRequestV2', () => { 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 request', + 'An unknown error occurred while handling the keyring (v2) request', ); }); }); From b74cf0846a20045502aa294bfa7af180b261d5d6 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 8 Dec 2025 10:12:52 +0100 Subject: [PATCH 16/25] fix: forward options for exportAccount --- .../src/v2/rpc-handler.test.ts | 34 +++++++++++++++++-- .../keyring-snap-sdk/src/v2/rpc-handler.ts | 2 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts index dd763d705..7fb6cec4b 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts @@ -1,4 +1,4 @@ -import { KeyringRpcV2Method } from '@metamask/keyring-api'; +import { KeyringRpcV2Method, PrivateKeyEncoding } from '@metamask/keyring-api'; import type { KeyringType, CreateAccountsV2Request, @@ -146,7 +146,7 @@ describe('handleKeyringRequestV2', () => { expect(result).toBe(mockedResult); }); - it('calls `keyring_v2_exportAccount`', async () => { + it('calls `keyring_v2_exportAccount` (without options)', async () => { const request: ExportAccountV2Request = { jsonrpc: '2.0', id: '7c507ff0-365f-4de0-8cd5-eb83c30ebda4', @@ -161,7 +161,35 @@ describe('handleKeyringRequestV2', () => { const result = await handleKeyringRequestV2(keyring, request); expect(keyring.exportAccount).toHaveBeenCalledWith( - '4f983fa2-4f53-4c63-a7c2-f9a5ed750041', + 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); }); diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts index 96cf9d36f..111af7b55 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts @@ -64,7 +64,7 @@ async function dispatchKeyringRequestV2( throw new MethodNotSupportedError(request.method); } assert(request, ExportAccountV2RequestStruct); - return keyring.exportAccount(request.params.id); + return keyring.exportAccount(request.params.id, request.params.options); } case `${KeyringRpcV2Method.SubmitRequest}`: { From f83d807a38e8925f16c3fc51b1c0e90ba98fd29f Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 8 Dec 2025 10:13:52 +0100 Subject: [PATCH 17/25] chore: be more DRY --- packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts index 7fb6cec4b..d62afe672 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts @@ -75,9 +75,7 @@ describe('handleKeyringRequestV2', () => { keyring.getAccount.mockResolvedValue(mockedResult); const result = await handleKeyringRequestV2(keyring, request); - expect(keyring.getAccount).toHaveBeenCalledWith( - '4f983fa2-4f53-4c63-a7c2-f9a5ed750041', - ); + expect(keyring.getAccount).toHaveBeenCalledWith(request.params.id); expect(result).toBe(mockedResult); }); @@ -140,9 +138,7 @@ describe('handleKeyringRequestV2', () => { keyring.deleteAccount.mockResolvedValue(mockedResult); const result = await handleKeyringRequestV2(keyring, request); - expect(keyring.deleteAccount).toHaveBeenCalledWith( - '4f983fa2-4f53-4c63-a7c2-f9a5ed750041', - ); + expect(keyring.deleteAccount).toHaveBeenCalledWith(request.params.id); expect(result).toBe(mockedResult); }); From 7de9e3e4f45f8269b8b31a235e4044ca1e62d0df Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 8 Dec 2025 10:32:37 +0100 Subject: [PATCH 18/25] test: fix test titles --- packages/keyring-api/src/api/v2/keyring-rpc.test.ts | 2 +- packages/keyring-api/src/rpc.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.test.ts b/packages/keyring-api/src/api/v2/keyring-rpc.test.ts index f98c2c27b..bac57e900 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.test.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.test.ts @@ -2,7 +2,7 @@ import { KeyringRpcV2Method, isKeyringRpcV2Method } from './keyring-rpc'; describe('isKeyringRpcV2Method', () => { it.each(Object.values(KeyringRpcV2Method))( - 'returns true for: KeyringRpcV2Method.$s', + 'returns true for: "%s"', (method) => { expect(isKeyringRpcV2Method(method)).toBe(true); }, 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); }, From 383dfefe99aa4355d7f8c3c1e302c4221d66159b Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 8 Dec 2025 10:54:02 +0100 Subject: [PATCH 19/25] fix: re-use MethodNotSupportedError --- packages/keyring-snap-sdk/src/v2/rpc-handler.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts index 111af7b55..e4d9a8d4d 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.ts @@ -13,14 +13,7 @@ import { JsonRpcRequestStruct } from '@metamask/keyring-utils'; import { assert } from '@metamask/superstruct'; import type { Json } from '@metamask/utils'; -/** - * Error thrown when a keyring JSON-RPC method is not supported. - */ -export class MethodNotSupportedError extends Error { - constructor(method: string) { - super(`Method not supported: ${method}`); - } -} +import { MethodNotSupportedError } from '../rpc-handler'; /** * Inner function that dispatches JSON-RPC request to the associated Keyring From ccd47f69059927d51f18834ec6a52034b675bfd4 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 8 Dec 2025 10:55:51 +0100 Subject: [PATCH 20/25] chore: add missing index.ts --- packages/keyring-snap-sdk/src/index.ts | 1 + packages/keyring-snap-sdk/src/v2/index.ts | 1 + 2 files changed, 2 insertions(+) create mode 100644 packages/keyring-snap-sdk/src/v2/index.ts 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'; From 167f0f8b599e1363266b7a4212fd91d5a03f28a2 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 8 Dec 2025 16:02:19 +0100 Subject: [PATCH 21/25] chore: typo Co-authored-by: Mathieu Artu --- .../src/v2/KeyringInternalSnapClientV2.ts | 6 +++--- packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts index 36112f078..43f0f0294 100644 --- a/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts +++ b/packages/keyring-internal-snap-client/src/v2/KeyringInternalSnapClientV2.ts @@ -13,7 +13,7 @@ export class KeyringInternalSnapClientV2 extends KeyringClientV2 { readonly #messenger: KeyringInternalSnapClientMessenger; /** - * Create a new instance of `KeyringInternalSnapClient`. + * 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 @@ -43,11 +43,11 @@ export class KeyringInternalSnapClientV2 extends KeyringClientV2 { } /** - * Create a new instance of `KeyringInternalSnapClient` with the specified + * 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 `KeyringInternalSnapClient` with the + * @returns A new instance of `KeyringInternalSnapClientV2` with the * specified Snap ID. */ withSnapId(snapId: SnapId): KeyringInternalSnapClientV2 { diff --git a/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts index 593eea936..50b38f191 100644 --- a/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts +++ b/packages/keyring-snap-client/src/v2/KeyringClientV2.test.ts @@ -8,7 +8,7 @@ import type { Json } from '@metamask/utils'; import { KeyringClientV2 } from './KeyringClientV2'; -describe('KeyringClient', () => { +describe('KeyringClientV2', () => { const mockSender = { send: jest.fn(), }; From 0b752385b112405a73cc597139369b5de3f6c8b6 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 8 Dec 2025 16:01:40 +0100 Subject: [PATCH 22/25] fix: make exportAccount optional --- packages/keyring-api/src/api/v2/keyring-rpc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts index d4aa11e2c..7856d68a2 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -21,8 +21,8 @@ export type KeyringRpcV2 = { getAccount: KeyringV2['getAccount']; createAccounts: KeyringV2['createAccounts']; deleteAccount: KeyringV2['deleteAccount']; - exportAccount: KeyringV2['exportAccount']; submitRequest: KeyringV2['submitRequest']; + exportAccount?: KeyringV2['exportAccount']; }; /** From 533f9cfe7e369326798372f567264de7826299f0 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 10 Dec 2025 15:57:49 +0100 Subject: [PATCH 23/25] test: better deleteAccount test --- packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts index d62afe672..a2a640968 100644 --- a/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts +++ b/packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts @@ -134,12 +134,10 @@ describe('handleKeyringRequestV2', () => { params: { id: '4f983fa2-4f53-4c63-a7c2-f9a5ed750041' }, }; - const mockedResult = 'DeleteAccount result'; - keyring.deleteAccount.mockResolvedValue(mockedResult); - const result = await handleKeyringRequestV2(keyring, request); + keyring.deleteAccount.mockResolvedValue(undefined); + await handleKeyringRequestV2(keyring, request); expect(keyring.deleteAccount).toHaveBeenCalledWith(request.params.id); - expect(result).toBe(mockedResult); }); it('calls `keyring_v2_exportAccount` (without options)', async () => { From 47cae5af1392aea5b2d9c387e91047c699014268 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 16 Dec 2025 19:30:53 +0100 Subject: [PATCH 24/25] refactor: use .v2 suffix --- .../keyring-api/src/api/v2/keyring-rpc.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts index 7856d68a2..7bc1f7749 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -1,7 +1,7 @@ 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 { JsonRpcRequest, JsonStruct } from '@metamask/utils'; import { CreateAccountOptionsStruct } from './create-account'; import { @@ -9,6 +9,7 @@ import { PrivateKeyExportedAccountStruct, } from './export-account'; import type { KeyringV2 } from './keyring'; +import { KeyringRpcMethod } from '../../rpc'; import { KeyringAccountStruct } from '../account'; import { KeyringRequestStruct } from '../request'; @@ -17,24 +18,30 @@ import { KeyringRequestStruct } from '../request'; * RPC calls. */ export type KeyringRpcV2 = { - getAccounts: KeyringV2['getAccounts']; getAccount: KeyringV2['getAccount']; + getAccounts: KeyringV2['getAccounts']; createAccounts: KeyringV2['createAccounts']; deleteAccount: KeyringV2['deleteAccount']; submitRequest: KeyringV2['submitRequest']; exportAccount?: KeyringV2['exportAccount']; }; +/** + * Keyring RPC version identifier. + */ +export const KEYRING_RPC_V2 = 'v2'; +const V2 = KEYRING_RPC_V2; // Local alias for easier usage. + /** * Keyring RPC methods used by the API. */ export enum KeyringRpcV2Method { - GetAccounts = 'keyring_v2_getAccounts', - GetAccount = 'keyring_v2_getAccount', - CreateAccounts = 'keyring_v2_createAccounts', - DeleteAccount = 'keyring_v2_deleteAccount', - ExportAccount = 'keyring_v2_exportAccount', - SubmitRequest = 'keyring_v2_submitRequest', + GetAccount = `${KeyringRpcMethod.GetAccount}.${V2}`, + GetAccounts = `keyring_getAccounts.${V2}`, + CreateAccounts = `keyring_createAccounts.${V2}`, + DeleteAccount = `${KeyringRpcMethod.DeleteAccount}.${V2}`, + ExportAccount = `${KeyringRpcMethod.ExportAccount}.${V2}`, + SubmitRequest = `${KeyringRpcMethod.SubmitRequest}.${V2}`, } /** From f14272183624f6b194ff3a9c76e93b8db6fecd10 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Mon, 5 Jan 2026 15:28:51 +0100 Subject: [PATCH 25/25] chore: fix lint --- packages/keyring-api/src/api/v2/keyring-rpc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-api/src/api/v2/keyring-rpc.ts b/packages/keyring-api/src/api/v2/keyring-rpc.ts index 7bc1f7749..cee69b23f 100644 --- a/packages/keyring-api/src/api/v2/keyring-rpc.ts +++ b/packages/keyring-api/src/api/v2/keyring-rpc.ts @@ -1,7 +1,7 @@ import { object, exactOptional, UuidStruct } from '@metamask/keyring-utils'; import type { Infer } from '@metamask/superstruct'; import { array, literal, number, string, union } from '@metamask/superstruct'; -import { JsonRpcRequest, JsonStruct } from '@metamask/utils'; +import { JsonStruct } from '@metamask/utils'; import { CreateAccountOptionsStruct } from './create-account'; import {