Skip to content

Commit

Permalink
Add device IDs endowment
Browse files Browse the repository at this point in the history
  • Loading branch information
Mrtenz committed Nov 1, 2024
1 parent 17b87e5 commit 911b279
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 5 deletions.
9 changes: 4 additions & 5 deletions packages/examples/packages/ledger/snap.manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "2.1.3",
"description": "MetaMask example snap demonstrating the use of the `onInstall` and `onUpdate` lifecycle hooks.",
"proposedName": "Lifecycle Hooks Example Snap",
"description": "MetaMask example Snap demonstrating how to communicate with a Ledger hardware wallet.",
"proposedName": "Ledger Example Snap",
"repository": {
"type": "git",
"url": "https://github.com/MetaMask/snaps.git"
Expand All @@ -11,14 +11,13 @@
"location": {
"npm": {
"filePath": "dist/bundle.js",
"packageName": "@metamask/lifecycle-hooks-example-snap",
"packageName": "@metamask/ledger-example-snap",
"registry": "https://registry.npmjs.org"
}
}
},
"initialPermissions": {
"snap_dialog": {},
"endowment:lifecycle-hooks": {}
"endowment:devices": {}
},
"manifestVersion": "0.1"
}
126 changes: 126 additions & 0 deletions packages/snaps-rpc-methods/src/endowments/devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type {
Caveat,
CaveatSpecificationConstraint,
EndowmentGetterParams,
PermissionConstraint,
PermissionSpecificationBuilder,
ValidPermissionSpecification,
} from '@metamask/permission-controller';
import { PermissionType, SubjectType } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import type { DeviceSpecification } from '@metamask/snaps-utils';
import {
SnapCaveatType,
isDeviceSpecificationArray,
} from '@metamask/snaps-utils';
import { hasProperty, isPlainObject, assert } from '@metamask/utils';

import { SnapEndowments } from './enum';

const permissionName = SnapEndowments.Devices;

type DevicesEndowmentSpecification = ValidPermissionSpecification<{
permissionType: PermissionType.Endowment;
targetName: typeof permissionName;
endowmentGetter: (_options?: any) => null;
allowedCaveats: [SnapCaveatType.DeviceIds];
}>;

/**
* The `endowment:devices` permission is intended to be used as a flag to
* determine whether the Snap wants to access the devices API.
*
* @param _builderOptions - Optional specification builder options.
* @returns The specification for the network endowment.
*/
const specificationBuilder: PermissionSpecificationBuilder<
PermissionType.Endowment,
any,
DevicesEndowmentSpecification
> = (_builderOptions?: any) => {
return {
permissionType: PermissionType.Endowment,
targetName: permissionName,
allowedCaveats: [SnapCaveatType.DeviceIds],
endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null,
subjectTypes: [SubjectType.Snap],
};
};

export const devicesEndowmentBuilder = Object.freeze({
targetName: permissionName,
specificationBuilder,
} as const);

/**
* Getter function to get the permitted device IDs from a permission
* specification.
*
* This does basic validation of the caveat, but does not validate the type or
* value of the namespaces object itself, as this is handled by the
* `PermissionsController` when the permission is requested.
*
* @param permission - The permission to get the keyring namespaces from.
* @returns The device IDs, or `null` if the permission does not have a
* device IDs caveat.
*/
export function getPermittedDeviceIds(
permission?: PermissionConstraint,
): DeviceSpecification[] | null {
if (!permission?.caveats) {
return null;
}

assert(permission.caveats.length === 1);
assert(permission.caveats[0].type === SnapCaveatType.DeviceIds);

const caveat = permission.caveats[0] as Caveat<
string,
{ devices: DeviceSpecification[] }
>;

return caveat.value?.devices ?? null;
}

/**
* Validate the cronjob specification values associated with a caveat.
* This validates that the value is a non-empty array with valid device
* specification objects.
*
* @param caveat - The caveat to validate.
* @throws If the value is invalid.
*/
export function validateDeviceIdsCaveat(caveat: Caveat<string, any>) {
if (!hasProperty(caveat, 'value') || !isPlainObject(caveat.value)) {
throw rpcErrors.invalidParams({
message: 'Expected a plain object.',
});
}

const { value } = caveat;

if (!hasProperty(value, 'devices') || !isPlainObject(value)) {
throw rpcErrors.invalidParams({
message: 'Expected a plain object.',
});
}

if (!isDeviceSpecificationArray(value.jobs)) {
throw rpcErrors.invalidParams({
message: 'Expected a valid device specification array.',
});
}
}

/**
* Caveat specification for the device IDs caveat.
*/
export const deviceIdsCaveatSpecifications: Record<
SnapCaveatType.DeviceIds,
CaveatSpecificationConstraint
> = {
[SnapCaveatType.DeviceIds]: Object.freeze({
type: SnapCaveatType.DeviceIds,
validator: (caveat) => validateDeviceIdsCaveat(caveat),
}),
};
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',
Devices = 'endowment:devices',
}
6 changes: 6 additions & 0 deletions packages/snaps-rpc-methods/src/endowments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
cronjobEndowmentBuilder,
getCronjobCaveatMapper,
} from './cronjob';
import {
deviceIdsCaveatSpecifications,
devicesEndowmentBuilder,
} from './devices';
import { ethereumProviderEndowmentBuilder } from './ethereum-provider';
import { homePageEndowmentBuilder } from './home-page';
import {
Expand Down Expand Up @@ -48,6 +52,7 @@ export const endowmentPermissionBuilders = {
[transactionInsightEndowmentBuilder.targetName]:
transactionInsightEndowmentBuilder,
[cronjobEndowmentBuilder.targetName]: cronjobEndowmentBuilder,
[devicesEndowmentBuilder.targetName]: devicesEndowmentBuilder,
[ethereumProviderEndowmentBuilder.targetName]:
ethereumProviderEndowmentBuilder,
[rpcEndowmentBuilder.targetName]: rpcEndowmentBuilder,
Expand All @@ -62,6 +67,7 @@ export const endowmentPermissionBuilders = {

export const endowmentCaveatSpecifications = {
...cronjobCaveatSpecifications,
...deviceIdsCaveatSpecifications,
...transactionInsightCaveatSpecifications,
...rpcCaveatSpecifications,
...nameLookupCaveatSpecifications,
Expand Down
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 the device IDs that can be interacted with.
*/
DeviceIds = 'deviceIds',
}
50 changes: 50 additions & 0 deletions packages/snaps-utils/src/devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { deviceId } from '@metamask/snaps-sdk';
import type { Infer } from '@metamask/superstruct';
import { array, is, object } from '@metamask/superstruct';

export const DeviceSpecificationStruct = object({
/**
* The device ID that the Snap has permission to access.
*/
deviceId: deviceId(),
});

/**
* A device specification, which is used as caveat value.
*/
export type DeviceSpecification = Infer<typeof DeviceSpecificationStruct>;

/**
* Check if the given value is a {@link DeviceSpecification} object.
*
* @param value - The value to check.
* @returns Whether the value is a {@link DeviceSpecification} object.
*/
export function isDeviceSpecification(
value: unknown,
): value is DeviceSpecification {
return is(value, DeviceSpecificationStruct);
}

export const DeviceSpecificationArrayStruct = object({
devices: array(DeviceSpecificationStruct),
});

/**
* A device specification array, which is used as caveat value.
*/
export type DeviceSpecificationArray = Infer<
typeof DeviceSpecificationArrayStruct
>;

/**
* Check if the given value is a {@link DeviceSpecificationArray} object.
*
* @param value - The value to check.
* @returns Whether the value is a {@link DeviceSpecificationArray} object.
*/
export function isDeviceSpecificationArray(
value: unknown,
): value is DeviceSpecificationArray {
return is(value, DeviceSpecificationArrayStruct);
}
1 change: 1 addition & 0 deletions packages/snaps-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './currency';
export * from './deep-clone';
export * from './default-endowments';
export * from './derivation-paths';
export * from './devices';
export * from './entropy';
export * from './errors';
export * from './handlers';
Expand Down

0 comments on commit 911b279

Please sign in to comment.