Repository navigation
Conversation
The main swap flow and the XLM reserve sheet now quote through POST /api/v1/swap/quotes on the JWT-signed freighterBackendV2 client (send quotes while typing, a receive quote for the 0.5 XLM estimate). No Horizon path-finding call remains in the swap flow. Until C2 moves building to /swap/tx, the client still builds a classic path payment, so it uses the response's classic route (selectSwapRoute) and treats its absence as "no path". The full response is kept in the swap store as swapQuotes for C2/C4. Status handling: 200 with no usable route and 400 show the existing "no path found" error; 502/503 show the new "Swaps are temporarily unavailable" (spec section 8). Other failures keep the existing generic error. Quotes now take seconds (LI.FI 1.7-3.3 s measured 2026-10 vs Horizon ~0.35 s, per the spec), so a stale response could overwrite a newer quote or a cleared amount; findSwapPath now drops responses superseded by a later request, clearPath or resetSwap. Also corrects two comments that still said the amount came from Horizon. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es as unavailable Stale quote (pre-pr-review D1): pathResult now carries quoteKey, the account, network, mode, assets and amount of the request it answered. SwapAmountScreen computes swapQuoteKey for the current inputs. The CTA stays in its loading state while they differ (amount edited inside the 500 ms debounce, destination/account/network changed), and setupSwapTransaction refuses to build from a mismatched quote with the existing "quote expired" error. Outage vs no route (D2, D3; ruling from spec section 8): transport failure (status 0), 401 and any 5xx show "Swaps are temporarily unavailable"; so does a 200 without a classic route when classic is listed unavailable with error, timeout or rate_limited, which is now logged to Sentry. no_route, insufficient_balance, no classic entry and 400 keep "No path found". The reserve sheet uses the same classifier and warns on a classic outage, as it already did for failed quotes. Adds the missing tests for resetSwap and the error-path stale check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e slippage - B1: the quote request and quote key send the amount as a plain decimal (BigNumber.toFixed()), so "5." and "1e-7" no longer 400 against the backend's ^[0-9]+(\.[0-9]+)?$ check. - B2: an HTTP 429 from the backend (Traefik rate limit) shows "Swaps are temporarily unavailable", like a source-level rate_limited. - M1: setupSwapTransaction computes the minimum received from the quote's destinationAmount and the current slippage setting, since slippage is not in the quote key and can change in the settings sheet after quoting. - M4: the reserve sheet no longer requests a receive quote when the sell side falls back to another token and the result would be discarded. - M7: a settings change while the matching quote is still pending skips the rebuild instead of throwing "quote expired" (Review is disabled until that quote lands); the throw stays as the build-to-sign backstop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-31b3df4debed144d1bf0 |
|
iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-ada6fc47fd68e03680b4 |
…t screen Shown after a no-route or swaps-unavailable quote result; tapping opens an information sheet (Figma 3405-86973). The test sheet mock now records each sheet's customContent so a test can find a sheet by its title. Moved here from the C4 branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store now records which inputs a quote error is for, so the line no longer shows an error from the previous amount or token during the re-quote debounce. It also hides behind the insufficient-balance and amount-error states, and the sheet dismisses when the line goes away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The path-finding effect re-fired on every slippage change and findSwapPath still computed destinationAmountMin from it, but nothing read that field: useSwapTransaction recomputes the minimum from the stored quote at build time. Changing slippage from the review sheet rebuilt the XDR from the current quote, then 500ms later the debounce nulled the quote and fetched a new one that nothing rebuilt from, so the sheet showed the new amount while the signed transaction carried the old path and minimum. The quote-expired retry path passed slippage for the same reason. Drop slippage from the hook's inputs and effect deps, from the findSwapPath params, and the unused minimum from SwapPathResult. Slippage stays a build-time input only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
handleSwapForXlmFromSheet awaited the backend receive quote, which can take seconds where Horizon answered in well under one, and then wrote source, destination and amount unconditionally. Backing out of the swap screen mid-flight let the result re-fill the store that resetSwap had just cleared, so the next swap opened with XLM as the receive token and a stale amount. findSwapPath got a request-id guard in this branch; this call site did not. Guard the writes on the hook still being mounted and on no newer tap having started. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
swapQuoteErrorReason mapped 400 to no-route and only 0, 401, 429 and 5xx to unavailable; anything else fell through to the generic "Couldn't fetch swap path" with no Quote unavailable sheet. Until freighter-backend-v2#178 is live in prd the route 404s, so the window both PR descriptions call out showed the generic message instead of "Swaps are temporarily unavailable". The extension already treats every non-400 failure as unavailable; match it. A 200 whose body isn't a quotes envelope threw a plain Error and landed in the same generic bucket. Give it a named error class and classify it as unavailable too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
findSwapPath logged every failure at error level, which reaches Sentry in production, and it runs once per debounced keystroke. A backend outage produced one event per amount edit per user, and a 400 for bad input or an unfunded account was reported as a fault. Report an outage once until a quote succeeds or the screen is left, whether it arrives as a thrown non-400 or as a 200 with the classic source down. Log 400s at warn. Anything the classifier doesn't recognise still reports every time. Matches the extension. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The three-second path-error toast still fired for no-route and swaps-unavailable, on top of the persistent "Quote unavailable" row and sheet the amount screen now renders for exactly those two, so the same failure surfaced twice and the toast re-fired over the row on every re-quote. Skip the toast for the two inline-handled errors; any other path error still toasts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
swapRouteIntermediateAssets turned an empty hop chain into an empty path, and findSwapPath still built a path result from the route's amounts, so a malformed classic route was offered as a quote and submitted as a direct pathPaymentStrictSend that can fail on-chain with the user paying the fee. Select only classic routes with hops; the rest reads as no route, as the extension already does, and the duck reports the malformed route to Sentry since it is a backend fault rather than a pair without liquidity. A real route always has at least one hop: freighter-backend-v2 builds classic hops from [from, ...path, to] in classicRoute (internal/services/swap/classic.go), so this guards against a regression there, not a normal reply. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Varial design (Transactions page, "Quote unavailble" frame) shows a disabled "Review swap" with the reason on the "Quote unavailable" line and sheet. A failed quote fell through to the "enter" state, so the disabled button read "Enter an amount" with an amount already typed. The pathError gate already disables it; only the label changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # src/components/screens/SwapScreen/hooks/useSwapTransaction.ts
There was a problem hiding this comment.
🟡 Changes recommended
Active issues remain, including a critical stale-request race that can apply obsolete swap data and moderate defects that preserve stale amounts or mishandle malformed quote responses.
3 open issues
What changed in this PR
Moves swap quote retrieval from Horizon path-finding to the authenticated freighter-backend-v2 quote endpoint while retaining client-side classic path-payment construction for C1.
Changes:
- Adds backend-v2 quote handling, route selection, error classification, and stale-quote protection.
- Updates swap UI, transaction building, slippage handling, and XLM reserve estimates for backend quotes.
- Adds localized quote-unavailable messaging and comprehensive unit tests.
| File | Description |
|---|---|
| src/services/backend.ts | Adds the backend-v2 swap quote API client. |
| src/i18n/locales/pt/translations.json | Adds Portuguese quote-availability translations. |
| src/i18n/locales/en/translations.json | Adds English quote-availability translations. |
| src/hooks/useTokenFiatConverter/reducer.ts | Updates amount serialization documentation for quote requests. |
| src/ducks/swap.ts | Replaces Horizon path-finding with backend quotes and manages quote freshness and errors. |
| src/config/types.ts | Defines backend swap quote response and route types. |
| src/components/screens/SwapScreen/screens/SwapAmountScreen.tsx | Displays quote-unavailable feedback and coordinates quote-aware transaction flow. |
| src/components/screens/SwapScreen/hooks/useSwapTransaction.ts | Validates quote freshness and derives minimum received using current slippage. |
| src/components/screens/SwapScreen/hooks/useSwapPathFinding.ts | Removes slippage from quote-triggering inputs and retains debounced quote fetching. |
| src/components/screens/SwapScreen/hooks/useSwapForXlmReserve.ts | Uses backend receive quotes for the XLM reserve estimate. |
| src/components/screens/SwapScreen/hooks/useSwapCtaState.ts | Controls CTA state for pending, stale, and failed quotes. |
| src/components/screens/SwapScreen/hooks/useSwapAmountError.ts | Prevents duplicate toasts for inline quote failures. |
| src/components/screens/SwapScreen/helpers/swapDisplayStrings.ts | Updates quote amount documentation. |
| src/components/screens/SwapScreen/helpers/selectSwapRoute.ts | Selects classic routes and classifies quote failures. |
| src/components/screens/SwapScreen/helpers/index.ts | Exports swap route and quote-classification helpers. |
| tests/services/backend.swapQuotes.test.ts | Tests backend quote requests and response validation. |
| tests/ducks/swap.findSwapPath.test.ts | Tests quote handling, route selection, stale responses, error mapping, and outage logging. |
| tests/components/screens/SwapScreen/useSwapTransaction.test.ts | Tests slippage recalculation and stale-quote rejection. |
| tests/components/screens/SwapScreen/SwapAmountScreen.test.tsx | Tests quote-unavailable UI, CTA behavior, settings changes, and sheet handling. |
| tests/components/screens/SwapScreen/hooks/useSwapPathFinding.test.ts | Updates path-finding hook tests for backend quote inputs. |
| tests/components/screens/SwapScreen/hooks/useSwapForXlmReserve.test.ts | Tests backend receive quotes, fallback behavior, and reserve-flow races. |
| tests/components/screens/SwapScreen/hooks/useSwapCtaState.test.ts | Tests CTA behavior for matching, stale, and failed quotes. |
| tests/components/screens/SwapScreen/hooks/useSwapAmountError.test.ts | Tests suppression of duplicate quote-error toasts. |
| tests/components/screens/SwapScreen/helpers/selectSwapRoute.test.ts | Tests classic route selection and quote error classification. |
Review effort: Lite
The unmount and newer-tap guards did not cover the user dismissing the sheet and changing the sell token, account or network while the receive quote was in flight: the old answer then set XLM as the destination and prefilled an amount for the previous inputs. The answer is now applied only when the inputs it was asked for are still current. The extension's prefill gets the same identity check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The extension reports every failed "Swap for 0.5 XLM" quote to Sentry, 400 included, because the request is built from the wallet's own balances; this hook only warned, so the same failure was invisible on mobile. A plain no-route answer is still not reported on either. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A failed quote cleared pathResult but left destinationAmount, which the receive card renders directly, so the previous quote's amount sat next to "Quote unavailable". Every failure branch now resets it. The amount stays on the card while the next quote is in flight, which is what the design shows; the extension hides it behind a shimmer there and resets it on failure the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>


What
Swap quotes now come from freighter-backend-v2 (
POST /api/v1/swap/quotes) instead of Horizon path finding. This covers the quote shown while typing and the XLM reserve sheet's 0.5 XLM estimate. No Horizon path-finding call remains in the swap flow. The app still builds and signs the classic path payment itself from the backend's classic route; moving the build to the backend is the next step (C2).When the backend can't quote (an outage, a timeout, a rate limit) or the pair has no route, a "Quote unavailable" line under the receive card opens an explanation sheet and "Review swap" is disabled. Outages go to Sentry; a pair with no route does not. The button keeps its label, as designed.
Ship order: this needs stellar/freighter-backend-v2#178 live in prd first. Released before that, every swap shows "Quote unavailable".
Why
Each client has its own Horizon swap code today, so every new liquidity source costs two client releases. With routing on the backend, a source is added once. Soroban tokens (through LI.FI) arrive this way, after C2.
Spec: stellar/wallet-eng-monorepo#131 (client item C1).
Known limitations
Quotes now take longer (LI.FI answered in 1.7–3.3 s when measured in 2026-10, against about 0.35 s for Horizon), so the amount takes longer to appear while typing.
Implementation details (for agents)
What changed
freighterBackendV2client. Until C2, the app builds from the response's classic route (selectSwapRoute) and treats its absence as "no path". The full response is kept in the swap store asswapQuotesfor C2/C4.pathResultcarries aquoteKey(account, network, mode, assets, amount). While it differs from the current inputs, for example when the amount was edited inside the 500 ms debounce, the CTA stays in its loading state, andsetupSwapTransactionrefuses to build with the existing "quote expired" error.findSwapPathdrops answers superseded by a later request,clearPathorresetSwap.5.and1e-7.error/timeout/rate_limitedset the "Swaps are temporarily unavailable" path error and are logged to Sentry once per outage;no_route,insufficient_balance, no classic entry and 400 set "No path found" and are not. Both render as the "Quote unavailable" line and sheet with "Review swap" disabled; the strings are no longer shown. The reserve sheet uses the same classifier and skips its quote when the sell side falls back to another token.Verification
/swap/*went to the local backend and everything else to stg backend-v2, with quotes JWT-signed by the app.Screenshots / video
iOS simulator, mainnet, quotes from a freighter-backend-v2 sandbox running stellar/freighter-backend-v2#178 as merged: quote landed, review sheet (not submitted), backend unreachable ("Quote unavailable" line, Review swap disabled), and the explanation sheet.
Android emulator (API 35), same setup and states.
Checklist
PR structure
Testing
Release
🤖 Generated with Claude Code