Repository navigation
Conversation
getSwapQuotes calls POST /swap/quotes through the existing FETCH_BACKEND_V2 chokepoint (JWT-signed in the service worker). 400 maps to a bad-input error, every other non-200 to "unavailable". getSwapQuote uses it on standard networks and keeps Horizon path finding on custom networks, which the backend can't reach. Until the backend builds the transaction (C2), the classic route is used; pickSwapRoute is the one place to switch that to routes[0]. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The debounced live quote, the review-time fallback quote and the XLM
reserve sheet's receive quote now go through getSwapQuote. The full
response is kept in transactionData.swapQuotes for C2/C4. A 502/503
(or any non-400 failure) shows "Swaps are temporarily unavailable";
empty routes, no classic route, or a 400 show "No quote available".
Unit tests mock fetchBackendV2 instead of Horizon. e2e mocks move from
page.route("**/paths**") to a context-level stubSwapQuotes, since the
request leaves from the service worker; stubAllExternalApis now
answers /swap/quotes with no route by default, and the two swap tests
that relied on live Horizon now stub a quote.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…lable Review reused the stored live quote whenever it was non-zero, so a quote for an earlier amount, another destination, or one that expired at submit could be built with the wrong destMin and path. The stored quote now carries the request it answered (account, network, assets, amount, mode). Review reuses it only on an exact match and otherwise re-quotes. A quote that expires at submit drops its key. A live quote that lands after the screen unmounts is discarded. An unreachable backend (fetchBackendV2 rejecting) and a 200 whose classic source reported error/timeout/rate_limited showed "No quote available" and reached no Sentry. Both are now "Swaps are temporarily unavailable" and are captured (spec §8). no_route/insufficient_balance and 400 keep the no-quote state. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e fixes - Quote amounts are sent and keyed as plain decimals (BigNumber toFixed). The backend rejects "5.", ".5" and exponents. The percentage buttons and the fiat-to-crypto sync produced "1e-7" via toString, which cleanAmount then turned into "17". - A 400 when review re-quotes shows "No quote available", as the live CTA does, instead of the generic error. - Starting the review cancels any pending or in-flight live quote, so it can't overwrite the quote the review builds. This covers the reserve- sheet path too. - The reserve prefill no longer re-reports SwapQuotesError to Sentry. getSwapQuotes already reports outages, and 400s stay out of Sentry. - The live quote re-runs when the account or network changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The only conflict was swap.test.ts's import list: this branch's stubSwapQuotes beside master's trending-route constants from #3065 (asset search through backend v2). Kept both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Contributor
|
PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-311503013c1b9c016d26 |
The Review swap CTA was enabled and "You receive" showed a stale value or 0
while a /swap/quotes request was pending (1.7-3.3 s via LI.FI), because
hasNoSwapPath was forced false while isLiveQuoteLoading, and for one render
after an amount change neither flag said a quote was pending.
A quote is now pending when the amount is positive, both tokens are picked,
and either a live quote is in flight or the stored quote's key does not match
the current inputs. While pending, the CTA keeps the "Review swap" label but
is disabled with the Button loading spinner, and the receive card shows the
Shimmer placeholder. Balance and XLM-fee blockers still take precedence. A
failed live quote now stores its request key so it settles to the existing
no-quote / unavailable states instead of staying pending (the review build
still re-fetches because the stored amount is 0).
Tests: unit and component tests that click Review now wait for the quote.
The two "starting the review with a live quote in flight" tests are replaced
by one asserting the review cannot start in that state (now unreachable).
e2e: swap and blockaidScan specs wait for the CTA to be enabled before
clicking; the blockaidScan.safe review assertion drops a .or(getByText
("Review")) that also matched the CTA and went strict-mode flaky once the
review opened faster. The quote-expiry recovery e2e now asserts the CTA
re-enables after the re-quote.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Pressing Review committed the cleaned amount ("1,000" -> "1000") to
Redux, which re-armed the live-quote effect after cancelLiveQuote had
run. The effect set isLiveQuoteLoading, the request's result landed
while the review sheet was open and was dropped as review-frozen, and
nothing reset the flag. Cancelling the review then left the CTA
disabled with a spinner and the receive amount shimmering until the
user edited an input.
The stored quote key already answers "is a quote pending": every
success and every failure stores the key it answered, and a dropped or
superseded response stores nothing. Drop the flag and derive pending
from the key alone, so no cancelled or frozen request can leave the
screen pending.
The CTA-gate test that reaches the review sheet now spreads the full
transactionSubmission initial state: the review reads hardwareWalletData
and crashed on the partial store, which the test only survived because
the sheet rendered after RTL cleanup. Awaiting the click exposed it on
the unfixed code too.
The redundant /swap/quotes request on each Review press remains; it is
harmless now that its result cannot leave the screen pending.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
getSwapQuotes and getSwapQuote captured to Sentry on every failure, and the live quote re-fires once per debounced keystroke, so an outage cost one event per 500ms of typing per user. Meanwhile a 400 was reported nowhere: the helpers skipped it as "user input", and the reserve prefill skipped every typed error on the claim that the helpers had already reported it. Move reporting to the two paths where the user acted: the review re-quote in useSimulateTxData and the swap-for-reserve prefill. Both report once per press and include badRequest, since the extension builds every field of the request and a 400 is a client bug. The helpers now only type the error; the live hook stays silent. Tests: the helper and classic-outage tests flip to "not reported"; the live-quote tests assert no capture on 400, 5xx, unreachable and classic outage; the review and prefill tests assert one capture each, 400 included. Both suites now clear the Sentry module mock between tests, which the review 400 assertion needed to be discriminating. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Every quote now goes through getSwapQuote, which calls horizonGetBestPath directly for custom networks; nothing dispatches the thunk, and no test references it. Its fulfilled/rejected reducers still wrote transactionData.path and destinationAmount, so a new caller could have bypassed the backend silently. Remove the thunk, the two reducers, and the two imports only they used. Co-Authored-By: Claude Fable 5.1 <[email protected]>
…arming The previous commit moved Sentry reporting to the review re-quote, but a live-quote failure disables the CTA, so that path is unreachable during an outage and nothing was reported at all. Report from the live hook instead, once per mount until a quote succeeds, so an outage costs one event per screen visit rather than one per keystroke. 400s stay unreported there: on the live path they are the user's state (unfunded account, unsupported token); the review and prefill paths still report them. Key the live-quote effect on the cleaned amount. Review commits "1,000" as "1000", which re-armed a quote that could land behind the review sheet and replace the path the XDR was built from, and cost a redundant request per press. Align the two 400 comments: a 400 is the request, not an outage; the review comment no longer claims it is always a client bug. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Shown when the CTA state is noQuote or unavailable; tapping opens a bottom sheet explaining that no quote could be fetched (Figma 3405-86973). Moved here from the C4 branch, which also adds the See details link that shares the SwapAsset__details-link style; the style now lands with this commit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Varial design (Transactions page, "Quote unavailble" frame) shows a disabled "Review swap" with the reason on the "Quote unavailable" line and sheet. The amount-screen CTA had carried the reason instead: "No quote available" since Swap to New Token (#2872), and "Swaps are temporarily unavailable" since bad1536, which predates the line and sheet. Both CTA labels go, with their now-unused translation keys; the CTA state reports isQuoteUnavailable for the line. The review sheet's error notice still shows the two strings when a re-quote fails there. The outage/no-route distinction only fed those labels, so the isSwapUnavailable state in useSwapLiveQuote and its pass-through in getSwapDerivedData are deleted. Sentry reporting is unchanged. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Four moderate active issues remain: incorrect no-quote handling, untranslated user-facing errors, over-reporting expected 400 responses, and broken parsing for token symbols containing colons.
4 open issues
What changed in this PR
This PR moves standard-network swap quote retrieval from Horizon path finding to freighter-backend-v2 while retaining Horizon for custom networks, and adds quote identity tracking, unavailable states, and updated test coverage.
Changes:
- Adds typed backend quote requests, route conversion, error classification, and custom-network fallback.
- Updates live quoting, review simulation, reserve preflight, quote expiry, and transaction state handling.
- Adds loading and unavailable UI, translations, unit tests, and service-worker-compatible Playwright stubs.
| File | Description |
|---|---|
| extension/src/popup/locales/pt/translation.json | Updates Portuguese translations for quote-related swap states. |
| extension/src/popup/locales/en/translation.json | Updates English translations for quote-related swap states. |
| extension/src/popup/locales/__tests__/translationParity.test.ts | Adjusts translation parity coverage for the updated swap keys. |
| extension/src/popup/helpers/swapQuote.ts | Implements backend and custom-network quote retrieval with route conversion. |
| extension/src/popup/helpers/__tests__/swapQuote.test.ts | Tests backend routing, asset conversion, error handling, and Horizon fallback. |
| extension/src/popup/ducks/transactionSubmission.ts | Stores swap quote responses and request keys in transaction state. |
| extension/src/popup/ducks/__tests__/transactionSubmission.test.ts | Tests quote-key persistence and expiry invalidation. |
| extension/src/popup/components/swap/SwapAmount/styles.scss | Styles quote-unavailable content and the explanation sheet. |
| extension/src/popup/components/swap/SwapAmount/index.tsx | Integrates live quotes, reserve quoting, loading states, and unavailable-quote UI. |
| extension/src/popup/components/swap/SwapAmount/hooks/useSwapQuoteExpiry.tsx | Updates swap quote expiry behavior for explicit re-quoting. |
| extension/src/popup/components/swap/SwapAmount/hooks/useSwapLiveQuote.ts | Adds debounced backend live quotes and stale-response protection. |
| extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx | Reuses matching quotes or fetches new quotes during review simulation. |
| extension/src/popup/components/swap/SwapAmount/hooks/__tests__/useSwapLiveQuote.test.ts | Tests live quote refreshes and stale-response behavior. |
| extension/src/popup/components/swap/SwapAmount/hooks/__tests__/useSimulateSwapData.quote.test.tsx | Tests review-time quote reuse, re-quoting, errors, and transaction construction. |
| extension/src/popup/components/swap/SwapAmount/helpers/swapCtaState.ts | Adds pending and unavailable quote states to CTA logic. |
| extension/src/popup/components/swap/SwapAmount/helpers/getSwapDerivedData.ts | Derives quote matching and pending status for the swap UI. |
| extension/src/popup/components/swap/SwapAmount/helpers/__tests__/swapCtaState.test.ts | Tests quote-related CTA precedence and states. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.telemetry.test.tsx | Updates telemetry tests for asynchronous quote behavior. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.reserveQuote.test.tsx | Tests backend receive quotes used for reserve prefilling. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.quoteUnavailable.test.tsx | Tests unavailable-quote messaging, sheet behavior, and recovery. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.liveQuote.test.tsx | Tests live quote loading, failures, recovery, and stale responses. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.fiatLabel.test.tsx | Updates fiat-label tests for asynchronous quote loading. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.directionToggle.test.tsx | Updates direction-toggle tests with backend quote mocks. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.ctaGate.test.tsx | Updates CTA and reserve-gate tests for quote readiness. |
| extension/src/popup/components/amount/AmountCard/index.tsx | Adds loading placeholders for pending receive amounts. |
| extension/e2e-tests/swap.test.ts | Migrates swap end-to-end scenarios to backend quote stubs. |
| extension/e2e-tests/helpers/stubs.ts | Adds context-level backend quote stubbing and default no-route behavior. |
| extension/e2e-tests/blockaidScan.unable.test.ts | Updates unable-to-scan swap setup to use backend quotes. |
| extension/e2e-tests/blockaidScan.suspicious.test.ts | Updates suspicious-scan swap setup to use backend quotes. |
| extension/e2e-tests/blockaidScan.safe.test.ts | Updates safe-scan swap setup to use backend quotes. |
| extension/e2e-tests/blockaidScan.malicious.test.ts | Updates malicious-scan swap setup to use backend quotes. |
| @shared/api/types/backend-api.ts | Defines backend swap quote request and response types. |
| @shared/api/helpers/getSwapQuotes.ts | Adds the typed backend quote API helper and error classification. |
| @shared/api/helpers/__tests__/getSwapQuotes.test.ts | Tests quote requests, response handling, validation, and HTTP error classification. |
Review effort: Lite
…route The live screen treats a response without a classic route as no quote; the review re-quote showed "No path found for swap." for the same answer. Both paths now use the no-quote message, and the unused NO_PATH_FOUND constant goes. Mobile already maps no route and 400 to one message at both points. Co-Authored-By: Claude Fable 5.1 <[email protected]>
…ntract ID toBackendAsset split the canonical on the first colon, so a symbol like "MY:TOKEN" sent "TOKEN" instead of the C… contract ID. splitCanonical already splits on the last colon for exactly this reason. Co-Authored-By: Claude Fable 5.1 <[email protected]>
The receive quote behind "Swap for 0.5 XLM" can take seconds. If the sell token, account or network changed before it landed, its amount was written over the new inputs. The answer is now applied only when the inputs it was asked for are still current, and never after the screen unmounted. Mobile's hook gets the same identity check. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What
Swap quotes now come from freighter-backend-v2 (
POST /api/v1/swap/quotes) instead of Horizon path finding. This covers the live "You receive" amount, the quote the review builds from, and the XLM reserve sheet. The extension 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, "You receive" shows 0, a "Quote unavailable" line 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 in every state, as designed.
Custom networks keep Horizon path finding, since the backend can't reach them.
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).
Implementation details (for agents)
What changed
getSwapQuotescalls/swap/quotesthrough the existingFETCH_BACKEND_V2path, so requests are JWT-signed in the service worker. A 400 is bad input; every other non-200 is "unavailable".getSwapQuotepicks the classic route (pickSwapRouteis the one place to switch toroutes[0]once the backend builds) and falls back to Horizon on custom networks. The full response is kept intransactionData.swapQuotesfor C2/C4.5.,.5and exponents. On this screen the percentage buttons and the fiat-to-crypto sync could produce1e-7, whichcleanAmountturned into17.error/timeout/rate_limitedare reported to Sentry;no_route,insufficient_balanceand 400 are not. Both show the same "Quote unavailable" line and sheet with "Review swap" disabled. At review time the review sheet's error notice still distinguishes them ("Swaps are temporarily unavailable" / "No quote available").page.route("**/paths**")to a context-levelstubSwapQuotes, because the request leaves from the service worker.stubAllExternalApisanswers/swap/quoteswith no route by default.Verification
fetchBackendV2instead of Horizon. Each fix has a test that was run against the code before the fix and failed./swap/*went to the local backend and everything else to stg backend-v2. All quote requests were JWT-signed by the extension.routes: []with classicno_route; same screen as backend down, without the Sentry report.Screenshots / video
Swap screen with quotes from freighter-backend-v2: quote loading, quote landed, quote unavailable (backend down or no route for the pair; the screen is the same), and the Quote unavailable sheet.
Checklist
🤖 Generated with Claude Code