diff --git a/packages/snaps-rpc-methods/CHANGELOG.md b/packages/snaps-rpc-methods/CHANGELOG.md index e49415c029..3f3134c69e 100644 --- a/packages/snaps-rpc-methods/CHANGELOG.md +++ b/packages/snaps-rpc-methods/CHANGELOG.md @@ -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 diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index c66ae815df..528a9004ea 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -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, }, }, }); diff --git a/packages/snaps-rpc-methods/src/endowments/index.ts b/packages/snaps-rpc-methods/src/endowments/index.ts index f170b96bfb..ec162587cb 100644 --- a/packages/snaps-rpc-methods/src/endowments/index.ts +++ b/packages/snaps-rpc-methods/src/endowments/index.ts @@ -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'; diff --git a/packages/snaps-rpc-methods/src/endowments/keyring.test.ts b/packages/snaps-rpc-methods/src/endowments/keyring.test.ts index b48b525a2b..56fc01b291 100644 --- a/packages/snaps-rpc-methods/src/endowments/keyring.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/keyring.test.ts @@ -3,6 +3,7 @@ import { SnapCaveatType } from '@metamask/snaps-utils'; import { SnapEndowments } from './enum'; import { + getKeyringCaveatCapabilities, getKeyringCaveatMapper, getKeyringCaveatOrigins, keyringCaveatSpecifications, @@ -18,6 +19,7 @@ describe('endowment:keyring', () => { endowmentGetter: expect.any(Function), allowedCaveats: [ SnapCaveatType.KeyringOrigin, + SnapCaveatType.KeyringCapabilities, SnapCaveatType.MaxRequestTime, ], subjectTypes: [SubjectType.Snap], @@ -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(() => @@ -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({ @@ -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', () => { @@ -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?.( @@ -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(); + }); + }); }); diff --git a/packages/snaps-rpc-methods/src/endowments/keyring.ts b/packages/snaps-rpc-methods/src/endowments/keyring.ts index 3c763fb02e..4126f21307 100644 --- a/packages/snaps-rpc-methods/src/endowments/keyring.ts +++ b/packages/snaps-rpc-methods/src/endowments/keyring.ts @@ -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'; @@ -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], @@ -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. @@ -79,10 +88,37 @@ function validateCaveatOrigins(caveat: Caveat) { 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) { + 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. @@ -90,14 +126,22 @@ function validateCaveatOrigins(caveat: Caveat) { export function getKeyringCaveatMapper( value: Json, ): Pick { - 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 }; } /** @@ -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 | 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) => validateCaveatOrigins(caveat), }), + [SnapCaveatType.KeyringCapabilities]: Object.freeze({ + type: SnapCaveatType.KeyringCapabilities, + validator: (caveat: Caveat) => + validateCaveatCapabilities(caveat), + }), }; diff --git a/packages/snaps-rpc-methods/src/permissions.test.ts b/packages/snaps-rpc-methods/src/permissions.test.ts index bf7be98f19..87b4120ad4 100644 --- a/packages/snaps-rpc-methods/src/permissions.test.ts +++ b/packages/snaps-rpc-methods/src/permissions.test.ts @@ -46,6 +46,7 @@ describe('buildSnapEndowmentSpecifications', () => { "endowment:keyring": { "allowedCaveats": [ "keyringOrigin", + "keyringCapabilities", "maxRequestTime", ], "endowmentGetter": [Function], diff --git a/packages/snaps-sdk/src/types/permissions.ts b/packages/snaps-sdk/src/types/permissions.ts index 623c065ea3..9e8fcfb843 100644 --- a/packages/snaps-sdk/src/types/permissions.ts +++ b/packages/snaps-sdk/src/types/permissions.ts @@ -74,6 +74,62 @@ export type RequestedSnap = { version?: string; }; +/** + * Supported encoding formats for private keys. + * + * Mirrors `PrivateKeyEncoding` from `@metamask/keyring-api`. + * Keep in sync with `PrivateKeyEncoding` in `@metamask/keyring-api`. + */ +type PrivateKeyEncoding = 'hexadecimal' | 'base58'; + +/** + * Supported account types for keyring accounts. + * + * Mirrors `KeyringAccountType` from `@metamask/keyring-api`. + * Keep in sync with `KeyringAccountType` in `@metamask/keyring-api`. + */ +type KeyringAccountType = + | 'eip155:eoa' + | 'eip155:erc4337' + | 'bip122:p2pkh' + | 'bip122:p2sh' + | 'bip122:p2wpkh' + | 'bip122:p2tr' + | 'solana:data-account' + | 'tron:eoa' + | 'entropy:account'; + +/** + * Capabilities object supported by a keyring Snap. + * + * Mirrors the shape validated by `KeyringCapabilitiesStruct` in + * `@metamask/keyring-api`. Keep in sync with that struct. + * + * Runtime validation uses the struct in `@metamask/snaps-utils`; this type + * exists purely for the `InitialPermissions` type signature. + */ +type Capabilities = { + scopes: CaipChainId[]; + bip44?: { + derivePath?: boolean; + deriveIndex?: boolean; + deriveIndexRange?: boolean; + discover?: boolean; + }; + privateKey?: { + importFormats?: { + encoding: PrivateKeyEncoding; + type?: KeyringAccountType; + }[]; + exportFormats?: { + encoding: PrivateKeyEncoding; + }[]; + }; + custom?: { + createAccounts?: boolean; + }; +}; + export type InitialPermissions = Partial<{ 'endowment:cronjob': { jobs?: Cronjob[]; @@ -82,6 +138,7 @@ export type InitialPermissions = Partial<{ 'endowment:ethereum-provider': EmptyObject; 'endowment:keyring': { allowedOrigins?: string[]; + capabilities?: Capabilities; maxRequestTime?: number; }; 'endowment:lifecycle-hooks'?: { diff --git a/packages/snaps-simulation/src/methods/specifications.test.ts b/packages/snaps-simulation/src/methods/specifications.test.ts index adf3c9ff8b..96e1742e3f 100644 --- a/packages/snaps-simulation/src/methods/specifications.test.ts +++ b/packages/snaps-simulation/src/methods/specifications.test.ts @@ -97,6 +97,7 @@ describe('getPermissionSpecifications', () => { "endowment:keyring": { "allowedCaveats": [ "keyringOrigin", + "keyringCapabilities", "maxRequestTime", ], "endowmentGetter": [Function], diff --git a/packages/snaps-utils/CHANGELOG.md b/packages/snaps-utils/CHANGELOG.md index d1cd9bdc05..3046b27a28 100644 --- a/packages/snaps-utils/CHANGELOG.md +++ b/packages/snaps-utils/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `KeyringCapabilities` type and `KeyringCapabilitiesStruct` / `assertIsKeyringCapabilities` exports for the `endowment:keyring` capabilities caveat ([#3903](https://github.com/MetaMask/snaps/pull/3903)) + ## [12.1.1] ### Changed diff --git a/packages/snaps-utils/src/caveats.ts b/packages/snaps-utils/src/caveats.ts index 8272be9bc8..b3c456f1f0 100644 --- a/packages/snaps-utils/src/caveats.ts +++ b/packages/snaps-utils/src/caveats.ts @@ -34,6 +34,11 @@ export enum SnapCaveatType { */ KeyringOrigin = 'keyringOrigin', + /** + * Caveat specifying the capabilities supported by a keyring Snap, used by `endowment:keyring`. + */ + KeyringCapabilities = 'keyringCapabilities', + /** * Caveat specifying the snap IDs that can be interacted with. */ diff --git a/packages/snaps-utils/src/json-rpc.test.ts b/packages/snaps-utils/src/json-rpc.test.ts index 420e20ae96..84cd8ba006 100644 --- a/packages/snaps-utils/src/json-rpc.test.ts +++ b/packages/snaps-utils/src/json-rpc.test.ts @@ -3,6 +3,7 @@ import { SubjectType } from '@metamask/permission-controller'; import type { RpcOrigins } from './json-rpc'; import { assertIsJsonRpcSuccess, + assertIsKeyringCapabilities, assertIsKeyringOrigins, assertIsRpcOrigins, isOriginAllowed, @@ -110,6 +111,142 @@ describe('assertIsKeyringOrigin', () => { }); }); +describe('assertIsKeyringCapabilities', () => { + it.each([ + {}, + { capabilities: undefined }, + { + capabilities: { + scopes: ['eip155:1'], + }, + }, + { + capabilities: { + scopes: ['bip122:000000000019d6689c085ae165831e93'], + bip44: { derivePath: true }, + }, + }, + { + capabilities: { + scopes: ['eip155:1'], + bip44: { + derivePath: true, + deriveIndex: true, + deriveIndexRange: true, + discover: true, + }, + }, + }, + { + capabilities: { + scopes: ['bip122:000000000019d6689c085ae165831e93'], + privateKey: { + importFormats: [{ encoding: 'base58', type: 'bip122:p2pkh' }], + exportFormats: [{ encoding: 'hexadecimal' }], + }, + }, + }, + { + capabilities: { + scopes: ['eip155:1'], + custom: { createAccounts: true }, + }, + }, + { + capabilities: { + scopes: ['eip155:1'], + bip44: {}, + privateKey: {}, + custom: {}, + }, + }, + ])('does not throw for %p', (value) => { + expect(() => assertIsKeyringCapabilities(value)).not.toThrow(); + }); + + it.each([true, false, null, undefined, 0, 1, '', 'foo', ['foo']])( + 'throws for %p', + (value) => { + expect(() => assertIsKeyringCapabilities(value)).toThrow( + 'Invalid keyring capabilities', + ); + }, + ); + + it('throws if scopes is empty', () => { + expect(() => + assertIsKeyringCapabilities({ + capabilities: { scopes: [] }, + }), + ).toThrow('Invalid keyring capabilities'); + }); + + it('throws if scopes contains an invalid CAIP chain ID', () => { + expect(() => + assertIsKeyringCapabilities({ + capabilities: { scopes: ['not-a-caip-id'] }, + }), + ).toThrow('Invalid keyring capabilities'); + }); + + it('throws if bip44 fields have non-boolean values', () => { + expect(() => + assertIsKeyringCapabilities({ + capabilities: { + scopes: ['eip155:1'], + bip44: { derivePath: 'yes' }, + }, + }), + ).toThrow('Invalid keyring capabilities'); + }); + + it('throws if privateKey encoding is invalid', () => { + expect(() => + assertIsKeyringCapabilities({ + capabilities: { + scopes: ['eip155:1'], + privateKey: { + importFormats: [{ encoding: 'invalid' }], + }, + }, + }), + ).toThrow('Invalid keyring capabilities'); + }); + + it('throws if importFormats type is invalid', () => { + expect(() => + assertIsKeyringCapabilities({ + capabilities: { + scopes: ['eip155:1'], + privateKey: { + importFormats: [{ encoding: 'base58', type: 'invalid:type' }], + }, + }, + }), + ).toThrow('Invalid keyring capabilities'); + }); + + it('throws if capabilities has unexpected fields', () => { + expect(() => + assertIsKeyringCapabilities({ + capabilities: { + scopes: ['eip155:1'], + unknownField: true, + }, + }), + ).toThrow('Invalid keyring capabilities'); + }); + + it('uses the provided error wrapper', () => { + const ErrorWrapper = ({ message }: { message: string }) => + new Error(message); + + expect(() => assertIsKeyringCapabilities(true, ErrorWrapper)).toThrow( + 'Invalid keyring capabilities', + ); + }); +}); + describe('isOriginAllowed', () => { it('returns `true` if all origins are allowed', () => { const origins: RpcOrigins = { diff --git a/packages/snaps-utils/src/json-rpc.ts b/packages/snaps-utils/src/json-rpc.ts index eeda77a8f7..e472c5afae 100644 --- a/packages/snaps-utils/src/json-rpc.ts +++ b/packages/snaps-utils/src/json-rpc.ts @@ -3,6 +3,8 @@ import type { Infer } from '@metamask/superstruct'; import { array, boolean, + enums, + nonempty, object, optional, refine, @@ -14,6 +16,7 @@ import type { } from '@metamask/utils'; import { assertStruct, + CaipChainIdStruct, isJsonRpcFailure, isJsonRpcSuccess, } from '@metamask/utils'; @@ -99,6 +102,98 @@ export function assertIsKeyringOrigins( ); } +/** + * Supported encoding formats for private keys. + * + * Mirrors `PrivateKeyEncoding` from `@metamask/keyring-api` to avoid pulling + * in that package's Node.js-only transitive dependencies into browser bundles. + */ +const PrivateKeyEncodingStruct = enums(['hexadecimal', 'base58']); + +/** + * Supported account types for keyring accounts. + * + * Mirrors `KeyringAccountTypeStruct` from `@metamask/keyring-api`. + */ +const KeyringAccountTypeStruct = enums([ + 'eip155:eoa', + 'eip155:erc4337', + 'bip122:p2pkh', + 'bip122:p2sh', + 'bip122:p2wpkh', + 'bip122:p2tr', + 'solana:data-account', + 'tron:eoa', + 'entropy:account', +]); + +/** + * Struct for the capabilities object supported by a keyring Snap. + * + * Mirrors `KeyringCapabilitiesStruct` from `@metamask/keyring-api` to avoid + * pulling in that package's Node.js-only transitive dependencies into browser + * bundles (via `@ethereumjs/util` → `micro-ftch`). + * + * Keep in sync with `KeyringCapabilitiesStruct` in `@metamask/keyring-api`. + */ +const CapabilitiesStruct = object({ + scopes: nonempty(array(CaipChainIdStruct)), + bip44: optional( + object({ + derivePath: optional(boolean()), + deriveIndex: optional(boolean()), + deriveIndexRange: optional(boolean()), + discover: optional(boolean()), + }), + ), + privateKey: optional( + object({ + importFormats: optional( + array( + object({ + encoding: PrivateKeyEncodingStruct, + type: optional(KeyringAccountTypeStruct), + }), + ), + ), + exportFormats: optional( + array(object({ encoding: PrivateKeyEncodingStruct })), + ), + }), + ), + custom: optional( + object({ + createAccounts: optional(boolean()), + }), + ), +}); + +export const KeyringCapabilitiesStruct = object({ + capabilities: optional(CapabilitiesStruct), +}); + +export type KeyringCapabilities = Infer; + +/** + * Assert that the given value is a valid {@link KeyringCapabilities} object. + * + * @param value - The value to assert. + * @param ErrorWrapper - An optional error wrapper to use. Defaults to + * {@link AssertionError}. + * @throws If the value is not a valid {@link KeyringCapabilities} object. + */ +export function assertIsKeyringCapabilities( + value: unknown, + ErrorWrapper?: AssertionErrorConstructor, +): asserts value is KeyringCapabilities { + assertStruct( + value, + KeyringCapabilitiesStruct, + 'Invalid keyring capabilities', + ErrorWrapper, + ); +} + /** * Create regular expression for matching against an origin while allowing wildcards. * diff --git a/packages/snaps-utils/src/manifest/validation.ts b/packages/snaps-utils/src/manifest/validation.ts index 43279d0b83..ae140545e3 100644 --- a/packages/snaps-utils/src/manifest/validation.ts +++ b/packages/snaps-utils/src/manifest/validation.ts @@ -33,7 +33,11 @@ import { import { isDerivationPathEqual } from '../array'; import { CronjobSpecificationArrayStruct } from '../cronjob'; import { SIP_6_MAGIC_VALUE, STATE_ENCRYPTION_MAGIC_VALUE } from '../entropy'; -import { KeyringOriginsStruct, RpcOriginsStruct } from '../json-rpc'; +import { + KeyringCapabilitiesStruct, + KeyringOriginsStruct, + RpcOriginsStruct, +} from '../json-rpc'; import { SnapIdStruct } from '../snaps'; import { mergeStructs, type InferMatching } from '../structs'; import { NameStruct, NpmSnapFileNames, uri } from '../types'; @@ -216,7 +220,11 @@ export const PermissionsStruct: Describe = type({ ), 'endowment:ethereum-provider': optional(EmptyObjectStruct), 'endowment:keyring': optional( - mergeStructs(HandlerCaveatsStruct, KeyringOriginsStruct), + mergeStructs( + HandlerCaveatsStruct, + KeyringOriginsStruct, + KeyringCapabilitiesStruct, + ), ), 'endowment:protocol': optional( mergeStructs(