Skip to content

Commit

Permalink
Add basic support for account RPC methods in snaps simulator (#1710)
Browse files Browse the repository at this point in the history
* Support account RPC methods in snaps simulator

* Update packages/snaps-simulator/src/features/simulation/middleware.ts

Co-authored-by: Maarten Zuidhoorn <[email protected]>

* Refactor getAccountsHandler

* Add E2E

* Add middleware test

---------

Co-authored-by: Maarten Zuidhoorn <[email protected]>
  • Loading branch information
FrederikBolding and Mrtenz authored Aug 30, 2023
1 parent 9677c20 commit be83c45
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 9 deletions.
17 changes: 17 additions & 0 deletions packages/examples/packages/ethereum-provider/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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'],
});
});
});
55 changes: 49 additions & 6 deletions packages/snaps-simulator/src/features/simulation/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BIP44Node } from '@metamask/key-tree';
import { logError } from '@metamask/snaps-utils';
import type {
JsonRpcEngineEndCallback,
Expand All @@ -7,10 +8,50 @@ 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,
eth_requestAccounts: getAccountsHandler,
eth_accounts: getAccountsHandler,
};
/* eslint-enable @typescript-eslint/naming-convention */

export type MiscMiddlewareHooks = {
getMnemonic: () => Promise<Uint8Array>;
};

/**
* 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<unknown>,
response: PendingJsonRpcResponse<unknown>,
_next: JsonRpcEngineNextCallback,
end: JsonRpcEngineEndCallback,
hooks: MiscMiddlewareHooks,
) {
const { getMnemonic } = hooks;

const node = await BIP44Node.fromDerivationPath({
derivationPath: [
await getMnemonic(),
`bip32:44'`,
`bip32:60'`,
`bip32:0'`,
`bip32:0`,
`bip32:0`,
],
});

response.result = [node.address];
return end();
}

/**
* A mock handler for metamask_getProviderState that always returns a specific hardcoded result.
Expand Down Expand Up @@ -39,12 +80,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<unknown, unknown> {
// This should probably use createAsyncMiddleware
// eslint-disable-next-line @typescript-eslint/no-misused-promises
return async function methodMiddleware(request, response, next, end) {
Expand All @@ -53,7 +96,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);
Expand Down
8 changes: 6 additions & 2 deletions packages/snaps-simulator/src/features/simulation/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,20 @@ export function* initSaga({ payload }: PayloadAction<string>) {

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<typeof showDialog>) =>
await runSaga(showDialog, ...args).toPromise(),
showNativeNotification: async (
Expand Down Expand Up @@ -137,7 +141,7 @@ export function* initSaga({ payload }: PayloadAction<string>) {

const engine = new JsonRpcEngine();

engine.push(createMiscMethodMiddleware());
engine.push(createMiscMethodMiddleware(sharedHooks));

engine.push(
permissionController.createPermissionMiddleware({
Expand Down

0 comments on commit be83c45

Please sign in to comment.