Skip to content

Commit b85b44d

Browse files
Add onProtocolRequest export
1 parent f0e8f91 commit b85b44d

File tree

11 files changed

+257
-10
lines changed

11 files changed

+257
-10
lines changed

packages/snaps-controllers/src/multichain/MultichainRoutingController.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import type {
1010
ValidPermission,
1111
} from '@metamask/permission-controller';
1212
import { rpcErrors } from '@metamask/rpc-errors';
13-
import { SnapEndowments } from '@metamask/snaps-rpc-methods';
13+
import {
14+
getProtocolCaveatChainIds,
15+
getProtocolCaveatRpcMethods,
16+
SnapEndowments,
17+
} from '@metamask/snaps-rpc-methods';
1418
import type {
1519
EmptyObject,
1620
Json,
@@ -106,7 +110,7 @@ export class MultichainRoutingController extends BaseController<
106110
return accounts.flatMap((account) => account.methods);
107111
}
108112

109-
#getProtocolSnaps(_chainId: Caip2ChainId, _method: string) {
113+
#getProtocolSnaps(chainId: Caip2ChainId, method: string) {
110114
const allSnaps = this.messagingSystem.call('SnapController:getAll');
111115
const filteredSnaps = getRunnableSnaps(allSnaps);
112116

@@ -115,13 +119,17 @@ export class MultichainRoutingController extends BaseController<
115119
'PermissionController:getPermissions',
116120
snap.id,
117121
);
118-
// TODO: Protocol Snap export
119-
// TODO: Filter based on chain ID and method
120-
if (permissions && hasProperty(permissions, SnapEndowments.Rpc)) {
121-
accumulator.push({
122-
snapId: snap.id,
123-
permission: permissions[SnapEndowments.Rpc],
124-
});
122+
if (permissions && hasProperty(permissions, SnapEndowments.Protocol)) {
123+
const permission = permissions[SnapEndowments.Protocol];
124+
const chains = getProtocolCaveatChainIds(permission);
125+
const methods = getProtocolCaveatRpcMethods(permission);
126+
// TODO: This may need to be more complicated depending on the decided format.
127+
if (chains?.includes(chainId) && methods?.includes(method)) {
128+
accumulator.push({
129+
snapId: snap.id,
130+
permission,
131+
});
132+
}
125133
}
126134

127135
return accumulator;
@@ -155,7 +163,7 @@ export class MultichainRoutingController extends BaseController<
155163
snapId,
156164
origin: 'metamask', // TODO: Determine origin of these requests?
157165
request,
158-
handler: HandlerType.OnRpcRequest, // TODO: Protocol Snap export
166+
handler: HandlerType.OnProtocolRequest,
159167
});
160168
}
161169

packages/snaps-execution-environments/src/common/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export function getHandlerArguments(
7676
}
7777
case HandlerType.OnRpcRequest:
7878
case HandlerType.OnKeyringRequest:
79+
case HandlerType.OnProtocolRequest: // TODO: Decide on origin
7980
return { origin, request };
8081

8182
case HandlerType.OnCronjob:

packages/snaps-rpc-methods/src/endowments/enum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export enum SnapEndowments {
1010
LifecycleHooks = 'endowment:lifecycle-hooks',
1111
Keyring = 'endowment:keyring',
1212
HomePage = 'endowment:page-home',
13+
Protocol = 'endowment:protocol',
1314
}

packages/snaps-rpc-methods/src/endowments/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ import {
2626
nameLookupEndowmentBuilder,
2727
} from './name-lookup';
2828
import { networkAccessEndowmentBuilder } from './network-access';
29+
import {
30+
getProtocolCaveatMapper,
31+
protocolCaveatSpecifications,
32+
protocolEndowmentBuilder,
33+
} from './protocol';
2934
import {
3035
getRpcCaveatMapper,
3136
rpcCaveatSpecifications,
@@ -55,6 +60,7 @@ export const endowmentPermissionBuilders = {
5560
[nameLookupEndowmentBuilder.targetName]: nameLookupEndowmentBuilder,
5661
[lifecycleHooksEndowmentBuilder.targetName]: lifecycleHooksEndowmentBuilder,
5762
[keyringEndowmentBuilder.targetName]: keyringEndowmentBuilder,
63+
[protocolEndowmentBuilder.targetName]: protocolEndowmentBuilder,
5864
[homePageEndowmentBuilder.targetName]: homePageEndowmentBuilder,
5965
[signatureInsightEndowmentBuilder.targetName]:
6066
signatureInsightEndowmentBuilder,
@@ -68,6 +74,7 @@ export const endowmentCaveatSpecifications = {
6874
...keyringCaveatSpecifications,
6975
...signatureInsightCaveatSpecifications,
7076
...maxRequestTimeCaveatSpecifications,
77+
...protocolCaveatSpecifications,
7178
};
7279

7380
export const endowmentCaveatMappers: Record<
@@ -88,6 +95,9 @@ export const endowmentCaveatMappers: Record<
8895
[keyringEndowmentBuilder.targetName]: createMaxRequestTimeMapper(
8996
getKeyringCaveatMapper,
9097
),
98+
[protocolEndowmentBuilder.targetName]: createMaxRequestTimeMapper(
99+
getProtocolCaveatMapper,
100+
),
91101
[signatureInsightEndowmentBuilder.targetName]: createMaxRequestTimeMapper(
92102
getSignatureInsightCaveatMapper,
93103
),
@@ -106,6 +116,7 @@ export const handlerEndowments: Record<HandlerType, string | null> = {
106116
[HandlerType.OnKeyringRequest]: keyringEndowmentBuilder.targetName,
107117
[HandlerType.OnHomePage]: homePageEndowmentBuilder.targetName,
108118
[HandlerType.OnSignature]: signatureInsightEndowmentBuilder.targetName,
119+
[HandlerType.OnProtocolRequest]: protocolEndowmentBuilder.targetName,
109120
[HandlerType.OnUserInput]: null,
110121
};
111122

@@ -117,3 +128,7 @@ export { getChainIdsCaveat, getLookupMatchersCaveat } from './name-lookup';
117128
export { getKeyringCaveatOrigins } from './keyring';
118129
export { getMaxRequestTimeCaveat } from './caveats';
119130
export { getCronjobCaveatJobs } from './cronjob';
131+
export {
132+
getProtocolCaveatChainIds,
133+
getProtocolCaveatRpcMethods,
134+
} from './protocol';
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import type {
2+
Caveat,
3+
CaveatConstraint,
4+
CaveatSpecificationConstraint,
5+
EndowmentGetterParams,
6+
PermissionConstraint,
7+
PermissionSpecificationBuilder,
8+
PermissionValidatorConstraint,
9+
ValidPermissionSpecification,
10+
} from '@metamask/permission-controller';
11+
import { PermissionType, SubjectType } from '@metamask/permission-controller';
12+
import { rpcErrors } from '@metamask/rpc-errors';
13+
import {
14+
ProtocolRpcMethodsStruct,
15+
SnapCaveatType,
16+
} from '@metamask/snaps-utils';
17+
import type { Json, NonEmptyArray } from '@metamask/utils';
18+
import {
19+
assertStruct,
20+
hasProperty,
21+
isObject,
22+
isPlainObject,
23+
} from '@metamask/utils';
24+
25+
import { createGenericPermissionValidator } from './caveats';
26+
import { SnapEndowments } from './enum';
27+
28+
const permissionName = SnapEndowments.Protocol;
29+
30+
type ProtocolEndowmentSpecification = ValidPermissionSpecification<{
31+
permissionType: PermissionType.Endowment;
32+
targetName: typeof permissionName;
33+
endowmentGetter: (_options?: EndowmentGetterParams) => null;
34+
allowedCaveats: Readonly<NonEmptyArray<string>> | null;
35+
validator: PermissionValidatorConstraint;
36+
subjectTypes: readonly SubjectType[];
37+
}>;
38+
39+
/**
40+
* `endowment:protocol` returns nothing; it is intended to be used as a flag
41+
* by the client to detect whether the Snap supports the Protocol API.
42+
*
43+
* @param _builderOptions - Optional specification builder options.
44+
* @returns The specification for the accounts chain endowment.
45+
*/
46+
const specificationBuilder: PermissionSpecificationBuilder<
47+
PermissionType.Endowment,
48+
any,
49+
ProtocolEndowmentSpecification
50+
> = (_builderOptions?: unknown) => {
51+
return {
52+
permissionType: PermissionType.Endowment,
53+
targetName: permissionName,
54+
allowedCaveats: [
55+
SnapCaveatType.KeyringOrigin,
56+
SnapCaveatType.ChainIds,
57+
SnapCaveatType.SnapRpcMethods,
58+
SnapCaveatType.MaxRequestTime,
59+
],
60+
endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null,
61+
validator: createGenericPermissionValidator([
62+
{ type: SnapCaveatType.ChainIds },
63+
{ type: SnapCaveatType.SnapRpcMethods },
64+
{ type: SnapCaveatType.MaxRequestTime, optional: true },
65+
]),
66+
subjectTypes: [SubjectType.Snap],
67+
};
68+
};
69+
70+
export const protocolEndowmentBuilder = Object.freeze({
71+
targetName: permissionName,
72+
specificationBuilder,
73+
} as const);
74+
75+
/**
76+
* Map a raw value from the `initialPermissions` to a caveat specification.
77+
* Note that this function does not do any validation, that's handled by the
78+
* PermissionsController when the permission is requested.
79+
*
80+
* @param value - The raw value from the `initialPermissions`.
81+
* @returns The caveat specification.
82+
*/
83+
export function getProtocolCaveatMapper(
84+
value: Json,
85+
): Pick<PermissionConstraint, 'caveats'> {
86+
if (!value || !isObject(value) || Object.keys(value).length === 0) {
87+
return { caveats: null };
88+
}
89+
90+
const caveats = [];
91+
92+
if (value.chains) {
93+
caveats.push({
94+
type: SnapCaveatType.ChainIds,
95+
value: value.chains,
96+
});
97+
}
98+
99+
if (value.methods) {
100+
caveats.push({
101+
type: SnapCaveatType.SnapRpcMethods,
102+
value: value.methods,
103+
});
104+
}
105+
106+
return { caveats: caveats as NonEmptyArray<CaveatConstraint> };
107+
}
108+
109+
/**
110+
* Getter function to get the {@link ChainIds} caveat value from a
111+
* permission.
112+
*
113+
* @param permission - The permission to get the caveat value from.
114+
* @returns The caveat value.
115+
*/
116+
export function getProtocolCaveatChainIds(
117+
permission?: PermissionConstraint,
118+
): string[] | null {
119+
const caveat = permission?.caveats?.find(
120+
(permCaveat) => permCaveat.type === SnapCaveatType.ChainIds,
121+
) as Caveat<string, string[]> | undefined;
122+
123+
return caveat ? caveat.value : null;
124+
}
125+
126+
/**
127+
* Getter function to get the {@link SnapRpcMethods} caveat value from a
128+
* permission.
129+
*
130+
* @param permission - The permission to get the caveat value from.
131+
* @returns The caveat value.
132+
*/
133+
export function getProtocolCaveatRpcMethods(
134+
permission?: PermissionConstraint,
135+
): string[] | null {
136+
const caveat = permission?.caveats?.find(
137+
(permCaveat) => permCaveat.type === SnapCaveatType.SnapRpcMethods,
138+
) as Caveat<string, string[]> | undefined;
139+
140+
return caveat ? caveat.value : null;
141+
}
142+
143+
/**
144+
* Validates the type of the caveat value.
145+
*
146+
* @param caveat - The caveat to validate.
147+
* @throws If the caveat value is invalid.
148+
*/
149+
function validateCaveat(caveat: Caveat<string, any>): void {
150+
if (!hasProperty(caveat, 'value') || !isPlainObject(caveat)) {
151+
throw rpcErrors.invalidParams({
152+
message: 'Expected a plain object.',
153+
});
154+
}
155+
156+
const { value } = caveat;
157+
assertStruct(
158+
value,
159+
ProtocolRpcMethodsStruct,
160+
'Invalid RPC methods specified',
161+
rpcErrors.invalidParams,
162+
);
163+
}
164+
165+
export const protocolCaveatSpecifications: Record<
166+
SnapCaveatType.SnapRpcMethods,
167+
CaveatSpecificationConstraint
168+
> = {
169+
[SnapCaveatType.SnapRpcMethods]: Object.freeze({
170+
type: SnapCaveatType.SnapRpcMethods,
171+
validator: (caveat: Caveat<string, any>) => validateCaveat(caveat),
172+
}),
173+
};

packages/snaps-sdk/src/types/handlers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './home-page';
33
export * from './keyring';
44
export * from './lifecycle';
55
export * from './name-lookup';
6+
export * from './protocol';
67
export * from './rpc-request';
78
export * from './transaction';
89
export * from './signature';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Json, JsonRpcParams, JsonRpcRequest } from '@metamask/utils';
2+
3+
/**
4+
* The `onProtocolRequest` handler, which is called when a Snap receives a
5+
* protocol request.
6+
*
7+
* Note that using this handler requires the `endowment:protocol` permission.
8+
*
9+
* @param args - The request arguments.
10+
* @param args.origin - The origin of the request. This can be the ID of another
11+
* Snap, or the URL of a website.
12+
* @param args.request - The protocol request sent to the Snap. This includes
13+
* the method name and parameters.
14+
* @returns The response to the protocol request. This must be a
15+
* JSON-serializable value. In order to return an error, throw a `SnapError`
16+
* instead.
17+
*/
18+
export type OnProtocolRequestHandler<
19+
Params extends JsonRpcParams = JsonRpcParams,
20+
> = (args: {
21+
origin: string;
22+
request: JsonRpcRequest<Params>;
23+
}) => Promise<Json>;

packages/snaps-utils/src/caveats.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,9 @@ export enum SnapCaveatType {
5353
* Caveat specifying the max request time for a handler endowment.
5454
*/
5555
MaxRequestTime = 'maxRequestTime',
56+
57+
/**
58+
* Caveat specifying a list of RPC methods serviced by an endowment.
59+
*/
60+
SnapRpcMethods = 'snapRpcMethods',
5661
}

packages/snaps-utils/src/handler-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export enum HandlerType {
99
OnKeyringRequest = 'onKeyringRequest',
1010
OnHomePage = 'onHomePage',
1111
OnUserInput = 'onUserInput',
12+
OnProtocolRequest = 'onProtocolRequest',
1213
}
1314

1415
export type SnapHandler = {

packages/snaps-utils/src/handlers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
OnInstallHandler,
55
OnKeyringRequestHandler,
66
OnNameLookupHandler,
7+
OnProtocolRequestHandler,
78
OnRpcRequestHandler,
89
OnSignatureHandler,
910
OnTransactionHandler,
@@ -103,6 +104,15 @@ export const SNAP_EXPORTS = {
103104
return typeof snapExport === 'function';
104105
},
105106
},
107+
[HandlerType.OnProtocolRequest]: {
108+
type: HandlerType.OnProtocolRequest,
109+
required: false,
110+
validator: (
111+
snapExport: unknown,
112+
): snapExport is OnProtocolRequestHandler => {
113+
return typeof snapExport === 'function';
114+
},
115+
},
106116
} as const;
107117

108118
export const OnTransactionSeverityResponseStruct = object({

packages/snaps-utils/src/manifest/validation.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ export const MaxRequestTimeStruct = size(
174174
MAXIMUM_REQUEST_TIMEOUT,
175175
);
176176

177+
// TODO: Decide on the format for this
178+
export const ProtocolRpcMethodsStruct = array(string());
179+
177180
// Utility type to union with for all handler structs
178181
export const HandlerCaveatsStruct = object({
179182
maxRequestTime: optional(MaxRequestTimeStruct),
@@ -198,6 +201,12 @@ export const PermissionsStruct: Describe<InitialPermissions> = type({
198201
'endowment:keyring': optional(
199202
mergeStructs(HandlerCaveatsStruct, KeyringOriginsStruct),
200203
),
204+
'endowment:protocol': optional(
205+
mergeStructs(
206+
HandlerCaveatsStruct,
207+
object({ chains: ChainIdsStruct, methods: ProtocolRpcMethodsStruct }),
208+
),
209+
),
201210
'endowment:lifecycle-hooks': optional(HandlerCaveatsStruct),
202211
'endowment:name-lookup': optional(
203212
mergeStructs(

0 commit comments

Comments
 (0)