From 2a0ca893e6fd49e81737a712247456066f0a84ec Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 29 May 2024 14:29:15 +0200 Subject: [PATCH 1/7] feat: add accounts chain API --- .../src/endowments/accounts-chain.test.ts | 143 ++++++++++++++++++ .../src/endowments/accounts-chain.ts | 133 ++++++++++++++++ .../snaps-rpc-methods/src/endowments/enum.ts | 1 + .../snaps-rpc-methods/src/endowments/index.ts | 9 ++ packages/snaps-utils/src/handler-types.ts | 1 + packages/snaps-utils/src/handlers.ts | 7 + .../snaps-utils/src/manifest/validation.ts | 6 + 7 files changed, 300 insertions(+) create mode 100644 packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts create mode 100644 packages/snaps-rpc-methods/src/endowments/accounts-chain.ts diff --git a/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts b/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts new file mode 100644 index 0000000000..73c97f81e1 --- /dev/null +++ b/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts @@ -0,0 +1,143 @@ +import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import { SnapCaveatType } from '@metamask/snaps-utils'; + +import { + getAccountsChainCaveatMapper, + getAccountsChainCaveatOrigins, + accountsChainCaveatSpecifications, + accountsChainEndowmentBuilder, +} from './accounts-chain'; +import { SnapEndowments } from './enum'; + +describe('endowment:accounts-chain', () => { + it('builds the expected permission specification', () => { + const specification = accountsChainEndowmentBuilder.specificationBuilder( + {}, + ); + expect(specification).toStrictEqual({ + permissionType: PermissionType.Endowment, + targetName: SnapEndowments.AccountsChain, + endowmentGetter: expect.any(Function), + allowedCaveats: [ + SnapCaveatType.KeyringOrigin, + SnapCaveatType.MaxRequestTime, + ], + subjectTypes: [SubjectType.Snap], + validator: expect.any(Function), + }); + }); + + describe('validator', () => { + it('throws if the caveat is not a single "keyringOrigin"', () => { + const specification = accountsChainEndowmentBuilder.specificationBuilder( + {}, + ); + + expect(() => + specification.validator({ + // @ts-expect-error Missing other required permission types. + caveats: undefined, + }), + ).toThrow('Expected the following caveats: "keyringOrigin".'); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [{ type: 'foo', value: 'bar' }], + }), + ).toThrow( + 'Expected the following caveats: "keyringOrigin", "maxRequestTime", received "foo".', + ); + + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [ + { type: 'keyringOrigin', value: { allowedOrgins: ['foo.com'] } }, + { type: 'keyringOrigin', value: { allowedOrgins: ['bar.com'] } }, + ], + }), + ).toThrow('Duplicate caveats are not allowed.'); + }); + }); +}); + +describe('getKeyringCaveatMapper', () => { + it('maps a value to a caveat', () => { + expect( + getAccountsChainCaveatMapper({ allowedOrigins: ['foo.com'] }), + ).toStrictEqual({ + caveats: [ + { + type: SnapCaveatType.KeyringOrigin, + value: { allowedOrigins: ['foo.com'] }, + }, + ], + }); + }); +}); + +describe('getAccountsChainCaveatOrigins', () => { + it('returns the origins from the caveat', () => { + expect( + // @ts-expect-error Missing other required permission types. + getAccountsChainCaveatOrigins({ + caveats: [ + { + type: SnapCaveatType.KeyringOrigin, + value: { allowedOrigins: ['foo.com'] }, + }, + ], + }), + ).toStrictEqual({ allowedOrigins: ['foo.com'] }); + }); + + it('throws if the caveat is not a single "rpcOrigin"', () => { + expect(() => + // @ts-expect-error Missing other required permission types. + getAccountsChainCaveatOrigins({ + caveats: [{ type: 'foo', value: 'bar' }], + }), + ).toThrow('Assertion failed.'); + + expect(() => + // @ts-expect-error Missing other required permission types. + getAccountsChainCaveatOrigins({ + caveats: [ + { type: 'keyringOrigin', value: { allowedOrigins: ['foo.com'] } }, + { type: 'keyringOrigin', value: { allowedOrigins: ['foo.com'] } }, + ], + }), + ).toThrow('Assertion failed.'); + }); +}); + +describe('keyringCaveatSpecifications', () => { + describe('validator', () => { + it('throws if the caveat values are invalid', () => { + expect(() => + accountsChainCaveatSpecifications[ + SnapCaveatType.KeyringOrigin + ].validator?.( + // @ts-expect-error Missing value type. + { + type: SnapCaveatType.KeyringOrigin, + }, + ), + ).toThrow('Invalid keyring origins: Expected a plain object.'); + + expect(() => + accountsChainCaveatSpecifications[ + SnapCaveatType.KeyringOrigin + ].validator?.({ + type: SnapCaveatType.KeyringOrigin, + value: { + foo: 'bar', + }, + }), + ).toThrow( + 'Invalid keyring origins: At path: foo -- Expected a value of type `never`, but received: `"bar"`.', + ); + }); + }); +}); diff --git a/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts b/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts new file mode 100644 index 0000000000..df383f32d4 --- /dev/null +++ b/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts @@ -0,0 +1,133 @@ +import type { + Caveat, + CaveatConstraint, + EndowmentGetterParams, + PermissionConstraint, + PermissionSpecificationBuilder, + PermissionValidatorConstraint, + ValidPermissionSpecification, +} from '@metamask/permission-controller'; +import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import type { KeyringOrigins } from '@metamask/snaps-utils'; +import { SnapCaveatType } from '@metamask/snaps-utils'; +import type { Json, NonEmptyArray } from '@metamask/utils'; +import { assert, isObject } from '@metamask/utils'; + +import { createGenericPermissionValidator } from './caveats'; +import { SnapEndowments } from './enum'; + +const permissionName = SnapEndowments.AccountsChain; + +type AccountsChainEndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetName: typeof permissionName; + endowmentGetter: (_options?: EndowmentGetterParams) => undefined; + allowedCaveats: Readonly> | null; + validator: PermissionValidatorConstraint; + subjectTypes: readonly SubjectType[]; +}>; + +/** + * `endowment:accounts-chain` returns nothing; it is intended to be used as a flag + * by the client to detect whether the Snap supports the Accounts Chain API. + * + * @param _builderOptions - Optional specification builder options. + * @returns The specification for the accounts chain endowment. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + any, + AccountsChainEndowmentSpecification +> = (_builderOptions?: unknown) => { + return { + permissionType: PermissionType.Endowment, + targetName: permissionName, + allowedCaveats: [ + SnapCaveatType.KeyringOrigin, + SnapCaveatType.ChainIds, + SnapCaveatType.MaxRequestTime, + ], + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => undefined, + validator: createGenericPermissionValidator([ + { type: SnapCaveatType.KeyringOrigin }, + { type: SnapCaveatType.ChainIds }, + { type: SnapCaveatType.MaxRequestTime, optional: true }, + ]), + subjectTypes: [SubjectType.Snap], + }; +}; + +export const accountsChainEndowmentBuilder = 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 getAccountsChainCaveatMapper( + value: Json, +): Pick { + if (!value || !isObject(value) || Object.keys(value).length === 0) { + return { caveats: null }; + } + + const caveats = []; + + if (value.chains) { + caveats.push({ + type: SnapCaveatType.ChainIds, + value: value.chains, + }); + } + + if (value.allowedOrigins) { + caveats.push({ + type: SnapCaveatType.KeyringOrigin, + value: { allowedOrigins: value.allowedOrigins }, + }); + } + + assert(caveats.length >= 2); + + return { caveats: caveats as NonEmptyArray }; +} + +/** + * Getter function to get the {@link KeyringOrigins} caveat value from a + * permission. + * + * @param permission - The permission to get the caveat value from. + * @returns The caveat value. + */ +export function getAccountsChainCaveatOrigins( + permission?: PermissionConstraint, +): KeyringOrigins | null { + const caveat = permission?.caveats?.find( + (permCaveat) => permCaveat.type === SnapCaveatType.KeyringOrigin, + ) as Caveat | undefined; + + return caveat ? caveat.value : null; +} + +/** + * Getter function to get the {@link ChainIds} caveat value from a + * permission. + * + * @param permission - The permission to get the caveat value from. + * @returns The caveat value. + */ +export function getAccountsChainCaveatChainIds( + permission?: PermissionConstraint, +): string[] | null { + const caveat = permission?.caveats?.find( + (permCaveat) => permCaveat.type === SnapCaveatType.ChainIds, + ) as Caveat | undefined; + + return caveat ? caveat.value : null; +} diff --git a/packages/snaps-rpc-methods/src/endowments/enum.ts b/packages/snaps-rpc-methods/src/endowments/enum.ts index f0d1577c6f..174c904361 100644 --- a/packages/snaps-rpc-methods/src/endowments/enum.ts +++ b/packages/snaps-rpc-methods/src/endowments/enum.ts @@ -10,4 +10,5 @@ export enum SnapEndowments { LifecycleHooks = 'endowment:lifecycle-hooks', Keyring = 'endowment:keyring', HomePage = 'endowment:page-home', + AccountsChain = 'endowment:accounts-chain', } diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index faa0c8fe06..3befb19b0b 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -2,6 +2,10 @@ import type { PermissionConstraint } from '@metamask/permission-controller'; import { HandlerType } from '@metamask/snaps-utils'; import type { Json } from '@metamask/utils'; +import { + getAccountsChainCaveatMapper, + accountsChainEndowmentBuilder, +} from './accounts-chain'; import { createMaxRequestTimeMapper, getMaxRequestTimeCaveatMapper, @@ -88,6 +92,9 @@ export const endowmentCaveatMappers: Record< [keyringEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getKeyringCaveatMapper, ), + [accountsChainEndowmentBuilder.targetName]: createMaxRequestTimeMapper( + getAccountsChainCaveatMapper, + ), [signatureInsightEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getSignatureInsightCaveatMapper, ), @@ -106,6 +113,8 @@ export const handlerEndowments: Record = { [HandlerType.OnKeyringRequest]: keyringEndowmentBuilder.targetName, [HandlerType.OnHomePage]: homePageEndowmentBuilder.targetName, [HandlerType.OnSignature]: signatureInsightEndowmentBuilder.targetName, + [HandlerType.OnAccountsChainRequest]: + accountsChainEndowmentBuilder.targetName, [HandlerType.OnUserInput]: null, }; diff --git a/packages/snaps-utils/src/handler-types.ts b/packages/snaps-utils/src/handler-types.ts index 642c201fec..a0c7b3faf2 100644 --- a/packages/snaps-utils/src/handler-types.ts +++ b/packages/snaps-utils/src/handler-types.ts @@ -7,6 +7,7 @@ export enum HandlerType { OnUpdate = 'onUpdate', OnNameLookup = 'onNameLookup', OnKeyringRequest = 'onKeyringRequest', + OnAccountsChainRequest = 'onAccountsChainRequest', OnHomePage = 'onHomePage', OnUserInput = 'onUserInput', } diff --git a/packages/snaps-utils/src/handlers.ts b/packages/snaps-utils/src/handlers.ts index 1353982705..e98856f88e 100644 --- a/packages/snaps-utils/src/handlers.ts +++ b/packages/snaps-utils/src/handlers.ts @@ -82,6 +82,13 @@ export const SNAP_EXPORTS = { return typeof snapExport === 'function'; }, }, + [HandlerType.OnAccountsChainRequest]: { + type: HandlerType.OnAccountsChainRequest, + required: true, + validator: (snapExport: unknown): snapExport is OnUserInputHandler => { + return typeof snapExport === 'function'; + }, + }, [HandlerType.OnHomePage]: { type: HandlerType.OnHomePage, required: true, diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 3064c2daf1..335ad943d2 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -199,6 +199,12 @@ export const PermissionsStruct: Describe = type({ 'endowment:keyring': optional( assign(HandlerCaveatsStruct, KeyringOriginsStruct), ), + 'endowment:accounts-chain': optional( + assign( + HandlerCaveatsStruct, + assign(KeyringOriginsStruct, object({ chains: ChainIdsStruct })), + ), + ), 'endowment:lifecycle-hooks': optional(HandlerCaveatsStruct), 'endowment:name-lookup': optional( assign( From c8eaea152417f771bd337eaf14b73d9640807ccd Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 29 May 2024 16:48:50 +0200 Subject: [PATCH 2/7] Add invokeAccountsSnap --- .../src/endowments/accounts-chain.test.ts | 32 +- .../src/permitted/invokeAccountsSnap.test.ts | 357 ++++++++++++++++++ .../src/permitted/invokeAccountsSnap.ts | 153 ++++++++ .../src/permitted/invokeKeyring.test.ts | 279 +------------- .../src/permitted/invokeKeyring.ts | 102 ++--- .../src/types/handlers/accounts-chain.ts | 23 ++ packages/snaps-sdk/src/types/methods/index.ts | 1 + .../src/types/methods/invoke-accounts-snap.ts | 25 ++ 8 files changed, 597 insertions(+), 375 deletions(-) create mode 100644 packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts create mode 100644 packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts create mode 100644 packages/snaps-sdk/src/types/handlers/accounts-chain.ts create mode 100644 packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts diff --git a/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts b/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts index 73c97f81e1..a998405e78 100644 --- a/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts @@ -4,7 +4,6 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import { getAccountsChainCaveatMapper, getAccountsChainCaveatOrigins, - accountsChainCaveatSpecifications, accountsChainEndowmentBuilder, } from './accounts-chain'; import { SnapEndowments } from './enum'; @@ -20,6 +19,7 @@ describe('endowment:accounts-chain', () => { endowmentGetter: expect.any(Function), allowedCaveats: [ SnapCaveatType.KeyringOrigin, + SnapCaveatType.ChainIds, SnapCaveatType.MaxRequestTime, ], subjectTypes: [SubjectType.Snap], @@ -111,33 +111,3 @@ describe('getAccountsChainCaveatOrigins', () => { ).toThrow('Assertion failed.'); }); }); - -describe('keyringCaveatSpecifications', () => { - describe('validator', () => { - it('throws if the caveat values are invalid', () => { - expect(() => - accountsChainCaveatSpecifications[ - SnapCaveatType.KeyringOrigin - ].validator?.( - // @ts-expect-error Missing value type. - { - type: SnapCaveatType.KeyringOrigin, - }, - ), - ).toThrow('Invalid keyring origins: Expected a plain object.'); - - expect(() => - accountsChainCaveatSpecifications[ - SnapCaveatType.KeyringOrigin - ].validator?.({ - type: SnapCaveatType.KeyringOrigin, - value: { - foo: 'bar', - }, - }), - ).toThrow( - 'Invalid keyring origins: At path: foo -- Expected a value of type `never`, but received: `"bar"`.', - ); - }); - }); -}); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts new file mode 100644 index 0000000000..6aef3d05dc --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts @@ -0,0 +1,357 @@ +import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { InvokeKeyringParams } from '@metamask/snaps-sdk'; +import { AccountsSnapHandlerType } 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 { invokeAccountSnapHandler } from './invokeAccountsSnap'; + +describe('wallet_invokeAccountsSnap', () => { + describe('invokeKeyringHandler', () => { + it('has the expected shape', () => { + expect(invokeAccountSnapHandler).toMatchObject({ + methodNames: ['wallet_invokeAccountsSnap'], + implementation: expect.any(Function), + hookNames: { + getSnap: true, + handleSnapRpcRequest: true, + hasPermission: true, + }, + }); + }); + }); + 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 getMockHooks = () => + ({ + getSnap: jest.fn(), + hasPermission: jest.fn(), + handleSnapRpcRequest: jest.fn(), + getAllowedKeyringMethods: jest.fn(), + } as any); + + it('invokes the snap and returns the result', async () => { + const { implementation } = invokeAccountSnapHandler; + + const hooks = getMockHooks(); + + hooks.hasPermission.mockImplementation(() => true); + hooks.getSnap.mockImplementation(() => getSnapObject()); + hooks.handleSnapRpcRequest.mockImplementation(() => 'bar'); + hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + type: AccountsSnapHandlerType.Keyring, + }, + })) as JsonRpcSuccess; + + expect(response.result).toBe('bar'); + expect(hooks.handleSnapRpcRequest).toHaveBeenCalledWith({ + handler: HandlerType.OnKeyringRequest, + request: { method: 'foo' }, + snapId: MOCK_SNAP_ID, + }); + }); + + it('fails if invoking the snap fails', async () => { + const { implementation } = invokeAccountSnapHandler; + + 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 engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + type: AccountsSnapHandlerType.Keyring, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: 'Failed to start snap.', + }) + .serialize(), + stack: expect.any(String), + }); + }); + + it('fails if origin is not authorized to call the method', async () => { + const { implementation } = invokeAccountSnapHandler; + + 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 engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + type: AccountsSnapHandlerType.Keyring, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: + 'The origin "metamask.io" is not allowed to invoke the method "foo".', + }) + .serialize(), + stack: expect.any(String), + }); + }); + + it("fails if the request doesn't have a method name", async () => { + const { implementation } = invokeAccountSnapHandler; + + 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 engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { something: 'foo' }, + type: AccountsSnapHandlerType.Keyring, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ message: 'The request must have a method.' }) + .serialize(), + stack: expect.any(String), + }); + }); + + it("fails if the origin doesn't have the permission to invoke the snap", async () => { + const { implementation } = invokeAccountSnapHandler; + + const hooks = getMockHooks(); + + hooks.hasPermission.mockImplementation(() => false); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + type: AccountsSnapHandlerType.Keyring, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: `The snap "${MOCK_SNAP_ID}" is not connected to "metamask.io". Please connect before invoking the snap.`, + }) + .serialize(), + stack: expect.any(String), + }); + }); + + it('fails if the snap is not installed', async () => { + const { implementation } = invokeAccountSnapHandler; + + const hooks = getMockHooks(); + + hooks.hasPermission.mockImplementation(() => true); + hooks.getSnap.mockImplementation(() => undefined); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + type: AccountsSnapHandlerType.Keyring, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: `The snap "${MOCK_SNAP_ID}" is not installed. Please install it first, before invoking the snap.`, + }) + .serialize(), + stack: expect.any(String), + }); + }); + + it('fails if params are invalid', async () => { + const { implementation } = invokeAccountSnapHandler; + + const hooks = getMockHooks(); + + const engine = new JsonRpcEngine(); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + request: [], + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidParams({ + message: 'Must specify a valid snap ID.', + }) + .serialize(), + stack: expect.any(String), + }); + }); + }); +}); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts new file mode 100644 index 0000000000..2fdbe0a3f3 --- /dev/null +++ b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts @@ -0,0 +1,153 @@ +import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; +import type { PermittedHandlerExport } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import type { + InvokeKeyringParams, + InvokeKeyringResult, + InvokeSnapParams, +} from '@metamask/snaps-sdk'; +import { AccountsSnapHandlerType } 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, JsonRpcRequest } from '@metamask/utils'; +import { hasProperty, type Json } from '@metamask/utils'; + +import type { MethodHooksObject } from '../utils'; +import { getValidatedParams } from './invokeSnapSugar'; + +const hookNames: MethodHooksObject = { + hasPermission: true, + handleSnapRpcRequest: true, + getSnap: true, + getAllowedKeyringMethods: true, +}; + +/** + * `wallet_invokeAccountsSnap` invokes an account Snap. + */ +export const invokeAccountSnapHandler: PermittedHandlerExport< + InvokeKeyringHooks, + InvokeSnapParams, + InvokeKeyringResult +> = { + methodNames: ['wallet_invokeAccountsSnap'], + implementation: invokeAccountSnapImplementation, + hookNames, +}; + +export type InvokeKeyringHooks = { + hasPermission: (permissionName: string) => boolean; + + handleSnapRpcRequest: ({ + snapId, + handler, + request, + }: Omit & { snapId: string }) => Promise; + + getSnap: (snapId: string) => Snap | undefined; + + getAllowedKeyringMethods: () => string[]; +}; + +const HANDLER_MAP = Object.freeze({ + [AccountsSnapHandlerType.Keyring]: HandlerType.OnKeyringRequest, + [AccountsSnapHandlerType.Chain]: HandlerType.OnAccountsChainRequest, +}); + +/** + * The `wallet_invokeAccountsSnap` method implementation. + * Invokes onKeyringRequest or onAccountsChainRequest if the snap requested is installed and connected to the dapp. + * + * @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.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. + * @returns Nothing. + */ +async function invokeAccountSnapImplementation( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, + _next: unknown, + end: JsonRpcEngineEndCallback, + { + handleSnapRpcRequest, + hasPermission, + getSnap, + getAllowedKeyringMethods, + }: InvokeKeyringHooks, +): Promise { + let params: InvokeSnapParams; + try { + params = getValidatedParams(req.params); + } catch (error) { + return end(error); + } + + // We expect the MM middleware stack to always add the origin to requests + const { origin } = req as JsonRpcRequest & { origin: string }; + const { snapId, request, type } = params; + + if (!origin || !hasPermission(WALLET_SNAP_PERMISSION_KEY)) { + return end( + rpcErrors.invalidRequest({ + message: `The snap "${snapId}" is not connected to "${origin}". Please connect before invoking the snap.`, + }), + ); + } + + const handler = HANDLER_MAP[type]; + + if (!handler) { + return end( + rpcErrors.invalidParams({ + message: `The handler type "${type}" does not exist.`, + }), + ); + } + + if (!getSnap(snapId)) { + return end( + rpcErrors.invalidRequest({ + message: `The snap "${snapId}" is not installed. Please install it first, before invoking the snap.`, + }), + ); + } + + if (!hasProperty(request, 'method') || typeof request.method !== 'string') { + return end( + rpcErrors.invalidRequest({ + message: 'The request must have a method.', + }), + ); + } + + if (type === AccountsSnapHandlerType.Keyring) { + const allowedMethods = getAllowedKeyringMethods(); + if (!allowedMethods.includes(request.method)) { + return end( + rpcErrors.invalidRequest({ + message: `The origin "${origin}" is not allowed to invoke the method "${request.method}".`, + }), + ); + } + } + + try { + res.result = (await handleSnapRpcRequest({ + snapId, + request, + handler, + })) as Json; + } catch (error) { + return end(error); + } + + return end(); +} diff --git a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts index bb4905708b..a336cbcc02 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts @@ -1,11 +1,10 @@ import { JsonRpcEngine } 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 { AccountsSnapHandlerType } from '@metamask/snaps-sdk'; +import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; import type { - JsonRpcRequest, JsonRpcFailure, + JsonRpcRequest, JsonRpcSuccess, } from '@metamask/utils'; @@ -18,28 +17,16 @@ describe('wallet_invokeKeyring', () => { methodNames: ['wallet_invokeKeyring'], implementation: expect.any(Function), hookNames: { - getSnap: true, - handleSnapRpcRequest: true, - hasPermission: true, + invokeAccountSnap: true, }, }); }); }); - 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(); - }; + describe('invokeKeyringImplementation', () => { const getMockHooks = () => ({ - getSnap: jest.fn(), - hasPermission: jest.fn(), - handleSnapRpcRequest: jest.fn(), - getAllowedKeyringMethods: jest.fn(), + invokeAccountSnap: jest.fn(), } as any); it('invokes the snap and returns the result', async () => { @@ -47,13 +34,9 @@ describe('wallet_invokeKeyring', () => { const hooks = getMockHooks(); - hooks.hasPermission.mockImplementation(() => true); - hooks.getSnap.mockImplementation(() => getSnapObject()); - hooks.handleSnapRpcRequest.mockImplementation(() => 'bar'); - hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); + hooks.invokeAccountSnap.mockImplementation(() => 'bar'); const engine = new JsonRpcEngine(); - engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( req as JsonRpcRequest, @@ -73,211 +56,24 @@ describe('wallet_invokeKeyring', () => { params: { snapId: MOCK_SNAP_ID, request: { method: 'foo' }, + type: AccountsSnapHandlerType.Keyring, }, })) as JsonRpcSuccess; expect(response.result).toBe('bar'); - expect(hooks.handleSnapRpcRequest).toHaveBeenCalledWith({ - handler: HandlerType.OnKeyringRequest, + expect(hooks.invokeAccountSnap).toHaveBeenCalledWith({ request: { method: 'foo' }, snapId: MOCK_SNAP_ID, + type: AccountsSnapHandlerType.Keyring, }); }); - it('fails if invoking the snap fails', 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(() => ['foo']); - - const engine = new JsonRpcEngine(); - engine.push(createOriginMiddleware('metamask.io')); - engine.push((req, res, next, end) => { - const result = implementation( - req as JsonRpcRequest, - res, - next, - end, - hooks, - ); - - result?.catch(end); - }); - - const response = (await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'wallet_invokeKeyring', - params: { - snapId: MOCK_SNAP_ID, - request: { method: 'foo' }, - }, - })) as JsonRpcFailure; - - expect(response.error).toStrictEqual({ - ...rpcErrors - .invalidRequest({ - message: 'Failed to start snap.', - }) - .serialize(), - stack: expect.any(String), - }); - }); - - 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 engine = new JsonRpcEngine(); - engine.push(createOriginMiddleware('metamask.io')); - engine.push((req, res, next, end) => { - const result = implementation( - req as JsonRpcRequest, - res, - next, - end, - hooks, - ); - - result?.catch(end); - }); - - const response = (await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'wallet_invokeKeyring', - params: { - snapId: MOCK_SNAP_ID, - request: { method: 'foo' }, - }, - })) as JsonRpcFailure; - - expect(response.error).toStrictEqual({ - ...rpcErrors - .invalidRequest({ - message: - 'The origin "metamask.io" is not allowed to invoke the method "foo".', - }) - .serialize(), - stack: expect.any(String), - }); - }); - - it("fails if the request doesn't have a method name", 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(() => ['foo']); - - const engine = new JsonRpcEngine(); - engine.push(createOriginMiddleware('metamask.io')); - engine.push((req, res, next, end) => { - const result = implementation( - req as JsonRpcRequest, - res, - next, - end, - hooks, - ); - - result?.catch(end); - }); - - const response = (await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'wallet_invokeKeyring', - params: { - snapId: MOCK_SNAP_ID, - request: { something: 'foo' }, - }, - })) as JsonRpcFailure; - - expect(response.error).toStrictEqual({ - ...rpcErrors - .invalidRequest({ message: 'The request must have a method.' }) - .serialize(), - stack: expect.any(String), - }); - }); - - it("fails if the origin doesn't have the permission to invoke the snap", async () => { - const { implementation } = invokeKeyringHandler; - - const hooks = getMockHooks(); - - hooks.hasPermission.mockImplementation(() => false); - - const engine = new JsonRpcEngine(); - engine.push(createOriginMiddleware('metamask.io')); - engine.push((req, res, next, end) => { - const result = implementation( - req as JsonRpcRequest, - res, - next, - end, - hooks, - ); - - result?.catch(end); - }); - - const response = (await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'wallet_invokeKeyring', - params: { - snapId: MOCK_SNAP_ID, - request: { method: 'foo' }, - }, - })) as JsonRpcFailure; - - expect(response.error).toStrictEqual({ - ...rpcErrors - .invalidRequest({ - message: `The snap "${MOCK_SNAP_ID}" is not connected to "metamask.io". Please connect before invoking the snap.`, - }) - .serialize(), - stack: expect.any(String), - }); - }); - - it('fails if the snap is not installed', async () => { + it('throws if the params is not an object', async () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); - hooks.hasPermission.mockImplementation(() => true); - hooks.getSnap.mockImplementation(() => undefined); - const engine = new JsonRpcEngine(); - engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( req as JsonRpcRequest, @@ -294,57 +90,12 @@ describe('wallet_invokeKeyring', () => { jsonrpc: '2.0', id: 1, method: 'wallet_invokeKeyring', - params: { - snapId: MOCK_SNAP_ID, - request: { method: 'foo' }, - }, + params: [], })) as JsonRpcFailure; - expect(response.error).toStrictEqual({ - ...rpcErrors - .invalidRequest({ - message: `The snap "${MOCK_SNAP_ID}" is not installed. Please install it first, before invoking the snap.`, - }) - .serialize(), - stack: expect.any(String), - }); - }); - - it('fails if params are invalid', async () => { - const { implementation } = invokeKeyringHandler; - - const hooks = getMockHooks(); - - const engine = new JsonRpcEngine(); - engine.push((req, res, next, end) => { - const result = implementation( - req as JsonRpcRequest, - res, - next, - end, - hooks, - ); - - result?.catch(end); - }); - - const response = (await engine.handle({ - jsonrpc: '2.0', - id: 1, - method: 'wallet_invokeKeyring', - params: { - request: [], - }, - })) as JsonRpcFailure; - - expect(response.error).toStrictEqual({ - ...rpcErrors - .invalidParams({ - message: 'Must specify a valid snap ID.', - }) - .serialize(), - stack: expect.any(String), - }); + expect(response.error.message).toBe( + 'Expected params to be a single object.', + ); }); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts index c81443943c..9db0a6df3a 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts @@ -1,24 +1,20 @@ import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; import type { PermittedHandlerExport } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { - InvokeKeyringParams, - InvokeKeyringResult, - InvokeSnapParams, +import { + AccountsSnapHandlerType, + type InvokeAccountsSnapParams, + type InvokeKeyringParams, + type InvokeKeyringResult, + type 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, JsonRpcRequest } from '@metamask/utils'; -import { hasProperty, type Json } from '@metamask/utils'; +import { isObject, type Json } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; -import { getValidatedParams } from './invokeSnapSugar'; const hookNames: MethodHooksObject = { - hasPermission: true, - handleSnapRpcRequest: true, - getSnap: true, - getAllowedKeyringMethods: true, + invokeAccountSnap: true, }; /** @@ -35,17 +31,7 @@ export const invokeKeyringHandler: PermittedHandlerExport< }; export type InvokeKeyringHooks = { - hasPermission: (permissionName: string) => boolean; - - handleSnapRpcRequest: ({ - snapId, - handler, - request, - }: Omit & { snapId: string }) => Promise; - - getSnap: (snapId: string) => Snap | undefined; - - getAllowedKeyringMethods: () => string[]; + invokeAccountSnap: (params: InvokeAccountsSnapParams) => Promise; }; /** @@ -58,11 +44,8 @@ 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 hooks.invokeAccountSnap - A function to invoke an account Snap designated by its parameters, + * bound to the requesting origin. * @returns Nothing. */ async function invokeKeyringImplementation( @@ -70,62 +53,21 @@ async function invokeKeyringImplementation( res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { - handleSnapRpcRequest, - hasPermission, - getSnap, - getAllowedKeyringMethods, - }: InvokeKeyringHooks, + { invokeAccountSnap }: InvokeKeyringHooks, ): Promise { - let params: InvokeSnapParams; try { - params = getValidatedParams(req.params); - } catch (error) { - return end(error); - } - - // We expect the MM middleware stack to always add the origin to requests - const { origin } = req as JsonRpcRequest & { origin: string }; - const { snapId, request } = params; + const { params } = req; - if (!origin || !hasPermission(WALLET_SNAP_PERMISSION_KEY)) { - return end( - rpcErrors.invalidRequest({ - message: `The snap "${snapId}" is not connected to "${origin}". Please connect before invoking the snap.`, - }), - ); - } - - if (!getSnap(snapId)) { - return end( - rpcErrors.invalidRequest({ - message: `The snap "${snapId}" is not installed. Please install it first, before invoking the snap.`, - }), - ); - } - - if (!hasProperty(request, 'method') || typeof request.method !== 'string') { - return end( - rpcErrors.invalidRequest({ - message: 'The request must have a method.', - }), - ); - } - - const allowedMethods = getAllowedKeyringMethods(); - if (!allowedMethods.includes(request.method)) { - return end( - rpcErrors.invalidRequest({ - message: `The origin "${origin}" is not allowed to invoke the method "${request.method}".`, - }), - ); - } + if (!isObject(params)) { + throw rpcErrors.invalidParams({ + message: 'Expected params to be a single object.', + }); + } - try { - res.result = (await handleSnapRpcRequest({ - snapId, - request, - handler: HandlerType.OnKeyringRequest, + res.result = (await invokeAccountSnap({ + snapId: params.snapId, + request: params.request, + type: AccountsSnapHandlerType.Keyring, })) as Json; } catch (error) { return end(error); diff --git a/packages/snaps-sdk/src/types/handlers/accounts-chain.ts b/packages/snaps-sdk/src/types/handlers/accounts-chain.ts new file mode 100644 index 0000000000..db4692785c --- /dev/null +++ b/packages/snaps-sdk/src/types/handlers/accounts-chain.ts @@ -0,0 +1,23 @@ +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; + +/** + * The `onAccountsChainRequest` handler, which is called when a Snap receives a + * accounts chain request. + * + * Note that using this handler requires the `endowment:accounts-chain` permission. + * + * @param args - The request arguments. + * @param args.origin - The origin of the request. This can be the ID of another + * Snap, or the URL of a website. + * @param args.request - The keyring request sent to the Snap. This includes + * the method name and parameters. + * @returns The response to the keyring request. This must be a + * JSON-serializable value. In order to return an error, throw a `SnapError` + * instead. + */ +export type OnAccountsChainRequestHandler< + Params extends JsonRpcParams = JsonRpcParams, +> = (args: { + origin: string; + request: JsonRpcRequest; +}) => Promise; diff --git a/packages/snaps-sdk/src/types/methods/index.ts b/packages/snaps-sdk/src/types/methods/index.ts index d3722bc9d6..e7c8953f55 100644 --- a/packages/snaps-sdk/src/types/methods/index.ts +++ b/packages/snaps-sdk/src/types/methods/index.ts @@ -9,6 +9,7 @@ export * from './get-file'; export * from './get-interface-state'; export * from './get-locale'; export * from './get-snaps'; +export * from './invoke-accounts-snap'; export * from './invoke-keyring'; export * from './invoke-snap'; export * from './manage-accounts'; diff --git a/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts b/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts new file mode 100644 index 0000000000..42daa4482c --- /dev/null +++ b/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts @@ -0,0 +1,25 @@ +import type { Json } from '@metamask/utils'; + +import type { InvokeSnapParams } from './invoke-snap'; + +export enum AccountsSnapHandlerType { + Keyring = 'keyring', + Chain = 'chain', +} + +/** + * The request parameters for the `wallet_invokeAccountsSnap` method. + * + * @property snapId - The ID of the snap to invoke. + * @property request - The JSON-RPC request to send to the snap. + * @property type - The type of handler to invoke. + */ +export type InvokeAccountsSnapParams = InvokeSnapParams & { + type: AccountsSnapHandlerType; +}; + +/** + * The result returned by the `wallet_invokeAccountsSnap` method, which is the result + * returned by the Snap. + */ +export type InvokeAccountsSnapResult = Json; From 7e8852804492758026c1559756c4775ec3239704 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 30 May 2024 10:53:28 +0200 Subject: [PATCH 3/7] Add test for export --- .../common/BaseSnapExecutor.test.browser.ts | 33 +++++++++++++++++++ .../src/common/commands.ts | 1 + 2 files changed, 34 insertions(+) diff --git a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts index cddf7bbfde..e69839774c 100644 --- a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts +++ b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts @@ -1449,6 +1449,39 @@ describe('BaseSnapExecutor', () => { }); }); + it('supports onAccountsChainRequest export', async () => { + const CODE = ` + module.exports.onAccountsChainRequest = ({ request }) => request.params[0]; + `; + + const executor = new TestSnapExecutor(); + await executor.executeSnap(1, MOCK_SNAP_ID, CODE, []); + + expect(await executor.readCommand()).toStrictEqual({ + jsonrpc: '2.0', + id: 1, + result: 'OK', + }); + + await executor.writeCommand({ + jsonrpc: '2.0', + id: 2, + method: 'snapRpc', + params: [ + MOCK_SNAP_ID, + HandlerType.OnAccountsChainRequest, + MOCK_ORIGIN, + { jsonrpc: '2.0', method: 'foo', params: ['bar'] }, + ], + }); + + expect(await executor.readCommand()).toStrictEqual({ + id: 2, + jsonrpc: '2.0', + result: 'bar', + }); + }); + it('supports onHomePage export', async () => { const CODE = ` module.exports.onHomePage = () => ({ content: { type: 'panel', children: [] }}); diff --git a/packages/snaps-execution-environments/src/common/commands.ts b/packages/snaps-execution-environments/src/common/commands.ts index 32d073f71f..04cb7aef69 100644 --- a/packages/snaps-execution-environments/src/common/commands.ts +++ b/packages/snaps-execution-environments/src/common/commands.ts @@ -76,6 +76,7 @@ export function getHandlerArguments( } case HandlerType.OnRpcRequest: case HandlerType.OnKeyringRequest: + case HandlerType.OnAccountsChainRequest: return { origin, request }; case HandlerType.OnCronjob: From d4baea7ab017f84848a5963c422ba8ea7a1d6b00 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 30 May 2024 11:26:10 +0200 Subject: [PATCH 4/7] Improve typing and testing --- .../src/endowments/accounts-chain.test.ts | 47 +++++--- .../src/endowments/accounts-chain.ts | 4 +- .../src/permitted/invokeAccountsSnap.test.ts | 104 ++++++++++++++++-- .../src/permitted/invokeAccountsSnap.ts | 25 ++--- .../methods/invoke-accounts-snap.test.ts | 9 ++ 5 files changed, 148 insertions(+), 41 deletions(-) create mode 100644 packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts diff --git a/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts b/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts index a998405e78..eb9df35656 100644 --- a/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts @@ -4,6 +4,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import { getAccountsChainCaveatMapper, getAccountsChainCaveatOrigins, + getAccountsChainCaveatChainIds, accountsChainEndowmentBuilder, } from './accounts-chain'; import { SnapEndowments } from './enum'; @@ -25,6 +26,8 @@ describe('endowment:accounts-chain', () => { subjectTypes: [SubjectType.Snap], validator: expect.any(Function), }); + + expect(specification.endowmentGetter()).toBeUndefined(); }); describe('validator', () => { @@ -38,7 +41,7 @@ describe('endowment:accounts-chain', () => { // @ts-expect-error Missing other required permission types. caveats: undefined, }), - ).toThrow('Expected the following caveats: "keyringOrigin".'); + ).toThrow('Expected the following caveats: "keyringOrigin", "chainIds".'); expect(() => // @ts-expect-error Missing other required permission types. @@ -46,7 +49,7 @@ describe('endowment:accounts-chain', () => { caveats: [{ type: 'foo', value: 'bar' }], }), ).toThrow( - 'Expected the following caveats: "keyringOrigin", "maxRequestTime", received "foo".', + 'Expected the following caveats: "keyringOrigin", "chainIds", "maxRequestTime", received "foo".', ); expect(() => @@ -62,12 +65,19 @@ describe('endowment:accounts-chain', () => { }); }); -describe('getKeyringCaveatMapper', () => { +describe('getAccountsChainCaveatMapper', () => { it('maps a value to a caveat', () => { expect( - getAccountsChainCaveatMapper({ allowedOrigins: ['foo.com'] }), + getAccountsChainCaveatMapper({ + allowedOrigins: ['foo.com'], + chains: ['bip122:000000000019d6689c085ae165831e93'], + }), ).toStrictEqual({ caveats: [ + { + type: SnapCaveatType.ChainIds, + value: ['bip122:000000000019d6689c085ae165831e93'], + }, { type: SnapCaveatType.KeyringOrigin, value: { allowedOrigins: ['foo.com'] }, @@ -75,6 +85,12 @@ describe('getKeyringCaveatMapper', () => { ], }); }); + + it('returns null if the input is null', () => { + expect(getAccountsChainCaveatMapper(null)).toStrictEqual({ + caveats: null, + }); + }); }); describe('getAccountsChainCaveatOrigins', () => { @@ -91,23 +107,20 @@ describe('getAccountsChainCaveatOrigins', () => { }), ).toStrictEqual({ allowedOrigins: ['foo.com'] }); }); +}); - it('throws if the caveat is not a single "rpcOrigin"', () => { - expect(() => - // @ts-expect-error Missing other required permission types. - getAccountsChainCaveatOrigins({ - caveats: [{ type: 'foo', value: 'bar' }], - }), - ).toThrow('Assertion failed.'); - - expect(() => +describe('getAccountsChainCaveatChainIds', () => { + it('returns the chain ids from the caveat', () => { + expect( // @ts-expect-error Missing other required permission types. - getAccountsChainCaveatOrigins({ + getAccountsChainCaveatChainIds({ caveats: [ - { type: 'keyringOrigin', value: { allowedOrigins: ['foo.com'] } }, - { type: 'keyringOrigin', value: { allowedOrigins: ['foo.com'] } }, + { + type: SnapCaveatType.ChainIds, + value: ['bip122:000000000019d6689c085ae165831e93'], + }, ], }), - ).toThrow('Assertion failed.'); + ).toStrictEqual(['bip122:000000000019d6689c085ae165831e93']); }); }); diff --git a/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts b/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts index df383f32d4..fbf6659f7b 100644 --- a/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts +++ b/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts @@ -11,7 +11,7 @@ import { PermissionType, SubjectType } from '@metamask/permission-controller'; import type { KeyringOrigins } from '@metamask/snaps-utils'; import { SnapCaveatType } from '@metamask/snaps-utils'; import type { Json, NonEmptyArray } from '@metamask/utils'; -import { assert, isObject } from '@metamask/utils'; +import { isObject } from '@metamask/utils'; import { createGenericPermissionValidator } from './caveats'; import { SnapEndowments } from './enum'; @@ -93,8 +93,6 @@ export function getAccountsChainCaveatMapper( }); } - assert(caveats.length >= 2); - return { caveats: caveats as NonEmptyArray }; } diff --git a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts index 6aef3d05dc..b4fe76dcab 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts @@ -1,6 +1,6 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; import { rpcErrors } from '@metamask/rpc-errors'; -import type { InvokeKeyringParams } from '@metamask/snaps-sdk'; +import type { InvokeAccountsSnapParams } from '@metamask/snaps-sdk'; import { AccountsSnapHandlerType } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; import { MOCK_SNAP_ID, getSnapObject } from '@metamask/snaps-utils/test-utils'; @@ -57,7 +57,7 @@ describe('wallet_invokeAccountsSnap', () => { engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequest, res, next, end, @@ -86,6 +86,49 @@ describe('wallet_invokeAccountsSnap', () => { }); }); + it('invokes the snap and returns the result when using the chain type', async () => { + const { implementation } = invokeAccountSnapHandler; + + const hooks = getMockHooks(); + + hooks.hasPermission.mockImplementation(() => true); + hooks.getSnap.mockImplementation(() => getSnapObject()); + hooks.handleSnapRpcRequest.mockImplementation(() => 'bar'); + hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + type: AccountsSnapHandlerType.Chain, + }, + })) as JsonRpcSuccess; + + expect(response.result).toBe('bar'); + expect(hooks.handleSnapRpcRequest).toHaveBeenCalledWith({ + handler: HandlerType.OnAccountsChainRequest, + request: { method: 'foo' }, + snapId: MOCK_SNAP_ID, + }); + }); + it('fails if invoking the snap fails', async () => { const { implementation } = invokeAccountSnapHandler; @@ -104,7 +147,7 @@ describe('wallet_invokeAccountsSnap', () => { engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequest, res, next, end, @@ -135,6 +178,51 @@ describe('wallet_invokeAccountsSnap', () => { }); }); + it('fails if the type is invalid', async () => { + const { implementation } = invokeAccountSnapHandler; + + const hooks = getMockHooks(); + + hooks.hasPermission.mockImplementation(() => true); + hooks.getSnap.mockImplementation(() => getSnapObject()); + hooks.handleSnapRpcRequest.mockImplementation(() => 'bar'); + hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeAccountsSnap', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + type: 'baz', + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidParams({ + message: 'The handler type "baz" does not exist.', + }) + .serialize(), + stack: expect.any(String), + }); + }); + it('fails if origin is not authorized to call the method', async () => { const { implementation } = invokeAccountSnapHandler; @@ -153,7 +241,7 @@ describe('wallet_invokeAccountsSnap', () => { engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequest, res, next, end, @@ -203,7 +291,7 @@ describe('wallet_invokeAccountsSnap', () => { engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequest, res, next, end, @@ -243,7 +331,7 @@ describe('wallet_invokeAccountsSnap', () => { engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequest, res, next, end, @@ -286,7 +374,7 @@ describe('wallet_invokeAccountsSnap', () => { engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequest, res, next, end, @@ -325,7 +413,7 @@ describe('wallet_invokeAccountsSnap', () => { const engine = new JsonRpcEngine(); engine.push((req, res, next, end) => { const result = implementation( - req as JsonRpcRequest, + req as JsonRpcRequest, res, next, end, diff --git a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts index 2fdbe0a3f3..a774dc5fb4 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts @@ -2,9 +2,8 @@ import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; import type { PermittedHandlerExport } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { - InvokeKeyringParams, - InvokeKeyringResult, - InvokeSnapParams, + InvokeAccountsSnapParams, + InvokeAccountsSnapResult, } from '@metamask/snaps-sdk'; import { AccountsSnapHandlerType } from '@metamask/snaps-sdk'; import type { Snap, SnapRpcHookArgs } from '@metamask/snaps-utils'; @@ -15,7 +14,7 @@ import { hasProperty, type Json } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; import { getValidatedParams } from './invokeSnapSugar'; -const hookNames: MethodHooksObject = { +const hookNames: MethodHooksObject = { hasPermission: true, handleSnapRpcRequest: true, getSnap: true, @@ -26,16 +25,16 @@ const hookNames: MethodHooksObject = { * `wallet_invokeAccountsSnap` invokes an account Snap. */ export const invokeAccountSnapHandler: PermittedHandlerExport< - InvokeKeyringHooks, - InvokeSnapParams, - InvokeKeyringResult + InvokeAccountsSnapHooks, + InvokeAccountsSnapParams, + InvokeAccountsSnapResult > = { methodNames: ['wallet_invokeAccountsSnap'], implementation: invokeAccountSnapImplementation, hookNames, }; -export type InvokeKeyringHooks = { +export type InvokeAccountsSnapHooks = { hasPermission: (permissionName: string) => boolean; handleSnapRpcRequest: ({ @@ -72,8 +71,8 @@ const HANDLER_MAP = Object.freeze({ * @returns Nothing. */ async function invokeAccountSnapImplementation( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, + req: JsonRpcRequest, + res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, { @@ -81,11 +80,11 @@ async function invokeAccountSnapImplementation( hasPermission, getSnap, getAllowedKeyringMethods, - }: InvokeKeyringHooks, + }: InvokeAccountsSnapHooks, ): Promise { - let params: InvokeSnapParams; + let params: InvokeAccountsSnapParams; try { - params = getValidatedParams(req.params); + params = getValidatedParams(req.params) as InvokeAccountsSnapParams; } catch (error) { return end(error); } diff --git a/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts b/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts new file mode 100644 index 0000000000..e134a26850 --- /dev/null +++ b/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts @@ -0,0 +1,9 @@ +import { AccountsSnapHandlerType } from './invoke-accounts-snap'; + +describe('AccountsSnapHandlerType', () => { + it('has the correct values', () => { + expect(Object.values(AccountsSnapHandlerType)).toHaveLength(2); + expect(AccountsSnapHandlerType.Keyring).toBe('keyring'); + expect(AccountsSnapHandlerType.Chain).toBe('chain'); + }); +}); From 55fee44808b8e65b7a5eeade60e6288fa95dc768 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 30 May 2024 11:40:35 +0200 Subject: [PATCH 5/7] Fix coverage --- packages/snaps-controllers/src/snaps/SnapController.test.tsx | 2 +- packages/snaps-execution-environments/coverage.json | 2 +- packages/snaps-rpc-methods/jest.config.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index 14a12f19d2..1dbbd09712 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -5014,7 +5014,7 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: {}, }), ).rejects.toThrow( - 'A snap must request at least one of the following permissions: endowment:rpc, endowment:transaction-insight, endowment:cronjob, endowment:name-lookup, endowment:lifecycle-hooks, endowment:keyring, endowment:page-home, endowment:signature-insight.', + 'A snap must request at least one of the following permissions: endowment:rpc, endowment:transaction-insight, endowment:cronjob, endowment:name-lookup, endowment:lifecycle-hooks, endowment:keyring, endowment:page-home, endowment:signature-insight, endowment:accounts-chain.', ); controller.destroy(); diff --git a/packages/snaps-execution-environments/coverage.json b/packages/snaps-execution-environments/coverage.json index a60ace0c3e..fb77c776d9 100644 --- a/packages/snaps-execution-environments/coverage.json +++ b/packages/snaps-execution-environments/coverage.json @@ -1,5 +1,5 @@ { - "branches": 80, + "branches": 80.13, "functions": 90.06, "lines": 90.77, "statements": 90.15 diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index a00a3ec4a2..3c7c4cd280 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,7 +10,7 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 91.21, + branches: 90.95, functions: 96.96, lines: 97.51, statements: 96.97, From 9526a9fcd1106100bfa503c3151eab0eb25c63d2 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 30 May 2024 12:33:18 +0200 Subject: [PATCH 6/7] Add handlers --- packages/snaps-rpc-methods/src/endowments/index.ts | 1 + packages/snaps-rpc-methods/src/permitted/handlers.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index 3befb19b0b..222078171c 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -59,6 +59,7 @@ export const endowmentPermissionBuilders = { [nameLookupEndowmentBuilder.targetName]: nameLookupEndowmentBuilder, [lifecycleHooksEndowmentBuilder.targetName]: lifecycleHooksEndowmentBuilder, [keyringEndowmentBuilder.targetName]: keyringEndowmentBuilder, + [accountsChainEndowmentBuilder.targetName]: accountsChainEndowmentBuilder, [homePageEndowmentBuilder.targetName]: homePageEndowmentBuilder, [signatureInsightEndowmentBuilder.targetName]: signatureInsightEndowmentBuilder, diff --git a/packages/snaps-rpc-methods/src/permitted/handlers.ts b/packages/snaps-rpc-methods/src/permitted/handlers.ts index b0b707364b..98bf19e656 100644 --- a/packages/snaps-rpc-methods/src/permitted/handlers.ts +++ b/packages/snaps-rpc-methods/src/permitted/handlers.ts @@ -4,6 +4,7 @@ import { getClientStatusHandler } from './getClientStatus'; import { getFileHandler } from './getFile'; import { getInterfaceStateHandler } from './getInterfaceState'; import { getSnapsHandler } from './getSnaps'; +import { invokeAccountSnapHandler } from './invokeAccountsSnap'; import { invokeKeyringHandler } from './invokeKeyring'; import { invokeSnapSugarHandler } from './invokeSnapSugar'; import { requestSnapsHandler } from './requestSnaps'; @@ -16,6 +17,7 @@ export const methodHandlers = { wallet_requestSnaps: requestSnapsHandler, wallet_invokeSnap: invokeSnapSugarHandler, wallet_invokeKeyring: invokeKeyringHandler, + wallet_invokeAccountsSnap: invokeAccountSnapHandler, snap_getClientStatus: getClientStatusHandler, snap_getFile: getFileHandler, snap_createInterface: createInterfaceHandler, From 6d3dc7abf7461b62f1edc38ad5caa26a2ab01dee Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Wed, 5 Jun 2024 14:33:27 +0200 Subject: [PATCH 7/7] Pivot pivot pivot --- .../src/snaps/SnapController.test.tsx | 2 +- .../common/BaseSnapExecutor.test.browser.ts | 6 +- .../src/common/commands.ts | 2 +- .../snaps-rpc-methods/src/endowments/enum.ts | 2 +- .../snaps-rpc-methods/src/endowments/index.ts | 19 +- ...ccounts-chain.test.ts => protocol.test.ts} | 49 +-- .../{accounts-chain.ts => protocol.ts} | 90 +++++- .../src/permitted/handlers.ts | 4 +- .../src/permitted/invokeKeyring.test.ts | 279 +++++++++++++++++- .../src/permitted/invokeKeyring.ts | 102 +++++-- ...nap.test.ts => invokeProtocolSnap.test.ts} | 28 +- ...eAccountsSnap.ts => invokeProtocolSnap.ts} | 113 +++---- .../{accounts-chain.ts => protocol.ts} | 6 +- packages/snaps-sdk/src/types/methods/index.ts | 2 +- .../methods/invoke-accounts-snap.test.ts | 9 - .../src/types/methods/invoke-accounts-snap.ts | 25 -- .../src/types/methods/invoke-protocol-snap.ts | 18 ++ packages/snaps-utils/src/caveats.ts | 5 + packages/snaps-utils/src/handler-types.ts | 2 +- .../snaps-utils/src/manifest/validation.ts | 9 +- 20 files changed, 578 insertions(+), 194 deletions(-) rename packages/snaps-rpc-methods/src/endowments/{accounts-chain.test.ts => protocol.test.ts} (73%) rename packages/snaps-rpc-methods/src/endowments/{accounts-chain.ts => protocol.ts} (60%) rename packages/snaps-rpc-methods/src/permitted/{invokeAccountsSnap.test.ts => invokeProtocolSnap.test.ts} (93%) rename packages/snaps-rpc-methods/src/permitted/{invokeAccountsSnap.ts => invokeProtocolSnap.ts} (52%) rename packages/snaps-sdk/src/types/handlers/{accounts-chain.ts => protocol.ts} (76%) delete mode 100644 packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts delete mode 100644 packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts create mode 100644 packages/snaps-sdk/src/types/methods/invoke-protocol-snap.ts diff --git a/packages/snaps-controllers/src/snaps/SnapController.test.tsx b/packages/snaps-controllers/src/snaps/SnapController.test.tsx index 1dbbd09712..14bd3fa9dc 100644 --- a/packages/snaps-controllers/src/snaps/SnapController.test.tsx +++ b/packages/snaps-controllers/src/snaps/SnapController.test.tsx @@ -5014,7 +5014,7 @@ describe('SnapController', () => { [MOCK_SNAP_ID]: {}, }), ).rejects.toThrow( - 'A snap must request at least one of the following permissions: endowment:rpc, endowment:transaction-insight, endowment:cronjob, endowment:name-lookup, endowment:lifecycle-hooks, endowment:keyring, endowment:page-home, endowment:signature-insight, endowment:accounts-chain.', + 'A snap must request at least one of the following permissions: endowment:rpc, endowment:transaction-insight, endowment:cronjob, endowment:name-lookup, endowment:lifecycle-hooks, endowment:keyring, endowment:page-home, endowment:signature-insight, endowment:protocol.', ); controller.destroy(); diff --git a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts index e69839774c..65679d57ba 100644 --- a/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts +++ b/packages/snaps-execution-environments/src/common/BaseSnapExecutor.test.browser.ts @@ -1449,9 +1449,9 @@ describe('BaseSnapExecutor', () => { }); }); - it('supports onAccountsChainRequest export', async () => { + it('supports onProtocolRequest export', async () => { const CODE = ` - module.exports.onAccountsChainRequest = ({ request }) => request.params[0]; + module.exports.onProtocolRequest = ({ request }) => request.params[0]; `; const executor = new TestSnapExecutor(); @@ -1469,7 +1469,7 @@ describe('BaseSnapExecutor', () => { method: 'snapRpc', params: [ MOCK_SNAP_ID, - HandlerType.OnAccountsChainRequest, + HandlerType.OnProtocolRequest, MOCK_ORIGIN, { jsonrpc: '2.0', method: 'foo', params: ['bar'] }, ], diff --git a/packages/snaps-execution-environments/src/common/commands.ts b/packages/snaps-execution-environments/src/common/commands.ts index 04cb7aef69..43c32ac8a9 100644 --- a/packages/snaps-execution-environments/src/common/commands.ts +++ b/packages/snaps-execution-environments/src/common/commands.ts @@ -76,7 +76,7 @@ export function getHandlerArguments( } case HandlerType.OnRpcRequest: case HandlerType.OnKeyringRequest: - case HandlerType.OnAccountsChainRequest: + case HandlerType.OnProtocolRequest: return { origin, request }; case HandlerType.OnCronjob: diff --git a/packages/snaps-rpc-methods/src/endowments/enum.ts b/packages/snaps-rpc-methods/src/endowments/enum.ts index 174c904361..87e844f4b3 100644 --- a/packages/snaps-rpc-methods/src/endowments/enum.ts +++ b/packages/snaps-rpc-methods/src/endowments/enum.ts @@ -10,5 +10,5 @@ export enum SnapEndowments { LifecycleHooks = 'endowment:lifecycle-hooks', Keyring = 'endowment:keyring', HomePage = 'endowment:page-home', - AccountsChain = 'endowment:accounts-chain', + Protocol = 'endowment:protocol', } diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index 222078171c..708af633ff 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -2,10 +2,6 @@ import type { PermissionConstraint } from '@metamask/permission-controller'; import { HandlerType } from '@metamask/snaps-utils'; import type { Json } from '@metamask/utils'; -import { - getAccountsChainCaveatMapper, - accountsChainEndowmentBuilder, -} from './accounts-chain'; import { createMaxRequestTimeMapper, getMaxRequestTimeCaveatMapper, @@ -30,6 +26,7 @@ import { nameLookupEndowmentBuilder, } from './name-lookup'; import { networkAccessEndowmentBuilder } from './network-access'; +import { getProtocolCaveatMapper, protocolEndowmentBuilder } from './protocol'; import { getRpcCaveatMapper, rpcCaveatSpecifications, @@ -59,7 +56,7 @@ export const endowmentPermissionBuilders = { [nameLookupEndowmentBuilder.targetName]: nameLookupEndowmentBuilder, [lifecycleHooksEndowmentBuilder.targetName]: lifecycleHooksEndowmentBuilder, [keyringEndowmentBuilder.targetName]: keyringEndowmentBuilder, - [accountsChainEndowmentBuilder.targetName]: accountsChainEndowmentBuilder, + [protocolEndowmentBuilder.targetName]: protocolEndowmentBuilder, [homePageEndowmentBuilder.targetName]: homePageEndowmentBuilder, [signatureInsightEndowmentBuilder.targetName]: signatureInsightEndowmentBuilder, @@ -93,8 +90,8 @@ export const endowmentCaveatMappers: Record< [keyringEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getKeyringCaveatMapper, ), - [accountsChainEndowmentBuilder.targetName]: createMaxRequestTimeMapper( - getAccountsChainCaveatMapper, + [protocolEndowmentBuilder.targetName]: createMaxRequestTimeMapper( + getProtocolCaveatMapper, ), [signatureInsightEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getSignatureInsightCaveatMapper, @@ -114,8 +111,7 @@ export const handlerEndowments: Record = { [HandlerType.OnKeyringRequest]: keyringEndowmentBuilder.targetName, [HandlerType.OnHomePage]: homePageEndowmentBuilder.targetName, [HandlerType.OnSignature]: signatureInsightEndowmentBuilder.targetName, - [HandlerType.OnAccountsChainRequest]: - accountsChainEndowmentBuilder.targetName, + [HandlerType.OnProtocolRequest]: protocolEndowmentBuilder.targetName, [HandlerType.OnUserInput]: null, }; @@ -127,3 +123,8 @@ export { getChainIdsCaveat, getLookupMatchersCaveat } from './name-lookup'; export { getKeyringCaveatOrigins } from './keyring'; export { getMaxRequestTimeCaveat } from './caveats'; export { getCronjobCaveatJobs } from './cronjob'; +export { + getProtocolCaveatChainIds, + getProtocolCaveatOrigins, + getProtocolCaveatRpcMethods, +} from './protocol'; diff --git a/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts b/packages/snaps-rpc-methods/src/endowments/protocol.test.ts similarity index 73% rename from packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts rename to packages/snaps-rpc-methods/src/endowments/protocol.test.ts index eb9df35656..8dc8893fbc 100644 --- a/packages/snaps-rpc-methods/src/endowments/accounts-chain.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/protocol.test.ts @@ -1,22 +1,20 @@ import { PermissionType, SubjectType } from '@metamask/permission-controller'; import { SnapCaveatType } from '@metamask/snaps-utils'; -import { - getAccountsChainCaveatMapper, - getAccountsChainCaveatOrigins, - getAccountsChainCaveatChainIds, - accountsChainEndowmentBuilder, -} from './accounts-chain'; import { SnapEndowments } from './enum'; +import { + getProtocolCaveatChainIds, + getProtocolCaveatMapper, + getProtocolCaveatOrigins, + protocolEndowmentBuilder, +} from './protocol'; -describe('endowment:accounts-chain', () => { +describe('endowment:protocol', () => { it('builds the expected permission specification', () => { - const specification = accountsChainEndowmentBuilder.specificationBuilder( - {}, - ); + const specification = protocolEndowmentBuilder.specificationBuilder({}); expect(specification).toStrictEqual({ permissionType: PermissionType.Endowment, - targetName: SnapEndowments.AccountsChain, + targetName: SnapEndowments.Protocol, endowmentGetter: expect.any(Function), allowedCaveats: [ SnapCaveatType.KeyringOrigin, @@ -31,10 +29,8 @@ describe('endowment:accounts-chain', () => { }); describe('validator', () => { - it('throws if the caveat is not a single "keyringOrigin"', () => { - const specification = accountsChainEndowmentBuilder.specificationBuilder( - {}, - ); + it('throws if the caveat is not a "keyringOrigin" and "chainIds"', () => { + const specification = protocolEndowmentBuilder.specificationBuilder({}); expect(() => specification.validator({ @@ -52,6 +48,15 @@ describe('endowment:accounts-chain', () => { 'Expected the following caveats: "keyringOrigin", "chainIds", "maxRequestTime", received "foo".', ); + expect(() => + // @ts-expect-error Missing other required permission types. + specification.validator({ + caveats: [ + { type: 'keyringOrigin', value: { allowedOrgins: ['foo.com'] } }, + ], + }), + ).toThrow('Expected the following caveats: "keyringOrigin", "chainIds".'); + expect(() => // @ts-expect-error Missing other required permission types. specification.validator({ @@ -65,10 +70,10 @@ describe('endowment:accounts-chain', () => { }); }); -describe('getAccountsChainCaveatMapper', () => { +describe('getProtocolCaveatMapper', () => { it('maps a value to a caveat', () => { expect( - getAccountsChainCaveatMapper({ + getProtocolCaveatMapper({ allowedOrigins: ['foo.com'], chains: ['bip122:000000000019d6689c085ae165831e93'], }), @@ -87,17 +92,17 @@ describe('getAccountsChainCaveatMapper', () => { }); it('returns null if the input is null', () => { - expect(getAccountsChainCaveatMapper(null)).toStrictEqual({ + expect(getProtocolCaveatMapper(null)).toStrictEqual({ caveats: null, }); }); }); -describe('getAccountsChainCaveatOrigins', () => { +describe('getProtocolCaveatOrigins', () => { it('returns the origins from the caveat', () => { expect( // @ts-expect-error Missing other required permission types. - getAccountsChainCaveatOrigins({ + getProtocolCaveatOrigins({ caveats: [ { type: SnapCaveatType.KeyringOrigin, @@ -109,11 +114,11 @@ describe('getAccountsChainCaveatOrigins', () => { }); }); -describe('getAccountsChainCaveatChainIds', () => { +describe('getProtocolCaveatChainIds', () => { it('returns the chain ids from the caveat', () => { expect( // @ts-expect-error Missing other required permission types. - getAccountsChainCaveatChainIds({ + getProtocolCaveatChainIds({ caveats: [ { type: SnapCaveatType.ChainIds, diff --git a/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts b/packages/snaps-rpc-methods/src/endowments/protocol.ts similarity index 60% rename from packages/snaps-rpc-methods/src/endowments/accounts-chain.ts rename to packages/snaps-rpc-methods/src/endowments/protocol.ts index fbf6659f7b..30600b14f8 100644 --- a/packages/snaps-rpc-methods/src/endowments/accounts-chain.ts +++ b/packages/snaps-rpc-methods/src/endowments/protocol.ts @@ -1,6 +1,7 @@ import type { Caveat, CaveatConstraint, + CaveatSpecificationConstraint, EndowmentGetterParams, PermissionConstraint, PermissionSpecificationBuilder, @@ -8,17 +9,26 @@ import type { ValidPermissionSpecification, } from '@metamask/permission-controller'; import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { KeyringOrigins } from '@metamask/snaps-utils'; -import { SnapCaveatType } from '@metamask/snaps-utils'; +import { + ProtocolRpcMethodsStruct, + SnapCaveatType, +} from '@metamask/snaps-utils'; import type { Json, NonEmptyArray } from '@metamask/utils'; -import { isObject } from '@metamask/utils'; +import { + assertStruct, + hasProperty, + isObject, + isPlainObject, +} from '@metamask/utils'; import { createGenericPermissionValidator } from './caveats'; import { SnapEndowments } from './enum'; -const permissionName = SnapEndowments.AccountsChain; +const permissionName = SnapEndowments.Protocol; -type AccountsChainEndowmentSpecification = ValidPermissionSpecification<{ +type ProtocolEndowmentSpecification = ValidPermissionSpecification<{ permissionType: PermissionType.Endowment; targetName: typeof permissionName; endowmentGetter: (_options?: EndowmentGetterParams) => undefined; @@ -28,8 +38,8 @@ type AccountsChainEndowmentSpecification = ValidPermissionSpecification<{ }>; /** - * `endowment:accounts-chain` returns nothing; it is intended to be used as a flag - * by the client to detect whether the Snap supports the Accounts Chain API. + * `endowment:protocol` returns nothing; it is intended to be used as a flag + * by the client to detect whether the Snap supports the Protocol API. * * @param _builderOptions - Optional specification builder options. * @returns The specification for the accounts chain endowment. @@ -37,7 +47,7 @@ type AccountsChainEndowmentSpecification = ValidPermissionSpecification<{ const specificationBuilder: PermissionSpecificationBuilder< PermissionType.Endowment, any, - AccountsChainEndowmentSpecification + ProtocolEndowmentSpecification > = (_builderOptions?: unknown) => { return { permissionType: PermissionType.Endowment, @@ -45,19 +55,21 @@ const specificationBuilder: PermissionSpecificationBuilder< allowedCaveats: [ SnapCaveatType.KeyringOrigin, SnapCaveatType.ChainIds, + SnapCaveatType.SnapRpcMethods, SnapCaveatType.MaxRequestTime, ], endowmentGetter: (_getterOptions?: EndowmentGetterParams) => undefined, validator: createGenericPermissionValidator([ { type: SnapCaveatType.KeyringOrigin }, { type: SnapCaveatType.ChainIds }, + { type: SnapCaveatType.SnapRpcMethods }, { type: SnapCaveatType.MaxRequestTime, optional: true }, ]), subjectTypes: [SubjectType.Snap], }; }; -export const accountsChainEndowmentBuilder = Object.freeze({ +export const protocolEndowmentBuilder = Object.freeze({ targetName: permissionName, specificationBuilder, } as const); @@ -70,7 +82,7 @@ export const accountsChainEndowmentBuilder = Object.freeze({ * @param value - The raw value from the `initialPermissions`. * @returns The caveat specification. */ -export function getAccountsChainCaveatMapper( +export function getProtocolCaveatMapper( value: Json, ): Pick { if (!value || !isObject(value) || Object.keys(value).length === 0) { @@ -93,6 +105,13 @@ export function getAccountsChainCaveatMapper( }); } + if (value.methods) { + caveats.push({ + type: SnapCaveatType.SnapRpcMethods, + value: value.methods, + }); + } + return { caveats: caveats as NonEmptyArray }; } @@ -103,7 +122,7 @@ export function getAccountsChainCaveatMapper( * @param permission - The permission to get the caveat value from. * @returns The caveat value. */ -export function getAccountsChainCaveatOrigins( +export function getProtocolCaveatOrigins( permission?: PermissionConstraint, ): KeyringOrigins | null { const caveat = permission?.caveats?.find( @@ -120,7 +139,7 @@ export function getAccountsChainCaveatOrigins( * @param permission - The permission to get the caveat value from. * @returns The caveat value. */ -export function getAccountsChainCaveatChainIds( +export function getProtocolCaveatChainIds( permission?: PermissionConstraint, ): string[] | null { const caveat = permission?.caveats?.find( @@ -129,3 +148,52 @@ export function getAccountsChainCaveatChainIds( return caveat ? caveat.value : null; } + +/** + * Getter function to get the {@link SnapRpcMethods} caveat value from a + * permission. + * + * @param permission - The permission to get the caveat value from. + * @returns The caveat value. + */ +export function getProtocolCaveatRpcMethods( + permission?: PermissionConstraint, +): string[] | null { + const caveat = permission?.caveats?.find( + (permCaveat) => permCaveat.type === SnapCaveatType.SnapRpcMethods, + ) as Caveat | undefined; + + return caveat ? caveat.value : 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') || !isPlainObject(caveat)) { + throw rpcErrors.invalidParams({ + message: 'Expected a plain object.', + }); + } + + const { value } = caveat; + assertStruct( + value, + ProtocolRpcMethodsStruct, + 'Invalid RPC methods specified', + rpcErrors.invalidParams, + ); +} + +export const protocolCaveatSpecifications: Record< + SnapCaveatType.SnapRpcMethods, + CaveatSpecificationConstraint +> = { + [SnapCaveatType.SnapRpcMethods]: Object.freeze({ + type: SnapCaveatType.SnapRpcMethods, + validator: (caveat: Caveat) => validateCaveat(caveat), + }), +}; diff --git a/packages/snaps-rpc-methods/src/permitted/handlers.ts b/packages/snaps-rpc-methods/src/permitted/handlers.ts index 98bf19e656..43fbee7eea 100644 --- a/packages/snaps-rpc-methods/src/permitted/handlers.ts +++ b/packages/snaps-rpc-methods/src/permitted/handlers.ts @@ -4,8 +4,8 @@ import { getClientStatusHandler } from './getClientStatus'; import { getFileHandler } from './getFile'; import { getInterfaceStateHandler } from './getInterfaceState'; import { getSnapsHandler } from './getSnaps'; -import { invokeAccountSnapHandler } from './invokeAccountsSnap'; import { invokeKeyringHandler } from './invokeKeyring'; +import { invokeProtocolSnapHandler } from './invokeProtocolSnap'; import { invokeSnapSugarHandler } from './invokeSnapSugar'; import { requestSnapsHandler } from './requestSnaps'; import { updateInterfaceHandler } from './updateInterface'; @@ -17,7 +17,7 @@ export const methodHandlers = { wallet_requestSnaps: requestSnapsHandler, wallet_invokeSnap: invokeSnapSugarHandler, wallet_invokeKeyring: invokeKeyringHandler, - wallet_invokeAccountsSnap: invokeAccountSnapHandler, + wallet_invokeProtocolSnap: invokeProtocolSnapHandler, snap_getClientStatus: getClientStatusHandler, snap_getFile: getFileHandler, snap_createInterface: createInterfaceHandler, diff --git a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts index a336cbcc02..bb4905708b 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.test.ts @@ -1,10 +1,11 @@ import { JsonRpcEngine } from '@metamask/json-rpc-engine'; +import { rpcErrors } from '@metamask/rpc-errors'; import type { InvokeKeyringParams } from '@metamask/snaps-sdk'; -import { AccountsSnapHandlerType } from '@metamask/snaps-sdk'; -import { MOCK_SNAP_ID } from '@metamask/snaps-utils/test-utils'; +import { HandlerType } from '@metamask/snaps-utils'; +import { MOCK_SNAP_ID, getSnapObject } from '@metamask/snaps-utils/test-utils'; import type { - JsonRpcFailure, JsonRpcRequest, + JsonRpcFailure, JsonRpcSuccess, } from '@metamask/utils'; @@ -17,16 +18,28 @@ describe('wallet_invokeKeyring', () => { methodNames: ['wallet_invokeKeyring'], implementation: expect.any(Function), hookNames: { - invokeAccountSnap: true, + getSnap: true, + handleSnapRpcRequest: true, + hasPermission: true, }, }); }); }); - 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 getMockHooks = () => ({ - invokeAccountSnap: jest.fn(), + getSnap: jest.fn(), + hasPermission: jest.fn(), + handleSnapRpcRequest: jest.fn(), + getAllowedKeyringMethods: jest.fn(), } as any); it('invokes the snap and returns the result', async () => { @@ -34,9 +47,13 @@ describe('wallet_invokeKeyring', () => { const hooks = getMockHooks(); - hooks.invokeAccountSnap.mockImplementation(() => 'bar'); + hooks.hasPermission.mockImplementation(() => true); + hooks.getSnap.mockImplementation(() => getSnapObject()); + hooks.handleSnapRpcRequest.mockImplementation(() => 'bar'); + hooks.getAllowedKeyringMethods.mockImplementation(() => ['foo']); const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( req as JsonRpcRequest, @@ -56,24 +73,211 @@ describe('wallet_invokeKeyring', () => { params: { snapId: MOCK_SNAP_ID, request: { method: 'foo' }, - type: AccountsSnapHandlerType.Keyring, }, })) as JsonRpcSuccess; expect(response.result).toBe('bar'); - expect(hooks.invokeAccountSnap).toHaveBeenCalledWith({ + expect(hooks.handleSnapRpcRequest).toHaveBeenCalledWith({ + handler: HandlerType.OnKeyringRequest, request: { method: 'foo' }, snapId: MOCK_SNAP_ID, - type: AccountsSnapHandlerType.Keyring, }); }); - it('throws if the params is not an object', async () => { + it('fails if invoking the snap fails', 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(() => ['foo']); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeKeyring', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: 'Failed to start snap.', + }) + .serialize(), + stack: expect.any(String), + }); + }); + + 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 engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeKeyring', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: + 'The origin "metamask.io" is not allowed to invoke the method "foo".', + }) + .serialize(), + stack: expect.any(String), + }); + }); + + it("fails if the request doesn't have a method name", 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(() => ['foo']); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeKeyring', + params: { + snapId: MOCK_SNAP_ID, + request: { something: 'foo' }, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ message: 'The request must have a method.' }) + .serialize(), + stack: expect.any(String), + }); + }); + + it("fails if the origin doesn't have the permission to invoke the snap", async () => { + const { implementation } = invokeKeyringHandler; + + const hooks = getMockHooks(); + + hooks.hasPermission.mockImplementation(() => false); + + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeKeyring', + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: `The snap "${MOCK_SNAP_ID}" is not connected to "metamask.io". Please connect before invoking the snap.`, + }) + .serialize(), + stack: expect.any(String), + }); + }); + + it('fails if the snap is not installed', async () => { const { implementation } = invokeKeyringHandler; const hooks = getMockHooks(); + hooks.hasPermission.mockImplementation(() => true); + hooks.getSnap.mockImplementation(() => undefined); + const engine = new JsonRpcEngine(); + engine.push(createOriginMiddleware('metamask.io')); engine.push((req, res, next, end) => { const result = implementation( req as JsonRpcRequest, @@ -90,12 +294,57 @@ describe('wallet_invokeKeyring', () => { jsonrpc: '2.0', id: 1, method: 'wallet_invokeKeyring', - params: [], + params: { + snapId: MOCK_SNAP_ID, + request: { method: 'foo' }, + }, })) as JsonRpcFailure; - expect(response.error.message).toBe( - 'Expected params to be a single object.', - ); + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidRequest({ + message: `The snap "${MOCK_SNAP_ID}" is not installed. Please install it first, before invoking the snap.`, + }) + .serialize(), + stack: expect.any(String), + }); + }); + + it('fails if params are invalid', async () => { + const { implementation } = invokeKeyringHandler; + + const hooks = getMockHooks(); + + const engine = new JsonRpcEngine(); + engine.push((req, res, next, end) => { + const result = implementation( + req as JsonRpcRequest, + res, + next, + end, + hooks, + ); + + result?.catch(end); + }); + + const response = (await engine.handle({ + jsonrpc: '2.0', + id: 1, + method: 'wallet_invokeKeyring', + params: { + request: [], + }, + })) as JsonRpcFailure; + + expect(response.error).toStrictEqual({ + ...rpcErrors + .invalidParams({ + message: 'Must specify a valid snap ID.', + }) + .serialize(), + stack: expect.any(String), + }); }); }); }); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts index 9db0a6df3a..c81443943c 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeKeyring.ts @@ -1,20 +1,24 @@ import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; import type { PermittedHandlerExport } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; -import { - AccountsSnapHandlerType, - type InvokeAccountsSnapParams, - type InvokeKeyringParams, - type InvokeKeyringResult, - type InvokeSnapParams, +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, JsonRpcRequest } from '@metamask/utils'; -import { isObject, type Json } from '@metamask/utils'; +import { hasProperty, type Json } from '@metamask/utils'; import type { MethodHooksObject } from '../utils'; +import { getValidatedParams } from './invokeSnapSugar'; const hookNames: MethodHooksObject = { - invokeAccountSnap: true, + hasPermission: true, + handleSnapRpcRequest: true, + getSnap: true, + getAllowedKeyringMethods: true, }; /** @@ -31,7 +35,17 @@ export const invokeKeyringHandler: PermittedHandlerExport< }; export type InvokeKeyringHooks = { - invokeAccountSnap: (params: InvokeAccountsSnapParams) => Promise; + hasPermission: (permissionName: string) => boolean; + + handleSnapRpcRequest: ({ + snapId, + handler, + request, + }: Omit & { snapId: string }) => Promise; + + getSnap: (snapId: string) => Snap | undefined; + + getAllowedKeyringMethods: () => string[]; }; /** @@ -44,8 +58,11 @@ export type InvokeKeyringHooks = { * function. * @param end - The `json-rpc-engine` "end" callback. * @param hooks - The RPC method hooks. - * @param hooks.invokeAccountSnap - A function to invoke an account Snap designated by its parameters, - * bound to the requesting origin. + * @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. * @returns Nothing. */ async function invokeKeyringImplementation( @@ -53,21 +70,62 @@ async function invokeKeyringImplementation( res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, - { invokeAccountSnap }: InvokeKeyringHooks, + { + handleSnapRpcRequest, + hasPermission, + getSnap, + getAllowedKeyringMethods, + }: InvokeKeyringHooks, ): Promise { + let params: InvokeSnapParams; try { - const { params } = req; + params = getValidatedParams(req.params); + } catch (error) { + return end(error); + } + + // We expect the MM middleware stack to always add the origin to requests + const { origin } = req as JsonRpcRequest & { origin: string }; + const { snapId, request } = params; - if (!isObject(params)) { - throw rpcErrors.invalidParams({ - message: 'Expected params to be a single object.', - }); - } + if (!origin || !hasPermission(WALLET_SNAP_PERMISSION_KEY)) { + return end( + rpcErrors.invalidRequest({ + message: `The snap "${snapId}" is not connected to "${origin}". Please connect before invoking the snap.`, + }), + ); + } + + if (!getSnap(snapId)) { + return end( + rpcErrors.invalidRequest({ + message: `The snap "${snapId}" is not installed. Please install it first, before invoking the snap.`, + }), + ); + } + + if (!hasProperty(request, 'method') || typeof request.method !== 'string') { + return end( + rpcErrors.invalidRequest({ + message: 'The request must have a method.', + }), + ); + } + + const allowedMethods = getAllowedKeyringMethods(); + if (!allowedMethods.includes(request.method)) { + return end( + rpcErrors.invalidRequest({ + message: `The origin "${origin}" is not allowed to invoke the method "${request.method}".`, + }), + ); + } - res.result = (await invokeAccountSnap({ - snapId: params.snapId, - request: params.request, - type: AccountsSnapHandlerType.Keyring, + try { + res.result = (await handleSnapRpcRequest({ + snapId, + request, + handler: HandlerType.OnKeyringRequest, })) as Json; } catch (error) { return end(error); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts b/packages/snaps-rpc-methods/src/permitted/invokeProtocolSnap.test.ts similarity index 93% rename from packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts rename to packages/snaps-rpc-methods/src/permitted/invokeProtocolSnap.test.ts index b4fe76dcab..531e4b1bcd 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.test.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeProtocolSnap.test.ts @@ -10,12 +10,12 @@ import type { JsonRpcSuccess, } from '@metamask/utils'; -import { invokeAccountSnapHandler } from './invokeAccountsSnap'; +import { invokeProtocolSnapHandler } from './invokeProtocolSnap'; -describe('wallet_invokeAccountsSnap', () => { - describe('invokeKeyringHandler', () => { +describe('wallet_invokeProtocolSnap', () => { + describe('invokeProtocolSnapHandler', () => { it('has the expected shape', () => { - expect(invokeAccountSnapHandler).toMatchObject({ + expect(invokeProtocolSnapHandler).toMatchObject({ methodNames: ['wallet_invokeAccountsSnap'], implementation: expect.any(Function), hookNames: { @@ -26,7 +26,7 @@ describe('wallet_invokeAccountsSnap', () => { }); }); }); - describe('invokeKeyringImplementation', () => { + describe('invokeProtocolSnapImplementation', () => { // Mirror the origin middleware in the extension const createOriginMiddleware = (origin: string) => @@ -44,7 +44,7 @@ describe('wallet_invokeAccountsSnap', () => { } as any); it('invokes the snap and returns the result', async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -87,7 +87,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it('invokes the snap and returns the result when using the chain type', async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -130,7 +130,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it('fails if invoking the snap fails', async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -179,7 +179,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it('fails if the type is invalid', async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -224,7 +224,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it('fails if origin is not authorized to call the method', async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -274,7 +274,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it("fails if the request doesn't have a method name", async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -321,7 +321,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it("fails if the origin doesn't have the permission to invoke the snap", async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -363,7 +363,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it('fails if the snap is not installed', async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); @@ -406,7 +406,7 @@ describe('wallet_invokeAccountsSnap', () => { }); it('fails if params are invalid', async () => { - const { implementation } = invokeAccountSnapHandler; + const { implementation } = invokeProtocolSnapHandler; const hooks = getMockHooks(); diff --git a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts b/packages/snaps-rpc-methods/src/permitted/invokeProtocolSnap.ts similarity index 52% rename from packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts rename to packages/snaps-rpc-methods/src/permitted/invokeProtocolSnap.ts index a774dc5fb4..c20a458a4d 100644 --- a/packages/snaps-rpc-methods/src/permitted/invokeAccountsSnap.ts +++ b/packages/snaps-rpc-methods/src/permitted/invokeProtocolSnap.ts @@ -1,40 +1,43 @@ import type { JsonRpcEngineEndCallback } from '@metamask/json-rpc-engine'; -import type { PermittedHandlerExport } from '@metamask/permission-controller'; +import type { + PermissionConstraint, + PermittedHandlerExport, +} from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; import type { - InvokeAccountsSnapParams, - InvokeAccountsSnapResult, + InvokeProtocolSnapParams, + InvokeProtocolSnapResult, } from '@metamask/snaps-sdk'; -import { AccountsSnapHandlerType } 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, JsonRpcRequest } from '@metamask/utils'; import { hasProperty, type Json } from '@metamask/utils'; +import { SnapEndowments, getProtocolCaveatRpcMethods } from '../endowments'; import type { MethodHooksObject } from '../utils'; import { getValidatedParams } from './invokeSnapSugar'; -const hookNames: MethodHooksObject = { +const hookNames: MethodHooksObject = { hasPermission: true, handleSnapRpcRequest: true, getSnap: true, - getAllowedKeyringMethods: true, + getSubjectPermissions: true, }; /** - * `wallet_invokeAccountsSnap` invokes an account Snap. + * `wallet_invokeProtocolSnap` invokes a protocol Snap. */ -export const invokeAccountSnapHandler: PermittedHandlerExport< - InvokeAccountsSnapHooks, - InvokeAccountsSnapParams, - InvokeAccountsSnapResult +export const invokeProtocolSnapHandler: PermittedHandlerExport< + InvokeProtocolSnapHooks, + InvokeProtocolSnapParams, + InvokeProtocolSnapResult > = { - methodNames: ['wallet_invokeAccountsSnap'], - implementation: invokeAccountSnapImplementation, + methodNames: ['wallet_invokeProtocolSnap'], + implementation: invokeProtocolSnapImplementation, hookNames, }; -export type InvokeAccountsSnapHooks = { +export type InvokeProtocolSnapHooks = { hasPermission: (permissionName: string) => boolean; handleSnapRpcRequest: ({ @@ -45,17 +48,25 @@ export type InvokeAccountsSnapHooks = { getSnap: (snapId: string) => Snap | undefined; - getAllowedKeyringMethods: () => string[]; + getSubjectPermissions: ( + origin: string, + ) => Promise | undefined>; }; -const HANDLER_MAP = Object.freeze({ - [AccountsSnapHandlerType.Keyring]: HandlerType.OnKeyringRequest, - [AccountsSnapHandlerType.Chain]: HandlerType.OnAccountsChainRequest, -}); +// These RPC methods are assumed to be supported by the Snap as they are required. +// TODO: Verify these. +const DEFAULT_RPC_METHODS = [ + 'chain_listTransactions', + 'chain_estimateFees', + 'chain_getBalances', + 'chain_broadcastTransaction', + 'chain_getDataForTransaction', + 'chain_getTransactionStatus', +]; /** - * The `wallet_invokeAccountsSnap` method implementation. - * Invokes onKeyringRequest or onAccountsChainRequest if the snap requested is installed and connected to the dapp. + * The `wallet_invokeProtocolSnap` method implementation. + * Invokes onKeyringRequest or onProtocolRequest if the snap requested is installed and connected to the dapp. * * @param req - The JSON-RPC request object. * @param res - The JSON-RPC response object. @@ -66,32 +77,31 @@ const HANDLER_MAP = Object.freeze({ * @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 hooks.getSubjectPermissions - Get the permissions for a specific origin. * @returns Nothing. */ -async function invokeAccountSnapImplementation( - req: JsonRpcRequest, - res: PendingJsonRpcResponse, +async function invokeProtocolSnapImplementation( + req: JsonRpcRequest, + res: PendingJsonRpcResponse, _next: unknown, end: JsonRpcEngineEndCallback, { handleSnapRpcRequest, hasPermission, getSnap, - getAllowedKeyringMethods, - }: InvokeAccountsSnapHooks, + getSubjectPermissions, + }: InvokeProtocolSnapHooks, ): Promise { - let params: InvokeAccountsSnapParams; + let params: InvokeProtocolSnapParams; try { - params = getValidatedParams(req.params) as InvokeAccountsSnapParams; + params = getValidatedParams(req.params) as InvokeProtocolSnapParams; } catch (error) { return end(error); } // We expect the MM middleware stack to always add the origin to requests const { origin } = req as JsonRpcRequest & { origin: string }; - const { snapId, request, type } = params; + const { snapId, request } = params; if (!origin || !hasPermission(WALLET_SNAP_PERMISSION_KEY)) { return end( @@ -101,16 +111,6 @@ async function invokeAccountSnapImplementation( ); } - const handler = HANDLER_MAP[type]; - - if (!handler) { - return end( - rpcErrors.invalidParams({ - message: `The handler type "${type}" does not exist.`, - }), - ); - } - if (!getSnap(snapId)) { return end( rpcErrors.invalidRequest({ @@ -127,21 +127,30 @@ async function invokeAccountSnapImplementation( ); } - if (type === AccountsSnapHandlerType.Keyring) { - const allowedMethods = getAllowedKeyringMethods(); - if (!allowedMethods.includes(request.method)) { - return end( - rpcErrors.invalidRequest({ - message: `The origin "${origin}" is not allowed to invoke the method "${request.method}".`, - }), - ); - } - } + const snapPermissions = await getSubjectPermissions(snapId); + + const protocolPermission = snapPermissions?.[SnapEndowments.Protocol]; + + const additionalRpcMethods = + getProtocolCaveatRpcMethods(protocolPermission) ?? []; + + const supportedMethods = [...DEFAULT_RPC_METHODS, ...additionalRpcMethods]; + + const isSigningRequest = !supportedMethods.includes(request.method); + + const wrappedRequest = isSigningRequest + ? // TODO: Other params + { method: 'keyring_submitRequest', params: { request } } + : request; + + const handler = isSigningRequest + ? HandlerType.OnKeyringRequest + : HandlerType.OnProtocolRequest; try { res.result = (await handleSnapRpcRequest({ snapId, - request, + request: wrappedRequest, handler, })) as Json; } catch (error) { diff --git a/packages/snaps-sdk/src/types/handlers/accounts-chain.ts b/packages/snaps-sdk/src/types/handlers/protocol.ts similarity index 76% rename from packages/snaps-sdk/src/types/handlers/accounts-chain.ts rename to packages/snaps-sdk/src/types/handlers/protocol.ts index db4692785c..8d17c915c7 100644 --- a/packages/snaps-sdk/src/types/handlers/accounts-chain.ts +++ b/packages/snaps-sdk/src/types/handlers/protocol.ts @@ -1,10 +1,10 @@ import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; /** - * The `onAccountsChainRequest` handler, which is called when a Snap receives a + * The `onProtocolRequest` handler, which is called when a Snap receives a * accounts chain request. * - * Note that using this handler requires the `endowment:accounts-chain` permission. + * Note that using this handler requires the `endowment:protocol` permission. * * @param args - The request arguments. * @param args.origin - The origin of the request. This can be the ID of another @@ -15,7 +15,7 @@ import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; * JSON-serializable value. In order to return an error, throw a `SnapError` * instead. */ -export type OnAccountsChainRequestHandler< +export type OnProtocolRequestHandler< Params extends JsonRpcParams = JsonRpcParams, > = (args: { origin: string; diff --git a/packages/snaps-sdk/src/types/methods/index.ts b/packages/snaps-sdk/src/types/methods/index.ts index e7c8953f55..3fa14eef5c 100644 --- a/packages/snaps-sdk/src/types/methods/index.ts +++ b/packages/snaps-sdk/src/types/methods/index.ts @@ -9,7 +9,7 @@ export * from './get-file'; export * from './get-interface-state'; export * from './get-locale'; export * from './get-snaps'; -export * from './invoke-accounts-snap'; +export * from './invoke-protocol-snap'; export * from './invoke-keyring'; export * from './invoke-snap'; export * from './manage-accounts'; diff --git a/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts b/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts deleted file mode 100644 index e134a26850..0000000000 --- a/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountsSnapHandlerType } from './invoke-accounts-snap'; - -describe('AccountsSnapHandlerType', () => { - it('has the correct values', () => { - expect(Object.values(AccountsSnapHandlerType)).toHaveLength(2); - expect(AccountsSnapHandlerType.Keyring).toBe('keyring'); - expect(AccountsSnapHandlerType.Chain).toBe('chain'); - }); -}); diff --git a/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts b/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts deleted file mode 100644 index 42daa4482c..0000000000 --- a/packages/snaps-sdk/src/types/methods/invoke-accounts-snap.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Json } from '@metamask/utils'; - -import type { InvokeSnapParams } from './invoke-snap'; - -export enum AccountsSnapHandlerType { - Keyring = 'keyring', - Chain = 'chain', -} - -/** - * The request parameters for the `wallet_invokeAccountsSnap` method. - * - * @property snapId - The ID of the snap to invoke. - * @property request - The JSON-RPC request to send to the snap. - * @property type - The type of handler to invoke. - */ -export type InvokeAccountsSnapParams = InvokeSnapParams & { - type: AccountsSnapHandlerType; -}; - -/** - * The result returned by the `wallet_invokeAccountsSnap` method, which is the result - * returned by the Snap. - */ -export type InvokeAccountsSnapResult = Json; diff --git a/packages/snaps-sdk/src/types/methods/invoke-protocol-snap.ts b/packages/snaps-sdk/src/types/methods/invoke-protocol-snap.ts new file mode 100644 index 0000000000..674fad1101 --- /dev/null +++ b/packages/snaps-sdk/src/types/methods/invoke-protocol-snap.ts @@ -0,0 +1,18 @@ +import type { Json } from '@metamask/utils'; + +import type { InvokeSnapParams } from './invoke-snap'; + +/** + * The request parameters for the `wallet_invokeProtocolSnap` method. + * + * @property snapId - The ID of the snap to invoke. + * @property request - The JSON-RPC request to send to the snap. + * @property type - The type of handler to invoke. + */ +export type InvokeProtocolSnapParams = InvokeSnapParams; + +/** + * The result returned by the `wallet_invokeProtocolSnap` method, which is the result + * returned by the Snap. + */ +export type InvokeProtocolSnapResult = Json; diff --git a/packages/snaps-utils/src/caveats.ts b/packages/snaps-utils/src/caveats.ts index 61bd80910e..7599e95b4c 100644 --- a/packages/snaps-utils/src/caveats.ts +++ b/packages/snaps-utils/src/caveats.ts @@ -53,4 +53,9 @@ export enum SnapCaveatType { * Caveat specifying the max request time for a handler endowment. */ MaxRequestTime = 'maxRequestTime', + + /** + * Caveat specifying a list of RPC methods serviced by an endowment. + */ + SnapRpcMethods = 'snapRpcMethods', } diff --git a/packages/snaps-utils/src/handler-types.ts b/packages/snaps-utils/src/handler-types.ts index a0c7b3faf2..ac3592520d 100644 --- a/packages/snaps-utils/src/handler-types.ts +++ b/packages/snaps-utils/src/handler-types.ts @@ -7,7 +7,7 @@ export enum HandlerType { OnUpdate = 'onUpdate', OnNameLookup = 'onNameLookup', OnKeyringRequest = 'onKeyringRequest', - OnAccountsChainRequest = 'onAccountsChainRequest', + OnProtocolRequest = 'OnProtocolRequest', OnHomePage = 'onHomePage', OnUserInput = 'onUserInput', } diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 335ad943d2..2e5c9d7658 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -180,6 +180,8 @@ export const HandlerCaveatsStruct = object({ maxRequestTime: optional(MaxRequestTimeStruct), }); +export const ProtocolRpcMethodsStruct = array(string()); + export type HandlerCaveats = Infer; export const EmptyObjectStruct = object({}) as unknown as Struct< @@ -199,10 +201,13 @@ export const PermissionsStruct: Describe = type({ 'endowment:keyring': optional( assign(HandlerCaveatsStruct, KeyringOriginsStruct), ), - 'endowment:accounts-chain': optional( + 'endowment:protocol': optional( assign( HandlerCaveatsStruct, - assign(KeyringOriginsStruct, object({ chains: ChainIdsStruct })), + assign( + KeyringOriginsStruct, + object({ chains: ChainIdsStruct, methods: ProtocolRpcMethodsStruct }), + ), ), ), 'endowment:lifecycle-hooks': optional(HandlerCaveatsStruct),