From c4b5d5aa558dd902291414f5ebc731bfc2bc3ebf Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 20 May 2026 14:41:34 +0200 Subject: [PATCH 01/18] fix(snap-account-service)!: prevent double-lock when handling KeyringEvent(s) --- packages/snap-account-service/package.json | 2 + .../src/SnapAccountService.ts | 80 ++++++++++++++++++- yarn.lock | 2 + 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/packages/snap-account-service/package.json b/packages/snap-account-service/package.json index 16265c0054..835f031167 100644 --- a/packages/snap-account-service/package.json +++ b/packages/snap-account-service/package.json @@ -56,7 +56,9 @@ "@metamask/account-api": "^1.0.4", "@metamask/account-tree-controller": "^7.4.0", "@metamask/eth-snap-keyring": "^22.0.1", + "@metamask/keyring-api": "^23.1.0", "@metamask/keyring-controller": "^25.5.0", + "@metamask/keyring-snap-sdk": "^9.0.1", "@metamask/messenger": "^1.2.0", "@metamask/snaps-controllers": "^19.0.0", "@metamask/snaps-sdk": "^11.0.0", diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts index 7f73096178..2fb2bec497 100644 --- a/packages/snap-account-service/src/SnapAccountService.ts +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -1,5 +1,6 @@ import { AccountGroupId } from '@metamask/account-api'; -import type { +import { SnapManageAccountsMethod } from '@metamask/keyring-snap-sdk'; +import { SnapKeyring as LegacySnapKeyring, SnapMessage, } from '@metamask/eth-snap-keyring'; @@ -10,7 +11,7 @@ import type { KeyringControllerWithControllerAction, KeyringEntry, } from '@metamask/keyring-controller'; -import { KeyringTypes } from '@metamask/keyring-controller'; +import { isKeyringNotFoundError, KeyringTypes } from '@metamask/keyring-controller'; import type { AccountId, BaseKeyring } from '@metamask/keyring-utils'; import type { Messenger } from '@metamask/messenger'; import type { @@ -47,6 +48,8 @@ import type { AccountTreeControllerAccountGroupRemovedEvent, AccountGroupObject, } from './types'; +import { KeyringEvent } from '@metamask/keyring-api'; +import { KeyringControllerWithKeyringUnsafeAction } from '@metamask/keyring-controller'; /** * The name of the {@link SnapAccountService}, used to namespace the service's @@ -83,6 +86,7 @@ type AllowedActions = | SnapControllerGetRunnableSnapsAction | KeyringControllerGetStateAction | KeyringControllerWithControllerAction + | KeyringControllerWithKeyringUnsafeAction | AccountTreeControllerGetAccountGroupObjectAction | AccountTreeControllerGetSelectedAccountGroupAction; @@ -348,6 +352,34 @@ export class SnapAccountService { return (result as Result).snapKeyring; } + + /** + * Gets the legacy (v1) Snap keyring but do not auto-create it if it doesn't exist. + * + * @returns The existing Snap keyring instance, or undefined if it doesn't exist. + */ + async #getLegacySnapKeyringIfAvailable(): Promise { + type Result = { + snapKeyring: LegacySnapKeyring; + }; + + try { + const result = await this.#messenger.call('KeyringController:withKeyringUnsafe', { filter: isLegacySnapKeyring }, async ({ keyring }): Promise => { + // The legacy Snap keyring is not compatible with `EthKeyring`, so we need to cast here. + return { snapKeyring: keyring } as unknown as Result; + }); + + return (result as Result).snapKeyring; + } catch (error) { + if (isKeyringNotFoundError(error)) { + log('Legacy Snap keyring not available yet.'); + return undefined; + } + + throw error; + } + } + /** * Handle a message from a Snap. * @@ -359,7 +391,42 @@ export class SnapAccountService { snapId: SnapId, message: SnapMessage, ): Promise { - const snapKeyring = await this.getLegacySnapKeyring(); + let snapKeyring: LegacySnapKeyring | undefined = await this.#getLegacySnapKeyringIfAvailable(); + + // Handle specific methods first. + if (message.method === SnapManageAccountsMethod.GetSelectedAccounts) { + if (snapKeyring) { + // The legacy Snap keyring already maintain a local list of selected accounts per Snaps, so we + // just delegate the call. + return snapKeyring.handleKeyringSnapMessage(snapId, message); + } + + // Some Snaps might be using `getSelectedAccounts` early in their lifecycle, before the keyring is created. So we + // do not throw in that case to avoid messing up their lifecycle. + return []; + } + + const event = message.method as KeyringEvent; // We assume the Snap platform always sends a valid `KeyringEvent` here. + log( + `Forwarding message "${event}" from Snap "${snapId}" to its keyring...`, + ); + + // We can create a new keyring if the message is an AccountCreated event. + const isAccountCreatedMessage = event === KeyringEvent.AccountCreated; + + // Create the Snap keyring if it doesn't exist yet (in an atomic way). We cannot assume + // the keyring exists (e.g for the MMI Snap). + // NOTE: We only auto-create it for v1 account creation flows. + if (isAccountCreatedMessage && !snapKeyring) { + snapKeyring = await this.getLegacySnapKeyring(); + } + + if (!snapKeyring) { + throw new Error( + `Legacy Snap keyring does not exist yet for snap "${snapId}".`, + ); + } + return snapKeyring.handleKeyringSnapMessage(snapId, message); } @@ -397,7 +464,12 @@ export class SnapAccountService { log(`Clearing selected accounts (from "${groupId}")`); } - const snapKeyring = await this.getLegacySnapKeyring(); + const snapKeyring = await this.#getLegacySnapKeyringIfAvailable(); + if (!snapKeyring) { + log('No legacy Snap keyring available, skipping forwarding selected accounts.'); + return; + } + await snapKeyring.setSelectedAccounts(accounts); }; diff --git a/yarn.lock b/yarn.lock index 9eff13004d..3489069ede 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5465,7 +5465,9 @@ __metadata: "@metamask/account-tree-controller": "npm:^7.4.0" "@metamask/auto-changelog": "npm:^6.1.0" "@metamask/eth-snap-keyring": "npm:^22.0.1" + "@metamask/keyring-api": "npm:^23.1.0" "@metamask/keyring-controller": "npm:^25.5.0" + "@metamask/keyring-snap-sdk": "npm:^9.0.1" "@metamask/keyring-utils": "npm:^3.2.1" "@metamask/messenger": "npm:^1.2.0" "@metamask/snaps-controllers": "npm:^19.0.0" From 340122b337a12b0622faecff01d45a7540319097 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 20 May 2026 15:09:39 +0200 Subject: [PATCH 02/18] chore: lint --- .../src/SnapAccountService.ts | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts index 2fb2bec497..24f41aa1fc 100644 --- a/packages/snap-account-service/src/SnapAccountService.ts +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -1,9 +1,9 @@ import { AccountGroupId } from '@metamask/account-api'; -import { SnapManageAccountsMethod } from '@metamask/keyring-snap-sdk'; import { SnapKeyring as LegacySnapKeyring, SnapMessage, } from '@metamask/eth-snap-keyring'; +import { KeyringEvent } from '@metamask/keyring-api'; import type { KeyringControllerGetStateAction, KeyringControllerStateChangeEvent, @@ -11,7 +11,12 @@ import type { KeyringControllerWithControllerAction, KeyringEntry, } from '@metamask/keyring-controller'; -import { isKeyringNotFoundError, KeyringTypes } from '@metamask/keyring-controller'; +import { + isKeyringNotFoundError, + KeyringTypes, +} from '@metamask/keyring-controller'; +import { KeyringControllerWithKeyringUnsafeAction } from '@metamask/keyring-controller'; +import { SnapManageAccountsMethod } from '@metamask/keyring-snap-sdk'; import type { AccountId, BaseKeyring } from '@metamask/keyring-utils'; import type { Messenger } from '@metamask/messenger'; import type { @@ -48,8 +53,6 @@ import type { AccountTreeControllerAccountGroupRemovedEvent, AccountGroupObject, } from './types'; -import { KeyringEvent } from '@metamask/keyring-api'; -import { KeyringControllerWithKeyringUnsafeAction } from '@metamask/keyring-controller'; /** * The name of the {@link SnapAccountService}, used to namespace the service's @@ -352,22 +355,27 @@ export class SnapAccountService { return (result as Result).snapKeyring; } - /** * Gets the legacy (v1) Snap keyring but do not auto-create it if it doesn't exist. * * @returns The existing Snap keyring instance, or undefined if it doesn't exist. */ - async #getLegacySnapKeyringIfAvailable(): Promise { + async #getLegacySnapKeyringIfAvailable(): Promise< + LegacySnapKeyring | undefined + > { type Result = { snapKeyring: LegacySnapKeyring; }; try { - const result = await this.#messenger.call('KeyringController:withKeyringUnsafe', { filter: isLegacySnapKeyring }, async ({ keyring }): Promise => { - // The legacy Snap keyring is not compatible with `EthKeyring`, so we need to cast here. - return { snapKeyring: keyring } as unknown as Result; - }); + const result = await this.#messenger.call( + 'KeyringController:withKeyringUnsafe', + { filter: isLegacySnapKeyring }, + async ({ keyring }): Promise => { + // The legacy Snap keyring is not compatible with `EthKeyring`, so we need to cast here. + return { snapKeyring: keyring } as unknown as Result; + }, + ); return (result as Result).snapKeyring; } catch (error) { @@ -391,7 +399,8 @@ export class SnapAccountService { snapId: SnapId, message: SnapMessage, ): Promise { - let snapKeyring: LegacySnapKeyring | undefined = await this.#getLegacySnapKeyringIfAvailable(); + let snapKeyring: LegacySnapKeyring | undefined = + await this.#getLegacySnapKeyringIfAvailable(); // Handle specific methods first. if (message.method === SnapManageAccountsMethod.GetSelectedAccounts) { @@ -466,7 +475,9 @@ export class SnapAccountService { const snapKeyring = await this.#getLegacySnapKeyringIfAvailable(); if (!snapKeyring) { - log('No legacy Snap keyring available, skipping forwarding selected accounts.'); + log( + 'No legacy Snap keyring available, skipping forwarding selected accounts.', + ); return; } From 7af6eb479e562cde1c15950022826b83cac8f9f8 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 20 May 2026 15:10:28 +0200 Subject: [PATCH 03/18] test: add tests --- .../src/SnapAccountService.test.ts | 187 +++++++++++++++++- 1 file changed, 185 insertions(+), 2 deletions(-) diff --git a/packages/snap-account-service/src/SnapAccountService.test.ts b/packages/snap-account-service/src/SnapAccountService.test.ts index e1569d63a0..5e560fea12 100644 --- a/packages/snap-account-service/src/SnapAccountService.test.ts +++ b/packages/snap-account-service/src/SnapAccountService.test.ts @@ -1,6 +1,9 @@ import type { AccountGroupId } from '@metamask/account-api'; import type { SnapKeyring, SnapMessage } from '@metamask/eth-snap-keyring'; +import { KeyringEvent } from '@metamask/keyring-api'; import { + KeyringControllerError, + KeyringControllerErrorMessage, KeyringControllerState, KeyringTypes, } from '@metamask/keyring-controller'; @@ -8,6 +11,7 @@ import type { KeyringEntry, RestrictedController, } from '@metamask/keyring-controller'; +import { SnapManageAccountsMethod } from '@metamask/keyring-snap-sdk'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MockAnyNamespace, @@ -62,6 +66,7 @@ type Mocks = { KeyringController: { getState: jest.MockedFunction<() => { keyrings: { type: string }[] }>; withController: jest.Mock; + withKeyringUnsafe: jest.Mock; }; // eslint-disable-next-line @typescript-eslint/naming-convention AccountTreeController: { @@ -103,6 +108,7 @@ function getMessenger( 'SnapController:getRunnableSnaps', 'KeyringController:getState', 'KeyringController:withController', + 'KeyringController:withKeyringUnsafe', 'AccountTreeController:getAccountGroupObject', 'AccountTreeController:getSelectedAccountGroup', ], @@ -323,6 +329,7 @@ function mockWithController( * @param keyring - The mocked Snap keyring methods. * @param keyring.handleKeyringSnapMessage - The mocked implementation. * @param keyring.setSelectedAccounts - The mocked implementation. + * @returns The mocked Snap keyring for assertions. */ function mockLegacySnapKeyring( mocks: Mocks, @@ -337,7 +344,7 @@ function mockLegacySnapKeyring( SnapKeyring['setSelectedAccounts'] >; }, -): void { +): MockSnapKeyring { const snapKeyring: MockSnapKeyring = { type: KeyringTypes.snap, handleKeyringSnapMessage, @@ -357,6 +364,28 @@ function mockLegacySnapKeyring( removeKeyring: jest.fn(), }), ); + mocks.KeyringController.withKeyringUnsafe.mockImplementation( + async (_selector, operation) => + operation({ + keyring: snapKeyring as KeyringEntry['keyring'], + metadata: { id: 'id-snap', name: KeyringTypes.snap }, + }), + ); + return snapKeyring; +} + +/** + * Configures `mocks.KeyringController.withKeyringUnsafe` to reject as if the + * legacy Snap keyring did not exist yet. + * + * @param mocks - The mocks object from {@link setup}. + */ +function mockLegacySnapKeyringMissing(mocks: Mocks): void { + mocks.KeyringController.withKeyringUnsafe.mockImplementation(async () => { + throw new KeyringControllerError( + KeyringControllerErrorMessage.KeyringNotFound, + ); + }); } /** @@ -398,6 +427,7 @@ function setup({ KeyringController: { getState: jest.fn().mockReturnValue({ keyrings }), withController: jest.fn(), + withKeyringUnsafe: jest.fn(), }, AccountTreeController: { getAccountGroupObject: jest.fn().mockReturnValue(undefined), @@ -421,6 +451,10 @@ function setup({ 'KeyringController:withController', mocks.KeyringController.withController, ); + rootMessenger.registerActionHandler( + 'KeyringController:withKeyringUnsafe', + mocks.KeyringController.withKeyringUnsafe, + ); rootMessenger.registerActionHandler( 'AccountTreeController:getAccountGroupObject', mocks.AccountTreeController.getAccountGroupObject, @@ -634,7 +668,7 @@ describe('SnapAccountService', () => { describe('handleKeyringSnapMessage', () => { const MOCK_MESSAGE = { - method: 'keyring_listAccounts', + method: KeyringEvent.AccountUpdated, params: {}, } as unknown as SnapMessage; @@ -689,6 +723,133 @@ describe('SnapAccountService', () => { ); expect(result).toBe('pong'); }); + + it('throws when the legacy Snap keyring does not exist yet for a non-AccountCreated message', async () => { + const { service, mocks } = setup(); + mockLegacySnapKeyringMissing(mocks); + + await expect( + service.handleKeyringSnapMessage(MOCK_SNAP_ID, MOCK_MESSAGE), + ).rejects.toThrow( + `Legacy Snap keyring does not exist yet for snap "${MOCK_SNAP_ID}".`, + ); + expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); + }); + + it('propagates non-KeyringNotFound errors from withKeyringUnsafe', async () => { + const { service, mocks } = setup(); + mocks.KeyringController.withKeyringUnsafe.mockImplementation(async () => { + throw new Error('boom'); + }); + + await expect( + service.handleKeyringSnapMessage(MOCK_SNAP_ID, MOCK_MESSAGE), + ).rejects.toThrow('boom'); + }); + + describe('when the message is an AccountCreated event', () => { + const ACCOUNT_CREATED_MESSAGE = { + method: KeyringEvent.AccountCreated, + params: {}, + } as unknown as SnapMessage; + + it('auto-creates the legacy Snap keyring when it does not exist yet', async () => { + const { service, mocks } = setup(); + mockLegacySnapKeyringMissing(mocks); + const handleKeyringSnapMessage = jest + .fn() + .mockResolvedValue({ ok: true }); + // `getLegacySnapKeyring` goes through `withController` and creates the + // keyring if missing. + mocks.KeyringController.withController.mockImplementation( + async (operation) => + operation({ + get keyrings() { + return Object.freeze([]); + }, + addNewKeyring: jest.fn(async () => ({ + keyring: { + type: KeyringTypes.snap, + handleKeyringSnapMessage, + } as unknown as KeyringEntry['keyring'], + metadata: { id: 'id-snap', name: KeyringTypes.snap }, + })), + removeKeyring: jest.fn(), + }), + ); + + const result = await service.handleKeyringSnapMessage( + MOCK_SNAP_ID, + ACCOUNT_CREATED_MESSAGE, + ); + + expect(mocks.KeyringController.withController).toHaveBeenCalled(); + expect(handleKeyringSnapMessage).toHaveBeenCalledWith( + MOCK_SNAP_ID, + ACCOUNT_CREATED_MESSAGE, + ); + expect(result).toStrictEqual({ ok: true }); + }); + + it('uses the existing legacy Snap keyring when it is already available', async () => { + const { service, mocks } = setup(); + const handleKeyringSnapMessage = jest + .fn() + .mockResolvedValue({ ok: true }); + mockLegacySnapKeyring(mocks, { handleKeyringSnapMessage }); + + const result = await service.handleKeyringSnapMessage( + MOCK_SNAP_ID, + ACCOUNT_CREATED_MESSAGE, + ); + + expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); + expect(handleKeyringSnapMessage).toHaveBeenCalledWith( + MOCK_SNAP_ID, + ACCOUNT_CREATED_MESSAGE, + ); + expect(result).toStrictEqual({ ok: true }); + }); + }); + + describe('when the message is a GetSelectedAccounts request', () => { + const GET_SELECTED_ACCOUNTS_MESSAGE = { + method: SnapManageAccountsMethod.GetSelectedAccounts, + params: {}, + } as unknown as SnapMessage; + + it('delegates to the legacy Snap keyring when it is available', async () => { + const { service, mocks } = setup(); + const handleKeyringSnapMessage = jest + .fn() + .mockResolvedValue(['account-1']); + mockLegacySnapKeyring(mocks, { handleKeyringSnapMessage }); + + const result = await service.handleKeyringSnapMessage( + MOCK_SNAP_ID, + GET_SELECTED_ACCOUNTS_MESSAGE, + ); + + expect(handleKeyringSnapMessage).toHaveBeenCalledWith( + MOCK_SNAP_ID, + GET_SELECTED_ACCOUNTS_MESSAGE, + ); + expect(result).toStrictEqual(['account-1']); + }); + + it('returns an empty list when the legacy Snap keyring does not exist yet', async () => { + const { service, mocks } = setup(); + mockLegacySnapKeyringMissing(mocks); + + const result = await service.handleKeyringSnapMessage( + MOCK_SNAP_ID, + GET_SELECTED_ACCOUNTS_MESSAGE, + ); + + expect(result).toStrictEqual([]); + expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); + }); + }); }); describe('on AccountTreeController:selectedAccountGroupChange', () => { @@ -773,6 +934,28 @@ describe('SnapAccountService', () => { consoleErrorSpy.mockRestore(); }); + + it('skips silently when the legacy Snap keyring does not exist yet', async () => { + const { service, rootMessenger, mocks } = setup(); + mockLegacySnapKeyringMissing(mocks); + mocks.AccountTreeController.getAccountGroupObject.mockReturnValue( + buildGroup(MOCK_GROUP_ID, MOCK_ACCOUNTS), + ); + const consoleErrorSpy = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); + expect(service).toBeDefined(); + + publishSelectedAccountGroupChange(rootMessenger, MOCK_GROUP_ID); + await flushMicrotasks(); + + // The forwarder must NOT auto-create the keyring through `withController`. + expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); + // And it must NOT bubble the "keyring not found" condition as an error. + expect(consoleErrorSpy).not.toHaveBeenCalled(); + + consoleErrorSpy.mockRestore(); + }); }); describe('on KeyringController:unlock', () => { From 8ceff621542e5f24e01f0fdf6caa7e9973361ab2 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 20 May 2026 15:17:45 +0200 Subject: [PATCH 04/18] chore: changelog --- packages/snap-account-service/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/snap-account-service/CHANGELOG.md b/packages/snap-account-service/CHANGELOG.md index 44fcb39bcd..abd991365c 100644 --- a/packages/snap-account-service/CHANGELOG.md +++ b/packages/snap-account-service/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Prevent double-lock in `:handleKeyringSnapMessage` for some events/methods ([#8860](https://github.com/MetaMask/core/pull/8860)) + ## [0.2.0] ### Added From bfa94c06015353d8593d318f9aacbdee95cc2021 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 20 May 2026 15:19:18 +0200 Subject: [PATCH 05/18] chore: changelog --- packages/snap-account-service/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/snap-account-service/CHANGELOG.md b/packages/snap-account-service/CHANGELOG.md index abd991365c..9372edbbd4 100644 --- a/packages/snap-account-service/CHANGELOG.md +++ b/packages/snap-account-service/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Prevent double-lock in `:handleKeyringSnapMessage` for some events/methods ([#8860](https://github.com/MetaMask/core/pull/8860)) + - The service messenger now requires the `KeyringController:withKeyringUnsafe` action. + - We now check if the keyring is available before delegating those messages. + - We still auto-create the keyring in some specific calls (e.g `notify:accountCreated`). ## [0.2.0] From e7c25d6b874f9b6dd7c510317a70bbfe5628788d Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 20 May 2026 18:01:19 +0200 Subject: [PATCH 06/18] fix: faster snap-account-service init --- .../src/SnapAccountService.test.ts | 40 +++++++++++++++---- .../src/SnapAccountService.ts | 9 +++++ 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/snap-account-service/src/SnapAccountService.test.ts b/packages/snap-account-service/src/SnapAccountService.test.ts index 5e560fea12..ec00232431 100644 --- a/packages/snap-account-service/src/SnapAccountService.test.ts +++ b/packages/snap-account-service/src/SnapAccountService.test.ts @@ -630,40 +630,64 @@ describe('SnapAccountService', () => { }); describe('getLegacySnapKeyring', () => { - it('returns the existing Snap keyring when one is already present', async () => { + it('returns the existing Snap keyring via the fast-path without acquiring the KeyringController mutex', async () => { const { service, mocks } = setup(); - const existing = buildKeyringEntry(KeyringTypes.snap); + const existing = mockLegacySnapKeyring(mocks, {}); + + const result = await service.getLegacySnapKeyring(); + + expect(result).toBe(existing as unknown as SnapKeyring); + expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); + }); + + it('falls back to withController and creates a new Snap keyring when the fast-path reports it missing', async () => { + const { service, mocks } = setup(); + mockLegacySnapKeyringMissing(mocks); const { addNewKeyring } = mockWithController(mocks, [ buildKeyringEntry(KeyringTypes.hd), - existing, ]); const result = await service.getLegacySnapKeyring(); - expect(result).toBe(existing.keyring as unknown as SnapKeyring); - expect(addNewKeyring).not.toHaveBeenCalled(); + expect(mocks.KeyringController.withKeyringUnsafe).toHaveBeenCalled(); + expect(addNewKeyring).toHaveBeenCalledWith(KeyringTypes.snap); + expect(result.type).toBe(KeyringTypes.snap); }); - it('creates a new Snap keyring when none exists', async () => { + it('returns the existing Snap keyring found within withController when the fast-path reports it missing', async () => { const { service, mocks } = setup(); + mockLegacySnapKeyringMissing(mocks); + const existing = buildKeyringEntry(KeyringTypes.snap); const { addNewKeyring } = mockWithController(mocks, [ buildKeyringEntry(KeyringTypes.hd), + existing, ]); const result = await service.getLegacySnapKeyring(); - expect(addNewKeyring).toHaveBeenCalledWith(KeyringTypes.snap); - expect(result.type).toBe(KeyringTypes.snap); + expect(result).toBe(existing.keyring as unknown as SnapKeyring); + expect(addNewKeyring).not.toHaveBeenCalled(); }); it('propagates errors thrown by withController', async () => { const { service, mocks } = setup(); + mockLegacySnapKeyringMissing(mocks); mocks.KeyringController.withController.mockImplementation(async () => { throw new Error('boom'); }); await expect(service.getLegacySnapKeyring()).rejects.toThrow('boom'); }); + + it('propagates non-KeyringNotFound errors thrown by the fast-path', async () => { + const { service, mocks } = setup(); + mocks.KeyringController.withKeyringUnsafe.mockImplementation(async () => { + throw new Error('boom'); + }); + + await expect(service.getLegacySnapKeyring()).rejects.toThrow('boom'); + expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); + }); }); describe('handleKeyringSnapMessage', () => { diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts index 24f41aa1fc..6cd1bba9e2 100644 --- a/packages/snap-account-service/src/SnapAccountService.ts +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -319,6 +319,15 @@ export class SnapAccountService { snapKeyring: LegacySnapKeyring; }; + // This is a fast-path for the common case where the keyring already exists, to avoid the + // overhead of acquiring the `KeyringController` mutex if we don't need to. + // NOTE: If it doesn't exist, we'll create it **safely** with `:withController` (which was + // not the case with the previous client's implementation). + const exists = await this.#getLegacySnapKeyringIfAvailable(); + if (exists) { + return exists; + } + // `KeyringController:withController` forbids returning a direct keyring // reference (it checks the result via `Object.is`), so we smuggle the // instance out wrapped in an object and unwrap it after the call. From 9c8d42a26adc72525d6e1cc46bc24583c10952ca Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 20 May 2026 21:18:10 +0200 Subject: [PATCH 07/18] chore: changelog --- packages/snap-account-service/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/snap-account-service/CHANGELOG.md b/packages/snap-account-service/CHANGELOG.md index 9372edbbd4..6d2621b9a7 100644 --- a/packages/snap-account-service/CHANGELOG.md +++ b/packages/snap-account-service/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- Faster `:getLegacySnapKeyring` ([#8865](https://github.com/MetaMask/core/pull/8865)) + - We now check if the keyring exists with `:withKeyringUnsafe` and returns it right away. + - If the keyring does not exist yet, we do create it with `:withController` (next calls will then be faster thanks to `:withKeyringUnsafe` pre-check). + ### Fixed - Prevent double-lock in `:handleKeyringSnapMessage` for some events/methods ([#8860](https://github.com/MetaMask/core/pull/8860)) From cf72603484e341f7c47235a7b4c0f2711355ae11 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 21 May 2026 14:38:40 +0200 Subject: [PATCH 08/18] refactor(snap-account-service)!: make init sync + use it in ctor --- .../src/SnapAccountService.test.ts | 34 +----- .../src/SnapAccountService.ts | 11 -- .../src/SnapTracker.test.ts | 113 +++--------------- .../snap-account-service/src/SnapTracker.ts | 31 +---- 4 files changed, 21 insertions(+), 168 deletions(-) diff --git a/packages/snap-account-service/src/SnapAccountService.test.ts b/packages/snap-account-service/src/SnapAccountService.test.ts index ec00232431..6f0475964d 100644 --- a/packages/snap-account-service/src/SnapAccountService.test.ts +++ b/packages/snap-account-service/src/SnapAccountService.test.ts @@ -472,22 +472,12 @@ function setup({ const MOCK_SNAP_ID = 'npm:@metamask/mock-snap' as SnapId; describe('SnapAccountService', () => { - describe('init', () => { - it('resolves without throwing', async () => { - const { service } = setup(); - - expect(await service.init()).toBeUndefined(); - }); - }); - describe('getSnaps', () => { - it('exposes tracked Snaps seeded by init', async () => { + it('exposes tracked Snaps seeded from construction', () => { const { service } = setup({ runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await service.init(); - expect(service.getSnaps()).toStrictEqual([MOCK_SNAP_ID]); }); }); @@ -496,18 +486,6 @@ describe('SnapAccountService', () => { it('throws when the Snap is not tracked', async () => { const { service } = setup(); - await service.init(); - - await expect(service.ensureReady(MOCK_SNAP_ID)).rejects.toThrow( - `Unknown snap: "${MOCK_SNAP_ID}"`, - ); - }); - - it('throws before init even for runnable Snaps', async () => { - const { service } = setup({ - runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], - }); - await expect(service.ensureReady(MOCK_SNAP_ID)).rejects.toThrow( `Unknown snap: "${MOCK_SNAP_ID}"`, ); @@ -518,8 +496,6 @@ describe('SnapAccountService', () => { runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await service.init(); - expect(await service.ensureReady(MOCK_SNAP_ID)).toBeUndefined(); }); @@ -529,8 +505,6 @@ describe('SnapAccountService', () => { runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await service.init(); - let resolved = false; const ensurePromise = service.ensureReady(MOCK_SNAP_ID).then(() => { resolved = true; @@ -551,8 +525,6 @@ describe('SnapAccountService', () => { runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await service.init(); - let resolved = false; const ensurePromise = service.ensureReady(MOCK_SNAP_ID).then(() => { resolved = true; @@ -580,8 +552,6 @@ describe('SnapAccountService', () => { }, }); - await service.init(); - jest.useFakeTimers(); const ensurePromise = service.ensureReady(MOCK_SNAP_ID); // Attach rejection handler before advancing timers to avoid unhandled rejection. @@ -610,8 +580,6 @@ describe('SnapAccountService', () => { config: { snapPlatformWatcher: { ensureOnboardingComplete } }, }); - await service.init(); - let resolved = false; const ensurePromise = service.ensureReady(MOCK_SNAP_ID).then(() => { resolved = true; diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts index 6cd1bba9e2..4a3bf8eaa2 100644 --- a/packages/snap-account-service/src/SnapAccountService.ts +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -267,17 +267,6 @@ export class SnapAccountService { } } - /** - * Initializes the snap account service. - * - * Seeds the internal set of account-management Snaps from - * `SnapController:getRunnableSnaps`, then starts processing lifecycle - * events. - */ - async init(): Promise { - await this.#tracker.init(); - } - /** * Returns the IDs of all currently tracked account-management Snaps — * Snaps that are installed, enabled, not blocked, and have the diff --git a/packages/snap-account-service/src/SnapTracker.test.ts b/packages/snap-account-service/src/SnapTracker.test.ts index 64b13fa450..5d5be4c310 100644 --- a/packages/snap-account-service/src/SnapTracker.test.ts +++ b/packages/snap-account-service/src/SnapTracker.test.ts @@ -206,24 +206,8 @@ const MOCK_SNAP_ID = 'npm:@metamask/mock-snap' as SnapId; const MOCK_OTHER_SNAP_ID = 'npm:@metamask/other-snap' as SnapId; describe('SnapTracker', () => { - describe('init', () => { - it('resolves without throwing', async () => { - const { tracker } = setup(); - - expect(await tracker.init()).toBeUndefined(); - }); - - it('does not re-init if already initialized', async () => { - const { tracker, mocks } = setup(); - - expect(await tracker.init()).toBeUndefined(); - expect(mocks.SnapController.getRunnableSnaps).toHaveBeenCalledTimes(1); - - expect(await tracker.init()).toBeUndefined(); - expect(mocks.SnapController.getRunnableSnaps).toHaveBeenCalledTimes(1); // Still only called once. - }); - - it('seeds tracked Snaps from getRunnableSnaps, filtering out non-keyring Snaps', async () => { + describe('getSnaps', () => { + it('returns seeded Snaps from construction, filtering out non-keyring Snaps', () => { const { tracker } = setup({ runnableSnaps: [ buildSnap(MOCK_SNAP_ID, true), @@ -231,106 +215,62 @@ describe('SnapTracker', () => { ], }); - await tracker.init(); - expect(tracker.getSnaps()).toStrictEqual([MOCK_SNAP_ID]); }); - }); - describe('getSnaps', () => { - it('returns an empty array before init', () => { - const { tracker } = setup({ - runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], - }); + it('returns an empty array when there are no runnable account-management Snaps', () => { + const { tracker } = setup(); expect(tracker.getSnaps()).toStrictEqual([]); }); }); describe('canUse', () => { - it('returns false before init', () => { - const { tracker } = setup({ - runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], - }); - - expect(tracker.canUse(MOCK_SNAP_ID)).toBe(false); - }); - - it('returns true for a tracked Snap', async () => { + it('returns true for a Snap seeded during construction', () => { const { tracker } = setup({ runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await tracker.init(); - expect(tracker.canUse(MOCK_SNAP_ID)).toBe(true); }); - it('returns false for an untracked Snap', async () => { + it('returns false for an untracked Snap', () => { const { tracker } = setup(); - await tracker.init(); - expect(tracker.canUse(MOCK_SNAP_ID)).toBe(false); }); }); describe('lifecycle events', () => { - it('ignores add events received before init', async () => { - const { tracker, rootMessenger } = setup(); - - publishSnapInstalled(rootMessenger, buildSnap(MOCK_SNAP_ID, true)); - - await tracker.init(); - - expect(tracker.getSnaps()).toStrictEqual([]); - }); - - it('ignores remove events received before init', async () => { - const { tracker, rootMessenger } = setup({ - runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], - }); - - publishSnapUninstalled(rootMessenger, buildSnap(MOCK_SNAP_ID, true)); - - await tracker.init(); - - expect(tracker.getSnaps()).toStrictEqual([MOCK_SNAP_ID]); - }); - - it('adds a Snap on snapInstalled when it has the keyring endowment', async () => { + it('adds a Snap on snapInstalled when it has the keyring endowment', () => { const { tracker, rootMessenger } = setup(); - await tracker.init(); publishSnapInstalled(rootMessenger, buildSnap(MOCK_SNAP_ID, true)); expect(tracker.getSnaps()).toStrictEqual([MOCK_SNAP_ID]); }); - it('does not add a Snap on snapInstalled when it lacks the keyring endowment', async () => { + it('does not add a Snap on snapInstalled when it lacks the keyring endowment', () => { const { tracker, rootMessenger } = setup(); - await tracker.init(); publishSnapInstalled(rootMessenger, buildSnap(MOCK_SNAP_ID, false)); expect(tracker.getSnaps()).toStrictEqual([]); }); - it('adds a Snap on snapEnabled when it has the keyring endowment', async () => { + it('adds a Snap on snapEnabled when it has the keyring endowment', () => { const { tracker, rootMessenger } = setup(); - await tracker.init(); publishSnapEnabled(rootMessenger, buildSnap(MOCK_SNAP_ID, true)); expect(tracker.getSnaps()).toStrictEqual([MOCK_SNAP_ID]); }); - it('removes a Snap on snapDisabled', async () => { + it('removes a Snap on snapDisabled', () => { const { tracker, rootMessenger } = setup({ runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await tracker.init(); expect(tracker.getSnaps()).toStrictEqual([MOCK_SNAP_ID]); publishSnapDisabled(rootMessenger, buildSnap(MOCK_SNAP_ID, true)); @@ -338,36 +278,19 @@ describe('SnapTracker', () => { expect(tracker.getSnaps()).toStrictEqual([]); }); - it('removes a Snap on snapBlocked', async () => { + it('removes a Snap on snapBlocked', () => { const { tracker, rootMessenger } = setup({ runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await tracker.init(); publishSnapBlocked(rootMessenger, MOCK_SNAP_ID); expect(tracker.getSnaps()).toStrictEqual([]); }); - it('ignores snapUnblocked received before init', async () => { - const { tracker, rootMessenger, mocks } = setup(); - - mocks.SnapController.getSnap.mockReturnValue({ - ...buildSnap(MOCK_SNAP_ID, true), - enabled: true, - blocked: false, - } as TruncatedSnap); - publishSnapUnblocked(rootMessenger, MOCK_SNAP_ID); - - await tracker.init(); - - expect(tracker.getSnaps()).toStrictEqual([]); - }); - - it('re-adds a Snap on snapUnblocked when it is enabled and has the keyring endowment', async () => { + it('re-adds a Snap on snapUnblocked when it is enabled and has the keyring endowment', () => { const { tracker, rootMessenger, mocks } = setup(); - await tracker.init(); mocks.SnapController.getSnap.mockReturnValue({ ...buildSnap(MOCK_SNAP_ID, true), enabled: true, @@ -379,10 +302,9 @@ describe('SnapTracker', () => { expect(tracker.getSnaps()).toStrictEqual([MOCK_SNAP_ID]); }); - it('does not re-add a Snap on snapUnblocked when it is disabled', async () => { + it('does not re-add a Snap on snapUnblocked when it is disabled', () => { const { tracker, rootMessenger, mocks } = setup(); - await tracker.init(); mocks.SnapController.getSnap.mockReturnValue({ ...buildSnap(MOCK_SNAP_ID, true), enabled: false, @@ -394,10 +316,9 @@ describe('SnapTracker', () => { expect(tracker.getSnaps()).toStrictEqual([]); }); - it('does not re-add a Snap on snapUnblocked when it lacks the keyring endowment', async () => { + it('does not re-add a Snap on snapUnblocked when it lacks the keyring endowment', () => { const { tracker, rootMessenger, mocks } = setup(); - await tracker.init(); mocks.SnapController.getSnap.mockReturnValue({ ...buildSnap(MOCK_SNAP_ID, false), enabled: true, @@ -409,21 +330,19 @@ describe('SnapTracker', () => { expect(tracker.getSnaps()).toStrictEqual([]); }); - it('does not re-add a Snap on snapUnblocked when getSnap returns null', async () => { + it('does not re-add a Snap on snapUnblocked when getSnap returns null', () => { const { tracker, rootMessenger } = setup(); - await tracker.init(); publishSnapUnblocked(rootMessenger, MOCK_SNAP_ID); expect(tracker.getSnaps()).toStrictEqual([]); }); - it('removes a Snap on snapUninstalled', async () => { + it('removes a Snap on snapUninstalled', () => { const { tracker, rootMessenger } = setup({ runnableSnaps: [buildSnap(MOCK_SNAP_ID, true)], }); - await tracker.init(); publishSnapUninstalled(rootMessenger, buildSnap(MOCK_SNAP_ID, true)); expect(tracker.getSnaps()).toStrictEqual([]); diff --git a/packages/snap-account-service/src/SnapTracker.ts b/packages/snap-account-service/src/SnapTracker.ts index 4bd13e7b28..b824892c4b 100644 --- a/packages/snap-account-service/src/SnapTracker.ts +++ b/packages/snap-account-service/src/SnapTracker.ts @@ -25,8 +25,6 @@ export class SnapTracker { readonly #snaps: Set = new Set(); - #initialized = false; - constructor(messenger: SnapAccountServiceMessenger) { this.#messenger = messenger; @@ -48,22 +46,15 @@ export class SnapTracker { this.#messenger.subscribe('SnapController:snapUnblocked', (snapId) => this.#handleSnapUnblocked(snapId as SnapId), ); + + this.#init(); } /** * Seeds the internal set of account-management Snaps from - * `SnapController:getRunnableSnaps`, then starts processing lifecycle - * events. + * `SnapController:getRunnableSnaps`. */ - async init(): Promise { - if (this.#initialized) { - // Do not re-init, once setup we only rely on lifecycle events to update the set of - // tracked Snaps. - return; - } - - this.#snaps.clear(); - + #init(): void { const runnable = this.#messenger.call('SnapController:getRunnableSnaps'); for (const snap of runnable) { if (isAccountManagementSnap(snap)) { @@ -71,8 +62,6 @@ export class SnapTracker { this.#snaps.add(snap.id); } } - - this.#initialized = true; } /** @@ -102,10 +91,6 @@ export class SnapTracker { * @param reason - The reason the Snap was added. */ #handleSnapAdded(snap: TruncatedSnap, reason: string): void { - if (!this.#initialized) { - return; - } - if (!snap.enabled || snap.blocked) { return; } @@ -124,10 +109,6 @@ export class SnapTracker { * @param snapId - The Snap ID that was unblocked. */ #handleSnapUnblocked(snapId: SnapId): void { - if (!this.#initialized) { - return; - } - const snap = this.#messenger.call('SnapController:getSnap', snapId); if (snap) { this.#handleSnapAdded(snap, 'unblocked'); @@ -142,10 +123,6 @@ export class SnapTracker { * @param reason - The reason the Snap was removed. */ #handleSnapRemoved(snapId: SnapId, reason: string): void { - if (!this.#initialized) { - return; - } - if (this.#snaps.has(snapId)) { log(`Removed account management Snap: ${snapId} (${reason})`); From 9fb8d00e74a6c37510e977f5b8c5018dfefb9407 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Thu, 21 May 2026 14:41:25 +0200 Subject: [PATCH 09/18] chore: changelog --- packages/snap-account-service/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/snap-account-service/CHANGELOG.md b/packages/snap-account-service/CHANGELOG.md index 6d2621b9a7..fb4a3878ae 100644 --- a/packages/snap-account-service/CHANGELOG.md +++ b/packages/snap-account-service/CHANGELOG.md @@ -20,6 +20,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - We now check if the keyring is available before delegating those messages. - We still auto-create the keyring in some specific calls (e.g `notify:accountCreated`). +## Removed + +- Removed `init` in favor of synchronous initialization when constructing the service ([#8877](https://github.com/MetaMask/core/pull/8877)) + ## [0.2.0] ### Added From 3b99fcf40a43574ec8835556b8c8cbc914cb7e0f Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 27 May 2026 22:01:30 +0200 Subject: [PATCH 10/18] feat(snap-account-service): always forward non-keyring events in :handleKeyringSnapMessage --- packages/accounts-controller/CHANGELOG.md | 8 ++ .../src/AccountsController.test.ts | 91 ++++++++++++++ .../src/AccountsController.ts | 45 +++++++ packages/accounts-controller/src/index.ts | 3 + packages/snap-account-service/CHANGELOG.md | 5 + .../src/SnapAccountService.test.ts | 111 ++++++++++++++++++ .../src/SnapAccountService.ts | 103 +++++++++++++++- packages/snap-account-service/src/index.ts | 3 + 8 files changed, 367 insertions(+), 2 deletions(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index eb53fa85f3..40589f96db 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Now requires `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. + +### Fixed + +- Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events. + ## [38.1.1] ### Changed diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 939c27df91..d115dc812a 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -331,6 +331,9 @@ function buildAccountsControllerMessenger( 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'SnapAccountService:accountAssetListUpdated', + 'SnapAccountService:accountBalancesUpdated', + 'SnapAccountService:accountTransactionsUpdated', 'MultichainNetworkController:networkDidChange', ], }); @@ -2167,6 +2170,29 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); + it('re-publishes keyring events: SnapAccountService:accountBalancesUpdated', () => { + const { account, messenger } = setupTest(); + + const payload: AccountBalancesUpdatedEventPayload = { + balances: { + [account.id]: { + 'bip122:000000000019d6689c085ae165831e93/slip44:0': { + amount: '0.1', + unit: 'BTC', + }, + }, + }, + }; + + const mockRePublishedCallback = jest.fn(); + messenger.subscribe( + 'AccountsController:accountBalancesUpdated', + mockRePublishedCallback, + ); + messenger.publish('SnapAccountService:accountBalancesUpdated', payload); + expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); + }); + it('re-publishes keyring events: SnapKeyring:accountAssetListUpdated', () => { const { account, messenger } = setupTest(); @@ -2188,6 +2214,27 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); + it('re-publishes keyring events: SnapAccountService:accountAssetListUpdated', () => { + const { account, messenger } = setupTest(); + + const payload: AccountAssetListUpdatedEventPayload = { + assets: { + [account.id]: { + added: ['bip122:000000000019d6689c085ae165831e93/slip44:0'], + removed: ['bip122:000000000933ea01ad0ee984209779ba/slip44:0'], + }, + }, + }; + + const mockRePublishedCallback = jest.fn(); + messenger.subscribe( + 'AccountsController:accountAssetListUpdated', + mockRePublishedCallback, + ); + messenger.publish('SnapAccountService:accountAssetListUpdated', payload); + expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); + }); + it('re-publishes keyring events: SnapKeyring:accountTransactionsUpdated', () => { const { account, messenger } = setupTest(); @@ -2228,6 +2275,50 @@ describe('AccountsController', () => { messenger.publish('SnapKeyring:accountTransactionsUpdated', payload); expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); + + it('re-publishes keyring events: SnapAccountService:accountTransactionsUpdated', () => { + const { account, messenger } = setupTest(); + + const payload: AccountTransactionsUpdatedEventPayload = { + transactions: { + [account.id]: [ + { + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'bip122:000000000019d6689c085ae165831e93', + status: 'submitted', + type: 'receive', + account: account.id, + from: [], + to: [], + fees: [ + { + type: 'base', + asset: { + fungible: true, + type: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + unit: 'BTC', + amount: '0.0001', + }, + }, + ], + events: [], + }, + ], + }, + }; + + const mockRePublishedCallback = jest.fn(); + messenger.subscribe( + 'AccountsController:accountTransactionsUpdated', + mockRePublishedCallback, + ); + messenger.publish( + 'SnapAccountService:accountTransactionsUpdated', + payload, + ); + expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); + }); }); describe('handle MultichainNetworkController:networkDidChange event', () => { diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index e5216b49b1..55b6122932 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -206,6 +206,21 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; }; +export type SnapAccountServiceAccountBalancesUpdatedEvent = { + type: `SnapAccountService:accountBalancesUpdated`; + payload: SnapKeyringAccountBalancesUpdatedEvent['payload']; +}; + +export type SnapAccountServiceAccountTransactionsUpdatedEvent = { + type: `SnapAccountService:accountTransactionsUpdated`; + payload: SnapKeyringAccountTransactionsUpdatedEvent['payload']; +}; + +export type SnapAccountServiceAccountAssetListUpdatedEvent = { + type: `SnapAccountService:accountAssetListUpdated`; + payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; +}; + /** * @deprecated This type is deprecated and will be removed in a future version. * Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead. @@ -215,6 +230,9 @@ export type AllowedEvents = | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent | SnapKeyringAccountTransactionsUpdatedEvent + | SnapAccountServiceAccountAssetListUpdatedEvent + | SnapAccountServiceAccountBalancesUpdatedEvent + | SnapAccountServiceAccountTransactionsUpdatedEvent | MultichainNetworkControllerNetworkDidChangeEvent; /** @@ -1287,6 +1305,15 @@ export class AccountsController extends BaseController< ), ); + this.messenger.subscribe( + 'SnapAccountService:accountAssetListUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountAssetListUpdated', + snapAccountEvent, + ), + ); + this.messenger.subscribe( 'SnapKeyring:accountBalancesUpdated', (snapAccountEvent) => @@ -1296,6 +1323,15 @@ export class AccountsController extends BaseController< ), ); + this.messenger.subscribe( + 'SnapAccountService:accountBalancesUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountBalancesUpdated', + snapAccountEvent, + ), + ); + this.messenger.subscribe( 'SnapKeyring:accountTransactionsUpdated', (snapAccountEvent) => @@ -1305,6 +1341,15 @@ export class AccountsController extends BaseController< ), ); + this.messenger.subscribe( + 'SnapAccountService:accountTransactionsUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountTransactionsUpdated', + snapAccountEvent, + ), + ); + // Handle account change when multichain network is changed this.messenger.subscribe( 'MultichainNetworkController:networkDidChange', diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index bfaf0be8e5..8700a73f61 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,6 +15,9 @@ export type { AccountsControllerAccountBalancesUpdatesEvent, AccountsControllerAccountTransactionsUpdatedEvent, AccountsControllerAccountAssetListUpdatedEvent, + SnapAccountServiceAccountBalancesUpdatedEvent, + SnapAccountServiceAccountTransactionsUpdatedEvent, + SnapAccountServiceAccountAssetListUpdatedEvent, AllowedEvents, AccountsControllerEvents, AccountsControllerMessenger, diff --git a/packages/snap-account-service/CHANGELOG.md b/packages/snap-account-service/CHANGELOG.md index fb4a3878ae..ac626bb3d4 100644 --- a/packages/snap-account-service/CHANGELOG.md +++ b/packages/snap-account-service/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events. + ### Changed - Faster `:getLegacySnapKeyring` ([#8865](https://github.com/MetaMask/core/pull/8865)) @@ -15,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Re-publish account-data update events from `:handleKeyringSnapMessage` without requiring the legacy Snap keyring. - Prevent double-lock in `:handleKeyringSnapMessage` for some events/methods ([#8860](https://github.com/MetaMask/core/pull/8860)) - The service messenger now requires the `KeyringController:withKeyringUnsafe` action. - We now check if the keyring is available before delegating those messages. diff --git a/packages/snap-account-service/src/SnapAccountService.test.ts b/packages/snap-account-service/src/SnapAccountService.test.ts index 6f0475964d..a0cccc11fb 100644 --- a/packages/snap-account-service/src/SnapAccountService.test.ts +++ b/packages/snap-account-service/src/SnapAccountService.test.ts @@ -1,5 +1,10 @@ import type { AccountGroupId } from '@metamask/account-api'; import type { SnapKeyring, SnapMessage } from '@metamask/eth-snap-keyring'; +import type { + AccountAssetListUpdatedEventPayload, + AccountBalancesUpdatedEventPayload, + AccountTransactionsUpdatedEventPayload, +} from '@metamask/keyring-api'; import { KeyringEvent } from '@metamask/keyring-api'; import { KeyringControllerError, @@ -663,6 +668,7 @@ describe('SnapAccountService', () => { method: KeyringEvent.AccountUpdated, params: {}, } as unknown as SnapMessage; + const MOCK_ACCOUNT_ID = '00000000-0000-4000-8000-000000000001'; it('forwards the call to the legacy Snap keyring and returns its result', async () => { const { service, mocks } = setup(); @@ -728,6 +734,111 @@ describe('SnapAccountService', () => { expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); }); + describe('when the message is an account data update event', () => { + const accountBalancesUpdatedPayload: AccountBalancesUpdatedEventPayload = + { + balances: { + [MOCK_ACCOUNT_ID]: { + 'eip155:1/slip44:60': { + amount: '1', + unit: 'ETH', + }, + }, + }, + }; + const accountAssetListUpdatedPayload: AccountAssetListUpdatedEventPayload = + { + assets: { + [MOCK_ACCOUNT_ID]: { + added: ['eip155:1/slip44:60'], + removed: [], + }, + }, + }; + const accountTransactionsUpdatedPayload: AccountTransactionsUpdatedEventPayload = + { + transactions: { + [MOCK_ACCOUNT_ID]: [], + }, + }; + + it.each([ + [ + KeyringEvent.AccountBalancesUpdated, + 'SnapAccountService:accountBalancesUpdated', + accountBalancesUpdatedPayload, + ], + [ + KeyringEvent.AccountAssetListUpdated, + 'SnapAccountService:accountAssetListUpdated', + accountAssetListUpdatedPayload, + ], + [ + KeyringEvent.AccountTransactionsUpdated, + 'SnapAccountService:accountTransactionsUpdated', + accountTransactionsUpdatedPayload, + ], + ] as const)( + 'publishes %s without requiring the legacy Snap keyring', + async (method, event, payload) => { + const { service, rootMessenger, mocks } = setup(); + const handleKeyringSnapMessage = jest + .fn() + .mockResolvedValue({ ok: true }); + mockLegacySnapKeyring(mocks, { handleKeyringSnapMessage }); + const listener = jest.fn(); + rootMessenger.subscribe(event, listener); + + const result = await service.handleKeyringSnapMessage(MOCK_SNAP_ID, { + method, + params: payload, + }); + + expect(result).toBeNull(); + expect(listener).toHaveBeenCalledWith(payload); + expect(handleKeyringSnapMessage).not.toHaveBeenCalled(); + expect( + mocks.KeyringController.withKeyringUnsafe, + ).not.toHaveBeenCalled(); + expect(mocks.KeyringController.withController).not.toHaveBeenCalled(); + }, + ); + }); + + describe('when the message is a request resolution event', () => { + it.each([ + [ + KeyringEvent.RequestApproved, + { id: '00000000-0000-0000-0000-000000000002', result: true }, + ], + [ + KeyringEvent.RequestRejected, + { id: '00000000-0000-0000-0000-000000000002' }, + ], + ] as const)( + 'delegates %s to the legacy Snap keyring', + async (method, params) => { + const { service, mocks } = setup(); + const message = { method, params }; + const handleKeyringSnapMessage = jest + .fn() + .mockResolvedValue({ ok: true }); + mockLegacySnapKeyring(mocks, { handleKeyringSnapMessage }); + + const result = await service.handleKeyringSnapMessage( + MOCK_SNAP_ID, + message, + ); + + expect(handleKeyringSnapMessage).toHaveBeenCalledWith( + MOCK_SNAP_ID, + message, + ); + expect(result).toStrictEqual({ ok: true }); + }, + ); + }); + it('propagates non-KeyringNotFound errors from withKeyringUnsafe', async () => { const { service, mocks } = setup(); mocks.KeyringController.withKeyringUnsafe.mockImplementation(async () => { diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts index 4a3bf8eaa2..d6ef5fa48e 100644 --- a/packages/snap-account-service/src/SnapAccountService.ts +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -3,7 +3,17 @@ import { SnapKeyring as LegacySnapKeyring, SnapMessage, } from '@metamask/eth-snap-keyring'; -import { KeyringEvent } from '@metamask/keyring-api'; +import type { + AccountAssetListUpdatedEventPayload, + AccountBalancesUpdatedEventPayload, + AccountTransactionsUpdatedEventPayload, +} from '@metamask/keyring-api'; +import { + AccountAssetListUpdatedEventStruct, + AccountBalancesUpdatedEventStruct, + AccountTransactionsUpdatedEventStruct, + KeyringEvent, +} from '@metamask/keyring-api'; import type { KeyringControllerGetStateAction, KeyringControllerStateChangeEvent, @@ -33,6 +43,7 @@ import type { } from '@metamask/snaps-controllers'; import { SnapId } from '@metamask/snaps-sdk'; import type { Json } from '@metamask/utils'; +import { assertStruct } from '@metamask/utils'; import { projectLogger as log } from './logger'; import type { @@ -96,7 +107,25 @@ type AllowedActions = /** * Events that {@link SnapAccountService} exposes to other consumers. */ -export type SnapAccountServiceEvents = never; +export type SnapAccountServiceAccountBalancesUpdatedEvent = { + type: `${typeof serviceName}:accountBalancesUpdated`; + payload: [AccountBalancesUpdatedEventPayload]; +}; + +export type SnapAccountServiceAccountAssetListUpdatedEvent = { + type: `${typeof serviceName}:accountAssetListUpdated`; + payload: [AccountAssetListUpdatedEventPayload]; +}; + +export type SnapAccountServiceAccountTransactionsUpdatedEvent = { + type: `${typeof serviceName}:accountTransactionsUpdated`; + payload: [AccountTransactionsUpdatedEventPayload]; +}; + +export type SnapAccountServiceEvents = + | SnapAccountServiceAccountAssetListUpdatedEvent + | SnapAccountServiceAccountBalancesUpdatedEvent + | SnapAccountServiceAccountTransactionsUpdatedEvent; /** * Events from other messengers that {@link SnapAccountService} subscribes to. @@ -154,6 +183,27 @@ function isLegacySnapKeyring(keyring: { return keyring.type === KeyringTypes.snap; } +type AccountDataUpdatedKeyringEvent = + | KeyringEvent.AccountAssetListUpdated + | KeyringEvent.AccountBalancesUpdated + | KeyringEvent.AccountTransactionsUpdated; + +/** + * Checks if a Snap message method is an account data update event. + * + * @param method - The Snap message method. + * @returns `true` if the method can be forwarded without the legacy Snap keyring. + */ +function isAccountDataUpdatedKeyringEvent( + method: string, +): method is AccountDataUpdatedKeyringEvent { + return ( + method === KeyringEvent.AccountAssetListUpdated || + method === KeyringEvent.AccountBalancesUpdated || + method === KeyringEvent.AccountTransactionsUpdated + ); +} + /** * Service responsible for managing account management snaps. */ @@ -397,6 +447,12 @@ export class SnapAccountService { snapId: SnapId, message: SnapMessage, ): Promise { + const { method } = message; + + if (isAccountDataUpdatedKeyringEvent(method)) { + return this.#publishAccountDataUpdatedEvent(snapId, method, message); + } + let snapKeyring: LegacySnapKeyring | undefined = await this.#getLegacySnapKeyringIfAvailable(); @@ -437,6 +493,49 @@ export class SnapAccountService { return snapKeyring.handleKeyringSnapMessage(snapId, message); } + /** + * Publishes an account data update event from a Snap. + * + * @param snapId - ID of the Snap. + * @param method - Account data update event method. + * @param message - Message sent by the Snap. + * @returns `null`. + */ + #publishAccountDataUpdatedEvent( + snapId: SnapId, + method: AccountDataUpdatedKeyringEvent, + message: SnapMessage, + ): null { + log( + `Forwarding message "${method}" from Snap "${snapId}" as a SnapAccountService event...`, + ); + + if (method === KeyringEvent.AccountAssetListUpdated) { + assertStruct(message, AccountAssetListUpdatedEventStruct); + this.#messenger.publish( + 'SnapAccountService:accountAssetListUpdated', + message.params, + ); + return null; + } + + if (method === KeyringEvent.AccountBalancesUpdated) { + assertStruct(message, AccountBalancesUpdatedEventStruct); + this.#messenger.publish( + 'SnapAccountService:accountBalancesUpdated', + message.params, + ); + return null; + } + + assertStruct(message, AccountTransactionsUpdatedEventStruct); + this.#messenger.publish( + 'SnapAccountService:accountTransactionsUpdated', + message.params, + ); + return null; + } + /** * Forwards the accounts of the given account group to the Snap keyring. * diff --git a/packages/snap-account-service/src/index.ts b/packages/snap-account-service/src/index.ts index 1713e9e6ce..be70e8ca14 100644 --- a/packages/snap-account-service/src/index.ts +++ b/packages/snap-account-service/src/index.ts @@ -3,6 +3,9 @@ export type { SnapAccountServiceActions, SnapAccountServiceConfig, SnapAccountServiceEvents, + SnapAccountServiceAccountBalancesUpdatedEvent, + SnapAccountServiceAccountTransactionsUpdatedEvent, + SnapAccountServiceAccountAssetListUpdatedEvent, SnapAccountServiceMessenger, SnapAccountServiceOptions, } from './SnapAccountService'; From b034cd29d6669caee4febf365dac1488fe1c71b9 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 2 Jun 2026 20:38:30 +0200 Subject: [PATCH 11/18] chore: changelog --- packages/accounts-controller/CHANGELOG.md | 7 ++----- packages/snap-account-service/CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 40589f96db..8e86458ec7 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -9,11 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Now requires `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. - -### Fixed - -- Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events. +- **BREAKING:** Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events ([#8916](https://github.com/MetaMask/core/pull/8916)) + - Now requires `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. ## [38.1.1] diff --git a/packages/snap-account-service/CHANGELOG.md b/packages/snap-account-service/CHANGELOG.md index ac626bb3d4..19070208df 100644 --- a/packages/snap-account-service/CHANGELOG.md +++ b/packages/snap-account-service/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events. +- Add `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events ([#8916](https://github.com/MetaMask/core/pull/8916)) ### Changed From 4258151f0de31d6f1554117064ab78aebd4be128 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 2 Jun 2026 21:51:25 +0200 Subject: [PATCH 12/18] refactor: wording --- .../src/SnapAccountService.ts | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts index 8aae251735..ff2e4d724d 100644 --- a/packages/snap-account-service/src/SnapAccountService.ts +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -191,16 +191,16 @@ type AccountDataUpdatedKeyringEvent = /** * Checks if a Snap message method is an account data update event. * - * @param method - The Snap message method. + * @param event - The Snap message event. * @returns `true` if the method can be forwarded without the legacy Snap keyring. */ function isAccountDataUpdatedKeyringEvent( - method: string, -): method is AccountDataUpdatedKeyringEvent { + event: string, +): event is AccountDataUpdatedKeyringEvent { return ( - method === KeyringEvent.AccountAssetListUpdated || - method === KeyringEvent.AccountBalancesUpdated || - method === KeyringEvent.AccountTransactionsUpdated + event === KeyringEvent.AccountAssetListUpdated || + event === KeyringEvent.AccountBalancesUpdated || + event === KeyringEvent.AccountTransactionsUpdated ); } @@ -447,17 +447,17 @@ export class SnapAccountService { snapId: SnapId, message: SnapMessage, ): Promise { - const { method } = message; - - if (isAccountDataUpdatedKeyringEvent(method)) { - return this.#publishAccountDataUpdatedEvent(snapId, method, message); + const { method: event } = message; + if (isAccountDataUpdatedKeyringEvent(event)) { + return this.#publishAccountDataUpdatedEvent(snapId, event, message); } let snapKeyring: LegacySnapKeyring | undefined = await this.#getLegacySnapKeyringIfAvailable(); // Handle specific methods first. - if (message.method === SnapManageAccountsMethod.GetSelectedAccounts) { + const { method } = message; + if (method === SnapManageAccountsMethod.GetSelectedAccounts) { if (snapKeyring) { // The legacy Snap keyring already maintain a local list of selected accounts per Snaps, so we // just delegate the call. @@ -469,7 +469,6 @@ export class SnapAccountService { return []; } - const event = message.method as KeyringEvent; // We assume the Snap platform always sends a valid `KeyringEvent` here. log( `Forwarding message "${event}" from Snap "${snapId}" to its keyring...`, ); @@ -497,42 +496,40 @@ export class SnapAccountService { * Publishes an account data update event from a Snap. * * @param snapId - ID of the Snap. - * @param method - Account data update event method. + * @param event - Account data update event. * @param message - Message sent by the Snap. * @returns `null`. */ #publishAccountDataUpdatedEvent( snapId: SnapId, - method: AccountDataUpdatedKeyringEvent, + event: AccountDataUpdatedKeyringEvent, message: SnapMessage, ): null { log( - `Forwarding message "${method}" from Snap "${snapId}" as a SnapAccountService event...`, + `Forwarding message "${event}" from Snap "${snapId}" as a SnapAccountService event...`, ); - if (method === KeyringEvent.AccountAssetListUpdated) { + if (event === KeyringEvent.AccountAssetListUpdated) { assertStruct(message, AccountAssetListUpdatedEventStruct); this.#messenger.publish( 'SnapAccountService:accountAssetListUpdated', message.params, ); - return null; - } - - if (method === KeyringEvent.AccountBalancesUpdated) { + } else if (event === KeyringEvent.AccountBalancesUpdated) { assertStruct(message, AccountBalancesUpdatedEventStruct); this.#messenger.publish( 'SnapAccountService:accountBalancesUpdated', message.params, ); - return null; + } else if (event === KeyringEvent.AccountTransactionsUpdated) { + assertStruct(message, AccountTransactionsUpdatedEventStruct); + this.#messenger.publish( + 'SnapAccountService:accountTransactionsUpdated', + message.params, + ); } - assertStruct(message, AccountTransactionsUpdatedEventStruct); - this.#messenger.publish( - 'SnapAccountService:accountTransactionsUpdated', - message.params, - ); + // We need to return a valid JSON value, so we cannot use `undefined` here. return null; } From ab40ded3eadd0ae7a1d23c7fd2579e7369c05237 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 2 Jun 2026 21:54:02 +0200 Subject: [PATCH 13/18] chore: jsdocs --- packages/snap-account-service/src/SnapAccountService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/snap-account-service/src/SnapAccountService.ts b/packages/snap-account-service/src/SnapAccountService.ts index ff2e4d724d..55757ce1de 100644 --- a/packages/snap-account-service/src/SnapAccountService.ts +++ b/packages/snap-account-service/src/SnapAccountService.ts @@ -183,6 +183,11 @@ function isLegacySnapKeyring(keyring: { return keyring.type === KeyringTypes.snap; } +/** + * Account data update events that can be forwarded from Snaps. + * + * These events are then re-emitted by the service for other consumers. + */ type AccountDataUpdatedKeyringEvent = | KeyringEvent.AccountAssetListUpdated | KeyringEvent.AccountBalancesUpdated From b08e13e12816c3814ecc143f7b4c09857bed2bf7 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 2 Jun 2026 22:50:18 +0200 Subject: [PATCH 14/18] chore: revert accounts-controller changes (will be split in its own PR) --- packages/accounts-controller/CHANGELOG.md | 5 - .../src/AccountsController.test.ts | 91 ------------------- .../src/AccountsController.ts | 45 --------- packages/accounts-controller/src/index.ts | 3 - 4 files changed, 144 deletions(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 0af4c57c6a..812cb46320 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,11 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed - -- **BREAKING:** Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events ([#8916](https://github.com/MetaMask/core/pull/8916)) - - Now requires `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. - ## [38.1.2] ### Changed diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index d115dc812a..939c27df91 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -331,9 +331,6 @@ function buildAccountsControllerMessenger( 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', - 'SnapAccountService:accountAssetListUpdated', - 'SnapAccountService:accountBalancesUpdated', - 'SnapAccountService:accountTransactionsUpdated', 'MultichainNetworkController:networkDidChange', ], }); @@ -2170,29 +2167,6 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); - it('re-publishes keyring events: SnapAccountService:accountBalancesUpdated', () => { - const { account, messenger } = setupTest(); - - const payload: AccountBalancesUpdatedEventPayload = { - balances: { - [account.id]: { - 'bip122:000000000019d6689c085ae165831e93/slip44:0': { - amount: '0.1', - unit: 'BTC', - }, - }, - }, - }; - - const mockRePublishedCallback = jest.fn(); - messenger.subscribe( - 'AccountsController:accountBalancesUpdated', - mockRePublishedCallback, - ); - messenger.publish('SnapAccountService:accountBalancesUpdated', payload); - expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); - }); - it('re-publishes keyring events: SnapKeyring:accountAssetListUpdated', () => { const { account, messenger } = setupTest(); @@ -2214,27 +2188,6 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); - it('re-publishes keyring events: SnapAccountService:accountAssetListUpdated', () => { - const { account, messenger } = setupTest(); - - const payload: AccountAssetListUpdatedEventPayload = { - assets: { - [account.id]: { - added: ['bip122:000000000019d6689c085ae165831e93/slip44:0'], - removed: ['bip122:000000000933ea01ad0ee984209779ba/slip44:0'], - }, - }, - }; - - const mockRePublishedCallback = jest.fn(); - messenger.subscribe( - 'AccountsController:accountAssetListUpdated', - mockRePublishedCallback, - ); - messenger.publish('SnapAccountService:accountAssetListUpdated', payload); - expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); - }); - it('re-publishes keyring events: SnapKeyring:accountTransactionsUpdated', () => { const { account, messenger } = setupTest(); @@ -2275,50 +2228,6 @@ describe('AccountsController', () => { messenger.publish('SnapKeyring:accountTransactionsUpdated', payload); expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); - - it('re-publishes keyring events: SnapAccountService:accountTransactionsUpdated', () => { - const { account, messenger } = setupTest(); - - const payload: AccountTransactionsUpdatedEventPayload = { - transactions: { - [account.id]: [ - { - id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', - timestamp: null, - chain: 'bip122:000000000019d6689c085ae165831e93', - status: 'submitted', - type: 'receive', - account: account.id, - from: [], - to: [], - fees: [ - { - type: 'base', - asset: { - fungible: true, - type: 'bip122:000000000019d6689c085ae165831e93/slip44:0', - unit: 'BTC', - amount: '0.0001', - }, - }, - ], - events: [], - }, - ], - }, - }; - - const mockRePublishedCallback = jest.fn(); - messenger.subscribe( - 'AccountsController:accountTransactionsUpdated', - mockRePublishedCallback, - ); - messenger.publish( - 'SnapAccountService:accountTransactionsUpdated', - payload, - ); - expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); - }); }); describe('handle MultichainNetworkController:networkDidChange event', () => { diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 55b6122932..e5216b49b1 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -206,21 +206,6 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; }; -export type SnapAccountServiceAccountBalancesUpdatedEvent = { - type: `SnapAccountService:accountBalancesUpdated`; - payload: SnapKeyringAccountBalancesUpdatedEvent['payload']; -}; - -export type SnapAccountServiceAccountTransactionsUpdatedEvent = { - type: `SnapAccountService:accountTransactionsUpdated`; - payload: SnapKeyringAccountTransactionsUpdatedEvent['payload']; -}; - -export type SnapAccountServiceAccountAssetListUpdatedEvent = { - type: `SnapAccountService:accountAssetListUpdated`; - payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; -}; - /** * @deprecated This type is deprecated and will be removed in a future version. * Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead. @@ -230,9 +215,6 @@ export type AllowedEvents = | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent | SnapKeyringAccountTransactionsUpdatedEvent - | SnapAccountServiceAccountAssetListUpdatedEvent - | SnapAccountServiceAccountBalancesUpdatedEvent - | SnapAccountServiceAccountTransactionsUpdatedEvent | MultichainNetworkControllerNetworkDidChangeEvent; /** @@ -1305,15 +1287,6 @@ export class AccountsController extends BaseController< ), ); - this.messenger.subscribe( - 'SnapAccountService:accountAssetListUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountAssetListUpdated', - snapAccountEvent, - ), - ); - this.messenger.subscribe( 'SnapKeyring:accountBalancesUpdated', (snapAccountEvent) => @@ -1323,15 +1296,6 @@ export class AccountsController extends BaseController< ), ); - this.messenger.subscribe( - 'SnapAccountService:accountBalancesUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountBalancesUpdated', - snapAccountEvent, - ), - ); - this.messenger.subscribe( 'SnapKeyring:accountTransactionsUpdated', (snapAccountEvent) => @@ -1341,15 +1305,6 @@ export class AccountsController extends BaseController< ), ); - this.messenger.subscribe( - 'SnapAccountService:accountTransactionsUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountTransactionsUpdated', - snapAccountEvent, - ), - ); - // Handle account change when multichain network is changed this.messenger.subscribe( 'MultichainNetworkController:networkDidChange', diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 8700a73f61..bfaf0be8e5 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,9 +15,6 @@ export type { AccountsControllerAccountBalancesUpdatesEvent, AccountsControllerAccountTransactionsUpdatedEvent, AccountsControllerAccountAssetListUpdatedEvent, - SnapAccountServiceAccountBalancesUpdatedEvent, - SnapAccountServiceAccountTransactionsUpdatedEvent, - SnapAccountServiceAccountAssetListUpdatedEvent, AllowedEvents, AccountsControllerEvents, AccountsControllerMessenger, From e33e897e68538b208f9d3817263d3a9f5df92d06 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 2 Jun 2026 22:54:35 +0200 Subject: [PATCH 15/18] Revert "chore: revert accounts-controller changes (will be split in its own PR)" This reverts commit b08e13e12816c3814ecc143f7b4c09857bed2bf7. --- packages/accounts-controller/CHANGELOG.md | 5 + .../src/AccountsController.test.ts | 91 +++++++++++++++++++ .../src/AccountsController.ts | 45 +++++++++ packages/accounts-controller/src/index.ts | 3 + 4 files changed, 144 insertions(+) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 812cb46320..0af4c57c6a 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **BREAKING:** Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events ([#8916](https://github.com/MetaMask/core/pull/8916)) + - Now requires `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. + ## [38.1.2] ### Changed diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index 939c27df91..d115dc812a 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -331,6 +331,9 @@ function buildAccountsControllerMessenger( 'SnapKeyring:accountAssetListUpdated', 'SnapKeyring:accountBalancesUpdated', 'SnapKeyring:accountTransactionsUpdated', + 'SnapAccountService:accountAssetListUpdated', + 'SnapAccountService:accountBalancesUpdated', + 'SnapAccountService:accountTransactionsUpdated', 'MultichainNetworkController:networkDidChange', ], }); @@ -2167,6 +2170,29 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); + it('re-publishes keyring events: SnapAccountService:accountBalancesUpdated', () => { + const { account, messenger } = setupTest(); + + const payload: AccountBalancesUpdatedEventPayload = { + balances: { + [account.id]: { + 'bip122:000000000019d6689c085ae165831e93/slip44:0': { + amount: '0.1', + unit: 'BTC', + }, + }, + }, + }; + + const mockRePublishedCallback = jest.fn(); + messenger.subscribe( + 'AccountsController:accountBalancesUpdated', + mockRePublishedCallback, + ); + messenger.publish('SnapAccountService:accountBalancesUpdated', payload); + expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); + }); + it('re-publishes keyring events: SnapKeyring:accountAssetListUpdated', () => { const { account, messenger } = setupTest(); @@ -2188,6 +2214,27 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); + it('re-publishes keyring events: SnapAccountService:accountAssetListUpdated', () => { + const { account, messenger } = setupTest(); + + const payload: AccountAssetListUpdatedEventPayload = { + assets: { + [account.id]: { + added: ['bip122:000000000019d6689c085ae165831e93/slip44:0'], + removed: ['bip122:000000000933ea01ad0ee984209779ba/slip44:0'], + }, + }, + }; + + const mockRePublishedCallback = jest.fn(); + messenger.subscribe( + 'AccountsController:accountAssetListUpdated', + mockRePublishedCallback, + ); + messenger.publish('SnapAccountService:accountAssetListUpdated', payload); + expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); + }); + it('re-publishes keyring events: SnapKeyring:accountTransactionsUpdated', () => { const { account, messenger } = setupTest(); @@ -2228,6 +2275,50 @@ describe('AccountsController', () => { messenger.publish('SnapKeyring:accountTransactionsUpdated', payload); expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); + + it('re-publishes keyring events: SnapAccountService:accountTransactionsUpdated', () => { + const { account, messenger } = setupTest(); + + const payload: AccountTransactionsUpdatedEventPayload = { + transactions: { + [account.id]: [ + { + id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', + timestamp: null, + chain: 'bip122:000000000019d6689c085ae165831e93', + status: 'submitted', + type: 'receive', + account: account.id, + from: [], + to: [], + fees: [ + { + type: 'base', + asset: { + fungible: true, + type: 'bip122:000000000019d6689c085ae165831e93/slip44:0', + unit: 'BTC', + amount: '0.0001', + }, + }, + ], + events: [], + }, + ], + }, + }; + + const mockRePublishedCallback = jest.fn(); + messenger.subscribe( + 'AccountsController:accountTransactionsUpdated', + mockRePublishedCallback, + ); + messenger.publish( + 'SnapAccountService:accountTransactionsUpdated', + payload, + ); + expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); + }); }); describe('handle MultichainNetworkController:networkDidChange event', () => { diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index e5216b49b1..55b6122932 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -206,6 +206,21 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; }; +export type SnapAccountServiceAccountBalancesUpdatedEvent = { + type: `SnapAccountService:accountBalancesUpdated`; + payload: SnapKeyringAccountBalancesUpdatedEvent['payload']; +}; + +export type SnapAccountServiceAccountTransactionsUpdatedEvent = { + type: `SnapAccountService:accountTransactionsUpdated`; + payload: SnapKeyringAccountTransactionsUpdatedEvent['payload']; +}; + +export type SnapAccountServiceAccountAssetListUpdatedEvent = { + type: `SnapAccountService:accountAssetListUpdated`; + payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; +}; + /** * @deprecated This type is deprecated and will be removed in a future version. * Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead. @@ -215,6 +230,9 @@ export type AllowedEvents = | SnapKeyringAccountAssetListUpdatedEvent | SnapKeyringAccountBalancesUpdatedEvent | SnapKeyringAccountTransactionsUpdatedEvent + | SnapAccountServiceAccountAssetListUpdatedEvent + | SnapAccountServiceAccountBalancesUpdatedEvent + | SnapAccountServiceAccountTransactionsUpdatedEvent | MultichainNetworkControllerNetworkDidChangeEvent; /** @@ -1287,6 +1305,15 @@ export class AccountsController extends BaseController< ), ); + this.messenger.subscribe( + 'SnapAccountService:accountAssetListUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountAssetListUpdated', + snapAccountEvent, + ), + ); + this.messenger.subscribe( 'SnapKeyring:accountBalancesUpdated', (snapAccountEvent) => @@ -1296,6 +1323,15 @@ export class AccountsController extends BaseController< ), ); + this.messenger.subscribe( + 'SnapAccountService:accountBalancesUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountBalancesUpdated', + snapAccountEvent, + ), + ); + this.messenger.subscribe( 'SnapKeyring:accountTransactionsUpdated', (snapAccountEvent) => @@ -1305,6 +1341,15 @@ export class AccountsController extends BaseController< ), ); + this.messenger.subscribe( + 'SnapAccountService:accountTransactionsUpdated', + (snapAccountEvent) => + this.#handleOnSnapKeyringAccountEvent( + 'AccountsController:accountTransactionsUpdated', + snapAccountEvent, + ), + ); + // Handle account change when multichain network is changed this.messenger.subscribe( 'MultichainNetworkController:networkDidChange', diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index bfaf0be8e5..8700a73f61 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,6 +15,9 @@ export type { AccountsControllerAccountBalancesUpdatesEvent, AccountsControllerAccountTransactionsUpdatedEvent, AccountsControllerAccountAssetListUpdatedEvent, + SnapAccountServiceAccountBalancesUpdatedEvent, + SnapAccountServiceAccountTransactionsUpdatedEvent, + SnapAccountServiceAccountAssetListUpdatedEvent, AllowedEvents, AccountsControllerEvents, AccountsControllerMessenger, From b04be5d8a3a7c0c7900647c028eb44f4185b5fb8 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Tue, 2 Jun 2026 23:06:16 +0200 Subject: [PATCH 16/18] refactor: remove SnapKering:* events --- packages/accounts-controller/CHANGELOG.md | 1 + .../src/AccountsController.test.ts | 88 ------------------- .../src/AccountsController.ts | 30 ------- 3 files changed, 1 insertion(+), 118 deletions(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index 0af4c57c6a..ef0acd6b09 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **BREAKING:** Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events ([#8916](https://github.com/MetaMask/core/pull/8916)) - Now requires `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. + - No longer requires `SnapKeyring:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. ## [38.1.2] diff --git a/packages/accounts-controller/src/AccountsController.test.ts b/packages/accounts-controller/src/AccountsController.test.ts index d115dc812a..1df8dfbf46 100644 --- a/packages/accounts-controller/src/AccountsController.test.ts +++ b/packages/accounts-controller/src/AccountsController.test.ts @@ -328,9 +328,6 @@ function buildAccountsControllerMessenger( ], events: [ 'KeyringController:stateChange', - 'SnapKeyring:accountAssetListUpdated', - 'SnapKeyring:accountBalancesUpdated', - 'SnapKeyring:accountTransactionsUpdated', 'SnapAccountService:accountAssetListUpdated', 'SnapAccountService:accountBalancesUpdated', 'SnapAccountService:accountTransactionsUpdated', @@ -2147,29 +2144,6 @@ describe('AccountsController', () => { return { messenger, account, accountsController }; }; - it('re-publishes keyring events: SnapKeyring:accountBalancesUpdated', () => { - const { account, messenger } = setupTest(); - - const payload: AccountBalancesUpdatedEventPayload = { - balances: { - [account.id]: { - 'bip122:000000000019d6689c085ae165831e93/slip44:0': { - amount: '0.1', - unit: 'BTC', - }, - }, - }, - }; - - const mockRePublishedCallback = jest.fn(); - messenger.subscribe( - 'AccountsController:accountBalancesUpdated', - mockRePublishedCallback, - ); - messenger.publish('SnapKeyring:accountBalancesUpdated', payload); - expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); - }); - it('re-publishes keyring events: SnapAccountService:accountBalancesUpdated', () => { const { account, messenger } = setupTest(); @@ -2193,27 +2167,6 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); - it('re-publishes keyring events: SnapKeyring:accountAssetListUpdated', () => { - const { account, messenger } = setupTest(); - - const payload: AccountAssetListUpdatedEventPayload = { - assets: { - [account.id]: { - added: ['bip122:000000000019d6689c085ae165831e93/slip44:0'], - removed: ['bip122:000000000933ea01ad0ee984209779ba/slip44:0'], - }, - }, - }; - - const mockRePublishedCallback = jest.fn(); - messenger.subscribe( - 'AccountsController:accountAssetListUpdated', - mockRePublishedCallback, - ); - messenger.publish('SnapKeyring:accountAssetListUpdated', payload); - expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); - }); - it('re-publishes keyring events: SnapAccountService:accountAssetListUpdated', () => { const { account, messenger } = setupTest(); @@ -2235,47 +2188,6 @@ describe('AccountsController', () => { expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); }); - it('re-publishes keyring events: SnapKeyring:accountTransactionsUpdated', () => { - const { account, messenger } = setupTest(); - - const payload: AccountTransactionsUpdatedEventPayload = { - transactions: { - [account.id]: [ - { - id: 'f5d8ee39a430901c91a5917b9f2dc19d6d1a0e9cea205b009ca73dd04470b9a6', - timestamp: null, - chain: 'bip122:000000000019d6689c085ae165831e93', - status: 'submitted', - type: 'receive', - account: account.id, - from: [], - to: [], - fees: [ - { - type: 'base', - asset: { - fungible: true, - type: 'bip122:000000000019d6689c085ae165831e93/slip44:0', - unit: 'BTC', - amount: '0.0001', - }, - }, - ], - events: [], - }, - ], - }, - }; - - const mockRePublishedCallback = jest.fn(); - messenger.subscribe( - 'AccountsController:accountTransactionsUpdated', - mockRePublishedCallback, - ); - messenger.publish('SnapKeyring:accountTransactionsUpdated', payload); - expect(mockRePublishedCallback).toHaveBeenCalledWith(payload); - }); - it('re-publishes keyring events: SnapAccountService:accountTransactionsUpdated', () => { const { account, messenger } = setupTest(); diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index 55b6122932..e6d926a27c 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -227,9 +227,6 @@ export type SnapAccountServiceAccountAssetListUpdatedEvent = { */ export type AllowedEvents = | KeyringControllerStateChangeEvent - | SnapKeyringAccountAssetListUpdatedEvent - | SnapKeyringAccountBalancesUpdatedEvent - | SnapKeyringAccountTransactionsUpdatedEvent | SnapAccountServiceAccountAssetListUpdatedEvent | SnapAccountServiceAccountBalancesUpdatedEvent | SnapAccountServiceAccountTransactionsUpdatedEvent @@ -1296,15 +1293,6 @@ export class AccountsController extends BaseController< this.#handleOnKeyringStateChange(keyringState), ); - this.messenger.subscribe( - 'SnapKeyring:accountAssetListUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountAssetListUpdated', - snapAccountEvent, - ), - ); - this.messenger.subscribe( 'SnapAccountService:accountAssetListUpdated', (snapAccountEvent) => @@ -1314,15 +1302,6 @@ export class AccountsController extends BaseController< ), ); - this.messenger.subscribe( - 'SnapKeyring:accountBalancesUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountBalancesUpdated', - snapAccountEvent, - ), - ); - this.messenger.subscribe( 'SnapAccountService:accountBalancesUpdated', (snapAccountEvent) => @@ -1332,15 +1311,6 @@ export class AccountsController extends BaseController< ), ); - this.messenger.subscribe( - 'SnapKeyring:accountTransactionsUpdated', - (snapAccountEvent) => - this.#handleOnSnapKeyringAccountEvent( - 'AccountsController:accountTransactionsUpdated', - snapAccountEvent, - ), - ); - this.messenger.subscribe( 'SnapAccountService:accountTransactionsUpdated', (snapAccountEvent) => From 7e64dc0a97a6914b32d31a70c6ebfb74b3ec2cf2 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 3 Jun 2026 10:07:07 +0200 Subject: [PATCH 17/18] chore: fix changelog --- packages/accounts-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/accounts-controller/CHANGELOG.md b/packages/accounts-controller/CHANGELOG.md index ef0acd6b09..77d4ee763a 100644 --- a/packages/accounts-controller/CHANGELOG.md +++ b/packages/accounts-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- **BREAKING:** Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events ([#8916](https://github.com/MetaMask/core/pull/8916)) +- **BREAKING:** Re-publish `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events as `AccountsController:account{AssetList,Balances,Transactions}Updated` events ([#8978](https://github.com/MetaMask/core/pull/8978)) - Now requires `SnapAccountService:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. - No longer requires `SnapKeyring:account{AssetList,Balances,Transactions}Updated` events to be registered on the messenger. From 4bcc1c6c19596d84b1859bdba6407e28b5ed1338 Mon Sep 17 00:00:00 2001 From: Charly Chevalier Date: Wed, 3 Jun 2026 21:46:38 +0200 Subject: [PATCH 18/18] fix: do not export SnapAccountService* + move them to types.ts --- .../src/AccountsController.ts | 22 +++++-------------- packages/accounts-controller/src/index.ts | 3 --- packages/accounts-controller/src/types.ts | 20 +++++++++++++++++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/accounts-controller/src/AccountsController.ts b/packages/accounts-controller/src/AccountsController.ts index e6d926a27c..4c1004df74 100644 --- a/packages/accounts-controller/src/AccountsController.ts +++ b/packages/accounts-controller/src/AccountsController.ts @@ -38,7 +38,12 @@ import { cloneDeep } from 'lodash'; import { AccountsControllerMethodActions } from './AccountsController-method-action-types'; import { projectLogger as log } from './logger'; -import type { MultichainNetworkControllerNetworkDidChangeEvent } from './types'; +import type { + MultichainNetworkControllerNetworkDidChangeEvent, + SnapAccountServiceAccountAssetListUpdatedEvent, + SnapAccountServiceAccountBalancesUpdatedEvent, + SnapAccountServiceAccountTransactionsUpdatedEvent, +} from './types'; import type { AccountsControllerStrictState } from './typing'; import type { HdSnapKeyringAccount } from './utils'; import { @@ -206,21 +211,6 @@ export type AccountsControllerAccountAssetListUpdatedEvent = { payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; }; -export type SnapAccountServiceAccountBalancesUpdatedEvent = { - type: `SnapAccountService:accountBalancesUpdated`; - payload: SnapKeyringAccountBalancesUpdatedEvent['payload']; -}; - -export type SnapAccountServiceAccountTransactionsUpdatedEvent = { - type: `SnapAccountService:accountTransactionsUpdated`; - payload: SnapKeyringAccountTransactionsUpdatedEvent['payload']; -}; - -export type SnapAccountServiceAccountAssetListUpdatedEvent = { - type: `SnapAccountService:accountAssetListUpdated`; - payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; -}; - /** * @deprecated This type is deprecated and will be removed in a future version. * Use `AccountTreeController`, `MultichainAccountService`, or the Keyring API v2 instead. diff --git a/packages/accounts-controller/src/index.ts b/packages/accounts-controller/src/index.ts index 8700a73f61..bfaf0be8e5 100644 --- a/packages/accounts-controller/src/index.ts +++ b/packages/accounts-controller/src/index.ts @@ -15,9 +15,6 @@ export type { AccountsControllerAccountBalancesUpdatesEvent, AccountsControllerAccountTransactionsUpdatedEvent, AccountsControllerAccountAssetListUpdatedEvent, - SnapAccountServiceAccountBalancesUpdatedEvent, - SnapAccountServiceAccountTransactionsUpdatedEvent, - SnapAccountServiceAccountAssetListUpdatedEvent, AllowedEvents, AccountsControllerEvents, AccountsControllerMessenger, diff --git a/packages/accounts-controller/src/types.ts b/packages/accounts-controller/src/types.ts index 1ee9421ec4..85678bb26e 100644 --- a/packages/accounts-controller/src/types.ts +++ b/packages/accounts-controller/src/types.ts @@ -1,6 +1,11 @@ // This file contains duplicate code from MultichainNetworkController.ts to avoid circular dependencies // It should be refactored to avoid duplication +import { + SnapKeyringAccountAssetListUpdatedEvent, + SnapKeyringAccountBalancesUpdatedEvent, + SnapKeyringAccountTransactionsUpdatedEvent, +} from '@metamask/eth-snap-keyring'; import type { CaipChainId } from '@metamask/keyring-api'; import type { NetworkClientId } from '@metamask/network-controller'; @@ -8,3 +13,18 @@ export type MultichainNetworkControllerNetworkDidChangeEvent = { type: `MultichainNetworkController:networkDidChange`; payload: [NetworkClientId | CaipChainId]; }; + +export type SnapAccountServiceAccountBalancesUpdatedEvent = { + type: `SnapAccountService:accountBalancesUpdated`; + payload: SnapKeyringAccountBalancesUpdatedEvent['payload']; +}; + +export type SnapAccountServiceAccountTransactionsUpdatedEvent = { + type: `SnapAccountService:accountTransactionsUpdated`; + payload: SnapKeyringAccountTransactionsUpdatedEvent['payload']; +}; + +export type SnapAccountServiceAccountAssetListUpdatedEvent = { + type: `SnapAccountService:accountAssetListUpdated`; + payload: SnapKeyringAccountAssetListUpdatedEvent['payload']; +};