Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cf24808
refactor: migrate `AccountTreeController` to `@metamask/messenger`
mikesposito Aug 25, 2025
83294c8
fix: re-delegate after unregister and register action
mikesposito Aug 25, 2025
75b5398
update changelog
mikesposito Aug 25, 2025
33c4ea2
revert unintentional change
mikesposito Aug 25, 2025
a8d9b51
fix lint
mikesposito Aug 26, 2025
b3df752
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Aug 26, 2025
da25c0e
use jest mock instead of actions handler swap
mikesposito Aug 28, 2025
b419ecd
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Sep 23, 2025
bee9e6f
refactor: use `MockAnyNamespace` namespace for root messenger
mikesposito Sep 23, 2025
a91b196
update tsconfig files and README
mikesposito Sep 23, 2025
d3691a5
update messenger to 0.3.0
mikesposito Sep 23, 2025
6746c38
update changelog
mikesposito Sep 23, 2025
8d8a971
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Oct 13, 2025
a88ad38
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Oct 15, 2025
7fe630b
run dedupe
mikesposito Oct 15, 2025
03224dd
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Oct 15, 2025
aa67cde
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Oct 20, 2025
234d462
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Oct 23, 2025
84be27c
Merge branch 'main' into mikesposito/messenger/account-tree-controller
mikesposito Oct 24, 2025
e0cbe0e
update readme content
mikesposito Oct 24, 2025
102de9d
update changelog
mikesposito Oct 24, 2025
a185851
Merge branch 'main' into mikesposito/messenger/account-tree-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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ linkStyle default opacity:0.5
transaction_controller(["@metamask/transaction-controller"]);
user_operation_controller(["@metamask/user-operation-controller"]);
account_tree_controller --> base_controller;
account_tree_controller --> messenger;
account_tree_controller --> accounts_controller;
account_tree_controller --> keyring_controller;
account_tree_controller --> multichain_account_service;
Expand Down Expand Up @@ -216,6 +217,7 @@ linkStyle default opacity:0.5
earn_controller --> transaction_controller;
eip_5792_middleware --> transaction_controller;
eip_5792_middleware --> keyring_controller;
eip_7702_internal_rpc_middleware --> controller_utils;
eip1193_permission_middleware --> chain_agnostic_permission;
eip1193_permission_middleware --> controller_utils;
eip1193_permission_middleware --> json_rpc_engine;
Expand Down
5 changes: 5 additions & 0 deletions packages/account-tree-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **BREAKING:** Use new `Messenger` from `@metamask/messenger` ([#6380](https://github.com/MetaMask/core/pull/6380))
- Previously, `AccountTreeController` accepted a `RestrictedMessenger` instance from `@metamask/base-controller`.

## [1.6.0]

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/account-tree-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"@metamask/base-controller": "^8.4.2",
"@metamask/messenger": "^0.3.0",
"@metamask/snaps-sdk": "^9.0.0",
"@metamask/snaps-utils": "^11.0.0",
"@metamask/superstruct": "^3.1.0",
Expand Down
128 changes: 37 additions & 91 deletions packages/account-tree-controller/src/AccountTreeController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type AccountGroupId,
} from '@metamask/account-api';
import type { AccountId } from '@metamask/accounts-controller';
import { Messenger, deriveStateFromMetadata } from '@metamask/base-controller';
import { deriveStateFromMetadata } from '@metamask/base-controller/next';
import {
BtcAccountType,
EthAccountType,
Expand All @@ -36,14 +36,11 @@ import type { BackupAndSyncAnalyticsEventPayload } from './backup-and-sync/analy
import { BackupAndSyncService } from './backup-and-sync/service';
import { isAccountGroupNameUnique } from './group';
import { getAccountWalletNameFromKeyringType } from './rules/keyring';
import { type AccountTreeControllerState } from './types';
import {
type AccountTreeControllerMessenger,
type AccountTreeControllerActions,
type AccountTreeControllerEvents,
type AccountTreeControllerState,
type AllowedActions,
type AllowedEvents,
} from './types';
getAccountTreeControllerMessenger,
getRootMessenger,
} from '../tests/mockMessenger';

// Local mock of EMPTY_ACCOUNT to avoid circular dependency
const EMPTY_ACCOUNT_MOCK: InternalAccount = {
Expand Down Expand Up @@ -233,53 +230,7 @@ const MOCK_HARDWARE_ACCOUNT_1: InternalAccount = {
},
};

/**
* Creates a new root messenger instance for testing.
*
* @returns A new Messenger instance.
*/
function getRootMessenger() {
return new Messenger<
AccountTreeControllerActions | AllowedActions,
AccountTreeControllerEvents | AllowedEvents
>();
}

/**
* Retrieves a restricted messenger for the AccountTreeController.
*
* @param messenger - The root messenger instance. Defaults to a new Messenger created by getRootMessenger().
* @returns The restricted messenger for the AccountTreeController.
*/
function getAccountTreeControllerMessenger(
messenger = getRootMessenger(),
): AccountTreeControllerMessenger {
return messenger.getRestricted({
name: 'AccountTreeController',
allowedEvents: [
'AccountsController:accountAdded',
'AccountsController:accountRemoved',
'AccountsController:selectedAccountChange',
'UserStorageController:stateChange',
'MultichainAccountService:walletStatusChange',
],
allowedActions: [
'AccountsController:listMultichainAccounts',
'AccountsController:getAccount',
'AccountsController:getSelectedMultichainAccount',
'AccountsController:setSelectedAccount',
'UserStorageController:getState',
'UserStorageController:performGetStorage',
'UserStorageController:performGetStorageAllFeatureEntries',
'UserStorageController:performSetStorage',
'UserStorageController:performBatchSetStorage',
'AuthenticationController:getSessionProfile',
'MultichainAccountService:createMultichainAccountGroup',
'KeyringController:getState',
'SnapController:get',
],
});
}
const mockGetSelectedMultichainAccountActionHandler = jest.fn();

/**
* Sets up the AccountTreeController for testing.
Expand Down Expand Up @@ -317,10 +268,7 @@ function setup({
},
}: {
state?: Partial<AccountTreeControllerState>;
messenger?: Messenger<
AccountTreeControllerActions | AllowedActions,
AccountTreeControllerEvents | AllowedEvents
>;
messenger?: ReturnType<typeof getRootMessenger>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Do we want to export a RootMessenger type from mockMessenger.ts?

accounts?: InternalAccount[];
keyrings?: KeyringObject[];
config?: {
Expand All @@ -338,9 +286,9 @@ function setup({
};
} = {}): {
controller: AccountTreeController;
messenger: Messenger<
AccountTreeControllerActions | AllowedActions,
AccountTreeControllerEvents | AllowedEvents
messenger: ReturnType<typeof getRootMessenger>;
accountTreeControllerMessenger: ReturnType<
typeof getAccountTreeControllerMessenger
>;
spies: {
consoleWarn: jest.SpyInstance;
Expand Down Expand Up @@ -401,6 +349,7 @@ function setup({
mocks.AccountsController.listMultichainAccounts.mockImplementation(
() => mocks.AccountsController.accounts,
);

messenger.registerActionHandler(
'AccountsController:listMultichainAccounts',
mocks.AccountsController.listMultichainAccounts,
Expand Down Expand Up @@ -474,8 +423,10 @@ function setup({
);
}

const accountTreeControllerMessenger =
getAccountTreeControllerMessenger(messenger);
const controller = new AccountTreeController({
messenger: getAccountTreeControllerMessenger(messenger),
messenger: accountTreeControllerMessenger,
state,
...(config && { config }),
});
Expand All @@ -487,6 +438,7 @@ function setup({
return {
controller,
messenger,
accountTreeControllerMessenger,
spies: { consoleWarn: consoleWarnSpy },
mocks,
};
Expand Down Expand Up @@ -1802,12 +1754,15 @@ describe('AccountTreeController', () => {
});

it('updates AccountsController selected account (with EVM account) when selectedAccountGroup changes', () => {
const { controller, messenger } = setup({
const { controller, accountTreeControllerMessenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1, MOCK_HD_ACCOUNT_2],
keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2],
});

const setSelectedAccountSpy = jest.spyOn(messenger, 'call');
const setSelectedAccountSpy = jest.spyOn(
accountTreeControllerMessenger,
'call',
);

controller.init();

Expand Down Expand Up @@ -1839,15 +1794,18 @@ describe('AccountTreeController', () => {
},
},
} as const;
const { controller, messenger } = setup({
const { controller, accountTreeControllerMessenger } = setup({
accounts: [
MOCK_HD_ACCOUNT_1,
nonEvmAccount2, // Wallet 2 > Account 1.
],
keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2],
});

const setSelectedAccountSpy = jest.spyOn(messenger, 'call');
const setSelectedAccountSpy = jest.spyOn(
accountTreeControllerMessenger,
'call',
);

controller.init();

Expand Down Expand Up @@ -1969,18 +1927,14 @@ describe('AccountTreeController', () => {
});

it('falls back to first wallet first group when AccountsController returns EMPTY_ACCOUNT', () => {
const { controller, messenger } = setup({
const { controller } = setup({
accounts: [MOCK_HD_ACCOUNT_1, MOCK_HD_ACCOUNT_2],
keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2],
});

// Unregister existing handler and register new one BEFORE init
messenger.unregisterActionHandler(
'AccountsController:getSelectedMultichainAccount',
);
messenger.registerActionHandler(
'AccountsController:getSelectedMultichainAccount',
() => EMPTY_ACCOUNT_MOCK,
// Mock action handler BEFORE init
mockGetSelectedMultichainAccountActionHandler.mockReturnValue(
EMPTY_ACCOUNT_MOCK,
);

controller.init();
Expand All @@ -1998,7 +1952,7 @@ describe('AccountTreeController', () => {
});

it('falls back to first wallet first group when selected account is not in tree', () => {
const { controller, messenger } = setup({
const { controller } = setup({
accounts: [MOCK_HD_ACCOUNT_1, MOCK_HD_ACCOUNT_2],
keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2],
});
Expand All @@ -2009,12 +1963,8 @@ describe('AccountTreeController', () => {
id: 'unknown-account-id',
};

messenger.unregisterActionHandler(
'AccountsController:getSelectedMultichainAccount',
);
messenger.registerActionHandler(
'AccountsController:getSelectedMultichainAccount',
() => unknownAccount,
mockGetSelectedMultichainAccountActionHandler.mockReturnValue(
unknownAccount,
);

controller.init();
Expand All @@ -2032,18 +1982,14 @@ describe('AccountTreeController', () => {
});

it('returns empty string when no wallets exist and getSelectedMultichainAccount returns EMPTY_ACCOUNT', () => {
const { controller, messenger } = setup({
const { controller } = setup({
accounts: [],
keyrings: [],
});

// Mock getSelectedMultichainAccount to return EMPTY_ACCOUNT_MOCK (id is '') BEFORE init
messenger.unregisterActionHandler(
'AccountsController:getSelectedMultichainAccount',
);
messenger.registerActionHandler(
'AccountsController:getSelectedMultichainAccount',
() => EMPTY_ACCOUNT_MOCK,
// Mock getSelectedAccount to return EMPTY_ACCOUNT_MOCK (id is '') BEFORE init
mockGetSelectedMultichainAccountActionHandler.mockReturnValue(
EMPTY_ACCOUNT_MOCK,
Copy link

Choose a reason for hiding this comment

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

Bug: Mock Handler Not Registered, Tests Fail

The mockGetSelectedMultichainAccountActionHandler is declared but not registered as an action handler. Tests using mockReturnValue() on this mock are ineffective, as the messenger uses the handler registered in the setup function instead. This means these tests are not verifying the intended scenarios.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Bug: Mock Handler Not Registered

The mockGetSelectedMultichainAccountActionHandler is defined but not registered as the handler for AccountsController:getSelectedMultichainAccount. Tests attempt to configure its return value, but the action is actually handled by mocks.AccountsController.getSelectedMultichainAccount. This means the test mocks have no effect, leading to unexpected test behavior.

Fix in Cursor Fix in Web

);

controller.init();
Expand Down Expand Up @@ -4328,7 +4274,7 @@ describe('AccountTreeController', () => {
deriveStateFromMetadata(
controller.state,
controller.metadata,
'anonymous',
'includeInDebugSnapshot',
),
).toMatchInlineSnapshot(`Object {}`);
});
Expand Down
Loading
Loading