fix(assets-controller): Dedup assets price requests#9189
Conversation
99438f4 to
18e83d0
Compare
18e83d0 to
07b547a
Compare
07b547a to
8633f7f
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8633f7f. Configure here.
| */ | ||
| invalidatePriceCache(): void { | ||
| this.#deduper.invalidate(); | ||
| } |
There was a problem hiding this comment.
Stale currency via inflight join
High Severity
invalidatePriceCache() clears freshness timestamps but leaves in-flight batch fetches running. After a currency change, a new price request can coalesce onto a fetch that already called the API with the previous getSelectedCurrency() value, so merged prices stay in the old denomination and keys can be marked fresh until the TTL expires.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8633f7f. Configure here.
| this.#subscribeAssets(); | ||
| } | ||
|
|
||
| // If chains were added and we have selected accounts, do one-time fetch |
There was a problem hiding this comment.
Missing balance fetch on chain add
Medium Severity
Removing the one-time getAssets when a data source adds active chains leaves no immediate balance fetch for sources whose subscribe isUpdate path only updates chain lists (e.g. Accounts API and Backend WebSocket). Balances for newly supported, already-enabled chains may not load until the next poll or websocket event.
Reviewed by Cursor Bugbot for commit 8633f7f. Configure here.
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |


Explanation
Problem
PriceDataSourcefetches spot prices from multiple, independently-triggeredpaths: enrichment middleware (on balance/metadata updates), the per-subscription
polling loop, currency changes, and manual refreshes. These triggers frequently
overlap, so the same asset's price was requested from the Price API multiple
times within a few seconds, producing redundant network traffic and load.
Solution
This PR introduces a small, reusable
DedupingBatchFetcherutility and routesall
PriceDataSourceprice fetches through it. The fetcher deduplicates by keyacross two dimensions:
freshnessTtlMsis skipped entirely(the absence of a value is also cached, so we don't re-ask for assets the API
had no price for until the TTL expires).
a fetch in progress join the existing promise instead of issuing a new request.
Stale, not-in-flight keys from a single call are batched into one
fetchBatchinvocation and then split back into per-key results so each key resolves
independently.
Wiring in
PriceDataSource:priceFreshnessTtlMsoption onPriceDataSourceConfig(defaults to thepoll interval) controls how long a fetched price is considered fresh.
PriceDataSource.invalidatePriceCache()clears the freshness cache;AssetsControllercalls it when the selected currency changes, since cachedprices are denominated in the previous currency and must be refreshed.
Non-obvious detail: freshness TTL must stay below the poll interval
The subscription poll fires on a fixed
setInterval(pollInterval), but a key'sfetchedAtis stamped when the fetch completes — slightly after the tick thattriggered it. If the TTL equals the poll interval, then at the next tick
now - fetchedAtispollInterval - latency, which is still within the TTL, sothe asset reads as "fresh" and the poll is skipped. The net effect is the
subscription only issues a real request every other poll (e.g. an observed
~2-minute cadence for a 1-minute poll interval).
To fix this, the TTL is capped at
0.9 * pollInterval(viaFRESHNESS_TTL_POLL_RATIO) rather than the previousMath.min(ttl, pollInterval),giving enough headroom to absorb network latency and timer jitter. A regression
test reproduces the every-other-poll skip by simulating fetch latency with
jest.setSystemTime(it fails with the old cap and passes with the new one).Also in this PR
AssetsControllerno longer triggers a redundant one-time balance fetch whenthe enabled-network list changes; subscription refresh and the
enabled-networks handler already cover those fetches.
References
Checklist
Note
Medium Risk
Changes core price-fetch timing and caching; incorrect TTL or invalidation could delay price updates or leave stale currency-denominated prices, though behavior is heavily covered by new tests.
Overview
Introduces
DedupingBatchFetcherand routes allPriceDataSourcespot-price fetches through it so middleware, subscription polls, currency changes, and manual refreshes no longer hammer the Price API for the same asset.Freshness TTL (
priceFreshnessTtlMs, default poll interval) skips re-fetching recently completed prices; in-flight coalescing joins parallel callers on one batch. On subscribe, TTL is capped at 0.9 × poll interval so completion latency does not make every other poll a no-op.Adds
invalidatePriceCache();AssetsControllercalls it on selected-currency change before the price-onlygetAssetsrefresh.handleActiveChainsUpdatedrops the extra one-timegetAssetswhen chains are added; it only re-subscribes (tests updated accordingly).Reviewed by Cursor Bugbot for commit 8633f7f. Bugbot is set up for automated code reviews on this repo. Configure here.