Skip to content

fix(assets-controller): clean accountTree event handlers - #10059

Merged
Kriys94 merged 2 commits into
mainfrom
fix/AccountTreeAndAssetsController
Sep 2, 2026
Merged

fix(assets-controller): clean accountTree event handlers#10059
Kriys94 merged 2 commits into
mainfrom
fix/AccountTreeAndAssetsController

Conversation

@Kriys94

@Kriys94 Kriys94 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Explanation

Mobile

Extension:

Context / motivation

Mobile and extension do not trigger AccountTreeController:initialized at the same point in the lifecycle:

  • Mobile calls AccountTreeController.init() at startup via AccountTreeInitService.initializeAccountTree() (during authentication/login), so :initialized fires early in the app session.
  • Extension does not call init() during controller construction (see FIXME in account-tree-controller-init.ts — the keyring must be unlocked first). Instead, it calls AccountTreeController:init later in #initAccountsAfterUnlock() after unlock.

Both clients delegate AccountTreeController:initialized to AssetsController, but the event timing differs. AssetsController could not reliably gate asset tracking on that event alone — mirroring #accountTreeInitialized from it was fragile across clients.

This PR adds AccountTreeController:isInitialized and has AssetsController read lifecycle state on demand so tracking starts/stops consistently regardless of when each client runs init().

Additional cleanup

While making that change, we also removed other sources of drift and complexity:

  • Local lifecycle mirrors: AssetsController previously cached #uiOpen, #keyringUnlocked, and #accountTreeInitialized inside event handlers. Those flags could get out of sync if events were missed, reordered, or read before handlers ran. UI open and keyring unlock now use ClientController:getState and KeyringController:isUnlocked on demand as well.
  • AccountTreeController:stateChange subscription: Used to detect mid-session tree mutations (e.g. a snap account added to the current group), with #handleAccountTreeStateChange, #lastKnownAccountIds, and overlap/diff logic to avoid redundant re-subscriptions during init(). That path is removed; explicit group switches stay on selectedAccountGroupChange, and snap accounts added mid-session without a group change are picked up on the next tracking restart.

Solution

@metamask/account-tree-controller

  • Add AccountTreeController:isInitialized so consumers can query whether init() has completed (returns false again after clearState() until the next successful init()).

@metamask/assets-controller

  • Read lifecycle state on demand via messenger actions:
    • #isUiOpen()ClientController:getState
    • #isKeyringUnlocked()KeyringController:isUnlocked
    • #isAccountTreeInitialized()AccountTreeController:isInitialized
  • Remove local lifecycle mirrors and #handleAccountTreeStateChange / #lastKnownAccountIds.
  • Stop subscribing to AccountTreeController:stateChange.
  • Keep AccountTreeController:selectedAccountGroupChange for explicit group switches.
  • Continue using AccountTreeController:initialized / :uninitialized to trigger #updateActive() (but gate on authoritative state via the actions above).

Trade-off: Snap accounts added mid-session without a group change are no longer picked up live via :stateChange; they are picked up on the next tracking restart (unlock / UI open cycle).

Non-obvious details

  • #updateActive() still reacts to ClientController:stateChange, KeyringController:lock / :unlock, and account-tree init events — but each gate now re-reads authoritative controller state instead of trusting cached flags.
  • #handleAccountGroupChanged still ignores events while #activeSubscriptions.size === 0, so init-time selectedAccountGroupChange replays do not race with :initialized-owned startup.
  • Test mocks were updated to publish ClientController:stateChanged (not :stateChange) so ClientController:getState reflects UI-open during lifecycle tests.

References

  • Consumer adoption (messenger delegation):
    • Extension: app/scripts/messenger-client-init/messengers/assets/assets-controller-messenger.ts
    • Mobile: app/core/Engine/messengers/assets-controller/assets-controller-messenger.ts
  • Related prior work: AccountTreeController:initialized / :uninitialized (core#9892)

Changelog

@metamask/account-tree-controller

  • Added: AccountTreeController:isInitialized action

@metamask/assets-controller

  • Changed (BREAKING): AssetsControllerMessenger now requires AccountTreeController:isInitialized, ClientController:getState, and KeyringController:isUnlocked
  • Changed (BREAKING): No longer subscribes to AccountTreeController:stateChange — remove it from delegated events if listed explicitly
  • Changed: Revert to selectedAccountGroupChange for account-group switches; snap accounts added mid-session are picked up on restart

Manual testing

  1. Build extension/mobile against this core branch.
  2. Unlock wallet with UI open on the portfolio/home screen.
  3. Verify balances load for the selected account group.
  4. Switch account groups (Account 1 → Account 2) and confirm balances refresh.
  5. Lock wallet → confirm tracking stops; unlock → confirm balances reload.
  6. Close UI / background extension → reopen → confirm tracking resumes.
  7. (Optional) Add a snap account to the current group mid-session and confirm it appears after lock/unlock or UI close/open (not immediately on tree mutation).

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
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Breaking messenger wiring for extension/mobile plus a deliberate behavior change for mid-session account-tree updates; lifecycle gating touches when balances subscribe but not auth or funds logic directly.

Overview
Adds AccountTreeController:isInitialized so consumers can tell whether init() finished (and it flips false again after clearState() until the next init()).

AssetsController no longer caches UI open, keyring unlock, or account-tree readiness from events. #updateActive() now gates tracking by calling ClientController:getState, KeyringController:isUnlocked, and AccountTreeController:isInitialized when it runs. It still listens to AccountTreeController:initialized / :uninitialized and client/keyring events, but only to re-evaluate those gates.

Breaking for hosts: the assets messenger must delegate the three new actions and can drop AccountTreeController:stateChange from allowed events. Mid-session account-tree mutations (e.g. snap added to the current group without a group switch) no longer trigger live refresh via stateChange; selectedAccountGroupChange still refreshes on explicit group switches, with snap pickup deferred until the next tracking restart (lock/unlock or UI cycle).

Reviewed by Cursor Bugbot for commit fc7cfd3. Bugbot is set up for automated code reviews on this repo. Configure here.

@Kriys94
Kriys94 force-pushed the fix/AccountTreeAndAssetsController branch 3 times, most recently from fc856d2 to 28e76aa Compare September 1, 2026 21:11
@Kriys94
Kriys94 force-pushed the fix/AccountTreeAndAssetsController branch from 28e76aa to a38f058 Compare September 1, 2026 21:14
@Kriys94

Kriys94 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-preview

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@8.0.0-preview-a38f058
@metamask-previews/accounts-controller@39.1.1-preview-a38f058
@metamask-previews/address-book-controller@7.1.2-preview-a38f058
@metamask-previews/ai-controllers@1.0.0-preview-a38f058
@metamask-previews/analytics-controller@2.0.0-preview-a38f058
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-a38f058
@metamask-previews/announcement-controller@8.1.0-preview-a38f058
@metamask-previews/app-metadata-controller@2.0.1-preview-a38f058
@metamask-previews/approval-controller@9.0.2-preview-a38f058
@metamask-previews/assets-controller@14.0.3-preview-a38f058
@metamask-previews/assets-controllers@111.1.3-preview-a38f058
@metamask-previews/authenticated-user-storage@3.0.2-preview-a38f058
@metamask-previews/base-controller@9.1.0-preview-a38f058
@metamask-previews/base-data-service@1.0.0-preview-a38f058
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-a38f058
@metamask-previews/bridge-controller@80.1.1-preview-a38f058
@metamask-previews/bridge-status-controller@75.4.0-preview-a38f058
@metamask-previews/build-utils@3.0.4-preview-a38f058
@metamask-previews/chain-agnostic-permission@1.7.0-preview-a38f058
@metamask-previews/chomp-api-service@4.0.1-preview-a38f058
@metamask-previews/claims-controller@0.6.1-preview-a38f058
@metamask-previews/client-controller@1.0.1-preview-a38f058
@metamask-previews/client-utils@2.1.1-preview-a38f058
@metamask-previews/compliance-controller@2.1.0-preview-a38f058
@metamask-previews/composable-controller@12.0.1-preview-a38f058
@metamask-previews/config-registry-controller@3.1.0-preview-a38f058
@metamask-previews/connectivity-controller@0.3.0-preview-a38f058
@metamask-previews/controller-utils@12.3.0-preview-a38f058
@metamask-previews/core-backend@9.0.0-preview-a38f058
@metamask-previews/delegation-controller@3.0.2-preview-a38f058
@metamask-previews/earn-controller@12.2.6-preview-a38f058
@metamask-previews/eip-5792-middleware@3.0.5-preview-a38f058
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-a38f058
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-a38f058
@metamask-previews/eth-block-tracker@15.0.1-preview-a38f058
@metamask-previews/eth-json-rpc-middleware@24.0.2-preview-a38f058
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-a38f058
@metamask-previews/foundryup@1.0.1-preview-a38f058
@metamask-previews/gas-fee-controller@26.3.2-preview-a38f058
@metamask-previews/gator-permissions-controller@5.0.2-preview-a38f058
@metamask-previews/geolocation-controller@1.0.0-preview-a38f058
@metamask-previews/java-tron-up@1.0.0-preview-a38f058
@metamask-previews/json-rpc-engine@10.5.0-preview-a38f058
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-a38f058
@metamask-previews/keyring-controller@27.1.1-preview-a38f058
@metamask-previews/kyc-controller@0.0.0-preview-a38f058
@metamask-previews/local-node-utils@1.0.0-preview-a38f058
@metamask-previews/logging-controller@9.0.0-preview-a38f058
@metamask-previews/message-manager@14.1.2-preview-a38f058
@metamask-previews/messenger@2.0.0-preview-a38f058
@metamask-previews/messenger-cli@0.2.0-preview-a38f058
@metamask-previews/money-account-api-data-service@0.4.1-preview-a38f058
@metamask-previews/money-account-balance-service@2.4.3-preview-a38f058
@metamask-previews/money-account-controller@1.0.0-preview-a38f058
@metamask-previews/money-account-upgrade-controller@3.0.2-preview-a38f058
@metamask-previews/money-account-utils@1.1.0-preview-a38f058
@metamask-previews/multichain-account-service@13.0.2-preview-a38f058
@metamask-previews/multichain-api-middleware@4.0.3-preview-a38f058
@metamask-previews/multichain-network-controller@3.2.4-preview-a38f058
@metamask-previews/multichain-transactions-controller@7.1.2-preview-a38f058
@metamask-previews/name-controller@9.1.2-preview-a38f058
@metamask-previews/network-connection-banner-controller@0.2.1-preview-a38f058
@metamask-previews/network-controller@36.0.0-preview-a38f058
@metamask-previews/network-enablement-controller@6.0.5-preview-a38f058
@metamask-previews/notification-services-controller@26.0.1-preview-a38f058
@metamask-previews/passkey-controller@3.1.0-preview-a38f058
@metamask-previews/permission-controller@13.1.1-preview-a38f058
@metamask-previews/permission-log-controller@5.1.0-preview-a38f058
@metamask-previews/perps-controller@15.0.0-preview-a38f058
@metamask-previews/phishing-controller@17.4.0-preview-a38f058
@metamask-previews/platform-api-docs@0.1.0-preview-a38f058
@metamask-previews/polling-controller@16.0.9-preview-a38f058
@metamask-previews/preferences-controller@23.1.0-preview-a38f058
@metamask-previews/profile-metrics-controller@4.0.3-preview-a38f058
@metamask-previews/profile-sync-controller@29.0.0-preview-a38f058
@metamask-previews/ramps-controller@20.2.0-preview-a38f058
@metamask-previews/rate-limit-controller@7.0.1-preview-a38f058
@metamask-previews/react-data-query@1.0.0-preview-a38f058
@metamask-previews/remote-feature-flag-controller@6.1.0-preview-a38f058
@metamask-previews/sample-controllers@5.0.6-preview-a38f058
@metamask-previews/seedless-onboarding-controller@10.1.1-preview-a38f058
@metamask-previews/selected-network-controller@26.1.7-preview-a38f058
@metamask-previews/sentinel-api-service@1.0.1-preview-a38f058
@metamask-previews/shield-controller@6.0.1-preview-a38f058
@metamask-previews/signature-controller@39.2.10-preview-a38f058
@metamask-previews/smart-transactions-controller@26.0.0-preview-a38f058
@metamask-previews/snap-account-service@2.1.2-preview-a38f058
@metamask-previews/social-controllers@2.8.0-preview-a38f058
@metamask-previews/solana-test-validator-up@1.0.0-preview-a38f058
@metamask-previews/stellar-quickstart-up@0.0.0-preview-a38f058
@metamask-previews/storage-service@1.0.2-preview-a38f058
@metamask-previews/subscription-controller@8.0.1-preview-a38f058
@metamask-previews/transaction-controller@69.7.0-preview-a38f058
@metamask-previews/transaction-pay-controller@27.1.1-preview-a38f058
@metamask-previews/user-operation-controller@41.2.9-preview-a38f058
@metamask-previews/wallet@12.0.2-preview-a38f058
@metamask-previews/wallet-cli@0.0.0-preview-a38f058

@Kriys94

Kriys94 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Preview builds in extension: MetaMask/metamask-extension#45953

@Kriys94

Kriys94 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Preview builds in mobile: MetaMask/metamask-mobile#35560

@Kriys94
Kriys94 marked this pull request as ready for review September 1, 2026 22:57
@Kriys94
Kriys94 requested review from a team as code owners September 1, 2026 22:57
@Kriys94
Kriys94 deployed to default-branch September 1, 2026 22:57 — with GitHub Actions Active
* @returns True when the controller has been initialized.
*/
isInitialized(): boolean {
return this.#initialized;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

hhmmm we do this through the messenger now , this is better

*/
#isUiOpen(): boolean {
return clientControllerSelectors.selectIsUiOpen(
this.messenger.call('ClientController:getState'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ooh we replace everything with new actions now, could be good to export specific action instead of using getState here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah that's better but minor because uiOpen is the only state of the Controller right now

* `:stateChange` events during `init()` are ignored — startup is driven by
* `:initialized` instead, so we do not fetch on every tree mutation.
*/
#handleAccountTreeStateChange(): void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you quick me help me to understand why you're removing this function ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, selectedAccountGroupChange should be enough. We were listening stageChange to cover the case when a new non-EVM account was added with Snaps. Now we dont cover this case because there is almost no users that install third party non-EVM Snaps in runtime. And we win simplification of the codebase

Comment thread packages/account-tree-controller/src/AccountTreeController.test.ts
salimtb
salimtb previously approved these changes Sep 2, 2026
@Kriys94
Kriys94 added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 6790485 Sep 2, 2026
433 checks passed
@Kriys94
Kriys94 deleted the fix/AccountTreeAndAssetsController branch September 2, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants