Skip to content

Commit 0f63355

Browse files
authored
fix(account-tree-controller): prevent :account{Added,Removed} if init has not been called yet (#6717)
## Explanation I initially thought calling `init` implicitly would solve some issues, but it actually adds one when it comes to automatically migrating accounts from v1 state. In the clients we usually use the tree like so: ```ts this.accountsController.updateAccounts(); this.accountTreeController.init(); ``` The `updateAccounts` part re-creates the list of internal accounts and potentially migrate them if needed (in the case of BIP-44, this is needed to inject the `options.entropy` for those accounts). If a `KeyingController:stateChange` happens before `updateAccounts`, this might trigger some `:accountAdded` event, but some accounts might not have been automatically migrated yet (since `updateAccounts` has not been called at this point). Thus, this can lead the `AccountTreeController` to misuse those accounts and put them in wrong wallet (like older HD accounts had not `options.entropy` objects, thus they are not considered BIP-44 accounts yet). To avoid such issues (which seems to mainly happen for E2E), we wait for `init` to have been called explicitly. In anyway, calling `updateAccounts` right before `init` would make sure that the list of accounts is up-to-date AND migrated. ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [ ] I've updated the test suite for new or updated code as appropriate - [ ] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent ea6a2f2 commit 0f63355

File tree

3 files changed

+53
-17
lines changed

3 files changed

+53
-17
lines changed

packages/account-tree-controller/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Prevent `:account{Added,Removed}` to be used if `init` has not been called yet ([#6717](https://github.com/MetaMask/core/pull/6717))
13+
- We now wait for `init` to have been called at least once. Clients will need to ensure internal accounts are fully ready before calling `init`.
14+
- This should also enforce account group ordering, since all accounts will be ready to consume right away.
15+
1016
## [1.2.0]
1117

1218
### Added

packages/account-tree-controller/src/AccountTreeController.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,28 @@ describe('AccountTreeController', () => {
12151215
},
12161216
} as AccountTreeControllerState);
12171217
});
1218+
1219+
it('does not remove account if init has not been called', () => {
1220+
const { controller, messenger } = setup({
1221+
accounts: [MOCK_HD_ACCOUNT_1],
1222+
});
1223+
1224+
// Force ref to the controller, even if we don't use it in this test.
1225+
expect(controller).toBeDefined();
1226+
1227+
const mockAccountTreeChange = jest.fn();
1228+
messenger.subscribe(
1229+
'AccountTreeController:accountTreeChange',
1230+
mockAccountTreeChange,
1231+
);
1232+
1233+
messenger.publish(
1234+
'AccountsController:accountRemoved',
1235+
MOCK_HD_ACCOUNT_1.id,
1236+
);
1237+
1238+
expect(mockAccountTreeChange).not.toHaveBeenCalled();
1239+
});
12181240
});
12191241

12201242
describe('account ordering by type', () => {
@@ -1487,6 +1509,14 @@ describe('AccountTreeController', () => {
14871509
hasAccountTreeSyncingSyncedAtLeastOnce: false,
14881510
} as AccountTreeControllerState);
14891511
});
1512+
1513+
it('does not add any account if init has not been called', () => {
1514+
const { controller, messenger } = setup();
1515+
1516+
expect(controller.state.accountTree.wallets).toStrictEqual({});
1517+
messenger.publish('AccountsController:accountAdded', MOCK_HD_ACCOUNT_1);
1518+
expect(controller.state.accountTree.wallets).toStrictEqual({});
1519+
});
14901520
});
14911521

14921522
describe('on MultichainAccountService:walletStatusUpdate', () => {
@@ -4266,6 +4296,8 @@ describe('AccountTreeController', () => {
42664296
it('names non-HD keyrings accounts properly', () => {
42674297
const { controller, messenger } = setup();
42684298

4299+
controller.init();
4300+
42694301
// Add all 3 accounts.
42704302
[mockAccount1, mockAccount2, mockAccount3].forEach(
42714303
(mockAccount, index) => {
@@ -4418,6 +4450,8 @@ describe('AccountTreeController', () => {
44184450
it('fallbacks to natural indexing if group names are not using our default name pattern', () => {
44194451
const { controller, messenger } = setup();
44204452

4453+
controller.init();
4454+
44214455
[mockAccount1, mockAccount2, mockAccount3].forEach((mockAccount) =>
44224456
messenger.publish('AccountsController:accountAdded', mockAccount),
44234457
);

packages/account-tree-controller/src/AccountTreeController.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -346,15 +346,6 @@ export class AccountTreeController extends BaseController<
346346
this.init();
347347
}
348348

349-
/**
350-
* Force-init if the controller's state has not been initilized yet.
351-
*/
352-
#initAtLeastOnce() {
353-
if (!this.#initialized) {
354-
this.init();
355-
}
356-
}
357-
358349
/**
359350
* Rule for entropy-base wallets.
360351
*
@@ -664,12 +655,14 @@ export class AccountTreeController extends BaseController<
664655
* @param account - New account.
665656
*/
666657
#handleAccountAdded(account: InternalAccount) {
667-
// We force-init to make sure we have the proper account groups for the
668-
// incoming account change.
669-
this.#initAtLeastOnce();
658+
// We wait for the first `init` to be called to actually build up the tree and
659+
// mutate it. We expect the caller to first update the `AccountsController` state
660+
// to force the migration of accounts, and then call `init`.
661+
if (!this.#initialized) {
662+
return;
663+
}
670664

671-
// Check if this account got already added by `#initAtLeastOnce`, if not, then we
672-
// can proceed.
665+
// Check if this account is already known by the tree to avoid double-insertion.
673666
if (!this.#accountIdToContext.has(account.id)) {
674667
this.update((state) => {
675668
this.#insert(state.accountTree.wallets, account);
@@ -700,9 +693,12 @@ export class AccountTreeController extends BaseController<
700693
* @param accountId - Removed account ID.
701694
*/
702695
#handleAccountRemoved(accountId: AccountId) {
703-
// We force-init to make sure we have the proper account groups for the
704-
// incoming account change.
705-
this.#initAtLeastOnce();
696+
// We wait for the first `init` to be called to actually build up the tree and
697+
// mutate it. We expect the caller to first update the `AccountsController` state
698+
// to force the migration of accounts, and then call `init`.
699+
if (!this.#initialized) {
700+
return;
701+
}
706702

707703
const context = this.#accountIdToContext.get(accountId);
708704

0 commit comments

Comments
 (0)