Skip to content

Commit 9a12df4

Browse files
authored
chore: decouple controllers from tokens list (MetaMask#8700)
## Explanation This PR decouples the `TokenDetectionController` and `TokensController` from the `TokenListController` by introducing a new shared `TokenListService`. This architectural improvement provides: - **Reduced coupling**: Controllers no longer depend on `TokenListController` state/messaging - **Enhanced caching**: Token lists are cached in-memory per chain for 4 hours using TanStack Query - **Better performance**: Optimized token list fetching with proper caching and deduplication - **Simplified data flow**: Token enrichment moves from reactive events to one-time initialization ### Key Changes #### New `TokenListService` - Introduces a TanStack Query-backed service for fetching and caching token lists - Provides 4-hour in-memory caching per chain ID - Exports `TokenListService` class and `buildTokenListMap` utility function - Adds `@tanstack/query-core` as a new dependency #### Breaking Changes to Controllers - **`TokenDetectionController`**: Now requires `tokenListService` in constructor options - **`TokensController`**: Now requires `tokenListService` in constructor options - Both controllers are decoupled from `TokenListController` messaging system - `TokenDetectionController` no longer automatically restarts detection on `TokenListController:stateChange` #### Token Detection Improvements - Token list metadata fetched per detection pass with address normalization - Enhanced error handling with failure-safe early returns - Improved mUSD deduplication and websocket/polling guards #### Token Enrichment Changes - `TokensController` switches from reactive events to one-time async enrichment at initialization - Multi-chain enrichment using `Promise.allSettled` for resilience - Token `name` and `rwaData` enriched once during initialization vs. on every state change ### Testing Updates - Updated tests for new architecture - Added comprehensive `TokenListService` unit tests - Enhanced token detection test coverage **Note**: This introduces breaking changes requiring constructor updates for both controllers. Consumers will need to provide the new `tokenListService` dependency. <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> ## References Mobile: MetaMask/metamask-mobile#29743 <!-- 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/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** > Introduces a new shared caching layer and changes `TokenDetectionController`/`TokensController` construction and runtime behavior, which can impact token discovery/enrichment if consumers don’t wire `tokenListService` correctly or if cache/normalization assumptions differ across chains. > > **Overview** > Adds a new `TokenListService` (TanStack Query-backed) to fetch + cache per-chain token lists in-memory for 4 hours, exporting both `TokenListService` and `buildTokenListMap`, and adds `@tanstack/query-core` as a direct dependency. > > **BREAKING:** `TokenDetectionController` and `TokensController` now require a `tokenListService` constructor option and no longer depend on `TokenListController` actions/events; token detection now pulls fresh token-list snapshots per chain from the service (with lowercase-key normalization and graceful early-return on fetch failures) and no longer restarts on `TokenListController:stateChange`. > > Changes token metadata enrichment in `TokensController` from reactive updates on token-list state changes to a one-time async initialization pass (multi-chain, `Promise.allSettled`) that fills `name`/`rwaData`, and updates tests/changelog accordingly (including new `TokenListService` unit tests and expanded mUSD/detection guard coverage). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit c67b9ef. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 526896d commit 9a12df4

12 files changed

Lines changed: 1016 additions & 896 deletions

eslint-suppressions.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -423,12 +423,12 @@
423423
},
424424
"packages/assets-controllers/src/TokenDetectionController.test.ts": {
425425
"no-restricted-syntax": {
426-
"count": 2
426+
"count": 1
427427
}
428428
},
429429
"packages/assets-controllers/src/TokenDetectionController.ts": {
430430
"no-restricted-syntax": {
431-
"count": 6
431+
"count": 3
432432
}
433433
},
434434
"packages/assets-controllers/src/TokenListController.test.ts": {
@@ -475,7 +475,7 @@
475475
"count": 6
476476
},
477477
"no-restricted-syntax": {
478-
"count": 4
478+
"count": 3
479479
}
480480
},
481481
"packages/assets-controllers/src/TokensController.ts": {
@@ -486,7 +486,7 @@
486486
"count": 1
487487
},
488488
"@typescript-eslint/prefer-optional-chain": {
489-
"count": 4
489+
"count": 3
490490
},
491491
"id-length": {
492492
"count": 1
@@ -498,7 +498,7 @@
498498
"count": 1
499499
},
500500
"no-restricted-syntax": {
501-
"count": 2
501+
"count": 1
502502
},
503503
"require-atomic-updates": {
504504
"count": 1

packages/assets-controllers/CHANGELOG.md

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

1010
### Added
1111

12+
- Add `TokenListService`, a shared service for fetching and caching the token list per chain ([#8700](https://github.com/MetaMask/core/pull/8700))
13+
- Wraps `@tanstack/query-core` to cache results in-memory for 4 hours per chain ID, matching `TokenListController`'s existing threshold.
14+
- Multiple controllers sharing the same `TokenListService` instance share the same cache: only one HTTP request is made per chain per 4-hour window regardless of how many callers invoke `fetchTokensByChainId`.
15+
- Exported from the package as `TokenListService` and `buildTokenListMap`.
16+
- Add `@tanstack/query-core` `^5.62.16` as a direct dependency ([#8700](https://github.com/MetaMask/core/pull/8700))
1217
- Expose missing public `AccountTrackerController` methods through its messenger ([#8693](https://github.com/MetaMask/core/pull/8693))
1318
- The following actions are now available:
1419
- `AccountTrackerController:refresh`
@@ -22,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2227

2328
### Changed
2429

30+
- **BREAKING:** `TokenDetectionController` now requires a `tokenListService: TokenListService` constructor option ([#8700](https://github.com/MetaMask/core/pull/8700))
31+
- Token list data is fetched directly from `TokenListService` instead of reading `TokenListController` state on each detection pass.
32+
- `GetTokenListState` has been removed from `AllowedActions` and `TokenListStateChange` has been removed from `AllowedEvents` on `TokenDetectionControllerMessenger`.
33+
- **BREAKING:** `TokensController` now requires a `tokenListService: TokenListService` constructor option ([#8700](https://github.com/MetaMask/core/pull/8700))
34+
- `TokenListStateChange` has been removed from `AllowedEvents` on `TokensControllerMessenger`.
35+
- Token `name` and `rwaData` enrichment now happens once at controller initialization instead of reactively on every `TokenListController:stateChange` event.
2536
- Bump `@metamask/transaction-controller` from `^65.0.0` to `^65.1.0` ([#8691](https://github.com/MetaMask/core/pull/8691))
2637
- Switch the default mUSD asset from upfront `allTokens` state seeding to detection-based discovery on Ethereum mainnet (`0x1`), Linea (`0xe708`), and Monad mainnet (`0x8f`) ([#8688](https://github.com/MetaMask/core/pull/8688))
2738
- `TokenDetectionController` now merges mUSD into the in-memory token list cache for these chains so it is treated as a regular detection candidate, replacing the previous `start()`-time `TokensController:addTokens` call and the per-event re-seed runs.
@@ -34,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3445

3546
### Removed
3647

48+
- `TokenDetectionController` no longer restarts token detection when `TokenListController` publishes a `stateChange` event ([#8700](https://github.com/MetaMask/core/pull/8700))
49+
- Detection is still triggered on wallet unlock, account change, network change, and preference changes; the extra restart that occurred whenever `TokenListController` refreshed its cache is gone.
3750
- Stop seeding mUSD directly into `TokensController` state and remove the related event subscriptions ([#8688](https://github.com/MetaMask/core/pull/8688))
3851
- `TokensController` no longer subscribes to `KeyringController:unlock`, `AccountsController:accountAdded`, `AccountsController:selectedEvmAccountChange`, `NetworkController:networkAdded`, or `NetworkController:stateChange` for mUSD seeding purposes.
3952
- `TokensControllerMessenger` no longer requires `NetworkControllerNetworkAddedEvent`, `AccountsControllerAccountAddedEvent`, or `KeyringControllerUnlockEvent` as allowed events.

packages/assets-controllers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"@metamask/storage-service": "^1.0.1",
8888
"@metamask/transaction-controller": "^65.1.0",
8989
"@metamask/utils": "^11.9.0",
90+
"@tanstack/query-core": "^5.62.16",
9091
"@types/bn.js": "^5.1.5",
9192
"@types/uuid": "^8.3.0",
9293
"async-mutex": "^0.5.0",

packages/assets-controllers/src/TokenDetectionController-method-action-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export type TokenDetectionControllerStopAction = {
3838
};
3939

4040
/**
41-
* For each token in the token list provided by the TokenListController, checks the token's balance for the selected account address on the active network.
41+
* For each token in the token list provided by the TokenListService, checks the token's balance for the selected account address on the active network.
4242
* On mainnet, if token detection is disabled in preferences, ERC20 token auto detection will be triggered for each contract address in the legacy token list from the @metamask/contract-metadata repo.
4343
*
4444
* @param options - Options for token detection.

0 commit comments

Comments
 (0)