diff --git a/packages/keyring-api/CHANGELOG.md b/packages/keyring-api/CHANGELOG.md index 25a48270b..c7b3cfff3 100644 --- a/packages/keyring-api/CHANGELOG.md +++ b/packages/keyring-api/CHANGELOG.md @@ -9,6 +9,10 @@ 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 `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)) - 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..1dd556860 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,22 @@ 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: { ...baseEntropyMnemonicOptions, @@ -55,7 +71,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..a8fbbeb26 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,46 @@ 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; + case KeyringAccountEntropyTypeOption.Mnemonic: + return KeyringAccountEntropyMnemonicOptionsStruct; + default: + return KeyringAccountEntropyMnemonicOptionsStruct; + } }, ); @@ -100,8 +137,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 +166,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..ceb5b4be0 --- /dev/null +++ b/packages/keyring-api/src/api/v2/create-account/custom.ts @@ -0,0 +1,26 @@ +import { literal, type, type Infer } from '@metamask/superstruct'; + +/** + * Struct for {@link CreateAccountCustomOptions}. + */ +export const CreateAccountCustomOptionsStruct = type({ + /** + * 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. Adaptors should + * handle any additional options as needed and add type intersections as necessary. + */ +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..47c3e9cce 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,20 @@ 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({ + createAccounts: 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..d320dc336 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, + }, +}); + +expectNotAssignable({ + 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,