From e939b223783d3289262f7580daedd1c997de31bc Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 14:47:17 -0400 Subject: [PATCH 01/11] refactor: replace withKeyring with withKeyringV2 in multichain account service types and base provider --- .../src/providers/BaseBip44AccountProvider.ts | 6 +++--- .../src/providers/BtcAccountProvider.test.ts | 2 +- .../src/providers/SnapAccountProvider.test.ts | 2 +- .../src/providers/SolAccountProvider.test.ts | 2 +- .../src/providers/TrxAccountProvider.test.ts | 2 +- packages/multichain-account-service/src/tests/messenger.ts | 2 +- packages/multichain-account-service/src/types.ts | 4 ++-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts b/packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts index 969ea506de3..35f1704f776 100644 --- a/packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts +++ b/packages/multichain-account-service/src/providers/BaseBip44AccountProvider.ts @@ -8,7 +8,7 @@ import type { } from '@metamask/keyring-api'; import type { KeyringMetadata, - KeyringSelector, + KeyringSelectorV2, } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; @@ -156,7 +156,7 @@ export abstract class BaseBip44AccountProvider< } protected async withKeyring( - selector: KeyringSelector, + selector: KeyringSelectorV2, operation: ({ keyring, metadata, @@ -166,7 +166,7 @@ export abstract class BaseBip44AccountProvider< }) => Promise, ): Promise { const result = await this.messenger.call( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', selector, ({ keyring, metadata }) => operation({ diff --git a/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts b/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts index 98dc3a8dd7b..8e8d79ca3ff 100644 --- a/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts @@ -212,7 +212,7 @@ function setup({ ); messenger.registerActionHandler( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', async (_, operation) => operation({ // We type-cast here, since `withKeyring` defaults to `EthKeyring` and the diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts index af33672f585..49a7aec51a1 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts @@ -231,7 +231,7 @@ const setup = ({ }; messenger.registerActionHandler( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', jest .fn() .mockImplementation( diff --git a/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts index 50935bdac49..2a216f9dbbb 100644 --- a/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts @@ -201,7 +201,7 @@ function setup({ ); messenger.registerActionHandler( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', async (_, operation) => operation({ // We type-cast here, since `withKeyring` defaults to `EthKeyring` and the diff --git a/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts b/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts index adb163ddd52..64c14f5e27a 100644 --- a/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts @@ -192,7 +192,7 @@ function setup({ ); messenger.registerActionHandler( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', async (_, operation) => operation({ // We type-cast here, since `withKeyring` defaults to `EthKeyring` and the diff --git a/packages/multichain-account-service/src/tests/messenger.ts b/packages/multichain-account-service/src/tests/messenger.ts index e6f3136b5a4..04af0a95801 100644 --- a/packages/multichain-account-service/src/tests/messenger.ts +++ b/packages/multichain-account-service/src/tests/messenger.ts @@ -64,7 +64,7 @@ export function getMultichainAccountServiceMessenger( 'AccountsController:listMultichainAccounts', 'SnapController:getState', 'SnapController:handleRequest', - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', 'KeyringController:getState', 'KeyringController:getKeyringsByType', 'KeyringController:addNewKeyring', diff --git a/packages/multichain-account-service/src/types.ts b/packages/multichain-account-service/src/types.ts index 8817722e102..d136086d4f9 100644 --- a/packages/multichain-account-service/src/types.ts +++ b/packages/multichain-account-service/src/types.ts @@ -22,7 +22,7 @@ import type { KeyringControllerGetStateAction, KeyringControllerRemoveAccountAction, KeyringControllerStateChangeEvent, - KeyringControllerWithKeyringAction, + KeyringControllerWithKeyringV2Action, } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; import type { @@ -78,7 +78,7 @@ type AllowedActions = | AccountsControllerGetAccountsAction | AccountsControllerGetAccountAction | AccountsControllerGetAccountByAddressAction - | KeyringControllerWithKeyringAction + | KeyringControllerWithKeyringV2Action | KeyringControllerGetStateAction | KeyringControllerGetKeyringsByTypeAction | KeyringControllerAddNewKeyringAction From 6b3d7111d9f121eb3b1390d87d56ceec7ab68e80 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 15:21:07 -0400 Subject: [PATCH 02/11] refactor: migrate EvmAccountProvider to use KeyringV2 interface --- .../src/providers/EvmAccountProvider.test.ts | 157 +++++++---------- .../src/providers/EvmAccountProvider.ts | 160 +++++++++--------- 2 files changed, 140 insertions(+), 177 deletions(-) diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts index ccd9cac13cb..383cf0aab6f 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts @@ -1,18 +1,13 @@ -import { publicToAddress } from '@ethereumjs/util'; import { isBip44Account } from '@metamask/account-api'; import { getUUIDFromAddressOfNormalAccount } from '@metamask/accounts-controller'; import { AccountCreationType } from '@metamask/keyring-api'; +import type { KeyringAccount } from '@metamask/keyring-api'; import type { KeyringMetadata } from '@metamask/keyring-controller'; -import type { - EthKeyring, - InternalAccount, -} from '@metamask/keyring-internal-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { AutoManagedNetworkClient, CustomNetworkClientConfiguration, } from '@metamask/network-controller'; -import type { Hex } from '@metamask/utils'; -import { createBytes } from '@metamask/utils'; import { TraceName } from '../analytics/traces'; import { @@ -34,38 +29,26 @@ import { } from './EvmAccountProvider'; import { TimeoutError } from './utils'; -jest.mock('@ethereumjs/util', () => { - const actual = jest.requireActual('@ethereumjs/util'); - return { - ...actual, - publicToAddress: jest.fn(), - }; -}); - -function mockNextDiscoveryAddress(address: string): void { - jest.mocked(publicToAddress).mockReturnValue(createBytes(address as Hex)); -} - -function mockNextDiscoveryAddressOnce(address: string): void { - jest.mocked(publicToAddress).mockReturnValueOnce(createBytes(address as Hex)); -} - -type MockHdKey = { - deriveChild: jest.Mock; -}; - -function mockHdKey(): MockHdKey { +/** + * Converts an InternalAccount to a minimal KeyringAccount shape + * that satisfies what the controller accesses from the V2 keyring. + * + * @param account - The internal account to convert. + * @returns A KeyringAccount-shaped object. + */ +function toKeyringAccount(account: InternalAccount): KeyringAccount { return { - deriveChild: jest.fn().mockImplementation(() => { - return { - publicKey: new Uint8Array(65), - }; - }), - }; + id: account.id, + address: account.address, + type: account.type, + options: account.options, + methods: account.methods, + scopes: account.scopes ?? [], + } as unknown as KeyringAccount; } -class MockEthKeyring implements EthKeyring { - readonly type = 'MockEthKeyring'; +class MockKeyringV2 { + readonly type = 'MockKeyringV2'; readonly metadata: KeyringMetadata = { id: 'mock-eth-keyring-id', @@ -74,46 +57,43 @@ class MockEthKeyring implements EthKeyring { readonly accounts: InternalAccount[]; - readonly root: MockHdKey; - constructor(accounts: InternalAccount[]) { this.accounts = accounts; - this.root = mockHdKey(); - } - - async serialize(): Promise { - return 'serialized'; - } - - async deserialize(_: string): Promise { - // Not required. } getAccounts = jest .fn() - .mockImplementation(() => this.accounts.map((account) => account.address)); - - addAccounts = jest.fn().mockImplementation((numberOfAccounts: number) => { - const newAccountsIndex = this.accounts.length; - - // Just generate a new address by appending the number of accounts owned by that fake keyring. - for (let i = 0; i < numberOfAccounts; i++) { - this.accounts.push( - MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .mockImplementation(() => this.accounts.map(toKeyringAccount)); + + createAccounts = jest.fn().mockImplementation((options: Record) => { + const newAccounts: InternalAccount[] = []; + + if (options.type === `${AccountCreationType.Bip44DeriveIndex}`) { + const account = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) + .withUuid() + .withAddressSuffix(`${this.accounts.length}`) + .withGroupIndex(this.accounts.length) + .get(); + this.accounts.push(account); + newAccounts.push(account); + } else if (options.type === `${AccountCreationType.Bip44DeriveIndexRange}`) { + const { range } = options; + for (let i = range.from; i <= range.to; i++) { + const account = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) .withUuid() .withAddressSuffix(`${this.accounts.length}`) .withGroupIndex(this.accounts.length) - .get(), - ); + .get(); + this.accounts.push(account); + newAccounts.push(account); + } } - return this.accounts - .slice(newAccountsIndex) - .map((account) => account.address); + return newAccounts.map(toKeyringAccount); }); - removeAccount = jest.fn().mockImplementation((address: string) => { - const index = this.accounts.findIndex((a) => a.address === address); + deleteAccount = jest.fn().mockImplementation((accountId: string) => { + const index = this.accounts.findIndex((a) => a.id === accountId); if (index >= 0) { this.accounts.splice(index, 1); } @@ -146,13 +126,13 @@ function setup({ } = {}): { provider: EvmAccountProvider; messenger: RootMessenger; - keyring: MockEthKeyring; + keyring: MockKeyringV2; mocks: { mockProviderRequest: jest.Mock; mockGetAccount: jest.Mock; }; } { - const keyring = new MockEthKeyring(accounts); + const keyring = new MockKeyringV2(accounts); messenger.registerActionHandler( 'AccountsController:getAccounts', @@ -196,7 +176,7 @@ function setup({ }); messenger.registerActionHandler( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', async (_, operation) => operation({ keyring, metadata: keyring.metadata }), ); @@ -218,8 +198,6 @@ function setup({ }, ); - mockNextDiscoveryAddress('0x123'); - const provider = new EvmAccountProvider( getMultichainAccountServiceMessenger(messenger), config, @@ -326,8 +304,9 @@ describe('EvmAccountProvider', () => { }); expect(newAccounts).toHaveLength(3); - expect(keyring.addAccounts).toHaveBeenCalledTimes(1); - expect(keyring.addAccounts).toHaveBeenCalledWith(3); + // HdKeyringV2 only supports bip44:derive-index, so range creation + // calls createAccounts once per new index. + expect(keyring.createAccounts).toHaveBeenCalledTimes(3); // Verify each account has the correct group index. for (const [index, account] of newAccounts.entries()) { @@ -351,8 +330,12 @@ describe('EvmAccountProvider', () => { }); expect(newAccounts).toHaveLength(3); - expect(keyring.addAccounts).toHaveBeenCalledTimes(1); - expect(keyring.addAccounts).toHaveBeenCalledWith(3); + expect(keyring.createAccounts).toHaveBeenCalledTimes(3); + expect(keyring.createAccounts).toHaveBeenCalledWith({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource: MOCK_HD_KEYRING_1.metadata.id, + groupIndex: 0, + }); }); it('creates a single account when range from equals to', async () => { @@ -381,7 +364,8 @@ describe('EvmAccountProvider', () => { }); expect(newAccounts).toHaveLength(1); - expect(keyring.addAccounts).toHaveBeenCalledTimes(2); // 1 call for range 0-4, 1 call for account 5. + // 5 calls for range 0-4 + 1 call for account 5. + expect(keyring.createAccounts).toHaveBeenCalledTimes(6); expect( isBip44Account(newAccounts[0]) && newAccounts[0].options.entropy.groupIndex, @@ -429,9 +413,8 @@ describe('EvmAccountProvider', () => { expect(newAccounts).toHaveLength(4); expect(newAccounts[0]).toStrictEqual(MOCK_HD_ACCOUNT_1); expect(newAccounts[1]).toStrictEqual(MOCK_HD_ACCOUNT_2); - // Only 2 new accounts should be created (indices 2 and 3) in a single batched call. - expect(keyring.addAccounts).toHaveBeenCalledTimes(1); - expect(keyring.addAccounts).toHaveBeenCalledWith(2); + // Only new accounts (indices 2 and 3) should be created — one call each. + expect(keyring.createAccounts).toHaveBeenCalledTimes(2); }); it('throws if the created account is not BIP-44 compatible', async () => { @@ -516,7 +499,6 @@ describe('EvmAccountProvider', () => { id: expect.any(String), }; - mockNextDiscoveryAddressOnce(account.address); expect( await provider.discoverAccounts({ @@ -550,20 +532,14 @@ describe('EvmAccountProvider', () => { ); }); - it('stops discovery if there is no transaction activity', async () => { - const { provider } = setup({ + it('stops discovery if there is no transaction activity and deletes the created account', async () => { + const { provider, keyring } = setup({ accounts: [], discovery: { transactionCount: '0x0', }, }); - const account = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) - .withAddressSuffix('0') - .get(); - - mockNextDiscoveryAddressOnce(account.address); - expect( await provider.discoverAccounts({ entropySource: MOCK_HD_KEYRING_1.metadata.id, @@ -572,6 +548,9 @@ describe('EvmAccountProvider', () => { ).toStrictEqual([]); expect(provider.getAccounts()).toStrictEqual([]); + // The account was created to peek at the address, then deleted + // because there was no on-chain activity. + expect(keyring.deleteAccount).toHaveBeenCalledTimes(1); }); it('retries RPC request up to 3 times if it fails and throws the last error', async () => { @@ -657,7 +636,6 @@ describe('EvmAccountProvider', () => { id: expect.any(String), }; - mockNextDiscoveryAddressOnce(account.address); // Create provider with custom trace callback const providerWithTrace = new EvmAccountProvider( @@ -695,7 +673,6 @@ describe('EvmAccountProvider', () => { id: expect.any(String), }; - mockNextDiscoveryAddressOnce(account.address); const result = await provider.discoverAccounts({ entropySource: MOCK_HD_KEYRING_1.metadata.id, @@ -721,12 +698,6 @@ describe('EvmAccountProvider', () => { }, }); - const account = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) - .withAddressSuffix('0') - .get(); - - mockNextDiscoveryAddressOnce(account.address); - const providerWithTrace = new EvmAccountProvider( getMultichainAccountServiceMessenger(messenger), { diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts index aa0e1eb97d9..e0cd1d57778 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts @@ -1,13 +1,12 @@ -import { publicToAddress } from '@ethereumjs/util'; import type { Bip44Account } from '@metamask/account-api'; import { getUUIDFromAddressOfNormalAccount } from '@metamask/accounts-controller'; import type { TraceCallback } from '@metamask/controller-utils'; -import type { HdKeyring } from '@metamask/eth-hd-keyring'; import type { CreateAccountOptions, EntropySourceId, KeyringAccount, KeyringCapabilities, + KeyringV2, } from '@metamask/keyring-api'; import { AccountCreationType, @@ -16,13 +15,10 @@ import { EthScope, } from '@metamask/keyring-api'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { - EthKeyring, - InternalAccount, -} from '@metamask/keyring-internal-api'; +import type { InternalAccount } from '@metamask/keyring-internal-api'; import { AccountId } from '@metamask/keyring-utils'; import type { Provider } from '@metamask/network-controller'; -import { add0x, assert, bytesToHex, isStrictHexString } from '@metamask/utils'; +import { isStrictHexString } from '@metamask/utils'; import type { Hex } from '@metamask/utils'; import { traceFallback } from '../analytics'; @@ -155,18 +151,18 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { async #createAccount({ entropySource, groupIndex, - throwOnGap = false, + throwOnGap, }: { entropySource: EntropySourceId; groupIndex: number; - throwOnGap?: boolean; + throwOnGap: boolean; }): Promise<[Hex, boolean]> { - const result = await this.withKeyring( + const result = await this.withKeyring( { id: entropySource }, async ({ keyring }) => { const existing = await keyring.getAccounts(); if (groupIndex < existing.length) { - return [existing[groupIndex], false]; + return [existing[groupIndex].address as Hex, false]; } // If the throwOnGap flag is set, we throw an error to prevent index gaps. @@ -174,8 +170,12 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { throw new Error('Trying to create too many accounts'); } - const [added] = await keyring.addAccounts(1); - return [added, true]; + const [added] = await keyring.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource, + groupIndex, + }); + return [added.address as Hex, true]; }, ); @@ -202,7 +202,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { const { range } = options; // Use a single withKeyring call for the entire range. - const accountIds = await this.withKeyring( + const accountIds = await this.withKeyring( { id: entropySource }, async ({ keyring }) => { const existing = await keyring.getAccounts(); @@ -224,21 +224,25 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { ) { if (groupIndex < existing.length) { // Account already exists. - result.push(this.#getAccountId(existing[groupIndex])); + result.push( + this.#getAccountId(existing[groupIndex].address as Hex), + ); } } - // Determine if we need to create new accounts. - const from = Math.max(range.from, existing.length); - if (from <= range.to) { - // Calculate how many new accounts to create. - const accountsToCreate = range.to - existing.length + 1; - - // Create all new accounts in one call. - const newAccounts = await keyring.addAccounts(accountsToCreate); - result.push( - ...newAccounts.map((address) => this.#getAccountId(address)), - ); + // Create new accounts one-by-one since HdKeyringV2 only supports + // bip44:derive-index (not bip44:derive-index-range). + for ( + let groupIndex = Math.max(range.from, existing.length); + groupIndex <= range.to; + groupIndex++ + ) { + const [created] = await keyring.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource, + groupIndex, + }); + result.push(this.#getAccountId(created.address as Hex)); } return result; @@ -328,39 +332,14 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { return parseInt(response, 16); } - async #getAddressFromGroupIndex({ - entropySource, - groupIndex, - }: { - entropySource: EntropySourceId; - groupIndex: number; - }): Promise { - // NOTE: To avoid exposing this function at keyring level, we just re-use its internal state - // and compute the derivation here. - return await this.withKeyring( - { id: entropySource }, - async ({ keyring }) => { - // If the account already exist, do not re-derive and just re-use that account. - const existing = await keyring.getAccounts(); - if (groupIndex < existing.length) { - return existing[groupIndex]; - } - - // If not, then we just "peek" the next address to avoid creating the account. - assert(keyring.root, 'Expected HD keyring.root to be set'); - const hdKey = keyring.root.deriveChild(groupIndex); - assert(hdKey.publicKey, 'Expected public key to be set'); - - return add0x( - bytesToHex(publicToAddress(hdKey.publicKey, true)).toLowerCase(), - ); - }, - ); - } - /** * Discover and create accounts for the EVM provider. * + * Uses a single withKeyringV2 callback to create the account, check for + * on-chain activity, and delete it if inactive — all atomically. This + * ensures that when no activity is found, the net keyring state change is + * zero (no vault write, no stateChange event). + * * @param opts - The options for the discovery and creation of accounts. * @param opts.entropySource - The entropy source to use for the discovery and creation of accounts. * @param opts.groupIndex - The index of the group to create the accounts for. @@ -385,39 +364,52 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { const provider = this.getEvmProvider(); const { entropySource, groupIndex } = opts; - const addressFromGroupIndex = await this.#getAddressFromGroupIndex({ - entropySource, - groupIndex, - }); + // Everything happens inside a single withKeyringV2 callback so that + // a create+delete for inactive accounts results in zero net state + // change (no vault write, no events fired). + return await this.withKeyring[]>( + { id: entropySource }, + async ({ keyring }) => { + const existing = await keyring.getAccounts(); - const count = await this.#getTransactionCount( - provider, - addressFromGroupIndex, - ); - if (count === 0) { - return []; - } + let address: Hex; + let created: KeyringAccount | undefined; - // We have some activity on this address, we try to create the account. - const [address] = await this.#createAccount({ - entropySource, - groupIndex, - }); - assert( - addressFromGroupIndex === address, - 'Created account does not match address from group index.', - ); + if (groupIndex < existing.length) { + // Account already exists — use its address. + address = existing[groupIndex].address as Hex; + } else { + // Account doesn't exist — create it to discover the address. + [created] = await keyring.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource, + groupIndex, + }); + address = created.address as Hex; + } - const accoundId = this.#getAccountId(address); + const count = await this.#getTransactionCount(provider, address); + if (count === 0) { + // No activity. If we created the account, undo it within this + // same callback so the net state change is zero. + if (created) { + await keyring.deleteAccount(created.id); + } + return []; + } + + const accountId = this.#getAccountId(address); - const account = this.messenger.call( - 'AccountsController:getAccount', - accoundId, + const account = this.messenger.call( + 'AccountsController:getAccount', + accountId, + ); + assertInternalAccountExists(account); + assertIsBip44Account(account); + this.accounts.add(account.id); + return [account]; + }, ); - assertInternalAccountExists(account); - assertIsBip44Account(account); - this.accounts.add(account.id); - return [account]; }, ); } From f114887e091c13695245ce9606cb21ce67185ba4 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 15:21:11 -0400 Subject: [PATCH 03/11] test: update MultichainAccountService test for withKeyringV2 --- .../src/MultichainAccountService.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain-account-service/src/MultichainAccountService.test.ts b/packages/multichain-account-service/src/MultichainAccountService.test.ts index d8b3456d113..3b6dee0f44a 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.test.ts @@ -1537,7 +1537,7 @@ describe('MultichainAccountService', () => { ); rootMessenger.registerActionHandler( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', async (_, operation) => { const newKeyring = mocks.KeyringController.keyrings.find( (keyring) => keyring.type === 'HD Key Tree', @@ -1580,7 +1580,7 @@ describe('MultichainAccountService', () => { ); rootMessenger.registerActionHandler( - 'KeyringController:withKeyring', + 'KeyringController:withKeyringV2', async (_, operation) => { const newKeyring = mocks.KeyringController.keyrings.find( (keyring) => keyring.type === 'HD Key Tree', From 4f37340b4d89e9307aaa1f369d4d3a780c1d9bf1 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 15:37:17 -0400 Subject: [PATCH 04/11] fix: implement Keyring interface in MockHdKeyringV2 test mock --- .../src/providers/EvmAccountProvider.test.ts | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts index 383cf0aab6f..301bed0b145 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts @@ -1,7 +1,8 @@ import { isBip44Account } from '@metamask/account-api'; import { getUUIDFromAddressOfNormalAccount } from '@metamask/accounts-controller'; -import { AccountCreationType } from '@metamask/keyring-api'; +import { AccountCreationType, EthScope } from '@metamask/keyring-api'; import type { KeyringAccount } from '@metamask/keyring-api'; +import type { Keyring } from '@metamask/keyring-api/v2'; import type { KeyringMetadata } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { @@ -47,16 +48,23 @@ function toKeyringAccount(account: InternalAccount): KeyringAccount { } as unknown as KeyringAccount; } -class MockKeyringV2 { - readonly type = 'MockKeyringV2'; +// Mock V2 HD Keyring implementing the Keyring interface from @metamask/keyring-api/v2. +class MockHdKeyringV2 implements Keyring { + readonly type = 'HD Key Tree' as `${string}`; + + readonly capabilities = { + scopes: [EthScope.Eoa] as [string, ...string[]], + bip44: { deriveIndex: true }, + }; + + // Internal test-only state — not part of the Keyring interface. + readonly accounts: InternalAccount[]; readonly metadata: KeyringMetadata = { id: 'mock-eth-keyring-id', name: '', }; - readonly accounts: InternalAccount[]; - constructor(accounts: InternalAccount[]) { this.accounts = accounts; } @@ -65,20 +73,20 @@ class MockKeyringV2 { .fn() .mockImplementation(() => this.accounts.map(toKeyringAccount)); - createAccounts = jest.fn().mockImplementation((options: Record) => { - const newAccounts: InternalAccount[] = []; - - if (options.type === `${AccountCreationType.Bip44DeriveIndex}`) { - const account = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) - .withUuid() - .withAddressSuffix(`${this.accounts.length}`) - .withGroupIndex(this.accounts.length) - .get(); - this.accounts.push(account); - newAccounts.push(account); - } else if (options.type === `${AccountCreationType.Bip44DeriveIndexRange}`) { - const { range } = options; - for (let i = range.from; i <= range.to; i++) { + getAccount = jest.fn().mockImplementation((accountId: string) => { + const account = this.accounts.find((a) => a.id === accountId); + if (!account) { + throw new Error(`Account not found: ${accountId}`); + } + return toKeyringAccount(account); + }); + + createAccounts = jest + .fn() + .mockImplementation((options: Record) => { + const newAccounts: InternalAccount[] = []; + + if (options.type === `${AccountCreationType.Bip44DeriveIndex}`) { const account = MockAccountBuilder.from(MOCK_HD_ACCOUNT_1) .withUuid() .withAddressSuffix(`${this.accounts.length}`) @@ -87,10 +95,9 @@ class MockKeyringV2 { this.accounts.push(account); newAccounts.push(account); } - } - return newAccounts.map(toKeyringAccount); - }); + return newAccounts.map(toKeyringAccount); + }); deleteAccount = jest.fn().mockImplementation((accountId: string) => { const index = this.accounts.findIndex((a) => a.id === accountId); @@ -98,6 +105,12 @@ class MockKeyringV2 { this.accounts.splice(index, 1); } }); + + serialize = jest.fn().mockResolvedValue({}); + + deserialize = jest.fn().mockResolvedValue(undefined); + + submitRequest = jest.fn(); } /** @@ -126,13 +139,13 @@ function setup({ } = {}): { provider: EvmAccountProvider; messenger: RootMessenger; - keyring: MockKeyringV2; + keyring: MockHdKeyringV2; mocks: { mockProviderRequest: jest.Mock; mockGetAccount: jest.Mock; }; } { - const keyring = new MockKeyringV2(accounts); + const keyring = new MockHdKeyringV2(accounts); messenger.registerActionHandler( 'AccountsController:getAccounts', @@ -499,7 +512,6 @@ describe('EvmAccountProvider', () => { id: expect.any(String), }; - expect( await provider.discoverAccounts({ entropySource: MOCK_HD_KEYRING_1.metadata.id, @@ -636,7 +648,6 @@ describe('EvmAccountProvider', () => { id: expect.any(String), }; - // Create provider with custom trace callback const providerWithTrace = new EvmAccountProvider( getMultichainAccountServiceMessenger(messenger), @@ -673,7 +684,6 @@ describe('EvmAccountProvider', () => { id: expect.any(String), }; - const result = await provider.discoverAccounts({ entropySource: MOCK_HD_KEYRING_1.metadata.id, groupIndex: 0, From 4dd112bc0c94a12bb53a58477e76f81f993f3e6f Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 15:58:31 -0400 Subject: [PATCH 05/11] fix: update types in tests --- .../src/providers/EvmAccountProvider.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts index 301bed0b145..58b6d524a38 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.test.ts @@ -3,6 +3,7 @@ import { getUUIDFromAddressOfNormalAccount } from '@metamask/accounts-controller import { AccountCreationType, EthScope } from '@metamask/keyring-api'; import type { KeyringAccount } from '@metamask/keyring-api'; import type { Keyring } from '@metamask/keyring-api/v2'; +import { KeyringType } from '@metamask/keyring-api/v2'; import type { KeyringMetadata } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import type { @@ -50,10 +51,10 @@ function toKeyringAccount(account: InternalAccount): KeyringAccount { // Mock V2 HD Keyring implementing the Keyring interface from @metamask/keyring-api/v2. class MockHdKeyringV2 implements Keyring { - readonly type = 'HD Key Tree' as `${string}`; + readonly type = KeyringType.Hd; readonly capabilities = { - scopes: [EthScope.Eoa] as [string, ...string[]], + scopes: [EthScope.Eoa], bip44: { deriveIndex: true }, }; From ea53d9c6a6b26d3bdcc10e24e62b5e072a2f73cd Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 16:35:55 -0400 Subject: [PATCH 06/11] chore: update changelog --- packages/multichain-account-service/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 428e0be92f7..de9b642ea9c 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **BREAKING:** Replace `KeyringController:withKeyring` with `KeyringController:withKeyringV2` across all account providers ([#8491](https://github.com/MetaMask/core/pull/8491)) - Bump `@metamask/accounts-controller` from `^37.1.1` to `^37.2.0` ([#8363](https://github.com/MetaMask/core/pull/8363)) - Bump `@metamask/keyring-controller` from `^25.1.1` to `^25.2.0` ([#8363](https://github.com/MetaMask/core/pull/8363)) - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) From 44bf6dbb589db0bdfd8135f00fee952b99be937c Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 17:13:47 -0400 Subject: [PATCH 07/11] fix: apply code review --- .../src/providers/EvmAccountProvider.ts | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts index ba3127136cf..86fe089bfe4 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts @@ -366,7 +366,12 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { // Everything happens inside a single withKeyringV2 callback so that // a create+delete for inactive accounts results in zero net state // change (no vault write, no events fired). - return await this.withKeyring[]>( + // + // The callback returns the address if activity was found, or null if + // not. The AccountsController lookup happens AFTER the callback + // completes, because newly created accounts are only visible to the + // AccountsController after withKeyringV2 persists and fires stateChange. + const discoveredAddress = await this.withKeyring( { id: entropySource }, async ({ keyring }) => { const existing = await keyring.getAccounts(); @@ -394,21 +399,28 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { if (created) { await keyring.deleteAccount(created.id); } - return []; + return null; } - const accountId = this.#getAccountId(address); - - const account = this.messenger.call( - 'AccountsController:getAccount', - accountId, - ); - assertInternalAccountExists(account); - assertIsBip44Account(account); - this.accounts.add(account.id); - return [account]; + // Activity found — account stays. Return the address so we can + // look it up in the AccountsController after the callback persists. + return address; }, ); + + if (!discoveredAddress) { + return []; + } + + const accountId = this.#getAccountId(discoveredAddress); + const account = this.messenger.call( + 'AccountsController:getAccount', + accountId, + ); + assertInternalAccountExists(account); + assertIsBip44Account(account); + this.accounts.add(account.id); + return [account]; }, ); } From 462990e23bf0a52470d5b47eac89e193c6ef7407 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 17:40:56 -0400 Subject: [PATCH 08/11] fix: add override for snap providers to use withKeyring --- .../src/providers/BtcAccountProvider.test.ts | 2 +- .../src/providers/SnapAccountProvider.test.ts | 2 +- .../src/providers/SnapAccountProvider.ts | 28 ++++++++++++++++++- .../src/providers/SolAccountProvider.test.ts | 2 +- .../src/providers/TrxAccountProvider.test.ts | 2 +- .../src/tests/messenger.ts | 1 + .../multichain-account-service/src/types.ts | 2 ++ 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts b/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts index 8e8d79ca3ff..98dc3a8dd7b 100644 --- a/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/BtcAccountProvider.test.ts @@ -212,7 +212,7 @@ function setup({ ); messenger.registerActionHandler( - 'KeyringController:withKeyringV2', + 'KeyringController:withKeyring', async (_, operation) => operation({ // We type-cast here, since `withKeyring` defaults to `EthKeyring` and the diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts index 49a7aec51a1..af33672f585 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.test.ts @@ -231,7 +231,7 @@ const setup = ({ }; messenger.registerActionHandler( - 'KeyringController:withKeyringV2', + 'KeyringController:withKeyring', jest .fn() .mockImplementation( diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index eb0ce052c0f..82f20406eb2 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -13,7 +13,7 @@ import type { EntropySourceId, KeyringAccount, } from '@metamask/keyring-api'; -import type { KeyringMetadata } from '@metamask/keyring-controller'; +import type { KeyringMetadata, KeyringSelector } from '@metamask/keyring-controller'; import { KeyringTypes } from '@metamask/keyring-controller'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import { KeyringClient } from '@metamask/keyring-snap-client'; @@ -149,6 +149,32 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { return this.#trace(request, fn); } + // Override to keep snap providers on V1 withKeyring until the snap keyring + // migration is complete. The base class calls withKeyringV2 which the snap + // providers are not yet compatible with (they use V1 SnapKeyring methods). + protected override async withKeyring( + selector: KeyringSelector, + operation: ({ + keyring, + metadata, + }: { + keyring: SelectedKeyring; + metadata: KeyringMetadata; + }) => Promise, + ): Promise { + const result = await this.messenger.call( + 'KeyringController:withKeyring', + selector, + ({ keyring, metadata }) => + operation({ + keyring: keyring as SelectedKeyring, + metadata, + }), + ); + + return result as CallbackResult; + } + async #getRestrictedSnapKeyring(): Promise { // NOTE: We're not supposed to make the keyring instance escape `withKeyring` but // we have to use the `SnapKeyring` instance to be able to create Solana account diff --git a/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts b/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts index 2a216f9dbbb..50935bdac49 100644 --- a/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/SolAccountProvider.test.ts @@ -201,7 +201,7 @@ function setup({ ); messenger.registerActionHandler( - 'KeyringController:withKeyringV2', + 'KeyringController:withKeyring', async (_, operation) => operation({ // We type-cast here, since `withKeyring` defaults to `EthKeyring` and the diff --git a/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts b/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts index 64c14f5e27a..adb163ddd52 100644 --- a/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts +++ b/packages/multichain-account-service/src/providers/TrxAccountProvider.test.ts @@ -192,7 +192,7 @@ function setup({ ); messenger.registerActionHandler( - 'KeyringController:withKeyringV2', + 'KeyringController:withKeyring', async (_, operation) => operation({ // We type-cast here, since `withKeyring` defaults to `EthKeyring` and the diff --git a/packages/multichain-account-service/src/tests/messenger.ts b/packages/multichain-account-service/src/tests/messenger.ts index 04af0a95801..8318ccaede7 100644 --- a/packages/multichain-account-service/src/tests/messenger.ts +++ b/packages/multichain-account-service/src/tests/messenger.ts @@ -64,6 +64,7 @@ export function getMultichainAccountServiceMessenger( 'AccountsController:listMultichainAccounts', 'SnapController:getState', 'SnapController:handleRequest', + 'KeyringController:withKeyring', 'KeyringController:withKeyringV2', 'KeyringController:getState', 'KeyringController:getKeyringsByType', diff --git a/packages/multichain-account-service/src/types.ts b/packages/multichain-account-service/src/types.ts index d136086d4f9..6376af180a4 100644 --- a/packages/multichain-account-service/src/types.ts +++ b/packages/multichain-account-service/src/types.ts @@ -22,6 +22,7 @@ import type { KeyringControllerGetStateAction, KeyringControllerRemoveAccountAction, KeyringControllerStateChangeEvent, + KeyringControllerWithKeyringAction, KeyringControllerWithKeyringV2Action, } from '@metamask/keyring-controller'; import type { Messenger } from '@metamask/messenger'; @@ -78,6 +79,7 @@ type AllowedActions = | AccountsControllerGetAccountsAction | AccountsControllerGetAccountAction | AccountsControllerGetAccountByAddressAction + | KeyringControllerWithKeyringAction | KeyringControllerWithKeyringV2Action | KeyringControllerGetStateAction | KeyringControllerGetKeyringsByTypeAction From 303547ffe351b8e0bae88fb9b383183aea180abe Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 18:07:25 -0400 Subject: [PATCH 09/11] fix: type issues --- .../src/providers/SnapAccountProvider.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts index 82f20406eb2..723bb4f81c1 100644 --- a/packages/multichain-account-service/src/providers/SnapAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/SnapAccountProvider.ts @@ -13,9 +13,16 @@ import type { EntropySourceId, KeyringAccount, } from '@metamask/keyring-api'; -import type { KeyringMetadata, KeyringSelector } from '@metamask/keyring-controller'; +import type { + KeyringMetadata, + KeyringSelector, + KeyringSelectorV2, +} from '@metamask/keyring-controller'; import { KeyringTypes } from '@metamask/keyring-controller'; -import type { InternalAccount } from '@metamask/keyring-internal-api'; +import type { + EthKeyring, + InternalAccount, +} from '@metamask/keyring-internal-api'; import { KeyringClient } from '@metamask/keyring-snap-client'; import type { Json, JsonRpcRequest, SnapId } from '@metamask/snaps-sdk'; import { HandlerType } from '@metamask/snaps-utils'; @@ -153,7 +160,7 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { // migration is complete. The base class calls withKeyringV2 which the snap // providers are not yet compatible with (they use V1 SnapKeyring methods). protected override async withKeyring( - selector: KeyringSelector, + selector: KeyringSelectorV2, operation: ({ keyring, metadata, @@ -164,11 +171,17 @@ export abstract class SnapAccountProvider extends BaseBip44AccountProvider { ): Promise { const result = await this.messenger.call( 'KeyringController:withKeyring', - selector, - ({ keyring, metadata }) => + selector as unknown as KeyringSelector, + ({ + keyring, + metadata: keyringMetadata, + }: { + keyring: EthKeyring; + metadata: KeyringMetadata; + }) => operation({ keyring: keyring as SelectedKeyring, - metadata, + metadata: keyringMetadata, }), ); From b02e88990d0cc235a60970517a7704b39f24eb30 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 18:34:03 -0400 Subject: [PATCH 10/11] fix: use string instead of Hex --- .../src/providers/EvmAccountProvider.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts index 86fe089bfe4..4e0444c06e5 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts @@ -18,7 +18,6 @@ import type { InternalAccount } from '@metamask/keyring-internal-api'; import { AccountId } from '@metamask/keyring-utils'; import type { Provider } from '@metamask/network-controller'; import { isStrictHexString } from '@metamask/utils'; -import type { Hex } from '@metamask/utils'; import { traceFallback } from '../analytics'; import { TraceName } from '../analytics/traces'; @@ -134,7 +133,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { * @param address - The address of the account. * @returns The account ID. */ - #getAccountId(address: Hex): string { + #getAccountId(address: string): string { return getUUIDFromAddressOfNormalAccount(address); } @@ -155,13 +154,13 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { entropySource: EntropySourceId; groupIndex: number; throwOnGap: boolean; - }): Promise<[Hex, boolean]> { - const result = await this.withKeyring( + }): Promise<[string, boolean]> { + const result = await this.withKeyring( { id: entropySource }, async ({ keyring }) => { const existing = await keyring.getAccounts(); if (groupIndex < existing.length) { - return [existing[groupIndex].address as Hex, false]; + return [existing[groupIndex].address, false]; } // If the throwOnGap flag is set, we throw an error to prevent index gaps. @@ -174,7 +173,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { entropySource, groupIndex, }); - return [added.address as Hex, true]; + return [added.address, true]; }, ); @@ -224,7 +223,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { if (groupIndex < existing.length) { // Account already exists. result.push( - this.#getAccountId(existing[groupIndex].address as Hex), + this.#getAccountId(existing[groupIndex].address), ); } } @@ -241,7 +240,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { entropySource, groupIndex, }); - result.push(this.#getAccountId(created.address as Hex)); + result.push(this.#getAccountId(created.address)); } return result; @@ -298,7 +297,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { */ async #getTransactionCount( provider: Provider, - address: Hex, + address: string, ): Promise { const method = 'eth_getTransactionCount'; @@ -371,17 +370,17 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { // not. The AccountsController lookup happens AFTER the callback // completes, because newly created accounts are only visible to the // AccountsController after withKeyringV2 persists and fires stateChange. - const discoveredAddress = await this.withKeyring( + const discoveredAddress = await this.withKeyring( { id: entropySource }, async ({ keyring }) => { const existing = await keyring.getAccounts(); - let address: Hex; + let address: string; let created: KeyringAccount | undefined; if (groupIndex < existing.length) { // Account already exists — use its address. - address = existing[groupIndex].address as Hex; + address = existing[groupIndex].address; } else { // Account doesn't exist — create it to discover the address. [created] = await keyring.createAccounts({ @@ -389,7 +388,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { entropySource, groupIndex, }); - address = created.address as Hex; + address = created.address; } const count = await this.#getTransactionCount(provider, address); From 15f79863292a210ebda314ff5f95212a52ee6202 Mon Sep 17 00:00:00 2001 From: Hassan Malik Date: Thu, 16 Apr 2026 18:41:35 -0400 Subject: [PATCH 11/11] fix: lint fix --- .../src/providers/EvmAccountProvider.ts | 66 +++++++++---------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts index 4e0444c06e5..8c951677b36 100644 --- a/packages/multichain-account-service/src/providers/EvmAccountProvider.ts +++ b/packages/multichain-account-service/src/providers/EvmAccountProvider.ts @@ -222,9 +222,7 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { ) { if (groupIndex < existing.length) { // Account already exists. - result.push( - this.#getAccountId(existing[groupIndex].address), - ); + result.push(this.#getAccountId(existing[groupIndex].address)); } } @@ -370,42 +368,42 @@ export class EvmAccountProvider extends BaseBip44AccountProvider { // not. The AccountsController lookup happens AFTER the callback // completes, because newly created accounts are only visible to the // AccountsController after withKeyringV2 persists and fires stateChange. - const discoveredAddress = await this.withKeyring( - { id: entropySource }, - async ({ keyring }) => { - const existing = await keyring.getAccounts(); + const discoveredAddress = await this.withKeyring< + Keyring, + string | null + >({ id: entropySource }, async ({ keyring }) => { + const existing = await keyring.getAccounts(); - let address: string; - let created: KeyringAccount | undefined; + let address: string; + let created: KeyringAccount | undefined; - if (groupIndex < existing.length) { - // Account already exists — use its address. - address = existing[groupIndex].address; - } else { - // Account doesn't exist — create it to discover the address. - [created] = await keyring.createAccounts({ - type: AccountCreationType.Bip44DeriveIndex, - entropySource, - groupIndex, - }); - address = created.address; - } + if (groupIndex < existing.length) { + // Account already exists — use its address. + address = existing[groupIndex].address; + } else { + // Account doesn't exist — create it to discover the address. + [created] = await keyring.createAccounts({ + type: AccountCreationType.Bip44DeriveIndex, + entropySource, + groupIndex, + }); + address = created.address; + } - const count = await this.#getTransactionCount(provider, address); - if (count === 0) { - // No activity. If we created the account, undo it within this - // same callback so the net state change is zero. - if (created) { - await keyring.deleteAccount(created.id); - } - return null; + const count = await this.#getTransactionCount(provider, address); + if (count === 0) { + // No activity. If we created the account, undo it within this + // same callback so the net state change is zero. + if (created) { + await keyring.deleteAccount(created.id); } + return null; + } - // Activity found — account stays. Return the address so we can - // look it up in the AccountsController after the callback persists. - return address; - }, - ); + // Activity found — account stays. Return the address so we can + // look it up in the AccountsController after the callback persists. + return address; + }); if (!discoveredAddress) { return [];