Skip to content

Commit c66ba32

Browse files
authored
fix: improve assets controller snap accounts subscription and websocket (#8430)
## Explanation ### Current state `AssetsController` had three related issues that prevented balances from loading correctly in certain scenarios: 1. **Snap accounts not subscribed after startup**: On app launch, `#start()` was called before `AccountTreeController.init()` had finished building the account tree, so snap accounts weren't yet in the selected account group. Because `#start()` is idempotent (it returns early when subscriptions already exist), snap accounts were never picked up when they became available, leaving Solana, Tron, and Bitcoin balances missing. 2. **Custom assets filtered by spam heuristics**: `TokenDataSource` applied the `MIN_TOKEN_OCCURRENCES` threshold (EVM ERC-20) and Blockaid bulk scan (non-EVM fungible tokens) to all tokens uniformly. This meant tokens manually imported by the user and stored in `customAssets` state were incorrectly filtered out as potential spam. 3. **No polling fallback when WebSocket is disabled**: `BackendWebsocketDataSource` and `AccountsApiDataSource` both call the same `fetchV2SupportedNetworks` API to determine which chains they support. Because `BackendWebsocketDataSource` has higher priority in the chain-claiming loop, it would claim all supported chains at initialization — even when the WebSocket never connected. This left `AccountsApiDataSource` with no chains to poll, so users with the WebSocket disabled or with a broken connection received no balance updates. ### Solution 1. **Re-subscribe on AccountTree state change**: Added `#handleAccountTreeStateChange()` which subscribes to `AccountTreeController:stateChange`. When the account tree updates (e.g. after snap accounts are hydrated), the method forces a full re-subscription and re-fetch with the complete account list. Added `AccountsController:getSelectedAccount` as a fallback in `#getSelectedAccounts()` for cases where the account tree is not yet initialized. 2. **Custom assets bypass spam filters**: `TokenDataSource` now reads `customAssets` from state and builds a `customAssetIds` set. Tokens in that set skip the `occurrences >= 3` EVM filter and are excluded from the non-EVM Blockaid scan input, ensuring user-imported assets always appear. 3. **Connection-aware chain claiming in BackendWebsocketDataSource**: Added an `#isConnected` flag (initially `false`). `#initializeActiveChains` and `#refreshActiveChains` now store supported chains in `#supportedChains` but only call `updateActiveChains` when connected. On disconnect, `activeChains` is cleared so the chain-claiming loop re-assigns those chains to `AccountsApiDataSource` for polling. On reconnect, chains are restored from `#supportedChains` before pending WebSocket subscriptions are reprocessed. ## References * Related to MetaMask/metamask-extension (assets multichain rollout) ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md) - [ ] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes asset subscription/chain-claiming behavior and adds onboarding-based RPC suppression, which can affect when and how balances update across multiple chains and account types. > > **Overview** > Fixes multi-chain balance tracking edge cases by making `AssetsController` re-subscribe/re-fetch when the account tree later gains accounts (e.g. snap accounts), using `AccountsController:getSelectedAccount` as a startup fallback and tracking `#lastKnownAccountIds` to avoid redundant work. > > Improves data-source coordination: `BackendWebsocketDataSource` now only claims chains while connected, releases them on disconnect so `AccountsApiDataSource` can poll as fallback, and reclaims on reconnect (dropping stale pending subscriptions). `TokenDataSource` now exempts user-imported `customAssets` from occurrence/Blockaid spam filtering. > > Adds an optional `isOnboarded` hook plumbed into `RpcDataSource` so `fetch`/`subscribe` become no-ops before onboarding completes, preventing premature on-chain RPC calls; tests, changelog, and dependency graph are updated accordingly. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d5ac722. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent e8ff5b7 commit c66ba32

File tree

11 files changed

+351
-65
lines changed

11 files changed

+351
-65
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ linkStyle default opacity:0.5
213213
approval_controller --> base_controller;
214214
approval_controller --> messenger;
215215
assets_controller --> account_tree_controller;
216+
assets_controller --> accounts_controller;
216217
assets_controller --> assets_controllers;
217218
assets_controller --> base_controller;
218219
assets_controller --> client_controller;

packages/assets-controller/CHANGELOG.md

Lines changed: 15 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+
### Added
11+
12+
- Added `isOnboarded` option to `AssetsControllerOptions` and `RpcDataSourceConfig` ([#8430](https://github.com/MetaMask/core/pull/8430))
13+
- When `isOnboarded` returns `false`, `RpcDataSource` skips `fetch` and `subscribe` calls, preventing on-chain RPC calls before onboarding is complete.
14+
- Defaults to `() => true` so existing consumers are unaffected.
15+
1016
### Changed
1117

1218
- Bump `@metamask/account-tree-controller` from `^7.0.0` to `^7.1.0` ([#8472](https://github.com/MetaMask/core/pull/8472))
@@ -21,6 +27,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2127

2228
### Fixed
2329

30+
- `AssetsController` now re-subscribes to all data sources when `AccountTreeController` state changes after initial startup, ensuring snap accounts and their chains are included ([#8430](https://github.com/MetaMask/core/pull/8430))
31+
- Previously, `#start()` would create subscriptions before snap accounts were available, and its idempotency guard prevented re-subscription when the full account list arrived.
32+
- Added `#handleAccountTreeStateChange()` which forces a full re-subscription and re-fetch when the account tree updates, picking up all accounts including snaps.
33+
- Added `AccountsController:getSelectedAccount` as a fallback in `#getSelectedAccounts()` for when the account tree is not yet initialized.
34+
- `TokenDataSource` no longer filters out user-imported custom assets with the `MIN_TOKEN_OCCURRENCES` spam filter or Blockaid bulk scan ([#8430](https://github.com/MetaMask/core/pull/8430))
35+
- Tokens present in `customAssets` state now bypass the EVM occurrence threshold and non-EVM Blockaid scan, ensuring manually imported assets always appear.
36+
- `BackendWebsocketDataSource` now properly releases chains to `AccountsApiDataSource` when the websocket is disconnected or disabled ([#8430](https://github.com/MetaMask/core/pull/8430))
37+
- Previously, `BackendWebsocketDataSource` eagerly claimed all supported chains on initialization regardless of connection state, preventing `AccountsApiDataSource` from polling.
38+
- Chains are now only claimed when the websocket is connected. On disconnect, chains are released so the chain-claiming loop assigns them to `AccountsApiDataSource` for polling fallback. On reconnect, chains are reclaimed.
2439
- `AssetsController` no longer silently skips asset fetching on startup for returning users ([#8412](https://github.com/MetaMask/core/pull/8412))
2540
- Previously, `#start()` was called at keyring unlock before `AccountTreeController.init()` had built the account tree, causing `#selectedAccounts` to return an empty array and all subscriptions and fetches to be skipped. `selectedAccountGroupChange` does not fire when the persisted selected group is unchanged, leaving the controller idle.
2641
- Now subscribes to `AccountTreeController:stateChange` (the base-controller event guaranteed to fire when `init()` calls `this.update()`), so the controller re-evaluates its active state once accounts are available.

packages/assets-controller/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@ethersproject/abi": "^5.7.0",
5858
"@ethersproject/providers": "^5.7.0",
5959
"@metamask/account-tree-controller": "^7.1.0",
60+
"@metamask/accounts-controller": "^37.2.0",
6061
"@metamask/assets-controllers": "^104.0.0",
6162
"@metamask/base-controller": "^9.1.0",
6263
"@metamask/client-controller": "^1.0.1",

packages/assets-controller/src/AssetsController.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,15 @@ async function withController<ReturnValue>(
142142
namespace: MOCK_ANY_NAMESPACE,
143143
});
144144

145+
// Mock AccountsController
146+
(
147+
messenger as {
148+
registerActionHandler: (a: string, h: () => unknown) => void;
149+
}
150+
).registerActionHandler('AccountsController:getSelectedAccount', () =>
151+
createMockInternalAccount(),
152+
);
153+
145154
// Mock AccountTreeController
146155
messenger.registerActionHandler(
147156
'AccountTreeController:getAccountsFromSelectedAccountGroup',

0 commit comments

Comments
 (0)