From e5bb0a52ddde4808b8096d5afc69c8dd866eb60c Mon Sep 17 00:00:00 2001 From: david0xd Date: Thu, 12 Mar 2026 17:10:18 +0100 Subject: [PATCH 01/16] Fix errors related to unavailable Snap keyring --- .../src/snaps/SnapPlatformWatcher.test.ts | 59 ++++++++++++++ .../src/snaps/SnapPlatformWatcher.ts | 79 ++++++++++++++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 2c01d5a8f84..6fe83076724 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -1,4 +1,5 @@ /* eslint-disable no-void */ +import { KeyringTypes } from '@metamask/keyring-controller'; import { SnapControllerState } from '@metamask/snaps-controllers'; import { createDeferredPromise } from '@metamask/utils'; @@ -26,6 +27,10 @@ function setup( SnapController: { getState: jest.Mock; }; + // eslint-disable-next-line @typescript-eslint/naming-convention + KeyringController: { + getKeyringsByType: jest.Mock; + }; }; watcher: SnapPlatformWatcher; } { @@ -33,6 +38,9 @@ function setup( SnapController: { getState: jest.fn(), }, + KeyringController: { + getKeyringsByType: jest.fn(), + }, }; rootMessenger.registerActionHandler( @@ -41,6 +49,13 @@ function setup( ); mocks.SnapController.getState.mockReturnValue({ isReady: false }); + rootMessenger.registerActionHandler( + 'KeyringController:getKeyringsByType', + mocks.KeyringController.getKeyringsByType, + ); + // By default, Snap keyring exists so ensureCanUseSnapPlatform can complete (including #waitForSnapKeyring). + mocks.KeyringController.getKeyringsByType.mockReturnValue([{}]); + const messenger = getMultichainAccountServiceMessenger(rootMessenger); const watcher = new SnapPlatformWatcher(messenger); @@ -235,6 +250,50 @@ describe('SnapPlatformWatcher', () => { expect(watcher.isReady).toBe(true); }); + it('resolves when Snap keyring is available (does not throw)', async () => { + const { rootMessenger, watcher, mocks } = setup(); + + publishIsReadyState(rootMessenger, true); + + expect(await watcher.ensureCanUseSnapPlatform()).toBeUndefined(); + + // When keyring exists, getKeyringsByType is called and returns non-empty, + // so we return without throwing. + expect(mocks.KeyringController.getKeyringsByType).toHaveBeenCalledWith( + KeyringTypes.snap, + ); + }); + + it('resolves after timeout when Snap keyring never appears (initial check returns empty)', async () => { + const { rootMessenger, watcher, mocks } = setup(); + + mocks.KeyringController.getKeyringsByType.mockReturnValue([]); + publishIsReadyState(rootMessenger, true); + + jest.useFakeTimers(); + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + await Promise.resolve(); + await jest.advanceTimersByTimeAsync(30_000); + jest.useRealTimers(); + expect(await ensurePromise).toBeUndefined(); + }); + + it('resolves after timeout when getKeyringsByType throws (covers #hasSnapKeyring catch path)', async () => { + const { rootMessenger, watcher, mocks } = setup(); + + mocks.KeyringController.getKeyringsByType.mockImplementation(() => { + throw new Error('KeyringController locked'); + }); + publishIsReadyState(rootMessenger, true); + + jest.useFakeTimers(); + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + await Promise.resolve(); + await jest.advanceTimersByTimeAsync(30_000); + jest.useRealTimers(); + expect(await ensurePromise).toBeUndefined(); + }); + it('waits for ensureOnboardingComplete first when platform is already ready', async () => { const { rootMessenger, messenger } = setup(); const { promise: onboardingPromise, resolve: resolveOnboarding } = diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index baea7452eb2..b0c28433cb4 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -1,9 +1,18 @@ +import { KeyringTypes } from '@metamask/keyring-controller'; import { createDeferredPromise, DeferredPromise } from '@metamask/utils'; import { once } from 'lodash'; -import { projectLogger as log } from '../logger'; +import { projectLogger as log, WARNING_PREFIX } from '../logger'; import { MultichainAccountServiceMessenger } from '../types'; +/** Minimal KeyringController state shape needed to detect Snap keyring. */ +type KeyringControllerStateSlice = { + keyrings: { type: string }[]; +}; + +/** How long to wait for the Snap keyring to appear before giving up (ms). */ +const SNAP_KEYRING_WAIT_TIMEOUT_MS = 5_000; + export type SnapPlatformWatcherOptions = { /** * Resolves when onboarding is complete. @@ -47,6 +56,74 @@ export class SnapPlatformWatcher { if (!this.#isReady) { throw new Error('Snap platform cannot be used now.'); } + + // After a restore/reset, the Snap keyring is created lazily by the client (e.g. when + // getSnapKeyring() is called). Non-EVM account creation needs the keyring to exist, so we + // wait for it here with a timeout to avoid "Keyring not found" errors. + await this.#waitForSnapKeyring(); + } + + /** + * Waits for KeyringController to have a Snap keyring available. + * Checks once, then subscribes to KeyringController:stateChange until the keyring + * appears or the timeout is reached. + */ + async #waitForSnapKeyring(): Promise { + if (this.#hasSnapKeyring()) { + return; + } + await this.#waitForSnapKeyringViaStateChange(); + } + + /** + * Returns true if KeyringController already has a Snap keyring. + * Logs and returns false on error. + * + * @returns True if a Snap keyring exists, false otherwise or on error. + */ + #hasSnapKeyring(): boolean { + try { + const keyrings = this.#messenger.call( + 'KeyringController:getKeyringsByType', + KeyringTypes.snap, + ); + + return Array.isArray(keyrings) && keyrings.length > 0; + } catch (error) { + log( + `${WARNING_PREFIX} KeyringController error while waiting for Snap keyring:`, + error, + ); + return false; + } + } + + /** + * Subscribes to KeyringController:stateChange and resolves when a Snap keyring + * appears in state, or after the timeout. + */ + async #waitForSnapKeyringViaStateChange(): Promise { + await new Promise((resolve) => { + const listener = (state: KeyringControllerStateSlice): void => { + const hasSnapKeyring = state.keyrings?.some( + (k) => k.type === (KeyringTypes.snap as string), + ); + if (hasSnapKeyring) { + this.#messenger.unsubscribe( + 'KeyringController:stateChange', + listener, + ); + resolve(); + } + }; + + setTimeout(() => { + this.#messenger.unsubscribe('KeyringController:stateChange', listener); + resolve(); + }, SNAP_KEYRING_WAIT_TIMEOUT_MS); + + this.#messenger.subscribe('KeyringController:stateChange', listener); + }); } #watch(): void { From 715ec8fc8ae02415f53fe8d3a5144d7695531ee1 Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 16 Mar 2026 16:12:01 +0100 Subject: [PATCH 02/16] Refactor keyring state check and use --- .../src/snaps/SnapPlatformWatcher.test.ts | 29 +++++++++---------- .../src/snaps/SnapPlatformWatcher.ts | 28 +++++++++++------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 6fe83076724..65bb7dbb3e9 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -29,7 +29,7 @@ function setup( }; // eslint-disable-next-line @typescript-eslint/naming-convention KeyringController: { - getKeyringsByType: jest.Mock; + getState: jest.Mock<{ keyrings: { type: string }[] }>; }; }; watcher: SnapPlatformWatcher; @@ -39,7 +39,7 @@ function setup( getState: jest.fn(), }, KeyringController: { - getKeyringsByType: jest.fn(), + getState: jest.fn(), }, }; @@ -50,11 +50,13 @@ function setup( mocks.SnapController.getState.mockReturnValue({ isReady: false }); rootMessenger.registerActionHandler( - 'KeyringController:getKeyringsByType', - mocks.KeyringController.getKeyringsByType, + 'KeyringController:getState', + mocks.KeyringController.getState, ); // By default, Snap keyring exists so ensureCanUseSnapPlatform can complete (including #waitForSnapKeyring). - mocks.KeyringController.getKeyringsByType.mockReturnValue([{}]); + mocks.KeyringController.getState.mockReturnValue({ + keyrings: [{ type: KeyringTypes.snap }], + }); const messenger = getMultichainAccountServiceMessenger(rootMessenger); @@ -257,31 +259,28 @@ describe('SnapPlatformWatcher', () => { expect(await watcher.ensureCanUseSnapPlatform()).toBeUndefined(); - // When keyring exists, getKeyringsByType is called and returns non-empty, - // so we return without throwing. - expect(mocks.KeyringController.getKeyringsByType).toHaveBeenCalledWith( - KeyringTypes.snap, - ); + // When keyring exists, getState is used to check for Snap keyring, so we return without throwing. + expect(mocks.KeyringController.getState).toHaveBeenCalled(); }); it('resolves after timeout when Snap keyring never appears (initial check returns empty)', async () => { const { rootMessenger, watcher, mocks } = setup(); - mocks.KeyringController.getKeyringsByType.mockReturnValue([]); + mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); publishIsReadyState(rootMessenger, true); jest.useFakeTimers(); const ensurePromise = watcher.ensureCanUseSnapPlatform(); await Promise.resolve(); - await jest.advanceTimersByTimeAsync(30_000); + await jest.advanceTimersByTimeAsync(5_000); jest.useRealTimers(); expect(await ensurePromise).toBeUndefined(); }); - it('resolves after timeout when getKeyringsByType throws (covers #hasSnapKeyring catch path)', async () => { + it('resolves after timeout when getState throws (covers #hasSnapKeyring catch path)', async () => { const { rootMessenger, watcher, mocks } = setup(); - mocks.KeyringController.getKeyringsByType.mockImplementation(() => { + mocks.KeyringController.getState.mockImplementation(() => { throw new Error('KeyringController locked'); }); publishIsReadyState(rootMessenger, true); @@ -289,7 +288,7 @@ describe('SnapPlatformWatcher', () => { jest.useFakeTimers(); const ensurePromise = watcher.ensureCanUseSnapPlatform(); await Promise.resolve(); - await jest.advanceTimersByTimeAsync(30_000); + await jest.advanceTimersByTimeAsync(5_000); jest.useRealTimers(); expect(await ensurePromise).toBeUndefined(); }); diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index b0c28433cb4..98b93d112ef 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -13,6 +13,19 @@ type KeyringControllerStateSlice = { /** How long to wait for the Snap keyring to appear before giving up (ms). */ const SNAP_KEYRING_WAIT_TIMEOUT_MS = 5_000; +/** + * Returns true if the given KeyringController state slice contains a Snap keyring. + * + * @param state - KeyringController state. + * @returns True if state.keyrings contains a keyring with type KeyringTypes.snap. + */ +function stateHasSnapKeyring(state: KeyringControllerStateSlice): boolean { + return Boolean( + Array.isArray(state?.keyrings) && + state.keyrings.some((k) => k.type === (KeyringTypes.snap as string)), + ); +} + export type SnapPlatformWatcherOptions = { /** * Resolves when onboarding is complete. @@ -83,12 +96,10 @@ export class SnapPlatformWatcher { */ #hasSnapKeyring(): boolean { try { - const keyrings = this.#messenger.call( - 'KeyringController:getKeyringsByType', - KeyringTypes.snap, - ); - - return Array.isArray(keyrings) && keyrings.length > 0; + const state = this.#messenger.call( + 'KeyringController:getState', + ) as KeyringControllerStateSlice; + return stateHasSnapKeyring(state); } catch (error) { log( `${WARNING_PREFIX} KeyringController error while waiting for Snap keyring:`, @@ -105,10 +116,7 @@ export class SnapPlatformWatcher { async #waitForSnapKeyringViaStateChange(): Promise { await new Promise((resolve) => { const listener = (state: KeyringControllerStateSlice): void => { - const hasSnapKeyring = state.keyrings?.some( - (k) => k.type === (KeyringTypes.snap as string), - ); - if (hasSnapKeyring) { + if (stateHasSnapKeyring(state)) { this.#messenger.unsubscribe( 'KeyringController:stateChange', listener, From dd8b174e14fbbdcdb0cb1fa5b43787d742e9ffd0 Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 16 Mar 2026 17:01:04 +0100 Subject: [PATCH 03/16] Add clear timeout --- .../src/snaps/SnapPlatformWatcher.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 98b93d112ef..32ab170f125 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -115,8 +115,13 @@ export class SnapPlatformWatcher { */ async #waitForSnapKeyringViaStateChange(): Promise { await new Promise((resolve) => { + const timeoutRef: { id: ReturnType | undefined } = { + id: undefined, + }; + const listener = (state: KeyringControllerStateSlice): void => { if (stateHasSnapKeyring(state)) { + clearTimeout(timeoutRef.id); this.#messenger.unsubscribe( 'KeyringController:stateChange', listener, @@ -125,7 +130,7 @@ export class SnapPlatformWatcher { } }; - setTimeout(() => { + timeoutRef.id = setTimeout(() => { this.#messenger.unsubscribe('KeyringController:stateChange', listener); resolve(); }, SNAP_KEYRING_WAIT_TIMEOUT_MS); From 97dc06d44a5f584b6118870e2adabd2f9a691f10 Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 16 Mar 2026 17:17:23 +0100 Subject: [PATCH 04/16] Add some refactoring changes --- .../src/snaps/SnapPlatformWatcher.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 32ab170f125..46d46cef80c 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -7,7 +7,7 @@ import { MultichainAccountServiceMessenger } from '../types'; /** Minimal KeyringController state shape needed to detect Snap keyring. */ type KeyringControllerStateSlice = { - keyrings: { type: string }[]; + keyrings: { type: KeyringTypes | string }[]; }; /** How long to wait for the Snap keyring to appear before giving up (ms). */ @@ -20,10 +20,7 @@ const SNAP_KEYRING_WAIT_TIMEOUT_MS = 5_000; * @returns True if state.keyrings contains a keyring with type KeyringTypes.snap. */ function stateHasSnapKeyring(state: KeyringControllerStateSlice): boolean { - return Boolean( - Array.isArray(state?.keyrings) && - state.keyrings.some((k) => k.type === (KeyringTypes.snap as string)), - ); + return state.keyrings.some((k) => k.type === KeyringTypes.snap); } export type SnapPlatformWatcherOptions = { From 54a5bedd176cb7b13d0b27454d0bc017aa94671e Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 16 Mar 2026 17:30:14 +0100 Subject: [PATCH 05/16] Add changelog --- packages/multichain-account-service/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 675a97a942c..71425fba765 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -33,6 +33,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `waitForAllProvidersToFinishCreatingAccounts` option (when set to `false`) was causing account creation to be asynchronous for non-EVM providers, which was potentially creating accounts after the wallet's internal lock was released. - We now run an internal account alignment operation which locks the wallet properly and runs in the background. +- Wait for Snap keyring in KeyringController before non-EVM account creation ([#8196](https://github.com/MetaMask/core/pull/8196)) + - After wallet reset or restore, the Snap keyring is created lazily (e.g. when `getSnapKeyring()` runs). We now wait for it to appear (via `KeyringController:getState` and `KeyringController:stateChange`) with a timeout, avoiding "Keyring not found" error. + ## [7.1.0] ### Added From 9e6f17631ff45c0b5f42043b216cf9fcc5c23bc3 Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 16 Mar 2026 17:33:23 +0100 Subject: [PATCH 06/16] Fix changelog formatting --- packages/multichain-account-service/CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 71425fba765..4c1f2f9b406 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -32,7 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prevent wallet's lock by-pass when creating non-EVM account asynchronously ([#7801](https://github.com/MetaMask/core/pull/7801)) - The `waitForAllProvidersToFinishCreatingAccounts` option (when set to `false`) was causing account creation to be asynchronous for non-EVM providers, which was potentially creating accounts after the wallet's internal lock was released. - We now run an internal account alignment operation which locks the wallet properly and runs in the background. - - Wait for Snap keyring in KeyringController before non-EVM account creation ([#8196](https://github.com/MetaMask/core/pull/8196)) - After wallet reset or restore, the Snap keyring is created lazily (e.g. when `getSnapKeyring()` runs). We now wait for it to appear (via `KeyringController:getState` and `KeyringController:stateChange`) with a timeout, avoiding "Keyring not found" error. From 218d487b37c42944f1af096cf6500e2f9ae4cfeb Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 16 Mar 2026 17:44:53 +0100 Subject: [PATCH 07/16] Try to fix failing tests on the CI --- .../src/MultichainAccountService.test.ts | 3 +++ packages/multichain-account-service/src/tests/accounts.ts | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/packages/multichain-account-service/src/MultichainAccountService.test.ts b/packages/multichain-account-service/src/MultichainAccountService.test.ts index 47b540dc0ae..7d1c01d5a50 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.test.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.test.ts @@ -39,6 +39,7 @@ import { import { MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2, + MOCK_SNAP_KEYRING, getMultichainAccountServiceMessenger, getRootMessenger, makeMockAccountProvider, @@ -1075,6 +1076,7 @@ describe('MultichainAccountService', () => { it('checks for Snap platform readiness with MultichainAccountService:ensureCanUseSnapPlatform', async () => { const { rootMessenger, spies } = await setup({ accounts: [], + keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2, MOCK_SNAP_KEYRING], }); await rootMessenger.call( @@ -1435,6 +1437,7 @@ describe('MultichainAccountService', () => { it('delegates Snap platform readiness check to SnapPlatformWatcher (method)', async () => { const { service, spies } = await setup({ accounts: [], + keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2, MOCK_SNAP_KEYRING], }); await service.ensureCanUseSnapPlatform(); diff --git a/packages/multichain-account-service/src/tests/accounts.ts b/packages/multichain-account-service/src/tests/accounts.ts index c7d6238b263..4479a3000ee 100644 --- a/packages/multichain-account-service/src/tests/accounts.ts +++ b/packages/multichain-account-service/src/tests/accounts.ts @@ -72,6 +72,13 @@ export const MOCK_HD_KEYRING_2 = { accounts: ['0x456'], }; +/** Used when tests need ensureCanUseSnapPlatform to resolve (SnapPlatformWatcher waits for Snap keyring). */ +export const MOCK_SNAP_KEYRING = { + type: KeyringTypes.snap, + metadata: { id: 'snap-keyring', name: 'Snap Keyring' }, + accounts: [], +}; + export const MOCK_HD_ACCOUNT_1: Bip44Account = { id: 'mock-id-1', address: '0x123', From 020aa05a1e488cedd800ab07919cbfed9c66722c Mon Sep 17 00:00:00 2001 From: david0xd Date: Mon, 16 Mar 2026 20:59:20 +0100 Subject: [PATCH 08/16] Add unit test to improve coverage --- .../src/snaps/SnapPlatformWatcher.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 65bb7dbb3e9..80ec7f4bc3f 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -263,6 +263,49 @@ describe('SnapPlatformWatcher', () => { expect(mocks.KeyringController.getState).toHaveBeenCalled(); }); + it('resolves when Snap keyring appears via stateChange before timeout (listener path)', async () => { + const rootMessenger = getRootMessenger(); + const mocks = { + SnapController: { getState: jest.fn() }, + KeyringController: { getState: jest.fn() }, + }; + mocks.SnapController.getState.mockReturnValue({ isReady: true }); + mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); + rootMessenger.registerActionHandler( + 'SnapController:getState', + mocks.SnapController.getState, + ); + rootMessenger.registerActionHandler( + 'KeyringController:getState', + mocks.KeyringController.getState, + ); + const messenger = getMultichainAccountServiceMessenger(rootMessenger); + const subscribeSpy = jest.spyOn(messenger, 'subscribe'); + const watcher = new SnapPlatformWatcher(messenger); + + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + await Promise.resolve(); + await Promise.resolve(); // flush so #waitForSnapKeyring runs and subscribe is called + + expect(subscribeSpy.mock.calls.map((call) => call[0])).toContain( + 'KeyringController:stateChange', + ); + const stateChangeCall = subscribeSpy.mock.calls.find( + (call) => call[0] === 'KeyringController:stateChange', + ); + if (stateChangeCall === undefined) { + throw new Error( + 'KeyringController:stateChange subscribe call not found', + ); + } + const listener = stateChangeCall[1] as (state: { + keyrings: { type: string }[]; + }) => void; + listener({ keyrings: [{ type: KeyringTypes.snap }] }); + + expect(await ensurePromise).toBeUndefined(); + }); + it('resolves after timeout when Snap keyring never appears (initial check returns empty)', async () => { const { rootMessenger, watcher, mocks } = setup(); From ed35fc45a9aba4e3197683ec149278945e2215c7 Mon Sep 17 00:00:00 2001 From: david0xd Date: Tue, 17 Mar 2026 12:37:54 +0100 Subject: [PATCH 09/16] Add error when keyring waiting fails --- .../src/snaps/SnapPlatformWatcher.test.ts | 63 ++++++++++++++----- .../src/snaps/SnapPlatformWatcher.ts | 14 +++-- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 80ec7f4bc3f..9ac2b2ca256 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -263,7 +263,7 @@ describe('SnapPlatformWatcher', () => { expect(mocks.KeyringController.getState).toHaveBeenCalled(); }); - it('resolves when Snap keyring appears via stateChange before timeout (listener path)', async () => { + it('resolves when Snap keyring appears via stateChange (listener path)', async () => { const rootMessenger = getRootMessenger(); const mocks = { SnapController: { getState: jest.fn() }, @@ -306,34 +306,69 @@ describe('SnapPlatformWatcher', () => { expect(await ensurePromise).toBeUndefined(); }); - it('resolves after timeout when Snap keyring never appears (initial check returns empty)', async () => { - const { rootMessenger, watcher, mocks } = setup(); - - mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); - publishIsReadyState(rootMessenger, true); + it('resolves when getState throws but stateChange later delivers Snap keyring (covers #hasSnapKeyring catch path)', async () => { + const rootMessenger = getRootMessenger(); + const mocks = { + SnapController: { getState: jest.fn() }, + KeyringController: { getState: jest.fn() }, + }; + mocks.SnapController.getState.mockReturnValue({ isReady: true }); + mocks.KeyringController.getState.mockImplementation(() => { + throw new Error('KeyringController locked'); + }); + rootMessenger.registerActionHandler( + 'SnapController:getState', + mocks.SnapController.getState, + ); + rootMessenger.registerActionHandler( + 'KeyringController:getState', + mocks.KeyringController.getState, + ); + const messenger = getMultichainAccountServiceMessenger(rootMessenger); + const subscribeSpy = jest.spyOn(messenger, 'subscribe'); + const watcher = new SnapPlatformWatcher(messenger); - jest.useFakeTimers(); const ensurePromise = watcher.ensureCanUseSnapPlatform(); await Promise.resolve(); - await jest.advanceTimersByTimeAsync(5_000); - jest.useRealTimers(); + await Promise.resolve(); + + expect(subscribeSpy.mock.calls.map((call) => call[0])).toContain( + 'KeyringController:stateChange', + ); + const stateChangeCall = subscribeSpy.mock.calls.find( + (call) => call[0] === 'KeyringController:stateChange', + ); + if (stateChangeCall === undefined) { + throw new Error( + 'KeyringController:stateChange subscribe call not found', + ); + } + const listener = stateChangeCall[1] as (state: { + keyrings: { type: string }[]; + }) => void; + listener({ keyrings: [{ type: KeyringTypes.snap }] }); + expect(await ensurePromise).toBeUndefined(); }); - it('resolves after timeout when getState throws (covers #hasSnapKeyring catch path)', async () => { + it('rejects with explicit error when Snap keyring does not appear within timeout', async () => { const { rootMessenger, watcher, mocks } = setup(); - mocks.KeyringController.getState.mockImplementation(() => { - throw new Error('KeyringController locked'); - }); + mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); publishIsReadyState(rootMessenger, true); jest.useFakeTimers(); const ensurePromise = watcher.ensureCanUseSnapPlatform(); + // Attach rejection handler before advancing timers to avoid unhandled rejection; await after. + // eslint-disable-next-line jest/valid-expect -- we await expectRejection after advancing timers + const expectRejection = expect(ensurePromise).rejects.toThrow( + 'Snap platform or keyrings still not ready. Aborting.', + ); await Promise.resolve(); await jest.advanceTimersByTimeAsync(5_000); jest.useRealTimers(); - expect(await ensurePromise).toBeUndefined(); + + await expectRejection; }); it('waits for ensureOnboardingComplete first when platform is already ready', async () => { diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 46d46cef80c..118abaa5c20 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -10,9 +10,13 @@ type KeyringControllerStateSlice = { keyrings: { type: KeyringTypes | string }[]; }; -/** How long to wait for the Snap keyring to appear before giving up (ms). */ +/** How long to wait for the Snap keyring to appear before rejecting (ms). */ const SNAP_KEYRING_WAIT_TIMEOUT_MS = 5_000; +/** Error message when Snap keyring does not appear within the timeout. */ +const SNAP_KEYRING_TIMEOUT_MESSAGE = + 'Snap platform or keyrings still not ready. Aborting.'; + /** * Returns true if the given KeyringController state slice contains a Snap keyring. * @@ -76,7 +80,7 @@ export class SnapPlatformWatcher { /** * Waits for KeyringController to have a Snap keyring available. * Checks once, then subscribes to KeyringController:stateChange until the keyring - * appears or the timeout is reached. + * appears or the timeout is reached (then throws). */ async #waitForSnapKeyring(): Promise { if (this.#hasSnapKeyring()) { @@ -108,10 +112,10 @@ export class SnapPlatformWatcher { /** * Subscribes to KeyringController:stateChange and resolves when a Snap keyring - * appears in state, or after the timeout. + * appears in state, or rejects with an error after the timeout. */ async #waitForSnapKeyringViaStateChange(): Promise { - await new Promise((resolve) => { + await new Promise((resolve, reject) => { const timeoutRef: { id: ReturnType | undefined } = { id: undefined, }; @@ -129,7 +133,7 @@ export class SnapPlatformWatcher { timeoutRef.id = setTimeout(() => { this.#messenger.unsubscribe('KeyringController:stateChange', listener); - resolve(); + reject(new Error(SNAP_KEYRING_TIMEOUT_MESSAGE)); }, SNAP_KEYRING_WAIT_TIMEOUT_MS); this.#messenger.subscribe('KeyringController:stateChange', listener); From f672d99698e409b09f90917ef974a3335d8f4166 Mon Sep 17 00:00:00 2001 From: david0xd Date: Tue, 17 Mar 2026 15:22:43 +0100 Subject: [PATCH 10/16] Add timeout configuration --- .../src/MultichainAccountService.ts | 1 + .../src/snaps/SnapPlatformWatcher.test.ts | 27 +++++++++++++++++-- .../src/snaps/SnapPlatformWatcher.ts | 16 ++++++++--- .../multichain-account-service/src/types.ts | 11 ++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index 874a9cdf8a0..dd042d8be6b 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -178,6 +178,7 @@ export class MultichainAccountService { this.#watcher = new SnapPlatformWatcher(messenger, { ensureOnboardingComplete, + snapKeyringWaitTimeoutMs: config?.snapWatcher?.timeoutMs, }); this.#messenger.registerMethodActionHandlers( diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 9ac2b2ca256..9b0de02a95e 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -359,8 +359,8 @@ describe('SnapPlatformWatcher', () => { jest.useFakeTimers(); const ensurePromise = watcher.ensureCanUseSnapPlatform(); - // Attach rejection handler before advancing timers to avoid unhandled rejection; await after. - // eslint-disable-next-line jest/valid-expect -- we await expectRejection after advancing timers + // Attach rejection handler before advancing timers to avoid unhandled rejection. + // eslint-disable-next-line jest/valid-expect -- assertion is awaited after advancing timers const expectRejection = expect(ensurePromise).rejects.toThrow( 'Snap platform or keyrings still not ready. Aborting.', ); @@ -371,6 +371,29 @@ describe('SnapPlatformWatcher', () => { await expectRejection; }); + it('uses custom snapKeyringWaitTimeoutMs when provided', async () => { + const { rootMessenger, messenger, mocks } = setup(); + mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); + + const watcher = new SnapPlatformWatcher(messenger, { + snapKeyringWaitTimeoutMs: 100, + }); + publishIsReadyState(rootMessenger, true); + + jest.useFakeTimers(); + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + // Attach a rejection handler before advancing timers to avoid unhandled rejection. + // eslint-disable-next-line jest/valid-expect -- assertion is awaited after advancing timers + const expectRejection = expect(ensurePromise).rejects.toThrow( + 'Snap platform or keyrings still not ready. Aborting.', + ); + await Promise.resolve(); + await jest.advanceTimersByTimeAsync(100); + jest.useRealTimers(); + + await expectRejection; + }); + it('waits for ensureOnboardingComplete first when platform is already ready', async () => { const { rootMessenger, messenger } = setup(); const { promise: onboardingPromise, resolve: resolveOnboarding } = diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 118abaa5c20..3f232284be8 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -10,8 +10,8 @@ type KeyringControllerStateSlice = { keyrings: { type: KeyringTypes | string }[]; }; -/** How long to wait for the Snap keyring to appear before rejecting (ms). */ -const SNAP_KEYRING_WAIT_TIMEOUT_MS = 5_000; +/** Default wait for Snap keyring to appear before rejecting (ms). */ +export const DEFAULT_SNAP_KEYRING_WAIT_TIMEOUT_MS = 5_000; /** Error message when Snap keyring does not appear within the timeout. */ const SNAP_KEYRING_TIMEOUT_MESSAGE = @@ -32,6 +32,12 @@ export type SnapPlatformWatcherOptions = { * Resolves when onboarding is complete. */ ensureOnboardingComplete?: () => Promise; + /** + * How long to wait for the Snap keyring to appear before rejecting (ms). + * + * @default DEFAULT_SNAP_KEYRING_WAIT_TIMEOUT_MS + */ + snapKeyringWaitTimeoutMs?: number; }; export class SnapPlatformWatcher { @@ -39,6 +45,8 @@ export class SnapPlatformWatcher { readonly #ensureOnboardingComplete?: () => Promise; + readonly #snapKeyringWaitTimeoutMs: number; + readonly #isReadyOnce: DeferredPromise; #isReady: boolean; @@ -49,6 +57,8 @@ export class SnapPlatformWatcher { ) { this.#messenger = messenger; this.#ensureOnboardingComplete = options.ensureOnboardingComplete; + this.#snapKeyringWaitTimeoutMs = + options.snapKeyringWaitTimeoutMs ?? DEFAULT_SNAP_KEYRING_WAIT_TIMEOUT_MS; this.#isReady = false; this.#isReadyOnce = createDeferredPromise(); @@ -134,7 +144,7 @@ export class SnapPlatformWatcher { timeoutRef.id = setTimeout(() => { this.#messenger.unsubscribe('KeyringController:stateChange', listener); reject(new Error(SNAP_KEYRING_TIMEOUT_MESSAGE)); - }, SNAP_KEYRING_WAIT_TIMEOUT_MS); + }, this.#snapKeyringWaitTimeoutMs); this.#messenger.subscribe('KeyringController:stateChange', listener); }); diff --git a/packages/multichain-account-service/src/types.ts b/packages/multichain-account-service/src/types.ts index 671c5a1d0ed..f540a23c828 100644 --- a/packages/multichain-account-service/src/types.ts +++ b/packages/multichain-account-service/src/types.ts @@ -110,6 +110,17 @@ export type MultichainAccountServiceMessenger = Messenger< MultichainAccountServiceEvents | AllowedEvents >; +/** + * Config for the Snap platform watcher (SnapPlatformWatcher). + */ +export type SnapWatcherConfig = { + /** + * How long to wait for the Snap keyring to appear before rejecting (ms). + */ + timeoutMs?: number; +}; + export type MultichainAccountServiceConfig = { trace?: TraceCallback; + snapWatcher?: SnapWatcherConfig; }; From 9cf7edbfc1209a54fc99918ea34a104c090ea760 Mon Sep 17 00:00:00 2001 From: david0xd Date: Wed, 18 Mar 2026 13:05:16 +0100 Subject: [PATCH 11/16] Add some type related refactoring --- .../src/MultichainAccountService.ts | 2 +- .../src/snaps/SnapPlatformWatcher.ts | 2 +- packages/multichain-account-service/src/types.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/multichain-account-service/src/MultichainAccountService.ts b/packages/multichain-account-service/src/MultichainAccountService.ts index dd042d8be6b..63d9520cb4f 100644 --- a/packages/multichain-account-service/src/MultichainAccountService.ts +++ b/packages/multichain-account-service/src/MultichainAccountService.ts @@ -178,7 +178,7 @@ export class MultichainAccountService { this.#watcher = new SnapPlatformWatcher(messenger, { ensureOnboardingComplete, - snapKeyringWaitTimeoutMs: config?.snapWatcher?.timeoutMs, + snapKeyringWaitTimeoutMs: config?.snapPlatformWatcher?.timeoutMs, }); this.#messenger.registerMethodActionHandlers( diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 3f232284be8..55c137fe8ce 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -7,7 +7,7 @@ import { MultichainAccountServiceMessenger } from '../types'; /** Minimal KeyringController state shape needed to detect Snap keyring. */ type KeyringControllerStateSlice = { - keyrings: { type: KeyringTypes | string }[]; + keyrings: { type: string }[]; }; /** Default wait for Snap keyring to appear before rejecting (ms). */ diff --git a/packages/multichain-account-service/src/types.ts b/packages/multichain-account-service/src/types.ts index f540a23c828..6b8c79d4837 100644 --- a/packages/multichain-account-service/src/types.ts +++ b/packages/multichain-account-service/src/types.ts @@ -113,7 +113,7 @@ export type MultichainAccountServiceMessenger = Messenger< /** * Config for the Snap platform watcher (SnapPlatformWatcher). */ -export type SnapWatcherConfig = { +export type SnapPlatformWatcherConfig = { /** * How long to wait for the Snap keyring to appear before rejecting (ms). */ @@ -122,5 +122,5 @@ export type SnapWatcherConfig = { export type MultichainAccountServiceConfig = { trace?: TraceCallback; - snapWatcher?: SnapWatcherConfig; + snapPlatformWatcher?: SnapPlatformWatcherConfig; }; From d2042732a1a2cbaddcd157b13c089249e8fa14bf Mon Sep 17 00:00:00 2001 From: david0xd Date: Wed, 18 Mar 2026 13:21:38 +0100 Subject: [PATCH 12/16] Add refactoring for keyring controller state change subscription --- .../src/snaps/SnapPlatformWatcher.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 55c137fe8ce..37b80110e18 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -146,7 +146,11 @@ export class SnapPlatformWatcher { reject(new Error(SNAP_KEYRING_TIMEOUT_MESSAGE)); }, this.#snapKeyringWaitTimeoutMs); - this.#messenger.subscribe('KeyringController:stateChange', listener); + this.#messenger.subscribe( + 'KeyringController:stateChange', + listener, + (state) => ({ keyrings: state.keyrings }), + ); }); } From 0fd337f7ca212cebb2c87d31060c2ce2a9bf5421 Mon Sep 17 00:00:00 2001 From: david0xd Date: Wed, 18 Mar 2026 13:41:23 +0100 Subject: [PATCH 13/16] Add new tests and do some type refactoring --- .../src/snaps/SnapPlatformWatcher.test.ts | 85 +++++++++++++++++-- .../src/snaps/SnapPlatformWatcher.ts | 8 +- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index 9b0de02a95e..e716e44d8b4 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -298,10 +298,53 @@ describe('SnapPlatformWatcher', () => { 'KeyringController:stateChange subscribe call not found', ); } - const listener = stateChangeCall[1] as (state: { - keyrings: { type: string }[]; - }) => void; - listener({ keyrings: [{ type: KeyringTypes.snap }] }); + const listener = stateChangeCall[1] as ( + keyrings: { + type: string; + }[], + ) => void; + listener([{ type: KeyringTypes.snap }]); + + expect(await ensurePromise).toBeUndefined(); + }); + + it('resolves when Snap keyring appears via published stateChange (selector path)', async () => { + const rootMessenger = getRootMessenger(); + const mocks = { + SnapController: { getState: jest.fn() }, + KeyringController: { getState: jest.fn() }, + }; + mocks.SnapController.getState.mockReturnValue({ isReady: true }); + mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); + rootMessenger.registerActionHandler( + 'SnapController:getState', + mocks.SnapController.getState, + ); + rootMessenger.registerActionHandler( + 'KeyringController:getState', + mocks.KeyringController.getState, + ); + const messenger = getMultichainAccountServiceMessenger(rootMessenger); + const watcher = new SnapPlatformWatcher(messenger); + + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + await Promise.resolve(); + await Promise.resolve(); + + rootMessenger.publish( + 'KeyringController:stateChange', + { + isUnlocked: true, + keyrings: [ + { + type: KeyringTypes.snap, + accounts: [], + metadata: { id: 'snap', name: 'Snap' }, + }, + ], + }, + [], + ); expect(await ensurePromise).toBeUndefined(); }); @@ -343,10 +386,12 @@ describe('SnapPlatformWatcher', () => { 'KeyringController:stateChange subscribe call not found', ); } - const listener = stateChangeCall[1] as (state: { - keyrings: { type: string }[]; - }) => void; - listener({ keyrings: [{ type: KeyringTypes.snap }] }); + const listener = stateChangeCall[1] as ( + keyrings: { + type: string; + }[], + ) => void; + listener([{ type: KeyringTypes.snap }]); expect(await ensurePromise).toBeUndefined(); }); @@ -365,12 +410,34 @@ describe('SnapPlatformWatcher', () => { 'Snap platform or keyrings still not ready. Aborting.', ); await Promise.resolve(); - await jest.advanceTimersByTimeAsync(5_000); + await jest.advanceTimersByTimeAsync(5_001); jest.useRealTimers(); await expectRejection; }); + it('rejects when timeout fires (covers timeout callback path)', async () => { + const { rootMessenger, messenger, mocks } = setup(); + mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); + const watcher = new SnapPlatformWatcher(messenger, { + snapKeyringWaitTimeoutMs: 1, + }); + publishIsReadyState(rootMessenger, true); + + jest.useFakeTimers(); + const ensurePromise = watcher.ensureCanUseSnapPlatform(); + await Promise.resolve(); + await Promise.resolve(); + // Attach rejection handler before advancing timers to avoid unhandled rejection. + // eslint-disable-next-line jest/valid-expect -- assertion is awaited after advancing timers + const rejectionAssertion = expect(ensurePromise).rejects.toThrow( + 'Snap platform or keyrings still not ready. Aborting.', + ); + await jest.advanceTimersByTimeAsync(10); + jest.useRealTimers(); + await rejectionAssertion; + }); + it('uses custom snapKeyringWaitTimeoutMs when provided', async () => { const { rootMessenger, messenger, mocks } = setup(); mocks.KeyringController.getState.mockReturnValue({ keyrings: [] }); diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts index 37b80110e18..4b6e6b17c2f 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.ts @@ -130,8 +130,10 @@ export class SnapPlatformWatcher { id: undefined, }; - const listener = (state: KeyringControllerStateSlice): void => { - if (stateHasSnapKeyring(state)) { + const listener = ( + keyrings: KeyringControllerStateSlice['keyrings'], + ): void => { + if (stateHasSnapKeyring({ keyrings })) { clearTimeout(timeoutRef.id); this.#messenger.unsubscribe( 'KeyringController:stateChange', @@ -149,7 +151,7 @@ export class SnapPlatformWatcher { this.#messenger.subscribe( 'KeyringController:stateChange', listener, - (state) => ({ keyrings: state.keyrings }), + (state) => state.keyrings, ); }); } From bbbae6f7cf21a5f51bbc42a6718123c9479ee75e Mon Sep 17 00:00:00 2001 From: david0xd Date: Wed, 18 Mar 2026 15:30:02 +0100 Subject: [PATCH 14/16] Use default time wait constant in tests --- .../src/snaps/SnapPlatformWatcher.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts index e716e44d8b4..fd04d785f40 100644 --- a/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts +++ b/packages/multichain-account-service/src/snaps/SnapPlatformWatcher.test.ts @@ -3,7 +3,10 @@ import { KeyringTypes } from '@metamask/keyring-controller'; import { SnapControllerState } from '@metamask/snaps-controllers'; import { createDeferredPromise } from '@metamask/utils'; -import { SnapPlatformWatcher } from './SnapPlatformWatcher'; +import { + DEFAULT_SNAP_KEYRING_WAIT_TIMEOUT_MS, + SnapPlatformWatcher, +} from './SnapPlatformWatcher'; import { getMultichainAccountServiceMessenger, getRootMessenger, @@ -410,7 +413,9 @@ describe('SnapPlatformWatcher', () => { 'Snap platform or keyrings still not ready. Aborting.', ); await Promise.resolve(); - await jest.advanceTimersByTimeAsync(5_001); + await jest.advanceTimersByTimeAsync( + DEFAULT_SNAP_KEYRING_WAIT_TIMEOUT_MS + 1, + ); jest.useRealTimers(); await expectRejection; From 6122f6af284a45c58f0edf5cf95867ebfc1ce7bf Mon Sep 17 00:00:00 2001 From: david0xd Date: Wed, 18 Mar 2026 15:39:54 +0100 Subject: [PATCH 15/16] Update CHANGELOG.md --- packages/multichain-account-service/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 4c1f2f9b406..3deb747158f 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add new `resyncAccounts.autoRemoveExtraSnapAccounts` configuration on Snap-based providers ([#8200](https://github.com/MetaMask/core/pull/8200)) - When enabled, this will make the `resyncAccounts` method automatically remove any extra accounts that exist on the Snap side but not on MetaMask side. - This behavior was enabled by default and can now be turned off by the clients. +- Add `snapKeyringWaitTimeoutMs` option to Snap platform watcher configuration ([#8196](https://github.com/MetaMask/core/pull/8196)) + - Allows configuring how long to wait for the Snap keyring to appear in KeyringController before timing out. Default is 5000 ms. ### Changed From db2650eb0d500329128f1f5d4e34a7f1d1569ecf Mon Sep 17 00:00:00 2001 From: david0xd Date: Wed, 18 Mar 2026 15:43:59 +0100 Subject: [PATCH 16/16] Update CHANGELOG.md again --- packages/multichain-account-service/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/multichain-account-service/CHANGELOG.md b/packages/multichain-account-service/CHANGELOG.md index 3deb747158f..e88fd4f8acf 100644 --- a/packages/multichain-account-service/CHANGELOG.md +++ b/packages/multichain-account-service/CHANGELOG.md @@ -13,8 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add new `resyncAccounts.autoRemoveExtraSnapAccounts` configuration on Snap-based providers ([#8200](https://github.com/MetaMask/core/pull/8200)) - When enabled, this will make the `resyncAccounts` method automatically remove any extra accounts that exist on the Snap side but not on MetaMask side. - This behavior was enabled by default and can now be turned off by the clients. -- Add `snapKeyringWaitTimeoutMs` option to Snap platform watcher configuration ([#8196](https://github.com/MetaMask/core/pull/8196)) - - Allows configuring how long to wait for the Snap keyring to appear in KeyringController before timing out. Default is 5000 ms. +- Add new `snapPlatformWatcher.timeoutMs` configuration ([#8196](https://github.com/MetaMask/core/pull/8196)) + - Allows configuring how long to wait for the Snap keyring to appear in `KeyringController` before timing out (Default is 5000 ms). ### Changed