From 861a1052b10ca8e1ef02b7f7199434dbfe144131 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 18 Dec 2025 10:02:16 +0100 Subject: [PATCH 1/6] feat: add custom capabilities & account creation types --- packages/keyring-api/CHANGELOG.md | 3 + .../src/api/account-options.test.ts | 32 +++++++++-- .../keyring-api/src/api/account-options.ts | 57 +++++++++++++++++-- .../src/api/v2/create-account/custom.ts | 25 ++++++++ .../src/api/v2/create-account/index.ts | 12 ++++ .../src/api/v2/keyring-capabilities.ts | 19 +++++++ .../keyring-api/src/api/v2/keyring.test-d.ts | 26 +++++++++ 7 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 packages/keyring-api/src/api/v2/create-account/custom.ts diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 25a48270b..b45170b16 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `custom` capability to `KeyringCapabilities` for keyrings with non-standard method options ([#TBD](https://github.com/MetaMask/accounts/pull/TBD)) +- Add `KeyringAccountEntropyTypeOption.Custom` for custom/opaque entropy sources ([#TBD](https://github.com/MetaMask/accounts/pull/TBD)) +- Add `AccountCreationType.Custom` and `CreateAccountCustomOptions` for custom account creation flows ([#TBD](https://github.com/MetaMask/accounts/pull/TBD)) - Add `EthKeyringWrapper` abstract class for Ethereum-based `KeyringV2` implementations ([#404](https://github.com/MetaMask/accounts/pull/404)) - Provides common Ethereum signing method routing (`submitRequest`) for all Ethereum-based keyrings. - Add `KeyringWrapper` base class to adapt legacy keyrings to `KeyringV2` ([#398](https://github.com/MetaMask/accounts/pull/398)), ([#410](https://github.com/MetaMask/accounts/pull/410)) diff --git a/packages/keyring-api/src/api/account-options.test.ts b/packages/keyring-api/src/api/account-options.test.ts index d125de4ce..bb2370fb2 100644 --- a/packages/keyring-api/src/api/account-options.test.ts +++ b/packages/keyring-api/src/api/account-options.test.ts @@ -1,11 +1,14 @@ import { assert } from '@metamask/superstruct'; -import { KeyringAccountOptionsStruct } from './account-options'; +import { + KeyringAccountEntropyTypeOption, + KeyringAccountOptionsStruct, +} from './account-options'; describe('api', () => { describe('KeyringAccountOptionsStruct', () => { const baseEntropyMnemonicOptions = { - type: 'mnemonic', + type: KeyringAccountEntropyTypeOption.Mnemonic, id: '01K0BX6VDR5DPDPGGNA8PZVBVB', derivationPath: "m/44'/60'/0'/0/0", }; @@ -14,9 +17,26 @@ describe('api', () => { {}, { exportable: true }, { exportable: false }, - { entropy: { type: 'private-key' } }, - { entropy: { type: 'private-key' }, exportable: true }, - { entropy: { type: 'private-key' }, exportable: false }, + { entropy: { type: KeyringAccountEntropyTypeOption.PrivateKey } }, + { + entropy: { type: KeyringAccountEntropyTypeOption.PrivateKey }, + exportable: true, + }, + { + entropy: { type: KeyringAccountEntropyTypeOption.PrivateKey }, + exportable: false, + }, + { + entropy: { type: KeyringAccountEntropyTypeOption.Custom }, + }, + { + entropy: { type: KeyringAccountEntropyTypeOption.Custom }, + exportable: true, + }, + { + entropy: { type: KeyringAccountEntropyTypeOption.Custom }, + exportable: false, + }, { entropy: { ...baseEntropyMnemonicOptions, @@ -55,7 +75,7 @@ describe('api', () => { it('throws if legacy options partially matches options.entropy.type', () => { const options = { entropy: { - type: 'mnemonic', + type: KeyringAccountEntropyTypeOption.Mnemonic, // Nothing else, like if it was legacy. }, }; diff --git a/packages/keyring-api/src/api/account-options.ts b/packages/keyring-api/src/api/account-options.ts index 152ffdcce..b6a69eea3 100644 --- a/packages/keyring-api/src/api/account-options.ts +++ b/packages/keyring-api/src/api/account-options.ts @@ -24,6 +24,12 @@ export enum KeyringAccountEntropyTypeOption { * Indicates that the account was imported from a private key. */ PrivateKey = 'private-key', + + /** + * Indicates that the account was created with custom, keyring-specific entropy. + * This is an opaque type where the entropy source is managed internally by the keyring. + */ + Custom = 'custom', } /** @@ -78,15 +84,44 @@ export type KeyringAccountEntropyPrivateKeyOptions = Infer< typeof KeyringAccountEntropyPrivateKeyOptionsStruct >; +/** + * Keyring account options struct for custom entropy. + * + * This is an opaque type where the entropy source is managed internally by the keyring. + * It behaves similarly to a private key import but allows keyrings to define their own + * entropy management strategy. + */ +export const KeyringAccountEntropyCustomOptionsStruct = object({ + /** + * Indicates that the account was created with custom, keyring-specific entropy. + */ + type: literal(`${KeyringAccountEntropyTypeOption.Custom}`), +}); + +/** + * Keyring account options for custom entropy {@link KeyringAccountEntropyCustomOptionsStruct}. + */ +export type KeyringAccountEntropyCustomOptions = Infer< + typeof KeyringAccountEntropyCustomOptionsStruct +>; + /** * Keyring account entropy options struct. */ export const KeyringAccountEntropyOptionsStruct = selectiveUnion( (value: any) => { - return isPlainObject(value) && - value.type === KeyringAccountEntropyTypeOption.PrivateKey - ? KeyringAccountEntropyPrivateKeyOptionsStruct - : KeyringAccountEntropyMnemonicOptionsStruct; + if (!isPlainObject(value)) { + return KeyringAccountEntropyMnemonicOptionsStruct; + } + + switch (value.type) { + case KeyringAccountEntropyTypeOption.PrivateKey: + return KeyringAccountEntropyPrivateKeyOptionsStruct; + case KeyringAccountEntropyTypeOption.Custom: + return KeyringAccountEntropyCustomOptionsStruct; + default: + return KeyringAccountEntropyMnemonicOptionsStruct; + } }, ); @@ -100,8 +135,9 @@ export type KeyringAccountEntropyOptions = Infer< /** * Keyring options struct. This represents various options for a Keyring account object. * - * See {@link KeyringAccountEntropyMnemonicOptionsStruct} and - * {@link KeyringAccountEntropyPrivateKeyOptionsStruct}. + * See {@link KeyringAccountEntropyMnemonicOptionsStruct}, + * {@link KeyringAccountEntropyPrivateKeyOptionsStruct}, and + * {@link KeyringAccountEntropyCustomOptionsStruct}. * * @example * ```ts @@ -128,6 +164,15 @@ export type KeyringAccountEntropyOptions = Infer< * @example * ```ts * { + * entropy: { + * type: 'custom', + * }, + * } + * ``` + * + * @example + * ```ts + * { * some: { * untyped: 'options', * something: true, diff --git a/packages/keyring-api/src/api/v2/create-account/custom.ts b/packages/keyring-api/src/api/v2/create-account/custom.ts new file mode 100644 index 000000000..296813d5d --- /dev/null +++ b/packages/keyring-api/src/api/v2/create-account/custom.ts @@ -0,0 +1,25 @@ +import { literal, object, type Infer } from '@metamask/superstruct'; + +/** + * Struct for {@link CreateAccountCustomOptions}. + */ +export const CreateAccountCustomOptionsStruct = object({ + /** + * The type of the options. + */ + type: literal('custom'), +}); + +/** + * Options for creating an account using a custom, keyring-specific method. + * + * This is an opaque type that allows keyrings with non-standard account + * creation flows to define their own options. Keyrings using this type + * should declare `custom.createAccounts: true` in their capabilities. + * + * The actual options accepted by the keyring are implementation-specific + * and not validated by this struct beyond the `type` field. + */ +export type CreateAccountCustomOptions = Infer< + typeof CreateAccountCustomOptionsStruct +>; diff --git a/packages/keyring-api/src/api/v2/create-account/index.ts b/packages/keyring-api/src/api/v2/create-account/index.ts index 259bba847..47dad44a9 100644 --- a/packages/keyring-api/src/api/v2/create-account/index.ts +++ b/packages/keyring-api/src/api/v2/create-account/index.ts @@ -6,9 +6,11 @@ import { CreateAccountBip44DeriveIndexOptionsStruct, CreateAccountBip44DerivePathOptionsStruct, } from './bip44'; +import { CreateAccountCustomOptionsStruct } from './custom'; import { CreateAccountPrivateKeyOptionsStruct } from './private-key'; export * from './bip44'; +export * from './custom'; export * from './private-key'; /** @@ -42,6 +44,14 @@ export enum AccountCreationType { * Represents an account imported from a private key. */ PrivateKeyImport = 'private-key:import', + + /** + * Represents an account created using a custom, keyring-specific method. + * + * This is used by keyrings that have non-standard account creation flows + * and declare `custom.createAccounts: true` in their capabilities. + */ + Custom = 'custom', } /** @@ -58,6 +68,8 @@ export const CreateAccountOptionsStruct = selectiveUnion((value: any) => { return CreateAccountBip44DiscoverOptionsStruct; case AccountCreationType.PrivateKeyImport: return CreateAccountPrivateKeyOptionsStruct; + case AccountCreationType.Custom: + return CreateAccountCustomOptionsStruct; default: // Return first struct as fallback - validation will fail with proper error indicating the type mismatch return CreateAccountBip44DerivePathOptionsStruct; diff --git a/packages/keyring-api/src/api/v2/keyring-capabilities.ts b/packages/keyring-api/src/api/v2/keyring-capabilities.ts index c881363f0..b05cb625c 100644 --- a/packages/keyring-api/src/api/v2/keyring-capabilities.ts +++ b/packages/keyring-api/src/api/v2/keyring-capabilities.ts @@ -4,6 +4,7 @@ import { exactOptional, nonempty, object, + partial, type Infer, } from '@metamask/superstruct'; @@ -55,6 +56,24 @@ export const KeyringCapabilitiesStruct = object({ exportFormats: exactOptional(array(ExportPrivateKeyFormatStruct)), }), ), + /** + * Indicates which KeyringV2 methods accept non-standard options. + * + * When a method is set to `true`, it signals that the keyring implementation + * accepts custom options for that method, different from the standard API. + * This is a workaround for keyrings with very specific requirements. + */ + custom: exactOptional( + partial( + object({ + deserialize: boolean(), + getAccount: boolean(), + createAccounts: boolean(), + deleteAccount: boolean(), + exportAccount: boolean(), + }), + ), + ), }); /** diff --git a/packages/keyring-api/src/api/v2/keyring.test-d.ts b/packages/keyring-api/src/api/v2/keyring.test-d.ts index 2e11915a0..41d954512 100644 --- a/packages/keyring-api/src/api/v2/keyring.test-d.ts +++ b/packages/keyring-api/src/api/v2/keyring.test-d.ts @@ -5,6 +5,7 @@ import { type CreateAccountBip44DiscoverOptions, type CreateAccountBip44DeriveIndexOptions, type CreateAccountBip44DerivePathOptions, + type CreateAccountCustomOptions, type CreateAccountOptions, type CreateAccountPrivateKeyOptions, } from './create-account'; @@ -33,6 +34,7 @@ expectAssignable(AccountCreationType.Bip44DerivePath); expectAssignable(AccountCreationType.Bip44DeriveIndex); expectAssignable(AccountCreationType.Bip44Discover); expectAssignable(AccountCreationType.PrivateKeyImport); +expectAssignable(AccountCreationType.Custom); // Test AccountExportType enum expectAssignable(AccountExportType.PrivateKey); @@ -73,6 +75,21 @@ expectAssignable({ }, }); +expectAssignable({ + scopes: ['eip155:1'], + custom: { + createAccounts: true, + }, +}); + +expectAssignable({ + scopes: ['eip155:1'], + custom: { + createAccounts: true, + exportAccount: true, + }, +}); + // Test CreateAccountBip44DerivePathOptions expectAssignable({ type: AccountCreationType.Bip44DerivePath, @@ -114,6 +131,11 @@ expectAssignable({ accountType: 'bip122:p2wpkh', }); +// Test CreateAccountCustomOptions +expectAssignable({ + type: AccountCreationType.Custom, +}); + // Test CreateAccountOptions union expectAssignable({ type: AccountCreationType.Bip44DerivePath, @@ -133,6 +155,10 @@ expectAssignable({ encoding: 'hexadecimal', }); +expectAssignable({ + type: AccountCreationType.Custom, +}); + // Test ExportAccountOptions expectAssignable({ type: AccountExportType.PrivateKey, From 8ea36aedf8db0521e674412dc86d024e865cb5fa Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 18 Dec 2025 10:04:14 +0100 Subject: [PATCH 2/6] fix: update CHANGELOG --- packages/keyring-api/CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index b45170b16..7874827d5 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -9,9 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `custom` capability to `KeyringCapabilities` for keyrings with non-standard method options ([#TBD](https://github.com/MetaMask/accounts/pull/TBD)) -- Add `KeyringAccountEntropyTypeOption.Custom` for custom/opaque entropy sources ([#TBD](https://github.com/MetaMask/accounts/pull/TBD)) -- Add `AccountCreationType.Custom` and `CreateAccountCustomOptions` for custom account creation flows ([#TBD](https://github.com/MetaMask/accounts/pull/TBD)) +- Add support for custom capabilities and entropy types in `KeyringV2` ([#415](https://github.com/MetaMask/accounts/pull/415)) + - Add `custom` capability to `KeyringCapabilities` for keyrings with non-standard method options. + - Add `KeyringAccountEntropyTypeOption.Custom` for custom/opaque entropy sources. + - Add `AccountCreationType.Custom` and `CreateAccountCustomOptions` for custom account creation flows. - Add `EthKeyringWrapper` abstract class for Ethereum-based `KeyringV2` implementations ([#404](https://github.com/MetaMask/accounts/pull/404)) - Provides common Ethereum signing method routing (`submitRequest`) for all Ethereum-based keyrings. - Add `KeyringWrapper` base class to adapt legacy keyrings to `KeyringV2` ([#398](https://github.com/MetaMask/accounts/pull/398)), ([#410](https://github.com/MetaMask/accounts/pull/410)) From 827ccc8e46ba766f458ece95f2dfb892b3e9530e Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 18 Dec 2025 11:38:08 +0100 Subject: [PATCH 3/6] fix: better jsdoc for CreateAccountCustomOptions --- packages/keyring-api/src/api/v2/create-account/custom.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/keyring-api/src/api/v2/create-account/custom.ts b/packages/keyring-api/src/api/v2/create-account/custom.ts index 296813d5d..f3d429bdd 100644 --- a/packages/keyring-api/src/api/v2/create-account/custom.ts +++ b/packages/keyring-api/src/api/v2/create-account/custom.ts @@ -18,7 +18,8 @@ export const CreateAccountCustomOptionsStruct = object({ * should declare `custom.createAccounts: true` in their capabilities. * * The actual options accepted by the keyring are implementation-specific - * and not validated by this struct beyond the `type` field. + * and not validated by this struct beyond the `type` field. Adaptors should + * handle any additional options as needed and add type intersections as necessary. */ export type CreateAccountCustomOptions = Infer< typeof CreateAccountCustomOptionsStruct From 31015612467f928bfb294fa4da95f19696c101cd Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 8 Jan 2026 14:59:57 +0100 Subject: [PATCH 4/6] fix: address PR feedbacks --- packages/keyring-api/src/api/account-options.ts | 2 ++ packages/keyring-api/src/api/v2/create-account/custom.ts | 4 ++-- packages/keyring-api/src/api/v2/keyring-capabilities.ts | 4 ---- packages/keyring-api/src/api/v2/keyring.test-d.ts | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/keyring-api/src/api/account-options.ts b/packages/keyring-api/src/api/account-options.ts index b6a69eea3..a8fbbeb26 100644 --- a/packages/keyring-api/src/api/account-options.ts +++ b/packages/keyring-api/src/api/account-options.ts @@ -119,6 +119,8 @@ export const KeyringAccountEntropyOptionsStruct = selectiveUnion( return KeyringAccountEntropyPrivateKeyOptionsStruct; case KeyringAccountEntropyTypeOption.Custom: return KeyringAccountEntropyCustomOptionsStruct; + case KeyringAccountEntropyTypeOption.Mnemonic: + return KeyringAccountEntropyMnemonicOptionsStruct; default: return KeyringAccountEntropyMnemonicOptionsStruct; } diff --git a/packages/keyring-api/src/api/v2/create-account/custom.ts b/packages/keyring-api/src/api/v2/create-account/custom.ts index f3d429bdd..ceb5b4be0 100644 --- a/packages/keyring-api/src/api/v2/create-account/custom.ts +++ b/packages/keyring-api/src/api/v2/create-account/custom.ts @@ -1,9 +1,9 @@ -import { literal, object, type Infer } from '@metamask/superstruct'; +import { literal, type, type Infer } from '@metamask/superstruct'; /** * Struct for {@link CreateAccountCustomOptions}. */ -export const CreateAccountCustomOptionsStruct = object({ +export const CreateAccountCustomOptionsStruct = type({ /** * The type of the options. */ diff --git a/packages/keyring-api/src/api/v2/keyring-capabilities.ts b/packages/keyring-api/src/api/v2/keyring-capabilities.ts index b05cb625c..47c3e9cce 100644 --- a/packages/keyring-api/src/api/v2/keyring-capabilities.ts +++ b/packages/keyring-api/src/api/v2/keyring-capabilities.ts @@ -66,11 +66,7 @@ export const KeyringCapabilitiesStruct = object({ custom: exactOptional( partial( object({ - deserialize: boolean(), - getAccount: boolean(), createAccounts: boolean(), - deleteAccount: boolean(), - exportAccount: boolean(), }), ), ), diff --git a/packages/keyring-api/src/api/v2/keyring.test-d.ts b/packages/keyring-api/src/api/v2/keyring.test-d.ts index 41d954512..d320dc336 100644 --- a/packages/keyring-api/src/api/v2/keyring.test-d.ts +++ b/packages/keyring-api/src/api/v2/keyring.test-d.ts @@ -82,7 +82,7 @@ expectAssignable({ }, }); -expectAssignable({ +expectNotAssignable({ scopes: ['eip155:1'], custom: { createAccounts: true, From cf5490b3adba7fc0ffea20f65f10abd016349a56 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 8 Jan 2026 17:13:05 +0100 Subject: [PATCH 5/6] fix: address PR feedback --- packages/keyring-api/CHANGELOG.md | 2 +- packages/keyring-api/src/api/account-options.test.ts | 5 ++++- packages/keyring-api/src/api/account-options.ts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 7874827d5..c7b3cfff3 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add support for custom capabilities and entropy types in `KeyringV2` ([#415](https://github.com/MetaMask/accounts/pull/415)) - - Add `custom` capability to `KeyringCapabilities` for keyrings with non-standard method options. + - Add `custom` capability to `KeyringCapabilities` for keyrings with non-standard `createAccounts` method. - Add `KeyringAccountEntropyTypeOption.Custom` for custom/opaque entropy sources. - Add `AccountCreationType.Custom` and `CreateAccountCustomOptions` for custom account creation flows. - Add `EthKeyringWrapper` abstract class for Ethereum-based `KeyringV2` implementations ([#404](https://github.com/MetaMask/accounts/pull/404)) diff --git a/packages/keyring-api/src/api/account-options.test.ts b/packages/keyring-api/src/api/account-options.test.ts index bb2370fb2..27a9cae82 100644 --- a/packages/keyring-api/src/api/account-options.test.ts +++ b/packages/keyring-api/src/api/account-options.test.ts @@ -34,7 +34,10 @@ describe('api', () => { exportable: true, }, { - entropy: { type: KeyringAccountEntropyTypeOption.Custom }, + entropy: { + type: KeyringAccountEntropyTypeOption.Custom, + customProperty: 123, + }, exportable: false, }, { diff --git a/packages/keyring-api/src/api/account-options.ts b/packages/keyring-api/src/api/account-options.ts index a8fbbeb26..3464b28b4 100644 --- a/packages/keyring-api/src/api/account-options.ts +++ b/packages/keyring-api/src/api/account-options.ts @@ -90,8 +90,10 @@ export type KeyringAccountEntropyPrivateKeyOptions = Infer< * This is an opaque type where the entropy source is managed internally by the keyring. * It behaves similarly to a private key import but allows keyrings to define their own * entropy management strategy. + * + * We use `type` instead of `object` here to allow extra, keyring-specific properties. */ -export const KeyringAccountEntropyCustomOptionsStruct = object({ +export const KeyringAccountEntropyCustomOptionsStruct = type({ /** * Indicates that the account was created with custom, keyring-specific entropy. */ From 69f8b5d5b79274238f560926da806f9a85bdd028 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Fri, 9 Jan 2026 16:12:14 +0100 Subject: [PATCH 6/6] fix: revert custom type strictness --- packages/keyring-api/src/api/account-options.test.ts | 7 ------- packages/keyring-api/src/api/account-options.ts | 4 +--- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/keyring-api/src/api/account-options.test.ts b/packages/keyring-api/src/api/account-options.test.ts index 27a9cae82..1dd556860 100644 --- a/packages/keyring-api/src/api/account-options.test.ts +++ b/packages/keyring-api/src/api/account-options.test.ts @@ -33,13 +33,6 @@ describe('api', () => { entropy: { type: KeyringAccountEntropyTypeOption.Custom }, exportable: true, }, - { - entropy: { - type: KeyringAccountEntropyTypeOption.Custom, - customProperty: 123, - }, - exportable: false, - }, { entropy: { ...baseEntropyMnemonicOptions, diff --git a/packages/keyring-api/src/api/account-options.ts b/packages/keyring-api/src/api/account-options.ts index 3464b28b4..a8fbbeb26 100644 --- a/packages/keyring-api/src/api/account-options.ts +++ b/packages/keyring-api/src/api/account-options.ts @@ -90,10 +90,8 @@ export type KeyringAccountEntropyPrivateKeyOptions = Infer< * This is an opaque type where the entropy source is managed internally by the keyring. * It behaves similarly to a private key import but allows keyrings to define their own * entropy management strategy. - * - * We use `type` instead of `object` here to allow extra, keyring-specific properties. */ -export const KeyringAccountEntropyCustomOptionsStruct = type({ +export const KeyringAccountEntropyCustomOptionsStruct = object({ /** * Indicates that the account was created with custom, keyring-specific entropy. */