Skip to content

Commit

Permalink
Add onProtocolRequest export
Browse files Browse the repository at this point in the history
  • Loading branch information
FrederikBolding committed Nov 5, 2024
1 parent f0e8f91 commit b85b44d
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand All @@ -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;
Expand Down Expand Up @@ -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,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/snaps-rpc-methods/src/endowments/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export enum SnapEndowments {
LifecycleHooks = 'endowment:lifecycle-hooks',
Keyring = 'endowment:keyring',
HomePage = 'endowment:page-home',
Protocol = 'endowment:protocol',
}
15 changes: 15 additions & 0 deletions packages/snaps-rpc-methods/src/endowments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import {
nameLookupEndowmentBuilder,
} from './name-lookup';
import { networkAccessEndowmentBuilder } from './network-access';
import {
getProtocolCaveatMapper,
protocolCaveatSpecifications,
protocolEndowmentBuilder,
} from './protocol';
import {
getRpcCaveatMapper,
rpcCaveatSpecifications,
Expand Down Expand Up @@ -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,
Expand All @@ -68,6 +74,7 @@ export const endowmentCaveatSpecifications = {
...keyringCaveatSpecifications,
...signatureInsightCaveatSpecifications,
...maxRequestTimeCaveatSpecifications,
...protocolCaveatSpecifications,
};

export const endowmentCaveatMappers: Record<
Expand All @@ -88,6 +95,9 @@ export const endowmentCaveatMappers: Record<
[keyringEndowmentBuilder.targetName]: createMaxRequestTimeMapper(
getKeyringCaveatMapper,
),
[protocolEndowmentBuilder.targetName]: createMaxRequestTimeMapper(
getProtocolCaveatMapper,
),
[signatureInsightEndowmentBuilder.targetName]: createMaxRequestTimeMapper(
getSignatureInsightCaveatMapper,
),
Expand All @@ -106,6 +116,7 @@ export const handlerEndowments: Record<HandlerType, string | null> = {
[HandlerType.OnKeyringRequest]: keyringEndowmentBuilder.targetName,
[HandlerType.OnHomePage]: homePageEndowmentBuilder.targetName,
[HandlerType.OnSignature]: signatureInsightEndowmentBuilder.targetName,
[HandlerType.OnProtocolRequest]: protocolEndowmentBuilder.targetName,
[HandlerType.OnUserInput]: null,
};

Expand All @@ -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';
173 changes: 173 additions & 0 deletions packages/snaps-rpc-methods/src/endowments/protocol.ts
Original file line number Diff line number Diff line change
@@ -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<NonEmptyArray<string>> | 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<PermissionConstraint, 'caveats'> {
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<CaveatConstraint> };
}

/**
* 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<string, string[]> | 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<string, string[]> | 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<string, any>): 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<string, any>) => validateCaveat(caveat),
}),
};
1 change: 1 addition & 0 deletions packages/snaps-sdk/src/types/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
23 changes: 23 additions & 0 deletions packages/snaps-sdk/src/types/handlers/protocol.ts
Original file line number Diff line number Diff line change
@@ -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<Params>;
}) => Promise<Json>;
5 changes: 5 additions & 0 deletions packages/snaps-utils/src/caveats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
1 change: 1 addition & 0 deletions packages/snaps-utils/src/handler-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum HandlerType {
OnKeyringRequest = 'onKeyringRequest',
OnHomePage = 'onHomePage',
OnUserInput = 'onUserInput',
OnProtocolRequest = 'onProtocolRequest',
}

export type SnapHandler = {
Expand Down
10 changes: 10 additions & 0 deletions packages/snaps-utils/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
OnInstallHandler,
OnKeyringRequestHandler,
OnNameLookupHandler,
OnProtocolRequestHandler,
OnRpcRequestHandler,
OnSignatureHandler,
OnTransactionHandler,
Expand Down Expand Up @@ -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({
Expand Down
9 changes: 9 additions & 0 deletions packages/snaps-utils/src/manifest/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -198,6 +201,12 @@ export const PermissionsStruct: Describe<InitialPermissions> = 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(
Expand Down

0 comments on commit b85b44d

Please sign in to comment.