Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add rpc-handler for v2
  • Loading branch information
ccharly committed Dec 4, 2025
commit 058543e78d7cec9d4435a9e13ffbe1c8612655c6
242 changes: 242 additions & 0 deletions packages/keyring-snap-sdk/src/v2/rpc-handler.test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});
});
115 changes: 115 additions & 0 deletions packages/keyring-snap-sdk/src/v2/rpc-handler.ts
Original file line number Diff line number Diff line change
@@ -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<Json | void> {
// 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<Json | void> {
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);
}
}