diff --git a/packages/examples/packages/preinstalled/package.json b/packages/examples/packages/preinstalled/package.json index ced673ddcb..de1218e89d 100644 --- a/packages/examples/packages/preinstalled/package.json +++ b/packages/examples/packages/preinstalled/package.json @@ -46,6 +46,7 @@ "@jest/globals": "^29.5.0", "@lavamoat/allow-scripts": "^4.0.0", "@metamask/auto-changelog": "^6.1.1", + "@metamask/messenger": "^1.2.0", "@metamask/snaps-cli": "workspace:^", "@metamask/snaps-jest": "workspace:^", "@metamask/snaps-sdk": "workspace:^", diff --git a/packages/examples/packages/preinstalled/snap.manifest.json b/packages/examples/packages/preinstalled/snap.manifest.json index 3437326496..d901676452 100644 --- a/packages/examples/packages/preinstalled/snap.manifest.json +++ b/packages/examples/packages/preinstalled/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/snaps.git" }, "source": { - "shasum": "HEK+eFeY5FqLG4XSbZzP7BoXJgXptNV0lHtuxuFEym8=", + "shasum": "4Orr2LFWey4elfpvTf6tdUOBsaVCnahshTHv3v9Bf7I=", "location": { "npm": { "filePath": "dist/bundle.js", @@ -25,7 +25,10 @@ }, "snap_dialog": {}, "endowment:page-settings": {}, - "snap_manageState": {} + "snap_manageState": {}, + "endowment:messenger": { + "actions": ["PhishingController:testOrigin"] + } }, "platformVersion": "11.1.1", "manifestVersion": "0.1" diff --git a/packages/examples/packages/preinstalled/src/index.test.tsx b/packages/examples/packages/preinstalled/src/index.test.tsx index f15dca009a..e6426db3ef 100644 --- a/packages/examples/packages/preinstalled/src/index.test.tsx +++ b/packages/examples/packages/preinstalled/src/index.test.tsx @@ -150,6 +150,18 @@ describe('onRpcRequest', () => { }); }); }); + + describe('messengerCall', () => { + it('calls the PhishingController', async () => { + const { request } = await installSnap(); + + const response = await request({ + method: 'messengerCall', + }); + + expect(response).toRespondWith(false); + }); + }); }); describe('onSettingsPage', () => { diff --git a/packages/examples/packages/preinstalled/src/index.tsx b/packages/examples/packages/preinstalled/src/index.tsx index 51dc440b8a..dcff9876ab 100644 --- a/packages/examples/packages/preinstalled/src/index.tsx +++ b/packages/examples/packages/preinstalled/src/index.tsx @@ -1,5 +1,7 @@ +import type { Messenger } from '@metamask/messenger'; import { getJsonError, + getMessenger, MethodNotFoundError, UserInputEventType, } from '@metamask/snaps-sdk'; @@ -17,6 +19,13 @@ type SnapState = { setting3?: 'option1' | 'option2'; }; +type PhishingControllerTestOrigin = { + type: 'PhishingController:testOrigin'; + handler: (origin: string) => { result: boolean; type: string }; +}; + +type SnapMessenger = Messenger; + /** * Handle incoming JSON-RPC requests from the dapp, sent through the * `wallet_invokeSnap` method. This handler handles the following methods: @@ -97,6 +106,15 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }); } + case 'messengerCall': { + const messenger = getMessenger(); + const { result } = await messenger.call( + 'PhishingController:testOrigin', + 'https://metamask.io', + ); + return result; + } + default: throw new MethodNotFoundError({ method: request.method }); } diff --git a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json index 56b910bd38..e1c41fe5ab 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/iframe/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true diff --git a/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json index 261dab6ff3..12ca2abd10 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-process/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true diff --git a/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json index 261dab6ff3..12ca2abd10 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/node-thread/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true diff --git a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json index 56b910bd38..e1c41fe5ab 100644 --- a/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json +++ b/packages/snaps-execution-environments/lavamoat/webpack/webview/policy.json @@ -2,7 +2,7 @@ "resources": { "@metamask/json-rpc-engine": { "packages": { - "@metamask/snaps-utils>@metamask/messenger": true, + "@metamask/snaps-sdk>@metamask/messenger": true, "@metamask/rpc-errors": true, "@metamask/providers>@metamask/safe-event-emitter": true, "@metamask/utils": true diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index 8f39566dfd..aa77775adc 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.28, - functions: 98.88, - lines: 99.19, - statements: 98.91, + branches: 97.38, + functions: 98.92, + lines: 99.22, + statements: 98.95, }, }, }); diff --git a/packages/snaps-rpc-methods/src/endowments/enum.ts b/packages/snaps-rpc-methods/src/endowments/enum.ts index fa8c695d6e..37bf1d669f 100644 --- a/packages/snaps-rpc-methods/src/endowments/enum.ts +++ b/packages/snaps-rpc-methods/src/endowments/enum.ts @@ -14,4 +14,5 @@ export enum SnapEndowments { SettingsPage = 'endowment:page-settings', Assets = 'endowment:assets', Protocol = 'endowment:protocol', + Messenger = 'endowment:messenger', } diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index ec162587cb..79fade8612 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -21,6 +21,11 @@ import { keyringEndowmentBuilder, } from './keyring'; import { lifecycleHooksEndowmentBuilder } from './lifecycle-hooks'; +import { + getMessengerCaveatMapper, + messengerCaveatSpecifications, + messengerEndowmentBuilder, +} from './messenger'; import { multichainProviderEndowmentBuilder } from './multichain-provider'; import { getNameLookupCaveatMapper, @@ -71,6 +76,7 @@ export const endowmentPermissionBuilders = { [signatureInsightEndowmentBuilder.targetName]: signatureInsightEndowmentBuilder, [assetsEndowmentBuilder.targetName]: assetsEndowmentBuilder, + [messengerEndowmentBuilder.targetName]: messengerEndowmentBuilder, } as const; export const endowmentCaveatSpecifications = { @@ -82,6 +88,7 @@ export const endowmentCaveatSpecifications = { ...signatureInsightCaveatSpecifications, ...maxRequestTimeCaveatSpecifications, ...protocolCaveatSpecifications, + ...messengerCaveatSpecifications, }; export const endowmentCaveatMappers: Record< @@ -114,6 +121,7 @@ export const endowmentCaveatMappers: Record< [assetsEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getAssetsCaveatMapper, ), + [messengerEndowmentBuilder.targetName]: getMessengerCaveatMapper, }; // We allow null because a permitted handler does not have an endowment diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.test.ts b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts new file mode 100644 index 0000000000..cd290890ec --- /dev/null +++ b/packages/snaps-rpc-methods/src/endowments/messenger.test.ts @@ -0,0 +1,149 @@ +import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import { SnapCaveatType } from '@metamask/snaps-utils'; + +import { SnapEndowments } from './enum'; +import { + getMessengerCaveatMapper, + messengerCaveatSpecifications, + messengerEndowmentBuilder, + getMessengerCaveatActions, +} from './messenger'; + +describe('endowment:messenger', () => { + it('builds the expected permission specification', () => { + const specification = messengerEndowmentBuilder.specificationBuilder({}); + expect(specification).toStrictEqual({ + permissionType: PermissionType.Endowment, + targetName: SnapEndowments.Messenger, + endowmentGetter: expect.any(Function), + allowedCaveats: [SnapCaveatType.MessengerScopes], + subjectTypes: [SubjectType.Snap], + validator: expect.any(Function), + }); + + expect(specification.endowmentGetter()).toBeNull(); + }); + + describe('validator', () => { + it('throws if the caveat is not a single "messengerScopes"', () => { + const specification = messengerEndowmentBuilder.specificationBuilder({}); + + expect(() => + specification.validator({ + // @ts-expect-error Missing other required permission types. + caveats: undefined, + }), + ).toThrow('Expected the following caveats: "messengerScopes".'); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [{ type: 'foo', value: 'bar' }], + }), + ).toThrow( + 'Expected the following caveats: "messengerScopes", received "foo".', + ); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [ + { type: SnapCaveatType.MessengerScopes, value: 'bar' }, + { type: SnapCaveatType.MessengerScopes, value: 'bar' }, + ], + }), + ).toThrow('Duplicate caveats are not allowed.'); + }); + }); +}); + +describe('getMessengerCaveatMapper', () => { + it('maps a value to a caveat', () => { + expect( + getMessengerCaveatMapper({ + actions: ['Foo:bar'], + }), + ).toStrictEqual({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { + actions: ['Foo:bar'], + }, + }, + ], + }); + }); + + it('returns null if value is empty', () => { + expect(getMessengerCaveatMapper({})).toStrictEqual({ caveats: null }); + }); +}); + +describe('getMessengerCaveatActions', () => { + it('returns the actions from the caveat', () => { + expect( + // @ts-expect-error Missing other required permission types. + getMessengerCaveatActions({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { actions: ['Foo:bar'] }, + }, + ], + }), + ).toStrictEqual(['Foo:bar']); + }); + + it('returns null if no caveat found', () => { + expect( + // @ts-expect-error Missing other required permission types. + getMessengerCaveatActions({ + caveats: null, + }), + ).toBeNull(); + }); +}); + +describe('messengerCaveatSpecifications', () => { + describe('validator', () => { + it('throws if the caveat values are invalid', () => { + expect(() => + messengerCaveatSpecifications[ + SnapCaveatType.MessengerScopes + ].validator?.( + // @ts-expect-error Missing value type. + { + type: SnapCaveatType.MessengerScopes, + }, + ), + ).toThrow('Expected an object.'); + + expect(() => + messengerCaveatSpecifications[ + SnapCaveatType.MessengerScopes + ].validator?.({ + type: SnapCaveatType.MessengerScopes, + value: { + foo: 'bar', + }, + }), + ).toThrow( + 'Invalid messenger scopes specified: At path: foo -- Expected a value of type `never`, but received: `"bar"`', + ); + + expect(() => + messengerCaveatSpecifications[ + SnapCaveatType.MessengerScopes + ].validator?.({ + type: SnapCaveatType.MessengerScopes, + value: { + actions: [], + }, + }), + ).toThrow( + 'Invalid messenger scopes specified: At path: actions -- Expected a array with a length between `1` and `Infinity` but received one with a length of `0`.', + ); + }); + }); +}); diff --git a/packages/snaps-rpc-methods/src/endowments/messenger.ts b/packages/snaps-rpc-methods/src/endowments/messenger.ts new file mode 100644 index 0000000000..b68ae8e551 --- /dev/null +++ b/packages/snaps-rpc-methods/src/endowments/messenger.ts @@ -0,0 +1,136 @@ +import type { + Caveat, + CaveatSpecificationConstraint, + EndowmentGetterParams, + PermissionConstraint, + PermissionSpecificationBuilder, + PermissionValidatorConstraint, + ValidPermissionSpecification, +} from '@metamask/permission-controller'; +import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { MessengerActionsStruct } from '@metamask/snaps-utils'; +import { MessengerScopesStruct, SnapCaveatType } from '@metamask/snaps-utils'; +import type { Infer } from '@metamask/superstruct'; +import type { Json, NonEmptyArray } from '@metamask/utils'; +import { assertStruct, hasProperty, isObject } from '@metamask/utils'; + +import { createGenericPermissionValidator } from './caveats'; +import { SnapEndowments } from './enum'; + +const permissionName = SnapEndowments.Messenger; + +type MessengerEndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetName: typeof permissionName; + endowmentGetter: (_options?: EndowmentGetterParams) => null; + allowedCaveats: Readonly> | null; + validator: PermissionValidatorConstraint; + subjectTypes: readonly SubjectType[]; +}>; + +/** + * `endowment:messenger` returns nothing; it is intended to be used as a + * flag by the Snaps Platform to detect whether a Snap has the capability to + * use the messenger API. + * + * @param _builderOptions - Optional specification builder options. + * @returns The specification for the messenger endowment. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + any, + MessengerEndowmentSpecification +> = (_builderOptions?: unknown) => { + return { + permissionType: PermissionType.Endowment, + targetName: permissionName, + allowedCaveats: [SnapCaveatType.MessengerScopes], + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, + validator: createGenericPermissionValidator([ + { type: SnapCaveatType.MessengerScopes }, + ]), + subjectTypes: [SubjectType.Snap], + }; +}; + +export const messengerEndowmentBuilder = Object.freeze({ + targetName: permissionName, + specificationBuilder, +} as const); + +/** + * Map a raw value from the `initialPermissions` to a caveat specification. + * Note that this function does not do any validation, that's handled by the + * PermissionsController when the permission is requested. + * + * @param value - The raw value from the `initialPermissions`. + * @returns The caveat specification. + */ +export function getMessengerCaveatMapper( + value: Json, +): Pick { + if (!value || !isObject(value) || Object.keys(value).length === 0) { + return { caveats: null }; + } + + return { + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value, + }, + ], + }; +} + +export type MessengerActions = Infer; + +/** + * Getter function to get the {@link MessengerActions} caveat value from a + * permission. + * + * @param permission - The permission to get the caveat value from. + * @returns The caveat value. + */ +export function getMessengerCaveatActions( + permission?: PermissionConstraint, +): MessengerActions | null { + const caveat = permission?.caveats?.find( + (permCaveat) => permCaveat.type === SnapCaveatType.MessengerScopes, + ) as Caveat | undefined; + + return caveat?.value.actions ? caveat.value.actions : null; +} + +/** + * Validates the type of the caveat value. + * + * @param caveat - The caveat to validate. + * @throws If the caveat value is invalid. + */ +function validateCaveat(caveat: Caveat): void { + if (!hasProperty(caveat, 'value') || !isObject(caveat)) { + throw rpcErrors.invalidParams({ + message: 'Expected an object.', + }); + } + + const { value } = caveat; + assertStruct( + value, + MessengerScopesStruct, + 'Invalid messenger scopes specified', + rpcErrors.invalidParams, + ); +} + +export const messengerCaveatSpecifications: Record< + SnapCaveatType.MessengerScopes, + CaveatSpecificationConstraint +> = { + [SnapCaveatType.MessengerScopes]: Object.freeze({ + type: SnapCaveatType.MessengerScopes, + validator: (caveat: Caveat) => validateCaveat(caveat), + }), +}; diff --git a/packages/snaps-rpc-methods/src/permissions.test.ts b/packages/snaps-rpc-methods/src/permissions.test.ts index 068f6b8743..49c975f646 100644 --- a/packages/snaps-rpc-methods/src/permissions.test.ts +++ b/packages/snaps-rpc-methods/src/permissions.test.ts @@ -71,6 +71,18 @@ describe('buildSnapEndowmentSpecifications', () => { "targetName": "endowment:lifecycle-hooks", "validator": [Function], }, + "endowment:messenger": { + "allowedCaveats": [ + "messengerScopes", + ], + "endowmentGetter": [Function], + "permissionType": "Endowment", + "subjectTypes": [ + "snap", + ], + "targetName": "endowment:messenger", + "validator": [Function], + }, "endowment:multichain-provider": { "allowedCaveats": null, "endowmentGetter": [Function], diff --git a/packages/snaps-rpc-methods/src/permitted/index.ts b/packages/snaps-rpc-methods/src/permitted/index.ts index 2d864fe9e1..e0cff45393 100644 --- a/packages/snaps-rpc-methods/src/permitted/index.ts +++ b/packages/snaps-rpc-methods/src/permitted/index.ts @@ -24,6 +24,10 @@ import type { ListEntropySourcesMethodActions, ListEntropySourcesMethodHooks, } from './listEntropySources'; +import type { + MessengerCallMethodActions, + MessengerCallMethodHooks, +} from './messengerCall'; import type { OpenWebSocketMethodActions } from './openWebSocket'; import type { RequestSnapsMethodActions } from './requestSnaps'; import type { ResolveInterfaceMethodActions } from './resolveInterface'; @@ -71,7 +75,8 @@ export type PermittedRpcMethodActions = | StartTraceMethodActions | TrackErrorMethodActions | TrackEventMethodActions - | UpdateInterfaceMethodActions; + | UpdateInterfaceMethodActions + | MessengerCallMethodActions; export type PermittedRpcMethodHooks = GetClientStatusMethodHooks & GetStateMethodHooks & @@ -81,6 +86,7 @@ export type PermittedRpcMethodHooks = GetClientStatusMethodHooks & TrackErrorMethodHooks & StartTraceMethodHooks & EndTraceMethodHooks & - InvokeKeyringMethodHooks; + InvokeKeyringMethodHooks & + MessengerCallMethodHooks; export * from './middleware'; diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts new file mode 100644 index 0000000000..a97f01cb44 --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.test.ts @@ -0,0 +1,350 @@ +import { + JsonRpcEngine, + createOriginMiddleware, +} from '@metamask/json-rpc-engine'; +import type { MockAnyNamespace } from '@metamask/messenger'; +import { MOCK_ANY_NAMESPACE, Messenger } from '@metamask/messenger'; +import type { MessengerCallParams } from '@metamask/snaps-sdk'; +import { SnapCaveatType } from '@metamask/snaps-utils'; +import { + MOCK_SNAP_ID, + MockControllerMessenger, + getSnapObject, +} from '@metamask/snaps-utils/test-utils'; + +import type { MessengerCallMethodActions } from './messengerCall'; +import { messengerCallHandler } from './messengerCall'; +import { SnapEndowments } from '../endowments'; +import type { JsonRpcRequestWithOrigin } from '../types'; + +type FooBarAction = { + type: 'Foo:bar'; + handler: (parameter: string) => string; +}; + +describe('snap_messengerCall', () => { + describe('messengerCallHandler', () => { + it('has the expected shape', () => { + expect(messengerCallHandler).toMatchObject({ + implementation: expect.any(Function), + hookNames: { + getMessenger: true, + }, + actionNames: [ + 'SnapController:getSnap', + 'PermissionController:getPermission', + ], + }); + }); + }); + + describe('implementation', () => { + const getMessenger = (preinstalled = true) => { + const messenger = new MockControllerMessenger< + MessengerCallMethodActions, + never + >(); + + messenger.registerActionHandler('SnapController:getSnap', () => ({ + ...getSnapObject(), + preinstalled, + })); + + messenger.registerActionHandler( + 'PermissionController:getPermission', + () => ({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { actions: ['Foo:bar'] }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: MOCK_SNAP_ID, + parentCapability: SnapEndowments.Messenger, + }), + ); + + jest.spyOn(messenger, 'call'); + + return messenger; + }; + + it('calls an action', async () => { + const { implementation } = messengerCallHandler; + + const snapMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); + + snapMessenger.registerActionHandler('Foo:bar', (arg) => arg); + + const getSnapMessenger = jest.fn().mockReturnValue(snapMessenger); + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + action: 'Foo:bar', + params: ['baz'], + }, + }); + + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + result: 'baz', + }); + expect(getSnapMessenger).toHaveBeenCalledWith({ actions: ['Foo:bar'] }); + }); + + it('throws an error if the Snap is not preinstalled', async () => { + const { implementation } = messengerCallHandler; + + const getSnapMessenger = jest.fn(); + const hooks = { getMessenger: getSnapMessenger }; + + 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 JsonRpcRequestWithOrigin, + response, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + action: 'Foo:bar', + params: [], + }, + }); + + expect(getSnapMessenger).not.toHaveBeenCalled(); + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32601, + message: 'The method does not exist / is not available.', + stack: expect.any(String), + }, + }); + }); + + it('throws an error if the Snap does not have the messenger endowment', async () => { + const { implementation } = messengerCallHandler; + + const getSnapMessenger = jest.fn(); + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(true); + + messenger.registerActionHandler( + 'PermissionController:getPermission', + () => undefined, + ); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + action: 'Foo:bar', + params: [], + }, + }); + + expect(getSnapMessenger).not.toHaveBeenCalled(); + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32601, + message: 'The method does not exist / is not available.', + stack: expect.any(String), + }, + }); + }); + + it('throws an error if the Snap calls a disallowed action', async () => { + const { implementation } = messengerCallHandler; + + const getSnapMessenger = jest.fn(); + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(true); + + messenger.registerActionHandler( + 'PermissionController:getPermission', + () => ({ + caveats: [ + { + type: SnapCaveatType.MessengerScopes, + value: { actions: ['PermissionController:getPermission'] }, + }, + ], + date: 1661166080905, + id: 'VyAsBJiDDKawv_XlNcm13', + invoker: MOCK_SNAP_ID, + parentCapability: SnapEndowments.Messenger, + }), + ); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params: { + action: 'PermissionController:getPermission', + params: [], + }, + }); + + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32602, + message: expect.stringContaining( + 'Access to the following messenger clients are disallowed in Snaps:', + ), + stack: expect.any(String), + }, + }); + }); + + it.each([ + [ + { foo: 'bar' }, + 'Invalid params: At path: action -- Expected a value of type `MessengerAction`, but received: `undefined`.', + ], + [ + { action: 1 }, + 'Invalid params: At path: action -- Expected a value of type `MessengerAction`, but received: `1`.', + ], + [ + { action: 'Foo:bar' }, + 'Invalid params: At path: params -- Expected an array value, but received: undefined.', + ], + ])( + 'throws an error if the parameters are invalid', + async (params, error) => { + const { implementation } = messengerCallHandler; + + const snapMessenger = new Messenger({ + namespace: MOCK_ANY_NAMESPACE, + }); + + snapMessenger.registerActionHandler('Foo:bar', (arg) => arg); + + const getSnapMessenger = jest.fn().mockReturnValue(snapMessenger); + + const callSpy = jest.spyOn(snapMessenger, 'call'); + + const hooks = { getMessenger: getSnapMessenger }; + + const messenger = getMessenger(); + + const engine = new JsonRpcEngine(); + + engine.push(createOriginMiddleware(MOCK_SNAP_ID)); + engine.push((request, response, next, end) => { + const result = implementation( + request as JsonRpcRequestWithOrigin, + response, + next, + end, + hooks, + messenger, + ); + + result?.catch(end); + }); + + const response = await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'snap_messengerCall', + params, + }); + + expect(callSpy).not.toHaveBeenCalled(); + expect(response).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + error: { + code: -32602, + message: error, + stack: expect.any(String), + }, + }); + }, + ); + }); +}); diff --git a/packages/snaps-rpc-methods/src/permitted/messengerCall.ts b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts new file mode 100644 index 0000000000..aeef727fa8 --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/messengerCall.ts @@ -0,0 +1,168 @@ +import type { + JsonRpcEngineEndCallback, + MethodHandler, +} from '@metamask/json-rpc-engine'; +import type { Messenger, NamespacedName } from '@metamask/messenger'; +import type { PermissionControllerGetPermissionAction } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { + MessengerCallParams, + MessengerCallResult, +} from '@metamask/snaps-sdk'; +import type { InferMatching } from '@metamask/snaps-utils'; +import { create, object, StructError, array } from '@metamask/superstruct'; +import type { Json, PendingJsonRpcResponse } from '@metamask/utils'; +import { definePattern, JsonStruct } from '@metamask/utils'; + +import { SnapEndowments } from '../endowments'; +import { getMessengerCaveatActions } from '../endowments/messenger'; +import type { + JsonRpcRequestWithOrigin, + SnapControllerGetSnapAction, +} from '../types'; +import type { MethodHooksObject } from '../utils'; + +const hookNames: MethodHooksObject = { + getMessenger: true, +}; + +type GenericMessengerAction = { + type: NamespacedName; + handler: (...args: Json[]) => Promise; +}; + +export type MessengerCallMethodHooks = { + getMessenger: (args: { + actions?: string[] | null; + events?: string[] | null; + }) => Messenger; +}; + +export type MessengerCallMethodActions = + | SnapControllerGetSnapAction + | PermissionControllerGetPermissionAction; + +const BLOCKED_MESSENGER_CLIENTS = [ + 'ApprovalController', + 'KeyringController', + 'PermissionController', + 'SnapController', + 'WebSocketService', + 'CronjobController', + 'SnapInsightsController', + 'SnapInterfaceController', + 'MultichainRoutingService', + 'ExecutionService', + 'SnapRegistryController', +]; + +const MessengerCallParametersStruct = object({ + action: definePattern<`${string}:${string}`>('MessengerAction', /^.+:.+$/u), + params: array(JsonStruct), +}); + +export type MessengerCallParameters = InferMatching< + typeof MessengerCallParametersStruct, + MessengerCallParams +>; + +/** + * Handler for the `snap_messengerCall` method. + * + * @internal + */ +export const messengerCallHandler = { + implementation: getMessengerCallImplementation, + hookNames, + actionNames: ['SnapController:getSnap', 'PermissionController:getPermission'], +} satisfies MethodHandler< + MessengerCallMethodHooks, + MessengerCallMethodActions, + MessengerCallParameters, + MessengerCallResult, + { origin: string } +>; + +/** + * The `snap_messengerCall` method implementation. This method is used to dispatch + * an action via the messenger. It is only available to preinstalled Snaps. + * + * @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.getMessenger - The hook function to create a custom messenger for the Snap. + * @param messenger - The messenger used to call controller actions. + * @returns Nothing. + */ +async function getMessengerCallImplementation( + request: JsonRpcRequestWithOrigin, + response: PendingJsonRpcResponse, + _next: unknown, + end: JsonRpcEngineEndCallback, + { getMessenger }: MessengerCallMethodHooks, + messenger: Messenger, +): Promise { + const snap = messenger.call('SnapController:getSnap', request.origin); + const permission = messenger.call( + 'PermissionController:getPermission', + request.origin, + SnapEndowments.Messenger, + ); + + if (!snap?.preinstalled || !permission) { + return end(rpcErrors.methodNotFound()); + } + + const actions = getMessengerCaveatActions(permission); + + const snapMessenger = getMessenger({ actions }); + + const { params } = request; + + try { + const { action, params: actionParams } = getValidatedParams(params); + + if ( + BLOCKED_MESSENGER_CLIENTS.some((namespace) => + action.startsWith(namespace), + ) + ) { + return end( + rpcErrors.invalidParams({ + message: `Access to the following messenger clients are disallowed in Snaps: ${BLOCKED_MESSENGER_CLIENTS.join(', ')}.`, + }), + ); + } + + response.result = await snapMessenger.call(action, ...actionParams); + } catch (error) { + return end(error); + } + + return end(); +} + +/** + * Validate the parameters for the `snap_messengerCall` method. + * + * @param params - Parameters to validate. + * @returns Validated parameters. + * @throws Throws RPC error if validation fails. + */ +function getValidatedParams(params: unknown): MessengerCallParameters { + try { + return create(params, MessengerCallParametersStruct); + } catch (error) { + if (error instanceof StructError) { + throw rpcErrors.invalidParams({ + message: `Invalid params: ${error.message}.`, + }); + } + + /* istanbul ignore next */ + throw rpcErrors.internal(); + } +} diff --git a/packages/snaps-rpc-methods/src/permitted/middleware.test.ts b/packages/snaps-rpc-methods/src/permitted/middleware.test.ts index 83f4144450..f287c91b4b 100644 --- a/packages/snaps-rpc-methods/src/permitted/middleware.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/middleware.test.ts @@ -20,6 +20,7 @@ describe('createSnapsMethodMiddleware', () => { trackEvent: jest.fn(), startTrace: jest.fn(), endTrace: jest.fn(), + getMessenger: jest.fn(), }); const getMessenger = () => diff --git a/packages/snaps-rpc-methods/src/permitted/middleware.ts b/packages/snaps-rpc-methods/src/permitted/middleware.ts index ea7a1c9ad0..d5fd549208 100644 --- a/packages/snaps-rpc-methods/src/permitted/middleware.ts +++ b/packages/snaps-rpc-methods/src/permitted/middleware.ts @@ -25,6 +25,7 @@ import { getWebSocketsHandler } from './getWebSockets'; import { invokeKeyringHandler } from './invokeKeyring'; import { invokeSnapSugarHandler } from './invokeSnapSugar'; import { listEntropySourcesHandler } from './listEntropySources'; +import { messengerCallHandler } from './messengerCall'; import { openWebSocketHandler } from './openWebSocket'; import { requestSnapsHandler } from './requestSnaps'; import { resolveInterfaceHandler } from './resolveInterface'; @@ -65,6 +66,7 @@ const methodHandlers = { snap_getWebSockets: getWebSocketsHandler, snap_startTrace: startTraceHandler, snap_endTrace: endTraceHandler, + snap_messengerCall: messengerCallHandler, }; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/packages/snaps-sdk/package.json b/packages/snaps-sdk/package.json index 99acf0f9a6..9c8ec4f6b6 100644 --- a/packages/snaps-sdk/package.json +++ b/packages/snaps-sdk/package.json @@ -93,6 +93,7 @@ }, "dependencies": { "@metamask/key-tree": "^10.1.1", + "@metamask/messenger": "^1.2.0", "@metamask/providers": "^22.1.1", "@metamask/rpc-errors": "^7.0.3", "@metamask/superstruct": "^3.3.0", diff --git a/packages/snaps-sdk/src/index.ts b/packages/snaps-sdk/src/index.ts index 776393c235..6ec702e8e0 100644 --- a/packages/snaps-sdk/src/index.ts +++ b/packages/snaps-sdk/src/index.ts @@ -38,5 +38,6 @@ export { assert } from '@metamask/utils'; export * from './errors'; export * from './error-wrappers'; export * from './images'; +export * from './messenger'; export * from './types'; export * from './ui'; diff --git a/packages/snaps-sdk/src/messenger.test.ts b/packages/snaps-sdk/src/messenger.test.ts new file mode 100644 index 0000000000..1487b403ab --- /dev/null +++ b/packages/snaps-sdk/src/messenger.test.ts @@ -0,0 +1,34 @@ +import type { Messenger } from '@metamask/messenger'; + +import { getMessenger } from './messenger'; +import type { SnapsProvider } from './types'; + +type FooBarAction = { + type: 'Foo:bar'; + handler: (parameter: string) => string; +}; + +type FooMessenger = Messenger<'Foo', FooBarAction>; + +describe('getMessenger', () => { + it('calls `snap_messengerCall` with the action and params', async () => { + const request = jest + .fn() + .mockImplementation(async ({ params }) => params.params[0]); + + Object.assign(globalThis, { + snap: { request } as unknown as SnapsProvider, + }); + + const messenger = getMessenger(); + + const result = await messenger.call('Foo:bar', 'baz'); + + expect(result).toBe('baz'); + + expect(request).toHaveBeenCalledWith({ + method: 'snap_messengerCall', + params: { action: 'Foo:bar', params: ['baz'] }, + }); + }); +}); diff --git a/packages/snaps-sdk/src/messenger.ts b/packages/snaps-sdk/src/messenger.ts new file mode 100644 index 0000000000..fe31f7103b --- /dev/null +++ b/packages/snaps-sdk/src/messenger.ts @@ -0,0 +1,59 @@ +import type { + ActionConstraint, + EventConstraint, + ExtractActionParameters, + ExtractActionResponse, + Messenger, + MessengerActions, + NamespacedName, +} from '@metamask/messenger'; +import type { Json } from '@metamask/utils'; + +/** + * A constraint type matching any messenger. Used to constrain the generic + * messenger type accepted by {@link getMessenger} and {@link AsyncMessenger}. + */ +type MessengerConstraint = Messenger; + +/** + * An asynchronous version of a messenger's `call` method. Calls made from + * within a Snap are dispatched to the client via the `snap_messengerCall` + * method, so the original (synchronous) return type is wrapped in a `Promise`. + * + * @template MessengerType - The messenger the Snap can call actions on. + */ +export type AsyncMessenger = { + call: ['type']>( + actionType: ActionType, + ...params: ExtractActionParameters< + MessengerActions, + ActionType + > + ) => Promise< + Awaited, ActionType>> + >; +}; + +/** + * Get a messenger that can be used to call actions on the client + * from within a Snap. The returned messenger is asynchronous. + * + * Note that this is only available to preinstalled Snaps. + * + * @template MessengerType - The messenger the Snap can call actions on. This is + * a `Messenger` type from `@metamask/messenger`; the callable actions are + * derived from it automatically. + * @returns An asynchronous messenger. + */ +export function getMessenger< + MessengerType extends MessengerConstraint, +>(): AsyncMessenger { + return { + async call(action: NamespacedName, ...params: Json[]) { + return snap.request({ + method: 'snap_messengerCall', + params: { action, params }, + }); + }, + } as AsyncMessenger; +} diff --git a/packages/snaps-sdk/src/types/methods/index.ts b/packages/snaps-sdk/src/types/methods/index.ts index 8b3e97b2aa..a1b633843f 100644 --- a/packages/snaps-sdk/src/types/methods/index.ts +++ b/packages/snaps-sdk/src/types/methods/index.ts @@ -20,6 +20,7 @@ export type * from './list-entropy-sources'; export type * from './manage-accounts'; export * from './manage-state'; export type * from './methods'; +export type * from './messenger-call'; export type * from './multichain'; export * from './notify'; export type * from './request-snaps'; diff --git a/packages/snaps-sdk/src/types/methods/messenger-call.ts b/packages/snaps-sdk/src/types/methods/messenger-call.ts new file mode 100644 index 0000000000..7b54e2ff3c --- /dev/null +++ b/packages/snaps-sdk/src/types/methods/messenger-call.ts @@ -0,0 +1,17 @@ +import type { Json } from '@metamask/utils'; + +/** + * The request parameters for the `snap_messengerCall` method. This method is used + * to call an action on the messenger. + * + * Note that this method is only available to preinstalled Snaps. + */ +export type MessengerCallParams = { + action: `${string}:${string}`; + params: Json[]; +}; + +/** + * The result returned by the `snap_messengerCall` method. + */ +export type MessengerCallResult = Json; diff --git a/packages/snaps-sdk/src/types/methods/methods.ts b/packages/snaps-sdk/src/types/methods/methods.ts index 147353fdc9..8665335fc9 100644 --- a/packages/snaps-sdk/src/types/methods/methods.ts +++ b/packages/snaps-sdk/src/types/methods/methods.ts @@ -68,6 +68,10 @@ import type { ManageAccountsResult, } from './manage-accounts'; import type { ManageStateParams, ManageStateResult } from './manage-state'; +import type { + MessengerCallParams, + MessengerCallResult, +} from './messenger-call'; import type { CreateSessionParams, CreateSessionResult, @@ -127,6 +131,7 @@ export type SnapMethods = { snap_listEntropySources: [ListEntropySourcesParams, ListEntropySourcesResult]; snap_manageAccounts: [ManageAccountsParams, ManageAccountsResult]; snap_manageState: [ManageStateParams, ManageStateResult]; + snap_messengerCall: [MessengerCallParams, MessengerCallResult]; snap_notify: [NotifyParams, NotifyResult]; snap_scheduleBackgroundEvent: [ ScheduleBackgroundEventParams, diff --git a/packages/snaps-simulation/src/methods/specifications.test.ts b/packages/snaps-simulation/src/methods/specifications.test.ts index e5a7ca11f2..e3dba27b23 100644 --- a/packages/snaps-simulation/src/methods/specifications.test.ts +++ b/packages/snaps-simulation/src/methods/specifications.test.ts @@ -118,6 +118,18 @@ describe('getPermissionSpecifications', () => { "targetName": "endowment:lifecycle-hooks", "validator": [Function], }, + "endowment:messenger": { + "allowedCaveats": [ + "messengerScopes", + ], + "endowmentGetter": [Function], + "permissionType": "Endowment", + "subjectTypes": [ + "snap", + ], + "targetName": "endowment:messenger", + "validator": [Function], + }, "endowment:multichain-provider": { "allowedCaveats": null, "endowmentGetter": [Function], diff --git a/packages/snaps-simulation/src/middleware/engine.test.ts b/packages/snaps-simulation/src/middleware/engine.test.ts index 55cbe25b44..9dd13499cc 100644 --- a/packages/snaps-simulation/src/middleware/engine.test.ts +++ b/packages/snaps-simulation/src/middleware/engine.test.ts @@ -33,6 +33,7 @@ describe('createJsonRpcEngine', () => { startTrace: jest.fn(), endTrace: jest.fn(), getAllowedKeyringMethods: jest.fn(), + getMessenger: jest.fn(), }, multichainHooks: { getAccounts: jest.fn(), diff --git a/packages/snaps-simulation/src/simulation.test.ts b/packages/snaps-simulation/src/simulation.test.ts index 05342126cb..8d0496d483 100644 --- a/packages/snaps-simulation/src/simulation.test.ts +++ b/packages/snaps-simulation/src/simulation.test.ts @@ -254,29 +254,70 @@ describe('getRestrictedHooks', () => { describe('getPermittedHooks', () => { const { runSaga } = createStore(getMockOptions()); + let controllerMessenger = getRootControllerMessenger(); + + beforeEach(() => { + controllerMessenger = getRootControllerMessenger(); + }); + it('returns the `getUnlockPromise` hook', async () => { - const { getUnlockPromise } = getPermittedHooks(runSaga); + const { getUnlockPromise } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(await getUnlockPromise(true)).toBeUndefined(); }); it('returns the `getIsActive` hook', async () => { - const { getIsActive } = getPermittedHooks(runSaga); + const { getIsActive } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(getIsActive()).toBe(true); }); it('returns the `getVersion` hook', async () => { - const { getVersion } = getPermittedHooks(runSaga); + const { getVersion } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(getVersion()).toBe('13.6.0-flask.0'); }); it('returns the `getAllowedKeyringMethods` hook', async () => { - const { getAllowedKeyringMethods } = getPermittedHooks(runSaga); + const { getAllowedKeyringMethods } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); expect(getAllowedKeyringMethods()).toStrictEqual([]); }); + + it('returns the `getMessenger` hook', () => { + const { getMessenger } = getPermittedHooks( + MOCK_SNAP_ID, + controllerMessenger, + runSaga, + ); + + const messenger = getMessenger({ + actions: ['PhishingController:testOrigin'], + }); + + expect( + messenger.call('PhishingController:testOrigin', 'https://example.com'), + ).toStrictEqual({ + result: false, + type: 'all', + }); + }); }); describe('getMultichainHooks', () => { diff --git a/packages/snaps-simulation/src/simulation.ts b/packages/snaps-simulation/src/simulation.ts index 13f0df3762..b38aa0a272 100644 --- a/packages/snaps-simulation/src/simulation.ts +++ b/packages/snaps-simulation/src/simulation.ts @@ -230,6 +230,16 @@ export type PermittedMiddlewareHooks = { * @returns The keyring methods. */ getAllowedKeyringMethods(): string[]; + + /** + * A hook that returns a specialized messenger for the Snap. + * + * @returns The messenger. + */ + getMessenger(args: { + actions?: string[]; + events?: string[]; + }): Messenger; }; export type MultichainMiddlewareHooks = { @@ -319,7 +329,11 @@ export async function installSnap< // Set up controllers and JSON-RPC stack. const restrictedHooks = getRestrictedHooks(options, store, runSaga); - const permittedHooks = getPermittedHooks(runSaga); + const permittedHooks = getPermittedHooks( + snapId, + controllerMessenger, + runSaga, + ); const multichainHooks = getMultichainHooks( snapId, @@ -453,10 +467,14 @@ export function getRestrictedHooks( /** * Get the permitted hooks for the simulation. * + * @param snapId - The Snap ID. + * @param controllerMessenger - The controller messenger. * @param runSaga - The run saga function. * @returns The permitted hooks for the simulation. */ export function getPermittedHooks( + snapId: string, + controllerMessenger: RootControllerMessenger, runSaga: RunSagaFunction, ): PermittedMiddlewareHooks { return { @@ -470,6 +488,21 @@ export function getPermittedHooks( trackEvent: getTrackEventImplementation(runSaga), startTrace: getStartTraceImplementation(runSaga), endTrace: getEndTraceImplementation(runSaga), + + getMessenger: ({ actions, events }) => { + const messenger = new Messenger({ + namespace: `${snapId}-messenger`, + parent: controllerMessenger, + }); + + controllerMessenger.delegate({ + actions: actions as never[], + events: events as never[], + messenger, + }); + + return messenger; + }, }; } diff --git a/packages/snaps-utils/src/caveats.ts b/packages/snaps-utils/src/caveats.ts index b3c456f1f0..2554df2058 100644 --- a/packages/snaps-utils/src/caveats.ts +++ b/packages/snaps-utils/src/caveats.ts @@ -63,4 +63,9 @@ export enum SnapCaveatType { * Caveat specifying a list of scopes serviced by an endowment. */ ProtocolSnapScopes = 'protocolSnapScopes', + + /** + * Caveat specifying the required messenger actions and events by a Snap. + */ + MessengerScopes = 'messengerScopes', } diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 51cda496df..c1c9dc8a8c 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -192,6 +192,12 @@ export const ProtocolScopesStruct = record( object({ methods: array(string()) }), ); +export const MessengerActionsStruct = size(array(string()), 1, Infinity); + +export const MessengerScopesStruct = object({ + actions: optional(MessengerActionsStruct), +}); + // Utility type to union with for all handler structs export const HandlerCaveatsStruct = object({ maxRequestTime: optional(MaxRequestTimeStruct), @@ -229,6 +235,7 @@ export const PermissionsStruct: Describe = type({ object({ scopes: ProtocolScopesStruct }), ), ), + 'endowment:messenger': optional(MessengerScopesStruct), 'endowment:lifecycle-hooks': optional(HandlerCaveatsStruct), 'endowment:name-lookup': optional( mergeStructs( diff --git a/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx b/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx index 43b3354874..7eb5f68708 100644 --- a/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx +++ b/packages/test-snaps/src/features/snaps/preinstalled/Preinstalled.tsx @@ -51,6 +51,13 @@ export const Preinstalled: FunctionComponent = () => { }).catch(logError); }; + const handleMessengerCall = () => { + invokeSnap({ + snapId: PREINSTALLED_SNAP_ID, + method: 'messengerCall', + }).catch(logError); + }; + return ( { > End trace + diff --git a/yarn.lock b/yarn.lock index 3f0d47a2b9..56d57e9e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4177,6 +4177,7 @@ __metadata: "@jest/globals": "npm:^29.5.0" "@lavamoat/allow-scripts": "npm:^4.0.0" "@metamask/auto-changelog": "npm:^6.1.1" + "@metamask/messenger": "npm:^1.2.0" "@metamask/snaps-cli": "workspace:^" "@metamask/snaps-jest": "workspace:^" "@metamask/snaps-sdk": "workspace:^" @@ -4768,6 +4769,7 @@ __metadata: "@lavamoat/allow-scripts": "npm:^4.0.0" "@metamask/auto-changelog": "npm:^6.1.1" "@metamask/key-tree": "npm:^10.1.1" + "@metamask/messenger": "npm:^1.2.0" "@metamask/providers": "npm:^22.1.1" "@metamask/rpc-errors": "npm:^7.0.3" "@metamask/superstruct": "npm:^3.3.0"