Skip to content

Commit 911b279

Browse files
committed
Add device IDs endowment
1 parent 17b87e5 commit 911b279

File tree

7 files changed

+193
-5
lines changed

7 files changed

+193
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"version": "2.1.3",
3-
"description": "MetaMask example snap demonstrating the use of the `onInstall` and `onUpdate` lifecycle hooks.",
4-
"proposedName": "Lifecycle Hooks Example Snap",
3+
"description": "MetaMask example Snap demonstrating how to communicate with a Ledger hardware wallet.",
4+
"proposedName": "Ledger Example Snap",
55
"repository": {
66
"type": "git",
77
"url": "https://github.com/MetaMask/snaps.git"
@@ -11,14 +11,13 @@
1111
"location": {
1212
"npm": {
1313
"filePath": "dist/bundle.js",
14-
"packageName": "@metamask/lifecycle-hooks-example-snap",
14+
"packageName": "@metamask/ledger-example-snap",
1515
"registry": "https://registry.npmjs.org"
1616
}
1717
}
1818
},
1919
"initialPermissions": {
20-
"snap_dialog": {},
21-
"endowment:lifecycle-hooks": {}
20+
"endowment:devices": {}
2221
},
2322
"manifestVersion": "0.1"
2423
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type {
2+
Caveat,
3+
CaveatSpecificationConstraint,
4+
EndowmentGetterParams,
5+
PermissionConstraint,
6+
PermissionSpecificationBuilder,
7+
ValidPermissionSpecification,
8+
} from '@metamask/permission-controller';
9+
import { PermissionType, SubjectType } from '@metamask/permission-controller';
10+
import { rpcErrors } from '@metamask/rpc-errors';
11+
import type { DeviceSpecification } from '@metamask/snaps-utils';
12+
import {
13+
SnapCaveatType,
14+
isDeviceSpecificationArray,
15+
} from '@metamask/snaps-utils';
16+
import { hasProperty, isPlainObject, assert } from '@metamask/utils';
17+
18+
import { SnapEndowments } from './enum';
19+
20+
const permissionName = SnapEndowments.Devices;
21+
22+
type DevicesEndowmentSpecification = ValidPermissionSpecification<{
23+
permissionType: PermissionType.Endowment;
24+
targetName: typeof permissionName;
25+
endowmentGetter: (_options?: any) => null;
26+
allowedCaveats: [SnapCaveatType.DeviceIds];
27+
}>;
28+
29+
/**
30+
* The `endowment:devices` permission is intended to be used as a flag to
31+
* determine whether the Snap wants to access the devices API.
32+
*
33+
* @param _builderOptions - Optional specification builder options.
34+
* @returns The specification for the network endowment.
35+
*/
36+
const specificationBuilder: PermissionSpecificationBuilder<
37+
PermissionType.Endowment,
38+
any,
39+
DevicesEndowmentSpecification
40+
> = (_builderOptions?: any) => {
41+
return {
42+
permissionType: PermissionType.Endowment,
43+
targetName: permissionName,
44+
allowedCaveats: [SnapCaveatType.DeviceIds],
45+
endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null,
46+
subjectTypes: [SubjectType.Snap],
47+
};
48+
};
49+
50+
export const devicesEndowmentBuilder = Object.freeze({
51+
targetName: permissionName,
52+
specificationBuilder,
53+
} as const);
54+
55+
/**
56+
* Getter function to get the permitted device IDs from a permission
57+
* specification.
58+
*
59+
* This does basic validation of the caveat, but does not validate the type or
60+
* value of the namespaces object itself, as this is handled by the
61+
* `PermissionsController` when the permission is requested.
62+
*
63+
* @param permission - The permission to get the keyring namespaces from.
64+
* @returns The device IDs, or `null` if the permission does not have a
65+
* device IDs caveat.
66+
*/
67+
export function getPermittedDeviceIds(
68+
permission?: PermissionConstraint,
69+
): DeviceSpecification[] | null {
70+
if (!permission?.caveats) {
71+
return null;
72+
}
73+
74+
assert(permission.caveats.length === 1);
75+
assert(permission.caveats[0].type === SnapCaveatType.DeviceIds);
76+
77+
const caveat = permission.caveats[0] as Caveat<
78+
string,
79+
{ devices: DeviceSpecification[] }
80+
>;
81+
82+
return caveat.value?.devices ?? null;
83+
}
84+
85+
/**
86+
* Validate the cronjob specification values associated with a caveat.
87+
* This validates that the value is a non-empty array with valid device
88+
* specification objects.
89+
*
90+
* @param caveat - The caveat to validate.
91+
* @throws If the value is invalid.
92+
*/
93+
export function validateDeviceIdsCaveat(caveat: Caveat<string, any>) {
94+
if (!hasProperty(caveat, 'value') || !isPlainObject(caveat.value)) {
95+
throw rpcErrors.invalidParams({
96+
message: 'Expected a plain object.',
97+
});
98+
}
99+
100+
const { value } = caveat;
101+
102+
if (!hasProperty(value, 'devices') || !isPlainObject(value)) {
103+
throw rpcErrors.invalidParams({
104+
message: 'Expected a plain object.',
105+
});
106+
}
107+
108+
if (!isDeviceSpecificationArray(value.jobs)) {
109+
throw rpcErrors.invalidParams({
110+
message: 'Expected a valid device specification array.',
111+
});
112+
}
113+
}
114+
115+
/**
116+
* Caveat specification for the device IDs caveat.
117+
*/
118+
export const deviceIdsCaveatSpecifications: Record<
119+
SnapCaveatType.DeviceIds,
120+
CaveatSpecificationConstraint
121+
> = {
122+
[SnapCaveatType.DeviceIds]: Object.freeze({
123+
type: SnapCaveatType.DeviceIds,
124+
validator: (caveat) => validateDeviceIdsCaveat(caveat),
125+
}),
126+
};

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

+1
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+
Devices = 'endowment:devices',
1314
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import {
1212
cronjobEndowmentBuilder,
1313
getCronjobCaveatMapper,
1414
} from './cronjob';
15+
import {
16+
deviceIdsCaveatSpecifications,
17+
devicesEndowmentBuilder,
18+
} from './devices';
1519
import { ethereumProviderEndowmentBuilder } from './ethereum-provider';
1620
import { homePageEndowmentBuilder } from './home-page';
1721
import {
@@ -48,6 +52,7 @@ export const endowmentPermissionBuilders = {
4852
[transactionInsightEndowmentBuilder.targetName]:
4953
transactionInsightEndowmentBuilder,
5054
[cronjobEndowmentBuilder.targetName]: cronjobEndowmentBuilder,
55+
[devicesEndowmentBuilder.targetName]: devicesEndowmentBuilder,
5156
[ethereumProviderEndowmentBuilder.targetName]:
5257
ethereumProviderEndowmentBuilder,
5358
[rpcEndowmentBuilder.targetName]: rpcEndowmentBuilder,
@@ -62,6 +67,7 @@ export const endowmentPermissionBuilders = {
6267

6368
export const endowmentCaveatSpecifications = {
6469
...cronjobCaveatSpecifications,
70+
...deviceIdsCaveatSpecifications,
6571
...transactionInsightCaveatSpecifications,
6672
...rpcCaveatSpecifications,
6773
...nameLookupCaveatSpecifications,

packages/snaps-utils/src/caveats.ts

+5
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 the device IDs that can be interacted with.
59+
*/
60+
DeviceIds = 'deviceIds',
5661
}

packages/snaps-utils/src/devices.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { deviceId } from '@metamask/snaps-sdk';
2+
import type { Infer } from '@metamask/superstruct';
3+
import { array, is, object } from '@metamask/superstruct';
4+
5+
export const DeviceSpecificationStruct = object({
6+
/**
7+
* The device ID that the Snap has permission to access.
8+
*/
9+
deviceId: deviceId(),
10+
});
11+
12+
/**
13+
* A device specification, which is used as caveat value.
14+
*/
15+
export type DeviceSpecification = Infer<typeof DeviceSpecificationStruct>;
16+
17+
/**
18+
* Check if the given value is a {@link DeviceSpecification} object.
19+
*
20+
* @param value - The value to check.
21+
* @returns Whether the value is a {@link DeviceSpecification} object.
22+
*/
23+
export function isDeviceSpecification(
24+
value: unknown,
25+
): value is DeviceSpecification {
26+
return is(value, DeviceSpecificationStruct);
27+
}
28+
29+
export const DeviceSpecificationArrayStruct = object({
30+
devices: array(DeviceSpecificationStruct),
31+
});
32+
33+
/**
34+
* A device specification array, which is used as caveat value.
35+
*/
36+
export type DeviceSpecificationArray = Infer<
37+
typeof DeviceSpecificationArrayStruct
38+
>;
39+
40+
/**
41+
* Check if the given value is a {@link DeviceSpecificationArray} object.
42+
*
43+
* @param value - The value to check.
44+
* @returns Whether the value is a {@link DeviceSpecificationArray} object.
45+
*/
46+
export function isDeviceSpecificationArray(
47+
value: unknown,
48+
): value is DeviceSpecificationArray {
49+
return is(value, DeviceSpecificationArrayStruct);
50+
}

packages/snaps-utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from './currency';
1010
export * from './deep-clone';
1111
export * from './default-endowments';
1212
export * from './derivation-paths';
13+
export * from './devices';
1314
export * from './entropy';
1415
export * from './errors';
1516
export * from './handlers';

0 commit comments

Comments
 (0)