Skip to content

feat(prices): migrate token prices to network-scoped v2 endpoint#917

Open
aristidesstaffieri wants to merge 14 commits into
mainfrom
feat/token-prices-v2
Open

feat(prices): migrate token prices to network-scoped v2 endpoint#917
aristidesstaffieri wants to merge 14 commits into
mainfrom
feat/token-prices-v2

Conversation

@aristidesstaffieri

@aristidesstaffieri aristidesstaffieri commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Migrates token-price fetching from freighter-backend v1 to the network-scoped v2 /token-prices endpoint (ported from stellar/freighter#2870), behind a use_token_prices_v2 Amplitude 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 — add use_token_prices_v2 flag (defaults to v2; Amplitude can roll back to v1).
  • services/backend.tsfetchTokenPrices selects v1/v2 by flag. v2 is network-scoped (?network=PUBLIC) and translates the native asset XLM"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. New usePricesForNetwork selector.
  • Consumers — thread active network + flag and read per-network prices: 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.00 that v2 returns for testnet assets.

Known limitations

  • The v2 prod backend (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).
  • Testnet shows no fiat by design (no real market; matches the extension) — including the XLM price that v1 incidentally surfaced on testnet.
  • The total is also hidden during the brief pre-price-load window on mainnet, then appears once prices land.
  • No price-specific loading/error UI — a fetch failure shows -- (failures now log to Sentry).

Testnet example(matches extension, new data source has no data for testnet currently)

simulator_screenshot_6F5E2736-75B8-4BBC-9CAF-1CAE1EF7D87D

Verification

  • Full unit suite green locally (yarn jest — 182 suites, 2545 passing) + yarn lint:ts / eslint clean.
  • Manual: mainnet prices correct on a v2 dev build.
  • Manual: testnet shows no fiat/total and Send/Swap enabled for a funded account.
  • CI green.
  • v2 prod backend deployed before flipping the flag on in prod.

Checklist

PR structure

  • This PR does not mix refactoring changes with feature changes (break it down into smaller PRs if not).
  • This PR has reasonably narrow scope (break it down into smaller PRs if not).
  • This PR includes relevant before and after screenshots/videos highlighting these changes.
  • I took the time to review my own PR.

Testing

  • These changes have been tested and confirmed to work as intended on Android.
  • These changes have been tested and confirmed to work as intended on iOS.
  • These changes have been tested and confirmed to work as intended on small iOS screens.
  • These changes have been tested and confirmed to work as intended on small Android screens.
  • I have tried to break these changes while extensively testing them.
  • This PR adds tests for the new functionality or fixes.

Release

  • This is not a breaking change.
  • This PR updates existing JSDocs when applicable.
  • This PR adds JSDocs to new functionalities.
  • I've checked with the product team if we should add metrics to these changes.
  • I've shared relevant before and after screenshots/videos highlighting these changes with the design team and they've approved the changes.

  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
@aristidesstaffieri aristidesstaffieri self-assigned this Jun 25, 2026
@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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)

@aristidesstaffieri aristidesstaffieri marked this pull request as ready for review June 26, 2026 16:16
Copilot AI review requested due to automatic review settings June 26, 2026 16:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_v2 boolean remote-config flag and default it to true for both dev/prod initial config.
  • Update fetchTokenPrices to accept { network, useV2 }, call freighterBackendV2 with ?network=PUBLIC|TESTNET when enabled, and short-circuit on unsupported networks / filtered-out token sets.
  • Thread network through 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.

Comment thread src/hooks/blockaid/useTransactionBalanceListItems.tsx Outdated
Comment thread __tests__/services/backend.test.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/ducks/prices.ts
aristidesstaffieri and others added 3 commits June 26, 2026 10:28
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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/ducks/remoteConfig.ts

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/ducks/prices.ts
Comment thread src/hooks/blockaid/useTransactionBalanceListItems.tsx Outdated
  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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/ducks/prices.ts Outdated
  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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/components/screens/SwapScreen/hooks/useSwapTokenPrices.ts Outdated
  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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/hooks/blockaid/useTransactionBalanceListItems.tsx Outdated
  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".

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread src/ducks/prices.ts Outdated
  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.
@aristidesstaffieri aristidesstaffieri requested a review from a team June 26, 2026 21:44
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