[WIP] Swap to New Token#2872
Draft
CassioMG wants to merge 108 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>
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>
The stellar.expert trending request is the slowest part of the idle swap pipeline, and its cache lived only in memory (Redux + the module cache), so it re-ran on every popup reopen. The verified lists, by contrast, are cheap static CDN JSON and change rarely, so they stay in-memory for now. Adds a chrome.storage.local-backed cache of the top-tokens list, keyed per network with the same 30-min staleness window as the Redux cache. The idle path now layers Redux (in-session) → disk (cross-session) → network, so reopening the extension paints Popular from disk instead of re-fetching. Best-effort: storage errors degrade to a network fetch. Blockaid scans stay ephemeral (tamper risk), and persisted trending is still bulk-scanned + intersected with the verified set, so a tampered entry can't masquerade as verified or skip the scan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With top-tokens persisted to disk, repeat Swap entries already paint instantly, but the first entry per staleness window still pays the slow trending request. Pre-warming primes that cache from the home screen so even the first Swap entry is instant. Adds useSwapTopTokensPrewarm, mounted on the Account view. It defers past first paint, runs mainnet-only, and only fetches when the persisted cache is missing or stale — so it costs at most one trending request per 30-min window, not one per home render. On a fresh fetch it updates both the Redux and disk caches; all errors are swallowed (the Swap pipeline still fetches on open). The core prewarmTopTokens logic is extracted and unit-tested independently of the timer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lers The top-tokens disk cache was writing chrome.storage.local directly from the popup via dataStorageAccess — the only popup-side direct storage writer in the codebase. Every other cache (asset icons, domains, memo-required accounts, etc.) is owned by the background and reached from the popup via messages, so this aligns the swap cache with that convention. - New GET_CACHED_SWAP_TOP_TOKENS / CACHE_SWAP_TOP_TOKENS service types + message-request shapes (tokens kept opaque as unknown[] so @shared doesn't depend on the popup's trending-asset type). - Background handlers own the write (keyed per network under CACHED_SWAP_TOP_TOKENS_ID, stamping updatedAt), registered in popupMessageListener and mirroring the asset-icon cache. - @shared/api/internal wrappers send the messages; the popup helper now calls those and applies the 30-min staleness window, with no direct storage access. Behavior is unchanged — same per-network 30-min cache; only the access path now follows the established background-owned pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On first entry the sell card grabbed focus and showed a white "0" with a blinking cursor, while the bottom CTA correctly read the missing-token state but was disabled — a dead end. - The sell card no longer auto-focuses until both tokens are picked (the source defaults to XLM, so the card stays unfocused on entry) and shows a gray "0" placeholder (empty input) until an amount is entered; redux keeps the canonical "0". - The missing-token CTA is now enabled and acts as a shortcut to the picker for the missing side, preferring the sell token when both are missing. - Renames the CTA from "Select an asset" to "Select a token" — we're standardizing on "token" over "asset" in user-facing copy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The swap-from search matched balances by code, issuer, and contractId, so a pasted Stellar Asset Contract (SAC) address for a held classic/native token found nothing and fell through to the "Soroban contract tokens aren't supported" empty state — even though the destination picker resolves the same SAC correctly via stellar.expert. Add a SAC branch to the source filter: when the term is a contract id, derive each held token's SAC address (pure, no extra API call) and match. Extracted the per-balance predicate into matchesSwapFromSearch for unit testing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The picker list shows a warning badge on flagged tokens, but the badge vanished once a token was selected: the "You sell" / "You receive" AmountCards never received a securityLevel, so AssetIcon rendered without the ScamAssetIcon overlay. Pass the source token's Blockaid verdict and the destination's pick-time verdict (DestinationTokenDetails.securityLevel) to their cards so the badge persists through the amount screen, matching the picker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing in the destination picker switched the list into search mode immediately — hiding the idle "Popular" tokens — but the previous lookup's held tokens lingered in state through the 300ms debounce, so stale "Your tokens" stayed visible until the new results arrived. Track a search-pending flag from the keystroke until the lookup settles and render the loader during that window, so every result clears at once. The source filter is synchronous, so it's unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Rate row split its width 50/50, so the short "Rate" label wasted half the
row while a long rate ("1 ABC ≈ 0.0000001 XYZ") was squeezed into the other
half and clipped. Scope a --rate row modifier that sizes the label to its
content plus a 20px gap and lets the value fill the rest, and step the value's
font-size down by length so it stays uncropped — mirroring the swap amount
screen's font scale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When swapping to a token the account doesn't hold yet, the icon cache has no entry for it, so the submit screen resolved its destination icon to undefined and showed a broken image during "Swapping"/"Swapped!" — even though the picker and review screen rendered it fine. Fall back to the iconUrl carried on the picked token's details (the same source those screens use) so the icon is present throughout the whole swap flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The search-pending loader was cleared by an effect keyed on lookupState.state, but the idle in-memory cache dispatches FETCH_DATA_SUCCESS while the state is already SUCCESS (a SUCCESS→SUCCESS repaint). That transition never changes the enum value, so the effect didn't re-run and the loader stuck forever when typing or clearing the search box. Clear the flag off the lookup promise instead: await lookupFetchData in the formik submit and reset isSearchPending in a finally, guarded by a monotonic sequence so a superseded (debounced/aborted) lookup can't unblock a newer pending search. The brittle lookupState-value effect is removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…m picker The "Soroban contract tokens aren't supported" empty state only makes sense on the swap-to picker — you can only swap FROM a token you already hold. The shared SwapPickerSections had no notion of which side it was on, so pasting a contract id into the swap-from search showed the Soroban message instead of "No tokens match …". Thread an isDestination prop and gate the Soroban branch on it. Also wrap long pasted addresses in the empty card (overflow-wrap) so a 56-char address no longer bleeds past the screen edge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Once both tokens were picked, the CTA correctly read "Enter an amount" but was hard-disabled while the user had spendable balance, so there was no affordance to start typing. Mirror mobile: the enter state is enabled and tapping it focuses the sell amount input. A new availableBalanceIsZero guard keeps the CTA disabled with "Insufficient balance" when the spendable balance is zero, so the enable doesn't give a dead-end button to an unfunded account. AmountCard gains an optional amountInputRef so the parent can focus the input. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two glitches appeared on the first crypto→fiat toggle and self-healed after an
edit:
- The fiat input carried onFocus={e => e.target.select()} (the crypto input
did not), so the autofocus that accompanies the toggle highlighted the whole
sell amount. Removed it to match the crypto input.
- Each input's width is measured from a hidden mirror span, but the spans only
existed inside their own inputType branch — so the fiat width was still 0 on
the first toggle and the value was clipped to the fallback width for a frame.
Render both mirror spans unconditionally so the inactive width is measured
before the toggle.
Also size the read-only receive amount from its own value instead of reusing
the sell-derived font-size class, which could mis-size it on toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…icons
The review screen could stack up to three Blockaid banners (transaction scan +
source token + destination token). Collapse to exactly one, chosen by priority
to mirror mobile's useReviewSecuritySummary: the transaction-scan verdict
outranks the token verdict, and among tokens the worse level wins (the
destination breaks a tie). The merged verdict that drives the Confirm-anyway
gate and the expandable Blockaid pane is unchanged.
Also surface the warning badge over the review token icons — SendAsset and
SendDestination hardcoded isSuspicious={false}, so a flagged token showed no
badge unlike the picker and amount screen. They now receive each token's
verdict.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping the "This will add a trustline" banner opened the trustline info sheet on top of the still-painted review sheet, leaving the review summary visible as a ghost behind it. Gate the review MultiPaneSlider on !isOnTrustlinePane (the same guard the action buttons already use) so the review body is hidden while the sheet is up and restored when it closes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Quote has expired" message was a fixed banner pinned above the cards, permanently taking layout space. Fire it as a sonner toast instead (the same component the Discover flow uses) — it floats over the screen, auto-dismisses, and is swipe-dismissible. A stable toast id dedupes the in-screen and submit-recovery triggers into one toast. Detection is unchanged (op-level op_under_dest_min / op_too_few_offers). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match Figma node 8641-33470: add the lilac plus-circle badge and a circular
close button in the header, a token-specific body ("To receive {{tokenCode}},
your wallet needs a trustline on Stellar.") with the "Why do I need XLM?" link
inline, and a reserve card showing the XLM icon + "0.5 XLM required" and a
get-it-back explanation. The CTAs become a filled primary "Swap for 0.5 XLM"
and an outlined "Copy my wallet address" (no icon); the standalone help button
is gone.
The "Why do I need XLM?" link pointed at an empty URL (opened a blank tab);
point it at the same help article mobile uses. New copy added to en + pt with
parity coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The single-banner choice keyed on txSecurityLevel being non-null, but on mainnet an absent transaction scan yields UNABLE_TO_SCAN (non-null). So a swap to a token flagged malicious at pick time would show the soft "proceed with caution" tx banner and suppress the malicious-token warning — both a downgrade and a divergence from mobile, which ranks unable-to-scan lowest. Pick the banner by the full mobile cascade (tx malicious/suspicious > token malicious/suspicious > any unable-to-scan) so a flagged token always outranks a merely-unscannable transaction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The read-only receive card's inputType flips from the sell card's toggle without its own value changing, so a value that straddles a font-size bucket could keep a stale width and clip after toggling. Add the font-size class to the width-measuring effects' deps. 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.