From a2805e045ab57b94eb30cc675632cd780bd67b73 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 30 Aug 2023 10:42:46 +0200 Subject: [PATCH 1/5] Support account RPC methods in snaps simulator --- .../src/features/simulation/middleware.ts | 56 +++++++++++++++++-- .../src/features/simulation/sagas.ts | 8 ++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/snaps-simulator/src/features/simulation/middleware.ts b/packages/snaps-simulator/src/features/simulation/middleware.ts index 219dad762c..356c053dc5 100644 --- a/packages/snaps-simulator/src/features/simulation/middleware.ts +++ b/packages/snaps-simulator/src/features/simulation/middleware.ts @@ -1,3 +1,7 @@ +import { + BIP44CoinTypeNode, + getBIP44AddressKeyDeriver, +} from '@metamask/key-tree'; import { logError } from '@metamask/snaps-utils'; import type { JsonRpcEngineEndCallback, @@ -10,8 +14,48 @@ import type { export const methodHandlers = { // eslint-disable-next-line @typescript-eslint/naming-convention metamask_getProviderState: getProviderStateHandler, + // eslint-disable-next-line @typescript-eslint/naming-convention + eth_requestAccounts: getAccountsHandler, + // eslint-disable-next-line @typescript-eslint/naming-convention + eth_accounts: getAccountsHandler, +}; + +export type MiscMiddlewareHooks = { + getMnemonic: () => Promise; }; +/** + * A mock handler for account related methods that always returns the first address for the selected SRP. + * + * @param _request - Incoming JSON-RPC request, ignored for this specific handler. + * @param response - The outgoing JSON-RPC response, modified to return the result. + * @param _next - The json-rpc-engine middleware next handler. + * @param end - The json-rpc-engine middleware end handler. + * @param hooks - Any hooks required by this handler. + */ +async function getAccountsHandler( + _request: JsonRpcRequest, + response: PendingJsonRpcResponse, + _next: JsonRpcEngineNextCallback, + end: JsonRpcEngineEndCallback, + hooks: MiscMiddlewareHooks, +) { + const { getMnemonic } = hooks; + + const node = await BIP44CoinTypeNode.fromDerivationPath([ + await getMnemonic(), + `bip32:44'`, + `bip32:60'`, + ]); + + const deriveAddress = await getBIP44AddressKeyDeriver(node); + + const { address } = await deriveAddress(0); + + response.result = [address]; + return end(); +} + /** * A mock handler for metamask_getProviderState that always returns a specific hardcoded result. * @@ -39,12 +83,14 @@ async function getProviderStateHandler( /** * Creates a middleware for handling misc RPC methods normally handled internally by the MM client. * + * NOTE: This middleware provides all `hooks` to all handlers and should therefore NOT be used outside of the simulator. + * + * @param hooks - Any hooks used by the middleware handlers. * @returns Nothing. */ -export function createMiscMethodMiddleware(): JsonRpcMiddleware< - unknown, - unknown -> { +export function createMiscMethodMiddleware( + hooks: MiscMiddlewareHooks, +): JsonRpcMiddleware { // This should probably use createAsyncMiddleware // eslint-disable-next-line @typescript-eslint/no-misused-promises return async function methodMiddleware(request, response, next, end) { @@ -53,7 +99,7 @@ export function createMiscMethodMiddleware(): JsonRpcMiddleware< if (handler) { try { // Implementations may or may not be async, so we must await them. - return await handler(request, response, next, end); + return await handler(request, response, next, end, hooks); } catch (error: any) { logError(error); return end(error); diff --git a/packages/snaps-simulator/src/features/simulation/sagas.ts b/packages/snaps-simulator/src/features/simulation/sagas.ts index 0acf1ca7dd..e68b9c4b24 100644 --- a/packages/snaps-simulator/src/features/simulation/sagas.ts +++ b/packages/snaps-simulator/src/features/simulation/sagas.ts @@ -80,16 +80,20 @@ export function* initSaga({ payload }: PayloadAction) { const srp: string = yield select(getSrp); + const sharedHooks = { + getMnemonic: async () => mnemonicPhraseToBytes(srp), + }; + const permissionSpecifications = { ...buildSnapEndowmentSpecifications(Object.keys(ExcludedSnapEndowments)), ...buildSnapRestrictedMethodSpecifications([], { + ...sharedHooks, // TODO: Add all the hooks required encrypt, decrypt, // TODO: Allow changing this? getLocale: async () => Promise.resolve('en'), getUnlockPromise: async () => Promise.resolve(true), - getMnemonic: async () => mnemonicPhraseToBytes(srp), showDialog: async (...args: Parameters) => await runSaga(showDialog, ...args).toPromise(), showNativeNotification: async ( @@ -137,7 +141,7 @@ export function* initSaga({ payload }: PayloadAction) { const engine = new JsonRpcEngine(); - engine.push(createMiscMethodMiddleware()); + engine.push(createMiscMethodMiddleware(sharedHooks)); engine.push( permissionController.createPermissionMiddleware({ From 6abeef9c29bb5e8130271518e9631be8c1ae1a80 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 30 Aug 2023 11:03:05 +0200 Subject: [PATCH 2/5] Update packages/snaps-simulator/src/features/simulation/middleware.ts Co-authored-by: Maarten Zuidhoorn --- .../snaps-simulator/src/features/simulation/middleware.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/snaps-simulator/src/features/simulation/middleware.ts b/packages/snaps-simulator/src/features/simulation/middleware.ts index 356c053dc5..3606928187 100644 --- a/packages/snaps-simulator/src/features/simulation/middleware.ts +++ b/packages/snaps-simulator/src/features/simulation/middleware.ts @@ -11,14 +11,13 @@ import type { PendingJsonRpcResponse, } from 'json-rpc-engine'; +/* eslint-disable @typescript-eslint/naming-convention */ export const methodHandlers = { - // eslint-disable-next-line @typescript-eslint/naming-convention metamask_getProviderState: getProviderStateHandler, - // eslint-disable-next-line @typescript-eslint/naming-convention eth_requestAccounts: getAccountsHandler, - // eslint-disable-next-line @typescript-eslint/naming-convention eth_accounts: getAccountsHandler, }; +/* eslint-enable @typescript-eslint/naming-convention */ export type MiscMiddlewareHooks = { getMnemonic: () => Promise; From 4ae1394b992e3cd7efac29ef09bb15e07e35c66f Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 30 Aug 2023 11:05:10 +0200 Subject: [PATCH 3/5] Refactor getAccountsHandler --- .../src/features/simulation/middleware.ts | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/snaps-simulator/src/features/simulation/middleware.ts b/packages/snaps-simulator/src/features/simulation/middleware.ts index 3606928187..0b6ff69efa 100644 --- a/packages/snaps-simulator/src/features/simulation/middleware.ts +++ b/packages/snaps-simulator/src/features/simulation/middleware.ts @@ -1,7 +1,4 @@ -import { - BIP44CoinTypeNode, - getBIP44AddressKeyDeriver, -} from '@metamask/key-tree'; +import { BIP44Node } from '@metamask/key-tree'; import { logError } from '@metamask/snaps-utils'; import type { JsonRpcEngineEndCallback, @@ -41,17 +38,18 @@ async function getAccountsHandler( ) { const { getMnemonic } = hooks; - const node = await BIP44CoinTypeNode.fromDerivationPath([ - await getMnemonic(), - `bip32:44'`, - `bip32:60'`, - ]); + const node = await BIP44Node.fromDerivationPath({ + derivationPath: [ + await getMnemonic(), + `bip32:44'`, + `bip32:60'`, + `bip32:0'`, + `bip32:0`, + `bip32:0`, + ], + }); - const deriveAddress = await getBIP44AddressKeyDeriver(node); - - const { address } = await deriveAddress(0); - - response.result = [address]; + response.result = [node.address]; return end(); } From bb4049cdee4efe9f95cb0e3ebd2ae3351f6337a2 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 30 Aug 2023 11:08:54 +0200 Subject: [PATCH 4/5] Add E2E --- .../ethereum-provider/src/index.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/examples/packages/ethereum-provider/src/index.test.ts b/packages/examples/packages/ethereum-provider/src/index.test.ts index e729884692..a73ddb1adf 100644 --- a/packages/examples/packages/ethereum-provider/src/index.test.ts +++ b/packages/examples/packages/ethereum-provider/src/index.test.ts @@ -68,4 +68,21 @@ describe('onRpcRequest', () => { await close(); }); }); + + describe('getAccounts', () => { + it('returns the addresses granted access to by the user', async () => { + const { request, close } = await installSnap(); + + const response = await request({ + method: 'getAccounts', + }); + + // Currently, snaps-jest will always return this account. + expect(response).toRespondWith([ + '0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf', + ]); + + await close(); + }); + }); }); From 261f8e2316ab3857dc3ee4dd71027d7a3369b028 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 30 Aug 2023 11:24:33 +0200 Subject: [PATCH 5/5] Add middleware test --- .../features/simulation/middleware.test.ts | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/packages/snaps-simulator/src/features/simulation/middleware.test.ts b/packages/snaps-simulator/src/features/simulation/middleware.test.ts index 893caa08a0..3e3e1078ab 100644 --- a/packages/snaps-simulator/src/features/simulation/middleware.test.ts +++ b/packages/snaps-simulator/src/features/simulation/middleware.test.ts @@ -1,11 +1,15 @@ +import { mnemonicPhraseToBytes } from '@metamask/key-tree'; import { JsonRpcEngine } from 'json-rpc-engine'; +import { DEFAULT_SRP } from '../configuration'; import { createMiscMethodMiddleware } from './middleware'; +const hooks = { getMnemonic: async () => mnemonicPhraseToBytes(DEFAULT_SRP) }; + describe('createMiscMethodMiddleware', () => { it('supports metamask_getProviderState', async () => { const engine = new JsonRpcEngine(); - engine.push(createMiscMethodMiddleware()); + engine.push(createMiscMethodMiddleware(hooks)); const response = await engine.handle({ jsonrpc: '2.0', @@ -25,4 +29,40 @@ describe('createMiscMethodMiddleware', () => { }, }); }); + + it('supports eth_accounts', async () => { + const engine = new JsonRpcEngine(); + engine.push(createMiscMethodMiddleware(hooks)); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'eth_accounts', + params: [], + }); + + expect(response).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: ['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf'], + }); + }); + + it('supports eth_requestAccounts', async () => { + const engine = new JsonRpcEngine(); + engine.push(createMiscMethodMiddleware(hooks)); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'eth_requestAccounts', + params: [], + }); + + expect(response).toStrictEqual({ + id: 1, + jsonrpc: '2.0', + result: ['0xc6d5a3c98ec9073b54fa0969957bd582e8d874bf'], + }); + }); });