Skip to content

feat(swap): route screen behind See details (C4) - #1051

Open
piyalbasu wants to merge 25 commits into
feat/swap-soroban-tokensfrom
feat/swap-route-screen
Open

piyalbasu wants to merge 25 commits into
feat/swap-soroban-tokensfrom
feat/swap-route-screen

Conversation

@piyalbasu

@piyalbasu piyalbasu commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds the swap Route screen. A "network fee · See details" line under the receive amount opens a Details sheet with three rows: network fee, rate, and Route ("N providers" with venue icons). The Route row switches the sheet to the Route view, which shows how the swap is routed, from the best route /swap/quotes returned:

  • how the swap splits across paths, with each path's share;
  • each path's venues and pool count;
  • an expandable list of tokens and venues per hop;
  • the providers involved.

The design follows Figma (Route expand, without amount). It shows no per-hop amounts; no source supplies them yet.

Classic routes use a stand-in until design draws them: the same sheet with "1 path", a 100% pill and no pool count.

The sheet is display only: nothing at quote, build or signing reads it. It shows only the quote for the inputs on screen, so a re-quote in flight never shows the previous route.

Draft, for diff review. Stacked on C3 (#1048). Middle-token names need stellar/freighter-backend-v2#185.

Fee before review matches Send. With stellar/freighter-backend-v2#185, each LI.FI route carries its Soroban resource fee (about 0.085 XLM). The fee shown before review is now the total, inclusion plus resource, with Send's existing "may adjust" note on LI.FI routes. Classic routes are unchanged.

  • Max and the percent buttons hold back the same total. The amount-error check stays on the inclusion fee: an error there clears the quote, so a fee-dependent error would clear the quote and re-quote in a loop. A separate check, for a non-XLM source as on the extension, blocks Review when the XLM above the minimum balance can't cover the total, without touching the amount error.
  • Before the current amount's quote lands, Max holds back the pair's last quoted resource fee, so the first tap sticks. A pair with no quote yet holds back the inclusion fee alone, as before.
  • The XLM-reserve check on Continue/Review uses the same total.

Why

Swap routing spec, client item C4: stellar/wallet-eng-monorepo#131 (open item 2).

Implementation details (for agents)

What changed

  • BottomSheetHeader gains an optional onBack (centered layout only; other sheets are unchanged).
  • buildSwapRouteView and useSwapRouteView build the view model from the swap store's swapQuotes routes[0]. The pool count counts the hops that carry a pool. Shares under 1% read "<1%". Providers are the distinct venues, capped at 3 plus "+N" in the Details row. Singular and plural labels are separate keys.
  • SwapAmountScreen renders the line and one sheet that switches between Details and Route. Tapping See details dismisses the keyboard first.
  • When the line, route and rate show: only when pathResult.quoteKey === currentQuoteKey, the same check as the CTA and the build.
  • Venues are matched against the protocols list the app already loads (case and whitespace ignored, first match wins) and named by that entry; an unlisted one keeps its raw name.
  • Middle tokens are named from held balances, the destination, then the verified token lists (code and icon), then truncateAddress; XLM's own contract shows as XLM. Nothing is written to the icon cache.

Verification

  • New tests cover the view model, the Details → Route → back flow, the amount screen, and the header's back arrow. The stale-quote test was run against the code before the fix and failed. The route and rate gates were each mutation-checked.
  • jest passes (4868 tests). tsc, eslint and prettier are clean.
  • /code-review at high, then medium on the fix diff.

Open for design

  • The classic stand-in.
  • Provider chips wrap rather than scroll.
  • There's no tooltip on the Rate row.
  • Shares are rounded one by one, so they can total 99% or 101% (same on the extension).

Known follow-ups

  • Middle-token names read the token-list cache once, when the screen opens; a cold cache leaves shortened addresses until it reopens.
  • Protocols unsupported over WalletConnect are dropped from the list, so those venues are unbranded here but not on the extension.
  • When only the resource-fee check blocks Review, the button doesn't say why (the extension's label does).
  • The reserve check uses the fee from the press; a rebuild with a larger resource fee isn't rechecked (both platforms).

Screenshots

pr1051-route-screen

iOS simulator, mainnet, a LI.FI route from a local freighter-backend-v2 (stellar/freighter-backend-v2#178) with LI.FI enabled: the See details line, the Details sheet, the Route view, and the path expanded to its hops (XLM → Aquarius → USDC → SushiSwap → deJTRSY). Nothing was submitted.

🤖 Generated with Claude Code

piyalbasu and others added 5 commits November 9, 2026 16:43
The swap Route sheet opens from Details and needs a back arrow on the left
with the X on the right. onBack adds the arrow to the centered layout only;
the other layouts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
buildSwapRouteView turns routes[0] of /swap/quotes into what the Route
sheet shows:

- share: the path's fraction as a whole percent ("0.32" -> "32%", "<1%"
  for a nonzero share that rounds to 0). Null on a single-path route.
- poolCount: the path's hop count, only when its hops carry `pool`. Classic
  hops never do (Horizon doesn't name the pool), so classic shows none.
- providers: the distinct venues across all paths, first-seen order.
- venue icons: matched by name against /protocols (useProtocolsStore,
  already loaded at app start for Discover), case-insensitively. An
  unlisted venue such as stellar-dex has no icon URL and gets a generic
  icon.
- tokens: XLM, CODE:ISSUER, or a C... contract. A contract token takes
  the code of a held balance with that contract ID, or the destination
  descriptor's code; otherwise it shows truncateAddress(id), as the C3
  picker subtitle does. Icons are read from the existing pipeline only;
  nothing writes to the shared icon cache.

Returns null with no quote or no path with hops, which hides the Route
row. Display only: nothing at signing reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1-C3 had no details sheet on the amount screen, so this adds the entry
point from Figma 2863:148115: "<fee> network fee · See details (i)" under
the receive card, shown once a quote is on screen. It opens Details:
network fee (XLM, plus fiat when it is at least a cent), rate (the review
sheet's sticky rate), and Route: "N providers" with stacked venue icons
and a chevron. The Route row is hidden when the quote has no route.

Route (Figma 2983:153139, expanded 4241:50151) replaces the Details
content in the same sheet. Back returns to Details; X closes. It shows the
sold -> bought tokens, "Split across N paths", one row per path (share
pill, up to 3 hop venue icons then "+N", "N pools", chevron), the hint,
and the provider chips. A path expands into its tokens with the venue
between each pair. No per-hop amounts: the current design dropped them.

Classic stand-in (design has not drawn it yet): the same sheet, with
"1 path" in place of "Split across N paths", no share pill on a
single-path route, and no "N pools" label when the hops carry no pool.
Review with design.

Other choices:
- The fee line shows fiat when the XLM price makes it at least $0.01,
  and the XLM amount otherwise, since formatFiatAmount rounds a typical
  0.00001 XLM fee to "$0.00".
- Provider chips wrap instead of scrolling sideways, to avoid a
  horizontal scroller inside the sheet's scroll view.
- "1 provider" / "1 pool" / "1 path" are separate keys rather than
  i18next plurals, which the app does not use yet.
- The Rate row has no (i) icon: there is no copy for it.
- Tapping See details dismisses the keyboard first, as Review does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store keeps the last quote until the debounced re-quote (500 ms)
replaces it. The See details line checked only that a pathResult
existed, so for that window, after a token or amount change, Details and
Route showed the previous inputs' route. The sticky rate also
re-formatted the old rate under the new token's symbol, and stayed after
the quote was cleared.

The line, the Route data and the Rate now require pathResult.quoteKey to
match the current inputs' key, as the CTA, the settings rebuild and the
build already do. The rate hook only sees a current pathResult.

Display only: nothing at signing reads these.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backend #185 adds an optional resourceFee (XLM) to each LI.FI route in
/swap/quotes. /swap/tx's fee is the inclusion fee plus that value, so the
fee shown before review left out most of what the swap pays.

The See details line and the Details fee row now show swapFee plus
routes[0].resourceFee with computeTotalFeeXlm, the BigNumber sum Send's
FeeBreakdownBottomSheet uses. A classic route has no resourceFee and
still shows swapFee. The fiat value uses the same total. The resource fee
is read only for a quote matching the current inputs.

When the route has a resourceFee, Details shows Send's Soroban fee note
(feeBreakdown.descriptionSoroban). No new strings.

Spendable/Max and the build are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-10d91cef7b50c9c0b41a
Backend: sandbox (piyalbasu). SDF collaborators only — install instructions in the release description.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-3bfd7de4b69899ad6938
Backend: sandbox (piyalbasu). SDF collaborators only — install instructions in the release description.

piyalbasu and others added 8 commits November 9, 2026 17:34
Spendable (Max and the percentage buttons) subtracted only swapFee, the
inclusion fee. On a LI.FI route the swap also pays the route's
resourceFee, so Max on an XLM source left too little XLM for the fee.

Spendable now uses the same total as the fee display: swapFee plus
routes[0].resourceFee for a quote matching the current inputs, via
computeTotalFeeXlm. Classic routes and stale quotes keep swapFee alone.
The quote-key and total-fee values moved above the spendable memo; they
do not depend on it.

The amount-error check (useSwapAmountError) stays on the inclusion fee on
purpose. A fee-dependent error would clear the quote (path-finding calls
clearPath on an amount error), which drops the resource fee, which clears
the error, which re-quotes: a loop of LI.FI quotes. Spendable feeds the
CTA's insufficient state and the error text, neither of which gates
path-finding.

Send, the build and the settings input are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Stellar logo

The venue is not in /protocols (listing it there would surface it in
Discovery), so the route view maps it locally. SwapRouteVenue gains an
optional bundled iconSource that SwapVenueIcon prefers over iconUrl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o Figma

Figma 3405-175470: 4px gap between token icon and code; path-row venue
icons 32px, 4px apart; 16px after the share badge; hop connector is a
solid line with a 5px dot, venue icon 24px with a 4px gap; provider
chip icons 24px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Figma 3405-175551 uses Blue/04 background, Blue/06 border, Blue/09
14px semibold text; navy-04/06/09 are the nearest SDS tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route-sheet spacing change also enlarged the Details row stack.
Figma 2863-148281 overlaps 16px icons by 4px there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	__tests__/components/screens/SwapScreen/SwapAmountScreen.test.tsx
#	src/components/screens/SwapScreen/screens/SwapAmountScreen.tsx
@piyalbasu
piyalbasu marked this pull request as ready for review November 12, 2026 02:26
@piyalbasu
piyalbasu requested a lite review from Copilot November 12, 2026 02:26

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.

🟡 Changes recommended

Two moderate correctness issues remain: mixed routes can overcount pooled hops, and non-XLM Soroban quotes may pass review without sufficient total XLM fees and fail only during transaction construction.

2 open issues
What changed in this PR

Adds a quote-aware swap Details sheet and expandable Route view, allowing users to inspect network fees, rates, providers, paths, pools, and intermediate tokens for the current quote.

Changes:

  • Added localized Details and Route content with expandable paths, provider icons, token icons, shares, and pool information.
  • Added Details → Route navigation with centered-sheet back-button support.
  • Integrated LI.FI Soroban resource fees into fee display and amount calculations while preserving stale-quote safeguards.
  • Added comprehensive unit and component tests for route modeling, fee handling, sheet navigation, and header behavior.
File Description
src/​i18n/​locales/​pt/​translations.json Adds Portuguese translations for the swap Details and Route views.
src/​i18n/​locales/​en/​translations.json Adds English translations for the swap Details and Route views.
src/​config/​types.ts Adds the optional LI.FI Soroban resource-fee field to swap route types.
src/​components/​screens/​SwapScreen/​screens/​SwapAmountScreen.tsx Displays quote-aware fee details, integrates the Details sheet, and applies total-fee handling to swap amount controls.
src/​components/​screens/​SwapScreen/​hooks/​useSwapRouteView.ts Builds the current quote's route view model from swap state and protocol data.
src/​components/​screens/​SwapScreen/​hooks/​index.ts Exports the swap route view hook.
src/​components/​screens/​SwapScreen/​helpers/​swapRouteView.ts Maps swap routes into display-ready paths, tokens, venues, shares, and pool information.
src/​components/​screens/​SwapScreen/​helpers/​index.ts Exports swap route view helpers and types.
src/​components/​screens/​SwapScreen/​components/​SwapVenueIcon.tsx Renders venue icons, fallback icons, and venue icon stacks.
src/​components/​screens/​SwapScreen/​components/​SwapRouteContent.tsx Renders expandable route paths with hops, tokens, venues, pools, shares, and providers.
src/​components/​screens/​SwapScreen/​components/​SwapDetailsBottomSheet.tsx Implements the Details and Route sheet views with fee, rate, provider, and route information.
src/​components/​screens/​SwapScreen/​components/​index.ts Exports the swap Details sheet component.
src/​components/​BottomSheetHeader.tsx Adds optional back-button support for centered bottom-sheet headers.
tests/​components/​screens/​SwapScreen/​SwapAmountScreen.test.tsx Tests fee presentation, stale-quote gating, Max behavior, and opening the Details sheet.
tests/​components/​screens/​SwapScreen/​helpers/​swapRouteView.test.ts Tests route transformation, shares, pools, providers, icons, and token labels.
tests/​components/​screens/​SwapScreen/​components/​SwapDetailsBottomSheet.test.tsx Tests Details-to-Route navigation, route expansion, icons, classic routes, and fee notes.
tests/​components/​BottomSheetHeader.test.tsx Tests centered-header back-button behavior.

Review effort: Lite

piyalbasu and others added 11 commits November 11, 2026 22:01
A path showed its total hop count as "N pools" whenever any hop had a
pool, so one pooled hop plus one orderbook hop read "2 pools". The
backend's pool is per hop and optional; the count now takes the hops
that carry one.

Copilot review on #1051 (same line flagged on #3076).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reserve preflight on Review computed the XLM left after fees with
the inclusion fee alone. Through LI.FI, a swap into a new trustline token
also pays the quote's resource fee, so an account that covers the
inclusion fee and the 0.5 XLM reserve but not the resource fee skipped
the reserve sheet and failed at submit. It now uses the total the review
builds with (the button only enables with a current quote).

Copilot review on #3076; the extension gets the same change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ce fee

The amount screen checked "enough XLM for fees" against the inclusion
fee only. A USDC to XLM swap through LI.FI, from an account that covers
the inclusion fee but not the quote's resource fee, reached Review and
failed at build. The extension already blocks it.

The new check compares XLM with the total (inclusion + the current
quote's resource fee), disables the CTA and shows the same "insufficient
XLM for fees" toast. It deliberately does not set the amount error:
that error pauses path-finding, and the resource fee comes from the
quote, so gating it on the total would keep a stale fee in place. The
test asserts path-finding still sees no amount error, and fails if the
total is fed into the amount error instead.

Code review, drift review and Copilot on #1051.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate added in d2d48a8 compared the whole XLM balance with the total
fee, so an account whose XLM sits mostly in its minimum balance passed
and failed on-chain; it also ran for an XLM source, where spendable
already nets the fee out, and could toast after a swap had submitted.
It now does what the extension's insufficientXlmForFees does: non-XLM
source only, XLM above the minimum balance against the total, and no
toast once a transaction hash exists.

Medium review on the C4 fix round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
formatRouteShare rounded half-up before checking for zero, so 0.6% showed
"1%". The extension shows "<1%" for anything under 1%; decided to align
on that (Piyal, 2026-11-11). A zero or invalid share still hides the pill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route sheet hid the share pill when a route had one path; the
extension always shows it. Decided to align on always showing it (Piyal,
2026-11-11), so single and split routes lay out the same. The sheet test
that pinned the old behaviour now expects the pill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hop showed the backend's venue string as sent and only took its icon
from /protocols, so a casing or spacing difference ("sushiswap" against
"SushiSwap") read differently from the extension, which shows the
protocol entry's name. Decided to align on the extension (Piyal,
2026-11-11); an unlisted venue still shows the backend string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s contract as XLM

LI.FI relabels only a route's first and last token; middle hops arrive as
bare contract IDs. Mobile named them from held balances or the
destination and otherwise showed a shortened ID, while the extension
also checks the token lists for a code and icon. Decided to align on the
extension (Piyal, 2026-11-11): the route view now falls back to the
verified token lists, read from the cache the swap picker already fills
(readCache, no new request), before the shortened ID.

XLM's own contract as a hop showed as a shortened address on both
platforms; it now reads XLM with the native token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Route row's provider preview rendered every provider, while the
route sheet's path rows and the extension cap at MAX_PATH_VENUE_ICONS
with a "+N". Decided to align on the cap (Piyal, 2026-11-11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided to use one Portuguese text on both platforms (Piyal, 2026-11-11).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te lands

Spendable netted out a Soroban route's resource fee only while a quote
for the current amount existed. Max changes the amount, so its re-quote
landed with a resource fee and pushed the amount over the limit; the
user had to tap Max again. Decided (Piyal, 2026-11-11, option A): the
swap duck keeps the last route's resource fee per pair (account,
network, from, to), across amount changes and cleared paths, and with no
current quote spendable uses it. No fee stored for the pair, or a
classic last route, behaves as before. The resource-fee XLM gate still
reads the current quote only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three parity gaps the medium review found in the route view, each with
its own test (all three failed before):
- an empty token-list code fell through as "" with ??; it now reaches
  the shortened contract ID, as the extension's || does
- a classic middle hop got no icon from the token lists (only the
  destination did); it now takes the listed icon by code and issuer
- a duplicate /protocols key let the last entry win; the first wins now,
  matching the extension's find()

One commit: the pre-commit hook runs the full suite, and the three
tests share one file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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