Skip to content

fix(assets-controller): Dedup assets price requests#9189

Open
Kriys94 wants to merge 1 commit into
mainfrom
fix/coalescePrice
Open

fix(assets-controller): Dedup assets price requests#9189
Kriys94 wants to merge 1 commit into
mainfrom
fix/coalescePrice

Conversation

@Kriys94

@Kriys94 Kriys94 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Explanation

Problem

PriceDataSource fetches spot prices from multiple, independently-triggered
paths: 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 DedupingBatchFetcher utility and routes
all PriceDataSource price fetches through it. The fetcher deduplicates by key
across two dimensions:

  • Freshness TTL — a key fetched within freshnessTtlMs is 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).
  • In-flight coalescing — concurrent callers requesting a key that already has
    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 fetchBatch
invocation and then split back into per-key results so each key resolves
independently.

Wiring in PriceDataSource:

  • New priceFreshnessTtlMs option on PriceDataSourceConfig (defaults to the
    poll interval) controls how long a fetched price is considered fresh.
  • New PriceDataSource.invalidatePriceCache() clears the freshness cache;
    AssetsController calls it when the selected currency changes, since cached
    prices 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's
fetchedAt is stamped when the fetch completes — slightly after the tick that
triggered it. If the TTL equals the poll interval, then at the next tick
now - fetchedAt is pollInterval - latency, which is still within the TTL, so
the 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 (via
FRESHNESS_TTL_POLL_RATIO) rather than the previous Math.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

  • AssetsController no longer triggers a redundant one-time balance fetch when
    the enabled-network list changes; subscription refresh and the
    enabled-networks handler already cover those fetches.

References

  • Related to the assets-controller price-fetch deduplication effort.

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
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 DedupingBatchFetcher and routes all PriceDataSource spot-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(); AssetsController calls it on selected-currency change before the price-only getAssets refresh.

handleActiveChainsUpdate drops the extra one-time getAssets when 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.

@Kriys94 Kriys94 force-pushed the fix/coalescePrice branch 4 times, most recently from 99438f4 to 18e83d0 Compare June 22, 2026 13:22
@Kriys94 Kriys94 changed the title fix(assets-controller): Coalesce assets price fix(assets-controller): Dedup assets price requests Jun 22, 2026
@Kriys94 Kriys94 force-pushed the fix/coalescePrice branch from 18e83d0 to 07b547a Compare June 22, 2026 13:30
@Kriys94 Kriys94 force-pushed the fix/coalescePrice branch from 07b547a to 8633f7f Compare June 22, 2026 13:42
@Kriys94 Kriys94 marked this pull request as ready for review June 22, 2026 13:45
@Kriys94 Kriys94 requested review from a team as code owners June 22, 2026 13:45
@Kriys94 Kriys94 temporarily deployed to default-branch June 22, 2026 13:45 — with GitHub Actions Inactive

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8633f7f. Configure here.

this.#subscribeAssets();
}

// If chains were added and we have selected accounts, do one-time fetch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8633f7f. Configure here.

@salimtb

salimtb commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

@metamaskbot publish-preview

@github-actions

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@7.5.2-preview-8633f7f2a
@metamask-previews/accounts-controller@39.0.2-preview-8633f7f2a
@metamask-previews/address-book-controller@7.1.2-preview-8633f7f2a
@metamask-previews/ai-controllers@0.7.0-preview-8633f7f2a
@metamask-previews/analytics-controller@1.1.1-preview-8633f7f2a
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-8633f7f2a
@metamask-previews/announcement-controller@8.1.0-preview-8633f7f2a
@metamask-previews/app-metadata-controller@2.0.1-preview-8633f7f2a
@metamask-previews/approval-controller@9.0.2-preview-8633f7f2a
@metamask-previews/assets-controller@9.0.2-preview-8633f7f2a
@metamask-previews/assets-controllers@109.2.1-preview-8633f7f2a
@metamask-previews/authenticated-user-storage@2.0.0-preview-8633f7f2a
@metamask-previews/base-controller@9.1.0-preview-8633f7f2a
@metamask-previews/base-data-service@0.1.3-preview-8633f7f2a
@metamask-previews/bitcoin-regtest-up@0.0.0-preview-8633f7f2a
@metamask-previews/bridge-controller@75.2.1-preview-8633f7f2a
@metamask-previews/bridge-status-controller@72.1.1-preview-8633f7f2a
@metamask-previews/build-utils@3.0.4-preview-8633f7f2a
@metamask-previews/chain-agnostic-permission@1.6.2-preview-8633f7f2a
@metamask-previews/chomp-api-service@3.1.0-preview-8633f7f2a
@metamask-previews/claims-controller@0.5.3-preview-8633f7f2a
@metamask-previews/client-controller@1.0.1-preview-8633f7f2a
@metamask-previews/compliance-controller@2.1.0-preview-8633f7f2a
@metamask-previews/composable-controller@12.0.1-preview-8633f7f2a
@metamask-previews/config-registry-controller@0.4.1-preview-8633f7f2a
@metamask-previews/connectivity-controller@0.2.0-preview-8633f7f2a
@metamask-previews/controller-utils@12.3.0-preview-8633f7f2a
@metamask-previews/core-backend@6.3.3-preview-8633f7f2a
@metamask-previews/delegation-controller@3.0.2-preview-8633f7f2a
@metamask-previews/earn-controller@12.2.1-preview-8633f7f2a
@metamask-previews/eip-5792-middleware@3.0.4-preview-8633f7f2a
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-8633f7f2a
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-8633f7f2a
@metamask-previews/ens-controller@19.1.4-preview-8633f7f2a
@metamask-previews/eth-block-tracker@15.0.1-preview-8633f7f2a
@metamask-previews/eth-json-rpc-middleware@23.1.3-preview-8633f7f2a
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-8633f7f2a
@metamask-previews/foundryup@1.0.1-preview-8633f7f2a
@metamask-previews/gas-fee-controller@26.2.3-preview-8633f7f2a
@metamask-previews/gator-permissions-controller@4.2.1-preview-8633f7f2a
@metamask-previews/geolocation-controller@0.1.3-preview-8633f7f2a
@metamask-previews/java-tron-up@0.0.0-preview-8633f7f2a
@metamask-previews/json-rpc-engine@10.5.0-preview-8633f7f2a
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-8633f7f2a
@metamask-previews/keyring-controller@27.1.0-preview-8633f7f2a
@metamask-previews/logging-controller@8.0.2-preview-8633f7f2a
@metamask-previews/message-manager@14.1.2-preview-8633f7f2a
@metamask-previews/messenger@1.2.0-preview-8633f7f2a
@metamask-previews/messenger-cli@0.2.0-preview-8633f7f2a
@metamask-previews/money-account-balance-service@2.1.1-preview-8633f7f2a
@metamask-previews/money-account-controller@0.3.3-preview-8633f7f2a
@metamask-previews/money-account-upgrade-controller@2.1.0-preview-8633f7f2a
@metamask-previews/multichain-account-service@10.0.3-preview-8633f7f2a
@metamask-previews/multichain-api-middleware@3.1.5-preview-8633f7f2a
@metamask-previews/multichain-network-controller@3.1.4-preview-8633f7f2a
@metamask-previews/multichain-transactions-controller@7.1.1-preview-8633f7f2a
@metamask-previews/name-controller@9.1.2-preview-8633f7f2a
@metamask-previews/network-controller@33.0.0-preview-8633f7f2a
@metamask-previews/network-enablement-controller@5.4.0-preview-8633f7f2a
@metamask-previews/notification-services-controller@24.1.3-preview-8633f7f2a
@metamask-previews/passkey-controller@2.0.1-preview-8633f7f2a
@metamask-previews/permission-controller@13.1.1-preview-8633f7f2a
@metamask-previews/permission-log-controller@5.1.0-preview-8633f7f2a
@metamask-previews/perps-controller@8.2.0-preview-8633f7f2a
@metamask-previews/phishing-controller@17.2.0-preview-8633f7f2a
@metamask-previews/polling-controller@16.0.7-preview-8633f7f2a
@metamask-previews/preferences-controller@23.1.0-preview-8633f7f2a
@metamask-previews/profile-metrics-controller@4.0.0-preview-8633f7f2a
@metamask-previews/profile-sync-controller@28.2.0-preview-8633f7f2a
@metamask-previews/ramps-controller@14.3.0-preview-8633f7f2a
@metamask-previews/rate-limit-controller@7.0.1-preview-8633f7f2a
@metamask-previews/react-data-query@0.2.1-preview-8633f7f2a
@metamask-previews/remote-feature-flag-controller@4.2.2-preview-8633f7f2a
@metamask-previews/sample-controllers@5.0.2-preview-8633f7f2a
@metamask-previews/seedless-onboarding-controller@10.0.2-preview-8633f7f2a
@metamask-previews/selected-network-controller@26.1.4-preview-8633f7f2a
@metamask-previews/shield-controller@5.1.2-preview-8633f7f2a
@metamask-previews/signature-controller@39.2.6-preview-8633f7f2a
@metamask-previews/smart-transactions-controller@24.2.3-preview-8633f7f2a
@metamask-previews/snap-account-service@0.3.1-preview-8633f7f2a
@metamask-previews/social-controllers@2.3.1-preview-8633f7f2a
@metamask-previews/solana-test-validator-up@0.0.0-preview-8633f7f2a
@metamask-previews/storage-service@1.0.2-preview-8633f7f2a
@metamask-previews/subscription-controller@6.2.0-preview-8633f7f2a
@metamask-previews/transaction-controller@68.1.1-preview-8633f7f2a
@metamask-previews/transaction-pay-controller@23.13.1-preview-8633f7f2a
@metamask-previews/user-operation-controller@41.2.5-preview-8633f7f2a
@metamask-previews/wallet@4.0.0-preview-8633f7f2a
@metamask-previews/wallet-cli@0.0.0-preview-8633f7f2a

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.

2 participants