feat(prices): migrate token prices to network-scoped v2 endpoint#917
feat(prices): migrate token prices to network-scoped v2 endpoint#917aristidesstaffieri wants to merge 14 commits into
Conversation
Port stellar/freighter#2870 to mobile. fetchTokenPrices now hits the v2 /token-prices endpoint with a `?network=` query param, gated behind a `use_token_prices_v2` remote-config flag (defaults to v2, rollback to v1 from Amplitude without a release). - remoteConfig: add use_token_prices_v2 boolean flag, default true in both the dev and prod initial-state branches - backend: fetchTokenPrices takes `network` + `useV2`; v2 POSTs to freighterBackendV2 with the network query param, v1 path unchanged. Unsupported networks (Futurenet) and empty token lists short-circuit to a null-filled map with no request, avoiding guaranteed-failing calls and Sentry noise - prices store: both fetch methods read the flag and thread network/useV2 through; fetchPricesForTokenIds gains a required `network` param - thread the active network from useAuthenticationStore through useSwapTokenPrices and useTransactionBalanceListItems - tests: cover v2/v1 selection, testnet mapping, and the Futurenet short-circuit; update prices and SwapAmountScreen assertions
|
iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-d62789afbd8a81409f7d (SDF collaborators only — install instructions in the release description) |
There was a problem hiding this comment.
Pull request overview
Migrates token price fetching in the mobile app to the freighter-backend v2 network-scoped /token-prices endpoint, gated behind a new use_token_prices_v2 remote-config flag (defaulting to v2) so the app can roll back to v1 without a release.
Changes:
- Add
use_token_prices_v2boolean remote-config flag and default it totruefor both dev/prod initial config. - Update
fetchTokenPricesto accept{ network, useV2 }, callfreighterBackendV2with?network=PUBLIC|TESTNETwhen enabled, and short-circuit on unsupported networks / filtered-out token sets. - Thread
networkthrough the prices duck and key callers; update/extend Jest coverage for the new behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/services/backend.ts | Implements v2/v1 selection, adds network query param for v2, and short-circuits unsupported networks/empty filtered token requests. |
| src/ducks/remoteConfig.ts | Adds use_token_prices_v2 boolean flag and defaults it to true. |
| src/ducks/prices.ts | Reads remote-config flag, forwards network/useV2 to backend fetch, and requires network for token-id-based fetches. |
| src/components/screens/SwapScreen/hooks/useSwapTokenPrices.ts | Threads active network into price fetches and refreshes. |
| src/hooks/blockaid/useTransactionBalanceListItems.tsx | Threads active network into the fire-and-forget missing-price fetch. |
| docs/superpowers/specs/2026-06-25-token-prices-v2-design.md | Adds design documentation for the migration and rollout strategy. |
| tests/services/backend.test.ts | Adds initial unit coverage for v2/v1 routing and Futurenet short-circuit behavior. |
| tests/ducks/prices.test.ts | Updates prices-duck tests to account for required network and forwarded useV2 flag. |
| tests/components/screens/SwapScreen/SwapAmountScreen.test.tsx | Updates assertions to include required network in price fetch calls. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f746d19c1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
useTransactionBalanceListItems read the active network via useAuthenticationStore.getState() inside its useMemo, with network absent from the dependency array. A network switch while the screen was mounted wouldn't recompute the memo, so missing-price fetches could run against the previous network — and v2 prices are network-scoped. Subscribe to network via a selector outside the memo and add it to the dependency array so fetches re-run on network changes, matching the pattern in useSwapTokenPrices.
fetchPricesForTokenIds deduped purely by token id, and the prices store had no notion of which network the cached prices belonged to. After a network switch the requested ids were already present, so the fetch returned early and the network-scoped v2 endpoint was never re-queried — the UI could show the previous network's prices indefinitely (same for the merged balances map and Futurenet's null-filled entries). Track the network the cached prices were fetched for (pricesNetwork) and drop the cache + reset the dedupe baseline when a fetch arrives for a different network, so every token is refetched for the new network. Same-network merge/dedupe behavior is unchanged. Add tests for refetch-on-network-change in both fetch paths.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f934c0d1d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f01cb345b7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
useTransactionBalanceListItems pre-filtered token ids against the flat prices map and only called fetchPricesForTokenIds for the missing ones. When the network changed but every simulated token already had an entry from the previous network, the missing set was empty, the call was skipped, and the store's network-change invalidation never ran — so a TESTNET dApp transaction could keep showing a cached PUBLIC price until some other refresh cleared the store. Pass all token ids and let the store decide what to fetch: it already dedupes already-loaded tokens and clears/refetches on network change. Removes the now-redundant prefilter (and the unused prices read).
Both fetch methods cleared stale prices on entry but merged their response unconditionally once the awaited, network-scoped call resolved. If the user switched networks mid-fetch, a slow PUBLIC response could write PUBLIC prices into the now-TESTNET cache (pricesNetwork already moved on), leaving the new network with stale prices. Capture the requested network and, after the await, discard the response when get().pricesNetwork no longer matches — before merging. Same-network overlapping fetches still merge normally. Add a test per path that flips the network mid-flight and asserts the stale response is not merged.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b4beeece66
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
fetchPricesForTokenIds read use_token_prices_v2 only after the dedupe early-return, so when Amplitude rolled the flag back while a non-held token already had a cached entry, toFetch was empty and the v1 endpoint was never called for it until a force refresh, network change, or app restart — the documented rollback didn't apply to cached lookups. Widen the cache identity from pricesNetwork to (pricesNetwork, pricesUseV2). Both fetch methods now read the flag before the dedupe check, drop the cache and reset the dedupe baseline when the network or endpoint version changes, and discard an in-flight response if either changed mid-fetch.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 566ca56473
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The prices store read use_token_prices_v2 internally, so callers could
not react to it. While the swap screen stayed mounted with an unchanged
token list and network, its effect never re-ran when Amplitude flipped
the flag — cached trending/non-held prices kept using the old endpoint
until a manual refresh or navigation.
Thread useV2 from the callers instead (the reference PR's pattern):
- prices store: both fetch methods take useV2 as a param; drop the
remoteConfig import and internal reads. The (network, useV2) cache
identity uses the passed value, so the store is a pure function of
its inputs.
- balances duck: read the flag via getState and pass it (non-hook
context; balance polling already covers flips).
- useSwapTokenPrices / useTransactionBalanceListItems: subscribe to the
flag via useRemoteConfigStore and add it to the effect/memo deps so a
rollback re-runs the fetch.
Drop the dead remoteConfig mock in prices tests, thread useV2 through
the calls, and assert it in the swap screen test.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0561a6d4d6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
useTransactionBalanceListItems fired fetchPricesForTokenIds inside its useMemo. Now that the store action synchronously clears/sets the price store on a source change, calling it during render mutates an external store mid-render. The hook also read prices via getState, so the async price response never recomputed the list. Move the fire-and-forget fetch into a useEffect keyed on the derived token ids, network, and flag (token ids are memoized so the effect doesn't fire every render), and subscribe to prices via the store selector so the list recomputes when the response lands. Network/flag drop out of the render memo's deps since only the effect uses them. Behavior-preserving; no store writes happen during render.
…r, observability
The flat global prices map guarded by a (network, useV2) value-identity check
was the root of a string of regressions: cross-network contamination, a
blank-price flicker on every source change, dropped/clobbered prices under
concurrent fetches, and silent failures. Replace the model rather than add
another guard.
Store (src/ducks/prices.ts, rewritten):
- Cache keyed per network: pricesByNetwork[network] (+ sourceByNetwork tracking
the endpoint that populated each). A network switch reads the other network's
submap — no clear, no flicker, and a cross-network read is impossible by
construction, so the in-flight cross-network discard guard is gone.
- A v1<->v2 rollback drops only that network's cache once and refetches; a
narrow post-await endpoint check discards a response whose endpoint was
superseded mid-flight.
- Export usePricesForNetwork(network) returning a stable empty map.
- (Chose this over a monotonic epoch: epoch "latest-wins" would drop tokens when
two fetches request disjoint sets; per-network keying removes the need.)
Service (src/services/backend.ts):
- Defensively coerce data?.data ?? {} so an unexpected v2 response shape can't
throw and wipe all prices; warn on shape mismatch.
- Wrap the request in logApiError + rethrow so a failing endpoint surfaces in
Sentry (warn for connectivity, error for 4xx/5xx/timeout) instead of being
swallowed by the store callers.
Consumers: read usePricesForNetwork(network) in the swap hooks, the dApp-sign
balance list, and the two swap review consumers; balances/auth use the new shape.
Tests: rewrite the prices suite around the per-network model (contamination,
rollback, in-flight discard, cross-network dedupe) and add backend error-logging
coverage; update the auth/balances/screen mocks the refactor touched.
freighter-backend-v2 keys the native asset as "native" (as the ported
browser extension sends), but mobile uses NATIVE_TOKEN_CODE ("XLM")
everywhere and was POSTing "XLM" to v2. If v2 doesn't accept "XLM" for
native, the XLM price — the most important in the wallet — silently
comes back null and renders "--", with no test catching it.
Translate only at the v2 wire boundary inside fetchTokenPrices: map
"XLM" -> "native" in the request body and "native" -> "XLM" in the
response, so the rest of the app keeps using "XLM". The v1 fallback is
left untouched (it's known to accept "XLM"). Also drop the stale
"only returns prices for Mainnet" comment (v2 is network-scoped).
Tests: v2 sends "native" + network param, v2 response is remapped to
"XLM", v1 still sends "XLM".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1516a0b36
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The per-network refactor unified the per-token dedupe into fetchPricesForBalances too, which regressed the balance path: the original fetched all held tokens on every call, so the 30s balance poll and Home pull-to-refresh kept currentPrice/fiatTotal current. With the dedupe, once a held token's price was cached — including a null-filled entry for unpriceable tokens — it was never refetched, freezing prices for the rest of the session (pull-to-refresh included) until a network switch, v1/v2 flag flip, or restart. Drop the dedupe from fetchPricesForBalances; refetch all held tokens each call (the poll is the refresh mechanism). reconcileSource, the in-flight endpoint guard, and the per-network merge are unchanged. fetchPricesForTokenIds keeps its dedupe + forceRefresh (eager swap path).
Two issues from gating fiat off on testnet:
- The total balance summed missing prices to "$0.00", implying the user
holds nothing. useTotalBalance now exposes hasFiatTotal (true only when
a held asset is actually priced); HomeScreen renders the total Display
only when it's true, so testnet (and the brief pre-price-load window on
mainnet) shows no total element instead of $0.00.
- Send/Swap were gated by hasZeroBalance = fiat total <= 0, so with no
testnet fiat they disabled for funded accounts. Gate on actual holdings
instead (any token balance > 0), which is fiat-independent; rawBalance
is no longer needed in HomeScreen.
Summary
Migrates token-price fetching from freighter-backend v1 to the network-scoped v2
/token-pricesendpoint (ported from stellar/freighter#2870), behind ause_token_prices_v2Amplitude flag that defaults to v2 and can roll back to v1 without a release. Prices are now cached per active network, and fiat is shown on mainnet only — testnet/futurenet are gated off (matching the extension), so they show no fiat instead of misleading$0.00.No UI redesign. The visible changes: testnet no longer shows fiat prices or a balance total, and Send/Swap enablement no longer depends on fiat value.
What
ducks/remoteConfig.ts— adduse_token_prices_v2flag (defaults to v2; Amplitude can roll back to v1).services/backend.ts—fetchTokenPricesselects v1/v2 by flag. v2 is network-scoped (?network=PUBLIC) and translates the native assetXLM↔"native"at the wire boundary. Fiat is mainnet-only — testnet/futurenet short-circuit to null prices on both v1 and v2. Defensive response-shape guard + Sentry logging on failure.ducks/prices.ts— cache prices per network (pricesByNetwork+sourceByNetwork): a network switch can't show another network's prices and needs no cache-clear flicker; a v1/v2 rollback drops+refetches that network's cache; in-flight responses for a superseded endpoint are discarded. Balance path refetches all held tokens each poll. NewusePricesForNetworkselector.useSwapTokenPrices,useTransactionBalanceListItems(also moved its fetch out of render into an effect),useReviewTokens,SwapTransactionDetailsBottomSheet.ducks/balances.ts,ducks/auth.ts— updated to the per-network store shape.HomeScreen/useTotalBalance— hide the total when no held asset is priced (no$0.00); gate Send/Swap on actual holdings instead of fiat.Why
v2 is network-scoped and is the path forward; v1 is being retired. The flag lets us default to v2 now while keeping an instant, release-free rollback. Per-network caching avoids cross-network price staleness, and gating fiat to mainnet avoids the meaningless
$0.00that v2 returns for testnet assets.Known limitations
freighter-backend-v2.stellar.org) currently 404s on/api/v1/token-prices— it must be deployed before the flag is enabled in prod (the flag defaults to v2, so the first fetch always hits v2).--(failures now log to Sentry).Testnet example(matches extension, new data source has no data for testnet currently)
Verification
yarn jest— 182 suites, 2545 passing) +yarn lint:ts/eslintclean.Checklist
PR structure
Testing
Release