diff --git a/packages/examples/packages/get-file/snap.manifest.json b/packages/examples/packages/get-file/snap.manifest.json index 72a905c790..dc4693321d 100644 --- a/packages/examples/packages/get-file/snap.manifest.json +++ b/packages/examples/packages/get-file/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "YEIejIfr+8rWTP0dSPc4HcLO1JwM/QQCwA/BItw5goE=", + "shasum": "HwhCRDjZxqIRT9rbHCNT11CoQ/YS8tO7ZFXrjxllpZ0=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/examples/packages/get-file/src/index.ts b/packages/examples/packages/get-file/src/index.ts index c405fc871d..5ff6f50ae4 100644 --- a/packages/examples/packages/get-file/src/index.ts +++ b/packages/examples/packages/get-file/src/index.ts @@ -1,5 +1,6 @@ import { MethodNotFoundError, + assert, type OnRpcRequestHandler, } from '@metamask/snaps-sdk'; @@ -28,6 +29,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { method: 'snap_getFile', params: { path: './files/foo.json', encoding: 'utf8' }, }); + assert(fileInPlaintext); return JSON.parse(fileInPlaintext); } diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 7756db68a4..8f39566dfd 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 97.29, - functions: 98.85, - lines: 99.14, - statements: 98.81, + branches: 97.28, + functions: 98.88, + lines: 99.19, + statements: 98.91, }, }, }); diff --git a/packages/snaps-rpc-methods/scripts/generate-schema.mts b/packages/snaps-rpc-methods/scripts/generate-schema.mts index 2b5f6ec3b3..f5f21a590a 100644 --- a/packages/snaps-rpc-methods/scripts/generate-schema.mts +++ b/packages/snaps-rpc-methods/scripts/generate-schema.mts @@ -1294,7 +1294,7 @@ async function processPermittedHandler( } /** - * Process the permitted handlers defined in `src/permitted/handlers.ts`, + * Process the permitted handlers defined in `src/permitted/middleware.ts`, * extracting the method names, descriptions, parameters, return types, and * subject types for each handler. * @@ -1303,7 +1303,7 @@ async function processPermittedHandler( * @returns An array of method schemas extracted from the permitted handlers. */ async function processPermittedHandlers(project: Project) { - const handlersFile = project.getSourceFile('src/permitted/handlers.ts'); + const handlersFile = project.getSourceFile('src/permitted/middleware.ts'); assert(handlersFile, 'Handlers file not found.'); const permittedHandlers = diff --git a/packages/snaps-rpc-methods/src/index.ts b/packages/snaps-rpc-methods/src/index.ts index ba9bd4cbb5..ce913d2608 100644 --- a/packages/snaps-rpc-methods/src/index.ts +++ b/packages/snaps-rpc-methods/src/index.ts @@ -1,10 +1,6 @@ -export { - handlers as permittedMethods, - createSnapsMethodMiddleware, -} from './permitted'; +export { createSnapsMethodMiddleware } from './permitted'; export type { PermittedRpcMethodHooks } from './permitted'; export { SnapCaveatType } from '@metamask/snaps-utils'; -export { selectHooks } from './utils'; export * from './endowments'; export * from './middleware'; export * from './permissions'; diff --git a/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.test.ts b/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.test.ts index bb5e382c7a..3ff262cb69 100644 --- a/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.test.ts @@ -1,55 +1,74 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { CancelBackgroundEventParams, CancelBackgroundEventResult, + SnapId, } from '@metamask/snaps-sdk'; -import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { CancelBackgroundEventMethodActions } from './cancelBackgroundEvent'; import { cancelBackgroundEventHandler } from './cancelBackgroundEvent'; describe('snap_cancelBackgroundEvent', () => { describe('cancelBackgroundEventHandler', () => { it('has the expected shape', () => { expect(cancelBackgroundEventHandler).toMatchObject({ - methodNames: ['snap_cancelBackgroundEvent'], implementation: expect.any(Function), - hookNames: { - cancelBackgroundEvent: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'CronjobController:cancel', + ], }); }); }); describe('implementation', () => { - const createOriginMiddleware = - (origin: string) => - (request: any, _response: unknown, next: () => void, _end: unknown) => { - request.origin = origin; - next(); - }; - - it('returns null after calling the `cancelBackgroundEvent` hook', async () => { - const { implementation } = cancelBackgroundEventHandler; + const getMessenger = () => { + const messenger = new MockControllerMessenger< + CancelBackgroundEventMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); - const cancelBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); + messenger.registerActionHandler( + 'CronjobController:cancel', + () => undefined, + ); - const hooks = { - cancelBackgroundEvent, - hasPermission, - }; + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('returns null after calling the `CronjobController:cancel` action', async () => { + const { implementation } = cancelBackgroundEventHandler; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -70,24 +89,21 @@ describe('snap_cancelBackgroundEvent', () => { it('cancels a background event', async () => { const { implementation } = cancelBackgroundEventHandler; - const cancelBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - cancelBackgroundEvent, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -102,30 +118,36 @@ describe('snap_cancelBackgroundEvent', () => { }, }); - expect(cancelBackgroundEvent).toHaveBeenCalledWith('foo'); + expect(messenger.call).toHaveBeenCalledWith( + 'CronjobController:cancel', + MOCK_SNAP_ID, + 'foo', + ); }); it('throws if a snap does not have the "endowment:cronjob" permission', async () => { const { implementation } = cancelBackgroundEventHandler; - const cancelBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => false); + const messenger = getMessenger(); - const hooks = { - cancelBackgroundEvent, - hasPermission, - }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -155,24 +177,21 @@ describe('snap_cancelBackgroundEvent', () => { it('throws on invalid params', async () => { const { implementation } = cancelBackgroundEventHandler; - const cancelBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - cancelBackgroundEvent, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.ts b/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.ts index a6b4c69f58..6f2ea09a88 100644 --- a/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.ts +++ b/packages/snaps-rpc-methods/src/permitted/cancelBackgroundEvent.ts @@ -1,29 +1,28 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, CancelBackgroundEventParams, CancelBackgroundEventResult, + SnapId, } from '@metamask/snaps-sdk'; import { type InferMatching } from '@metamask/snaps-utils'; import { StructError, create, object, string } from '@metamask/superstruct'; import { type PendingJsonRpcResponse } from '@metamask/utils'; import { SnapEndowments } from '../endowments'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; - -const methodName = 'snap_cancelBackgroundEvent'; - -const hookNames: MethodHooksObject = { - cancelBackgroundEvent: true, - hasPermission: true, -}; +import type { + CronjobControllerCancelAction, + JsonRpcRequestWithOrigin, +} from '../types'; -export type CancelBackgroundEventMethodHooks = { - cancelBackgroundEvent: (id: string) => void; - hasPermission: (permissionName: string) => boolean; -}; +export type CancelBackgroundEventMethodActions = + | PermissionControllerHasPermissionAction + | CronjobControllerCancelAction; /** * Cancel a background event created by @@ -46,13 +45,17 @@ export type CancelBackgroundEventMethodHooks = { * ``` */ export const cancelBackgroundEventHandler = { - methodNames: [methodName] as const, implementation: getCancelBackgroundEventImplementation, - hookNames, -} satisfies PermittedHandlerExport< - CancelBackgroundEventMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'CronjobController:cancel', + ], +} satisfies MethodHandler< + never, + CancelBackgroundEventMethodActions, CancelBackgroundEventParameters, - CancelBackgroundEventResult + CancelBackgroundEventResult, + { origin: SnapId } >; const CancelBackgroundEventsParametersStruct = object({ @@ -72,21 +75,27 @@ export type CancelBackgroundEventParameters = InferMatching< * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.cancelBackgroundEvent - The function to cancel a background event. - * @param hooks.hasPermission - The function to check if a snap has the `endowment:cronjob` permission. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function getCancelBackgroundEventImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { cancelBackgroundEvent, hasPermission }: CancelBackgroundEventMethodHooks, + _hooks: never, + messenger: Messenger, ): Promise { - const { params } = req; + const { params, origin } = req; - if (!hasPermission(SnapEndowments.Cronjob)) { + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + SnapEndowments.Cronjob, + ) + ) { return end(providerErrors.unauthorized()); } @@ -95,7 +104,7 @@ async function getCancelBackgroundEventImplementation( const { id } = validatedParams; - cancelBackgroundEvent(id); + messenger.call('CronjobController:cancel', origin, id); res.result = null; } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/clearState.test.ts b/packages/snaps-rpc-methods/src/permitted/clearState.test.ts index d3ca4987e2..1bda8ec63b 100644 --- a/packages/snaps-rpc-methods/src/permitted/clearState.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/clearState.test.ts @@ -1,46 +1,73 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { errorCodes } from '@metamask/rpc-errors'; import type { ClearStateResult } from '@metamask/snaps-sdk'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; - -import type { ClearStateParameters } from './clearState'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; + +import type { + ClearStateMethodActions, + ClearStateParameters, +} from './clearState'; import { clearStateHandler } from './clearState'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_clearState', () => { describe('clearStateHandler', () => { it('has the expected shape', () => { expect(clearStateHandler).toMatchObject({ - methodNames: ['snap_clearState'], implementation: expect.any(Function), - hookNames: { - clearSnapState: true, - hasPermission: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:clearSnapState', + ], }); }); }); describe('implementation', () => { - it('returns the result from the `clearSnapState` hook', async () => { - const { implementation } = clearStateHandler; + const getMessenger = () => { + const messenger = new MockControllerMessenger< + ClearStateMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapController:clearSnapState', + () => undefined, + ); - const clearSnapState = jest.fn().mockReturnValue(null); - const hasPermission = jest.fn().mockReturnValue(true); + jest.spyOn(messenger, 'call'); - const hooks = { - clearSnapState, - hasPermission, - }; + return messenger; + }; + + it('returns the result from the `clearSnapState` action', async () => { + const { implementation } = clearStateHandler; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -53,7 +80,11 @@ describe('snap_clearState', () => { params: {}, }); - expect(clearSnapState).toHaveBeenCalledWith(true); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:clearSnapState', + MOCK_SNAP_ID, + true, + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -64,23 +95,19 @@ describe('snap_clearState', () => { it('clears unencrypted state if specified', async () => { const { implementation } = clearStateHandler; - const clearSnapState = jest.fn().mockReturnValue(null); - const hasPermission = jest.fn().mockReturnValue(true); - - const hooks = { - clearSnapState, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -95,7 +122,11 @@ describe('snap_clearState', () => { }, }); - expect(clearSnapState).toHaveBeenCalledWith(false); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:clearSnapState', + MOCK_SNAP_ID, + false, + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -106,23 +137,24 @@ describe('snap_clearState', () => { it('throws if the requesting origin does not have the required permission', async () => { const { implementation } = clearStateHandler; - const clearSnapState = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(false); + const messenger = getMessenger(); - const hooks = { - clearSnapState, - hasPermission, - }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -135,7 +167,11 @@ describe('snap_clearState', () => { params: {}, }); - expect(clearSnapState).not.toHaveBeenCalled(); + expect(messenger.call).not.toHaveBeenCalledWith( + 'SnapController:clearSnapState', + expect.anything(), + expect.anything(), + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -151,23 +187,19 @@ describe('snap_clearState', () => { it('throws if the parameters are invalid', async () => { const { implementation } = clearStateHandler; - const clearSnapState = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(true); - - const hooks = { - clearSnapState, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/clearState.ts b/packages/snaps-rpc-methods/src/permitted/clearState.ts index 07790af8d7..505e3efb85 100644 --- a/packages/snaps-rpc-methods/src/permitted/clearState.ts +++ b/packages/snaps-rpc-methods/src/permitted/clearState.ts @@ -1,4 +1,9 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { ClearStateParams, ClearStateResult } from '@metamask/snaps-sdk'; import { type InferMatching } from '@metamask/snaps-utils'; @@ -9,18 +14,17 @@ import { optional, StructError, } from '@metamask/superstruct'; -import type { PendingJsonRpcResponse, JsonRpcRequest } from '@metamask/utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; import { manageStateBuilder } from '../restricted/manageState'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerClearSnapStateAction, +} from '../types'; -const methodName = 'snap_clearState'; - -const hookNames: MethodHooksObject = { - clearSnapState: true, - hasPermission: true, -}; +export type ClearStateMethodActions = + | PermissionControllerHasPermissionAction + | SnapControllerClearSnapStateAction; /** * Clear the entire state of the Snap. @@ -36,30 +40,19 @@ const hookNames: MethodHooksObject = { * ``` */ export const clearStateHandler = { - methodNames: [methodName] as const, implementation: clearStateImplementation, - hookNames, -} satisfies PermittedHandlerExport< - ClearStateHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:clearSnapState', + ], +} satisfies MethodHandler< + never, + ClearStateMethodActions, ClearStateParameters, - ClearStateResult + ClearStateResult, + { origin: string } >; -export type ClearStateHooks = { - /** - * A function that clears the state of the requesting Snap. - */ - clearSnapState: (encrypted: boolean) => void; - - /** - * Check if the requesting origin has a given permission. - * - * @param permissionName - The name of the permission to check. - * @returns Whether the origin has the permission. - */ - hasPermission: (permissionName: string) => boolean; -}; - const ClearStateParametersStruct = object({ encrypted: optional(boolean()), }); @@ -77,23 +70,27 @@ export type ClearStateParameters = InferMatching< * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.clearSnapState - A function that clears the state of the - * requesting Snap. - * @param hooks.hasPermission - Check whether a given origin has a given - * permission. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function clearStateImplementation( - request: JsonRpcRequest, + request: JsonRpcRequestWithOrigin, response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { clearSnapState, hasPermission }: ClearStateHooks, + _hooks: never, + messenger: Messenger, ): Promise { - const { params } = request; + const { params, origin } = request; - if (!hasPermission(manageStateBuilder.targetName)) { + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + manageStateBuilder.targetName, + ) + ) { return end(providerErrors.unauthorized()); } @@ -101,7 +98,7 @@ async function clearStateImplementation( const validatedParams = getValidatedParams(params); const { encrypted = true } = validatedParams; - clearSnapState(encrypted); + messenger.call('SnapController:clearSnapState', origin, encrypted); response.result = null; } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/closeWebSocket.test.ts b/packages/snaps-rpc-methods/src/permitted/closeWebSocket.test.ts index a8b6f018f4..648ed29609 100644 --- a/packages/snaps-rpc-methods/src/permitted/closeWebSocket.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/closeWebSocket.test.ts @@ -1,41 +1,78 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { CloseWebSocketResult } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; -import type { CloseWebSocketParameters } from './closeWebSocket'; +import type { + CloseWebSocketMethodActions, + CloseWebSocketParameters, +} from './closeWebSocket'; import { closeWebSocketHandler } from './closeWebSocket'; describe('snap_closeWebSocket', () => { describe('closeWebSocketHandler', () => { it('has the expected shape', () => { expect(closeWebSocketHandler).toMatchObject({ - methodNames: ['snap_closeWebSocket'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - closeWebSocket: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'WebSocketService:close', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + CloseWebSocketMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'WebSocketService:close', + () => undefined, + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission', async () => { const { implementation } = closeWebSocketHandler; - const closeWebSocket = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(false); - const hooks = { hasPermission, closeWebSocket }; + const messenger = getMessenger(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -63,19 +100,21 @@ describe('snap_closeWebSocket', () => { it('throws if invalid parameters are passed', async () => { const { implementation } = closeWebSocketHandler; - const closeWebSocket = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, closeWebSocket }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -103,19 +142,21 @@ describe('snap_closeWebSocket', () => { it('closes a WebSocket and returns null', async () => { const { implementation } = closeWebSocketHandler; - const closeWebSocket = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, closeWebSocket }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -129,7 +170,11 @@ describe('snap_closeWebSocket', () => { }); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: null }); - expect(closeWebSocket).toHaveBeenCalledWith('foo'); + expect(messenger.call).toHaveBeenCalledWith( + 'WebSocketService:close', + MOCK_SNAP_ID, + 'foo', + ); }); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/closeWebSocket.ts b/packages/snaps-rpc-methods/src/permitted/closeWebSocket.ts index b412dfba2c..f5f0e626f6 100644 --- a/packages/snaps-rpc-methods/src/permitted/closeWebSocket.ts +++ b/packages/snaps-rpc-methods/src/permitted/closeWebSocket.ts @@ -1,7 +1,11 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, CloseWebSocketParams, CloseWebSocketResult, } from '@metamask/snaps-sdk'; @@ -10,20 +14,14 @@ import { create, object, string, StructError } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; import { SnapEndowments } from '../endowments'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; - -const methodName = 'snap_closeWebSocket'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - closeWebSocket: true, -}; +import type { + JsonRpcRequestWithOrigin, + WebSocketServiceCloseAction, +} from '../types'; -export type CloseWebSocketMethodHooks = { - hasPermission: (permissionName: string) => boolean; - closeWebSocket: (id: string) => void; -}; +export type CloseWebSocketMethodActions = + | PermissionControllerHasPermissionAction + | WebSocketServiceCloseAction; const CloseWebSocketParametersStruct = object({ id: string(), @@ -54,13 +52,14 @@ export type CloseWebSocketParameters = InferMatching< * ``` */ export const closeWebSocketHandler = { - methodNames: [methodName] as const, implementation: closeWebSocketImplementation, - hookNames, -} satisfies PermittedHandlerExport< - CloseWebSocketMethodHooks, + actionNames: ['PermissionController:hasPermission', 'WebSocketService:close'], +} satisfies MethodHandler< + never, + CloseWebSocketMethodActions, CloseWebSocketParams, - CloseWebSocketResult + CloseWebSocketResult, + { origin: string } >; /** @@ -70,27 +69,33 @@ export const closeWebSocketHandler = { * @param res - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. Not used by this function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if a snap has the `endowment:network-access` permission. - * @param hooks.closeWebSocket - The function to close a WebSocket. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function closeWebSocketImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, closeWebSocket }: CloseWebSocketMethodHooks, + _hooks: never, + messenger: Messenger, ): void { - if (!hasPermission(SnapEndowments.NetworkAccess)) { + const { params, origin } = req; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + SnapEndowments.NetworkAccess, + ) + ) { return end(providerErrors.unauthorized()); } - const { params } = req; - try { const { id } = getValidatedParams(params); - closeWebSocket(id); + messenger.call('WebSocketService:close', origin, id); res.result = null; } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx index bac78f688d..a4fce189d5 100644 --- a/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx +++ b/packages/snaps-rpc-methods/src/permitted/createInterface.test.tsx @@ -1,4 +1,7 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { NodeType, type CreateInterfaceResult } from '@metamask/snaps-sdk'; import type { JSXElement } from '@metamask/snaps-sdk/jsx'; import { @@ -11,43 +14,76 @@ import { Footer, Copyable, } from '@metamask/snaps-sdk/jsx'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; -import type { CreateInterfaceParameters } from './createInterface'; +import type { + CreateInterfaceMethodActions, + CreateInterfaceParameters, +} from './createInterface'; import { createInterfaceHandler } from './createInterface'; describe('snap_createInterface', () => { describe('createInterfaceHandler', () => { it('has the expected shape', () => { expect(createInterfaceHandler).toMatchObject({ - methodNames: ['snap_createInterface'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - createInterface: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:createInterface', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + CreateInterfaceMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapInterfaceController:createInterface', + () => 'foo', + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission to show UI', async () => { const { implementation } = createInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(false); - const createInterface = jest.fn().mockReturnValue('foo'); + const messenger = getMessenger(); - const hooks = { hasPermission, createInterface }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -78,26 +114,24 @@ describe('snap_createInterface', () => { }); }); - it('returns the result from the `createInterface` hook', async () => { + it('returns the result from the `SnapInterfaceController:createInterface` action', async () => { const { implementation } = createInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const createInterface = jest.fn().mockReturnValue('foo'); - - const hooks = { - hasPermission, - createInterface, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -118,23 +152,21 @@ describe('snap_createInterface', () => { it('creates an interface from a JSX element', async () => { const { implementation } = createInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const createInterface = jest.fn().mockReturnValue('foo'); - - const hooks = { - hasPermission, - createInterface, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -155,162 +187,158 @@ describe('snap_createInterface', () => { expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: 'foo' }); }); - }); - it('throws on invalid params', async () => { - const { implementation } = createInterfaceHandler; - - const hasPermission = jest.fn().mockReturnValue(true); - const createInterface = jest.fn().mockReturnValue('foo'); + it('throws on invalid params', async () => { + const { implementation } = createInterfaceHandler; - const hooks = { - hasPermission, - createInterface, - }; + const messenger = getMessenger(); - const engine = new JsonRpcEngine(); + const engine = new JsonRpcEngine(); - engine.push((request, response, next, end) => { - const result = implementation( - request as JsonRpcRequest, - response as PendingJsonRpcResponse, - next, - end, - hooks, - ); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest & { + origin: string; + }, + response as PendingJsonRpcResponse, + next, + end, + {} as never, + messenger, + ); - result?.catch(end); - }); + result?.catch(end); + }); - const response = await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'snap_createInterface', - params: { - ui: 'foo', - }, - }); + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_createInterface', + params: { + ui: 'foo', + }, + }); - expect(response).toStrictEqual({ - error: { - code: -32602, - message: - 'Invalid params: At path: ui -- Expected type to be one of: "AccountSelector", "Address", "AssetSelector", "AddressInput", "Bold", "Box", "Button", "Copyable", "DateTimePicker", "Divider", "Dropdown", "RadioGroup", "Field", "FileInput", "Form", "Heading", "Input", "Image", "Italic", "Link", "Row", "Spinner", "Text", "Tooltip", "Checkbox", "Card", "Icon", "Selector", "Section", "Avatar", "Banner", "Skeleton", "CollapsibleSection", "Container", but received: undefined.', - stack: expect.any(String), - }, - id: 1, - jsonrpc: '2.0', + expect(response).toStrictEqual({ + error: { + code: -32602, + message: expect.stringContaining( + 'Invalid params: At path: ui -- Expected type to be one of:', + ), + stack: expect.any(String), + }, + id: 1, + jsonrpc: '2.0', + }); }); - }); - it('throws on invalid UI', async () => { - const { implementation } = createInterfaceHandler; + it('throws on invalid UI', async () => { + const { implementation } = createInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const createInterface = jest.fn().mockReturnValue('foo'); + const messenger = getMessenger(); - const hooks = { - hasPermission, - createInterface, - }; - - const engine = new JsonRpcEngine(); + const engine = new JsonRpcEngine(); - engine.push((request, response, next, end) => { - const result = implementation( - request as JsonRpcRequest, - response as PendingJsonRpcResponse, - next, - end, - hooks, - ); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest & { + origin: string; + }, + response as PendingJsonRpcResponse, + next, + end, + {} as never, + messenger, + ); - result?.catch(end); - }); + result?.catch(end); + }); - const response = await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'snap_createInterface', - params: { - ui: ( - - - - - - ) as JSXElement, - }, - }); + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_createInterface', + params: { + ui: ( + + + + + + ) as JSXElement, + }, + }); - expect(response).toStrictEqual({ - error: { - code: -32602, - message: - 'Invalid params: At path: ui.props.children.props.children -- Expected type to be one of: "AssetSelector", "AddressInput", "AccountSelector", "Input", "Dropdown", "RadioGroup", "FileInput", "Checkbox", "Selector", "DateTimePicker", but received: "Copyable".', - stack: expect.any(String), - }, - id: 1, - jsonrpc: '2.0', + expect(response).toStrictEqual({ + error: { + code: -32602, + message: expect.stringContaining( + 'Invalid params: At path: ui.props.children.props.children -- Expected type to be one of:', + ), + stack: expect.any(String), + }, + id: 1, + jsonrpc: '2.0', + }); }); - }); - - it('throws on invalid nested UI', async () => { - const { implementation } = createInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const createInterface = jest.fn().mockReturnValue('foo'); + it('throws on invalid nested UI', async () => { + const { implementation } = createInterfaceHandler; - const hooks = { - hasPermission, - createInterface, - }; + const messenger = getMessenger(); - const engine = new JsonRpcEngine(); + const engine = new JsonRpcEngine(); - engine.push((request, response, next, end) => { - const result = implementation( - request as JsonRpcRequest, - response as PendingJsonRpcResponse, - next, - end, - hooks, - ); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest & { + origin: string; + }, + response as PendingJsonRpcResponse, + next, + end, + {} as never, + messenger, + ); - result?.catch(end); - }); + result?.catch(end); + }); - const response = await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'snap_createInterface', - params: { - ui: ( - - -
- - - -
-
-
- Foo -
-
- ) as JSXElement, - }, - }); + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_createInterface', + params: { + ui: ( + + +
+ + + +
+
+
+ Foo +
+
+ ) as JSXElement, + }, + }); - expect(response).toStrictEqual({ - error: { - code: -32602, - message: - 'Invalid params: At path: ui.props.children.1.props.children.type -- Expected the literal `"Button"`, but received: "Text".', - stack: expect.any(String), - }, - id: 1, - jsonrpc: '2.0', + expect(response).toStrictEqual({ + error: { + code: -32602, + message: + 'Invalid params: At path: ui.props.children.1.props.children.type -- Expected the literal `"Button"`, but received: "Text".', + stack: expect.any(String), + }, + id: 1, + jsonrpc: '2.0', + }); }); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/createInterface.ts b/packages/snaps-rpc-methods/src/permitted/createInterface.ts index 83654c05d9..cdfbf69d0f 100644 --- a/packages/snaps-rpc-methods/src/permitted/createInterface.ts +++ b/packages/snaps-rpc-methods/src/permitted/createInterface.ts @@ -1,12 +1,13 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { CreateInterfaceParams, CreateInterfaceResult, - JsonRpcRequest, - ComponentOrElement, - InterfaceContext, - ContentType, } from '@metamask/snaps-sdk'; import { ComponentOrElementStruct, @@ -16,36 +17,15 @@ import { type InferMatching } from '@metamask/snaps-utils'; import { StructError, create, object, optional } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapInterfaceControllerCreateInterfaceAction, +} from '../types'; import { UI_PERMISSIONS } from '../utils'; -const methodName = 'snap_createInterface'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - createInterface: true, -}; - -export type CreateInterfaceMethodHooks = { - /** - * @param permissionName - The name of the permission to check. - * @returns Whether the Snap has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * @param ui - The UI components. - * @param context - An optional interface context object. - * @param contentType - The optional content type. - * @returns The unique identifier of the interface. - */ - createInterface: ( - ui: ComponentOrElement, - context?: InterfaceContext, - contentType?: ContentType, - ) => string; -}; +export type CreateInterfaceMethodActions = + | PermissionControllerHasPermissionAction + | SnapInterfaceControllerCreateInterfaceAction; /** * Create the interactive interface for use in the @@ -69,13 +49,17 @@ export type CreateInterfaceMethodHooks = { * ``` */ export const createInterfaceHandler = { - methodNames: [methodName] as const, implementation: getCreateInterfaceImplementation, - hookNames, -} satisfies PermittedHandlerExport< - CreateInterfaceMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:createInterface', + ], +} satisfies MethodHandler< + never, + CreateInterfaceMethodActions, CreateInterfaceParameters, - CreateInterfaceResult + CreateInterfaceResult, + { origin: string } >; const CreateInterfaceParametersStruct = object({ @@ -96,20 +80,25 @@ export type CreateInterfaceParameters = InferMatching< * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if the Snap has a given - * permission. - * @param hooks.createInterface - The function to create the interface. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getCreateInterfaceImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, createInterface }: CreateInterfaceMethodHooks, + _hooks: never, + messenger: Messenger, ): void { - if (!UI_PERMISSIONS.some(hasPermission)) { + const { params, origin } = req; + + const isPermitted = UI_PERMISSIONS.some((permission) => + messenger.call('PermissionController:hasPermission', origin, permission), + ); + + if (!isPermitted) { return end( providerErrors.unauthorized({ message: `This method can only be used if the Snap has one of the following permissions: ${UI_PERMISSIONS.join(', ')}.`, @@ -117,14 +106,17 @@ function getCreateInterfaceImplementation( ); } - const { params } = req; - try { const validatedParams = getValidatedParams(params); const { ui, context } = validatedParams; - res.result = createInterface(ui, context); + res.result = messenger.call( + 'SnapInterfaceController:createInterface', + origin, + ui, + context, + ); } catch (error) { return end(error); } diff --git a/packages/snaps-rpc-methods/src/permitted/endTrace.test.ts b/packages/snaps-rpc-methods/src/permitted/endTrace.test.ts index 1077ca49b7..bfcb59f231 100644 --- a/packages/snaps-rpc-methods/src/permitted/endTrace.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/endTrace.test.ts @@ -1,41 +1,68 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { EndTraceParams, EndTraceResult } from '@metamask/snaps-sdk'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; - +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; + +import type { EndTraceMethodActions } from './endTrace'; import { endTraceHandler } from './endTrace'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_endTrace', () => { describe('endTraceHandler', () => { it('has the expected shape', () => { expect(endTraceHandler).toMatchObject({ - methodNames: ['snap_endTrace'], implementation: expect.any(Function), hookNames: { endTrace: true, - getSnap: true, }, + actionNames: ['SnapController:getSnap'], }); }); }); describe('implementation', () => { + const getMessenger = (preinstalled = true) => { + const messenger = new MockControllerMessenger< + EndTraceMethodActions, + never + >(); + + messenger.registerActionHandler('SnapController:getSnap', () => ({ + ...getSnapObject(), + preinstalled, + })); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('calls the `endTrace` hook with the provided parameters', async () => { const { implementation } = endTraceHandler; const endTrace = jest.fn().mockReturnValue(null); + const hooks = { endTrace }; - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { endTrace, getSnap }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -69,18 +96,21 @@ describe('snap_endTrace', () => { const { implementation } = endTraceHandler; const endTrace = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); - const hooks = { endTrace, getSnap }; + const hooks = { endTrace }; + + const messenger = getMessenger(false); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -131,18 +161,21 @@ describe('snap_endTrace', () => { const { implementation } = endTraceHandler; const endTrace = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { endTrace, getSnap }; + const hooks = { endTrace }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/endTrace.ts b/packages/snaps-rpc-methods/src/permitted/endTrace.ts index 5f50a8ee4c..6ff0a5e18b 100644 --- a/packages/snaps-rpc-methods/src/permitted/endTrace.ts +++ b/packages/snaps-rpc-methods/src/permitted/endTrace.ts @@ -1,12 +1,15 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import { rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, EndTraceParams, EndTraceResult, EndTraceRequest, } from '@metamask/snaps-sdk'; -import type { InferMatching, Snap } from '@metamask/snaps-utils'; +import type { InferMatching } from '@metamask/snaps-utils'; import { number, create, @@ -17,14 +20,14 @@ import { } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; -const methodName = 'snap_endTrace'; - const hookNames: MethodHooksObject = { endTrace: true, - getSnap: true, }; export type EndTraceMethodHooks = { @@ -35,15 +38,10 @@ export type EndTraceMethodHooks = { * @returns The performance trace context. */ endTrace: (request: EndTraceRequest) => void; - - /** - * Get Snap metadata. - * - * @param snapId - The ID of a Snap. - */ - getSnap: (snapId: string) => Snap | undefined; }; +export type EndTraceMethodActions = SnapControllerGetSnapAction; + const EndTraceParametersStruct = object({ id: exactOptional(string()), name: string(), @@ -62,13 +60,15 @@ export type EndTraceParameters = InferMatching< * @internal */ export const endTraceHandler = { - methodNames: [methodName] as const, implementation: getEndTraceImplementation, hookNames, -} satisfies PermittedHandlerExport< + actionNames: ['SnapController:getSnap'], +} satisfies MethodHandler< EndTraceMethodHooks, + EndTraceMethodActions, EndTraceParameters, - EndTraceResult + EndTraceResult, + { origin: string } >; /** @@ -82,19 +82,18 @@ export const endTraceHandler = { * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. * @param hooks.endTrace - The hook function to end a performance trace. - * @param hooks.getSnap - The hook function to get Snap metadata. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getEndTraceImplementation( - request: JsonRpcRequest, + request: JsonRpcRequestWithOrigin, response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { endTrace, getSnap }: EndTraceMethodHooks, + { endTrace }: EndTraceMethodHooks, + messenger: Messenger, ): void { - const snap = getSnap( - (request as JsonRpcRequest & { origin: string }).origin, - ); + const snap = messenger.call('SnapController:getSnap', request.origin); if (!snap?.preinstalled) { return end(rpcErrors.methodNotFound()); diff --git a/packages/snaps-rpc-methods/src/permitted/getAllSnaps.test.ts b/packages/snaps-rpc-methods/src/permitted/getAllSnaps.test.ts index cc3f82a1dd..3292a51ca1 100644 --- a/packages/snaps-rpc-methods/src/permitted/getAllSnaps.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getAllSnaps.test.ts @@ -1,41 +1,60 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { GetSnapsResult } from '@metamask/snaps-sdk'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; +import { + MockControllerMessenger, + getTruncatedSnap, +} from '@metamask/snaps-utils/test-utils'; import type { PendingJsonRpcResponse } from '@metamask/utils'; +import type { + GetAllSnapsMethodActions, + GetAllSnapsResult, +} from './getAllSnaps'; import { getAllSnapsHandler } from './getAllSnaps'; describe('wallet_getAllSnaps', () => { describe('getAllSnapsHandler', () => { it('has the expected shape', () => { expect(getAllSnapsHandler).toMatchObject({ - methodNames: ['wallet_getAllSnaps'], implementation: expect.any(Function), - hookNames: { - getAllSnaps: true, - }, + actionNames: ['SnapController:getAllSnaps'], }); }); }); describe('implementation', () => { - it('returns the result received from the `getAllSnaps` hook', async () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + GetAllSnapsMethodActions, + never + >(); + + messenger.registerActionHandler('SnapController:getAllSnaps', () => [ + getTruncatedSnap(), + ]); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('returns the result received from the `SnapController:getAllSnaps` action', async () => { const { implementation } = getAllSnapsHandler; - const getAllSnaps = jest.fn().mockResolvedValue(['foo', 'bar']); - const hooks = { - getAllSnaps, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('https://snaps.metamask.io')); engine.push((request, response, next, end) => { const result = implementation( - // @ts-expect-error - `origin` is not part of the type, but in practice - // it is added by the MetaMask middleware stack. - { ...request, origin: 'https://snaps.metamask.io' }, - response as PendingJsonRpcResponse, + request as Parameters[0], + response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -50,28 +69,25 @@ describe('wallet_getAllSnaps', () => { expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, - result: ['foo', 'bar'], + result: [getTruncatedSnap()], }); }); it('returns an error if the origin is not allowed', async () => { const { implementation } = getAllSnapsHandler; - const getAllSnaps = jest.fn().mockResolvedValue(['foo', 'bar']); - const hooks = { - getAllSnaps, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('https://example.com')); engine.push((request, response, next, end) => { const result = implementation( - // @ts-expect-error - `origin` is not part of the type, but in practice - // it is added by the MetaMask middleware stack. - { ...request, origin: 'https://example.com' }, - response as PendingJsonRpcResponse, + request as Parameters[0], + response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/getAllSnaps.ts b/packages/snaps-rpc-methods/src/permitted/getAllSnaps.ts index c802cf07ce..a292586c21 100644 --- a/packages/snaps-rpc-methods/src/permitted/getAllSnaps.ts +++ b/packages/snaps-rpc-methods/src/permitted/getAllSnaps.ts @@ -1,20 +1,20 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; -import { rpcErrors } from '@metamask/rpc-errors'; -import type { GetSnapsResult } from '@metamask/snaps-sdk'; import type { - JsonRpcParams, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { TruncatedSnap } from '@metamask/snaps-utils'; +import type { JsonRpcParams, PendingJsonRpcResponse } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetAllSnapsAction, +} from '../types'; -const methodName = 'wallet_getAllSnaps'; +export type GetAllSnapsResult = TruncatedSnap[]; -const hookNames: MethodHooksObject = { - getAllSnaps: true, -}; +export type GetAllSnapsMethodActions = SnapControllerGetAllSnapsAction; /** * `wallet_getAllSnaps` gets all installed Snaps. Currently, this can only be @@ -23,22 +23,16 @@ const hookNames: MethodHooksObject = { * @internal */ export const getAllSnapsHandler = { - methodNames: [methodName] as const, implementation: getAllSnapsImplementation, - hookNames, -} satisfies PermittedHandlerExport< - GetAllSnapsHooks, + actionNames: ['SnapController:getAllSnaps'], +} satisfies MethodHandler< + never, + GetAllSnapsMethodActions, JsonRpcParams, - GetSnapsResult + GetAllSnapsResult, + { origin: string } >; -export type GetAllSnapsHooks = { - /** - * @returns All installed Snaps. - */ - getAllSnaps: () => Promise; -}; - /** * The `wallet_getAllSnaps` method implementation. * Fetches all installed snaps and adds them to the JSON-RPC response. @@ -48,24 +42,24 @@ export type GetAllSnapsHooks = { * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.getAllSnaps - A function that returns all installed snaps. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function getAllSnapsImplementation( - request: JsonRpcRequest, - response: PendingJsonRpcResponse, + request: JsonRpcRequestWithOrigin, + response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { getAllSnaps }: GetAllSnapsHooks, + _hooks: never, + messenger: Messenger, ): Promise { - // The origin is added by the MetaMask middleware stack. - const { origin } = request as JsonRpcRequest & { origin: string }; + const { origin } = request; if (origin !== 'https://snaps.metamask.io') { return end(rpcErrors.methodNotFound()); } - response.result = await getAllSnaps(); + response.result = messenger.call('SnapController:getAllSnaps'); return end(); } diff --git a/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.test.ts b/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.test.ts index c78af4cbb4..aa25a28f15 100644 --- a/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.test.ts @@ -1,71 +1,87 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { GetBackgroundEventsParams, GetBackgroundEventsResult, + SnapId, } from '@metamask/snaps-sdk'; -import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { GetBackgroundEventsMethodActions } from './getBackgroundEvents'; import { getBackgroundEventsHandler } from './getBackgroundEvents'; describe('snap_getBackgroundEvents', () => { describe('getBackgroundEventsHandler', () => { it('has the expected shape', () => { expect(getBackgroundEventsHandler).toMatchObject({ - methodNames: ['snap_getBackgroundEvents'], implementation: expect.any(Function), - hookNames: { - getBackgroundEvents: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'CronjobController:get', + ], }); }); }); describe('implementation', () => { - const createOriginMiddleware = - (origin: string) => - (request: any, _response: unknown, next: () => void, _end: unknown) => { - request.origin = origin; - next(); - }; - - it('returns an array of background events after calling the `getBackgroundEvents` hook', async () => { - const { implementation } = getBackgroundEventsHandler; - - const backgroundEvents = [ - { - id: 'foo', - snapId: MOCK_SNAP_ID, - date: '2022-01-01T01:00Z', - scheduledAt: '2021-01-01', - request: { - method: 'handleExport', - params: ['p1'], - }, + const backgroundEvents = [ + { + id: 'foo', + snapId: MOCK_SNAP_ID, + date: '2022-01-01T01:00Z', + scheduledAt: '2021-01-01', + request: { + method: 'handleExport', + params: ['p1'], }, - ]; + }, + ]; + + const getMessenger = () => { + const messenger = new MockControllerMessenger< + GetBackgroundEventsMethodActions, + never + >(); - const getBackgroundEvents = jest - .fn() - .mockImplementation(() => backgroundEvents); + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); - const hasPermission = jest.fn().mockImplementation(() => true); + messenger.registerActionHandler( + 'CronjobController:get', + () => backgroundEvents, + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('returns an array of background events after calling the `CronjobController:get` action', async () => { + const { implementation } = getBackgroundEventsHandler; - const hooks = { - getBackgroundEvents, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -87,25 +103,21 @@ describe('snap_getBackgroundEvents', () => { it('gets background events', async () => { const { implementation } = getBackgroundEventsHandler; - const getBackgroundEvents = jest.fn(); - - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - getBackgroundEvents, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -117,33 +129,34 @@ describe('snap_getBackgroundEvents', () => { method: 'snap_getBackgroundEvents', }); - expect(getBackgroundEvents).toHaveBeenCalled(); + expect(messenger.call).toHaveBeenCalledWith( + 'CronjobController:get', + MOCK_SNAP_ID, + ); }); - it('will throw if the call to the `getBackgroundEvents` hook fails', async () => { + it('will throw if the call to the `CronjobController:get` action fails', async () => { const { implementation } = getBackgroundEventsHandler; - const getBackgroundEvents = jest.fn().mockImplementation(() => { + const messenger = getMessenger(); + + messenger.registerActionHandler('CronjobController:get', () => { throw new Error('foobar'); }); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - getBackgroundEvents, - hasPermission, - }; - const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -173,24 +186,26 @@ describe('snap_getBackgroundEvents', () => { it('throws if a snap does not have the "endowment:cronjob" permission', async () => { const { implementation } = getBackgroundEventsHandler; - const getBackgroundEvents = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => false); + const messenger = getMessenger(); - const hooks = { - getBackgroundEvents, - hasPermission, - }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.ts b/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.ts index fd5a49cef8..8c99fecb19 100644 --- a/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.ts +++ b/packages/snaps-rpc-methods/src/permitted/getBackgroundEvents.ts @@ -1,28 +1,26 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors } from '@metamask/rpc-errors'; import type { - BackgroundEvent, GetBackgroundEventsParams, GetBackgroundEventsResult, - JsonRpcRequest, + SnapId, } from '@metamask/snaps-sdk'; import { type PendingJsonRpcResponse } from '@metamask/utils'; import { SnapEndowments } from '../endowments'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; - -const methodName = 'snap_getBackgroundEvents'; - -const hookNames: MethodHooksObject = { - getBackgroundEvents: true, - hasPermission: true, -}; +import type { + CronjobControllerGetAction, + JsonRpcRequestWithOrigin, +} from '../types'; -export type GetBackgroundEventsMethodHooks = { - getBackgroundEvents: () => BackgroundEvent[]; - hasPermission: (permissionName: string) => boolean; -}; +export type GetBackgroundEventsMethodActions = + | PermissionControllerHasPermissionAction + | CronjobControllerGetAction; /** * Get the scheduled background events for the Snap. @@ -49,41 +47,49 @@ export type GetBackgroundEventsMethodHooks = { * ``` */ export const getBackgroundEventsHandler = { - methodNames: [methodName] as const, implementation: getGetBackgroundEventsImplementation, - hookNames, -} satisfies PermittedHandlerExport< - GetBackgroundEventsMethodHooks, + actionNames: ['PermissionController:hasPermission', 'CronjobController:get'], +} satisfies MethodHandler< + never, + GetBackgroundEventsMethodActions, GetBackgroundEventsParams, - GetBackgroundEventsResult + GetBackgroundEventsResult, + { origin: SnapId } >; /** * The `snap_getBackgroundEvents` method implementation. * - * @param _req - The JSON-RPC request object. Not used by this function. + * @param req - The JSON-RPC request object. * @param res - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. * Not used by this function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.getBackgroundEvents - The function to get the background events. - * @param hooks.hasPermission - The function to check if a snap has the `endowment:cronjob` permission. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns An array of background events. */ async function getGetBackgroundEventsImplementation( - _req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { getBackgroundEvents, hasPermission }: GetBackgroundEventsMethodHooks, + _hooks: never, + messenger: Messenger, ): Promise { - if (!hasPermission(SnapEndowments.Cronjob)) { + const { origin } = req; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + SnapEndowments.Cronjob, + ) + ) { return end(providerErrors.unauthorized()); } try { - const events = getBackgroundEvents(); - res.result = events; + res.result = messenger.call('CronjobController:get', origin); } catch (error) { return end(error); } diff --git a/packages/snaps-rpc-methods/src/permitted/getClientStatus.test.ts b/packages/snaps-rpc-methods/src/permitted/getClientStatus.test.ts index 863c862655..4b1d4ab05a 100644 --- a/packages/snaps-rpc-methods/src/permitted/getClientStatus.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getClientStatus.test.ts @@ -1,38 +1,58 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { MockAnyNamespace } from '@metamask/messenger'; +import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger'; import type { GetClientStatusResult } from '@metamask/snaps-sdk'; import { getPlatformVersion } from '@metamask/snaps-utils'; import type { PendingJsonRpcResponse } from '@metamask/utils'; +import type { GetClientStatusMethodActions } from './getClientStatus'; import { getClientStatusHandler } from './getClientStatus'; describe('snap_getClientStatus', () => { describe('getClientStatusHandler', () => { it('has the expected shape', () => { expect(getClientStatusHandler).toMatchObject({ - methodNames: ['snap_getClientStatus'], implementation: expect.any(Function), hookNames: { - getIsLocked: true, getIsActive: true, getVersion: true, }, + actionNames: ['KeyringController:getState'], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new Messenger< + MockAnyNamespace, + GetClientStatusMethodActions + >({ + namespace: MOCK_ANY_NAMESPACE, + }); + + messenger.registerActionHandler('KeyringController:getState', () => ({ + isUnlocked: false, + keyrings: [], + })); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('returns the result', async () => { const { implementation } = getClientStatusHandler; - const getIsLocked = jest.fn().mockReturnValue(true); const getIsActive = jest.fn().mockReturnValue(false); const getVersion = jest.fn().mockReturnValue('13.6.0-flask.0'); const hooks = { - getIsLocked, getIsActive, getVersion, }; + const messenger = getMessenger(); + const engine = new JsonRpcEngine(); engine.push((request, response, next, end) => { const result = implementation( @@ -41,6 +61,7 @@ describe('snap_getClientStatus', () => { next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/getClientStatus.ts b/packages/snaps-rpc-methods/src/permitted/getClientStatus.ts index 20654c057a..4edc4236e8 100644 --- a/packages/snaps-rpc-methods/src/permitted/getClientStatus.ts +++ b/packages/snaps-rpc-methods/src/permitted/getClientStatus.ts @@ -1,4 +1,8 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import type { GetClientStatusResult } from '@metamask/snaps-sdk'; import { getPlatformVersion } from '@metamask/snaps-utils'; import type { @@ -7,17 +11,28 @@ import type { PendingJsonRpcResponse, } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; +import type { KeyringControllerGetStateAction } from '../types'; import type { MethodHooksObject } from '../utils'; -const methodName = 'snap_getClientStatus'; - -const hookNames: MethodHooksObject = { - getIsLocked: true, +const hookNames: MethodHooksObject = { getIsActive: true, getVersion: true, }; +export type GetClientStatusMethodHooks = { + /** + * @returns Whether the client is active or not. + */ + getIsActive: () => boolean; + + /** + * @returns The version string for the client. + */ + getVersion: () => string; +}; + +export type GetClientStatusMethodActions = KeyringControllerGetStateAction; + /** * Get the status of the client running the Snap. * @@ -46,32 +61,16 @@ const hookNames: MethodHooksObject = { * ``` */ export const getClientStatusHandler = { - methodNames: [methodName] as const, implementation: getClientStatusImplementation, hookNames, -} satisfies PermittedHandlerExport< - GetClientStatusHooks, + actionNames: ['KeyringController:getState'], +} satisfies MethodHandler< + GetClientStatusMethodHooks, + GetClientStatusMethodActions, JsonRpcParams, GetClientStatusResult >; -export type GetClientStatusHooks = { - /** - * @returns Whether the client is locked or not. - */ - getIsLocked: () => boolean; - - /** - * @returns Whether the client is active or not. - */ - getIsActive: () => boolean; - - /** - * @returns The version string for the client. - */ - getVersion: () => string; -}; - /** * The `snap_getClientStatus` method implementation. * Returns useful information about the client running the snap. @@ -82,9 +81,9 @@ export type GetClientStatusHooks = { * function. * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. - * @param hooks.getIsLocked - A function that returns whether the client is locked or not. * @param hooks.getIsActive - A function that returns whether the client is opened or not. * @param hooks.getVersion - A function that returns the client version. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function getClientStatusImplementation( @@ -92,10 +91,13 @@ async function getClientStatusImplementation( response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { getIsLocked, getIsActive, getVersion }: GetClientStatusHooks, + { getIsActive, getVersion }: GetClientStatusMethodHooks, + messenger: Messenger, ): Promise { + const { isUnlocked } = messenger.call('KeyringController:getState'); + response.result = { - locked: getIsLocked(), + locked: !isUnlocked, active: getIsActive(), clientVersion: getVersion(), platformVersion: getPlatformVersion(), diff --git a/packages/snaps-rpc-methods/src/permitted/getFile.test.ts b/packages/snaps-rpc-methods/src/permitted/getFile.test.ts index bb880634c5..d93f49d225 100644 --- a/packages/snaps-rpc-methods/src/permitted/getFile.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getFile.test.ts @@ -1,58 +1,78 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { GetFileParams } from '@metamask/snaps-sdk'; import { AuxiliaryFileEncoding } from '@metamask/snaps-sdk'; import { VirtualFile } from '@metamask/snaps-utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { - JsonRpcRequest, PendingJsonRpcResponse, JsonRpcFailure, JsonRpcSuccess, } from '@metamask/utils'; import { stringToBytes } from '@metamask/utils'; -import type { GetFileHooks } from './getFile'; +import type { GetFileMethodActions } from './getFile'; import { getFileHandler } from './getFile'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_getFile', () => { describe('getFileHandler', () => { it('has the expected shape', () => { expect(getFileHandler).toMatchObject({ - methodNames: ['snap_getFile'], implementation: expect.any(Function), - hookNames: { - getSnapFile: true, - }, + actionNames: ['SnapController:getSnapFile'], }); }); }); describe('implementation', () => { - const getMockHooks = () => - ({ - getSnapFile: jest.fn(), - }) as GetFileHooks; + const getMessenger = () => { + const messenger = new MockControllerMessenger< + GetFileMethodActions, + never + >(); + + messenger.registerActionHandler( + 'SnapController:getSnapFile', + async () => null, + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; - it('returns the result received from the getSnapFile hook', async () => { + it('returns the result received from the `SnapController:getSnapFile` action', async () => { const { implementation } = getFileHandler; - const hooks = getMockHooks(); + const messenger = getMessenger(); const vfile = new VirtualFile( stringToBytes(JSON.stringify({ foo: 'bar' })), ); const base64 = vfile.toString('base64'); - ( - hooks.getSnapFile as jest.MockedFunction - ).mockImplementation(async (_path: string) => base64); + + messenger.registerActionHandler( + 'SnapController:getSnapFile', + async () => base64, + ); const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -68,7 +88,9 @@ describe('snap_getFile', () => { })) as JsonRpcSuccess; expect(response.result).toBe(base64); - expect(hooks.getSnapFile).toHaveBeenCalledWith( + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapFile', + MOCK_SNAP_ID, './src/foo.json', AuxiliaryFileEncoding.Base64, ); @@ -77,24 +99,29 @@ describe('snap_getFile', () => { it('supports hex in encoding parameter', async () => { const { implementation } = getFileHandler; - const hooks = getMockHooks(); + const messenger = getMessenger(); const vfile = new VirtualFile( stringToBytes(JSON.stringify({ foo: 'bar' })), ); const hexadecimal = vfile.toString('hex'); - ( - hooks.getSnapFile as jest.MockedFunction - ).mockImplementation(async (_path: string) => hexadecimal); + + messenger.registerActionHandler( + 'SnapController:getSnapFile', + async () => hexadecimal, + ); const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -111,31 +138,37 @@ describe('snap_getFile', () => { })) as JsonRpcSuccess; expect(response.result).toBe(hexadecimal); - expect(hooks.getSnapFile).toHaveBeenCalledWith( + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapFile', + MOCK_SNAP_ID, './src/foo.json', AuxiliaryFileEncoding.Hex, ); }); - it('ends with error if hook throws', async () => { + it('ends with error if action throws', async () => { const { implementation } = getFileHandler; - const hooks = getMockHooks(); + const messenger = getMessenger(); - ( - hooks.getSnapFile as jest.MockedFunction - ).mockImplementation(async (_path: string) => { - throw new Error('foo bar'); - }); + messenger.registerActionHandler( + 'SnapController:getSnapFile', + async () => { + throw new Error('foo bar'); + }, + ); const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -161,7 +194,9 @@ describe('snap_getFile', () => { }, }); - expect(hooks.getSnapFile).toHaveBeenCalledWith( + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapFile', + MOCK_SNAP_ID, './src/foo.json', AuxiliaryFileEncoding.Base64, ); diff --git a/packages/snaps-rpc-methods/src/permitted/getFile.ts b/packages/snaps-rpc-methods/src/permitted/getFile.ts index 6aba711082..07a84d422d 100644 --- a/packages/snaps-rpc-methods/src/permitted/getFile.ts +++ b/packages/snaps-rpc-methods/src/permitted/getFile.ts @@ -1,14 +1,20 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import { rpcErrors } from '@metamask/rpc-errors'; import type { GetFileParams, GetFileResult } from '@metamask/snaps-sdk'; import { AuxiliaryFileEncoding, enumValue } from '@metamask/snaps-sdk'; import type { InferMatching } from '@metamask/snaps-utils'; import { object, optional, string, union } from '@metamask/superstruct'; -import type { PendingJsonRpcResponse, JsonRpcRequest } from '@metamask/utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; import { assertStruct } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapFileAction, +} from '../types'; export const GetFileArgsStruct = object({ path: string(), @@ -26,11 +32,7 @@ export type InferredGetFileParams = InferMatching< GetFileParams >; -const methodName = 'snap_getFile'; - -const hookNames: MethodHooksObject = { - getSnapFile: true, -}; +export type GetFileMethodActions = SnapControllerGetSnapFileAction; /** * Gets a static file's content in UTF-8, Base64, or hexadecimal. @@ -59,17 +61,15 @@ const hookNames: MethodHooksObject = { * ``` */ export const getFileHandler = { - methodNames: [methodName] as const, implementation, - hookNames, -} satisfies PermittedHandlerExport; - -export type GetFileHooks = { - getSnapFile: ( - path: InferredGetFileParams['path'], - encoding: InferredGetFileParams['encoding'], - ) => Promise; -}; + actionNames: ['SnapController:getSnapFile'], +} satisfies MethodHandler< + never, + GetFileMethodActions, + InferredGetFileParams, + GetFileResult, + { origin: string } +>; /** * The `snap_getFile` method implementation. @@ -79,18 +79,19 @@ export type GetFileHooks = { * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.getSnapFile - The function to load a static snap file. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function implementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { getSnapFile }: GetFileHooks, + _hooks: never, + messenger: Messenger, ): Promise { - const { params } = req; + const { params, origin } = req; assertStruct( params, @@ -100,9 +101,12 @@ async function implementation( ); try { - res.result = await getSnapFile( + res.result = await messenger.call( + 'SnapController:getSnapFile', + origin, params.path, - params.encoding ?? AuxiliaryFileEncoding.Base64, + (params.encoding as AuxiliaryFileEncoding) ?? + AuxiliaryFileEncoding.Base64, ); } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts index 4effaf1364..9f300b8f26 100644 --- a/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.test.ts @@ -1,42 +1,83 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { type GetInterfaceContextResult } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; -import type { GetInterfaceContextParameters } from './getInterfaceContext'; +import type { + GetInterfaceContextMethodActions, + GetInterfaceContextParameters, +} from './getInterfaceContext'; import { getInterfaceContextHandler } from './getInterfaceContext'; describe('snap_getInterfaceContext', () => { describe('getInterfaceContextHandler', () => { it('has the expected shape', () => { expect(getInterfaceContextHandler).toMatchObject({ - methodNames: ['snap_getInterfaceContext'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - getInterfaceContext: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:getInterface', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + GetInterfaceContextMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapInterfaceController:getInterface', + () => ({ + content: { type: 'Text', key: 'foo', props: { children: 'Foo' } }, + snapId: MOCK_SNAP_ID, + state: {}, + context: { foo: 'bar' }, + }), + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission to show UI', async () => { const { implementation } = getInterfaceContextHandler; - const hasPermission = jest.fn().mockReturnValue(false); - const getInterfaceContext = jest.fn().mockReturnValue({ foo: 'bar' }); + const messenger = getMessenger(); - const hooks = { hasPermission, getInterfaceContext }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -63,26 +104,24 @@ describe('snap_getInterfaceContext', () => { }); }); - it('returns the result from the `getInterfaceContext` hook', async () => { + it('returns the result from the `SnapInterfaceController:getInterface` action', async () => { const { implementation } = getInterfaceContextHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const getInterfaceContext = jest.fn().mockReturnValue({ foo: 'bar' }); - - const hooks = { - hasPermission, - getInterfaceContext, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -107,23 +146,21 @@ describe('snap_getInterfaceContext', () => { it('throws on invalid params', async () => { const { implementation } = getInterfaceContextHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const getInterfaceContext = jest.fn().mockReturnValue({ foo: 'bar' }); - - const hooks = { - hasPermission, - getInterfaceContext, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts index fad694cd6c..a89d47c4ba 100644 --- a/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceContext.ts @@ -1,39 +1,27 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { GetInterfaceContextParams, GetInterfaceContextResult, - InterfaceContext, - JsonRpcRequest, } from '@metamask/snaps-sdk'; import { type InferMatching } from '@metamask/snaps-utils'; import { StructError, create, object, string } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapInterfaceControllerGetInterfaceAction, +} from '../types'; import { UI_PERMISSIONS } from '../utils'; -const methodName = 'snap_getInterfaceContext'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - getInterfaceContext: true, -}; - -export type GetInterfaceContextMethodHooks = { - /** - * @param permissionName - The name of the permission to check. - * @returns Whether the Snap has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * @param id - The interface ID. - * @returns The interface context. - */ - getInterfaceContext: (id: string) => InterfaceContext | null; -}; +export type GetInterfaceContextMethodActions = + | PermissionControllerHasPermissionAction + | SnapInterfaceControllerGetInterfaceAction; /** * Get the context of an [interface](https://docs.metamask.io/snaps/features/custom-ui/interactive-ui/) @@ -72,13 +60,17 @@ export type GetInterfaceContextMethodHooks = { * ``` */ export const getInterfaceContextHandler = { - methodNames: [methodName] as const, implementation: getInterfaceContextImplementation, - hookNames, -} satisfies PermittedHandlerExport< - GetInterfaceContextMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:getInterface', + ], +} satisfies MethodHandler< + never, + GetInterfaceContextMethodActions, GetInterfaceContextParameters, - GetInterfaceContextResult + GetInterfaceContextResult, + { origin: string } >; const GetInterfaceContextParametersStruct = object({ @@ -98,20 +90,25 @@ export type GetInterfaceContextParameters = InferMatching< * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if the Snap has a given - * permission. - * @param hooks.getInterfaceContext - The function to get the interface context. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getInterfaceContextImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, getInterfaceContext }: GetInterfaceContextMethodHooks, + _hooks: never, + messenger: Messenger, ): void { - if (!UI_PERMISSIONS.some(hasPermission)) { + const { params, origin } = req; + + const isPermitted = UI_PERMISSIONS.some((permission) => + messenger.call('PermissionController:hasPermission', origin, permission), + ); + + if (!isPermitted) { return end( providerErrors.unauthorized({ message: `This method can only be used if the Snap has one of the following permissions: ${UI_PERMISSIONS.join(', ')}.`, @@ -119,14 +116,18 @@ function getInterfaceContextImplementation( ); } - const { params } = req; - try { const validatedParams = getValidatedParams(params); const { id } = validatedParams; - res.result = getInterfaceContext(id); + const { context } = messenger.call( + 'SnapInterfaceController:getInterface', + origin, + id, + ); + + res.result = context; } catch (error) { return end(error); } diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts index ab32a2e26d..a3ed1c6f11 100644 --- a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.test.ts @@ -1,42 +1,78 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { type GetInterfaceStateResult } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { + GetInterfaceStateMethodActions, + GetInterfaceStateParameters, +} from './getInterfaceState'; import { getInterfaceStateHandler } from './getInterfaceState'; -import type { GetInterfaceStateParameters } from './getInterfaceState'; describe('snap_getInterfaceState', () => { describe('getInterfaceStateHandler', () => { it('has the expected shape', () => { expect(getInterfaceStateHandler).toMatchObject({ - methodNames: ['snap_getInterfaceState'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - getInterfaceState: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:getInterfaceState', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + GetInterfaceStateMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapInterfaceController:getInterfaceState', + () => ({ foo: 'bar' }), + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission to show UI', async () => { const { implementation } = getInterfaceStateHandler; - const hasPermission = jest.fn().mockReturnValue(false); - const getInterfaceState = jest.fn().mockReturnValue({ foo: 'bar' }); + const messenger = getMessenger(); - const hooks = { hasPermission, getInterfaceState }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -63,26 +99,24 @@ describe('snap_getInterfaceState', () => { }); }); - it('returns the result from the `getInterfaceState` hook', async () => { + it('returns the result from the `SnapInterfaceController:getInterfaceState` action', async () => { const { implementation } = getInterfaceStateHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const getInterfaceState = jest.fn().mockReturnValue({ foo: 'bar' }); - - const hooks = { - hasPermission, - getInterfaceState, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -107,23 +141,21 @@ describe('snap_getInterfaceState', () => { it('throws on invalid params', async () => { const { implementation } = getInterfaceStateHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const getInterfaceState = jest.fn().mockReturnValue({ foo: 'bar' }); - - const hooks = { - hasPermission, - getInterfaceState, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts index d07b484f94..e722cedf08 100644 --- a/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts +++ b/packages/snaps-rpc-methods/src/permitted/getInterfaceState.ts @@ -1,39 +1,27 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { GetInterfaceStateParams, GetInterfaceStateResult, - InterfaceState, - JsonRpcRequest, } from '@metamask/snaps-sdk'; import { type InferMatching } from '@metamask/snaps-utils'; import { StructError, create, object, string } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapInterfaceControllerGetInterfaceStateAction, +} from '../types'; import { UI_PERMISSIONS } from '../utils'; -const methodName = 'snap_getInterfaceState'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - getInterfaceState: true, -}; - -export type GetInterfaceStateMethodHooks = { - /** - * @param permissionName - The name of the permission to check. - * @returns Whether the Snap has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * @param id - The interface ID. - * @returns The interface state. - */ - getInterfaceState: (id: string) => InterfaceState; -}; +export type GetInterfaceStateMethodActions = + | PermissionControllerHasPermissionAction + | SnapInterfaceControllerGetInterfaceStateAction; /** * Get the form state of an [interface](https://docs.metamask.io/snaps/features/custom-ui/interactive-ui/) @@ -50,13 +38,17 @@ export type GetInterfaceStateMethodHooks = { * ``` */ export const getInterfaceStateHandler = { - methodNames: [methodName] as const, implementation: getGetInterfaceStateImplementation, - hookNames, -} satisfies PermittedHandlerExport< - GetInterfaceStateMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:getInterfaceState', + ], +} satisfies MethodHandler< + never, + GetInterfaceStateMethodActions, GetInterfaceStateParameters, - GetInterfaceStateResult + GetInterfaceStateResult, + { origin: string } >; const GetInterfaceStateParametersStruct = object({ @@ -76,20 +68,25 @@ export type GetInterfaceStateParameters = InferMatching< * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if the Snap has a given - * permission. - * @param hooks.getInterfaceState - The function to get the interface state. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getGetInterfaceStateImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, getInterfaceState }: GetInterfaceStateMethodHooks, + _hooks: never, + messenger: Messenger, ): void { - if (!UI_PERMISSIONS.some(hasPermission)) { + const { params, origin } = req; + + const isPermitted = UI_PERMISSIONS.some((permission) => + messenger.call('PermissionController:hasPermission', origin, permission), + ); + + if (!isPermitted) { return end( providerErrors.unauthorized({ message: `This method can only be used if the Snap has one of the following permissions: ${UI_PERMISSIONS.join(', ')}.`, @@ -97,14 +94,16 @@ function getGetInterfaceStateImplementation( ); } - const { params } = req; - try { const validatedParams = getValidatedParams(params); const { id } = validatedParams; - res.result = getInterfaceState(id); + res.result = messenger.call( + 'SnapInterfaceController:getInterfaceState', + origin, + id, + ); } catch (error) { return end(error); } diff --git a/packages/snaps-rpc-methods/src/permitted/getSnaps.ts b/packages/snaps-rpc-methods/src/permitted/getSnaps.ts index 41252c2f2a..caca34db85 100644 --- a/packages/snaps-rpc-methods/src/permitted/getSnaps.ts +++ b/packages/snaps-rpc-methods/src/permitted/getSnaps.ts @@ -1,15 +1,18 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import type { GetSnapsResult } from '@metamask/snaps-sdk'; -import type { JsonRpcParams, PendingJsonRpcResponse } from '@metamask/utils'; - -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcParams, + JsonRpcRequest, + PendingJsonRpcResponse, +} from '@metamask/utils'; -const methodName = 'wallet_getSnaps'; +import type { SnapControllerGetPermittedSnapsAction } from '../types'; -const hookNames: MethodHooksObject = { - getSnaps: true, -}; +export type GetSnapsMethodActions = SnapControllerGetPermittedSnapsAction; /** * Get permitted and installed Snaps for the requesting origin. @@ -33,43 +36,37 @@ const hookNames: MethodHooksObject = { * ``` */ export const getSnapsHandler = { - methodNames: [methodName] as const, implementation: getSnapsImplementation, - hookNames, -} satisfies PermittedHandlerExport< - GetSnapsHooks, + actionNames: ['SnapController:getPermittedSnaps'], +} satisfies MethodHandler< + never, + GetSnapsMethodActions, JsonRpcParams, - GetSnapsResult + GetSnapsResult, + { origin: string } >; -export type GetSnapsHooks = { - /** - * @returns The permitted and installed snaps for the requesting origin. - */ - getSnaps: () => Promise; -}; - /** * The `wallet_getSnaps` method implementation. * Fetches available snaps for the requesting origin and adds them to the JSON-RPC response. * - * @param _req - The JSON-RPC request object. Not used by this function. + * @param req - The JSON-RPC request object. * @param res - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.getSnaps - A function that returns the snaps available for the requesting origin. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function getSnapsImplementation( - _req: unknown, + req: JsonRpcRequest & { origin: string }, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { getSnaps }: GetSnapsHooks, + _hooks: never, + messenger: Messenger, ): Promise { - // getSnaps is already bound to the origin - res.result = await getSnaps(); + res.result = messenger.call('SnapController:getPermittedSnaps', req.origin); return end(); } diff --git a/packages/snaps-rpc-methods/src/permitted/getState.test.ts b/packages/snaps-rpc-methods/src/permitted/getState.test.ts index 11cb3f01be..2e1683928c 100644 --- a/packages/snaps-rpc-methods/src/permitted/getState.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getState.test.ts @@ -1,50 +1,74 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { errorCodes } from '@metamask/rpc-errors'; -import type { JsonRpcRequest } from '@metamask/utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; -import type { GetStateParameters } from './getState'; +import type { GetStateMethodActions, GetStateParameters } from './getState'; import { get, getStateHandler } from './getState'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_getState', () => { describe('getStateHandler', () => { it('has the expected shape', () => { expect(getStateHandler).toMatchObject({ - methodNames: ['snap_getState'], implementation: expect.any(Function), hookNames: { - getSnapState: true, - hasPermission: true, + getUnlockPromise: true, }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:getSnapState', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + GetStateMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapController:getSnapState', + async () => ({ foo: 'bar' }), + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('returns the encrypted state', async () => { const { implementation } = getStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - getUnlockPromise, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); @@ -59,7 +83,11 @@ describe('snap_getState', () => { }, }); - expect(getSnapState).toHaveBeenCalledWith(true); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapState', + MOCK_SNAP_ID, + true, + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -70,28 +98,22 @@ describe('snap_getState', () => { it('returns the entire state if no key is specified', async () => { const { implementation } = getStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - getUnlockPromise, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); @@ -104,7 +126,11 @@ describe('snap_getState', () => { params: {}, }); - expect(getSnapState).toHaveBeenCalledWith(true); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapState', + MOCK_SNAP_ID, + true, + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -117,28 +143,22 @@ describe('snap_getState', () => { it('returns the unencrypted state', async () => { const { implementation } = getStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - getUnlockPromise, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); @@ -154,7 +174,11 @@ describe('snap_getState', () => { }, }); - expect(getSnapState).toHaveBeenCalledWith(false); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapState', + MOCK_SNAP_ID, + false, + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -165,28 +189,27 @@ describe('snap_getState', () => { it('throws if the requesting origin does not have the required permission', async () => { const { implementation } = getStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(false); + const hooks = { getUnlockPromise }; + + const messenger = getMessenger(); - const hooks = { - getSnapState, - getUnlockPromise, - hasPermission, - }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); @@ -199,7 +222,11 @@ describe('snap_getState', () => { params: {}, }); - expect(getSnapState).not.toHaveBeenCalled(); + expect(messenger.call).not.toHaveBeenCalledWith( + 'SnapController:getSnapState', + expect.anything(), + expect.anything(), + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -215,28 +242,22 @@ describe('snap_getState', () => { it('throws if the parameters are invalid', async () => { const { implementation } = getStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - getUnlockPromise, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/getState.ts b/packages/snaps-rpc-methods/src/permitted/getState.ts index aa9ac80b47..b0380f98ad 100644 --- a/packages/snaps-rpc-methods/src/permitted/getState.ts +++ b/packages/snaps-rpc-methods/src/permitted/getState.ts @@ -1,4 +1,9 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { GetStateParams, GetStateResult } from '@metamask/snaps-sdk'; import { type InferMatching } from '@metamask/snaps-utils'; @@ -9,26 +14,34 @@ import { optional, StructError, } from '@metamask/superstruct'; -import type { - PendingJsonRpcResponse, - Json, - JsonRpcRequest, -} from '@metamask/utils'; +import type { PendingJsonRpcResponse, Json } from '@metamask/utils'; import { hasProperty, isObject } from '@metamask/utils'; import { manageStateBuilder } from '../restricted/manageState'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapStateAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; import { FORBIDDEN_KEYS, StateKeyStruct } from '../utils'; -const methodName = 'snap_getState'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - getSnapState: true, +const hookNames: MethodHooksObject = { getUnlockPromise: true, }; +export type GetStateMethodHooks = { + /** + * Wait for the extension to be unlocked. + * + * @returns A promise that resolves once the extension is unlocked. + */ + getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise; +}; + +export type GetStateMethodActions = + | PermissionControllerHasPermissionAction + | SnapControllerGetSnapStateAction; + /** * Get the state of the Snap, or a specific value within the state. By default, * the data is automatically encrypted using a Snap-specific key and @@ -54,39 +67,20 @@ const hookNames: MethodHooksObject = { * ``` */ export const getStateHandler = { - methodNames: [methodName] as const, implementation: getStateImplementation, hookNames, -} satisfies PermittedHandlerExport< - GetStateHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:getSnapState', + ], +} satisfies MethodHandler< + GetStateMethodHooks, + GetStateMethodActions, GetStateParameters, - GetStateResult + GetStateResult, + { origin: string } >; -export type GetStateHooks = { - /** - * Check if the requesting origin has a given permission. - * - * @param permissionName - The name of the permission to check. - * @returns Whether the origin has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * Get the state of the requesting Snap. - * - * @returns The current state of the Snap. - */ - getSnapState: (encrypted: boolean) => Promise>; - - /** - * Wait for the extension to be unlocked. - * - * @returns A promise that resolves once the extension is unlocked. - */ - getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise; -}; - const GetStateParametersStruct = object({ key: optional(StateKeyStruct), encrypted: optional(boolean()), @@ -106,14 +100,12 @@ export type GetStateParameters = InferMatching< * function. * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - Check whether a given origin has a given - * permission. - * @param hooks.getSnapState - Get the state of the requesting Snap. * @param hooks.getUnlockPromise - Wait for the extension to be unlocked. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function getStateImplementation( - request: JsonRpcRequest, + request: JsonRpcRequestWithOrigin, // `GetStateResult` is an alias for `Json` (which is the default type argument // for `PendingJsonRpcResponse`), but that may not be the case in the future. // We use `GetStateResult` here to make it clear that this is the expected @@ -122,11 +114,18 @@ async function getStateImplementation( response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, getSnapState, getUnlockPromise }: GetStateHooks, + { getUnlockPromise }: GetStateMethodHooks, + messenger: Messenger, ): Promise { - const { params } = request; - - if (!hasPermission(manageStateBuilder.targetName)) { + const { params, origin } = request; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + manageStateBuilder.targetName, + ) + ) { return end(providerErrors.unauthorized()); } @@ -138,7 +137,11 @@ async function getStateImplementation( await getUnlockPromise(true); } - const state = await getSnapState(encrypted); + const state = await messenger.call( + 'SnapController:getSnapState', + origin, + encrypted, + ); response.result = get(state, key); } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/getWebSockets.test.ts b/packages/snaps-rpc-methods/src/permitted/getWebSockets.test.ts index 97b54185d6..06f0dfb218 100644 --- a/packages/snaps-rpc-methods/src/permitted/getWebSockets.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/getWebSockets.test.ts @@ -1,44 +1,77 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { rpcErrors } from '@metamask/rpc-errors'; import type { GetWebSocketsParams, GetWebSocketsResult, } from '@metamask/snaps-sdk'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; +import type { GetWebSocketsMethodActions } from './getWebSockets'; import { getWebSocketsHandler } from './getWebSockets'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_getWebSockets', () => { describe('openWebSocketHandler', () => { it('has the expected shape', () => { expect(getWebSocketsHandler).toMatchObject({ - methodNames: ['snap_getWebSockets'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - getWebSockets: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'WebSocketService:getAll', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + GetWebSocketsMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler('WebSocketService:getAll', () => [ + { id: 'foo', url: 'wss://metamask.io', protocols: [] }, + ]); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission', async () => { const { implementation } = getWebSocketsHandler; - const getWebSockets = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(false); - const hooks = { hasPermission, getWebSockets }; + const messenger = getMessenger(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -65,21 +98,19 @@ describe('snap_getWebSockets', () => { it('returns the open WebSocket connections', async () => { const { implementation } = getWebSocketsHandler; - const getWebSockets = jest - .fn() - .mockReturnValue([{ id: 'foo', url: 'wss://metamask.io' }]); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, getWebSockets }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -94,29 +125,34 @@ describe('snap_getWebSockets', () => { expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, - result: [{ id: 'foo', url: 'wss://metamask.io' }], + result: [{ id: 'foo', url: 'wss://metamask.io', protocols: [] }], }); - expect(getWebSockets).toHaveBeenCalledWith(); + expect(messenger.call).toHaveBeenCalledWith( + 'WebSocketService:getAll', + MOCK_SNAP_ID, + ); }); - it('returns an error thrown by the hook', async () => { + it('returns an error thrown by the action', async () => { const { implementation } = getWebSocketsHandler; - const getWebSockets = jest.fn().mockImplementation(() => { + const messenger = getMessenger(); + + messenger.registerActionHandler('WebSocketService:getAll', () => { throw rpcErrors.internal(); }); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, getWebSockets }; const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/getWebSockets.ts b/packages/snaps-rpc-methods/src/permitted/getWebSockets.ts index 5c840bdf9b..0a5d1d09c0 100644 --- a/packages/snaps-rpc-methods/src/permitted/getWebSockets.ts +++ b/packages/snaps-rpc-methods/src/permitted/getWebSockets.ts @@ -1,27 +1,25 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors } from '@metamask/rpc-errors'; import type { GetWebSocketsParams, GetWebSocketsResult, - JsonRpcRequest, } from '@metamask/snaps-sdk'; import type { PendingJsonRpcResponse } from '@metamask/utils'; import { SnapEndowments } from '../endowments'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; - -const methodName = 'snap_getWebSockets'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - getWebSockets: true, -}; +import type { + JsonRpcRequestWithOrigin, + WebSocketServiceGetAllAction, +} from '../types'; -export type GetWebSocketsMethodHooks = { - hasPermission: (permissionName: string) => boolean; - getWebSockets: () => GetWebSocketsResult; -}; +export type GetWebSocketsMethodActions = + | PermissionControllerHasPermissionAction + | WebSocketServiceGetAllAction; /** * Get the connected WebSockets for the Snap. @@ -53,40 +51,52 @@ export type GetWebSocketsMethodHooks = { * ``` */ export const getWebSocketsHandler = { - methodNames: [methodName] as const, implementation: getWebSocketsImplementation, - hookNames, -} satisfies PermittedHandlerExport< - GetWebSocketsMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'WebSocketService:getAll', + ], +} satisfies MethodHandler< + never, + GetWebSocketsMethodActions, GetWebSocketsParams, - GetWebSocketsResult + GetWebSocketsResult, + { origin: string } >; /** * The `snap_getWebSockets` method implementation. * - * @param _req - The JSON-RPC request object. Not used by this function. + * @param req - The JSON-RPC request object. * @param res - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. Not used by this function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if a snap has the `endowment:network-access` permission. - * @param hooks.getWebSockets - The function to get the connected WebSockets for the origin. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getWebSocketsImplementation( - _req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, getWebSockets }: GetWebSocketsMethodHooks, + _hooks: never, + messenger: Messenger, ): void { - if (!hasPermission(SnapEndowments.NetworkAccess)) { + const { origin } = req; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + SnapEndowments.NetworkAccess, + ) + ) { return end(providerErrors.unauthorized()); } try { - res.result = getWebSockets(); + res.result = messenger.call('WebSocketService:getAll', origin); } catch (error) { return end(error); } diff --git a/packages/snaps-rpc-methods/src/permitted/handlers.ts b/packages/snaps-rpc-methods/src/permitted/handlers.ts deleted file mode 100644 index fd59dfb0d5..0000000000 --- a/packages/snaps-rpc-methods/src/permitted/handlers.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { cancelBackgroundEventHandler } from './cancelBackgroundEvent'; -import { clearStateHandler } from './clearState'; -import { closeWebSocketHandler } from './closeWebSocket'; -import { createInterfaceHandler } from './createInterface'; -import { endTraceHandler } from './endTrace'; -import { getAllSnapsHandler } from './getAllSnaps'; -import { getBackgroundEventsHandler } from './getBackgroundEvents'; -import { getClientStatusHandler } from './getClientStatus'; -import { getFileHandler } from './getFile'; -import { getInterfaceContextHandler } from './getInterfaceContext'; -import { getInterfaceStateHandler } from './getInterfaceState'; -import { getSnapsHandler } from './getSnaps'; -import { getStateHandler } from './getState'; -import { getWebSocketsHandler } from './getWebSockets'; -import { invokeKeyringHandler } from './invokeKeyring'; -import { invokeSnapSugarHandler } from './invokeSnapSugar'; -import { listEntropySourcesHandler } from './listEntropySources'; -import { openWebSocketHandler } from './openWebSocket'; -import { requestSnapsHandler } from './requestSnaps'; -import { resolveInterfaceHandler } from './resolveInterface'; -import { scheduleBackgroundEventHandler } from './scheduleBackgroundEvent'; -import { sendWebSocketMessageHandler } from './sendWebSocketMessage'; -import { setStateHandler } from './setState'; -import { startTraceHandler } from './startTrace'; -import { trackErrorHandler } from './trackError'; -import { trackEventHandler } from './trackEvent'; -import { updateInterfaceHandler } from './updateInterface'; - -/* eslint-disable @typescript-eslint/naming-convention */ -export const methodHandlers = { - wallet_getAllSnaps: getAllSnapsHandler, - wallet_getSnaps: getSnapsHandler, - wallet_requestSnaps: requestSnapsHandler, - wallet_invokeSnap: invokeSnapSugarHandler, - wallet_invokeKeyring: invokeKeyringHandler, - snap_clearState: clearStateHandler, - snap_getClientStatus: getClientStatusHandler, - snap_getFile: getFileHandler, - snap_getState: getStateHandler, - snap_createInterface: createInterfaceHandler, - snap_updateInterface: updateInterfaceHandler, - snap_getInterfaceState: getInterfaceStateHandler, - snap_getInterfaceContext: getInterfaceContextHandler, - snap_listEntropySources: listEntropySourcesHandler, - snap_resolveInterface: resolveInterfaceHandler, - snap_scheduleBackgroundEvent: scheduleBackgroundEventHandler, - snap_cancelBackgroundEvent: cancelBackgroundEventHandler, - snap_getBackgroundEvents: getBackgroundEventsHandler, - snap_setState: setStateHandler, - snap_trackError: trackErrorHandler, - snap_trackEvent: trackEventHandler, - snap_openWebSocket: openWebSocketHandler, - snap_closeWebSocket: closeWebSocketHandler, - snap_sendWebSocketMessage: sendWebSocketMessageHandler, - snap_getWebSockets: getWebSocketsHandler, - snap_startTrace: startTraceHandler, - snap_endTrace: endTraceHandler, -}; -/* eslint-enable @typescript-eslint/naming-convention */ - -export const handlers = Object.values(methodHandlers); diff --git a/packages/snaps-rpc-methods/src/permitted/index.ts b/packages/snaps-rpc-methods/src/permitted/index.ts index 1c0b8f709d..2d864fe9e1 100644 --- a/packages/snaps-rpc-methods/src/permitted/index.ts +++ b/packages/snaps-rpc-methods/src/permitted/index.ts @@ -1,50 +1,86 @@ -import type { CancelBackgroundEventMethodHooks } from './cancelBackgroundEvent'; -import type { ClearStateHooks } from './clearState'; -import type { CloseWebSocketMethodHooks } from './closeWebSocket'; -import type { CreateInterfaceMethodHooks } from './createInterface'; -import type { EndTraceMethodHooks } from './endTrace'; -import type { GetAllSnapsHooks } from './getAllSnaps'; -import type { GetBackgroundEventsMethodHooks } from './getBackgroundEvents'; -import type { GetClientStatusHooks } from './getClientStatus'; -import type { GetInterfaceStateMethodHooks } from './getInterfaceState'; -import type { GetSnapsHooks } from './getSnaps'; -import type { GetStateHooks } from './getState'; -import type { GetWebSocketsMethodHooks } from './getWebSockets'; -import type { ListEntropySourcesHooks } from './listEntropySources'; -import type { OpenWebSocketMethodHooks } from './openWebSocket'; -import type { RequestSnapsHooks } from './requestSnaps'; -import type { ResolveInterfaceMethodHooks } from './resolveInterface'; -import type { ScheduleBackgroundEventMethodHooks } from './scheduleBackgroundEvent'; -import type { SendWebSocketMessageMethodHooks } from './sendWebSocketMessage'; -import type { SetStateHooks } from './setState'; -import type { StartTraceMethodHooks } from './startTrace'; -import type { TrackErrorMethodHooks } from './trackError'; -import type { TrackEventMethodHooks } from './trackEvent'; -import type { UpdateInterfaceMethodHooks } from './updateInterface'; +import type { CancelBackgroundEventMethodActions } from './cancelBackgroundEvent'; +import type { ClearStateMethodActions } from './clearState'; +import type { CloseWebSocketMethodActions } from './closeWebSocket'; +import type { CreateInterfaceMethodActions } from './createInterface'; +import type { EndTraceMethodActions, EndTraceMethodHooks } from './endTrace'; +import type { GetAllSnapsMethodActions } from './getAllSnaps'; +import type { GetBackgroundEventsMethodActions } from './getBackgroundEvents'; +import type { + GetClientStatusMethodActions, + GetClientStatusMethodHooks, +} from './getClientStatus'; +import type { GetFileMethodActions } from './getFile'; +import type { GetInterfaceContextMethodActions } from './getInterfaceContext'; +import type { GetInterfaceStateMethodActions } from './getInterfaceState'; +import type { GetSnapsMethodActions } from './getSnaps'; +import type { GetStateMethodActions, GetStateMethodHooks } from './getState'; +import type { GetWebSocketsMethodActions } from './getWebSockets'; +import type { + InvokeKeyringMethodActions, + InvokeKeyringMethodHooks, +} from './invokeKeyring'; +import type { InvokeSnapSugarMethodActions } from './invokeSnapSugar'; +import type { + ListEntropySourcesMethodActions, + ListEntropySourcesMethodHooks, +} from './listEntropySources'; +import type { OpenWebSocketMethodActions } from './openWebSocket'; +import type { RequestSnapsMethodActions } from './requestSnaps'; +import type { ResolveInterfaceMethodActions } from './resolveInterface'; +import type { ScheduleBackgroundEventMethodActions } from './scheduleBackgroundEvent'; +import type { SendWebSocketMessageMethodActions } from './sendWebSocketMessage'; +import type { SetStateMethodActions, SetStateMethodHooks } from './setState'; +import type { + StartTraceMethodActions, + StartTraceMethodHooks, +} from './startTrace'; +import type { + TrackErrorMethodActions, + TrackErrorMethodHooks, +} from './trackError'; +import type { + TrackEventMethodActions, + TrackEventMethodHooks, +} from './trackEvent'; +import type { UpdateInterfaceMethodActions } from './updateInterface'; -export type PermittedRpcMethodHooks = ClearStateHooks & - GetAllSnapsHooks & - GetClientStatusHooks & - GetSnapsHooks & - GetStateHooks & - ListEntropySourcesHooks & - RequestSnapsHooks & - CreateInterfaceMethodHooks & - UpdateInterfaceMethodHooks & - GetInterfaceStateMethodHooks & - ResolveInterfaceMethodHooks & - ScheduleBackgroundEventMethodHooks & - CancelBackgroundEventMethodHooks & - GetBackgroundEventsMethodHooks & - SetStateHooks & - OpenWebSocketMethodHooks & - CloseWebSocketMethodHooks & - SendWebSocketMessageMethodHooks & - GetWebSocketsMethodHooks & +export type PermittedRpcMethodActions = + | CancelBackgroundEventMethodActions + | ClearStateMethodActions + | CloseWebSocketMethodActions + | CreateInterfaceMethodActions + | EndTraceMethodActions + | GetAllSnapsMethodActions + | GetBackgroundEventsMethodActions + | GetClientStatusMethodActions + | GetFileMethodActions + | GetInterfaceContextMethodActions + | GetInterfaceStateMethodActions + | GetSnapsMethodActions + | GetStateMethodActions + | GetWebSocketsMethodActions + | InvokeKeyringMethodActions + | InvokeSnapSugarMethodActions + | ListEntropySourcesMethodActions + | OpenWebSocketMethodActions + | RequestSnapsMethodActions + | ResolveInterfaceMethodActions + | ScheduleBackgroundEventMethodActions + | SendWebSocketMessageMethodActions + | SetStateMethodActions + | StartTraceMethodActions + | TrackErrorMethodActions + | TrackEventMethodActions + | UpdateInterfaceMethodActions; + +export type PermittedRpcMethodHooks = GetClientStatusMethodHooks & + GetStateMethodHooks & + ListEntropySourcesMethodHooks & + SetStateMethodHooks & TrackEventMethodHooks & TrackErrorMethodHooks & StartTraceMethodHooks & - EndTraceMethodHooks; + EndTraceMethodHooks & + InvokeKeyringMethodHooks; -export * from './handlers'; export * from './middleware'; diff --git a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts index 70b266195b..b7ab0c6ab4 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts @@ -1,66 +1,84 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { rpcErrors } from '@metamask/rpc-errors'; import type { InvokeKeyringParams } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; -import { MOCK_SNAP_ID, getSnapObject } from '@metamask/snaps-utils/test-utils'; -import type { - JsonRpcRequest, - JsonRpcFailure, - JsonRpcSuccess, -} from '@metamask/utils'; - +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; +import type { JsonRpcFailure, JsonRpcSuccess } from '@metamask/utils'; + +import type { InvokeKeyringMethodActions } from './invokeKeyring'; import { invokeKeyringHandler } from './invokeKeyring'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('wallet_invokeKeyring', () => { describe('invokeKeyringHandler', () => { it('has the expected shape', () => { expect(invokeKeyringHandler).toMatchObject({ - methodNames: ['wallet_invokeKeyring'], implementation: expect.any(Function), hookNames: { - getSnap: true, - handleSnapRpcRequest: true, - hasPermission: true, + getAllowedKeyringMethods: true, }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:handleRequest', + 'SnapController:getSnap', + ], }); }); }); + describe('invokeKeyringImplementation', () => { - // Mirror the origin middleware in the extension - const createOriginMiddleware = - (origin: string) => - (request: any, _response: unknown, next: () => void, _end: unknown) => { - request.origin = origin; - next(); - }; + const getMessenger = () => { + const messenger = new MockControllerMessenger< + InvokeKeyringMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler('SnapController:getSnap', () => + getSnapObject(), + ); + + messenger.registerActionHandler( + 'SnapController:handleRequest', + async () => 'bar', + ); - const getMockHooks = () => - ({ - getSnap: jest.fn(), - hasPermission: jest.fn(), - handleSnapRpcRequest: jest.fn(), - getAllowedKeyringMethods: jest.fn(), - }) as any; + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + const getMockHooks = () => ({ + getAllowedKeyringMethods: jest.fn().mockReturnValue(['foo']), + }); it('invokes the snap and returns the result', async () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); - - hooks.hasPermission.mockImplementation(() => true); - hooks.getSnap.mockImplementation(() => getSnapObject()); - hooks.handleSnapRpcRequest.mockImplementation(() => 'bar'); - hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res, next, end, hooks, + messenger, ); result?.catch(end); @@ -77,36 +95,42 @@ describe('wallet_invokeKeyring', () => { })) as JsonRpcSuccess; expect(response.result).toBe('bar'); - expect(hooks.handleSnapRpcRequest).toHaveBeenCalledWith({ - handler: HandlerType.OnKeyringRequest, - request: { method: 'foo' }, - snapId: MOCK_SNAP_ID, - }); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:handleRequest', + { + handler: HandlerType.OnKeyringRequest, + origin: 'metamask.io', + request: { method: 'foo' }, + snapId: MOCK_SNAP_ID, + }, + ); }); it('fails if invoking the snap fails', async () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); + const messenger = getMessenger(); - hooks.hasPermission.mockImplementation(() => true); - hooks.getSnap.mockImplementation(() => getSnapObject()); - hooks.handleSnapRpcRequest.mockImplementation(() => { - throw rpcErrors.invalidRequest({ - message: 'Failed to start snap.', - }); - }); - hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); + messenger.registerActionHandler( + 'SnapController:handleRequest', + async () => { + throw rpcErrors.invalidRequest({ + message: 'Failed to start snap.', + }); + }, + ); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res, next, end, hooks, + messenger, ); result?.catch(end); @@ -135,26 +159,21 @@ describe('wallet_invokeKeyring', () => { it('fails if origin is not authorized to call the method', async () => { const { implementation } = invokeKeyringHandler; - const hooks = getMockHooks(); - - hooks.hasPermission.mockImplementation(() => true); - hooks.getSnap.mockImplementation(() => getSnapObject()); - hooks.handleSnapRpcRequest.mockImplementation(() => { - throw rpcErrors.invalidRequest({ - message: 'Failed to start snap.', - }); - }); - hooks.getAllowedKeyringMethods.mockImplementation(() => ['bar']); + const hooks = { + getAllowedKeyringMethods: jest.fn().mockReturnValue(['bar']), + }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res, next, end, hooks, + messenger, ); result?.catch(end); @@ -185,25 +204,18 @@ describe('wallet_invokeKeyring', () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); - - hooks.hasPermission.mockImplementation(() => true); - hooks.getSnap.mockImplementation(() => getSnapObject()); - hooks.handleSnapRpcRequest.mockImplementation(() => { - throw rpcErrors.invalidRequest({ - message: 'Failed to start snap.', - }); - }); - hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res, next, end, hooks, + messenger, ); result?.catch(end); @@ -231,18 +243,23 @@ describe('wallet_invokeKeyring', () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); + const messenger = getMessenger(); - hooks.hasPermission.mockImplementation(() => false); + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res, next, end, hooks, + messenger, ); result?.catch(end); @@ -272,19 +289,20 @@ describe('wallet_invokeKeyring', () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); + const messenger = getMessenger(); - hooks.hasPermission.mockImplementation(() => true); - hooks.getSnap.mockImplementation(() => undefined); + messenger.registerActionHandler('SnapController:getSnap', () => null); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res, next, end, hooks, + messenger, ); result?.catch(end); @@ -314,15 +332,17 @@ describe('wallet_invokeKeyring', () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts index 4c2dbca4e6..22c779ebcc 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts @@ -1,32 +1,45 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { InvokeKeyringParams, InvokeKeyringResult, InvokeSnapParams, } from '@metamask/snaps-sdk'; -import type { Snap, SnapRpcHookArgs } from '@metamask/snaps-utils'; import { HandlerType, WALLET_SNAP_PERMISSION_KEY } from '@metamask/snaps-utils'; -import type { - PendingJsonRpcResponse, - Json, - JsonRpcRequest, -} from '@metamask/utils'; +import type { PendingJsonRpcResponse, Json } from '@metamask/utils'; import { hasProperty } from '@metamask/utils'; import { getValidatedParams } from './invokeSnapSugar'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, + SnapControllerHandleRequestAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; -const methodName = 'wallet_invokeKeyring'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - handleSnapRpcRequest: true, - getSnap: true, +const hookNames: MethodHooksObject = { getAllowedKeyringMethods: true, }; +export type InvokeKeyringMethodHooks = { + /** + * Get the list of allowed Keyring methods for a given origin. + * + * @returns The list of allowed Keyring methods. + */ + getAllowedKeyringMethods: () => string[]; +}; + +export type InvokeKeyringMethodActions = + | PermissionControllerHasPermissionAction + | SnapControllerHandleRequestAction + | SnapControllerGetSnapAction; + /** * Invoke a keyring method of a Snap. This calls the `onKeyringRequest` handler * of the Snap. @@ -36,29 +49,21 @@ const hookNames: MethodHooksObject = { * request permission to communicate with it using [`wallet_requestSnaps`](https://docs.metamask.io/snaps/reference/snaps-api/wallet_requestsnaps). */ export const invokeKeyringHandler = { - methodNames: [methodName] as const, implementation: invokeKeyringImplementation, hookNames, -} satisfies PermittedHandlerExport< - InvokeKeyringHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:handleRequest', + 'SnapController:getSnap', + ], +} satisfies MethodHandler< + InvokeKeyringMethodHooks, + InvokeKeyringMethodActions, InvokeSnapParams, - InvokeKeyringResult + InvokeKeyringResult, + { origin: string } >; -export type InvokeKeyringHooks = { - hasPermission: (permissionName: string) => boolean; - - handleSnapRpcRequest: ({ - snapId, - handler, - request, - }: Omit & { snapId: string }) => Promise; - - getSnap: (snapId: string) => Snap | undefined; - - getAllowedKeyringMethods: () => string[]; -}; - /** * The `wallet_invokeKeyring` method implementation. * Invokes onKeyringRequest if the snap requested is installed and connected to the dapp. @@ -69,15 +74,13 @@ export type InvokeKeyringHooks = { * function. * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. - * @param hooks.handleSnapRpcRequest - Invokes a snap with a given RPC request. - * @param hooks.hasPermission - Checks whether a given origin has a given permission. - * @param hooks.getSnap - Gets information about a given snap. * @param hooks.getAllowedKeyringMethods - Get the list of allowed Keyring * methods for a given origin. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function invokeKeyringImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, // `InvokeKeyringResult` is an alias for `Json` (which is the default type // argument for `PendingJsonRpcResponse`), but that may not be the case in the // future. We use `InvokeKeyringResult` here to make it clear that this is the @@ -86,12 +89,8 @@ async function invokeKeyringImplementation( res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { - handleSnapRpcRequest, - hasPermission, - getSnap, - getAllowedKeyringMethods, - }: InvokeKeyringHooks, + { getAllowedKeyringMethods }: InvokeKeyringMethodHooks, + messenger: Messenger, ): Promise { let params: InvokeSnapParams; try { @@ -100,11 +99,17 @@ async function invokeKeyringImplementation( return end(error); } - // We expect the MM middleware stack to always add the origin to requests - const { origin } = req as JsonRpcRequest & { origin: string }; + const { origin } = req; const { snapId, request } = params; - if (!origin || !hasPermission(WALLET_SNAP_PERMISSION_KEY)) { + if ( + !origin || + !messenger.call( + 'PermissionController:hasPermission', + origin, + WALLET_SNAP_PERMISSION_KEY, + ) + ) { return end( rpcErrors.invalidRequest({ message: `The snap "${snapId}" is not connected to "${origin}". Please connect before invoking the snap.`, @@ -112,7 +117,7 @@ async function invokeKeyringImplementation( ); } - if (!getSnap(snapId)) { + if (!messenger.call('SnapController:getSnap', snapId)) { return end( // Mirror error message from SnapController. rpcErrors.invalidRequest({ @@ -139,7 +144,8 @@ async function invokeKeyringImplementation( } try { - res.result = (await handleSnapRpcRequest({ + res.result = (await messenger.call('SnapController:handleRequest', { + origin, snapId, request, handler: HandlerType.OnKeyringRequest, diff --git a/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.test.ts b/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.test.ts index b72271e5b1..61adeed941 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.test.ts @@ -4,19 +4,24 @@ import type { } from '@metamask/json-rpc-engine'; import { rpcErrors } from '@metamask/rpc-errors'; import type { InvokeSnapParams } from '@metamask/snaps-sdk'; +import { MockControllerMessenger } from '@metamask/snaps-utils/test-utils'; import type { PendingJsonRpcResponse } from '@metamask/utils'; import { assertIsJsonRpcSuccess, jsonrpc2 } from '@metamask/utils'; +import type { InvokeSnapSugarMethodActions } from './invokeSnapSugar'; import { getValidatedParams, invokeSnapSugar } from './invokeSnapSugar'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('wallet_invokeSnap', () => { describe('invokeSnapSugar', () => { - const getMockRpcRequest = (params: InvokeSnapParams) => ({ - id: 'some-id', - jsonrpc: jsonrpc2, - method: 'wallet_invokeSnap', - params, - }); + const getMockRpcRequest = (params: InvokeSnapParams) => + ({ + id: 'some-id', + jsonrpc: jsonrpc2, + method: 'wallet_invokeSnap', + params, + origin: 'https://example.com', + }) as JsonRpcRequestWithOrigin; const getMockRpcResponse = () => ({ @@ -24,7 +29,23 @@ describe('wallet_invokeSnap', () => { jsonrpc: jsonrpc2, }) as PendingJsonRpcResponse; - it('invokes snap using hook', async () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + InvokeSnapSugarMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:executeRestrictedMethod', + async () => true, + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('invokes snap via the messenger', async () => { const params = { snapId: 'npm:@metamask/example-snap', request: { method: 'hello' }, @@ -33,14 +54,19 @@ describe('wallet_invokeSnap', () => { const res = getMockRpcResponse(); const next: JsonRpcEngineNextCallback = jest.fn(); const end: JsonRpcEngineEndCallback = jest.fn(); - const invokeSnap = jest.fn().mockResolvedValue(true); - await invokeSnapSugar(req, res, next, end, { invokeSnap }); + const messenger = getMessenger(); + + await invokeSnapSugar(req, res, next, end, {} as never, messenger); assertIsJsonRpcSuccess(res); expect(next).not.toHaveBeenCalled(); - expect(invokeSnap).toHaveBeenCalledTimes(1); - expect(invokeSnap).toHaveBeenCalledWith({ ...params }); + expect(messenger.call).toHaveBeenCalledWith( + 'PermissionController:executeRestrictedMethod', + 'https://example.com', + 'wallet_snap', + params, + ); expect(end).toHaveBeenCalledTimes(1); }); @@ -54,9 +80,10 @@ describe('wallet_invokeSnap', () => { const res = getMockRpcResponse(); const next: JsonRpcEngineNextCallback = jest.fn(); const end: JsonRpcEngineEndCallback = jest.fn(); - const invokeSnap = jest.fn(); - await invokeSnapSugar(req, res, next, end, { invokeSnap }); + const messenger = getMessenger(); + + await invokeSnapSugar(req, res, next, end, {} as never, messenger); expect(next).not.toHaveBeenCalled(); expect(end).toHaveBeenCalledTimes(1); @@ -65,7 +92,7 @@ describe('wallet_invokeSnap', () => { message: 'Must specify a valid snap ID.', }), ); - expect(invokeSnap).not.toHaveBeenCalled(); + expect(messenger.call).not.toHaveBeenCalled(); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.ts b/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.ts index 79dd2614bf..442694d586 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeSnapSugar.ts @@ -1,15 +1,20 @@ import type { JsonRpcEngineEndCallback, JsonRpcEngineNextCallback, + MethodHandler, } from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerExecuteRestrictedMethodAction } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { InvokeSnapParams, InvokeSnapResult } from '@metamask/snaps-sdk'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; import { isObject } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; +import { WALLET_SNAP_PERMISSION_KEY } from '../restricted/invokeSnap'; +import type { JsonRpcRequestWithOrigin } from '../types'; -const methodName = 'wallet_invokeSnap'; +export type InvokeSnapSugarMethodActions = + PermissionControllerExecuteRestrictedMethodAction; /** * Invoke a method of a Snap, designated by the `snapId` parameter, with a @@ -37,21 +42,16 @@ const methodName = 'wallet_invokeSnap'; * ``` */ export const invokeSnapSugarHandler = { - methodNames: [methodName] as const, implementation: invokeSnapSugar, - hookNames: { - invokeSnap: true, - }, -} satisfies PermittedHandlerExport< - InvokeSnapSugarHooks, + actionNames: ['PermissionController:executeRestrictedMethod'], +} satisfies MethodHandler< + never, + InvokeSnapSugarMethodActions, InvokeSnapParams, - InvokeSnapResult + InvokeSnapResult, + { origin: string } >; -export type InvokeSnapSugarHooks = { - invokeSnap: (params: InvokeSnapParams) => Promise; -}; - /** * The `wallet_invokeSnap` method implementation. * Effectively calls `wallet_snap` under the hood. @@ -60,14 +60,13 @@ export type InvokeSnapSugarHooks = { * @param res - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.invokeSnap - A function to invoke a snap designated by its parameters, - * bound to the requesting origin. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. * @throws If the params are invalid. */ export async function invokeSnapSugar( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, // `InvokeSnapResult` is an alias for `Json` (which is the default type // argument for `PendingJsonRpcResponse`), but that may not be the case in the // future. We use `InvokeSnapResult` here to make it clear that this is the @@ -76,11 +75,17 @@ export async function invokeSnapSugar( res: PendingJsonRpcResponse, _next: JsonRpcEngineNextCallback, end: JsonRpcEngineEndCallback, - { invokeSnap }: InvokeSnapSugarHooks, + _hooks: never, + messenger: Messenger, ): Promise { try { const params = getValidatedParams(req.params); - res.result = await invokeSnap(params); + res.result = await messenger.call( + 'PermissionController:executeRestrictedMethod', + req.origin, + WALLET_SNAP_PERMISSION_KEY, + params, + ); } catch (error) { return end(error); } diff --git a/packages/snaps-rpc-methods/src/permitted/listEntropySources.test.ts b/packages/snaps-rpc-methods/src/permitted/listEntropySources.test.ts index 591ee3e219..0094187c0f 100644 --- a/packages/snaps-rpc-methods/src/permitted/listEntropySources.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/listEntropySources.test.ts @@ -1,68 +1,98 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { ListEntropySourcesParams, ListEntropySourcesResult, } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { ListEntropySourcesMethodActions } from './listEntropySources'; import { listEntropySourcesHandler } from './listEntropySources'; describe('snap_listEntropySources', () => { describe('listEntropySourcesHandler', () => { it('has the expected shape', () => { expect(listEntropySourcesHandler).toMatchObject({ - methodNames: ['snap_listEntropySources'], implementation: expect.any(Function), hookNames: { - hasPermission: true, - getEntropySources: true, getUnlockPromise: true, }, + actionNames: [ + 'PermissionController:hasPermission', + 'KeyringController:getState', + ], }); }); }); describe('implementation', () => { - it('returns the result from the `getEntropySources` hook', async () => { + const keyrings = [ + { + type: 'HD Key Tree', + metadata: { id: 'baz', name: 'Primary secret recovery phrase' }, + }, + { + type: 'HD Key Tree', + metadata: { id: 'foo', name: 'Secret recovery phrase 1' }, + }, + { + type: 'HD Key Tree', + metadata: { id: 'bar', name: 'Secret recovery phrase 2' }, + }, + { + type: 'Ledger Hardware', + metadata: { id: 'ledger', name: 'Ledger' }, + }, + ]; + + const getMessenger = () => { + const messenger = new MockControllerMessenger< + ListEntropySourcesMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler('KeyringController:getState', () => ({ + isUnlocked: true, + keyrings, + })); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('returns the result derived from the `KeyringController:getState` action', async () => { const { implementation } = listEntropySourcesHandler; const getUnlockPromise = jest.fn(); - const getEntropySources = jest.fn().mockReturnValue([ - { - name: 'Secret recovery phrase 1', - id: 'foo', - type: 'mnemonic', - primary: false, - }, - { - name: 'Secret recovery phrase 2', - id: 'bar', - type: 'mnemonic', - primary: false, - }, - { - name: 'Primary secret recovery phrase', - id: 'baz', - type: 'mnemonic', - primary: true, - }, - ]); + const hooks = { getUnlockPromise }; - const hooks = { - hasPermission: jest.fn().mockReturnValue(true), - getEntropySources, - getUnlockPromise, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -79,6 +109,12 @@ describe('snap_listEntropySources', () => { jsonrpc: '2.0', id: 1, result: [ + { + name: 'Primary secret recovery phrase', + id: 'baz', + type: 'mnemonic', + primary: true, + }, { name: 'Secret recovery phrase 1', id: 'foo', @@ -91,12 +127,6 @@ describe('snap_listEntropySources', () => { type: 'mnemonic', primary: false, }, - { - name: 'Primary secret recovery phrase', - id: 'baz', - type: 'mnemonic', - primary: true, - }, ], }); }); @@ -105,21 +135,28 @@ describe('snap_listEntropySources', () => { const { implementation } = listEntropySourcesHandler; const getUnlockPromise = jest.fn(); - const hooks = { - hasPermission: jest.fn().mockReturnValue(false), - getEntropySources: jest.fn(), - getUnlockPromise, - }; + const hooks = { getUnlockPromise }; + + const messenger = getMessenger(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/listEntropySources.ts b/packages/snaps-rpc-methods/src/permitted/listEntropySources.ts index 66c5f80645..dc05169064 100644 --- a/packages/snaps-rpc-methods/src/permitted/listEntropySources.ts +++ b/packages/snaps-rpc-methods/src/permitted/listEntropySources.ts @@ -1,8 +1,12 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors } from '@metamask/rpc-errors'; import type { EntropySource, - JsonRpcRequest, ListEntropySourcesParams, ListEntropySourcesResult, } from '@metamask/snaps-sdk'; @@ -12,7 +16,10 @@ import { getBip32EntropyBuilder } from '../restricted/getBip32Entropy'; import { getBip32PublicKeyBuilder } from '../restricted/getBip32PublicKey'; import { getBip44EntropyBuilder } from '../restricted/getBip44Entropy'; import { getEntropyBuilder } from '../restricted/getEntropy'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + KeyringControllerGetStateAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; /** @@ -26,30 +33,16 @@ const REQUIRED_PERMISSIONS = [ getEntropyBuilder.targetName, ]; -const methodName = 'snap_listEntropySources'; +/** + * The keyring type used by HD (mnemonic-based) keyrings. + */ +const HD_KEYRING_TYPE = 'HD Key Tree'; -const hookNames: MethodHooksObject = { - hasPermission: true, - getEntropySources: true, +const hookNames: MethodHooksObject = { getUnlockPromise: true, }; -export type ListEntropySourcesHooks = { - /** - * Check if the requesting origin has a given permission. - * - * @param permissionName - The name of the permission to check. - * @returns Whether the origin has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * Get the entropy sources from the client. - * - * @returns The entropy sources. - */ - getEntropySources: () => EntropySource[]; - +export type ListEntropySourcesMethodHooks = { /** * Wait for the extension to be unlocked. * @@ -58,6 +51,10 @@ export type ListEntropySourcesHooks = { getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise; }; +export type ListEntropySourcesMethodActions = + | PermissionControllerHasPermissionAction + | KeyringControllerGetStateAction; + /** * Get a list of entropy sources available to the Snap. The requesting origin * must have at least one of the following permissions to access entropy source @@ -97,48 +94,71 @@ export type ListEntropySourcesHooks = { * ``` */ export const listEntropySourcesHandler = { - methodNames: [methodName] as const, implementation: listEntropySourcesImplementation, hookNames, -} satisfies PermittedHandlerExport< - ListEntropySourcesHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'KeyringController:getState', + ], +} satisfies MethodHandler< + ListEntropySourcesMethodHooks, + ListEntropySourcesMethodActions, ListEntropySourcesParams, - ListEntropySourcesResult + ListEntropySourcesResult, + { origin: string } >; /** * The `snap_listEntropySources` method implementation. * - * @param _request - The JSON-RPC request object. Not used by this function. + * @param request - The JSON-RPC request object. * @param response - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if the origin has a - * permission. - * @param hooks.getEntropySources - The function to get the entropy sources. * @param hooks.getUnlockPromise - The function to get the unlock promise. - * @returns Noting. + * @param messenger - The messenger used to call controller actions. + * @returns Nothing. */ async function listEntropySourcesImplementation( - _request: JsonRpcRequest, + request: JsonRpcRequestWithOrigin, response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { - hasPermission, - getEntropySources, - getUnlockPromise, - }: ListEntropySourcesHooks, + { getUnlockPromise }: ListEntropySourcesMethodHooks, + messenger: Messenger, ): Promise { - const isPermitted = REQUIRED_PERMISSIONS.some(hasPermission); + const { origin } = request; + + const isPermitted = REQUIRED_PERMISSIONS.some((permission) => + messenger.call('PermissionController:hasPermission', origin, permission), + ); + if (!isPermitted) { return end(providerErrors.unauthorized()); } await getUnlockPromise(true); - response.result = getEntropySources(); + const { keyrings } = messenger.call('KeyringController:getState'); + + response.result = keyrings + .map((keyring, index) => { + if (keyring.type === HD_KEYRING_TYPE) { + return { + id: keyring.metadata.id, + name: keyring.metadata.name, + type: 'mnemonic', + primary: index === 0, + }; + } + + return null; + }) + .filter((entropySource): entropySource is EntropySource => + Boolean(entropySource), + ); + return end(); } diff --git a/packages/snaps-rpc-methods/src/permitted/middleware.test.ts b/packages/snaps-rpc-methods/src/permitted/middleware.test.ts index 4b9630d16f..83f4144450 100644 --- a/packages/snaps-rpc-methods/src/permitted/middleware.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/middleware.test.ts @@ -1,18 +1,41 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import type { MockAnyNamespace } from '@metamask/messenger'; +import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger'; import { rpcErrors, serializeError } from '@metamask/rpc-errors'; import { MOCK_SNAP_ID, getTruncatedSnap, } from '@metamask/snaps-utils/test-utils'; +import type { PermittedRpcMethodActions } from '.'; import { createSnapsMethodMiddleware } from './middleware'; describe('createSnapsMethodMiddleware', () => { - it('supports wallet_getSnaps', async () => { - const middleware = createSnapsMethodMiddleware(true, { - getSnaps: () => ({ [MOCK_SNAP_ID]: getTruncatedSnap() }), + const getHooks = () => ({ + getAllowedKeyringMethods: jest.fn(), + getIsActive: jest.fn(), + getVersion: jest.fn(), + getUnlockPromise: jest.fn(), + trackError: jest.fn(), + trackEvent: jest.fn(), + startTrace: jest.fn(), + endTrace: jest.fn(), + }); + + const getMessenger = () => + new Messenger({ + namespace: MOCK_ANY_NAMESPACE, }); + it('supports wallet_getSnaps', async () => { + const messenger = getMessenger(); + + messenger.registerActionHandler('SnapController:getPermittedSnaps', () => ({ + [MOCK_SNAP_ID]: getTruncatedSnap(), + })); + + const middleware = createSnapsMethodMiddleware(true, getHooks(), messenger); + const engine = new JsonRpcEngine(); engine.push(middleware); @@ -32,7 +55,11 @@ describe('createSnapsMethodMiddleware', () => { }); it('blocks snap_ prefixed RPC methods for non-snaps', async () => { - const middleware = createSnapsMethodMiddleware(false, {}); + const middleware = createSnapsMethodMiddleware( + false, + getHooks(), + getMessenger(), + ); const engine = new JsonRpcEngine(); @@ -57,12 +84,14 @@ describe('createSnapsMethodMiddleware', () => { }); it('handles errors', async () => { - const middleware = createSnapsMethodMiddleware(true, { - getSnaps: () => { - throw new Error('foo'); - }, + const messenger = getMessenger(); + + messenger.registerActionHandler('SnapController:getPermittedSnaps', () => { + throw new Error('foo'); }); + const middleware = createSnapsMethodMiddleware(true, getHooks(), messenger); + const engine = new JsonRpcEngine(); engine.push(middleware); @@ -88,7 +117,11 @@ describe('createSnapsMethodMiddleware', () => { }); it('ignores unknown methods', async () => { - const middleware = createSnapsMethodMiddleware(true, {}); + const middleware = createSnapsMethodMiddleware( + true, + getHooks(), + getMessenger(), + ); const engine = new JsonRpcEngine(); diff --git a/packages/snaps-rpc-methods/src/permitted/middleware.ts b/packages/snaps-rpc-methods/src/permitted/middleware.ts index 89d2664db4..ea7a1c9ad0 100644 --- a/packages/snaps-rpc-methods/src/permitted/middleware.ts +++ b/packages/snaps-rpc-methods/src/permitted/middleware.ts @@ -1,52 +1,100 @@ -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; +import { + createMethodMiddleware, + type JsonRpcMiddleware, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import { rpcErrors } from '@metamask/rpc-errors'; import { logError } from '@metamask/snaps-utils'; import type { Json, JsonRpcParams } from '@metamask/utils'; -import { methodHandlers } from './handlers'; -import { selectHooks } from '../utils'; +import type { PermittedRpcMethodActions, PermittedRpcMethodHooks } from '.'; +import { cancelBackgroundEventHandler } from './cancelBackgroundEvent'; +import { clearStateHandler } from './clearState'; +import { closeWebSocketHandler } from './closeWebSocket'; +import { createInterfaceHandler } from './createInterface'; +import { endTraceHandler } from './endTrace'; +import { getAllSnapsHandler } from './getAllSnaps'; +import { getBackgroundEventsHandler } from './getBackgroundEvents'; +import { getClientStatusHandler } from './getClientStatus'; +import { getFileHandler } from './getFile'; +import { getInterfaceContextHandler } from './getInterfaceContext'; +import { getInterfaceStateHandler } from './getInterfaceState'; +import { getSnapsHandler } from './getSnaps'; +import { getStateHandler } from './getState'; +import { getWebSocketsHandler } from './getWebSockets'; +import { invokeKeyringHandler } from './invokeKeyring'; +import { invokeSnapSugarHandler } from './invokeSnapSugar'; +import { listEntropySourcesHandler } from './listEntropySources'; +import { openWebSocketHandler } from './openWebSocket'; +import { requestSnapsHandler } from './requestSnaps'; +import { resolveInterfaceHandler } from './resolveInterface'; +import { scheduleBackgroundEventHandler } from './scheduleBackgroundEvent'; +import { sendWebSocketMessageHandler } from './sendWebSocketMessage'; +import { setStateHandler } from './setState'; +import { startTraceHandler } from './startTrace'; +import { trackErrorHandler } from './trackError'; +import { trackEventHandler } from './trackEvent'; +import { updateInterfaceHandler } from './updateInterface'; + +/* eslint-disable @typescript-eslint/naming-convention */ +const methodHandlers = { + wallet_getAllSnaps: getAllSnapsHandler, + wallet_getSnaps: getSnapsHandler, + wallet_requestSnaps: requestSnapsHandler, + wallet_invokeSnap: invokeSnapSugarHandler, + wallet_invokeKeyring: invokeKeyringHandler, + snap_clearState: clearStateHandler, + snap_getClientStatus: getClientStatusHandler, + snap_getFile: getFileHandler, + snap_getState: getStateHandler, + snap_createInterface: createInterfaceHandler, + snap_updateInterface: updateInterfaceHandler, + snap_getInterfaceState: getInterfaceStateHandler, + snap_getInterfaceContext: getInterfaceContextHandler, + snap_listEntropySources: listEntropySourcesHandler, + snap_resolveInterface: resolveInterfaceHandler, + snap_scheduleBackgroundEvent: scheduleBackgroundEventHandler, + snap_cancelBackgroundEvent: cancelBackgroundEventHandler, + snap_getBackgroundEvents: getBackgroundEventsHandler, + snap_setState: setStateHandler, + snap_trackError: trackErrorHandler, + snap_trackEvent: trackEventHandler, + snap_openWebSocket: openWebSocketHandler, + snap_closeWebSocket: closeWebSocketHandler, + snap_sendWebSocketMessage: sendWebSocketMessageHandler, + snap_getWebSockets: getWebSocketsHandler, + snap_startTrace: startTraceHandler, + snap_endTrace: endTraceHandler, +}; +/* eslint-enable @typescript-eslint/naming-convention */ /** * Creates a middleware that handles permitted snap RPC methods. * * @param isSnap - A flag that should indicate whether the requesting origin is a snap or not. * @param hooks - An object containing the hooks made available to the permitted RPC methods. + * @param messenger - The messenger. * @returns The middleware. */ export function createSnapsMethodMiddleware( isSnap: boolean, - hooks: Record, + hooks: PermittedRpcMethodHooks, + messenger: Messenger, ): JsonRpcMiddleware { + const nestedMiddleware = createMethodMiddleware({ + handlers: methodHandlers, + hooks, + messenger, + onError: logError, + }); + // This is not actually a misused promise, the type is just wrong // eslint-disable-next-line @typescript-eslint/no-misused-promises return async function methodMiddleware(request, response, next, end) { - const handler = - methodHandlers[request.method as keyof typeof methodHandlers]; - if (handler) { - if ( - String.prototype.startsWith.call(request.method, 'snap_') && - !isSnap - ) { - return end(rpcErrors.methodNotFound()); - } - - // TODO: Once json-rpc-engine types are up to date, we should type this correctly - const { implementation, hookNames } = handler as any; - try { - // Implementations may or may not be async, so we must await them. - return await implementation( - request, - response, - next, - end, - selectHooks(hooks, hookNames), - ); - } catch (error) { - logError(error); - return end(error); - } + if (String.prototype.startsWith.call(request.method, 'snap_') && !isSnap) { + return end(rpcErrors.methodNotFound()); } - return next(); + return nestedMiddleware(request, response, next, end); }; } diff --git a/packages/snaps-rpc-methods/src/permitted/openWebSocket.test.ts b/packages/snaps-rpc-methods/src/permitted/openWebSocket.test.ts index b95d2d89c0..618a4c78f8 100644 --- a/packages/snaps-rpc-methods/src/permitted/openWebSocket.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/openWebSocket.test.ts @@ -1,41 +1,78 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { OpenWebSocketResult } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; -import type { OpenWebSocketParameters } from './openWebSocket'; +import type { + OpenWebSocketMethodActions, + OpenWebSocketParameters, +} from './openWebSocket'; import { openWebSocketHandler } from './openWebSocket'; describe('snap_openWebSocket', () => { describe('openWebSocketHandler', () => { it('has the expected shape', () => { expect(openWebSocketHandler).toMatchObject({ - methodNames: ['snap_openWebSocket'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - openWebSocket: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'WebSocketService:open', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + OpenWebSocketMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'WebSocketService:open', + async () => 'foo', + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission', async () => { const { implementation } = openWebSocketHandler; - const openWebSocket = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(false); - const hooks = { hasPermission, openWebSocket }; + const messenger = getMessenger(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -65,19 +102,21 @@ describe('snap_openWebSocket', () => { it('throws if invalid parameters are passed', async () => { const { implementation } = openWebSocketHandler; - const openWebSocket = jest.fn(); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, openWebSocket }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -107,19 +146,21 @@ describe('snap_openWebSocket', () => { it('opens a WebSocket and returns the ID', async () => { const { implementation } = openWebSocketHandler; - const openWebSocket = jest.fn().mockResolvedValue('foo'); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, openWebSocket }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -135,7 +176,9 @@ describe('snap_openWebSocket', () => { }); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: 'foo' }); - expect(openWebSocket).toHaveBeenCalledWith( + expect(messenger.call).toHaveBeenCalledWith( + 'WebSocketService:open', + MOCK_SNAP_ID, 'wss://metamask.io', undefined, ); diff --git a/packages/snaps-rpc-methods/src/permitted/openWebSocket.ts b/packages/snaps-rpc-methods/src/permitted/openWebSocket.ts index 81b8b15cff..32e6b99d6d 100644 --- a/packages/snaps-rpc-methods/src/permitted/openWebSocket.ts +++ b/packages/snaps-rpc-methods/src/permitted/openWebSocket.ts @@ -1,9 +1,13 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { literal, union, - type JsonRpcRequest, type OpenWebSocketParams, type OpenWebSocketResult, } from '@metamask/snaps-sdk'; @@ -19,20 +23,14 @@ import { import type { PendingJsonRpcResponse } from '@metamask/utils'; import { SnapEndowments } from '../endowments'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + WebSocketServiceOpenAction, +} from '../types'; -const methodName = 'snap_openWebSocket'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - openWebSocket: true, -}; - -export type OpenWebSocketMethodHooks = { - hasPermission: (permissionName: string) => boolean; - openWebSocket: (url: string, protocols?: string[]) => Promise; -}; +export type OpenWebSocketMethodActions = + | PermissionControllerHasPermissionAction + | WebSocketServiceOpenAction; const OpenWebSocketParametersStruct = object({ url: uri({ protocol: union([literal('wss:'), literal('ws:')]) }), @@ -91,13 +89,14 @@ export type OpenWebSocketParameters = InferMatching< * ``` */ export const openWebSocketHandler = { - methodNames: [methodName] as const, implementation: openWebSocketImplementation, - hookNames, -} satisfies PermittedHandlerExport< - OpenWebSocketMethodHooks, + actionNames: ['PermissionController:hasPermission', 'WebSocketService:open'], +} satisfies MethodHandler< + never, + OpenWebSocketMethodActions, OpenWebSocketParams, - OpenWebSocketResult + OpenWebSocketResult, + { origin: string } >; /** @@ -107,27 +106,38 @@ export const openWebSocketHandler = { * @param res - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. Not used by this function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if a snap has the `endowment:network-access` permission. - * @param hooks.openWebSocket - The function to open a WebSocket. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function openWebSocketImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, openWebSocket }: OpenWebSocketMethodHooks, + _hooks: never, + messenger: Messenger, ): Promise { - if (!hasPermission(SnapEndowments.NetworkAccess)) { + const { params, origin } = req; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + SnapEndowments.NetworkAccess, + ) + ) { return end(providerErrors.unauthorized()); } - const { params } = req; - try { const { url, protocols } = getValidatedParams(params); - res.result = await openWebSocket(url, protocols); + res.result = await messenger.call( + 'WebSocketService:open', + origin, + url, + protocols, + ); } catch (error) { return end(error); } diff --git a/packages/snaps-rpc-methods/src/permitted/requestSnaps.test.ts b/packages/snaps-rpc-methods/src/permitted/requestSnaps.test.ts index af9860da12..2d0475cc8b 100644 --- a/packages/snaps-rpc-methods/src/permitted/requestSnaps.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/requestSnaps.test.ts @@ -1,7 +1,11 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { RequestedPermissions, PermissionConstraint, + PermissionController, } from '@metamask/permission-controller'; import type { RequestSnapsParams, @@ -11,32 +15,30 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID, MOCK_ORIGIN, - getTruncatedSnap, MOCK_LOCAL_SNAP_ID, + MockControllerMessenger, + getTruncatedSnap, } from '@metamask/snaps-utils/test-utils'; -import type { - JsonRpcRequest, - JsonRpcSuccess, - PendingJsonRpcResponse, -} from '@metamask/utils'; +import type { JsonRpcSuccess, PendingJsonRpcResponse } from '@metamask/utils'; +import type { RequestSnapsMethodActions } from './requestSnaps'; import { requestSnapsHandler, hasRequestedSnaps, getSnapPermissionsRequest, } from './requestSnaps'; import { WALLET_SNAP_PERMISSION_KEY } from '../restricted/invokeSnap'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('requestSnapsHandler', () => { it('has the expected shape', () => { expect(requestSnapsHandler).toMatchObject({ - methodNames: ['wallet_requestSnaps'], implementation: expect.any(Function), - hookNames: { - installSnaps: true, - requestPermissions: true, - getPermissions: true, - }, + actionNames: [ + 'SnapController:installSnaps', + 'PermissionController:requestPermissions', + 'PermissionController:getPermissions', + ], }); }); }); @@ -173,46 +175,74 @@ describe('getSnapPermissionsRequest', () => { }); describe('implementation', () => { - const getMockHooks = () => - ({ - installSnaps: jest.fn(), - requestPermissions: jest.fn(), - getPermissions: jest.fn(), - }) as any; + const getMessenger = () => { + const messenger = new MockControllerMessenger< + RequestSnapsMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:getPermissions', + () => undefined, + ); + + messenger.registerActionHandler( + 'PermissionController:requestPermissions', + async () => + [ + { + [WALLET_SNAP_PERMISSION_KEY]: { + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: 'https://metamask.github.io', + parentCapability: WALLET_SNAP_PERMISSION_KEY, + }, + }, + { + id: 'foo', + origin: MOCK_ORIGIN, + data: { + [WALLET_SNAP_PERMISSION_KEY]: { + [MOCK_SNAP_ID]: getTruncatedSnap(), + }, + }, + }, + ] as Awaited>, + ); + + messenger.registerActionHandler( + 'SnapController:installSnaps', + async () => ({ + [MOCK_SNAP_ID]: getTruncatedSnap(), + }), + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; it('requests permissions if needed', async () => { const { implementation } = requestSnapsHandler; - const hooks = getMockHooks(); - - hooks.requestPermissions.mockImplementation(() => [ - { - caveats: [ - { - type: SnapCaveatType.SnapIds, - value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, - }, - ], - date: 1661166080905, - id: 'VyAsBJiDDKawv_XlNcm13', - invoker: 'https://metamask.github.io', - parentCapability: WALLET_SNAP_PERMISSION_KEY, - }, - { - data: { - [WALLET_SNAP_PERMISSION_KEY]: { [MOCK_SNAP_ID]: getTruncatedSnap() }, - }, - }, - ]); + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_ORIGIN)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -227,54 +257,59 @@ describe('implementation', () => { }, })) as JsonRpcSuccess; - expect(hooks.requestPermissions).toHaveBeenCalledWith({ - [WALLET_SNAP_PERMISSION_KEY]: { - caveats: [ - { - type: SnapCaveatType.SnapIds, - value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, - }, - ], + expect(messenger.call).toHaveBeenCalledWith( + 'PermissionController:requestPermissions', + { origin: MOCK_ORIGIN }, + { + [WALLET_SNAP_PERMISSION_KEY]: { + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, + }, + ], + }, }, - }); + ); expect(response.result).toStrictEqual({ [MOCK_SNAP_ID]: getTruncatedSnap(), }); }); - it('doesnt request permissions if already present', async () => { + it('does not request permissions if already present', async () => { const { implementation } = requestSnapsHandler; - const hooks = getMockHooks(); - - hooks.getPermissions.mockImplementation(() => ({ - [WALLET_SNAP_PERMISSION_KEY]: { - caveats: [ - { - type: SnapCaveatType.SnapIds, - value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, - }, - ], - date: 1661166080905, - id: 'VyAsBJiDDKawv_XlNcm13', - invoker: 'https://metamask.github.io', - parentCapability: WALLET_SNAP_PERMISSION_KEY, - }, - })); + const messenger = getMessenger(); - hooks.installSnaps.mockImplementation(() => ({ - [MOCK_SNAP_ID]: getTruncatedSnap(), - })); + messenger.registerActionHandler( + 'PermissionController:getPermissions', + () => ({ + [WALLET_SNAP_PERMISSION_KEY]: { + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: 'https://metamask.github.io', + parentCapability: WALLET_SNAP_PERMISSION_KEY, + }, + }), + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_ORIGIN)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -289,17 +324,19 @@ describe('implementation', () => { }, })) as JsonRpcSuccess; - expect(hooks.requestPermissions).not.toHaveBeenCalledWith({ - [WALLET_SNAP_PERMISSION_KEY]: { - caveats: [ - { type: SnapCaveatType.SnapIds, value: { [MOCK_SNAP_ID]: {} } }, - ], - }, - }); + expect(messenger.call).not.toHaveBeenCalledWith( + 'PermissionController:requestPermissions', + expect.anything(), + expect.anything(), + ); - expect(hooks.installSnaps).toHaveBeenCalledWith({ - [MOCK_SNAP_ID]: { version: '^1.0.0' }, - }); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:installSnaps', + MOCK_ORIGIN, + { + [MOCK_SNAP_ID]: { version: '^1.0.0' }, + }, + ); expect(response.result).toStrictEqual({ [MOCK_SNAP_ID]: getTruncatedSnap(), @@ -309,57 +346,72 @@ describe('implementation', () => { it('merges permission requests when missing snaps', async () => { const { implementation } = requestSnapsHandler; - const hooks = getMockHooks(); + const messenger = getMessenger(); - hooks.getPermissions.mockImplementation(() => ({ - [WALLET_SNAP_PERMISSION_KEY]: { - caveats: [ - { - type: SnapCaveatType.SnapIds, - value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, - }, - ], - date: 1661166080905, - id: 'VyAsBJiDDKawv_XlNcm13', - invoker: 'https://metamask.github.io', - parentCapability: WALLET_SNAP_PERMISSION_KEY, - }, - })); + messenger.registerActionHandler( + 'PermissionController:getPermissions', + () => ({ + [WALLET_SNAP_PERMISSION_KEY]: { + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: 'https://metamask.github.io', + parentCapability: WALLET_SNAP_PERMISSION_KEY, + }, + }), + ); - hooks.requestPermissions.mockImplementation(() => [ - { - caveats: [ + messenger.registerActionHandler( + 'PermissionController:requestPermissions', + async () => + [ { - type: SnapCaveatType.SnapIds, - value: { - [MOCK_SNAP_ID]: { version: '^1.0.0' }, - [MOCK_LOCAL_SNAP_ID]: { version: '^1.0.0' }, + [WALLET_SNAP_PERMISSION_KEY]: { + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: { + [MOCK_SNAP_ID]: { version: '^1.0.0' }, + [MOCK_LOCAL_SNAP_ID]: { version: '^1.0.0' }, + }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: 'https://metamask.github.io', + parentCapability: WALLET_SNAP_PERMISSION_KEY, }, }, - ], - date: 1661166080905, - id: 'VyAsBJiDDKawv_XlNcm13', - invoker: 'https://metamask.github.io', - parentCapability: WALLET_SNAP_PERMISSION_KEY, - }, - { - data: { - [WALLET_SNAP_PERMISSION_KEY]: { - [MOCK_SNAP_ID]: getTruncatedSnap(), - [MOCK_LOCAL_SNAP_ID]: getTruncatedSnap({ id: MOCK_LOCAL_SNAP_ID }), + { + id: 'foo', + origin: MOCK_ORIGIN, + data: { + [WALLET_SNAP_PERMISSION_KEY]: { + [MOCK_SNAP_ID]: getTruncatedSnap(), + [MOCK_LOCAL_SNAP_ID]: getTruncatedSnap({ + id: MOCK_LOCAL_SNAP_ID, + }), + }, + }, }, - }, - }, - ]); + ] as Awaited>, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_ORIGIN)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -375,19 +427,23 @@ describe('implementation', () => { }, })) as JsonRpcSuccess; - expect(hooks.requestPermissions).toHaveBeenCalledWith({ - [WALLET_SNAP_PERMISSION_KEY]: { - caveats: [ - { - type: SnapCaveatType.SnapIds, - value: { - [MOCK_SNAP_ID]: { version: '^1.0.0' }, - [MOCK_LOCAL_SNAP_ID]: { version: '^1.0.0' }, + expect(messenger.call).toHaveBeenCalledWith( + 'PermissionController:requestPermissions', + { origin: MOCK_ORIGIN }, + { + [WALLET_SNAP_PERMISSION_KEY]: { + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: { + [MOCK_SNAP_ID]: { version: '^1.0.0' }, + [MOCK_LOCAL_SNAP_ID]: { version: '^1.0.0' }, + }, }, - }, - ], + ], + }, }, - }); + ); expect(response.result).toStrictEqual({ [MOCK_SNAP_ID]: getTruncatedSnap(), @@ -398,20 +454,25 @@ describe('implementation', () => { it('throws with the appropriate error if the side-effect fails', async () => { const { implementation } = requestSnapsHandler; - const hooks = getMockHooks(); + const messenger = getMessenger(); - hooks.requestPermissions.mockImplementation(async () => { - throw new Error('error'); - }); + messenger.registerActionHandler( + 'PermissionController:requestPermissions', + async () => { + throw new Error('error'); + }, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_ORIGIN)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -426,16 +487,20 @@ describe('implementation', () => { }, })) as JsonRpcSuccess; - expect(hooks.requestPermissions).toHaveBeenCalledWith({ - [WALLET_SNAP_PERMISSION_KEY]: { - caveats: [ - { - type: SnapCaveatType.SnapIds, - value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, - }, - ], + expect(messenger.call).toHaveBeenCalledWith( + 'PermissionController:requestPermissions', + { origin: MOCK_ORIGIN }, + { + [WALLET_SNAP_PERMISSION_KEY]: { + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: { [MOCK_SNAP_ID]: { version: '^1.0.0' } }, + }, + ], + }, }, - }); + ); expect(response).toStrictEqual({ error: { @@ -451,16 +516,18 @@ describe('implementation', () => { it('throws if params is not an object', async () => { const { implementation } = requestSnapsHandler; - const hooks = getMockHooks(); + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_ORIGIN)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -487,16 +554,18 @@ describe('implementation', () => { it('throws if params is an empty object', async () => { const { implementation } = requestSnapsHandler; - const hooks = getMockHooks(); + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_ORIGIN)); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequestWithOrigin, res as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/requestSnaps.ts b/packages/snaps-rpc-methods/src/permitted/requestSnaps.ts index 232ee39d6d..283a2179b6 100644 --- a/packages/snaps-rpc-methods/src/permitted/requestSnaps.ts +++ b/packages/snaps-rpc-methods/src/permitted/requestSnaps.ts @@ -1,8 +1,14 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { + Caveat, PermissionConstraint, + PermissionControllerGetPermissionsAction, + PermissionControllerRequestPermissionsAction, RequestedPermissions, - Caveat, } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { @@ -14,25 +20,20 @@ import { SnapCaveatType, verifyRequestedSnapPermissions, } from '@metamask/snaps-utils'; -import type { - JsonRpcRequest, - PendingJsonRpcResponse, - Json, -} from '@metamask/utils'; -import { hasProperty, isObject } from '@metamask/utils'; +import type { PendingJsonRpcResponse, Json } from '@metamask/utils'; +import { assert, hasProperty, isObject } from '@metamask/utils'; import { Mutex } from 'async-mutex'; import { WALLET_SNAP_PERMISSION_KEY } from '../restricted/invokeSnap'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; - -const methodName = 'wallet_requestSnaps'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerInstallSnapsAction, +} from '../types'; -const hookNames: MethodHooksObject = { - installSnaps: true, - requestPermissions: true, - getPermissions: true, -}; +export type RequestSnapsMethodActions = + | SnapControllerInstallSnapsAction + | PermissionControllerRequestPermissionsAction + | PermissionControllerGetPermissionsAction; /** * Request permission for a dapp to communicate with the specified Snaps and @@ -60,47 +61,20 @@ const hookNames: MethodHooksObject = { * ``` */ export const requestSnapsHandler = { - methodNames: [methodName] as const, implementation: requestSnapsImplementation, - hookNames, -} satisfies PermittedHandlerExport< - RequestSnapsHooks, + actionNames: [ + 'SnapController:installSnaps', + 'PermissionController:requestPermissions', + 'PermissionController:getPermissions', + ], +} satisfies MethodHandler< + never, + RequestSnapsMethodActions, RequestSnapsParams, - RequestSnapsResult + RequestSnapsResult, + { origin: string } >; -export type RequestSnapsHooks = { - /** - * Installs the requested snaps if they are permitted. - */ - installSnaps: ( - requestedSnaps: RequestSnapsParams, - ) => Promise; - - /** - * Initiates a permission request for the requesting origin. - * - * @returns The result of the permissions request. - */ - requestPermissions: ( - permissions: RequestedPermissions, - ) => Promise< - [ - Record, - { data: Record; id: string; origin: string }, - ] - >; - - /** - * Gets the current permissions for the requesting origin. - * - * @returns The current permissions of the requesting origin. - */ - getPermissions: () => Promise< - Record | undefined - >; -}; - /** * Checks whether an origin has existing `wallet_snap` permission and * whether or not it has the requested snapIds caveat. @@ -200,20 +174,18 @@ function getMutex(origin: string) { * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.installSnaps - A function that tries to install a given snap, prompting the user if necessary. - * @param hooks.requestPermissions - A function that requests permissions on - * behalf of a subject. - * @param hooks.getPermissions - A function that gets the current permissions. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns A promise that resolves once the JSON-RPC response has been modified. * @throws If the params are invalid. */ async function requestSnapsImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { installSnaps, requestPermissions, getPermissions }: RequestSnapsHooks, + _hooks: never, + messenger: Messenger, ): Promise { const requestedSnaps = req.params; if (!isObject(requestedSnaps)) { @@ -232,8 +204,7 @@ async function requestSnapsImplementation( ); } - // We expect the MM middleware stack to always add the origin to requests - const { origin } = req as JsonRpcRequest & { origin: string }; + const { origin } = req; const mutex = getMutex(origin); @@ -246,22 +217,43 @@ async function requestSnapsImplementation( caveats: [{ type: SnapCaveatType.SnapIds, value: requestedSnaps }], }, } as RequestedPermissions; - const existingPermissions = await getPermissions(); + const existingPermissions = messenger.call( + 'PermissionController:getPermissions', + origin, + ); if (!existingPermissions) { - const [, metadata] = await requestPermissions(requestedPermissions); + const [, metadata] = await messenger.call( + 'PermissionController:requestPermissions', + { origin }, + requestedPermissions, + ); + + assert(metadata.data); + res.result = metadata.data[ WALLET_SNAP_PERMISSION_KEY ] as RequestSnapsResult; } else if (hasRequestedSnaps(existingPermissions, requestedSnaps)) { - res.result = await installSnaps(requestedSnaps); + res.result = await messenger.call( + 'SnapController:installSnaps', + origin, + requestedSnaps, + ); } else { const mergedPermissionsRequest = getSnapPermissionsRequest( existingPermissions, requestedPermissions, ); - const [, metadata] = await requestPermissions(mergedPermissionsRequest); + const [, metadata] = await messenger.call( + 'PermissionController:requestPermissions', + { origin }, + mergedPermissionsRequest, + ); + + assert(metadata.data); + res.result = metadata.data[ WALLET_SNAP_PERMISSION_KEY ] as RequestSnapsResult; diff --git a/packages/snaps-rpc-methods/src/permitted/resolveInterface.test.ts b/packages/snaps-rpc-methods/src/permitted/resolveInterface.test.ts index 5d8fe2fc67..2a10e65b68 100644 --- a/packages/snaps-rpc-methods/src/permitted/resolveInterface.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/resolveInterface.test.ts @@ -1,44 +1,78 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { ResolveInterfaceParams, ResolveInterfaceResult, } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { ResolveInterfaceMethodActions } from './resolveInterface'; import { resolveInterfaceHandler } from './resolveInterface'; describe('snap_resolveInterface', () => { describe('resolveInterfaceHandler', () => { it('has the expected shape', () => { expect(resolveInterfaceHandler).toMatchObject({ - methodNames: ['snap_resolveInterface'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - resolveInterface: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:resolveInterface', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + ResolveInterfaceMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapInterfaceController:resolveInterface', + async () => undefined, + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission to show UI', async () => { const { implementation } = resolveInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(false); - const resolveInterface = jest.fn(); + const messenger = getMessenger(); - const hooks = { hasPermission, resolveInterface }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -66,26 +100,24 @@ describe('snap_resolveInterface', () => { }); }); - it('returns null after calling the `resolveInterface` hook', async () => { + it('returns null after calling the `SnapInterfaceController:resolveInterface` action', async () => { const { implementation } = resolveInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const resolveInterface = jest.fn(); - - const hooks = { - hasPermission, - resolveInterface, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -107,23 +139,21 @@ describe('snap_resolveInterface', () => { it('resolves an interface', async () => { const { implementation } = resolveInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const resolveInterface = jest.fn(); - - const hooks = { - hasPermission, - resolveInterface, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -139,53 +169,56 @@ describe('snap_resolveInterface', () => { }, }); - expect(resolveInterface).toHaveBeenCalledWith('foo', 'bar'); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapInterfaceController:resolveInterface', + MOCK_SNAP_ID, + 'foo', + 'bar', + ); }); - }); - - it('throws on invalid params', async () => { - const { implementation } = resolveInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const resolveInterface = jest.fn(); + it('throws on invalid params', async () => { + const { implementation } = resolveInterfaceHandler; - const hooks = { - hasPermission, - resolveInterface, - }; + const messenger = getMessenger(); - const engine = new JsonRpcEngine(); + const engine = new JsonRpcEngine(); - engine.push((request, response, next, end) => { - const result = implementation( - request as JsonRpcRequest, - response as PendingJsonRpcResponse, - next, - end, - hooks, - ); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest & { + origin: string; + }, + response as PendingJsonRpcResponse, + next, + end, + {} as never, + messenger, + ); - result?.catch(end); - }); + result?.catch(end); + }); - const response = await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'snap_resolveInterface', - params: { - id: 42, - }, - }); + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_resolveInterface', + params: { + id: 42, + }, + }); - expect(response).toStrictEqual({ - error: { - code: -32602, - message: - 'Invalid params: At path: id -- Expected a string, but received: 42.', - stack: expect.any(String), - }, - id: 1, - jsonrpc: '2.0', + expect(response).toStrictEqual({ + error: { + code: -32602, + message: + 'Invalid params: At path: id -- Expected a string, but received: 42.', + stack: expect.any(String), + }, + id: 1, + jsonrpc: '2.0', + }); }); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/resolveInterface.ts b/packages/snaps-rpc-methods/src/permitted/resolveInterface.ts index 8d789c3201..ecf2b2672d 100644 --- a/packages/snaps-rpc-methods/src/permitted/resolveInterface.ts +++ b/packages/snaps-rpc-methods/src/permitted/resolveInterface.ts @@ -1,39 +1,28 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, ResolveInterfaceParams, ResolveInterfaceResult, } from '@metamask/snaps-sdk'; import type { InferMatching } from '@metamask/snaps-utils'; import { StructError, create, object, string } from '@metamask/superstruct'; -import type { Json, PendingJsonRpcResponse } from '@metamask/utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; import { JsonStruct } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapInterfaceControllerResolveInterfaceAction, +} from '../types'; import { UI_PERMISSIONS } from '../utils'; -const methodName = 'snap_resolveInterface'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - resolveInterface: true, -}; - -export type ResolveInterfaceMethodHooks = { - /** - * @param permissionName - The name of the permission to check. - * @returns Whether the Snap has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * @param id - The interface id. - * @param value - The value to resolve the interface with. - */ - resolveInterface: (id: string, value: Json) => Promise; -}; +export type ResolveInterfaceMethodActions = + | PermissionControllerHasPermissionAction + | SnapInterfaceControllerResolveInterfaceAction; /** * Resolve an interactive interface. For use in @@ -62,13 +51,17 @@ export type ResolveInterfaceMethodHooks = { * ``` */ export const resolveInterfaceHandler = { - methodNames: [methodName] as const, implementation: getResolveInterfaceImplementation, - hookNames, -} satisfies PermittedHandlerExport< - ResolveInterfaceMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:resolveInterface', + ], +} satisfies MethodHandler< + never, + ResolveInterfaceMethodActions, ResolveInterfaceParameters, - ResolveInterfaceResult + ResolveInterfaceResult, + { origin: string } >; const ResolveInterfaceParametersStruct = object({ @@ -89,20 +82,25 @@ export type ResolveInterfaceParameters = InferMatching< * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if the Snap has a given - * permission. - * @param hooks.resolveInterface - The function to resolve the interface. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function getResolveInterfaceImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, resolveInterface }: ResolveInterfaceMethodHooks, + _hooks: never, + messenger: Messenger, ): Promise { - if (!UI_PERMISSIONS.some(hasPermission)) { + const { params, origin } = req; + + const isPermitted = UI_PERMISSIONS.some((permission) => + messenger.call('PermissionController:hasPermission', origin, permission), + ); + + if (!isPermitted) { return end( providerErrors.unauthorized({ message: `This method can only be used if the Snap has one of the following permissions: ${UI_PERMISSIONS.join(', ')}.`, @@ -110,14 +108,17 @@ async function getResolveInterfaceImplementation( ); } - const { params } = req; - try { const validatedParams = getValidatedParams(params); const { id, value } = validatedParams; - await resolveInterface(id, value); + await messenger.call( + 'SnapInterfaceController:resolveInterface', + origin, + id, + value, + ); res.result = null; } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.test.ts b/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.test.ts index 2a95b90d96..63deb54d9a 100644 --- a/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.test.ts @@ -1,23 +1,30 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { ScheduleBackgroundEventParams, ScheduleBackgroundEventResult, + SnapId, } from '@metamask/snaps-sdk'; -import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { ScheduleBackgroundEventMethodActions } from './scheduleBackgroundEvent'; import { scheduleBackgroundEventHandler } from './scheduleBackgroundEvent'; describe('snap_scheduleBackgroundEvent', () => { describe('scheduleBackgroundEventHandler', () => { it('has the expected shape', () => { expect(scheduleBackgroundEventHandler).toMatchObject({ - methodNames: ['snap_scheduleBackgroundEvent'], implementation: expect.any(Function), - hookNames: { - scheduleBackgroundEvent: true, - hasPermission: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'CronjobController:schedule', + ], }); }); }); @@ -34,34 +41,45 @@ describe('snap_scheduleBackgroundEvent', () => { jest.useRealTimers(); }); - const createOriginMiddleware = - (origin: string) => - (request: any, _response: unknown, next: () => void, _end: unknown) => { - request.origin = origin; - next(); - }; + const getMessenger = () => { + const messenger = new MockControllerMessenger< + ScheduleBackgroundEventMethodActions, + never + >(); - it('returns an id after calling the `scheduleBackgroundEvent` hook', async () => { - const { implementation } = scheduleBackgroundEventHandler; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); - const scheduleBackgroundEvent = jest.fn().mockImplementation(() => 'foo'); - const hasPermission = jest.fn().mockImplementation(() => true); + messenger.registerActionHandler( + 'CronjobController:schedule', + () => 'foo', + ); - const hooks = { - scheduleBackgroundEvent, - hasPermission, - }; + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('returns an id after calling the `CronjobController:schedule` action', async () => { + const { implementation } = scheduleBackgroundEventHandler; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -86,24 +104,21 @@ describe('snap_scheduleBackgroundEvent', () => { it('schedules a background event', async () => { const { implementation } = scheduleBackgroundEventHandler; - const scheduleBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - scheduleBackgroundEvent, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -122,36 +137,37 @@ describe('snap_scheduleBackgroundEvent', () => { }, }); - expect(scheduleBackgroundEvent).toHaveBeenCalledWith({ - schedule: '2022-01-01T01:00:35.786+02:00', - request: { - method: 'handleExport', - params: ['p1'], + expect(messenger.call).toHaveBeenCalledWith( + 'CronjobController:schedule', + { + snapId: MOCK_SNAP_ID, + schedule: '2022-01-01T01:00:35.786+02:00', + request: { + method: 'handleExport', + params: ['p1'], + }, }, - }); + ); }); it('schedules a background event using a duration', async () => { const { implementation } = scheduleBackgroundEventHandler; - const scheduleBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - scheduleBackgroundEvent, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -170,36 +186,37 @@ describe('snap_scheduleBackgroundEvent', () => { }, }); - expect(scheduleBackgroundEvent).toHaveBeenCalledWith({ - schedule: 'PT1S', - request: { - method: 'handleExport', - params: ['p1'], + expect(messenger.call).toHaveBeenCalledWith( + 'CronjobController:schedule', + { + snapId: MOCK_SNAP_ID, + schedule: 'PT1S', + request: { + method: 'handleExport', + params: ['p1'], + }, }, - }); + ); }); it('throws on an invalid duration', async () => { const { implementation } = scheduleBackgroundEventHandler; - const scheduleBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - scheduleBackgroundEvent, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -233,24 +250,26 @@ describe('snap_scheduleBackgroundEvent', () => { it('throws if a snap does not have the "endowment:cronjob" permission', async () => { const { implementation } = scheduleBackgroundEventHandler; - const scheduleBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => false); + const messenger = getMessenger(); - const hooks = { - scheduleBackgroundEvent, - hasPermission, - }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -284,24 +303,21 @@ describe('snap_scheduleBackgroundEvent', () => { it('throws if no timezone information is provided in the ISO 8601 date', async () => { const { implementation } = scheduleBackgroundEventHandler; - const scheduleBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - scheduleBackgroundEvent, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -335,24 +351,21 @@ describe('snap_scheduleBackgroundEvent', () => { it('throws on invalid params', async () => { const { implementation } = scheduleBackgroundEventHandler; - const scheduleBackgroundEvent = jest.fn(); - const hasPermission = jest.fn().mockImplementation(() => true); - - const hooks = { - scheduleBackgroundEvent, - hasPermission, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: SnapId; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts b/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts index 970c60b63a..620af9d085 100644 --- a/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts +++ b/packages/snaps-rpc-methods/src/permitted/scheduleBackgroundEvent.ts @@ -1,12 +1,17 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import { selectiveUnion, - type JsonRpcRequest, type ScheduleBackgroundEventParams, type ScheduleBackgroundEventResult, + type SnapId, } from '@metamask/snaps-sdk'; -import type { CronjobRpcRequest, InferMatching } from '@metamask/snaps-utils'; +import type { InferMatching } from '@metamask/snaps-utils'; import { CronjobRpcRequestStruct, ISO8601DateStruct, @@ -16,28 +21,14 @@ import { StructError, create, object } from '@metamask/superstruct'; import { hasProperty, type PendingJsonRpcResponse } from '@metamask/utils'; import { SnapEndowments } from '../endowments'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + CronjobControllerScheduleAction, + JsonRpcRequestWithOrigin, +} from '../types'; -const methodName = 'snap_scheduleBackgroundEvent'; - -const hookNames: MethodHooksObject = { - scheduleBackgroundEvent: true, - hasPermission: true, -}; - -type ScheduleBackgroundEventHookParams = { - schedule: string; - request: CronjobRpcRequest; -}; - -export type ScheduleBackgroundEventMethodHooks = { - scheduleBackgroundEvent: ( - snapEvent: ScheduleBackgroundEventHookParams, - ) => string; - - hasPermission: (permissionName: string) => boolean; -}; +export type ScheduleBackgroundEventMethodActions = + | PermissionControllerHasPermissionAction + | CronjobControllerScheduleAction; /** * Schedule a background event for a Snap. The background event will trigger a @@ -66,13 +57,17 @@ export type ScheduleBackgroundEventMethodHooks = { * ``` */ export const scheduleBackgroundEventHandler = { - methodNames: [methodName] as const, implementation: getScheduleBackgroundEventImplementation, - hookNames, -} satisfies PermittedHandlerExport< - ScheduleBackgroundEventMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'CronjobController:schedule', + ], +} satisfies MethodHandler< + never, + ScheduleBackgroundEventMethodActions, ScheduleBackgroundEventParameters, - ScheduleBackgroundEventResult + ScheduleBackgroundEventResult, + { origin: SnapId } >; const ScheduleBackgroundEventParametersWithDateStruct = object({ @@ -120,24 +115,27 @@ function getSchedule(params: ScheduleBackgroundEventParameters): string { * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.scheduleBackgroundEvent - The function to schedule a background event. - * @param hooks.hasPermission - The function to check if a snap has the `endowment:cronjob` permission. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns An id representing the background event. */ async function getScheduleBackgroundEventImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { - scheduleBackgroundEvent, - hasPermission, - }: ScheduleBackgroundEventMethodHooks, + _hooks: never, + messenger: Messenger, ): Promise { - const { params } = req; - - if (!hasPermission(SnapEndowments.Cronjob)) { + const { params, origin } = req; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + SnapEndowments.Cronjob, + ) + ) { return end(providerErrors.unauthorized()); } @@ -146,7 +144,8 @@ async function getScheduleBackgroundEventImplementation( const { request } = validatedParams; const schedule = getSchedule(validatedParams); - const id = scheduleBackgroundEvent({ + const id = messenger.call('CronjobController:schedule', { + snapId: origin, schedule, request, }); diff --git a/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.test.ts b/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.test.ts index 69b62b9e77..8333d958dc 100644 --- a/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.test.ts @@ -1,41 +1,78 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { SendWebSocketMessageResult } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; -import type { SendWebSocketMessageParameters } from './sendWebSocketMessage'; +import type { + SendWebSocketMessageMethodActions, + SendWebSocketMessageParameters, +} from './sendWebSocketMessage'; import { sendWebSocketMessageHandler } from './sendWebSocketMessage'; describe('snap_sendWebSocketMessage', () => { describe('sendWebSocketMessageHandler', () => { it('has the expected shape', () => { expect(sendWebSocketMessageHandler).toMatchObject({ - methodNames: ['snap_sendWebSocketMessage'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - sendWebSocketMessage: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'WebSocketService:sendMessage', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + SendWebSocketMessageMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'WebSocketService:sendMessage', + async () => undefined, + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission', async () => { const { implementation } = sendWebSocketMessageHandler; - const sendWebSocketMessage = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(false); - const hooks = { hasPermission, sendWebSocketMessage }; + const messenger = getMessenger(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -63,19 +100,21 @@ describe('snap_sendWebSocketMessage', () => { it('throws if invalid parameters are passed', async () => { const { implementation } = sendWebSocketMessageHandler; - const sendWebSocketMessage = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, sendWebSocketMessage }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -103,19 +142,21 @@ describe('snap_sendWebSocketMessage', () => { it('sends a WebSocket message and returns null', async () => { const { implementation } = sendWebSocketMessageHandler; - const sendWebSocketMessage = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const hooks = { hasPermission, sendWebSocketMessage }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -132,7 +173,12 @@ describe('snap_sendWebSocketMessage', () => { }); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, result: null }); - expect(sendWebSocketMessage).toHaveBeenCalledWith('foo', 'Hello world!'); + expect(messenger.call).toHaveBeenCalledWith( + 'WebSocketService:sendMessage', + MOCK_SNAP_ID, + 'foo', + 'Hello world!', + ); }); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.ts b/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.ts index 77fea18b03..86e0c9fa24 100644 --- a/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.ts +++ b/packages/snaps-rpc-methods/src/permitted/sendWebSocketMessage.ts @@ -1,7 +1,11 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, SendWebSocketMessageParams, SendWebSocketMessageResult, } from '@metamask/snaps-sdk'; @@ -18,20 +22,14 @@ import { import type { PendingJsonRpcResponse } from '@metamask/utils'; import { SnapEndowments } from '../endowments'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; - -const methodName = 'snap_sendWebSocketMessage'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - sendWebSocketMessage: true, -}; +import type { + JsonRpcRequestWithOrigin, + WebSocketServiceSendMessageAction, +} from '../types'; -export type SendWebSocketMessageMethodHooks = { - hasPermission: (permissionName: string) => boolean; - sendWebSocketMessage: (id: string, data: string | number[]) => Promise; -}; +export type SendWebSocketMessageMethodActions = + | PermissionControllerHasPermissionAction + | WebSocketServiceSendMessageAction; const SendWebSocketMessageParametersStruct = object({ id: string(), @@ -68,13 +66,17 @@ export type SendWebSocketMessageParameters = InferMatching< * ``` */ export const sendWebSocketMessageHandler = { - methodNames: [methodName] as const, implementation: sendWebSocketMessageImplementation, - hookNames, -} satisfies PermittedHandlerExport< - SendWebSocketMessageMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'WebSocketService:sendMessage', + ], +} satisfies MethodHandler< + never, + SendWebSocketMessageMethodActions, SendWebSocketMessageParams, - SendWebSocketMessageResult + SendWebSocketMessageResult, + { origin: string } >; /** @@ -84,27 +86,33 @@ export const sendWebSocketMessageHandler = { * @param res - The JSON-RPC response object. * @param _next - The `json-rpc-engine` "next" callback. Not used by this function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if a snap has the `endowment:network-access` permission. - * @param hooks.sendWebSocketMessage - The function to send a WebSocket message. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function sendWebSocketMessageImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, sendWebSocketMessage }: SendWebSocketMessageMethodHooks, + _hooks: never, + messenger: Messenger, ): Promise { - if (!hasPermission(SnapEndowments.NetworkAccess)) { + const { params, origin } = req; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + SnapEndowments.NetworkAccess, + ) + ) { return end(providerErrors.unauthorized()); } - const { params } = req; - try { const { id, message } = getValidatedParams(params); - await sendWebSocketMessage(id, message); + await messenger.call('WebSocketService:sendMessage', origin, id, message); res.result = null; } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/setState.test.ts b/packages/snaps-rpc-methods/src/permitted/setState.test.ts index 59a4e484b6..adfbc445f6 100644 --- a/packages/snaps-rpc-methods/src/permitted/setState.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/setState.test.ts @@ -1,59 +1,96 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import { errorCodes } from '@metamask/rpc-errors'; import type { SetStateResult } from '@metamask/snaps-sdk'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; import { createDeferredPromise, type Json, - type JsonRpcRequest, type PendingJsonRpcResponse, } from '@metamask/utils'; -import { setStateHandler, type SetStateParameters, set } from './setState'; +import { + type SetStateMethodActions, + setStateHandler, + type SetStateParameters, + set, +} from './setState'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_setState', () => { describe('setStateHandler', () => { it('has the expected shape', () => { expect(setStateHandler).toMatchObject({ - methodNames: ['snap_setState'], implementation: expect.any(Function), hookNames: { - getSnapState: true, - hasPermission: true, + getUnlockPromise: true, }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:getSnapState', + 'SnapController:updateSnapState', + 'SnapController:getSnap', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + SetStateMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapController:getSnapState', + async () => ({ foo: 'bar' }), + ); + + messenger.registerActionHandler( + 'SnapController:updateSnapState', + async () => undefined, + ); + + messenger.registerActionHandler('SnapController:getSnap', () => + getSnapObject(), + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('sets the encrypted state', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -70,8 +107,17 @@ describe('snap_setState', () => { }); expect(getUnlockPromise).toHaveBeenCalled(); - expect(getSnapState).toHaveBeenCalledWith(true); - expect(updateSnapState).toHaveBeenCalledWith({ foo: 'baz' }, true); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapState', + MOCK_SNAP_ID, + true, + ); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:updateSnapState', + MOCK_SNAP_ID, + { foo: 'baz' }, + true, + ); expect(response).toStrictEqual({ jsonrpc: '2.0', @@ -83,32 +129,22 @@ describe('snap_setState', () => { it('sets the entire state if no key is specified', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -126,8 +162,17 @@ describe('snap_setState', () => { }); expect(getUnlockPromise).toHaveBeenCalled(); - expect(getSnapState).not.toHaveBeenCalled(); - expect(updateSnapState).toHaveBeenCalledWith({ foo: 'baz' }, true); + expect(messenger.call).not.toHaveBeenCalledWith( + 'SnapController:getSnapState', + expect.anything(), + expect.anything(), + ); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:updateSnapState', + MOCK_SNAP_ID, + { foo: 'baz' }, + true, + ); expect(response).toStrictEqual({ jsonrpc: '2.0', @@ -139,32 +184,22 @@ describe('snap_setState', () => { it('sets the unencrypted state', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -182,11 +217,15 @@ describe('snap_setState', () => { }); expect(getUnlockPromise).not.toHaveBeenCalled(); - expect(getSnapState).toHaveBeenCalledWith(false); - expect(updateSnapState).toHaveBeenCalledWith( - { - foo: 'baz', - }, + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:getSnapState', + MOCK_SNAP_ID, + false, + ); + expect(messenger.call).toHaveBeenCalledWith( + 'SnapController:updateSnapState', + MOCK_SNAP_ID, + { foo: 'baz' }, false, ); @@ -202,36 +241,41 @@ describe('snap_setState', () => { const { promise: getStateCalled, resolve: resolveGetStateCalled } = createDeferredPromise(); - const getSnapState = jest.fn().mockImplementation(() => { + + const getUnlockPromise = jest.fn().mockResolvedValue(undefined); + const hooks = { getUnlockPromise }; + + const messenger = getMessenger(); + + const getSnapState = jest.fn().mockImplementation(async () => { resolveGetStateCalled(); return {}; }); + messenger.registerActionHandler( + 'SnapController:getSnapState', + getSnapState, + ); const { promise: updateSnapStatePromise, resolve } = createDeferredPromise(); const updateSnapState = jest.fn().mockReturnValue(updateSnapStatePromise); - const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); - - const hooks = { - getSnapState, + messenger.registerActionHandler( + 'SnapController:updateSnapState', updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -269,7 +313,12 @@ describe('snap_setState', () => { const response2 = await responsePromise2; expect(getSnapState).toHaveBeenCalledTimes(2); - expect(updateSnapState).toHaveBeenNthCalledWith(2, { foo: 'bar' }, false); + expect(updateSnapState).toHaveBeenNthCalledWith( + 2, + MOCK_SNAP_ID, + { foo: 'bar' }, + false, + ); expect(response1).toStrictEqual({ jsonrpc: '2.0', @@ -287,32 +336,27 @@ describe('snap_setState', () => { it('throws if the requesting origin does not have the required permission', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(false); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -325,7 +369,12 @@ describe('snap_setState', () => { params: {}, }); - expect(updateSnapState).not.toHaveBeenCalled(); + expect(messenger.call).not.toHaveBeenCalledWith( + 'SnapController:updateSnapState', + expect.anything(), + expect.anything(), + expect.anything(), + ); expect(response).toStrictEqual({ jsonrpc: '2.0', id: 1, @@ -341,32 +390,22 @@ describe('snap_setState', () => { it('throws if the parameters are invalid', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -394,32 +433,22 @@ describe('snap_setState', () => { it('throws if the encrypted parameter is invalid', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -451,32 +480,22 @@ describe('snap_setState', () => { it('throws if `key` is not provided and `value` is not an object', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue({ - foo: 'bar', - }); - - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -506,29 +525,22 @@ describe('snap_setState', () => { it('throws if the new state is not JSON serialisable', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue(null); - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -561,29 +573,22 @@ describe('snap_setState', () => { it('throws if the new state exceeds the size limit', async () => { const { implementation } = setStateHandler; - const getSnapState = jest.fn().mockReturnValue(null); - const updateSnapState = jest.fn().mockReturnValue(null); const getUnlockPromise = jest.fn().mockResolvedValue(undefined); - const hasPermission = jest.fn().mockReturnValue(true); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); + const hooks = { getUnlockPromise }; - const hooks = { - getSnapState, - updateSnapState, - getUnlockPromise, - hasPermission, - getSnap, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/setState.ts b/packages/snaps-rpc-methods/src/permitted/setState.ts index 5c21ada95f..96842dabe7 100644 --- a/packages/snaps-rpc-methods/src/permitted/setState.ts +++ b/packages/snaps-rpc-methods/src/permitted/setState.ts @@ -1,4 +1,9 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { SetStateParams, @@ -6,11 +11,7 @@ import type { SnapId, } from '@metamask/snaps-sdk'; import type { JsonObject } from '@metamask/snaps-sdk/jsx'; -import { - getJsonSizeUnsafe, - type InferMatching, - type Snap, -} from '@metamask/snaps-utils'; +import { getJsonSizeUnsafe, type InferMatching } from '@metamask/snaps-utils'; import { boolean, create, @@ -18,11 +19,7 @@ import { optional, StructError, } from '@metamask/superstruct'; -import type { - PendingJsonRpcResponse, - Json, - JsonRpcRequest, -} from '@metamask/utils'; +import type { PendingJsonRpcResponse, Json } from '@metamask/utils'; import { hasProperty, isObject, assert, JsonStruct } from '@metamask/utils'; import { Mutex } from 'async-mutex'; @@ -30,20 +27,34 @@ import { manageStateBuilder, STORAGE_SIZE_LIMIT, } from '../restricted/manageState'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, + SnapControllerGetSnapStateAction, + SnapControllerUpdateSnapStateAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; import { FORBIDDEN_KEYS, StateKeyStruct } from '../utils'; -const methodName = 'snap_setState'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - getSnapState: true, +const hookNames: MethodHooksObject = { getUnlockPromise: true, - updateSnapState: true, - getSnap: true, }; +export type SetStateMethodHooks = { + /** + * Wait for the extension to be unlocked. + * + * @returns A promise that resolves once the extension is unlocked. + */ + getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise; +}; + +export type SetStateMethodActions = + | PermissionControllerHasPermissionAction + | SnapControllerGetSnapStateAction + | SnapControllerUpdateSnapStateAction + | SnapControllerGetSnapAction; + /** * Allow the Snap to persist up to 64 MB of data to disk and retrieve it at * will. By default, the data is automatically encrypted using a Snap-specific @@ -93,58 +104,22 @@ const hookNames: MethodHooksObject = { * ``` */ export const setStateHandler = { - methodNames: [methodName] as const, implementation: setStateImplementation, hookNames, -} satisfies PermittedHandlerExport< - SetStateHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapController:getSnapState', + 'SnapController:updateSnapState', + 'SnapController:getSnap', + ], +} satisfies MethodHandler< + SetStateMethodHooks, + SetStateMethodActions, SetStateParameters, - SetStateResult + SetStateResult, + { origin: SnapId } >; -export type SetStateHooks = { - /** - * Check if the requesting origin has a given permission. - * - * @param permissionName - The name of the permission to check. - * @returns Whether the origin has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * Get the state of the requesting Snap. - * - * @param encrypted - Whether the state is encrypted. - * @returns The current state of the Snap. - */ - getSnapState: (encrypted: boolean) => Promise>; - - /** - * Wait for the extension to be unlocked. - * - * @returns A promise that resolves once the extension is unlocked. - */ - getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise; - - /** - * Update the state of the requesting Snap. - * - * @param newState - The new state of the Snap. - * @param encrypted - Whether the state should be encrypted. - */ - updateSnapState: ( - newState: Record, - encrypted: boolean, - ) => Promise; - - /** - * Get Snap metadata. - * - * @param snapId - The ID of a Snap. - */ - getSnap: (snapId: string) => Snap | undefined; -}; - const mutexes = new Map(); /** @@ -180,30 +155,27 @@ export type SetStateParameters = InferMatching< * function. * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - Check whether a given origin has a given - * permission. - * @param hooks.getSnapState - Get the state of the requesting Snap. * @param hooks.getUnlockPromise - Wait for the extension to be unlocked. - * @param hooks.updateSnapState - Update the state of the requesting Snap. - * @param hooks.getSnap - The hook function to get Snap metadata. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ async function setStateImplementation( - request: JsonRpcRequest, + request: JsonRpcRequestWithOrigin, response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { - hasPermission, - getSnapState, - getUnlockPromise, - updateSnapState, - getSnap, - }: SetStateHooks, + { getUnlockPromise }: SetStateMethodHooks, + messenger: Messenger, ): Promise { - const { params } = request; - - if (!hasPermission(manageStateBuilder.targetName)) { + const { params, origin } = request; + + if ( + !messenger.call( + 'PermissionController:hasPermission', + origin, + manageStateBuilder.targetName, + ) + ) { return end(providerErrors.unauthorized()); } @@ -223,9 +195,7 @@ async function setStateImplementation( await getUnlockPromise(true); } - const snapId = ( - request as JsonRpcRequest & { origin: string } - ).origin as SnapId; + const snapId = origin as SnapId; const mutex = getMutex(snapId); @@ -233,9 +203,15 @@ async function setStateImplementation( // to do in parallel. The mutex ensures that and prevents a bug that was // mostly prevalent on mobile and caused data loss. await mutex.runExclusive(async () => { - const newState = await getNewState(key, value, encrypted, getSnapState); + const newState = await getNewState( + snapId, + key, + value, + encrypted, + messenger, + ); - const snap = getSnap(snapId); + const snap = messenger.call('SnapController:getSnap', origin); if (!snap?.preinstalled) { // We know that the state is valid JSON as per previous validation. @@ -249,7 +225,12 @@ async function setStateImplementation( } } - await updateSnapState(newState, encrypted); + await messenger.call( + 'SnapController:updateSnapState', + origin, + newState, + encrypted, + ); response.result = null; }); } catch (error) { @@ -290,24 +271,30 @@ function getValidatedParams(params?: unknown) { * the key does not exist, it is created (and any missing intermediate keys are * created as well). * + * @param snapId - The Snap ID. * @param key - The key to set. * @param value - The value to set the key to. * @param encrypted - Whether the state is encrypted. - * @param getSnapState - The `getSnapState` hook. + * @param messenger - The messenger used to call controller actions. * @returns The new state of the Snap. */ async function getNewState( + snapId: SnapId, key: string | undefined, value: Json, encrypted: boolean, - getSnapState: SetStateHooks['getSnapState'], -) { + messenger: Messenger, +): Promise> { if (key === undefined) { assert(isObject(value)); return value; } - const state = await getSnapState(encrypted); + const state = await messenger.call( + 'SnapController:getSnapState', + snapId, + encrypted, + ); return set(state, key, value); } diff --git a/packages/snaps-rpc-methods/src/permitted/startTrace.test.ts b/packages/snaps-rpc-methods/src/permitted/startTrace.test.ts index 24120101d0..a883340eb6 100644 --- a/packages/snaps-rpc-methods/src/permitted/startTrace.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/startTrace.test.ts @@ -1,43 +1,69 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { StartTraceParams } from '@metamask/snaps-sdk'; -import type { JsonRpcRequest } from '@metamask/utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; +import type { StartTraceMethodActions } from './startTrace'; import { startTraceHandler } from './startTrace'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_startTrace', () => { describe('startTraceHandler', () => { it('has the expected shape', () => { expect(startTraceHandler).toMatchObject({ - methodNames: ['snap_startTrace'], implementation: expect.any(Function), hookNames: { startTrace: true, - getSnap: true, }, + actionNames: ['SnapController:getSnap'], }); }); }); describe('implementation', () => { + const getMessenger = (preinstalled = true) => { + const messenger = new MockControllerMessenger< + StartTraceMethodActions, + never + >(); + + messenger.registerActionHandler('SnapController:getSnap', () => ({ + ...getSnapObject(), + preinstalled, + })); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('calls the `startTrace` hook with the provided parameters', async () => { const { implementation } = startTraceHandler; const startTrace = jest.fn().mockReturnValue({ traceId: 'test-trace-id', }); + const hooks = { startTrace }; - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { startTrace, getSnap }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); @@ -77,18 +103,21 @@ describe('snap_startTrace', () => { const { implementation } = startTraceHandler; const startTrace = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); - const hooks = { startTrace, getSnap }; + const hooks = { startTrace }; + + const messenger = getMessenger(false); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); @@ -146,18 +175,21 @@ describe('snap_startTrace', () => { const { implementation } = startTraceHandler; const startTrace = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { startTrace, getSnap }; + const hooks = { startTrace }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/startTrace.ts b/packages/snaps-rpc-methods/src/permitted/startTrace.ts index 5bc1aa98c7..483dd75288 100644 --- a/packages/snaps-rpc-methods/src/permitted/startTrace.ts +++ b/packages/snaps-rpc-methods/src/permitted/startTrace.ts @@ -1,13 +1,16 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import { rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, StartTraceParams, StartTraceResult, TraceContext, TraceRequest, } from '@metamask/snaps-sdk'; -import type { InferMatching, Snap } from '@metamask/snaps-utils'; +import type { InferMatching } from '@metamask/snaps-utils'; import { boolean, number, @@ -22,14 +25,14 @@ import { import type { PendingJsonRpcResponse } from '@metamask/utils'; import { JsonStruct } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; -const methodName = 'snap_startTrace'; - const hookNames: MethodHooksObject = { startTrace: true, - getSnap: true, }; export type StartTraceMethodHooks = { @@ -40,15 +43,10 @@ export type StartTraceMethodHooks = { * @returns The performance trace context. */ startTrace: (request: TraceRequest) => TraceContext; - - /** - * Get Snap metadata. - * - * @param snapId - The ID of a Snap. - */ - getSnap: (snapId: string) => Snap | undefined; }; +export type StartTraceMethodActions = SnapControllerGetSnapAction; + const StartTraceParametersStruct = object({ data: exactOptional(record(string(), union([string(), number(), boolean()]))), id: exactOptional(string()), @@ -69,13 +67,15 @@ export type StartTraceParameters = InferMatching< * @internal */ export const startTraceHandler = { - methodNames: [methodName] as const, implementation: getStartTraceImplementation, hookNames, -} satisfies PermittedHandlerExport< + actionNames: ['SnapController:getSnap'], +} satisfies MethodHandler< StartTraceMethodHooks, + StartTraceMethodActions, StartTraceParameters, - StartTraceResult + StartTraceResult, + { origin: string } >; /** @@ -89,19 +89,18 @@ export const startTraceHandler = { * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. * @param hooks.startTrace - The hook function to start a performance trace. - * @param hooks.getSnap - The hook function to get Snap metadata. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getStartTraceImplementation( - request: JsonRpcRequest, + request: JsonRpcRequestWithOrigin, response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { startTrace, getSnap }: StartTraceMethodHooks, + { startTrace }: StartTraceMethodHooks, + messenger: Messenger, ): void { - const snap = getSnap( - (request as JsonRpcRequest & { origin: string }).origin, - ); + const snap = messenger.call('SnapController:getSnap', request.origin); if (!snap?.preinstalled) { return end(rpcErrors.methodNotFound()); diff --git a/packages/snaps-rpc-methods/src/permitted/trackError.test.ts b/packages/snaps-rpc-methods/src/permitted/trackError.test.ts index ead76aee3e..74de6c9574 100644 --- a/packages/snaps-rpc-methods/src/permitted/trackError.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/trackError.test.ts @@ -1,41 +1,69 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { TrackErrorParams, TrackErrorResult } from '@metamask/snaps-sdk'; import { getJsonError } from '@metamask/snaps-sdk'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; - +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; + +import type { TrackErrorMethodActions } from './trackError'; import { trackErrorHandler } from './trackError'; +import type { JsonRpcRequestWithOrigin } from '../types'; describe('snap_trackError', () => { describe('trackErrorHandler', () => { it('has the expected shape', () => { expect(trackErrorHandler).toMatchObject({ - methodNames: ['snap_trackError'], implementation: expect.any(Function), hookNames: { trackError: true, - getSnap: true, }, + actionNames: ['SnapController:getSnap'], }); }); }); describe('implementation', () => { + const getMessenger = (preinstalled = true) => { + const messenger = new MockControllerMessenger< + TrackErrorMethodActions, + never + >(); + + messenger.registerActionHandler('SnapController:getSnap', () => ({ + ...getSnapObject(), + preinstalled, + })); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('tracks an error with a name, message, and stack', async () => { const { implementation } = trackErrorHandler; const trackError = jest.fn().mockReturnValue('test-id'); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackError, getSnap }; + const hooks = { trackError }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -77,18 +105,21 @@ describe('snap_trackError', () => { const { implementation } = trackErrorHandler; const trackError = jest.fn().mockReturnValue('test-id'); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackError, getSnap }; + const hooks = { trackError }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -143,18 +174,21 @@ describe('snap_trackError', () => { const { implementation } = trackErrorHandler; const trackError = jest.fn().mockReturnValue('test-id'); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackError, getSnap }; + const hooks = { trackError }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -194,18 +228,21 @@ describe('snap_trackError', () => { const { implementation } = trackErrorHandler; const trackError = jest.fn().mockReturnValue('test-id'); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackError, getSnap }; + const hooks = { trackError }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -243,18 +280,21 @@ describe('snap_trackError', () => { const { implementation } = trackErrorHandler; const trackError = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); - const hooks = { trackError, getSnap }; + const hooks = { trackError }; + + const messenger = getMessenger(false); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -309,18 +349,21 @@ describe('snap_trackError', () => { const { implementation } = trackErrorHandler; const trackError = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackError, getSnap }; + const hooks = { trackError }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/trackError.ts b/packages/snaps-rpc-methods/src/permitted/trackError.ts index a96f65d557..0544a9e5e9 100644 --- a/packages/snaps-rpc-methods/src/permitted/trackError.ts +++ b/packages/snaps-rpc-methods/src/permitted/trackError.ts @@ -1,24 +1,27 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; import { rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, TrackableError, TrackErrorParams, TrackErrorResult, } from '@metamask/snaps-sdk'; -import type { InferMatching, Snap } from '@metamask/snaps-utils'; +import type { InferMatching } from '@metamask/snaps-utils'; import { TrackableErrorStruct } from '@metamask/snaps-utils'; import { create, object, StructError } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; -const methodName = 'snap_trackError'; - const hookNames: MethodHooksObject = { trackError: true, - getSnap: true, }; export type TrackErrorMethodHooks = { @@ -30,15 +33,10 @@ export type TrackErrorMethodHooks = { * in the client. */ trackError: (error: Error) => string; - - /** - * Get Snap metadata. - * - * @param snapId - The ID of a Snap. - */ - getSnap: (snapId: string) => Snap | undefined; }; +export type TrackErrorMethodActions = SnapControllerGetSnapAction; + const TrackErrorParametersStruct = object({ error: TrackableErrorStruct, }); @@ -54,13 +52,15 @@ export type TrackErrorParameters = InferMatching< * @internal */ export const trackErrorHandler = { - methodNames: [methodName] as const, implementation: getTrackErrorImplementation, hookNames, -} satisfies PermittedHandlerExport< + actionNames: ['SnapController:getSnap'], +} satisfies MethodHandler< TrackErrorMethodHooks, + TrackErrorMethodActions, TrackErrorParameters, - TrackErrorResult + TrackErrorResult, + { origin: string } >; /** @@ -74,19 +74,18 @@ export const trackErrorHandler = { * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. * @param hooks.trackError - The hook function to track an error. - * @param hooks.getSnap - The hook function to get Snap metadata. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getTrackErrorImplementation( - request: JsonRpcRequest, + request: JsonRpcRequestWithOrigin, response: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { trackError, getSnap }: TrackErrorMethodHooks, + { trackError }: TrackErrorMethodHooks, + messenger: Messenger, ): void { - const snap = getSnap( - (request as JsonRpcRequest & { origin: string }).origin, - ); + const snap = messenger.call('SnapController:getSnap', request.origin); if (!snap?.preinstalled) { return end(rpcErrors.methodNotFound()); diff --git a/packages/snaps-rpc-methods/src/permitted/trackEvent.test.ts b/packages/snaps-rpc-methods/src/permitted/trackEvent.test.ts index 2910d9dd1d..c9524908b5 100644 --- a/packages/snaps-rpc-methods/src/permitted/trackEvent.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/trackEvent.test.ts @@ -1,41 +1,69 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { TrackEventParams, TrackEventResult } from '@metamask/snaps-sdk'; -import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; - +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; +import type { PendingJsonRpcResponse } from '@metamask/utils'; + +import type { TrackEventMethodActions } from './trackEvent'; import { trackEventHandler } from './trackEvent'; +import type { JsonRpcRequestWithOrigin } from '../types'; /* eslint-disable @typescript-eslint/naming-convention */ describe('snap_trackEvent', () => { describe('trackEventHandler', () => { it('has the expected shape', () => { expect(trackEventHandler).toMatchObject({ - methodNames: ['snap_trackEvent'], implementation: expect.any(Function), hookNames: { trackEvent: true, - getSnap: true, }, + actionNames: ['SnapController:getSnap'], }); }); }); describe('implementation', () => { + const getMessenger = (preinstalled = true) => { + const messenger = new MockControllerMessenger< + TrackEventMethodActions, + never + >(); + + messenger.registerActionHandler('SnapController:getSnap', () => ({ + ...getSnapObject(), + preinstalled, + })); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('tracks an event with no properties', async () => { const { implementation } = trackEventHandler; const trackEvent = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackEvent, getSnap }; + const hooks = { trackEvent }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -62,18 +90,21 @@ describe('snap_trackEvent', () => { const { implementation } = trackEventHandler; const trackEvent = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackEvent, getSnap }; + const hooks = { trackEvent }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -108,18 +139,21 @@ describe('snap_trackEvent', () => { const { implementation } = trackEventHandler; const trackEvent = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackEvent, getSnap }; + const hooks = { trackEvent }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -154,18 +188,21 @@ describe('snap_trackEvent', () => { const { implementation } = trackEventHandler; const trackEvent = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackEvent, getSnap }; + const hooks = { trackEvent }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -203,18 +240,21 @@ describe('snap_trackEvent', () => { const { implementation } = trackEventHandler; const trackEvent = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackEvent, getSnap }; + const hooks = { trackEvent }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -252,18 +292,21 @@ describe('snap_trackEvent', () => { const { implementation } = trackEventHandler; const trackEvent = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: true }); - const hooks = { trackEvent, getSnap }; + const hooks = { trackEvent }; + + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); @@ -295,18 +338,21 @@ describe('snap_trackEvent', () => { const { implementation } = trackEventHandler; const trackEvent = jest.fn(); - const getSnap = jest.fn().mockReturnValue({ preinstalled: false }); - const hooks = { trackEvent, getSnap }; + const hooks = { trackEvent }; + + const messenger = getMessenger(false); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequestWithOrigin, response as PendingJsonRpcResponse, next, end, hooks, + messenger, ); result?.catch(end); diff --git a/packages/snaps-rpc-methods/src/permitted/trackEvent.ts b/packages/snaps-rpc-methods/src/permitted/trackEvent.ts index 0ab344f04d..629fe58a97 100644 --- a/packages/snaps-rpc-methods/src/permitted/trackEvent.ts +++ b/packages/snaps-rpc-methods/src/permitted/trackEvent.ts @@ -1,11 +1,11 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; -import { rpcErrors } from '@metamask/rpc-errors'; import type { - JsonRpcRequest, - TrackEventParams, - TrackEventResult, -} from '@metamask/snaps-sdk'; -import type { InferMatching, Snap } from '@metamask/snaps-utils'; + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { TrackEventParams, TrackEventResult } from '@metamask/snaps-sdk'; +import type { InferMatching } from '@metamask/snaps-utils'; import { create, object, @@ -18,11 +18,12 @@ import { import type { Json, PendingJsonRpcResponse } from '@metamask/utils'; import { JsonStruct } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, +} from '../types'; import type { MethodHooksObject } from '../utils'; -const methodName = 'snap_trackEvent'; - const PropertiesStruct = optional(record(string(), JsonStruct)); const snakeCaseRegex = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/u; @@ -41,7 +42,6 @@ const SnakeCasePropertiesStruct = refine( const hookNames: MethodHooksObject = { trackEvent: true, - getSnap: true, }; export type TrackEventMethodHooks = { @@ -51,14 +51,10 @@ export type TrackEventMethodHooks = { * @param event - The event object containing event details and properties. */ trackEvent: (event: TrackEventObject) => void; - /** - * Get Snap metadata. - * - * @param snapId - The ID of a Snap. - */ - getSnap: (snapId: string) => Snap | undefined; }; +export type TrackEventMethodActions = SnapControllerGetSnapAction; + export type TrackEventObject = { event: string; properties?: Record; @@ -84,13 +80,15 @@ export type TrackEventParameters = InferMatching< * @internal */ export const trackEventHandler = { - methodNames: [methodName] as const, implementation: getTrackEventImplementation, hookNames, -} satisfies PermittedHandlerExport< + actionNames: ['SnapController:getSnap'], +} satisfies MethodHandler< TrackEventMethodHooks, + TrackEventMethodActions, TrackEventParameters, - TrackEventResult + TrackEventResult, + { origin: string } >; /** @@ -103,19 +101,18 @@ export const trackEventHandler = { * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. * @param hooks.trackEvent - The function to track the event. - * @param hooks.getSnap - The function to get Snap metadata. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getTrackEventImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { trackEvent, getSnap }: TrackEventMethodHooks, + { trackEvent }: TrackEventMethodHooks, + messenger: Messenger, ): void { - const snap = getSnap( - (req as JsonRpcRequest & { origin: string }).origin, - ); + const snap = messenger.call('SnapController:getSnap', req.origin); if (!snap?.preinstalled) { return end(rpcErrors.methodNotFound()); diff --git a/packages/snaps-rpc-methods/src/permitted/updateInterface.test.tsx b/packages/snaps-rpc-methods/src/permitted/updateInterface.test.tsx index 9e7e027dc5..577be889e8 100644 --- a/packages/snaps-rpc-methods/src/permitted/updateInterface.test.tsx +++ b/packages/snaps-rpc-methods/src/permitted/updateInterface.test.tsx @@ -1,46 +1,80 @@ -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; import type { UpdateInterfaceParams, UpdateInterfaceResult, } from '@metamask/snaps-sdk'; import { NodeType } from '@metamask/snaps-sdk'; import { Box, type JSXElement, Text } from '@metamask/snaps-sdk/jsx'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, +} from '@metamask/snaps-utils/test-utils'; import type { JsonRpcRequest, PendingJsonRpcResponse } from '@metamask/utils'; +import type { UpdateInterfaceMethodActions } from './updateInterface'; import { updateInterfaceHandler } from './updateInterface'; describe('snap_updateInterface', () => { describe('updateInterfaceHandler', () => { it('has the expected shape', () => { expect(updateInterfaceHandler).toMatchObject({ - methodNames: ['snap_updateInterface'], implementation: expect.any(Function), - hookNames: { - hasPermission: true, - updateInterface: true, - }, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:updateInterface', + ], }); }); }); describe('implementation', () => { + const getMessenger = () => { + const messenger = new MockControllerMessenger< + UpdateInterfaceMethodActions, + never + >(); + + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => true, + ); + + messenger.registerActionHandler( + 'SnapInterfaceController:updateInterface', + () => undefined, + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + it('throws if the origin does not have permission to show UI', async () => { const { implementation } = updateInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(false); - const updateInterface = jest.fn(); + const messenger = getMessenger(); - const hooks = { hasPermission, updateInterface }; + messenger.registerActionHandler( + 'PermissionController:hasPermission', + () => false, + ); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -72,26 +106,24 @@ describe('snap_updateInterface', () => { }); }); - it('returns the result from the `updateInterface` hook', async () => { + it('returns null after calling the `SnapInterfaceController:updateInterface` action', async () => { const { implementation } = updateInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const updateInterface = jest.fn(); - - const hooks = { - hasPermission, - updateInterface, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -113,23 +145,21 @@ describe('snap_updateInterface', () => { it('updates a JSX interface', async () => { const { implementation } = updateInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const updateInterface = jest.fn(); - - const hooks = { - hasPermission, - updateInterface, - }; + const messenger = getMessenger(); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); engine.push((request, response, next, end) => { const result = implementation( - request as JsonRpcRequest, + request as JsonRpcRequest & { + origin: string; + }, response as PendingJsonRpcResponse, next, end, - hooks, + {} as never, + messenger, ); result?.catch(end); @@ -149,7 +179,9 @@ describe('snap_updateInterface', () => { }, }); - expect(updateInterface).toHaveBeenCalledWith( + expect(messenger.call).toHaveBeenCalledWith( + 'SnapInterfaceController:updateInterface', + MOCK_SNAP_ID, 'foo', Hello, world! @@ -157,100 +189,98 @@ describe('snap_updateInterface', () => { undefined, ); }); - }); - it('updates the interface context', async () => { - const { implementation } = updateInterfaceHandler; + it('updates the interface context', async () => { + const { implementation } = updateInterfaceHandler; - const hasPermission = jest.fn().mockReturnValue(true); - const updateInterface = jest.fn(); + const messenger = getMessenger(); - const hooks = { - hasPermission, - updateInterface, - }; + const engine = new JsonRpcEngine(); - const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest & { + origin: string; + }, + response as PendingJsonRpcResponse, + next, + end, + {} as never, + messenger, + ); - engine.push((request, response, next, end) => { - const result = implementation( - request as JsonRpcRequest, - response as PendingJsonRpcResponse, - next, - end, - hooks, - ); + result?.catch(end); + }); - result?.catch(end); - }); + await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_updateInterface', + params: { + id: 'foo', + ui: ( + + Hello, world! + + ) as JSXElement, + context: { foo: 'bar' }, + }, + }); - await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'snap_updateInterface', - params: { - id: 'foo', - ui: ( - - Hello, world! - - ) as JSXElement, - context: { foo: 'bar' }, - }, + expect(messenger.call).toHaveBeenCalledWith( + 'SnapInterfaceController:updateInterface', + MOCK_SNAP_ID, + 'foo', + + Hello, world! + , + { foo: 'bar' }, + ); }); - expect(updateInterface).toHaveBeenCalledWith( - 'foo', - - Hello, world! - , - { foo: 'bar' }, - ); - }); - - it('throws on invalid params', async () => { - const { implementation } = updateInterfaceHandler; - - const hasPermission = jest.fn().mockReturnValue(true); - const updateInterface = jest.fn(); + it('throws on invalid params', async () => { + const { implementation } = updateInterfaceHandler; - const hooks = { - hasPermission, - updateInterface, - }; + const messenger = getMessenger(); - const engine = new JsonRpcEngine(); + const engine = new JsonRpcEngine(); - engine.push((request, response, next, end) => { - const result = implementation( - request as JsonRpcRequest, - response as PendingJsonRpcResponse, - next, - end, - hooks, - ); + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequest & { + origin: string; + }, + response as PendingJsonRpcResponse, + next, + end, + {} as never, + messenger, + ); - result?.catch(end); - }); + result?.catch(end); + }); - const response = await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'snap_updateInterface', - params: { - id: 42, - }, - }); + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_updateInterface', + params: { + id: 42, + }, + }); - expect(response).toStrictEqual({ - error: { - code: -32602, - message: - 'Invalid params: At path: id -- Expected a string, but received: 42.', - stack: expect.any(String), - }, - id: 1, - jsonrpc: '2.0', + expect(response).toStrictEqual({ + error: { + code: -32602, + message: + 'Invalid params: At path: id -- Expected a string, but received: 42.', + stack: expect.any(String), + }, + id: 1, + jsonrpc: '2.0', + }); }); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/updateInterface.ts b/packages/snaps-rpc-methods/src/permitted/updateInterface.ts index 03710a36c2..0906af6b2b 100644 --- a/packages/snaps-rpc-methods/src/permitted/updateInterface.ts +++ b/packages/snaps-rpc-methods/src/permitted/updateInterface.ts @@ -1,11 +1,13 @@ -import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger } from '@metamask/messenger'; +import type { PermissionControllerHasPermissionAction } from '@metamask/permission-controller'; import { providerErrors, rpcErrors } from '@metamask/rpc-errors'; import type { UpdateInterfaceParams, UpdateInterfaceResult, - JsonRpcRequest, - ComponentOrElement, - InterfaceContext, } from '@metamask/snaps-sdk'; import { ComponentOrElementStruct, @@ -21,35 +23,15 @@ import { } from '@metamask/superstruct'; import type { PendingJsonRpcResponse } from '@metamask/utils'; -import type { PermittedHandlerExport } from '../types'; -import type { MethodHooksObject } from '../utils'; +import type { + JsonRpcRequestWithOrigin, + SnapInterfaceControllerUpdateInterfaceAction, +} from '../types'; import { UI_PERMISSIONS } from '../utils'; -const methodName = 'snap_updateInterface'; - -const hookNames: MethodHooksObject = { - hasPermission: true, - updateInterface: true, -}; - -export type UpdateInterfaceMethodHooks = { - /** - * @param permissionName - The name of the permission to check. - * @returns Whether the Snap has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * @param id - The interface ID. - * @param ui - The UI components. - * @param context - The optional interface context object. - */ - updateInterface: ( - id: string, - ui: ComponentOrElement, - context?: InterfaceContext, - ) => void; -}; +export type UpdateInterfaceMethodActions = + | PermissionControllerHasPermissionAction + | SnapInterfaceControllerUpdateInterfaceAction; /** * Update an interactive interface. For use in @@ -87,13 +69,17 @@ export type UpdateInterfaceMethodHooks = { * ``` */ export const updateInterfaceHandler = { - methodNames: [methodName] as const, implementation: getUpdateInterfaceImplementation, - hookNames, -} satisfies PermittedHandlerExport< - UpdateInterfaceMethodHooks, + actionNames: [ + 'PermissionController:hasPermission', + 'SnapInterfaceController:updateInterface', + ], +} satisfies MethodHandler< + never, + UpdateInterfaceMethodActions, UpdateInterfaceParameters, - UpdateInterfaceResult + UpdateInterfaceResult, + { origin: string } >; const UpdateInterfaceParametersStruct = object({ @@ -115,20 +101,25 @@ export type UpdateInterfaceParameters = InferMatching< * @param _next - The `json-rpc-engine` "next" callback. Not used by this * function. * @param end - The `json-rpc-engine` "end" callback. - * @param hooks - The RPC method hooks. - * @param hooks.hasPermission - The function to check if the Snap has a given - * permission. - * @param hooks.updateInterface - The function to update the interface. + * @param _hooks - The RPC method hooks. Not used by this function. + * @param messenger - The messenger used to call controller actions. * @returns Nothing. */ function getUpdateInterfaceImplementation( - req: JsonRpcRequest, + req: JsonRpcRequestWithOrigin, res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { hasPermission, updateInterface }: UpdateInterfaceMethodHooks, + _hooks: never, + messenger: Messenger, ): void { - if (!UI_PERMISSIONS.some(hasPermission)) { + const { params, origin } = req; + + const isPermitted = UI_PERMISSIONS.some((permission) => + messenger.call('PermissionController:hasPermission', origin, permission), + ); + + if (!isPermitted) { return end( providerErrors.unauthorized({ message: `This method can only be used if the Snap has one of the following permissions: ${UI_PERMISSIONS.join(', ')}.`, @@ -136,14 +127,18 @@ function getUpdateInterfaceImplementation( ); } - const { params } = req; - try { const validatedParams = getValidatedParams(params); const { id, ui, context } = validatedParams; - updateInterface(id, ui, context); + messenger.call( + 'SnapInterfaceController:updateInterface', + origin, + id, + ui, + context, + ); res.result = null; } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/restricted/dialog.test.tsx b/packages/snaps-rpc-methods/src/restricted/dialog.test.tsx index bb4ca52c2f..3988bedabe 100644 --- a/packages/snaps-rpc-methods/src/restricted/dialog.test.tsx +++ b/packages/snaps-rpc-methods/src/restricted/dialog.test.tsx @@ -64,6 +64,7 @@ describe('implementation', () => { content: { type: NodeType.Text as const, value: 'foo' }, state: {}, snapId: 'foo' as SnapId, + context: null, }), ); diff --git a/packages/snaps-rpc-methods/src/types.ts b/packages/snaps-rpc-methods/src/types.ts index 09ad4d558d..ebeeabdd7d 100644 --- a/packages/snaps-rpc-methods/src/types.ts +++ b/packages/snaps-rpc-methods/src/types.ts @@ -1,61 +1,26 @@ import type { - JsonRpcEngineEndCallback, - JsonRpcEngineNextCallback, -} from '@metamask/json-rpc-engine'; -import type { + AuxiliaryFileEncoding, + BackgroundEvent, ComponentOrElement, - ContentType, + GetSnapsResult, + GetWebSocketsResult, InterfaceContext, InterfaceState, + RequestSnapsResult, + ContentType, + RequestSnapsParams, SnapId, } from '@metamask/snaps-sdk'; -import type { Snap, SnapRpcHookArgs } from '@metamask/snaps-utils'; import type { - Json, - JsonRpcParams, - JsonRpcRequest, - PendingJsonRpcResponse, -} from '@metamask/utils'; - -// The types below are temporarily copied to this repo until we can migrate away from `PermittedHandlerExport`. - -/** - * A middleware function for handling a permitted method. - */ -type HandlerMiddlewareFunction< - Hooks, - Params extends JsonRpcParams, - Result extends Json, -> = ( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, - next: JsonRpcEngineNextCallback, - end: JsonRpcEngineEndCallback, - hooks: Hooks, -) => void | Promise; - -/** - * We use a mapped object type in order to create a type that requires the - * presence of the names of all hooks for the given handler. - * This can then be used to select only the necessary hooks whenever a method - * is called for purposes of POLA. - */ -type HookNames = { - [Property in keyof HookMap]: true; -}; - -/** - * A handler for a permitted method. - */ -export type PermittedHandlerExport< - Hooks, - Params extends JsonRpcParams, - Result extends Json, -> = { - implementation: HandlerMiddlewareFunction; - hookNames: HookNames; - methodNames: string[]; -}; + Snap, + SnapRpcHookArgs, + TruncatedSnap, +} from '@metamask/snaps-utils'; +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; + +export type JsonRpcRequestWithOrigin< + Params extends JsonRpcParams = JsonRpcParams, +> = JsonRpcRequest & { origin: string }; export type HdKeyring = { type: 'HD Key Tree'; @@ -76,6 +41,17 @@ export type KeyringControllerWithKeyringAction = { ) => Promise; }; +export type KeyringControllerGetStateAction = { + type: `KeyringController:getState`; + handler: () => { + isUnlocked: boolean; + keyrings: { + type: string; + metadata: { id: string; name: string }; + }[]; + }; +}; + export type ApprovalControllerAddRequestAction = { type: 'ApprovalController:addRequest'; handler: ( @@ -118,6 +94,26 @@ export type SnapInterfaceControllerSetInterfaceDisplayedAction = { handler: (snapId: string, id: string) => void; }; +export type SnapInterfaceControllerGetInterfaceStateAction = { + type: `SnapInterfaceController:getInterfaceState`; + handler: (snapId: string, id: string) => InterfaceState; +}; + +export type SnapInterfaceControllerUpdateInterfaceAction = { + type: `SnapInterfaceController:updateInterface`; + handler: ( + snapId: string, + id: string, + content: ComponentOrElement, + context?: InterfaceContext, + ) => void; +}; + +export type SnapInterfaceControllerResolveInterfaceAction = { + type: `SnapInterfaceController:resolveInterface`; + handler: (snapId: string, id: string, value: Json) => Promise; +}; + export type SnapControllerHandleRequestAction = { type: 'SnapController:handleRequest'; handler: (args: SnapRpcHookArgs & { snapId: string }) => Promise; @@ -150,6 +146,33 @@ export type SnapControllerUpdateSnapStateAction = { ) => Promise; }; +export type SnapControllerGetAllSnapsAction = { + type: `SnapController:getAllSnaps`; + handler: () => TruncatedSnap[]; +}; + +export type SnapControllerGetPermittedSnapsAction = { + type: `SnapController:getPermittedSnaps`; + handler: (origin: string) => GetSnapsResult; +}; + +export type SnapControllerInstallSnapsAction = { + type: `SnapController:installSnaps`; + handler: ( + origin: string, + requestedSnaps: RequestSnapsParams, + ) => Promise; +}; + +export type SnapControllerGetSnapFileAction = { + type: `SnapController:getSnapFile`; + handler: ( + snapId: string, + path: string, + encoding?: AuxiliaryFileEncoding, + ) => Promise; +}; + export type RateLimitControllerCallAction = { type: 'RateLimitController:call'; handler: ( @@ -158,3 +181,56 @@ export type RateLimitControllerCallAction = { ...args: unknown[] ) => Promise; }; + +export type CronjobControllerCancelAction = { + type: `CronjobController:cancel`; + handler: (origin: string, id: string) => void; +}; + +export type CronjobControllerScheduleAction = { + type: `CronjobController:schedule`; + handler: (event: { + snapId: string; + request: { + method: string; + jsonrpc?: '2.0'; + id?: string | number | null; + params?: Json[] | Record; + }; + schedule: string; + id?: string; + }) => string; +}; + +export type CronjobControllerGetAction = { + type: `CronjobController:get`; + handler: (snapId: string) => BackgroundEvent[]; +}; + +export type WebSocketServiceOpenAction = { + type: `WebSocketService:open`; + handler: ( + snapId: string, + url: string, + protocols?: string[], + ) => Promise; +}; + +export type WebSocketServiceCloseAction = { + type: `WebSocketService:close`; + handler: (snapId: string, id: string) => void; +}; + +export type WebSocketServiceSendMessageAction = { + type: `WebSocketService:sendMessage`; + handler: ( + snapId: string, + id: string, + data: string | number[], + ) => Promise; +}; + +export type WebSocketServiceGetAllAction = { + type: `WebSocketService:getAll`; + handler: (snapId: string) => GetWebSocketsResult; +}; diff --git a/packages/snaps-rpc-methods/src/utils.ts b/packages/snaps-rpc-methods/src/utils.ts index ca9abee802..31e050a13d 100644 --- a/packages/snaps-rpc-methods/src/utils.ts +++ b/packages/snaps-rpc-methods/src/utils.ts @@ -36,38 +36,6 @@ export type MethodHooksObject> = { [Key in keyof HooksType]: true; }; -/** - * Returns the subset of the specified `hooks` that are included in the - * `hookNames` object. This is a Principle of Least Authority (POLA) measure - * to ensure that each RPC method implementation only has access to the - * API "hooks" it needs to do its job. - * - * @param hooks - The hooks to select from. - * @param hookNames - The names of the hooks to select. - * @returns The selected hooks. - * @template Hooks - The hooks to select from. - * @template HookName - The names of the hooks to select. - */ -export function selectHooks< - Hooks extends Record, - HookName extends keyof Hooks, ->( - hooks: Hooks, - hookNames?: Record, -): Pick | undefined { - if (hookNames) { - return Object.keys(hookNames).reduce>>( - (hookSubset, _hookName) => { - const hookName = _hookName as HookName; - hookSubset[hookName] = hooks[hookName]; - return hookSubset; - }, - {}, - ) as Pick; - } - return undefined; -} - /** * Get a BIP-32 derivation path array from a hash, which is compatible with * `@metamask/key-tree`. The hash is assumed to be 32 bytes long. diff --git a/packages/snaps-sdk/src/types/methods/get-file.ts b/packages/snaps-sdk/src/types/methods/get-file.ts index a9b950b8fc..c4065ee1ac 100644 --- a/packages/snaps-sdk/src/types/methods/get-file.ts +++ b/packages/snaps-sdk/src/types/methods/get-file.ts @@ -26,6 +26,7 @@ export type GetFileParams = { }; /** - * The file content as a string in the requested encoding. + * The file content as a string in the requested encoding, or `null` if the + * file does not exist. */ -export type GetFileResult = string; +export type GetFileResult = string | null; diff --git a/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.test.ts b/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.test.ts deleted file mode 100644 index a84bb839d8..0000000000 --- a/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { getGetEntropySourcesImplementation } from './get-entropy-sources'; - -describe('getGetEntropySourcesImplementation', () => { - it('returns the implementation of the `getEntropySources` hook', async () => { - const fn = getGetEntropySourcesImplementation(); - - expect(fn()).toStrictEqual([ - { - id: 'default', - name: 'Default Secret Recovery Phrase', - type: 'mnemonic', - primary: true, - }, - { - id: 'alternative', - name: 'Alternative Secret Recovery Phrase', - type: 'mnemonic', - primary: false, - }, - ]); - }); -}); diff --git a/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.ts b/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.ts deleted file mode 100644 index d8adce3945..0000000000 --- a/packages/snaps-simulation/src/methods/hooks/get-entropy-sources.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Get the implementation of the `getEntropySources` hook. - * - * @returns The implementation of the `getEntropySources` hook. Right now, it - * only returns two hard coded entropy source. In the future, it could return a - * configurable list of entropy sources. - */ -export function getGetEntropySourcesImplementation() { - return () => { - return [ - { - id: 'default', - name: 'Default Secret Recovery Phrase', - type: 'mnemonic' as const, - primary: true, - }, - { - id: 'alternative', - name: 'Alternative Secret Recovery Phrase', - type: 'mnemonic' as const, - primary: false, - }, - ]; - }; -} diff --git a/packages/snaps-simulation/src/methods/hooks/index.ts b/packages/snaps-simulation/src/methods/hooks/index.ts index 5cbb44772f..291a2af50a 100644 --- a/packages/snaps-simulation/src/methods/hooks/index.ts +++ b/packages/snaps-simulation/src/methods/hooks/index.ts @@ -1,6 +1,5 @@ export * from './chain'; export * from './end-trace'; -export * from './get-entropy-sources'; export * from './get-mnemonic'; export * from './get-preferences'; export * from './get-snap'; diff --git a/packages/snaps-simulation/src/middleware/engine.test.ts b/packages/snaps-simulation/src/middleware/engine.test.ts index 86f96fcba7..55cbe25b44 100644 --- a/packages/snaps-simulation/src/middleware/engine.test.ts +++ b/packages/snaps-simulation/src/middleware/engine.test.ts @@ -1,9 +1,18 @@ +import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; + import { createJsonRpcEngine } from './engine'; import { createStore } from '../store'; -import { getMockOptions } from '../test-utils'; +import { getMockOptions, getRootControllerMessenger } from '../test-utils'; describe('createJsonRpcEngine', () => { it('creates a JSON-RPC engine', async () => { + const messenger = getRootControllerMessenger(); + + messenger.registerActionHandler( + 'SnapController:getSnapFile', + async () => 'foo', + ); + const { store } = createStore(getMockOptions()); const engine = createJsonRpcEngine({ store, @@ -11,19 +20,29 @@ describe('createJsonRpcEngine', () => { getMnemonic: jest.fn(), getIsLocked: jest.fn(), getClientCryptography: jest.fn(), + getSimulationState: jest.fn(), + getSnap: jest.fn(), + setCurrentChain: jest.fn(), }, permittedHooks: { - getSnapFile: jest.fn().mockResolvedValue('foo'), - getSnapState: jest.fn(), - updateSnapState: jest.fn(), - clearSnapState: jest.fn(), - getInterfaceState: jest.fn(), - getInterfaceContext: jest.fn(), - createInterface: jest.fn(), - updateInterface: jest.fn(), - resolveInterface: jest.fn(), + getIsActive: jest.fn(), + getVersion: jest.fn(), + getUnlockPromise: jest.fn(), + trackError: jest.fn(), + trackEvent: jest.fn(), + startTrace: jest.fn(), + endTrace: jest.fn(), + getAllowedKeyringMethods: jest.fn(), + }, + multichainHooks: { + getAccounts: jest.fn(), + getCaveat: jest.fn(), + grantPermissions: jest.fn(), + revokePermission: jest.fn(), }, - permissionMiddleware: jest.fn(), + messenger, + isMultichain: false, + snapId: MOCK_SNAP_ID, }); expect(engine).toBeDefined(); diff --git a/packages/snaps-simulation/src/middleware/engine.ts b/packages/snaps-simulation/src/middleware/engine.ts index fd3df1fe9d..fff1d6c841 100644 --- a/packages/snaps-simulation/src/middleware/engine.ts +++ b/packages/snaps-simulation/src/middleware/engine.ts @@ -1,13 +1,16 @@ -import type { JsonRpcMiddleware } from '@metamask/json-rpc-engine'; -import { JsonRpcEngine } from '@metamask/json-rpc-engine'; -import type { RestrictedMethodParameters } from '@metamask/permission-controller'; +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; +import { createPermissionMiddleware } from '@metamask/permission-controller'; import { createSnapsMethodMiddleware } from '@metamask/snaps-rpc-methods'; -import type { Json } from '@metamask/utils'; +import type { SnapId } from '@metamask/snaps-sdk'; import { createInternalMethodsMiddleware } from './internal-methods'; import { createMockMiddleware } from './mock'; import { createMultichainMiddleware } from './multichain'; import { createProviderMiddleware } from './provider'; +import type { RootControllerMessenger } from '../controllers'; import type { MultichainMiddlewareHooks, PermittedMiddlewareHooks, @@ -16,11 +19,12 @@ import type { import type { Store } from '../store'; export type CreateJsonRpcEngineOptions = { + snapId: SnapId; + messenger: RootControllerMessenger; store: Store; restrictedHooks: RestrictedMiddlewareHooks; permittedHooks: PermittedMiddlewareHooks; multichainHooks: MultichainMiddlewareHooks; - permissionMiddleware: JsonRpcMiddleware; endpoint?: string; isMultichain: boolean; }; @@ -32,24 +36,28 @@ export type CreateJsonRpcEngineOptions = { * well as Snap-specific requests. * * @param options - The options to use when creating the engine. + * @param options.snapId - The Snap ID. + * @param options.messenger - The messenger. * @param options.store - The Redux store to use. * @param options.restrictedHooks - Any hooks used by the middleware handlers. * @param options.permittedHooks - Any hooks used by the middleware handlers. - * @param options.permissionMiddleware - The permission middleware to use. * @param options.multichainHooks - Hooks used by the multichain middleware. * @param options.isMultichain - Whether the engine is used for multichain. * @returns A JSON-RPC engine. */ export function createJsonRpcEngine({ + snapId, + messenger, store, restrictedHooks, permittedHooks, - permissionMiddleware, multichainHooks, isMultichain, }: CreateJsonRpcEngineOptions) { const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware(snapId)); + engine.push(createMultichainMiddleware(isMultichain, multichainHooks)); engine.push(createMockMiddleware(store)); @@ -57,9 +65,11 @@ export function createJsonRpcEngine({ // The hooks here do not match the hooks used by the clients, so this // middleware should not be used outside of the simulation environment. engine.push(createInternalMethodsMiddleware(restrictedHooks)); - engine.push(createSnapsMethodMiddleware(true, permittedHooks)); - engine.push(permissionMiddleware); + // @ts-expect-error Hooks type mismatch. + engine.push(createSnapsMethodMiddleware(true, permittedHooks, messenger)); + + engine.push(createPermissionMiddleware({ messenger, origin: snapId })); engine.push(createProviderMiddleware(store)); return engine; diff --git a/packages/snaps-simulation/src/simulation.test.ts b/packages/snaps-simulation/src/simulation.test.ts index 9f56dc73a7..ba8cecddbd 100644 --- a/packages/snaps-simulation/src/simulation.test.ts +++ b/packages/snaps-simulation/src/simulation.test.ts @@ -5,20 +5,18 @@ import { import { mnemonicPhraseToBytes, mnemonicToSeed } from '@metamask/key-tree'; import { PermissionDoesNotExistError } from '@metamask/permission-controller'; import { - detectSnapLocation, - fetchSnap, NodeProcessExecutionService, NodeThreadExecutionService, - SnapInterfaceController, } from '@metamask/snaps-controllers/node'; import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import type { CaipAssetType, CaipChainId } from '@metamask/snaps-sdk'; -import { AuxiliaryFileEncoding, NodeType } from '@metamask/snaps-sdk'; +import { AuxiliaryFileEncoding } from '@metamask/snaps-sdk'; import { VirtualFile } from '@metamask/snaps-utils'; import { getSnapManifest, MOCK_SNAP_ID, } from '@metamask/snaps-utils/test-utils'; +import { stringToBytes } from '@metamask/utils'; import { DEFAULT_ALTERNATIVE_SRP, DEFAULT_SRP } from './constants'; import { MOCK_CAVEAT } from './middleware/multichain/test-utils'; @@ -29,11 +27,10 @@ import { installSnap, registerActions, } from './simulation'; -import { createStore, setInterface, setState } from './store'; +import { createStore, setInterface } from './store'; import { getMockOptions, getMockServer, - getRestrictedSnapInterfaceControllerMessenger, getRootControllerMessenger, } from './test-utils'; import { addSnapMetadataToAccount } from './utils/account'; @@ -255,453 +252,30 @@ describe('getRestrictedHooks', () => { }); describe('getPermittedHooks', () => { - const { runSaga, store } = createStore(getMockOptions()); - let controllerMessenger = getRootControllerMessenger(); - - beforeEach(() => { - controllerMessenger = getRootControllerMessenger(); - }); - - it('returns the `hasPermission` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { hasPermission } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - expect(hasPermission('snap_manageState')).toBe(true); - - await close(); - }); + const { runSaga } = createStore(getMockOptions()); it('returns the `getUnlockPromise` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { getUnlockPromise } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); + const { getUnlockPromise } = getPermittedHooks(runSaga); expect(await getUnlockPromise(true)).toBeUndefined(); - - await close(); - }); - - it('returns the `getIsLocked` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { getIsLocked } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - expect(getIsLocked()).toBe(false); - - await close(); }); it('returns the `getIsActive` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { getIsActive } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); + const { getIsActive } = getPermittedHooks(runSaga); expect(getIsActive()).toBe(true); - - await close(); }); it('returns the `getVersion` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { getVersion } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); + const { getVersion } = getPermittedHooks(runSaga); expect(getVersion()).toBe('13.6.0-flask.0'); - - await close(); - }); - - it('returns the `getSnapFile` hook', async () => { - const value = JSON.stringify({ bar: 'baz' }); - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest({ - files: ['foo.json'], - }), - auxiliaryFiles: [ - new VirtualFile({ - path: 'foo.json', - value, - }), - ], - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { getSnapFile } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - const file = await getSnapFile('foo.json', AuxiliaryFileEncoding.Utf8); - expect(file).toStrictEqual(value); - - await close(); - }); - - it('returns the `getSnapState` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { getSnapState } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - store.dispatch( - setState({ state: JSON.stringify({ foo: 'bar' }), encrypted: true }), - ); - - expect(await getSnapState(true)).toStrictEqual({ foo: 'bar' }); - - await close(); }); - it('returns the `updateSnapState` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { updateSnapState } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - store.dispatch( - setState({ state: JSON.stringify({ foo: 'bar' }), encrypted: true }), - ); - - await updateSnapState({ bar: 'baz' }, true); - - expect(store.getState().state.encrypted).toStrictEqual( - JSON.stringify({ bar: 'baz' }), - ); + it('returns the `getAllowedKeyringMethods` hook', async () => { + const { getAllowedKeyringMethods } = getPermittedHooks(runSaga); - await close(); - }); - - it('returns the `clearSnapState` hook', async () => { - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { clearSnapState } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - store.dispatch( - setState({ state: JSON.stringify({ foo: 'bar' }), encrypted: true }), - ); - - await clearSnapState(true); - - expect(store.getState().state.encrypted).toBeNull(); - - await close(); - }); - it('returns the `createInterface` hook', async () => { - // eslint-disable-next-line no-new - new SnapInterfaceController({ - messenger: - getRestrictedSnapInterfaceControllerMessenger(controllerMessenger), - }); - - jest.spyOn(controllerMessenger, 'call'); - - const content = { type: NodeType.Text as const, value: 'foo' }; - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { createInterface } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - createInterface(content); - - expect(controllerMessenger.call).toHaveBeenCalledWith( - 'SnapInterfaceController:createInterface', - snapId, - content, - ); - - await close(); - }); - - it('returns the `updateInterface` hook', async () => { - // eslint-disable-next-line no-new - new SnapInterfaceController({ - messenger: - getRestrictedSnapInterfaceControllerMessenger(controllerMessenger), - }); - - jest.spyOn(controllerMessenger, 'call'); - - const content = { type: NodeType.Text as const, value: 'bar' }; - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { createInterface, updateInterface } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - const id = createInterface({ type: NodeType.Text as const, value: 'foo' }); - - updateInterface(id, content); - - expect(controllerMessenger.call).toHaveBeenNthCalledWith( - 2, - 'SnapInterfaceController:updateInterface', - snapId, - id, - content, - ); - - await close(); - }); - - it('returns the `getInterfaceState` hook', async () => { - const controller = new SnapInterfaceController({ - messenger: - getRestrictedSnapInterfaceControllerMessenger(controllerMessenger), - }); - - jest.spyOn(controllerMessenger, 'call'); - - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { createInterface, getInterfaceState } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - const id = createInterface({ type: NodeType.Text as const, value: 'foo' }); - controller.setInterfaceDisplayed(snapId, id); - - const result = getInterfaceState(id); - - expect(controllerMessenger.call).toHaveBeenNthCalledWith( - 2, - 'SnapInterfaceController:getInterfaceState', - snapId, - id, - ); - - expect(result).toStrictEqual({}); - await close(); - }); - - it('returns the `getInterfaceContext` hook', async () => { - // eslint-disable-next-line no-new - new SnapInterfaceController({ - messenger: - getRestrictedSnapInterfaceControllerMessenger(controllerMessenger), - }); - - jest.spyOn(controllerMessenger, 'call'); - - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const { createInterface, getInterfaceContext } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - const id = createInterface( - { type: NodeType.Text as const, value: 'foo' }, - { bar: 'baz' }, - ); - - const result = getInterfaceContext(id); - - expect(controllerMessenger.call).toHaveBeenNthCalledWith( - 2, - 'SnapInterfaceController:getInterface', - snapId, - id, - ); - - expect(result).toStrictEqual({ bar: 'baz' }); - await close(); - }); - - it('returns the `resolveInterface` hook', async () => { - const snapInterfaceController = new SnapInterfaceController({ - messenger: - getRestrictedSnapInterfaceControllerMessenger(controllerMessenger), - }); - - jest.spyOn(controllerMessenger, 'call'); - - const { snapId, close } = await getMockServer({ - manifest: getSnapManifest(), - }); - - const location = detectSnapLocation(snapId, { - allowLocal: true, - }); - - const snapFiles = await fetchSnap(snapId, location); - - const id = snapInterfaceController.createInterface(snapId, { - type: NodeType.Text as const, - value: 'foo', - }); - - const { resolveInterface } = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); - - await resolveInterface(id, 'foobar'); - - expect(controllerMessenger.call).toHaveBeenNthCalledWith( - 1, - 'SnapInterfaceController:resolveInterface', - snapId, - id, - 'foobar', - ); - - await close(); + expect(getAllowedKeyringMethods()).toStrictEqual([]); }); }); @@ -846,7 +420,7 @@ describe('registerActions', () => { const controllerMessenger = getRootControllerMessenger(false); it('registers `PhishingController:testOrigin`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); expect( controllerMessenger.call('PhishingController:testOrigin', 'foo'), @@ -854,7 +428,7 @@ describe('registerActions', () => { }); it('registers `ApprovalController:hasRequest`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); store.dispatch( setInterface({ type: DIALOG_APPROVAL_TYPES.default, id: 'foo' }), @@ -866,7 +440,7 @@ describe('registerActions', () => { }); it('registers `ApprovalController:acceptRequest`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); store.dispatch( setInterface({ type: DIALOG_APPROVAL_TYPES.default, id: 'foo' }), @@ -882,7 +456,7 @@ describe('registerActions', () => { }); it('registers `AccountsController:getAccountByAddress`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); expect( controllerMessenger.call( @@ -897,7 +471,7 @@ describe('registerActions', () => { }); it('registers `AccountsController:getSelectedMultichainAccount`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); expect( controllerMessenger.call( @@ -912,6 +486,7 @@ describe('registerActions', () => { runSaga, { ...options, accounts: [] }, MOCK_SNAP_ID, + [], ); expect( @@ -922,7 +497,7 @@ describe('registerActions', () => { }); it('registers `AccountsController:listMultichainAccounts`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); expect( controllerMessenger.call('AccountsController:listMultichainAccounts'), @@ -934,7 +509,7 @@ describe('registerActions', () => { }); it('registers `MultichainAssetsController:getState`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); expect( controllerMessenger.call('MultichainAssetsController:getState'), @@ -948,7 +523,7 @@ describe('registerActions', () => { }); it('registers `KeyringController:withKeyring`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); expect( await controllerMessenger.call( @@ -976,7 +551,7 @@ describe('registerActions', () => { }); it('registers `RateLimitController:call`', async () => { - registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID); + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); expect( await controllerMessenger.call( @@ -998,4 +573,45 @@ describe('registerActions', () => { ), ).toBeNull(); }); + + it('registers `KeyringController:getState`', async () => { + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, []); + + expect( + controllerMessenger.call('KeyringController:getState'), + ).toStrictEqual({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + metadata: { + id: 'default', + name: 'Default Secret Recovery Phrase', + }, + }, + { + type: 'HD Key Tree', + metadata: { + id: 'alternative', + name: 'Alternative Secret Recovery Phrase', + }, + }, + ], + }); + }); + + it('registers `SnapController:getSnapFile`', async () => { + registerActions(controllerMessenger, runSaga, options, MOCK_SNAP_ID, [ + new VirtualFile({ value: stringToBytes('bar'), path: 'foo.txt' }), + ]); + + expect( + await controllerMessenger.call( + 'SnapController:getSnapFile', + MOCK_SNAP_ID, + 'foo.txt', + AuxiliaryFileEncoding.Utf8, + ), + ).toBe('bar'); + }); }); diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 5ccf68104a..f4c9465582 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -12,7 +12,6 @@ import { PermissionDoesNotExistError, type Caveat, type RequestedPermissions, - createPermissionMiddleware, } from '@metamask/permission-controller'; import type { ExecutionService } from '@metamask/snaps-controllers'; import { @@ -24,17 +23,12 @@ import { import { DIALOG_APPROVAL_TYPES } from '@metamask/snaps-rpc-methods'; import type { TrackEventParams, - AuxiliaryFileEncoding, - Component, - InterfaceState, - InterfaceContext, SnapId, - EntropySource, TraceRequest, EndTraceRequest, TraceContext, } from '@metamask/snaps-sdk'; -import type { FetchedSnapFiles, Snap } from '@metamask/snaps-utils'; +import type { Snap, VirtualFile } from '@metamask/snaps-utils'; import { logError } from '@metamask/snaps-utils'; import { assertExhaustive, hasProperty } from '@metamask/utils'; import type { CaipAssetType, Hex, Json } from '@metamask/utils'; @@ -51,10 +45,6 @@ import { getHelpers } from './helpers'; import { resolveWithSaga } from './interface'; import { asyncResolve, getEndowments } from './methods'; import { - getPermittedClearSnapStateMethodImplementation, - getPermittedGetSnapStateMethodImplementation, - getPermittedUpdateSnapStateMethodImplementation, - getGetEntropySourcesImplementation, getGetMnemonicImplementation, getGetSnapImplementation, getTrackEventImplementation, @@ -183,21 +173,6 @@ export type RestrictedMiddlewareHooks = { }; export type PermittedMiddlewareHooks = { - /** - * A hook that gets whether the requesting origin has a given permission. - * - * @param permissionName - The name of the permission to check. - * @returns Whether the origin has the permission. - */ - hasPermission: (permissionName: string) => boolean; - - /** - * A hook that returns the entropy sources available to the Snap. - * - * @returns The entropy sources available to the Snap. - */ - getEntropySources: () => EntropySource[]; - /** * A hook that returns a promise that resolves once the extension is unlocked. * @@ -206,13 +181,6 @@ export type PermittedMiddlewareHooks = { */ getUnlockPromise: (shouldShowUnlockRequest: boolean) => Promise; - /** - * A hook that returns whether the client is locked or not. - * - * @returns A boolean flag signaling whether the client is locked. - */ - getIsLocked: () => boolean; - /** * A hook that returns whether the client is active or not. * @@ -227,101 +195,6 @@ export type PermittedMiddlewareHooks = { */ getVersion: () => string; - /** - * A hook that returns the Snap's auxiliary file for the given path. This hook - * is bound to the Snap ID. - * - * @param path - The path of the auxiliary file to get. - * @param encoding - The encoding to use when returning the file. - * @returns The Snap's auxiliary file for the given path. - */ - getSnapFile: ( - path: string, - encoding: AuxiliaryFileEncoding, - ) => Promise; - - /** - * A hook that gets the state of the Snap. This hook is bound to the Snap ID. - * - * @param encrypted - Whether to get the encrypted or unencrypted state. - * @returns The current state of the Snap. - */ - getSnapState: (encrypted: boolean) => Promise>; - - /** - * A hook that updates the state of the Snap. This hook is bound to the Snap - * ID. - * - * @param newState - The new state. - * @param encrypted - Whether to update the encrypted or unencrypted state. - */ - updateSnapState: ( - newState: Record, - encrypted: boolean, - ) => Promise; - - /** - * A hook that clears the state of the Snap. This hook is bound to the Snap - * ID. - * - * @param encrypted - Whether to clear the encrypted or unencrypted state. - */ - clearSnapState: (encrypted: boolean) => Promise; - - /** - * A hook that creates an interface for the Snap. This hook is bound to the - * Snap ID. - * - * @param content - The content of the interface. - * @param context - The context of the interface. - * @returns The ID of the created interface. - */ - createInterface: (content: Component, context?: InterfaceContext) => string; - - /** - * A hook that updates an interface for the Snap. This hook is bound to the - * Snap ID. - * - * @param id - The ID of the interface to update. - * @param content - The content of the interface. - */ - updateInterface: (id: string, content: Component) => void; - - /** - * A hook that gets the state of an interface for the Snap. This hook is bound - * to the Snap ID. - * - * @param id - The ID of the interface to get. - * @returns The state of the interface. - */ - getInterfaceState: (id: string) => InterfaceState; - - /** - * A hook that gets the context of an interface for the Snap. This hook is - * bound to the Snap ID. - * - * @param id - The ID of the interface to get. - * @returns The context of the interface. - */ - getInterfaceContext: (id: string) => InterfaceContext | null; - - /** - * A hook that resolves an interface for the Snap. This hook is bound to the - * Snap ID. - * - * @param id - The ID of the interface to resolve. - * @param value - The value to resolve the interface with. - */ - resolveInterface: (id: string, value: Json) => Promise; - - /** - * A hook that gets the Snap's metadata. - * - * @param snapId - The ID of the Snap to get. - * @returns The Snap's metadata. - */ - getSnap(snapId: string): Snap; - /** * A hook that tracks an error. * @@ -350,6 +223,13 @@ export type PermittedMiddlewareHooks = { * @returns The trace data. */ endTrace(request: EndTraceRequest): void; + + /** + * A hook that returns the allowed keyring methods. + * + * @returns The keyring methods. + */ + getAllowedKeyringMethods(): string[]; }; export type MultichainMiddlewareHooks = { @@ -428,17 +308,18 @@ export async function installSnap< namespace: MOCK_ANY_NAMESPACE, }); - registerActions(controllerMessenger, runSaga, options, snapId); + registerActions( + controllerMessenger, + runSaga, + options, + snapId, + snapFiles.auxiliaryFiles, + ); // Set up controllers and JSON-RPC stack. const restrictedHooks = getRestrictedHooks(options, store, runSaga); - const permittedHooks = getPermittedHooks( - snapId, - snapFiles, - controllerMessenger, - runSaga, - ); + const permittedHooks = getPermittedHooks(runSaga); const multichainHooks = getMultichainHooks( snapId, @@ -453,25 +334,22 @@ export async function installSnap< options, }); - const permissionMiddleware = createPermissionMiddleware({ - origin: snapId, - messenger: controllerMessenger, - }); - const engine = createJsonRpcEngine({ + snapId, + messenger: controllerMessenger, store, restrictedHooks, permittedHooks, - permissionMiddleware, multichainHooks, isMultichain: false, }); const multichainEngine = createJsonRpcEngine({ + snapId, + messenger: controllerMessenger, store, restrictedHooks, permittedHooks, - permissionMiddleware, multichainHooks, isMultichain: true, }); @@ -575,65 +453,19 @@ export function getRestrictedHooks( /** * Get the permitted hooks for the simulation. * - * @param snapId - The ID of the Snap. - * @param snapFiles - The fetched Snap files. - * @param controllerMessenger - The controller messenger. * @param runSaga - The run saga function. * @returns The permitted hooks for the simulation. */ export function getPermittedHooks( - snapId: SnapId, - snapFiles: FetchedSnapFiles, - controllerMessenger: RootControllerMessenger, runSaga: RunSagaFunction, ): PermittedMiddlewareHooks { return { - hasPermission: () => true, getUnlockPromise: asyncResolve(), - getIsLocked: () => false, getIsActive: () => true, getVersion: () => '13.6.0-flask.0', - getSnapFile: async (path: string, encoding: AuxiliaryFileEncoding) => - await getSnapFile(snapFiles.auxiliaryFiles, path, encoding), + getAllowedKeyringMethods: () => [], - createInterface: (...args) => - controllerMessenger.call( - 'SnapInterfaceController:createInterface', - snapId, - ...args, - ), - updateInterface: (...args) => - controllerMessenger.call( - 'SnapInterfaceController:updateInterface', - snapId, - ...args, - ), - getInterfaceState: (...args) => - controllerMessenger.call( - 'SnapInterfaceController:getInterfaceState', - snapId, - ...args, - ), - getInterfaceContext: (...args) => - controllerMessenger.call( - 'SnapInterfaceController:getInterface', - snapId, - ...args, - ).context, - resolveInterface: async (...args) => - controllerMessenger.call( - 'SnapInterfaceController:resolveInterface', - snapId, - ...args, - ), - - getEntropySources: getGetEntropySourcesImplementation(), - getSnapState: getPermittedGetSnapStateMethodImplementation(runSaga), - updateSnapState: getPermittedUpdateSnapStateMethodImplementation(runSaga), - clearSnapState: getPermittedClearSnapStateMethodImplementation(runSaga), - - getSnap: getGetSnapImplementation(true), trackError: getTrackErrorImplementation(runSaga), trackEvent: getTrackEventImplementation(runSaga), startTrace: getStartTraceImplementation(runSaga), @@ -707,12 +539,14 @@ export function getMultichainHooks( * @param runSaga - The run saga function. * @param options - The simulation options. * @param snapId - The ID of the Snap. + * @param auxiliaryFiles - Auxiliary files from the fetched Snap. */ export function registerActions( controllerMessenger: RootControllerMessenger, runSaga: RunSagaFunction, options: SimulationOptions, snapId: SnapId, + auxiliaryFiles: VirtualFile[], ) { controllerMessenger.registerActionHandler( 'PhishingController:testOrigin', @@ -839,6 +673,12 @@ export function registerActions( getClearSnapStateMethodImplementation(runSaga), ); + controllerMessenger.registerActionHandler( + 'SnapController:getSnapFile', + async (_snapId, path, encoding) => + getSnapFile(auxiliaryFiles, path, encoding), + ); + const showNativeNotification = getShowNativeNotificationImplementation(runSaga); const showInAppNotification = getShowInAppNotificationImplementation(runSaga); @@ -873,6 +713,30 @@ export function registerActions( options.secretRecoveryPhrase, ); + controllerMessenger.registerActionHandler( + // @ts-expect-error - `KeyringController` is not part of the simulation messenger types. + 'KeyringController:getState', + () => ({ + isUnlocked: true, + keyrings: [ + { + type: 'HD Key Tree', + metadata: { + id: 'default', + name: 'Default Secret Recovery Phrase', + }, + }, + { + type: 'HD Key Tree', + metadata: { + id: 'alternative', + name: 'Alternative Secret Recovery Phrase', + }, + }, + ], + }), + ); + controllerMessenger.registerActionHandler( // @ts-expect-error - `KeyringController` is not part of the simulation messenger types. 'KeyringController:withKeyring',