Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/snaps-rpc-methods/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `capabilities` caveat support to `endowment:keyring` ([#3903](https://github.com/MetaMask/snaps/pull/3903))
- Snap manifests can now declare a `capabilities` object under `endowment:keyring` the capabilities supported by the keyring.
- New `getKeyringCaveatCapabilities` function to retrieve the capabilities caveat value from a permission.

## [15.0.0]

### Changed
Expand Down
8 changes: 4 additions & 4 deletions packages/snaps-rpc-methods/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ module.exports = deepmerge(baseConfig, {
],
coverageThreshold: {
global: {
branches: 96.57,
functions: 99.19,
lines: 99.05,
statements: 98.77,
branches: 96.62,
functions: 99.2,
lines: 99.06,
statements: 98.78,
},
},
});
5 changes: 4 additions & 1 deletion packages/snaps-rpc-methods/src/endowments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ export { getRpcCaveatOrigins } from './rpc';
export { getSignatureOriginCaveat } from './signature-insight';
export { getTransactionOriginCaveat } from './transaction-insight';
export { getChainIdsCaveat, getLookupMatchersCaveat } from './name-lookup';
export { getKeyringCaveatOrigins } from './keyring';
export {
getKeyringCaveatOrigins,
getKeyringCaveatCapabilities,
} from './keyring';
export { getMaxRequestTimeCaveat } from './caveats';
export { getCronjobCaveatJobs } from './cronjob';
export { getProtocolCaveatScopes } from './protocol';
149 changes: 146 additions & 3 deletions packages/snaps-rpc-methods/src/endowments/keyring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils';

import { SnapEndowments } from './enum';
import {
getKeyringCaveatCapabilities,
getKeyringCaveatMapper,
getKeyringCaveatOrigins,
keyringCaveatSpecifications,
Expand All @@ -18,6 +19,7 @@ describe('endowment:keyring', () => {
endowmentGetter: expect.any(Function),
allowedCaveats: [
SnapCaveatType.KeyringOrigin,
SnapCaveatType.KeyringCapabilities,
SnapCaveatType.MaxRequestTime,
],
subjectTypes: [SubjectType.Snap],
Expand All @@ -44,7 +46,7 @@ describe('endowment:keyring', () => {
caveats: [{ type: 'foo', value: 'bar' }],
}),
).toThrow(
'Expected the following caveats: "keyringOrigin", "maxRequestTime", received "foo".',
'Expected the following caveats: "keyringOrigin", "keyringCapabilities", "maxRequestTime", received "foo".',
);

expect(() =>
Expand All @@ -61,7 +63,7 @@ describe('endowment:keyring', () => {
});

describe('getKeyringCaveatMapper', () => {
it('maps a value to a caveat', () => {
it('maps a value to a caveat without capabilities', () => {
expect(
getKeyringCaveatMapper({ allowedOrigins: ['foo.com'] }),
).toStrictEqual({
Expand All @@ -73,6 +75,36 @@ describe('getKeyringCaveatMapper', () => {
],
});
});

it('maps a value to caveats including capabilities', () => {
expect(
getKeyringCaveatMapper({
allowedOrigins: ['foo.com'],
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
}),
).toStrictEqual({
caveats: [
{
type: SnapCaveatType.KeyringOrigin,
value: {
allowedOrigins: ['foo.com'],
},
},
{
type: SnapCaveatType.KeyringCapabilities,
value: {
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
},
},
],
});
});
});

describe('getKeyringCaveatOrigins', () => {
Expand Down Expand Up @@ -118,8 +150,52 @@ describe('getKeyringCaveatOrigins', () => {
});
});

describe('getKeyringCaveatCapabilities', () => {
it('returns the capabilities from the caveat', () => {
expect(
// @ts-expect-error Missing other required permission types.
getKeyringCaveatCapabilities({
caveats: [
{
type: SnapCaveatType.KeyringCapabilities,
value: {
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
},
},
],
}),
).toStrictEqual({
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: { derivePath: true },
},
});
});

it('returns null when the capabilities caveat is absent', () => {
expect(
// @ts-expect-error Missing other required permission types.
getKeyringCaveatCapabilities({
caveats: [
{
type: SnapCaveatType.KeyringOrigin,
value: { allowedOrigins: ['foo.com'] },
},
],
}),
).toBeNull();
});

it('returns null when permission is undefined', () => {
expect(getKeyringCaveatCapabilities(undefined)).toBeNull();
});
});

describe('keyringCaveatSpecifications', () => {
describe('validator', () => {
describe('keyringOrigin validator', () => {
it('throws if the caveat values are invalid', () => {
expect(() =>
keyringCaveatSpecifications[SnapCaveatType.KeyringOrigin].validator?.(
Expand All @@ -142,4 +218,71 @@ describe('keyringCaveatSpecifications', () => {
);
});
});

describe('keyringCapabilities validator', () => {
it('throws if the caveat value is not a plain object', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.(
// @ts-expect-error Missing value type.
{
type: SnapCaveatType.KeyringCapabilities,
},
),
).toThrow('Invalid keyring capabilities: Expected a plain object.');
});

it('throws if the caveat value has invalid fields', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.({
type: SnapCaveatType.KeyringCapabilities,
value: { foo: 'bar' },
}),
).toThrow('Invalid keyring capabilities');
});

it('throws if scopes is missing', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.({
type: SnapCaveatType.KeyringCapabilities,
value: { capabilities: { bip44: { derivePath: true } } },
}),
).toThrow('Invalid keyring capabilities');
});

it('does not throw for a valid capabilities value', () => {
expect(() =>
keyringCaveatSpecifications[
SnapCaveatType.KeyringCapabilities
].validator?.({
type: SnapCaveatType.KeyringCapabilities,
value: {
capabilities: {
scopes: ['bip122:000000000019d6689c085ae165831e93'],
bip44: {
derivePath: true,
deriveIndex: true,
deriveIndexRange: true,
discover: true,
},
privateKey: {
importFormats: [
{
encoding: 'base58',
type: 'bip122:p2pkh',
},
],
exportFormats: [{ encoding: 'base58' }],
},
},
},
}),
).not.toThrow();
});
});
});
97 changes: 82 additions & 15 deletions packages/snaps-rpc-methods/src/endowments/keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import type {
} from '@metamask/permission-controller';
import { PermissionType, SubjectType } from '@metamask/permission-controller';
import { rpcErrors } from '@metamask/rpc-errors';
import type { KeyringOrigins } from '@metamask/snaps-utils';
import { assertIsKeyringOrigins, SnapCaveatType } from '@metamask/snaps-utils';
import type {
KeyringCapabilities,
KeyringOrigins,
} from '@metamask/snaps-utils';
import {
assertIsKeyringCapabilities,
assertIsKeyringOrigins,
SnapCaveatType,
} from '@metamask/snaps-utils';
import type { Json, NonEmptyArray } from '@metamask/utils';
import { assert, hasProperty, isPlainObject } from '@metamask/utils';

Expand Down Expand Up @@ -45,11 +52,13 @@ const specificationBuilder: PermissionSpecificationBuilder<
targetName: permissionName,
allowedCaveats: [
SnapCaveatType.KeyringOrigin,
SnapCaveatType.KeyringCapabilities,
SnapCaveatType.MaxRequestTime,
],
endowmentGetter: (_getterOptions?: EndowmentGetterParams) => null,
validator: createGenericPermissionValidator([
{ type: SnapCaveatType.KeyringOrigin },
{ type: SnapCaveatType.KeyringCapabilities, optional: true },
{ type: SnapCaveatType.MaxRequestTime, optional: true },
]),
subjectTypes: [SubjectType.Snap],
Expand All @@ -62,8 +71,8 @@ export const keyringEndowmentBuilder = Object.freeze({
} as const);

/**
* Validate the value of a caveat. This does not validate the type of the
* caveat itself, only the value of the caveat.
* Validate the value of a keyring origins caveat. This does not validate the
* type of the caveat itself, only the value of the caveat.
*
* @param caveat - The caveat to validate.
* @throws If the caveat value is invalid.
Expand All @@ -79,25 +88,60 @@ function validateCaveatOrigins(caveat: Caveat<string, any>) {
assertIsKeyringOrigins(value, rpcErrors.invalidParams);
}

/**
* Validate the value of a keyring capabilities caveat. This does not validate
* the type of the caveat itself, only the value of the caveat.
*
* @param caveat - The caveat to validate.
* @throws If the caveat value is invalid.
*/
function validateCaveatCapabilities(caveat: Caveat<string, any>) {
if (!hasProperty(caveat, 'value') || !isPlainObject(caveat.value)) {
throw rpcErrors.invalidParams({
message: 'Invalid keyring capabilities: Expected a plain object.',
});
}

const { value } = caveat;
assertIsKeyringCapabilities(value, rpcErrors.invalidParams);
}

/**
* Expected shape of the keyring endowment value from `initialPermissions`.
* The mapper assumes this shape for typing only; it does not validate.
* Invalid data is rejected when the permission is requested (see validator).
*/
type KeyringCaveatMapperInput = KeyringOrigins & KeyringCapabilities;

/**
* 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.
* This function only maps: it does not validate. The permission validator
* runs when the permission is requested and will reject invalid caveats.
* We assume the manifest supplies a KeyringCaveatMapperInput-shaped value;
* the public signature accepts Json to satisfy CaveatMapperFunction.
*
* @param value - The raw value from the `initialPermissions`.
* @returns The caveat specification.
*/
export function getKeyringCaveatMapper(
value: Json,
): Pick<PermissionConstraint, 'caveats'> {
return {
caveats: [
{
type: SnapCaveatType.KeyringOrigin,
value,
},
],
};
const input = value as KeyringCaveatMapperInput;
const caveats: PermissionConstraint['caveats'] = [
{
type: SnapCaveatType.KeyringOrigin,
value: { allowedOrigins: input.allowedOrigins } as Json,
},
];

if (hasProperty(input, 'capabilities')) {
caveats.push({
type: SnapCaveatType.KeyringCapabilities,
value: { capabilities: input.capabilities } as Json,
});
}

return { caveats };
}

/**
Expand All @@ -120,12 +164,35 @@ export function getKeyringCaveatOrigins(
return caveat.value;
}

/**
* Getter function to get the {@link KeyringCapabilities} caveat value from a
* permission.
*
* @param permission - The permission to get the caveat value from.
* @returns The caveat value, or `null` if the permission does not have a
* {@link KeyringCapabilities} caveat.
*/
export function getKeyringCaveatCapabilities(
permission?: PermissionConstraint,
): KeyringCapabilities | null {
const caveat = permission?.caveats?.find(
(permCaveat) => permCaveat.type === SnapCaveatType.KeyringCapabilities,
) as Caveat<string, KeyringCapabilities> | undefined;

return caveat?.value ?? null;
}

export const keyringCaveatSpecifications: Record<
SnapCaveatType.KeyringOrigin,
SnapCaveatType.KeyringOrigin | SnapCaveatType.KeyringCapabilities,
CaveatSpecificationConstraint
> = {
[SnapCaveatType.KeyringOrigin]: Object.freeze({
type: SnapCaveatType.KeyringOrigin,
validator: (caveat: Caveat<string, any>) => validateCaveatOrigins(caveat),
}),
[SnapCaveatType.KeyringCapabilities]: Object.freeze({
type: SnapCaveatType.KeyringCapabilities,
validator: (caveat: Caveat<string, any>) =>
validateCaveatCapabilities(caveat),
}),
};
1 change: 1 addition & 0 deletions packages/snaps-rpc-methods/src/permissions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('buildSnapEndowmentSpecifications', () => {
"endowment:keyring": {
"allowedCaveats": [
"keyringOrigin",
"keyringCapabilities",
"maxRequestTime",
],
"endowmentGetter": [Function],
Expand Down
Loading
Loading