generated from MetaMask/metamask-module-template
-
-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add KeyringClientV2 support
#408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ccharly
wants to merge
23
commits into
main
Choose a base branch
from
feat/keyring-client-v2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
5622413
feat: add KeyringClientV2 support
ccharly 0ed4812
fix: remove unused function
ccharly e59cd8f
fix: remove unused eslint directive
ccharly f52e5b6
fix: fix createAccounts params
ccharly 085b7f9
fix: properly name request structs
ccharly 058543e
feat: add rpc-handler for v2
ccharly 45e3ed5
feat: better typing for isKeyringRpcMethod
ccharly 84b6e95
feat: add isKeyringRpcV2Method
ccharly 6b260f0
feat: fix createAccounts for client v2
ccharly fac383e
chore: lint
ccharly 0d94203
fix: fix jsdocs
ccharly 4b72e8b
feat: add KeyringInternalSnapClientV2 + missing v2 exports
ccharly 2ae5c15
fix: add missing code
ccharly 7f68ec4
refactor: revert class split
ccharly 273e431
fix: fix test
ccharly b74cf08
fix: forward options for exportAccount
ccharly f83d807
chore: be more DRY
ccharly 7de9e3e
test: fix test titles
ccharly 383dfef
fix: re-use MethodNotSupportedError
ccharly ccd47f6
chore: add missing index.ts
ccharly 167f0f8
chore: typo
ccharly 0b75238
fix: make exportAccount optional
ccharly 533f9cf
test: better deleteAccount test
ccharly File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: add rpc-handler for v2
- Loading branch information
commit 058543e78d7cec9d4435a9e13ffbe1c8612655c6
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
| ); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
cursor[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.