[WIP] Swap to New Token#2872
Draft
CassioMG wants to merge 89 commits into
Draft
Conversation
Addresses PR review: the clipboard "Paste" button was removed from all extension designs since a programmatic clipboard read needs an extra clipboardRead manifest permission + user opt-in (not worth it on web). Manual paste into the search field still works. Documents the omission as a mobile-vs-extension difference in §2.4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses PR review: the extension supports custom networks (mobile does not), and verified-token lists + stellar.expert exist only for Mainnet/ Testnet. Document that on custom networks the picker gracefully omits the Popular section and search degrades to held-only (held-to-held swaps still work via the network's Horizon). Covered in §3.1 + §2.3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses PR review: isNew was vague. requiresTrustline reads as it is used (when true, bundle a changeTrust op). Renamed everywhere including the telemetry payload field, to be kept in sync with freighter-mobile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…wns width InputWidthContext and InputWidthProvider were only used to pass width state to SendAmount, which has been replaced by AmountCard owning width internally (per design §3.3). Grep confirmed zero remaining consumers; removed provider wrapper and deleted context file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pular tokens Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n results Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p picker Idle = Your tokens + Popular (volume7d ∩ verified, held filtered out); search = Your tokens + Verified + Unverified. Classic-only filter with hadSorobanMatches empty-state, pick-time bulk Blockaid scan of non-held candidates, AbortController cancellation + CODE:ISSUER dedupe, and a held-only graceful fallback on stellar.expert errors / custom networks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Create two small informational SlideupModal components for the swap token picker: VerifiedTokenInfoSheet (explains asset list verification) and UnverifiedTokenInfoSheet (cautions about unverified tokens). Each has a single "Got it" dismiss button. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ack states) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a reusable `buildChangeTrustOperation({ assetCode, assetIssuer, isRemove, sdk })`
helper exported from getManageAssetXDR.ts. `getManageAssetXDR` now delegates to it
internally with no behavior change for the existing add/remove trustline flow.
Task E2 (swap builder) will reuse this helper to prepend a changeTrust op.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s + direction chevron Replace the bespoke inline input / AssetTile form body with two AmountCards (sell = editable, receive = read-only fed by destinationAmount), PercentageButtons wired to the existing percentage/Max math, and a direction chevron that emits swapDirectionToggled and swaps saveAsset/saveDestinationAsset. Crypto/fiat toggle, path-finding trigger, and fee/slippage footer row are fully preserved. Slippage default was already "2" (Phase A2). Seams left for F8 (CTA state machine) and F9 (selectionType / destination-details / telemetry). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t to SwapAmount Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…emetry Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BalanceRow forwarded an empty assetIcons map when callers passed a single iconUrl (the swap picker's held list), so AssetIcon's isFetchingAssetIcons (isEmpty(assetIcons) && !isXlm) stayed true and every non-XLM held row spun forever (XLM was exempt). Synthesize a one-entry icons map from iconUrl when assetIcons is empty; the account-home path (real non-empty map) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuilds the verified/unverified token info bottom sheets to the Figma spec: a colored icon badge + circular close on top, an 18px title, the secondary body copy, and a full-width dismiss button. Extracts a shared InfoBottomSheet (badge variant + close + title + body + CTA) so the two sheets stay consistent; the modal-agnostic InfoSheetContent can be reused by the trustline pane next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The banner becomes a compact lilac pill (alert-square icon + label + chevron) instead of the oversized primary Notification. The info sheet is rebuilt on the shared InfoSheetContent (lilac badge + plus icon, X close, title, body, "Got it" CTA) with the token-specific reserve copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The source picker only filtered once the search term exceeded two characters, so 1-2 letter token codes never matched until a third character was typed. The empty-term case is already handled separately, so always filter when a term is present — matching the Swap-to search. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sort the held "Your tokens" list by descending fiat value via the shared sortBalancesByValue helper, matching the account-home list. - Show Classic assets only: exclude liquidity-pool shares (already) and custom Soroban contract tokens (have a contractId), since swaps run over the Classic path. The stellar.expert search already drops non-SAC Soroban results via the isClassic filter. - Never filter "Your tokens" by hiddenAssets, so a held token stays visible even when it is the asset already selected on the other side. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
useNetworkFees expresses recommendedFee in XLM (the fetched value is
converted from stroops), but the initial state and the error fallback
used the raw stroop BASE_FEE ("100"). On the Swap amount screen that
rendered as "100 XLM" and was subtracted from the XLM available balance
until feeStats resolved ~0.5s later, causing both labels to jump.
Seed and fall back to stroopToXlm(BASE_FEE) ("0.00001 XLM") instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The receive picker derived its icon solely from the account icons map, which only holds logos for held tokens. A non-held destination token (picked from search/popular) therefore rendered with no logo even though the picker list showed it fine. Fall back to the icon URL captured on the picked token (destinationTokenDetails.iconUrl) when the map has none. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A non-held token can never be the source (swaps run between held/classic assets). When the receive side holds a non-held token, the direction toggle now drops it back to "(+) Select" and moves the held source into the receive slot instead of swapping it into the sell slot. Held-or-empty pairs still swap positions normally. To support the resulting empty source slot, the sell card, available balance, simulate params and CTA now tolerate an unset source asset (mirroring the existing destination handling). The amount is reset whenever the source token changes, since it was denominated in the old source token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render the trustline body via <Trans> so "0.5 XLM will be reserved" is emphasized inline (medium weight, brighter text) per Figma, using a named <bold> component to keep the sentence a single translatable key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match Figma: the empty-result message (e.g. a search that matched only unsupported Soroban contract tokens) renders as a gray-03 rounded card with centered medium text instead of bare muted text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
You can't swap a token for itself. Now that "Your tokens" no longer hides the asset selected on the other side, picking a token that matches the other picker clears that picker: a destination pick that equals the source resets the source (and amount), and a source pick that equals the destination resets the destination (and its token details). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fiat label: the amount cards always render the fiat line — it shows "--" when the selected asset has no price and "$0.00" for the empty "(+) Select" state, instead of disappearing. Only the input-type toggle stays gated on a usable USD price. Applies to both Swap and Send. Fee label: the swap fee is always shown in XLM regardless of input mode; it previously flipped to a "$0.00" fiat value in fiat mode. Removes the now-unused recommendedFeeUsd / xlmPrice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The info sheet styles referenced --font-weight-medium / --font-weight-regular, which aren't defined anywhere, so those font-weight rules silently no-opped. Switch to the real SDS tokens (--sds-fw-medium = 500, --sds-fw-regular = 400) so the title and the inline-emphasized reserve amount render at medium weight. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The trustline sheet was rendered as a MultiPaneSlider pane inside the review View, which double-padded its content (View inset + the sheet's own padding) and rendered the "Got it" CTA in the wrong context. Render it via InfoBottomSheet (SlideupModal) as an overlay — the same path the verified/unverified swap sheets use — so it gets single padding and the correct filled "Got it" button, and matches the Figma bottom sheet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "You receive" card (and the review step's destination fiat, which is derived from the same destination rate) showed "--" for non-held tokens because their price was never fetched. Mirror freighter-mobile: - Fetch /token-prices for the selected destination canonical, not just the account's held balances. This runs as a SEPARATE, best-effort request in useGetTokenPrices (additionalAssetIds) and merges its result — so a failure pricing the destination can never wipe the reliable balance prices (the regression where both cards lost their price). - Capture stellar.expert's spot `price` from the search result (SwapTokenRecord.spotPrice -> DestinationTokenDetails) and use it as a mainnet-only fallback when /token-prices has no entry. The review step needs no change: dstAssetPrice already flows into the simulation's destinationRate -> dstAmountPriceUsd. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The assetScanResults slice slot (reducer + selector + dispatch) was written on every swap-token lookup but never read back — the live scan verdicts come from mergeScanResults decorating the lookup payload in session, not from this cache. Persisting per-token Blockaid verdicts in extension storage is also a tampering risk we explicitly deferred, so the slot has no path to becoming useful as-is. Drops saveAssetScanResults/assetScanResultsSelector and the assetScanResults state, keeping the in-session mergeScanResults decoration and the still-used popularTokens slot untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both stellar.expert fetchers behind the swap picker swallowed backend outages: searchAsset returned res.json() with no res.ok check, and fetchTrendingAssets caught every error and returned []. A 5xx with a JSON error body (or any non-records payload) therefore rendered an empty picker with no indication discovery was down. Both now throw on a non-ok status (and fetchTrendingAssets lets rejections propagate), so the swap lookup's catch flips to the held-only fallback and shows the existing "Token discovery is temporarily unavailable" notice — matching mobile, which returns null on outage to drive the same state. A successful-but-empty response still yields []. Aborts remain handled by the lookup's signal.aborted guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aligns the extension swap review with mobile, which surfaces the conversion Rate (1 SRC ≈ N DST) and keeps the minimum-received figure off the main review. The Rate row was previously suppressed because it was gated on destMin, which the swap simulation never populates for display. - Rate row now renders whenever a destination asset is present, computed from the send/destination amounts (destMin is still computed for the on-chain path-payment floor, just not displayed). - Removes the Minimum received row and the now-unused destMin prop. - Hides the Memo row on swaps — swaps are path payments to self and carry no memo. Send is unaffected (no destination asset => not a swap). Fee and XDR rows are kept as-is. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…w token When the source is XLM and the destination needs a new trustline, 0.5 XLM of the spendable balance is locked for the trustline subentry and isn't actually swappable. The amount screen showed the full spendable, so Max / percentage buttons and the insufficient-balance check could let the user stage a swap that leaves no room for the reserve. Adds deductNewTrustlineReserve, which subtracts BASE_RESERVE from the displayed source spendable in exactly that case — but only when spendable is already >= 0.5, so accounts that can't even cover the reserve fall through to shouldShowXlmReservePreflight and the XlmReserveSheet instead of seeing a clamped/zero balance. Mirrors mobile's SwapAmountScreen deduction. The preflight still gates on the undeducted spendable, as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new-trustline swap is two operations (changeTrust + pathPaymentStrictSend), but the amount screen's default fee was the single recommended base fee. It was then split across both ops at build time, so each op paid only half the recommended fee — underpriced under congestion — and the preflight + spendable deducted a 1-op fee for a 2-op transaction. getSwapTotalFee now scales the recommended default by op count (each op pays the recommended fee), while a user-set custom fee is still treated as the total and split per op at build time. Because this single fee value feeds the displayed fee, the simulation, the built transaction, the reserve preflight, and the spendable balance, all of them now reflect the true 2-op total. Mirrors mobile's recommendedFee × ops default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The XlmReserveSheet's "Swap for 0.5 XLM" button was inert: canSwapForReserve was hardcoded false and the handler only set the receive token to XLM, leaving the user to figure out the sell side and amount themselves. Implements the mobile flow: - canSwapForReserve is true when the current source is already a non-XLM classic token, or the account holds one (pickBestNonXlmClassicCanonical selects the largest such balance); otherwise the button stays hidden. - The handler sets the receive side to XLM (clearing the now-stale trustline details). If the source is reused, it pre-fills the amount needed to receive ~0.5 XLM via horizonGetBestReceivePath, capped to the sell token's spendable so the user never lands on an insufficient-balance state. If it falls back to a different sell token, the amount resets to 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the frozen quote no longer clears on-chain at submit time, Horizon returns op_under_dest_min / op_too_few_offers. The shared submit flow routed that to the terminal SubmitFail screen, so the user lost the swap and had to start over. The submit reducer now flags isSwapQuoteExpired on rejection (via the existing quote-expiry op-code helper; a no-op for sends, which never produce these codes). The Swap view detects the flag, emits the swapQuoteExpired metric with the result codes, clears the error status, and routes back to the amount screen — where SwapAmount's live-quote effect auto-refetches a fresh quote and the quote-expired notice is shown until the user retries. No SubmitFail flash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The best-non-XLM-classic-balance lookup added for the reserve-recovery action used useMemo, but it sits below the component's early returns — a hook there violates the rules of hooks. Replace it with a plain computation; the filter/sort over held balances is cheap and needs no memoization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The amount-screen CTA only disabled on a missing asset, a zero amount, or an over-balance amount. It stayed clickable when the live quote found no route (the submit would then fail) and when a non-XLM swap had no XLM to pay the network fee. - Extracts the CTA logic into a pure getSwapCtaState state machine (mirrors mobile's useSwapCtaState) with explicit labels per blocking state. - Adds an isLiveQuoteLoading flag so "no path" is distinguished from "quote still loading" — the CTA only disables once a quote settles with no route, avoiding flicker between keystrokes (2.5). - Adds an insufficient-XLM-for-fees gate for non-XLM sources, since their fee comes from the separate XLM balance the swap amount never touches (2.4). No change for 2.6: the extension's roundUsdValue floors to cents, so a fiat Max can't round-trip above spendable — the overspend mobile fixed never occurs here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The swap picker only surfaced the "Soroban tokens aren't supported, try a Classic token" hint when the search actually returned soroban records (hadSorobanMatches). A user pasting a bare contract id that stellar.expert returns nothing for instead saw the generic "No tokens match" message. Now the hint also shows when the search term itself is a contract id, matching mobile's empty-state condition (hadSorobanMatches || isContractId(searchTerm)). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The classic-only filter ran a SAC check (isAssetSac) on contract records that could never pass: search results map a contract token's issuer to its contract id, which isAssetSac can't validate against a classic issuer, so the branch was dead — the record was always dropped by the issuer-is-contract-id check anyway. Replaces it with a single contract-id test on issuer/contract, matching mobile's isSorobanRecord (isContractId(record.asset)). Behavior is unchanged: classic CODE-ISSUER records are kept (including SAC-backed assets, which stellar.expert returns in classic form), and custom Soroban tokens + LPs are dropped. Drops the now-unused isAssetSac import. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The review screen evaluated only the transaction (XDR) scan: a malicious or suspicious destination token was scanned at pick time and stored on destinationTokenDetails.securityLevel, but ReviewTransaction never read it, so swapping into a flagged token raised no warning at review. mergeSecurityLevels now rolls the destination token verdict together with the transaction verdict (most-severe wins; SAFE/absent never escalate). The merged level drives the existing warning + "Confirm anyway" soft gate — matching mobile, which warns rather than hard-blocks. A flagged token also shows its own banner (the transaction-scan banner stays gated on the tx verdict so a token-only warning never opens an empty scan pane). Unable-to-scan tokens surface the same way (4.2/4.5). Send is unaffected: it passes no token level, so the merge reduces to the transaction verdict and the gate behaves exactly as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the swap security model: the sell (source) token's verdict — read from its held balance's blockaidData via getAssetSecurityLevel — is now folded into the same merged review gate as the destination and transaction verdicts, and shown in its own banner. A flagged sell token now warns and requires "Confirm anyway", and the review surfaces source and destination verdicts independently (§4.3/§4.7). XLM sources are skipped (never scanned by Blockaid). Send is unaffected: it passes no source level. 4.4 (selectable-before-scan) needs no change — an unscanned token yields no verdict, so the merge simply doesn't escalate. 4.8 is already satisfied: the destination candidate set is scanned in one chunked bulk request. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
swapTrustlineAdded fired at review time, the moment the user tapped Confirm — before the swap was even submitted — so it counted intended, not completed, trustlines. And swaps had no dedicated success event (they reused the Send payment-success metric). Both now fire from useSubmitTxData only after the transaction actually settles, matching mobile: a new swapSuccess event (source/dest tokens, amounts, slippage) plus swapTrustlineAdded when the confirmed transaction included the changeTrust op. Sends are unchanged — they still emit sendPaymentSuccess. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-serve Re-entering the swap picker within a popup session re-ran the full idle pipeline (popular fetch + verified-list split + bulk scan) behind a spinner, and a transient stellar.expert/Blockaid failure dropped Popular entirely to the held-only fallback. Adds a module-scoped per-network cache of the last successful idle (no search term) result, mirroring mobile's trendingMemoryCacheByNetwork: - On idle re-entry the cached result repaints immediately and the pipeline revalidates silently — no spinner flash (§1.10). - On a transient fetch failure the cached result is served instead of dropping to held-only; held-only remains the fallback only when nothing is cached (§5.4, in-memory). In-memory only (dies on popup close); cross-session disk persistence (§5.3) is a separate concern. Exposes resetSwapIdleCacheForTests for test isolation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
No description provided.