From cebf6281a3d35d3006bf1eef7c4ec198e1be751c Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 2 Apr 2026 22:08:42 +0200 Subject: [PATCH 1/3] feat(keyring-controller): add withKeyringV2 and deprecate withKeyring --- packages/keyring-controller/CHANGELOG.md | 8 + packages/keyring-controller/package.json | 4 +- .../src/KeyringController.test.ts | 200 ++++++++++++++++++ .../src/KeyringController.ts | 143 ++++++++++++- packages/keyring-controller/src/constants.ts | 1 + yarn.lock | 33 ++- 6 files changed, 376 insertions(+), 13 deletions(-) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index ea9fb2f7f8b..94e119c3fe9 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added `withKeyringV2` method and `KeyringController:withKeyringV2` messenger action for atomic operations using the `KeyringV2` API ([#XXX](https://github.com/MetaMask/core/pull/XXX)) + - Selects a V1 keyring, wraps it in an ephemeral `KeyringV2` adapter via a registered `KeyringV2Builder`, and passes it to the callback. + - Accepts a `KeyringSelectorV2` (alias for `KeyringSelector`) to select keyrings by `type`, `address`, `id`, or `filter`. + - Ships with default V2 builders for HD (`HdKeyringV2`) and Simple (`SimpleKeyringV2`) keyrings; additional builders can be registered via the `keyringV2Builders` constructor option. - Added `filter` selector variant to `withKeyring` ([#8348](https://github.com/MetaMask/core/pull/8348)) - `KeyringSelector` now accepts `{ filter: ({ keyring, metadata }) => boolean }`, which selects the first keyring for which the predicate returns `true`. - Add `isKeyringNotFoundError` ([#8351](https://github.com/MetaMask/core/pull/8351)) @@ -25,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Can be used to read immutable data safely. - Add `KeyringTypes.money` enum value ([#8360](https://github.com/MetaMask/core/pull/8360)) +### Changed + +- Deprecated `withKeyring` in favor of `withKeyringV2`, which supports the `KeyringV2` API. + ## [25.1.1] ### Changed diff --git a/packages/keyring-controller/package.json b/packages/keyring-controller/package.json index 35cfdab48b7..55509a7ccb1 100644 --- a/packages/keyring-controller/package.json +++ b/packages/keyring-controller/package.json @@ -50,9 +50,9 @@ "@ethereumjs/util": "^9.1.0", "@metamask/base-controller": "^9.0.1", "@metamask/browser-passworder": "^6.0.0", - "@metamask/eth-hd-keyring": "^13.0.0", + "@metamask/eth-hd-keyring": "^13.1.0", "@metamask/eth-sig-util": "^8.2.0", - "@metamask/eth-simple-keyring": "^11.0.0", + "@metamask/eth-simple-keyring": "^11.1.1", "@metamask/keyring-api": "^21.6.0", "@metamask/keyring-internal-api": "^10.0.0", "@metamask/messenger": "^1.1.0", diff --git a/packages/keyring-controller/src/KeyringController.test.ts b/packages/keyring-controller/src/KeyringController.test.ts index 5ead0613292..b08bc63c023 100644 --- a/packages/keyring-controller/src/KeyringController.test.ts +++ b/packages/keyring-controller/src/KeyringController.test.ts @@ -4027,6 +4027,7 @@ describe('KeyringController', () => { await expect( controller.withKeyringUnsafe({ type: 'NonExistentType' }, fn), ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); + expect(fn).not.toHaveBeenCalled(); }); }); @@ -4143,6 +4144,205 @@ describe('KeyringController', () => { }); }); + describe('withKeyringV2', () => { + it('should wrap the V1 keyring using the default builder and call the operation', async () => { + await withController(async ({ controller }) => { + const fn = jest.fn(); + await controller.withKeyringV2({ type: KeyringTypes.hd }, fn); + + expect(fn).toHaveBeenCalledWith( + expect.objectContaining({ + keyring: expect.any(Object), + metadata: expect.objectContaining({ id: expect.any(String) }), + }), + ); + }); + }); + + it('should return the result of the operation', async () => { + await withController(async ({ controller }) => { + const result = await controller.withKeyringV2( + { type: KeyringTypes.hd }, + async () => 'result-value', + ); + + expect(result).toBe('result-value'); + }); + }); + + it('should throw KeyringNotFound when no keyring matches', async () => { + await withController(async ({ controller }) => { + const fn = jest.fn(); + + await expect( + controller.withKeyringV2({ type: 'non-existent' }, fn), + ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); + + expect(fn).not.toHaveBeenCalled(); + }); + }); + + it('should throw KeyringV2NotSupported when no builder is registered for the type', async () => { + await withController( + { + keyringBuilders: [keyringBuilderFactory(MockShallowKeyring)], + }, + async ({ controller }) => { + await controller.addNewKeyring(MockShallowKeyring.type); + + const fn = jest.fn(); + await expect( + controller.withKeyringV2( + { type: MockShallowKeyring.type }, + fn, + ), + ).rejects.toThrow( + KeyringControllerErrorMessage.KeyringV2NotSupported, + ); + + expect(fn).not.toHaveBeenCalled(); + }, + ); + }); + + it('should throw an error if the callback returns the wrapped keyring', async () => { + await withController(async ({ controller }) => { + await expect( + controller.withKeyringV2( + { type: KeyringTypes.hd }, + async ({ keyring }) => keyring, + ), + ).rejects.toThrow( + KeyringControllerErrorMessage.UnsafeDirectKeyringAccess, + ); + }); + }); + + it('should throw an error if the controller is locked', async () => { + await withController(async ({ controller }) => { + await controller.setLocked(); + + await expect( + controller.withKeyringV2( + { type: KeyringTypes.hd }, + jest.fn(), + ), + ).rejects.toThrow(KeyringControllerErrorMessage.ControllerLocked); + }); + }); + + describe('when the keyring is selected by address', () => { + it('should wrap the V1 keyring that holds the given address', async () => { + await withController(async ({ controller, initialState }) => { + const fn = jest.fn(); + const address = initialState.keyrings[0].accounts[0] as Hex; + + await controller.withKeyringV2({ address }, fn); + + expect(fn).toHaveBeenCalledWith( + expect.objectContaining({ + keyring: expect.any(Object), + metadata: expect.objectContaining({ id: expect.any(String) }), + }), + ); + }); + }); + }); + + describe('when the keyring is selected by id', () => { + it('should wrap the V1 keyring with the matching metadata id', async () => { + await withController(async ({ controller, initialState }) => { + const fn = jest.fn(); + const keyringId = initialState.keyrings[0].metadata.id; + + await controller.withKeyringV2({ id: keyringId }, fn); + + expect(fn).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: expect.objectContaining({ id: keyringId }), + }), + ); + }); + }); + + it('should throw KeyringNotFound if no keyring has the id', async () => { + await withController(async ({ controller }) => { + await expect( + controller.withKeyringV2( + { id: 'non-existent-id' }, + jest.fn(), + ), + ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); + }); + }); + }); + + describe('when the keyring is selected by filter', () => { + it('should wrap the V1 keyring that matches the filter', async () => { + await withController(async ({ controller }) => { + const fn = jest.fn(); + await controller.withKeyringV2( + { filter: (k): boolean => k.type === KeyringTypes.hd }, + fn, + ); + + expect(fn).toHaveBeenCalledWith( + expect.objectContaining({ + keyring: expect.any(Object), + metadata: expect.objectContaining({ id: expect.any(String) }), + }), + ); + }); + }); + + it('should throw KeyringNotFound if no keyring matches the filter', async () => { + await withController(async ({ controller }) => { + await expect( + controller.withKeyringV2( + { filter: (): boolean => false }, + jest.fn(), + ), + ).rejects.toThrow(KeyringControllerErrorMessage.KeyringNotFound); + }); + }); + }); + + describe('rollback', () => { + it('should rollback the underlying V1 keyring if the operation throws', async () => { + await withController(async ({ controller, initialState }) => { + await expect( + controller.withKeyringV2( + { type: KeyringTypes.hd }, + async () => { + throw new Error('Rollback test'); + }, + ), + ).rejects.toThrow('Rollback test'); + + expect(controller.state.keyrings[0].accounts).toStrictEqual( + initialState.keyrings[0].accounts, + ); + }); + }); + }); + + describe('messenger action', () => { + it('should be callable through the messenger', async () => { + await withController(async ({ messenger }) => { + const fn = jest.fn(); + + await messenger.call( + 'KeyringController:withKeyringV2', + { type: KeyringTypes.hd }, + fn, + ); + + expect(fn).toHaveBeenCalled(); + }); + }); + }); + }); + describe('isCustodyKeyring', () => { it('should return true if keyring is custody keyring', () => { expect(isCustodyKeyring('Custody JSON-RPC')).toBe(true); diff --git a/packages/keyring-controller/src/KeyringController.ts b/packages/keyring-controller/src/KeyringController.ts index bb6fa5d85b2..8dea79b3e82 100644 --- a/packages/keyring-controller/src/KeyringController.ts +++ b/packages/keyring-controller/src/KeyringController.ts @@ -2,15 +2,16 @@ import type { TypedTransaction, TypedTxData } from '@ethereumjs/tx'; import { isValidPrivate, getBinarySize } from '@ethereumjs/util'; import { BaseController } from '@metamask/base-controller'; import type * as encryptorUtils from '@metamask/browser-passworder'; -import { HdKeyring } from '@metamask/eth-hd-keyring'; +import { HdKeyring, HdKeyringV2 } from '@metamask/eth-hd-keyring'; import { normalize as ethNormalize } from '@metamask/eth-sig-util'; -import SimpleKeyring from '@metamask/eth-simple-keyring'; +import SimpleKeyring, { SimpleKeyringV2 } from '@metamask/eth-simple-keyring'; import type { KeyringExecutionContext, EthBaseTransaction, EthBaseUserOperation, EthUserOperation, EthUserOperationPatch, + KeyringV2, } from '@metamask/keyring-api'; import type { EthKeyring } from '@metamask/keyring-internal-api'; import type { Keyring, KeyringClass } from '@metamask/keyring-utils'; @@ -195,6 +196,11 @@ export type KeyringControllerWithKeyringUnsafeAction = { handler: KeyringController['withKeyringUnsafe']; }; +export type KeyringControllerWithKeyringV2Action = { + type: `${typeof name}:withKeyringV2`; + handler: KeyringController['withKeyringV2']; +}; + export type KeyringControllerCreateNewVaultAndKeychainAction = { type: `${typeof name}:createNewVaultAndKeychain`; handler: KeyringController['createNewVaultAndKeychain']; @@ -253,6 +259,7 @@ export type KeyringControllerActions = | KeyringControllerAddNewAccountAction | KeyringControllerWithKeyringAction | KeyringControllerWithKeyringUnsafeAction + | KeyringControllerWithKeyringV2Action | KeyringControllerAddNewKeyringAction | KeyringControllerCreateNewVaultAndKeychainAction | KeyringControllerCreateNewVaultAndRestoreAction @@ -277,6 +284,7 @@ export type KeyringControllerOptions< EncryptionResultConstraint = DefaultEncryptionResult, > = { keyringBuilders?: { (): EthKeyring; type: string }[]; + keyringV2Builders?: KeyringV2Builder[]; messenger: KeyringControllerMessenger; state?: { vault?: string; keyringsMetadata?: KeyringMetadata[] }; encryptor: Encryptor< @@ -551,6 +559,17 @@ export type KeyringBuilder = { type: string; }; +/** + * A builder that wraps a legacy `Keyring` into a `KeyringV2` adapter. + * + * The controller calls the builder every time `withKeyringV2` is + * invoked; the resulting wrapper is not cached. + */ +export type KeyringV2Builder = { + (keyring: Keyring, metadata: KeyringMetadata): KeyringV2; + type: string; +}; + /** * A function executed within a mutually exclusive lock, with * a mutex releaser in its option bag. @@ -588,6 +607,28 @@ const defaultKeyringBuilders = [ keyringBuilderFactory(HdKeyring), ]; +const hdKeyringV2Builder: KeyringV2Builder = Object.assign( + (keyring: Keyring, metadata: KeyringMetadata): KeyringV2 => + new HdKeyringV2({ + legacyKeyring: keyring as HdKeyring, + entropySource: metadata.id, + }), + { type: KeyringTypes.hd as string }, +); + +const simpleKeyringV2Builder: KeyringV2Builder = Object.assign( + (keyring: Keyring): KeyringV2 => + new SimpleKeyringV2({ + legacyKeyring: keyring as SimpleKeyring, + }), + { type: KeyringTypes.simple as string }, +); + +const defaultKeyringV2Builders: KeyringV2Builder[] = [ + simpleKeyringV2Builder, + hdKeyringV2Builder, +]; + export const getDefaultKeyringState = (): KeyringControllerState => { return { isUnlocked: false, @@ -751,6 +792,8 @@ export class KeyringController< readonly #keyringBuilders: { (): EthKeyring; type: string }[]; + readonly #keyringV2Builders: KeyringV2Builder[]; + readonly #encryptor: Encryptor< EncryptionKey, SupportedKeyDerivationOptions, @@ -780,7 +823,8 @@ export class KeyringController< EncryptionResult >, ) { - const { encryptor, keyringBuilders, messenger, state } = options; + const { encryptor, keyringBuilders, keyringV2Builders, messenger, state } = + options; super({ name, @@ -827,6 +871,10 @@ export class KeyringController< ? keyringBuilders.concat(defaultKeyringBuilders) : defaultKeyringBuilders; + this.#keyringV2Builders = keyringV2Builders + ? keyringV2Builders.concat(defaultKeyringV2Builders) + : defaultKeyringV2Builders; + this.#encryptor = encryptor; this.#keyrings = []; this.#unsupportedKeyrings = []; @@ -1750,7 +1798,7 @@ export class KeyringController< * @returns Promise resolving to the result of the function execution. * @template SelectedKeyring - The type of the selected keyring. * @template CallbackResult - The type of the value resolved by the callback function. - * @deprecated This method overload is deprecated. Use `withKeyring` without options instead. + * @deprecated Use `withKeyringV2` instead, which supports the KeyringV2 API. */ async withKeyring< SelectedKeyring extends EthKeyring = EthKeyring, @@ -1784,6 +1832,7 @@ export class KeyringController< * @returns Promise resolving to the result of the function execution. * @template SelectedKeyring - The type of the selected keyring. * @template CallbackResult - The type of the value resolved by the callback function. + * @deprecated Use `withKeyringV2` instead, which supports the KeyringV2 API. */ async withKeyring< SelectedKeyring extends EthKeyring = EthKeyring, @@ -1914,6 +1963,77 @@ export class KeyringController< ); } + /** + * Select a keyring, wrap it in a `KeyringV2` adapter, and execute + * the given operation with the wrapped keyring as a mutually + * exclusive atomic operation. + * + * We re-wrap the keyring in a `KeyringV2` adapter on each invocation, + * since V2 wrappers are ephemeral adapters created on-the-fly, and cheap to create. + * + * The method automatically persists changes at the end of the + * function execution, or rolls back the changes if an error + * is thrown. + * + * A `KeyringV2Builder` for the selected keyring's type must exist + * (either as a default or registered via the `keyringV2Builders` + * constructor option); otherwise an error is thrown. + * + * Selection is performed against the V1 keyrings in `#keyrings`, since + * V2 wrappers are ephemeral adapters created on-the-fly. + * + * @param selector - Keyring selector object. + * @param operation - Function to execute with the wrapped V2 keyring. + * @returns Promise resolving to the result of the function execution. + * @template CallbackResult - The type of the value resolved by the callback function. + */ + async withKeyringV2( + selector: KeyringSelector, + operation: ({ + keyring, + metadata, + }: { + keyring: KeyringV2; + metadata: KeyringMetadata; + }) => Promise, + ): Promise { + this.#assertIsUnlocked(); + + return this.#persistOrRollback(async () => { + const v1Keyring = await this.#selectKeyring(selector); + + if (!v1Keyring) { + throw new KeyringControllerError( + KeyringControllerErrorMessage.KeyringNotFound, + ); + } + + const metadata = this.#getKeyringMetadata(v1Keyring); + const builder = this.#getKeyringV2BuilderForType(v1Keyring.type); + + if (!builder) { + throw new KeyringControllerError( + KeyringControllerErrorMessage.KeyringV2NotSupported, + ); + } + + const wrappedKeyring = builder(v1Keyring, metadata); + + const result = await operation({ + keyring: wrappedKeyring, + metadata, + }); + + if (Object.is(result, wrappedKeyring)) { + throw new KeyringControllerError( + KeyringControllerErrorMessage.UnsafeDirectKeyringAccess, + ); + } + + return result; + }); + } + async getAccountKeyringType(account: string): Promise { this.#assertIsUnlocked(); @@ -2006,6 +2126,11 @@ export class KeyringController< this.withKeyringUnsafe.bind(this), ); + this.messenger.registerActionHandler( + `${name}:withKeyringV2`, + this.withKeyringV2.bind(this), + ); + this.messenger.registerActionHandler( `${name}:addNewKeyring`, this.addNewKeyring.bind(this), @@ -2117,6 +2242,16 @@ export class KeyringController< ); } + /** + * Get the V2 keyring builder for the given `type`. + * + * @param type - The type of keyring to get the builder for. + * @returns The V2 keyring builder, or undefined if none exists. + */ + #getKeyringV2BuilderForType(type: string): KeyringV2Builder | undefined { + return this.#keyringV2Builders.find((builder) => builder.type === type); + } + /** * Create new vault with an initial keyring * diff --git a/packages/keyring-controller/src/constants.ts b/packages/keyring-controller/src/constants.ts index 3e7355fac00..a5b71a6408a 100644 --- a/packages/keyring-controller/src/constants.ts +++ b/packages/keyring-controller/src/constants.ts @@ -38,4 +38,5 @@ export enum KeyringControllerErrorMessage { ControllerLockRequired = 'KeyringController - attempt to update vault during a non mutually exclusive operation', LastAccountInPrimaryKeyring = 'KeyringController - Last account in primary keyring cannot be removed', EncryptionKeyNotSet = 'KeyringController - Encryption key not set', + KeyringV2NotSupported = 'KeyringController - The selected keyring does not support the KeyringV2 API.', } diff --git a/yarn.lock b/yarn.lock index d156862a4b7..85d80431e75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3798,16 +3798,18 @@ __metadata: languageName: node linkType: hard -"@metamask/eth-simple-keyring@npm:^11.0.0": - version: 11.0.0 - resolution: "@metamask/eth-simple-keyring@npm:11.0.0" +"@metamask/eth-simple-keyring@npm:^11.1.1": + version: 11.1.1 + resolution: "@metamask/eth-simple-keyring@npm:11.1.1" dependencies: "@ethereumjs/util": "npm:^9.1.0" "@metamask/eth-sig-util": "npm:^8.2.0" - "@metamask/utils": "npm:^11.1.0" + "@metamask/keyring-api": "npm:^22.0.0" + "@metamask/keyring-sdk": "npm:^1.1.0" + "@metamask/utils": "npm:^11.10.0" ethereum-cryptography: "npm:^2.1.2" randombytes: "npm:^2.1.0" - checksum: 10/fba27f2db11ad7ee3aceea6746e32f2875a692bd12a31a18ed63f6c637a9ecd990ed1b55423d6c010380a8539b39d627c72ffedbdc44b88512778426df71d26d + checksum: 10/de23af73a97b4c5f28e8203deadfc61d4736e9d267f3d022acd9c6eff29ac3685d2110cfcccff65d7a7fd66c47986cd41b6b2591da9803091c87d12280393f12 languageName: node linkType: hard @@ -4192,9 +4194,9 @@ __metadata: "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/base-controller": "npm:^9.0.1" "@metamask/browser-passworder": "npm:^6.0.0" - "@metamask/eth-hd-keyring": "npm:^13.0.0" + "@metamask/eth-hd-keyring": "npm:^13.1.0" "@metamask/eth-sig-util": "npm:^8.2.0" - "@metamask/eth-simple-keyring": "npm:^11.0.0" + "@metamask/eth-simple-keyring": "npm:^11.1.1" "@metamask/keyring-api": "npm:^21.6.0" "@metamask/keyring-internal-api": "npm:^10.0.0" "@metamask/keyring-utils": "npm:^3.1.0" @@ -4243,6 +4245,23 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-sdk@npm:^1.1.0": + version: 1.1.0 + resolution: "@metamask/keyring-sdk@npm:1.1.0" + dependencies: + "@ethereumjs/tx": "npm:^5.4.0" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/keyring-api": "npm:^22.0.0" + "@metamask/keyring-utils": "npm:^3.2.0" + "@metamask/scure-bip39": "npm:^2.1.1" + "@metamask/superstruct": "npm:^3.1.0" + "@metamask/utils": "npm:^11.10.0" + async-mutex: "npm:^0.5.0" + uuid: "npm:^9.0.1" + checksum: 10/1c5f686e76ba65e7b164bae7e9a086648edbe485350f8fbb0c8e82b242464663be0489f11be9e2cc3ed13db2ae57dff8e41b853fbf72ecb50be380c4d212ce1a + languageName: node + linkType: hard + "@metamask/keyring-snap-client@npm:^8.2.0": version: 8.2.0 resolution: "@metamask/keyring-snap-client@npm:8.2.0" From 457f1d3d4f1f5c1480dddca45a899428dc0296ce Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 2 Apr 2026 22:10:59 +0200 Subject: [PATCH 2/3] fix: update CHANGELOG --- packages/keyring-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 94e119c3fe9..503c170eec0 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added `withKeyringV2` method and `KeyringController:withKeyringV2` messenger action for atomic operations using the `KeyringV2` API ([#XXX](https://github.com/MetaMask/core/pull/XXX)) +- Added `withKeyringV2` method and `KeyringController:withKeyringV2` messenger action for atomic operations using the `KeyringV2` API ([#8372](https://github.com/MetaMask/core/pull/8372)) - Selects a V1 keyring, wraps it in an ephemeral `KeyringV2` adapter via a registered `KeyringV2Builder`, and passes it to the callback. - Accepts a `KeyringSelectorV2` (alias for `KeyringSelector`) to select keyrings by `type`, `address`, `id`, or `filter`. - Ships with default V2 builders for HD (`HdKeyringV2`) and Simple (`SimpleKeyringV2`) keyrings; additional builders can be registered via the `keyringV2Builders` constructor option. From 5b9a85a4b9bfb035484369eb6cb510f0cba5df4c Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 2 Apr 2026 22:12:51 +0200 Subject: [PATCH 3/3] fix: update CHANGELOG --- packages/keyring-controller/CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/keyring-controller/CHANGELOG.md b/packages/keyring-controller/CHANGELOG.md index 503c170eec0..58155e8846b 100644 --- a/packages/keyring-controller/CHANGELOG.md +++ b/packages/keyring-controller/CHANGELOG.md @@ -7,18 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `withKeyringV2` method and `KeyringController:withKeyringV2` messenger action for atomic operations using the `KeyringV2` API ([#8372](https://github.com/MetaMask/core/pull/8372)) + - Selects a V1 keyring, wraps it in an ephemeral `KeyringV2` adapter via a registered `KeyringV2Builder`, and passes it to the callback. + - Accepts a `KeyringSelectorV2` (alias for `KeyringSelector`) to select keyrings by `type`, `address`, `id`, or `filter`. + - Ships with default V2 builders for HD (`HdKeyringV2`) and Simple (`SimpleKeyringV2`) keyrings; additional builders can be registered via the `keyringV2Builders` constructor option. + ### Changed +- Deprecated `withKeyring` in favor of `withKeyringV2`, which supports the `KeyringV2` API. - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.0` ([#8364](https://github.com/MetaMask/core/pull/8364)) ## [25.2.0] ### Added -- Added `withKeyringV2` method and `KeyringController:withKeyringV2` messenger action for atomic operations using the `KeyringV2` API ([#8372](https://github.com/MetaMask/core/pull/8372)) - - Selects a V1 keyring, wraps it in an ephemeral `KeyringV2` adapter via a registered `KeyringV2Builder`, and passes it to the callback. - - Accepts a `KeyringSelectorV2` (alias for `KeyringSelector`) to select keyrings by `type`, `address`, `id`, or `filter`. - - Ships with default V2 builders for HD (`HdKeyringV2`) and Simple (`SimpleKeyringV2`) keyrings; additional builders can be registered via the `keyringV2Builders` constructor option. - Added `filter` selector variant to `withKeyring` ([#8348](https://github.com/MetaMask/core/pull/8348)) - `KeyringSelector` now accepts `{ filter: ({ keyring, metadata }) => boolean }`, which selects the first keyring for which the predicate returns `true`. - Add `isKeyringNotFoundError` ([#8351](https://github.com/MetaMask/core/pull/8351)) @@ -29,10 +33,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Can be used to read immutable data safely. - Add `KeyringTypes.money` enum value ([#8360](https://github.com/MetaMask/core/pull/8360)) -### Changed - -- Deprecated `withKeyring` in favor of `withKeyringV2`, which supports the `KeyringV2` API. - ## [25.1.1] ### Changed