diff --git a/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts b/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts index 1a0fc083c2..974fd36a2c 100644 --- a/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts +++ b/packages/snaps-controllers/src/multichain/MultichainRoutingController.ts @@ -10,7 +10,11 @@ import type { ValidPermission, } from '@metamask/permission-controller'; import { rpcErrors } from '@metamask/rpc-errors'; -import { SnapEndowments } from '@metamask/snaps-rpc-methods'; +import { + getProtocolCaveatChainIds, + getProtocolCaveatRpcMethods, + SnapEndowments, +} from '@metamask/snaps-rpc-methods'; import type { EmptyObject, Json, @@ -106,7 +110,7 @@ export class MultichainRoutingController extends BaseController< return accounts.flatMap((account) => account.methods); } - #getProtocolSnaps(_chainId: Caip2ChainId, _method: string) { + #getProtocolSnaps(chainId: Caip2ChainId, method: string) { const allSnaps = this.messagingSystem.call('SnapController:getAll'); const filteredSnaps = getRunnableSnaps(allSnaps); @@ -115,13 +119,17 @@ export class MultichainRoutingController extends BaseController< 'PermissionController:getPermissions', snap.id, ); - // TODO: Protocol Snap export - // TODO: Filter based on chain ID and method - if (permissions && hasProperty(permissions, SnapEndowments.Rpc)) { - accumulator.push({ - snapId: snap.id, - permission: permissions[SnapEndowments.Rpc], - }); + if (permissions && hasProperty(permissions, SnapEndowments.Protocol)) { + const permission = permissions[SnapEndowments.Protocol]; + const chains = getProtocolCaveatChainIds(permission); + const methods = getProtocolCaveatRpcMethods(permission); + // TODO: This may need to be more complicated depending on the decided format. + if (chains?.includes(chainId) && methods?.includes(method)) { + accumulator.push({ + snapId: snap.id, + permission, + }); + } } return accumulator; @@ -155,7 +163,7 @@ export class MultichainRoutingController extends BaseController< snapId, origin: 'metamask', // TODO: Determine origin of these requests? request, - handler: HandlerType.OnRpcRequest, // TODO: Protocol Snap export + handler: HandlerType.OnProtocolRequest, }); } diff --git a/packages/snaps-execution-environments/src/common/commands.ts b/packages/snaps-execution-environments/src/common/commands.ts index 32d073f71f..222e5caa13 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.OnProtocolRequest: // TODO: Decide on origin 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 f0d1577c6f..87e844f4b3 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', + Protocol = 'endowment:protocol', } diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index faa0c8fe06..2efe293d85 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -26,6 +26,11 @@ import { nameLookupEndowmentBuilder, } from './name-lookup'; import { networkAccessEndowmentBuilder } from './network-access'; +import { + getProtocolCaveatMapper, + protocolCaveatSpecifications, + protocolEndowmentBuilder, +} from './protocol'; import { getRpcCaveatMapper, rpcCaveatSpecifications, @@ -55,6 +60,7 @@ export const endowmentPermissionBuilders = { [nameLookupEndowmentBuilder.targetName]: nameLookupEndowmentBuilder, [lifecycleHooksEndowmentBuilder.targetName]: lifecycleHooksEndowmentBuilder, [keyringEndowmentBuilder.targetName]: keyringEndowmentBuilder, + [protocolEndowmentBuilder.targetName]: protocolEndowmentBuilder, [homePageEndowmentBuilder.targetName]: homePageEndowmentBuilder, [signatureInsightEndowmentBuilder.targetName]: signatureInsightEndowmentBuilder, @@ -68,6 +74,7 @@ export const endowmentCaveatSpecifications = { ...keyringCaveatSpecifications, ...signatureInsightCaveatSpecifications, ...maxRequestTimeCaveatSpecifications, + ...protocolCaveatSpecifications, }; export const endowmentCaveatMappers: Record< @@ -88,6 +95,9 @@ export const endowmentCaveatMappers: Record< [keyringEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getKeyringCaveatMapper, ), + [protocolEndowmentBuilder.targetName]: createMaxRequestTimeMapper( + getProtocolCaveatMapper, + ), [signatureInsightEndowmentBuilder.targetName]: createMaxRequestTimeMapper( getSignatureInsightCaveatMapper, ), @@ -106,6 +116,7 @@ export const handlerEndowments: Record = { [HandlerType.OnKeyringRequest]: keyringEndowmentBuilder.targetName, [HandlerType.OnHomePage]: homePageEndowmentBuilder.targetName, [HandlerType.OnSignature]: signatureInsightEndowmentBuilder.targetName, + [HandlerType.OnProtocolRequest]: protocolEndowmentBuilder.targetName, [HandlerType.OnUserInput]: null, }; @@ -117,3 +128,7 @@ export { getChainIdsCaveat, getLookupMatchersCaveat } from './name-lookup'; export { getKeyringCaveatOrigins } from './keyring'; export { getMaxRequestTimeCaveat } from './caveats'; export { getCronjobCaveatJobs } from './cronjob'; +export { + getProtocolCaveatChainIds, + getProtocolCaveatRpcMethods, +} from './protocol'; diff --git a/packages/snaps-rpc-methods/src/endowments/protocol.ts b/packages/snaps-rpc-methods/src/endowments/protocol.ts new file mode 100644 index 0000000000..5cbddd2576 --- /dev/null +++ b/packages/snaps-rpc-methods/src/endowments/protocol.ts @@ -0,0 +1,173 @@ +import type { + Caveat, + CaveatConstraint, + CaveatSpecificationConstraint, + EndowmentGetterParams, + PermissionConstraint, + PermissionSpecificationBuilder, + PermissionValidatorConstraint, + ValidPermissionSpecification, +} from '@metamask/permission-controller'; +import { PermissionType, SubjectType } from '@metamask/permission-controller'; +import { rpcErrors } from '@metamask/rpc-errors'; +import { + ProtocolRpcMethodsStruct, + SnapCaveatType, +} from '@metamask/snaps-utils'; +import type { Json, NonEmptyArray } from '@metamask/utils'; +import { + assertStruct, + hasProperty, + isObject, + isPlainObject, +} from '@metamask/utils'; + +import { createGenericPermissionValidator } from './caveats'; +import { SnapEndowments } from './enum'; + +const permissionName = SnapEndowments.Protocol; + +type ProtocolEndowmentSpecification = ValidPermissionSpecification<{ + permissionType: PermissionType.Endowment; + targetName: typeof permissionName; + endowmentGetter: (_options?: EndowmentGetterParams) => null; + allowedCaveats: Readonly> | null; + validator: PermissionValidatorConstraint; + subjectTypes: readonly SubjectType[]; +}>; + +/** + * `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. + */ +const specificationBuilder: PermissionSpecificationBuilder< + PermissionType.Endowment, + any, + ProtocolEndowmentSpecification +> = (_builderOptions?: unknown) => { + return { + permissionType: PermissionType.Endowment, + targetName: permissionName, + allowedCaveats: [ + SnapCaveatType.KeyringOrigin, + SnapCaveatType.ChainIds, + SnapCaveatType.SnapRpcMethods, + SnapCaveatType.MaxRequestTime, + ], + endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null, + validator: createGenericPermissionValidator([ + { type: SnapCaveatType.ChainIds }, + { type: SnapCaveatType.SnapRpcMethods }, + { type: SnapCaveatType.MaxRequestTime, optional: true }, + ]), + subjectTypes: [SubjectType.Snap], + }; +}; + +export const protocolEndowmentBuilder = 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 getProtocolCaveatMapper( + 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.methods) { + caveats.push({ + type: SnapCaveatType.SnapRpcMethods, + value: value.methods, + }); + } + + return { caveats: caveats as NonEmptyArray }; +} + +/** + * 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 getProtocolCaveatChainIds( + permission?: PermissionConstraint, +): string[] | null { + const caveat = permission?.caveats?.find( + (permCaveat) => permCaveat.type === SnapCaveatType.ChainIds, + ) as Caveat | undefined; + + 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-sdk/src/types/handlers/index.ts b/packages/snaps-sdk/src/types/handlers/index.ts index 1e4fc63634..8065072756 100644 --- a/packages/snaps-sdk/src/types/handlers/index.ts +++ b/packages/snaps-sdk/src/types/handlers/index.ts @@ -3,6 +3,7 @@ export * from './home-page'; export * from './keyring'; export * from './lifecycle'; export * from './name-lookup'; +export * from './protocol'; export * from './rpc-request'; export * from './transaction'; export * from './signature'; diff --git a/packages/snaps-sdk/src/types/handlers/protocol.ts b/packages/snaps-sdk/src/types/handlers/protocol.ts new file mode 100644 index 0000000000..601b6cf926 --- /dev/null +++ b/packages/snaps-sdk/src/types/handlers/protocol.ts @@ -0,0 +1,23 @@ +import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils'; + +/** + * The `onProtocolRequest` handler, which is called when a Snap receives a + * protocol request. + * + * 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 + * Snap, or the URL of a website. + * @param args.request - The protocol request sent to the Snap. This includes + * the method name and parameters. + * @returns The response to the protocol request. This must be a + * JSON-serializable value. In order to return an error, throw a `SnapError` + * instead. + */ +export type OnProtocolRequestHandler< + Params extends JsonRpcParams = JsonRpcParams, +> = (args: { + origin: string; + request: JsonRpcRequest; +}) => Promise; 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 642c201fec..d389062003 100644 --- a/packages/snaps-utils/src/handler-types.ts +++ b/packages/snaps-utils/src/handler-types.ts @@ -9,6 +9,7 @@ export enum HandlerType { OnKeyringRequest = 'onKeyringRequest', OnHomePage = 'onHomePage', OnUserInput = 'onUserInput', + OnProtocolRequest = 'onProtocolRequest', } export type SnapHandler = { diff --git a/packages/snaps-utils/src/handlers.ts b/packages/snaps-utils/src/handlers.ts index 6c4e37a9e6..44515aec94 100644 --- a/packages/snaps-utils/src/handlers.ts +++ b/packages/snaps-utils/src/handlers.ts @@ -4,6 +4,7 @@ import type { OnInstallHandler, OnKeyringRequestHandler, OnNameLookupHandler, + OnProtocolRequestHandler, OnRpcRequestHandler, OnSignatureHandler, OnTransactionHandler, @@ -103,6 +104,15 @@ export const SNAP_EXPORTS = { return typeof snapExport === 'function'; }, }, + [HandlerType.OnProtocolRequest]: { + type: HandlerType.OnProtocolRequest, + required: false, + validator: ( + snapExport: unknown, + ): snapExport is OnProtocolRequestHandler => { + return typeof snapExport === 'function'; + }, + }, } as const; export const OnTransactionSeverityResponseStruct = object({ diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index dd65132a10..15dfdd7ac4 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -174,6 +174,9 @@ export const MaxRequestTimeStruct = size( MAXIMUM_REQUEST_TIMEOUT, ); +// TODO: Decide on the format for this +export const ProtocolRpcMethodsStruct = array(string()); + // Utility type to union with for all handler structs export const HandlerCaveatsStruct = object({ maxRequestTime: optional(MaxRequestTimeStruct), @@ -198,6 +201,12 @@ export const PermissionsStruct: Describe = type({ 'endowment:keyring': optional( mergeStructs(HandlerCaveatsStruct, KeyringOriginsStruct), ), + 'endowment:protocol': optional( + mergeStructs( + HandlerCaveatsStruct, + object({ chains: ChainIdsStruct, methods: ProtocolRpcMethodsStruct }), + ), + ), 'endowment:lifecycle-hooks': optional(HandlerCaveatsStruct), 'endowment:name-lookup': optional( mergeStructs(