From c0f6e6dc31c5494af18ffa2f6a5a92c716d96223 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 19 Mar 2026 02:32:15 -0400 Subject: [PATCH 01/12] feature: add capabilities to keyring endowment --- packages/snaps-rpc-methods/CHANGELOG.md | 6 + packages/snaps-rpc-methods/jest.config.js | 8 +- .../src/endowments/keyring.test.ts | 131 +++++++++++++++- .../src/endowments/keyring.ts | 100 ++++++++++-- .../snaps-rpc-methods/src/permissions.test.ts | 1 + packages/snaps-utils/CHANGELOG.md | 5 + packages/snaps-utils/package.json | 1 + packages/snaps-utils/src/caveats.ts | 5 + packages/snaps-utils/src/json-rpc.ts | 28 ++++ .../snaps-utils/src/manifest/validation.ts | 12 +- yarn.lock | 145 +++++++++++++++++- 11 files changed, 416 insertions(+), 26 deletions(-) diff --git a/packages/snaps-rpc-methods/CHANGELOG.md b/packages/snaps-rpc-methods/CHANGELOG.md index e49415c029..e8d20eb6c4 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` ([#XXXX](https://github.com/MetaMask/snaps/pull/XXXX)) + - 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..bac60d40d6 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.61, + functions: 99.2, + lines: 99.06, + statements: 98.78, }, }, }); diff --git a/packages/snaps-rpc-methods/src/endowments/keyring.test.ts b/packages/snaps-rpc-methods/src/endowments/keyring.test.ts index b48b525a2b..c321b00849 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,34 @@ 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 }, + }, + }); + }); +}); + describe('keyringCaveatSpecifications', () => { - describe('validator', () => { + describe('keyringOrigin validator', () => { it('throws if the caveat values are invalid', () => { expect(() => keyringCaveatSpecifications[SnapCaveatType.KeyringOrigin].validator?.( @@ -142,4 +200,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..9eb7c6e7f7 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,39 @@ 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 & { + capabilities?: 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 +128,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 +166,36 @@ 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; + + assert(caveat); + return caveat.value; +} + 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-utils/CHANGELOG.md b/packages/snaps-utils/CHANGELOG.md index d1cd9bdc05..484363ff9c 100644 --- a/packages/snaps-utils/CHANGELOG.md +++ b/packages/snaps-utils/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add capabilities caveat for `endowment:keyring` ([#XXXX](https://github.com/MetaMask/snaps/pull/XXXX)) + - `KeyringCapabilities` and `KeyringCapabilitiesStruct` from `@metamask/keyring-api` are now re-exported from this package as well. + ## [12.1.1] ### Changed diff --git a/packages/snaps-utils/package.json b/packages/snaps-utils/package.json index 60625d249e..3dff80ab1b 100644 --- a/packages/snaps-utils/package.json +++ b/packages/snaps-utils/package.json @@ -81,6 +81,7 @@ "@babel/core": "^7.23.2", "@babel/types": "^7.23.0", "@metamask/key-tree": "^10.1.1", + "@metamask/keyring-api": "^21.5.0", "@metamask/messenger": "^0.3.0", "@metamask/permission-controller": "^12.2.0", "@metamask/rpc-errors": "^7.0.3", 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.ts b/packages/snaps-utils/src/json-rpc.ts index eeda77a8f7..610b71f8e5 100644 --- a/packages/snaps-utils/src/json-rpc.ts +++ b/packages/snaps-utils/src/json-rpc.ts @@ -1,3 +1,5 @@ +import { KeyringCapabilitiesStruct as CapabilitiesStruct } from '@metamask/keyring-api'; +import type { KeyringCapabilities as Capabilities } from '@metamask/keyring-api'; import { SubjectType } from '@metamask/permission-controller'; import type { Infer } from '@metamask/superstruct'; import { @@ -99,6 +101,32 @@ export function assertIsKeyringOrigins( ); } +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( diff --git a/yarn.lock b/yarn.lock index e82fb734cc..de602c595b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2013,6 +2013,15 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/common@npm:^4.4.0": + version: 4.4.0 + resolution: "@ethereumjs/common@npm:4.4.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + checksum: 10/dd5cc78575a762b367601f94d6af7e36cb3a5ecab45eec0c1259c433e755a16c867753aa88f331e3963791a18424ad0549682a3a6a0a160640fe846db6ce8014 + languageName: node + linkType: hard + "@ethereumjs/rlp@npm:^4.0.1": version: 4.0.1 resolution: "@ethereumjs/rlp@npm:4.0.1" @@ -2022,6 +2031,15 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/rlp@npm:^5.0.2": + version: 5.0.2 + resolution: "@ethereumjs/rlp@npm:5.0.2" + bin: + rlp: bin/rlp.cjs + checksum: 10/2af80d98faf7f64dfb6d739c2df7da7350ff5ad52426c3219897e843ee441215db0ffa346873200a6be6d11142edb9536e66acd62436b5005fa935baaf7eb6bd + languageName: node + linkType: hard + "@ethereumjs/tx@npm:^4.2.0": version: 4.2.0 resolution: "@ethereumjs/tx@npm:4.2.0" @@ -2034,6 +2052,18 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/tx@npm:^5.4.0": + version: 5.4.0 + resolution: "@ethereumjs/tx@npm:5.4.0" + dependencies: + "@ethereumjs/common": "npm:^4.4.0" + "@ethereumjs/rlp": "npm:^5.0.2" + "@ethereumjs/util": "npm:^9.1.0" + ethereum-cryptography: "npm:^2.2.1" + checksum: 10/8d2c0a69ab37015f945f9de065cfb9f05e8e79179efeed725ea0a14760c3eb8ff900bcf915bb71ec29fe2f753db35d1b78a15ac4ddec489e87c995dec1ba6e85 + languageName: node + linkType: hard + "@ethereumjs/util@npm:^8.1.0": version: 8.1.0 resolution: "@ethereumjs/util@npm:8.1.0" @@ -2045,6 +2075,16 @@ __metadata: languageName: node linkType: hard +"@ethereumjs/util@npm:^9.1.0": + version: 9.1.0 + resolution: "@ethereumjs/util@npm:9.1.0" + dependencies: + "@ethereumjs/rlp": "npm:^5.0.2" + ethereum-cryptography: "npm:^2.2.1" + checksum: 10/4e22c4081c63eebb808eccd54f7f91cd3407f4cac192da5f30a0d6983fe07d51f25e6a9d08624f1376e604bb7dce574aafcf0fbf0becf42f62687c11e710ac41 + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.6.0": version: 1.6.9 resolution: "@floating-ui/core@npm:1.6.9" @@ -2816,6 +2856,16 @@ __metadata: languageName: node linkType: hard +"@metamask/abi-utils@npm:^3.0.0": + version: 3.0.0 + resolution: "@metamask/abi-utils@npm:3.0.0" + dependencies: + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.0.1" + checksum: 10/068b98185148b9e185b4af4392c6a6f82f1d4b1ff60013c57679c618f37afe9030e3ccc940e1a8b690be6f62ea91115ab18b73f3c3c09f4eff1794e31ababb9b + languageName: node + linkType: hard + "@metamask/action-utils@npm:^1.0.0": version: 1.1.1 resolution: "@metamask/action-utils@npm:1.1.1" @@ -3338,6 +3388,21 @@ __metadata: languageName: node linkType: hard +"@metamask/eth-sig-util@npm:^8.2.0": + version: 8.2.0 + resolution: "@metamask/eth-sig-util@npm:8.2.0" + dependencies: + "@ethereumjs/rlp": "npm:^4.0.1" + "@ethereumjs/util": "npm:^8.1.0" + "@metamask/abi-utils": "npm:^3.0.0" + "@metamask/utils": "npm:^11.0.1" + "@scure/base": "npm:~1.1.3" + ethereum-cryptography: "npm:^2.1.2" + tweetnacl: "npm:^1.0.3" + checksum: 10/385df1ec541116e1bd725a1df1a519996bad167f99d1b2677126e398cdfda6fc3f03d2ff8f1ca523966bc0aae3ea92a9050953a45d5a7711f4128aacf9242bfc + languageName: node + linkType: hard + "@metamask/ethereum-provider-example-snap@workspace:^, @metamask/ethereum-provider-example-snap@workspace:packages/examples/packages/ethereum-provider": version: 0.0.0-use.local resolution: "@metamask/ethereum-provider-example-snap@workspace:packages/examples/packages/ethereum-provider" @@ -3697,6 +3762,35 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-api@npm:^21.5.0": + version: 21.5.0 + resolution: "@metamask/keyring-api@npm:21.5.0" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/keyring-utils": "npm:^3.2.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + "@types/uuid": "npm:^9.0.8" + async-mutex: "npm:^0.5.0" + bitcoin-address-validation: "npm:^2.2.3" + uuid: "npm:^9.0.1" + checksum: 10/a7f2a8c66bc76edabde15b66b80904208b71fd62406ebb91579db4c65d74a8f66db6c610afe265823313df7423b6727a4de03cb1f43e41d840d51a5039f9ef4d + languageName: node + linkType: hard + +"@metamask/keyring-utils@npm:^3.2.0": + version: 3.2.0 + resolution: "@metamask/keyring-utils@npm:3.2.0" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.1.0" + bitcoin-address-validation: "npm:^2.2.3" + checksum: 10/e71aa1b9ec9a24c72ea6d4864a10f11e68e5b77789728067230ec40cee2e85ad69073404d2fa62c760f014fd910fb68b3305a08f906f2534b9119e2a26d06a2b + languageName: node + linkType: hard + "@metamask/lifecycle-hooks-example-snap@workspace:^, @metamask/lifecycle-hooks-example-snap@workspace:packages/examples/packages/lifecycle-hooks": version: 0.0.0-use.local resolution: "@metamask/lifecycle-hooks-example-snap@workspace:packages/examples/packages/lifecycle-hooks" @@ -4622,6 +4716,7 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.4.3" "@metamask/auto-changelog": "npm:^5.3.2" "@metamask/key-tree": "npm:^10.1.1" + "@metamask/keyring-api": "npm:^21.5.0" "@metamask/messenger": "npm:^0.3.0" "@metamask/permission-controller": "npm:^12.2.0" "@metamask/post-message-stream": "npm:^10.0.0" @@ -4800,7 +4895,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": version: 11.10.0 resolution: "@metamask/utils@npm:11.10.0" dependencies: @@ -6679,6 +6774,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^9.0.8": + version: 9.0.8 + resolution: "@types/uuid@npm:9.0.8" + checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 + languageName: node + linkType: hard + "@types/validate-npm-package-name@npm:^4.0.0": version: 4.0.0 resolution: "@types/validate-npm-package-name@npm:4.0.0" @@ -8640,6 +8742,13 @@ __metadata: languageName: node linkType: hard +"base58-js@npm:^1.0.0": + version: 1.0.5 + resolution: "base58-js@npm:1.0.5" + checksum: 10/46c1b39d3a70bca0a47d56069c74a25d547680afd0f28609c90f280f5d614f5de36db5df993fa334db24008a68ab784a72fcdaa13eb40078e03c8999915a1100 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -8654,6 +8763,13 @@ __metadata: languageName: node linkType: hard +"bech32@npm:^2.0.0": + version: 2.0.0 + resolution: "bech32@npm:2.0.0" + checksum: 10/fa15acb270b59aa496734a01f9155677b478987b773bf701f465858bf1606c6a970085babd43d71ce61895f1baa594cb41a2cd1394bd2c6698f03cc2d811300e + languageName: node + linkType: hard + "before-after-hook@npm:^2.2.0": version: 2.2.3 resolution: "before-after-hook@npm:2.2.3" @@ -8713,6 +8829,17 @@ __metadata: languageName: node linkType: hard +"bitcoin-address-validation@npm:^2.2.3": + version: 2.2.3 + resolution: "bitcoin-address-validation@npm:2.2.3" + dependencies: + base58-js: "npm:^1.0.0" + bech32: "npm:^2.0.0" + sha256-uint8array: "npm:^0.10.3" + checksum: 10/01603b5edf610ecf0843ae546534313f1cffabc8e7435a3678bc9788f18a54e51302218a539794aafd49beb5be70b5d1d507eb7442cb33970fcd665592a71305 + languageName: node + linkType: hard + "bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -11056,7 +11183,7 @@ __metadata: languageName: node linkType: hard -"ethereum-cryptography@npm:^2.0.0": +"ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.2, ethereum-cryptography@npm:^2.2.1": version: 2.2.1 resolution: "ethereum-cryptography@npm:2.2.1" dependencies: @@ -16861,6 +16988,13 @@ __metadata: languageName: node linkType: hard +"sha256-uint8array@npm:^0.10.3": + version: 0.10.7 + resolution: "sha256-uint8array@npm:0.10.7" + checksum: 10/e427f9d2f9c521dea552f033d3f0c3bd641ab214d214dd41bde3c805edde393519cf982b3eee7d683b32e5f28fa23b2278d25935940e13fbe831b216a37832be + languageName: node + linkType: hard + "shallow-clone@npm:^0.1.2": version: 0.1.2 resolution: "shallow-clone@npm:0.1.2" @@ -18124,6 +18258,13 @@ __metadata: languageName: node linkType: hard +"tweetnacl@npm:^1.0.3": + version: 1.0.3 + resolution: "tweetnacl@npm:1.0.3" + checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" From ea8c62f18acda7c0b49048d41579ee96132903bb Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 10:58:00 -0400 Subject: [PATCH 02/12] refactor: remove keyring-api dep and add local definition for capabilities --- packages/snaps-utils/package.json | 1 - packages/snaps-utils/src/json-rpc.ts | 75 +++++++++++++- yarn.lock | 145 +-------------------------- 3 files changed, 75 insertions(+), 146 deletions(-) diff --git a/packages/snaps-utils/package.json b/packages/snaps-utils/package.json index 3dff80ab1b..60625d249e 100644 --- a/packages/snaps-utils/package.json +++ b/packages/snaps-utils/package.json @@ -81,7 +81,6 @@ "@babel/core": "^7.23.2", "@babel/types": "^7.23.0", "@metamask/key-tree": "^10.1.1", - "@metamask/keyring-api": "^21.5.0", "@metamask/messenger": "^0.3.0", "@metamask/permission-controller": "^12.2.0", "@metamask/rpc-errors": "^7.0.3", diff --git a/packages/snaps-utils/src/json-rpc.ts b/packages/snaps-utils/src/json-rpc.ts index 610b71f8e5..fbb78d94e0 100644 --- a/packages/snaps-utils/src/json-rpc.ts +++ b/packages/snaps-utils/src/json-rpc.ts @@ -1,12 +1,14 @@ -import { KeyringCapabilitiesStruct as CapabilitiesStruct } from '@metamask/keyring-api'; -import type { KeyringCapabilities as Capabilities } from '@metamask/keyring-api'; import { SubjectType } from '@metamask/permission-controller'; import type { Infer } from '@metamask/superstruct'; import { array, boolean, + enums, + exactOptional, + nonempty, object, optional, + partial, refine, string, } from '@metamask/superstruct'; @@ -16,6 +18,7 @@ import type { } from '@metamask/utils'; import { assertStruct, + CaipChainIdStruct, isJsonRpcFailure, isJsonRpcSuccess, } from '@metamask/utils'; @@ -101,6 +104,74 @@ 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: exactOptional( + object({ + derivePath: exactOptional(boolean()), + deriveIndex: exactOptional(boolean()), + deriveIndexRange: exactOptional(boolean()), + discover: exactOptional(boolean()), + }), + ), + privateKey: exactOptional( + object({ + importFormats: exactOptional( + array( + object({ + encoding: PrivateKeyEncodingStruct, + type: exactOptional(KeyringAccountTypeStruct), + }), + ), + ), + exportFormats: exactOptional( + array(object({ encoding: PrivateKeyEncodingStruct })), + ), + }), + ), + custom: exactOptional( + partial( + object({ + createAccounts: boolean(), + }), + ), + ), +}); + export const KeyringCapabilitiesStruct = object({ capabilities: optional(CapabilitiesStruct), }); diff --git a/yarn.lock b/yarn.lock index de602c595b..e82fb734cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2013,15 +2013,6 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/common@npm:^4.4.0": - version: 4.4.0 - resolution: "@ethereumjs/common@npm:4.4.0" - dependencies: - "@ethereumjs/util": "npm:^9.1.0" - checksum: 10/dd5cc78575a762b367601f94d6af7e36cb3a5ecab45eec0c1259c433e755a16c867753aa88f331e3963791a18424ad0549682a3a6a0a160640fe846db6ce8014 - languageName: node - linkType: hard - "@ethereumjs/rlp@npm:^4.0.1": version: 4.0.1 resolution: "@ethereumjs/rlp@npm:4.0.1" @@ -2031,15 +2022,6 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/rlp@npm:^5.0.2": - version: 5.0.2 - resolution: "@ethereumjs/rlp@npm:5.0.2" - bin: - rlp: bin/rlp.cjs - checksum: 10/2af80d98faf7f64dfb6d739c2df7da7350ff5ad52426c3219897e843ee441215db0ffa346873200a6be6d11142edb9536e66acd62436b5005fa935baaf7eb6bd - languageName: node - linkType: hard - "@ethereumjs/tx@npm:^4.2.0": version: 4.2.0 resolution: "@ethereumjs/tx@npm:4.2.0" @@ -2052,18 +2034,6 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/tx@npm:^5.4.0": - version: 5.4.0 - resolution: "@ethereumjs/tx@npm:5.4.0" - dependencies: - "@ethereumjs/common": "npm:^4.4.0" - "@ethereumjs/rlp": "npm:^5.0.2" - "@ethereumjs/util": "npm:^9.1.0" - ethereum-cryptography: "npm:^2.2.1" - checksum: 10/8d2c0a69ab37015f945f9de065cfb9f05e8e79179efeed725ea0a14760c3eb8ff900bcf915bb71ec29fe2f753db35d1b78a15ac4ddec489e87c995dec1ba6e85 - languageName: node - linkType: hard - "@ethereumjs/util@npm:^8.1.0": version: 8.1.0 resolution: "@ethereumjs/util@npm:8.1.0" @@ -2075,16 +2045,6 @@ __metadata: languageName: node linkType: hard -"@ethereumjs/util@npm:^9.1.0": - version: 9.1.0 - resolution: "@ethereumjs/util@npm:9.1.0" - dependencies: - "@ethereumjs/rlp": "npm:^5.0.2" - ethereum-cryptography: "npm:^2.2.1" - checksum: 10/4e22c4081c63eebb808eccd54f7f91cd3407f4cac192da5f30a0d6983fe07d51f25e6a9d08624f1376e604bb7dce574aafcf0fbf0becf42f62687c11e710ac41 - languageName: node - linkType: hard - "@floating-ui/core@npm:^1.6.0": version: 1.6.9 resolution: "@floating-ui/core@npm:1.6.9" @@ -2856,16 +2816,6 @@ __metadata: languageName: node linkType: hard -"@metamask/abi-utils@npm:^3.0.0": - version: 3.0.0 - resolution: "@metamask/abi-utils@npm:3.0.0" - dependencies: - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.0.1" - checksum: 10/068b98185148b9e185b4af4392c6a6f82f1d4b1ff60013c57679c618f37afe9030e3ccc940e1a8b690be6f62ea91115ab18b73f3c3c09f4eff1794e31ababb9b - languageName: node - linkType: hard - "@metamask/action-utils@npm:^1.0.0": version: 1.1.1 resolution: "@metamask/action-utils@npm:1.1.1" @@ -3388,21 +3338,6 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-sig-util@npm:^8.2.0": - version: 8.2.0 - resolution: "@metamask/eth-sig-util@npm:8.2.0" - dependencies: - "@ethereumjs/rlp": "npm:^4.0.1" - "@ethereumjs/util": "npm:^8.1.0" - "@metamask/abi-utils": "npm:^3.0.0" - "@metamask/utils": "npm:^11.0.1" - "@scure/base": "npm:~1.1.3" - ethereum-cryptography: "npm:^2.1.2" - tweetnacl: "npm:^1.0.3" - checksum: 10/385df1ec541116e1bd725a1df1a519996bad167f99d1b2677126e398cdfda6fc3f03d2ff8f1ca523966bc0aae3ea92a9050953a45d5a7711f4128aacf9242bfc - languageName: node - linkType: hard - "@metamask/ethereum-provider-example-snap@workspace:^, @metamask/ethereum-provider-example-snap@workspace:packages/examples/packages/ethereum-provider": version: 0.0.0-use.local resolution: "@metamask/ethereum-provider-example-snap@workspace:packages/examples/packages/ethereum-provider" @@ -3762,35 +3697,6 @@ __metadata: languageName: node linkType: hard -"@metamask/keyring-api@npm:^21.5.0": - version: 21.5.0 - resolution: "@metamask/keyring-api@npm:21.5.0" - dependencies: - "@ethereumjs/tx": "npm:^5.4.0" - "@metamask/eth-sig-util": "npm:^8.2.0" - "@metamask/keyring-utils": "npm:^3.2.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.1.0" - "@types/uuid": "npm:^9.0.8" - async-mutex: "npm:^0.5.0" - bitcoin-address-validation: "npm:^2.2.3" - uuid: "npm:^9.0.1" - checksum: 10/a7f2a8c66bc76edabde15b66b80904208b71fd62406ebb91579db4c65d74a8f66db6c610afe265823313df7423b6727a4de03cb1f43e41d840d51a5039f9ef4d - languageName: node - linkType: hard - -"@metamask/keyring-utils@npm:^3.2.0": - version: 3.2.0 - resolution: "@metamask/keyring-utils@npm:3.2.0" - dependencies: - "@ethereumjs/tx": "npm:^5.4.0" - "@metamask/superstruct": "npm:^3.1.0" - "@metamask/utils": "npm:^11.1.0" - bitcoin-address-validation: "npm:^2.2.3" - checksum: 10/e71aa1b9ec9a24c72ea6d4864a10f11e68e5b77789728067230ec40cee2e85ad69073404d2fa62c760f014fd910fb68b3305a08f906f2534b9119e2a26d06a2b - languageName: node - linkType: hard - "@metamask/lifecycle-hooks-example-snap@workspace:^, @metamask/lifecycle-hooks-example-snap@workspace:packages/examples/packages/lifecycle-hooks": version: 0.0.0-use.local resolution: "@metamask/lifecycle-hooks-example-snap@workspace:packages/examples/packages/lifecycle-hooks" @@ -4716,7 +4622,6 @@ __metadata: "@lavamoat/allow-scripts": "npm:^3.4.3" "@metamask/auto-changelog": "npm:^5.3.2" "@metamask/key-tree": "npm:^10.1.1" - "@metamask/keyring-api": "npm:^21.5.0" "@metamask/messenger": "npm:^0.3.0" "@metamask/permission-controller": "npm:^12.2.0" "@metamask/post-message-stream": "npm:^10.0.0" @@ -4895,7 +4800,7 @@ __metadata: languageName: unknown linkType: soft -"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.1.0, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": +"@metamask/utils@npm:^11.0.1, @metamask/utils@npm:^11.10.0, @metamask/utils@npm:^11.4.0, @metamask/utils@npm:^11.4.2, @metamask/utils@npm:^11.8.1, @metamask/utils@npm:^11.9.0": version: 11.10.0 resolution: "@metamask/utils@npm:11.10.0" dependencies: @@ -6774,13 +6679,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.8": - version: 9.0.8 - resolution: "@types/uuid@npm:9.0.8" - checksum: 10/b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 - languageName: node - linkType: hard - "@types/validate-npm-package-name@npm:^4.0.0": version: 4.0.0 resolution: "@types/validate-npm-package-name@npm:4.0.0" @@ -8742,13 +8640,6 @@ __metadata: languageName: node linkType: hard -"base58-js@npm:^1.0.0": - version: 1.0.5 - resolution: "base58-js@npm:1.0.5" - checksum: 10/46c1b39d3a70bca0a47d56069c74a25d547680afd0f28609c90f280f5d614f5de36db5df993fa334db24008a68ab784a72fcdaa13eb40078e03c8999915a1100 - languageName: node - linkType: hard - "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -8763,13 +8654,6 @@ __metadata: languageName: node linkType: hard -"bech32@npm:^2.0.0": - version: 2.0.0 - resolution: "bech32@npm:2.0.0" - checksum: 10/fa15acb270b59aa496734a01f9155677b478987b773bf701f465858bf1606c6a970085babd43d71ce61895f1baa594cb41a2cd1394bd2c6698f03cc2d811300e - languageName: node - linkType: hard - "before-after-hook@npm:^2.2.0": version: 2.2.3 resolution: "before-after-hook@npm:2.2.3" @@ -8829,17 +8713,6 @@ __metadata: languageName: node linkType: hard -"bitcoin-address-validation@npm:^2.2.3": - version: 2.2.3 - resolution: "bitcoin-address-validation@npm:2.2.3" - dependencies: - base58-js: "npm:^1.0.0" - bech32: "npm:^2.0.0" - sha256-uint8array: "npm:^0.10.3" - checksum: 10/01603b5edf610ecf0843ae546534313f1cffabc8e7435a3678bc9788f18a54e51302218a539794aafd49beb5be70b5d1d507eb7442cb33970fcd665592a71305 - languageName: node - linkType: hard - "bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -11183,7 +11056,7 @@ __metadata: languageName: node linkType: hard -"ethereum-cryptography@npm:^2.0.0, ethereum-cryptography@npm:^2.1.2, ethereum-cryptography@npm:^2.2.1": +"ethereum-cryptography@npm:^2.0.0": version: 2.2.1 resolution: "ethereum-cryptography@npm:2.2.1" dependencies: @@ -16988,13 +16861,6 @@ __metadata: languageName: node linkType: hard -"sha256-uint8array@npm:^0.10.3": - version: 0.10.7 - resolution: "sha256-uint8array@npm:0.10.7" - checksum: 10/e427f9d2f9c521dea552f033d3f0c3bd641ab214d214dd41bde3c805edde393519cf982b3eee7d683b32e5f28fa23b2278d25935940e13fbe831b216a37832be - languageName: node - linkType: hard - "shallow-clone@npm:^0.1.2": version: 0.1.2 resolution: "shallow-clone@npm:0.1.2" @@ -18258,13 +18124,6 @@ __metadata: languageName: node linkType: hard -"tweetnacl@npm:^1.0.3": - version: 1.0.3 - resolution: "tweetnacl@npm:1.0.3" - checksum: 10/ca122c2f86631f3c0f6d28efb44af2a301d4a557a62a3e2460286b08e97567b258c2212e4ad1cfa22bd6a57edcdc54ba76ebe946847450ab0999e6d48ccae332 - languageName: node - linkType: hard - "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" From 1e2eff9249cd1733c5942c3c76e3f25f521d0a16 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 11:04:02 -0400 Subject: [PATCH 03/12] chore: update changelog --- packages/snaps-utils/CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/snaps-utils/CHANGELOG.md b/packages/snaps-utils/CHANGELOG.md index 484363ff9c..3046b27a28 100644 --- a/packages/snaps-utils/CHANGELOG.md +++ b/packages/snaps-utils/CHANGELOG.md @@ -9,8 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add capabilities caveat for `endowment:keyring` ([#XXXX](https://github.com/MetaMask/snaps/pull/XXXX)) - - `KeyringCapabilities` and `KeyringCapabilitiesStruct` from `@metamask/keyring-api` are now re-exported from this package as well. +- Add `KeyringCapabilities` type and `KeyringCapabilitiesStruct` / `assertIsKeyringCapabilities` exports for the `endowment:keyring` capabilities caveat ([#3903](https://github.com/MetaMask/snaps/pull/3903)) ## [12.1.1] From cfded47fdeaa67185381423ba5842824b28e0b02 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 11:05:22 -0400 Subject: [PATCH 04/12] chore: update snap-rpc-methods changelog --- packages/snaps-rpc-methods/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-rpc-methods/CHANGELOG.md b/packages/snaps-rpc-methods/CHANGELOG.md index e8d20eb6c4..3f3134c69e 100644 --- a/packages/snaps-rpc-methods/CHANGELOG.md +++ b/packages/snaps-rpc-methods/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `capabilities` caveat support to `endowment:keyring` ([#XXXX](https://github.com/MetaMask/snaps/pull/XXXX)) +- 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. From 1b7c802750cbd3034d0591dd760a040882285974 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 11:16:01 -0400 Subject: [PATCH 05/12] fix: fix capabilities getter --- .../src/endowments/keyring.test.ts | 18 ++++++++++++++++++ .../src/endowments/keyring.ts | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/snaps-rpc-methods/src/endowments/keyring.test.ts b/packages/snaps-rpc-methods/src/endowments/keyring.test.ts index c321b00849..56fc01b291 100644 --- a/packages/snaps-rpc-methods/src/endowments/keyring.test.ts +++ b/packages/snaps-rpc-methods/src/endowments/keyring.test.ts @@ -174,6 +174,24 @@ describe('getKeyringCaveatCapabilities', () => { }, }); }); + + 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', () => { diff --git a/packages/snaps-rpc-methods/src/endowments/keyring.ts b/packages/snaps-rpc-methods/src/endowments/keyring.ts index 9eb7c6e7f7..72e3536d83 100644 --- a/packages/snaps-rpc-methods/src/endowments/keyring.ts +++ b/packages/snaps-rpc-methods/src/endowments/keyring.ts @@ -181,8 +181,7 @@ export function getKeyringCaveatCapabilities( (permCaveat) => permCaveat.type === SnapCaveatType.KeyringCapabilities, ) as Caveat | undefined; - assert(caveat); - return caveat.value; + return caveat?.value ?? null; } export const keyringCaveatSpecifications: Record< From d122b58fd810741f7643b0a880c333230dffa978 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 11:17:29 -0400 Subject: [PATCH 06/12] chore: update coverage --- packages/snaps-rpc-methods/jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snaps-rpc-methods/jest.config.js b/packages/snaps-rpc-methods/jest.config.js index bac60d40d6..528a9004ea 100644 --- a/packages/snaps-rpc-methods/jest.config.js +++ b/packages/snaps-rpc-methods/jest.config.js @@ -10,7 +10,7 @@ module.exports = deepmerge(baseConfig, { ], coverageThreshold: { global: { - branches: 96.61, + branches: 96.62, functions: 99.2, lines: 99.06, statements: 98.78, From 7cfe31baffd57f31db3ea7fac95e769b10a85fb1 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 11:28:44 -0400 Subject: [PATCH 07/12] fix: update type --- packages/snaps-rpc-methods/src/endowments/keyring.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/snaps-rpc-methods/src/endowments/keyring.ts b/packages/snaps-rpc-methods/src/endowments/keyring.ts index 72e3536d83..4126f21307 100644 --- a/packages/snaps-rpc-methods/src/endowments/keyring.ts +++ b/packages/snaps-rpc-methods/src/endowments/keyring.ts @@ -111,9 +111,7 @@ function validateCaveatCapabilities(caveat: Caveat) { * 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 & { - capabilities?: KeyringCapabilities; -}; +type KeyringCaveatMapperInput = KeyringOrigins & KeyringCapabilities; /** * Map a raw value from the `initialPermissions` to a caveat specification. From 9210d91648dd16dd76d5f2bff124b60e3b58e180 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 11:30:23 -0400 Subject: [PATCH 08/12] fix: add capabilities export --- packages/snaps-rpc-methods/src/endowments/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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'; From 7be44a0def057e299f11505ece908b52dc494e74 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 11:37:19 -0400 Subject: [PATCH 09/12] fix: update snapshot --- packages/snaps-simulation/src/methods/specifications.test.ts | 1 + 1 file changed, 1 insertion(+) 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], From 1662a286340eed42552d7c60b361b903eca5425b Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 12:04:14 -0400 Subject: [PATCH 10/12] test: add tests for assertIsKeyringCapabilities --- packages/snaps-utils/src/json-rpc.test.ts | 137 ++++++++++++++++++++++ 1 file changed, 137 insertions(+) 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 = { From cdc079fadf15a37dd035d92571aa340042f838c9 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 12:41:46 -0400 Subject: [PATCH 11/12] fix: update initial permissions type --- packages/snaps-sdk/src/types/permissions.ts | 63 ++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/snaps-sdk/src/types/permissions.ts b/packages/snaps-sdk/src/types/permissions.ts index 623c065ea3..14ac47514e 100644 --- a/packages/snaps-sdk/src/types/permissions.ts +++ b/packages/snaps-sdk/src/types/permissions.ts @@ -1,5 +1,9 @@ import type { SupportedCurve } from '@metamask/key-tree'; -import type { CaipChainId, JsonRpcParams } from '@metamask/utils'; +import type { + CaipChainId, + JsonRpcParams, + NonEmptyArray, +} from '@metamask/utils'; export type EmptyObject = Record; @@ -74,6 +78,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: NonEmptyArray; + 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 +142,7 @@ export type InitialPermissions = Partial<{ 'endowment:ethereum-provider': EmptyObject; 'endowment:keyring': { allowedOrigins?: string[]; + capabilities?: Capabilities; maxRequestTime?: number; }; 'endowment:lifecycle-hooks'?: { From 5f1946d2e8dcf909a823b3d32f312b65d698244f Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Fri, 20 Mar 2026 13:10:23 -0400 Subject: [PATCH 12/12] fix: fix types --- packages/snaps-sdk/src/types/permissions.ts | 8 ++---- packages/snaps-utils/src/json-rpc.ts | 30 +++++++++------------ 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/snaps-sdk/src/types/permissions.ts b/packages/snaps-sdk/src/types/permissions.ts index 14ac47514e..9e8fcfb843 100644 --- a/packages/snaps-sdk/src/types/permissions.ts +++ b/packages/snaps-sdk/src/types/permissions.ts @@ -1,9 +1,5 @@ import type { SupportedCurve } from '@metamask/key-tree'; -import type { - CaipChainId, - JsonRpcParams, - NonEmptyArray, -} from '@metamask/utils'; +import type { CaipChainId, JsonRpcParams } from '@metamask/utils'; export type EmptyObject = Record; @@ -113,7 +109,7 @@ type KeyringAccountType = * exists purely for the `InitialPermissions` type signature. */ type Capabilities = { - scopes: NonEmptyArray; + scopes: CaipChainId[]; bip44?: { derivePath?: boolean; deriveIndex?: boolean; diff --git a/packages/snaps-utils/src/json-rpc.ts b/packages/snaps-utils/src/json-rpc.ts index fbb78d94e0..e472c5afae 100644 --- a/packages/snaps-utils/src/json-rpc.ts +++ b/packages/snaps-utils/src/json-rpc.ts @@ -4,11 +4,9 @@ import { array, boolean, enums, - exactOptional, nonempty, object, optional, - partial, refine, string, } from '@metamask/superstruct'; @@ -140,35 +138,33 @@ const KeyringAccountTypeStruct = enums([ */ const CapabilitiesStruct = object({ scopes: nonempty(array(CaipChainIdStruct)), - bip44: exactOptional( + bip44: optional( object({ - derivePath: exactOptional(boolean()), - deriveIndex: exactOptional(boolean()), - deriveIndexRange: exactOptional(boolean()), - discover: exactOptional(boolean()), + derivePath: optional(boolean()), + deriveIndex: optional(boolean()), + deriveIndexRange: optional(boolean()), + discover: optional(boolean()), }), ), - privateKey: exactOptional( + privateKey: optional( object({ - importFormats: exactOptional( + importFormats: optional( array( object({ encoding: PrivateKeyEncodingStruct, - type: exactOptional(KeyringAccountTypeStruct), + type: optional(KeyringAccountTypeStruct), }), ), ), - exportFormats: exactOptional( + exportFormats: optional( array(object({ encoding: PrivateKeyEncodingStruct })), ), }), ), - custom: exactOptional( - partial( - object({ - createAccounts: boolean(), - }), - ), + custom: optional( + object({ + createAccounts: optional(boolean()), + }), ), });