Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3e62633
refactor: migrate `AccountsController` to `@metamask/messenger`
mikesposito Sep 1, 2025
443a6ee
update changelog
mikesposito Sep 1, 2025
b25ee8e
refactor `buildMessenger`
mikesposito Sep 1, 2025
3453a8d
remove unused variables
mikesposito Sep 1, 2025
3c6828e
update messenger imports
mikesposito Sep 3, 2025
4255c39
Merge branch 'main' into mikesposito/messenger/accounts-controller
mikesposito Sep 22, 2025
7db8785
update messenger package
mikesposito Sep 22, 2025
bff1536
update tsconfigs
mikesposito Sep 22, 2025
74bd264
update messenger package to `^0.3.0`
mikesposito Sep 22, 2025
17359ad
rename `anonymous` to `includeInDebugSnapshot`
mikesposito Sep 22, 2025
53e3844
Update packages/accounts-controller/src/AccountsController.test.ts
mikesposito Sep 22, 2025
86a9e0b
apply suggestion from @mcmire
mikesposito Sep 22, 2025
11a662d
update readme
mikesposito Sep 22, 2025
6c94ae5
Merge branch 'main' into mikesposito/messenger/accounts-controller
mikesposito Sep 23, 2025
06e5534
Merge branch 'main' into mikesposito/messenger/accounts-controller
mikesposito Oct 13, 2025
7f6b1f2
Merge branch 'main' into mikesposito/messenger/accounts-controller
mikesposito Oct 15, 2025
80e5d01
dedupe packages
mikesposito Oct 15, 2025
93bfbf9
Merge branch 'main' into mikesposito/messenger/accounts-controller
mikesposito Oct 24, 2025
b097a4e
Merge branch 'main' into mikesposito/messenger/accounts-controller
mikesposito Oct 24, 2025
74f2117
Merge branch 'main' into mikesposito/messenger/accounts-controller
mikesposito Oct 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/accounts-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **BREAKING:** Use new `Messenger` from `@metamask/messenger` ([#6426](https://github.com/MetaMask/core/pull/6426))
- Previously, `AccountsController` accepted a `RestrictedMessenger` instance from `@metamask/base-controller`.
- Bump `@metamask/base-controller` from `^8.1.0` to `^8.2.0` ([#6355](https://github.com/MetaMask/core/pull/6355))

## [33.0.0]
Expand Down
1 change: 1 addition & 0 deletions packages/accounts-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@metamask/keyring-api": "^20.1.0",
"@metamask/keyring-internal-api": "^8.1.0",
"@metamask/keyring-utils": "^3.1.0",
"@metamask/messenger": "^0.1.0",
"@metamask/snaps-sdk": "^9.0.0",
"@metamask/snaps-utils": "^11.0.0",
"@metamask/superstruct": "^3.1.0",
Expand Down
153 changes: 88 additions & 65 deletions packages/accounts-controller/src/AccountsController.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Messenger } from '@metamask/base-controller';
import { InfuraNetworkType } from '@metamask/controller-utils';
import type {
AccountAssetListUpdatedEventPayload,
Expand All @@ -15,19 +14,23 @@
} from '@metamask/keyring-api';
import { KeyringTypes } from '@metamask/keyring-controller';
import type { InternalAccount } from '@metamask/keyring-internal-api';
import {
MOCK_ANY_NAMESPACE,
Messenger,
type MessengerActions,
type MessengerEvents,
type MockAnyNamespace,
} from '@metamask/messenger';
import type { NetworkClientId } from '@metamask/network-controller';
import type { SnapControllerState } from '@metamask/snaps-controllers';
import { SnapStatus } from '@metamask/snaps-utils';
import type { CaipChainId } from '@metamask/utils';
import type { V4Options } from 'uuid';
import * as uuid from 'uuid';

Check warning on line 29 in packages/accounts-controller/src/AccountsController.test.ts

View workflow job for this annotation

GitHub Actions / Lint, build, and test / Lint (22.x)

No exported names found in module 'uuid'

import type {
AccountsControllerActions,
AccountsControllerEvents,
AccountsControllerMessenger,
AccountsControllerState,
AllowedActions,
AllowedEvents,
} from './AccountsController';
import { AccountsController, EMPTY_ACCOUNT } from './AccountsController';
import {
Expand All @@ -41,6 +44,17 @@
keyringTypeToName,
} from './utils';

type AllAccountsControllerActions =
MessengerActions<AccountsControllerMessenger>;

type AllAccountsControllerEvents = MessengerEvents<AccountsControllerMessenger>;

type RootMessenger = Messenger<
MockAnyNamespace,
AllAccountsControllerActions,
AllAccountsControllerEvents
>;

jest.mock('uuid');
const mockUUID = jest.spyOn(uuid, 'v4');
const actualUUID = jest.requireActual('uuid').v4; // We also use uuid.v4 in our mocks
Expand Down Expand Up @@ -217,64 +231,66 @@
}

/**
* Builds a new instance of the Messenger class for the AccountsController.
* Builds a new instance of the Root Messenger.
*
* @returns A new instance of the Messenger class for the AccountsController.
* @returns A new instance of the Root Messenger.
*/
function buildMessenger() {
return new Messenger<
AccountsControllerActions | AllowedActions,
AccountsControllerEvents | AllowedEvents
>();
function buildMessenger(): RootMessenger {
return new Messenger({ namespace: MOCK_ANY_NAMESPACE });
}

/**
* Builds a restricted messenger for the AccountsController.
* Builds a messenger for the AccountsController.
*
* @param messenger - The messenger to restrict.
* @returns The restricted messenger.
* @param rootMessenger - The parent messenger.
* @returns The messenger for AccountsController.
*/
function buildAccountsControllerMessenger(messenger = buildMessenger()) {
return messenger.getRestricted({
name: 'AccountsController',
allowedEvents: [
function buildAccountsControllerMessenger(rootMessenger = buildMessenger()) {
const accountsControllerMessenger = new Messenger<
'AccountsController',
AllAccountsControllerActions,
AllAccountsControllerEvents,
typeof rootMessenger
>({
namespace: 'AccountsController',
parent: rootMessenger,
});
rootMessenger.delegate({
messenger: accountsControllerMessenger,
actions: [
'KeyringController:getState',
'KeyringController:getKeyringsByType',
],
events: [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Delegation Missing for AccountsController Events

The delegation setup is incomplete. The rootMessenger.delegate() call delegates actions and events FROM the root messenger TO the AccountsController messenger, but it doesn't set up delegation for AccountsController's own published events (like AccountsController:accountAdded, AccountsController:accountRemoved, AccountsController:selectedAccountChange, etc.) to propagate back to the root messenger. This means other controllers subscribing to AccountsController events via the root messenger won't receive them. The delegation should likely include AccountsController's own events or use a bidirectional delegation mechanism.

Fix in Cursor Fix in Web

'SnapController:stateChange',
'KeyringController:stateChange',
'SnapKeyring:accountAssetListUpdated',
'SnapKeyring:accountBalancesUpdated',
'SnapKeyring:accountTransactionsUpdated',
'MultichainNetworkController:networkDidChange',
],
allowedActions: [
'KeyringController:getState',
'KeyringController:getKeyringsByType',
],
});
return accountsControllerMessenger;
}

/**
* Sets up an instance of the AccountsController class with the given initial state and callbacks.
*
* @param options - The options object.
* @param [options.initialState] - The initial state to use for the AccountsController.
* @param [options.messenger] - Messenger to use for the AccountsController.
* @param [options.messenger] - The root messenger to use for creating the AccountsController messenger.
* @returns An instance of the AccountsController class.
*/
function setupAccountsController({
initialState = {},
messenger = buildMessenger(),
}: {
initialState?: Partial<AccountsControllerState>;
messenger?: Messenger<
AccountsControllerActions | AllowedActions,
AccountsControllerEvents | AllowedEvents
>;
messenger?: RootMessenger;
}): {
accountsController: AccountsController;
messenger: Messenger<
AccountsControllerActions | AllowedActions,
AccountsControllerEvents | AllowedEvents
>;
messenger: RootMessenger;
accountsControllerMessenger: AccountsControllerMessenger;
triggerMultichainNetworkChange: (id: NetworkClientId | CaipChainId) => void;
} {
const accountsControllerMessenger =
Expand All @@ -288,7 +304,12 @@
const triggerMultichainNetworkChange = (id: NetworkClientId | CaipChainId) =>
messenger.publish('MultichainNetworkController:networkDidChange', id);

return { accountsController, messenger, triggerMultichainNetworkChange };
return {
accountsController,
messenger,
accountsControllerMessenger,
triggerMultichainNetworkChange,
};
}

describe('AccountsController', () => {
Expand Down Expand Up @@ -1136,11 +1157,10 @@

it('publishes accountAdded event', async () => {
const messenger = buildMessenger();
const messengerSpy = jest.spyOn(messenger, 'publish');

mockUUIDWithNormalAccounts([mockAccount, mockAccount2]);

setupAccountsController({
const { accountsControllerMessenger } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
Expand All @@ -1152,6 +1172,8 @@
messenger,
});

const messengerSpy = jest.spyOn(accountsControllerMessenger, 'publish');

const mockNewKeyringState = {
isUnlocked: true,
keyrings: [
Expand All @@ -1172,11 +1194,10 @@
[],
);

// First call is 'KeyringController:stateChange'
// First call is 'AccountsController:stateChange'
expect(messengerSpy).toHaveBeenNthCalledWith(
// 1. KeyringController:stateChange
// 2. AccountsController:stateChange
3,
// 1. AccountsController:stateChange
2,
'AccountsController:accountAdded',
MockExpectedInternalAccountBuilder.from(mockAccount2)
.setExpectedLastSelectedAsAny()
Expand Down Expand Up @@ -1437,11 +1458,10 @@

it('publishes accountRemoved event', async () => {
const messenger = buildMessenger();
const messengerSpy = jest.spyOn(messenger, 'publish');

mockUUIDWithNormalAccounts([mockAccount, mockAccount2]);

setupAccountsController({
const { accountsControllerMessenger } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
Expand All @@ -1454,6 +1474,8 @@
messenger,
});

const messengerSpy = jest.spyOn(accountsControllerMessenger, 'publish');

const mockNewKeyringState = {
isUnlocked: true,
keyrings: [
Expand All @@ -1473,11 +1495,10 @@
[],
);

// First call is 'KeyringController:stateChange'
// First call is 'AccountsController:stateChange'
expect(messengerSpy).toHaveBeenNthCalledWith(
// 1. KeyringController:stateChange
// 2. AccountsController:stateChange
3,
// 1. AccountsController:stateChange
2,
'AccountsController:accountRemoved',
mockAccount3.id,
);
Expand Down Expand Up @@ -3085,19 +3106,20 @@
},
type: BtcAccountType.P2wpkh,
});
const { accountsController, messenger } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
[mockAccount.id]: mockAccount,
[mockNonEvmAccount.id]: mockNonEvmAccount,
const { accountsController, accountsControllerMessenger } =
setupAccountsController({
initialState: {
internalAccounts: {
accounts: {
[mockAccount.id]: mockAccount,
[mockNonEvmAccount.id]: mockNonEvmAccount,
},
selectedAccount: mockAccount.id,
},
selectedAccount: mockAccount.id,
},
},
});
});

const messengerSpy = jest.spyOn(messenger, 'publish');
const messengerSpy = jest.spyOn(accountsControllerMessenger, 'publish');

accountsController.setSelectedAccount(mockNonEvmAccount.id);

Expand Down Expand Up @@ -3191,10 +3213,10 @@
});

it('publishes the accountRenamed event', () => {
const { accountsController, messenger } =
const { accountsController, accountsControllerMessenger } =
setupAccountsController(mockState);

const messengerSpy = jest.spyOn(messenger, 'publish');
const messengerSpy = jest.spyOn(accountsControllerMessenger, 'publish');

accountsController.setAccountNameAndSelectAccount(
mockAccount.id,
Expand Down Expand Up @@ -3269,16 +3291,17 @@
});

it('publishes the accountRenamed event', () => {
const { accountsController, messenger } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: { [mockAccount.id]: mockAccount },
selectedAccount: mockAccount.id,
const { accountsController, accountsControllerMessenger } =
setupAccountsController({
initialState: {
internalAccounts: {
accounts: { [mockAccount.id]: mockAccount },
selectedAccount: mockAccount.id,
},
},
},
});
});

const messengerSpy = jest.spyOn(messenger, 'publish');
const messengerSpy = jest.spyOn(accountsControllerMessenger, 'publish');

accountsController.setAccountName(mockAccount.id, 'new name');

Expand Down
Loading
Loading