Skip to content

Swap to New Token#2872

Merged
CassioMG merged 212 commits into
masterfrom
feature/swap-to-new-token
Jul 2, 2026
Merged

Swap to New Token#2872
CassioMG merged 212 commits into
masterfrom
feature/swap-to-new-token

Conversation

@CassioMG

@CassioMG CassioMG commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables swapping from a held token to any Stellar classic asset in a single flow — token discovery (held + popular + free-text search), Blockaid coverage on non-held destinations, and an atomic changeTrust + pathPaymentStrictSend when a trustline is needed.

This is the browser-extension counterpart to the mobile feature. See the reference PR and its design doc:

Scope: 189 files changed, ~16.3k LOC across the branch:

  • src/ (+ @shared) — ~7.7k lines (47%)
  • tests (unit + Playwright E2E) — ~7.7k lines (47%)
  • design doc — ~730 lines (4%)
  • i18n (en + pt) — ~130 lines (1%)

Most of the large diffs are net-new files (new picker sections, new hooks, new sheets) plus tests written alongside them, and shared UI components that Send and the account-home balances list now reuse.


What's new

  • useSwapTokenLookup — owns destination-token discovery: held balances, popular tokens (volume7d ∩ verified lists), and free-text search via stellar.expert. Closes a security gap: every non-held destination is Blockaid-scanned before it can be selected.
  • SwapAsset picker, parameterized by selectionType — one picker drives both the Sell and Receive sides, replacing the old single-list picker.
  • SwapPickerSections — the labeled sections (Your tokens / Popular / Verified / Unverified), the (i) info sheets, and the empty / Soroban / fallback states.
  • SwapAmount rewrite — built on the new shared AmountCard + PercentageButtons, with a Sell/Receive layout, a swap-direction chevron, crypto⇄fiat toggle, and a debounced live "You receive" quote that updates as you type.
  • Atomic transactionbuildChangeTrustOperation is extracted and prepended so a new-token swap is a single changeTrust + pathPaymentStrictSend XDR (fee split total-across-ops); Blockaid scans the combined XDR for defence in depth.
  • XlmReserveSheet — a pre-flight when the account lacks XLM headroom for the new 0.5 XLM trustline reserve; offers a "Swap for 0.5 XLM" affordance, a copy-address fallback, and a "why do I need XLM?" link.
  • Trustline banner + info sheet + rate row on the review screen — explains the trustline reserve up-front before the user confirms.
  • Transaction details sheet (Swap + Send) — the shared review screen gains an expandable, operation-by-operation breakdown (amounts, network fee, memo, per-op fields), now surfaced in both the Swap and Send review flows.
  • Unified Blockaid coverage — a single reusable BlockaidBanner across Add-a-token, Sign Transaction, Sign Message, ChangeTrust, Grant Access, and Swap; verdict-colored token badges (amber = suspicious, red = malicious); a scan-on-select recovery path so a token picked before its verdict lands still gets assessed.
  • Quote-expiry recovery — Horizon op_under_dest_min / op_too_few_offers at submit are classified and surfaced as a toast, and the flow recovers to the amount screen with a fresh quote instead of dead-ending.
  • Shared UI componentsAmountCard, PercentageButtons, AssetListRow, and BalanceRow are now shared by Send, Swap, and the account-home balances list, removing duplicated markup.

Search results strategy

When the user types a search term, the picker renders three labeled, mutually-exclusive sections — chosen over a flat "Results" list so users can visually distinguish levels of trust:

  1. Your tokens — held tokens matching the term.
  2. Verified — verified-list tokens. The section header has an (i) icon opening a sheet explaining what the verified-token lists are.
  3. Unverified — remaining stellar.expert matches. Its header has its own (i) icon for the unverified caveat.

G… issuer addresses surface the assets that account issues via stellar.expert's fuzzy ?search=. C… contract pastes fall under a Soroban-specific empty state explaining the flow is classic-only for now — Soroban contract tokens are filtered out at every stage.


Caching & pre-warm

Popular ("top tokens") lists are cached in two layers, keyed per network:

  1. Persistent disk cachechrome.storage.local (background-owned; the popup messages the background, matching every other cache in the codebase), with a 30-minute staleness window. Because it survives popup close, reopening the extension paints Popular straight from disk with no spinner.
  2. In-memory Redux cache slice — the session working copy, read first on each open.

On open the picker reads Redux → disk → network in freshness order, using whichever layer is still fresh. Search/discover results repaint instantly from the last cached result and revalidate silently in the background (SWR-style), so re-typing a term never flashes a spinner.

Pre-warm. useSwapTopTokensPrewarm mounts on the account/home screen and — deferred ~1s past first paint, mainnet-only, aborting on unmount — populates both cache layers when the persisted entry is stale or absent. So by the time the user opens Swap the Popular list is usually already warm (the analog of mobile's tab-mount pre-warm). It costs at most one trending request per staleness window.

Blockaid asset scans are deliberately not persisted. Unlike Popular tokens, per-token Blockaid verdicts stay in an in-memory (module-scoped) cache and are re-scanned each session. Persisting verdicts to disk would carry a cache-poisoning risk — a tampered entry could mark a malicious token "safe" — so we keep them in memory only.

Defence in depth at review. The scan caches are a first-paint UX optimization. The review step still issues a fresh scanTransaction against the built XDR every time, so transaction-level signals are always evaluated live per confirmation.


Graceful fallback (stellar.expert unreachable)

Held-to-held swaps keep working even when stellar.expert is down:

  • Popular tokens section — omitted.
  • Search — degrades to held-only in-memory matches.
  • Soft inline notice — "Token discovery is temporarily unavailable" is rendered at the top of the picker; non-blocking.

Path-finding is unaffected, since it uses Horizon strict-send paths, not stellar.expert.


Notes for reviewers

  • State lives in the Redux transactionSubmission slice (destinationTokenDetails, best-path, cache slots), not a separate store.
  • Overlays use the existing SlideupModal / LoadingBackground pattern rather than native bottom sheets; toasts use sonner.
  • SwapAmount business logic is split into focused hooks + helpers (useSwapLiveQuote, useSwapQuoteExpiry, useSwapDestinationScan, getSwapDerivedData, …), mirroring the mobile SwapScreen decomposition, so the component stays presentation-focused.

Telemetry

Eight new events instrument the flow (screen-view events like swap: amount already existed on master):

Event Fires when Key properties
swap: picker opened The Sell/Receive picker is opened side (source/destination), source (cta/dropdown)
swap: source selected A sell token is picked tokenCode, tokenIssuer, source
swap: destination selected A receive token is picked tokenCode, tokenIssuer, requiresTrustline, source
swap: direction toggled The user flips sell ⇄ receive
swap: xlm reserve shown The XLM-reserve pre-flight sheet is shown
swap: quote expired A quote expires at submit (Horizon op_under_dest_min / op_too_few_offers) sourceToken, destToken, sourceAmount, destAmount, allowedSlippage, resultCode
swap: trustline added A new-token swap settles on-chain tokenCode, tokenIssuer
swap: success The swap settles on-chain sourceToken, destToken, sourceAmount, destAmount, allowedSlippage

swap: trustline added and swap: success fire post-confirmation — once the transaction actually settles, not at review time — mirroring mobile.


Shared Blockaid banner

Now we have a single reusable BlockaidBanner component that renders in every flow that surfaces a security verdict — SAFE renders nothing, suspicious = amber, malicious = red — replacing the old per-flow warning components. The screenshot below covers all of them: Grant access / connect, Add a token, Sign message, Sign transaction, Send review, and Swap review.

Screenshot 2026-07-01 at 14 20 41

Transaction details sheet

The shared transaction-details sheet (operation-by-operation breakdown), is now available in both the Swap and Send review flows and aligned with mobile UI.

Screenshot 2026-07-01 at 14 29 23

Testing

Manually exercised in Chrome on the popup and sidebar. Full unit suite passing locally; the Playwright E2E swap spec was updated for the new flow (12/12), and the broader Blockaid / add-token / send E2E specs were updated to the new shared components and pass.

Videos

Swapping to a new token

swap-to-new-token-hh.mov

Swapping to a held token

swap-to-held-token-hh.mov

Swapping with not enough XLM for reserves

swap-xlm-reserve-sheet.mov

Using the search input

swap-using-the-search-box.mov

Quote expiration warning + retry

swap-quote-expired-retry.mov

Swapping to/from a token with no price

swap-with-no-prices.mov

Blockaid warning

swap-blockaid-warning.mov

Graceful degradation when stellar.expert is down

swap-stellar-expert-down.mov

Amount and path errors

swap-amount-and-path-errors.mov

CassioMG and others added 30 commits June 23, 2026 22:25
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>
…eout

The swap picker's first paint is gated on fully-stubbed responses
(account-balances, paths, top50), so it resolves well under Playwright's
default 5s expect timeout. The explicit 15s/10s waits were over-provisioned.

Verified at retries=0: swap.test.ts (12/12), sendPayment.test.ts (37/37), and
the four blockaid swap tests (12/12 across 3 repeats).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread extension/src/popup/components/amount/AmountCard/index.tsx Outdated
@CassioMG

CassioMG commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Code-wise, this is looking pretty good! I just left a couple of small comments plus a comment from Claude. I'm still doing some additional manual testing

Something I'm noticing - it seems like there's a few different refactors that are being folded into this PR:

  1. Blockaid UX
  2. SEP-42 validation
  3. Webpack updates
  4. Updating inputWidthContext
    plus some others

I appreciate that this work is getting done, but I'm wondering if some of this could have been separate PR's to make this diff a little smaller and the review process a little easier

@piyalbasu thanks for reviewing! Yeah, you are right that those could be separate PRs, I will keep that in mind for a next big feature like this so we ship it in smaller pieces 🚢

Comment thread extension/src/popup/components/swap/SwapAsset/hooks/useSwapTokenLookup.ts Outdated
Comment thread extension/src/popup/components/swap/SwapAmount/index.tsx Outdated
The two hidden mirror spans (used to measure the amount input's content
width) carried identical static inline styles. Extract them into a
`&__hidden-mirror` class, appended to the existing font class so width
measurement is unaffected, and keep visibility:hidden so offsetWidth stays
measurable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@piyalbasu

Copy link
Copy Markdown
Contributor

1 small thing I noted, the asset list schema is being loaded 3 times when I click an asset to Swap To (assuming this is loading once for every asset list). Not a huge deal because the browser is smart enough to load it from disk cache rather than doing a roundtrip, but something worth noting:
Screenshot 2026-07-02 at 12 40 05 PM

CassioMG and others added 5 commits July 2, 2026 15:00
Send and Swap each defined a byte-identical AVAILABLE_BALANCE_FONT_SIZES, and
the swap rate row hand-rolled the same length-stepped lookup. Extract a shared
popup/components/amount/fontScale module: a generic fitFontSizePx(value, scale)
helper plus the single AVAILABLE_BALANCE_FONT_SIZES scale and its
getAvailableBalanceFontSizePx accessor. Send, Swap, and SwapRateRow now import
from it; SwapRateRow keeps its own (distinct) scale but shares the lookup.

Behavior-preserving. Added fontScale tests; tsc clean; swap/send unit suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
priceValueUsd and dstPriceValueUsd computed the same
formatAmount(roundUsdValue(price * cleanAmount(amount))) expression. Extract a
single formatUsdValue(price, amount) helper for both and drop the no-op
template-literal wrapper that was on priceValueUsd.

Behavior-preserving; tsc clean, SwapAmount unit suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The swap and add-token lookups each defined their own MAX_ASSETS_TO_SCAN = 10
(the /scan-asset-bulk batch size). Export it once from popup/helpers/blockaid
next to scanAssetBulk and import it in both hooks so the batch size has a single
source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The canonical zero amount ("0") and zero USD amount ("0.00") were repeated
throughout SwapAmount. Introduce DEFAULT_AMOUNT / DEFAULT_AMOUNT_USD constants
and use them for the amount/amountUsd occurrences. Scoped to this file's amount
literals; the fee/balance/display-string zeros in the helpers are left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
schemaValidatedAssetList is called once per enabled asset list, and each call
re-fetched the identical SEP-0042 schema (3 fetches when opening the Swap-to
list or searching, once per list). The browser HTTP-cached it, but the requests
were still issued every time.

Fetch the schema once per session and reuse it; a rejected fetch is not cached
so a transient failure still retries. Added a test asserting a single fetch
across multiple validations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CassioMG

CassioMG commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

1 small thing I noted, the asset list schema is being loaded 3 times when I click an asset to Swap To (assuming this is loading once for every asset list). Not a huge deal because the browser is smart enough to load it from disk cache rather than doing a roundtrip, but something worth noting: Screenshot 2026-07-02 at 12 40 05 PM

@piyalbasu good catch, and exactly right — schemaValidatedAssetList runs once per enabled asset list, and each call re-fetched the same SEP-0042 schema (3 lists → 3 requests, disk-cached by the browser but still issued every time).

Fixed in c40fd69: the schema is now fetched once and memoized, so all list validations in a session reuse it — a single request instead of one per list (and it's shared across the Swap-to / search / Add-a-token flows too, not just deduped per open). A failed fetch isn't cached, so a transient error still retries on the next attempt.

The percentage buttons compute a fraction of the crypto available balance, but
in fiat mode the result was stored as a rounded USD amount and the swap
committed amountUsd / price — so 50% in fiat mode resolved to a slightly
different crypto amount than 50% in crypto mode.

Make the crypto amount the committed source of truth in both display modes: the
percentage sets the crypto amount directly (the fiat field mirrors it, rounded,
for display), the fiat input keeps the crypto amount in sync as the user types,
and continue/validate use it. A tapped percentage now resolves to the same
crypto amount regardless of display mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CassioMG and others added 6 commits July 2, 2026 17:02
Tapping Review ran a second horizonGetBestPath and overwrote the displayed
destinationAmount, so the receive amount ticked slightly right as the user
confirmed. Build the review transaction from the path + destination amount the
live quote already surfaced (in Redux), so the reviewed amount matches exactly
what was on screen. Drift by submit time stays bounded by destMin (slippage)
and the quote-expiry recovery; fall back to a fresh path lookup only when no
live quote exists yet.

Verified: swap E2E (review + submit + quote-expiry recovery) and SwapAmount
unit suites pass; tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In fiat mode the sell card's crypto line, the too-high check, and the live
'You receive' quote all recomputed the crypto amount as amountUsd / price. Since
the fiat field only shows dollars to the cent, that derived amount was slightly
smaller than the actual crypto amount — so after tapping Max in crypto mode and
switching to fiat, the crypto shown (and quoted) was a hair under the balance,
and tapping Max again in fiat couldn't fix it.

Use the crypto amount (already the committed source of truth, kept in sync by
the fiat input) directly for the sell display, the too-high check, and the live
quote. Removes the now-redundant priceValue. The receive quote and the reviewed
amount now match what's shown in either display mode.

Verified: swap E2E + SwapAmount unit suites pass; tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The amount screen (fiat mode) shows the entered dollars (amountUsd), while the
review computed USD from the crypto amount x price (priceValueUsd) — so the two
could disagree by ~1 cent (crypto-amount rounding or a price refresh between
setting the amount and reviewing). Pass the amount screen's displayed figure to
the review: amountUsd in fiat mode, priceValueUsd in crypto mode. The crypto
amount (the actual swap) was already identical on both screens.

Also tidied a now-stale comment (the pipeline reads the crypto amount, not
amountUsd, since the source-of-truth change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the swap fixes: the send amount screen tracked which field the user last
edited (editedInputType) and, in fiat mode, committed amountUsd / price and
displayed effectiveTokenAmount — so a tapped percentage or a typed fiat amount
resolved to a slightly different crypto amount than crypto mode, and the review
showed a different fiat figure than the amount screen.

Make the crypto amount authoritative in both display modes:
- Percentage buttons always set the crypto amount (the fiat field mirrors it,
  rounded, for display).
- The fiat input keeps the crypto amount in sync as the user types.
- Continue/validate/too-high and the fiat crypto-line all read the crypto
  amount directly; removed editedInputType / effectiveTokenAmount / priceValue.
- The review shows the same fiat figure as the amount screen (entered dollars
  in fiat mode, computed USD in crypto mode).

Verified: sendPayment E2E (37/37, incl. amount-preserved-across-toggle) and
SendAmount unit suites pass; tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The schema-fetch memoization made schemaValidatedAssetList reuse the first
fetched schema across the whole test file, so the fetch-fails and
validation-error cases in searchAsset.test.js got a stale cached schema instead
of their own mock. Clear the cache in beforeEach (as token-list.schema.test.ts
already does).

Verified: full test:ci passes (1342/1342).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a destination token was picked before its Blockaid scan landed and the
recovery scan then failed (transient error → scanAssetBulk returns null), the
hook bailed without dispatching, so securityLevel stayed undefined and the
review showed the token as clean with a normal confirm — the malicious/unable
warning never reached the user.

Drop the `|| !scan` early-return: a missing scan now flows through
getAssetSecurityLevel, which maps absent data to UNABLE_TO_SCAN on a
Blockaid-enabled network (matching the picker's unscanned-row handling), so the
review shows the 'couldn't be scanned' banner + 'Confirm anyway' gate. A
successful scan still flags malicious/suspicious; a superseded scan still
writes nothing.

Adds useSwapDestinationScan unit tests (fail-open, flagged, superseded).
Verified: test:ci passes (1345).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CassioMG

CassioMG commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author
  1. When switching to/from token units vs. dollars, the amounts change.
  2. The buy amount unexpectedly updates after I select "Review Swap".
  1. When I opened the token picker for the first time, for both the buy and sell tokens, there was a decently long loading state before rendering. Can we start loading this data ahead of time, so users don't have to wait to see the tokens they can pick?

@JakeUrban the points 1 & 4 above have been fixed on both Swap and Send flows, you should see stable crypto and fiat amounts now.

About the point 3, as I mentioned earlier we are already loading data ahead of time which makes it very hard to repro the long loading time on first Swap picker open. I had to delete and reinstall the extension several times to be able to repro it once, and I also had to immediately open the picker right after opening the pop-up to be able to repro it. Then on the rare times I was able to repro it it happened just on the very first rendering, after that the result is cached and you should always see the popular tokens rendering immediately on following renders.

So from my testing it appears that it is very unlikely that a regular user will see it unless they open the Swap picker blazing fast right after updating the app version for the first time, and, in case they see it, they will only see it once because the next time there should be a cache in place already.

I'm thinking we could release it like this and revisit it in case we start seeing long loading times happening more often. How does that sound?

Thanks!

The swap flow scans destination tokens via the /scan-asset-bulk endpoint, whose
response is { data: { results: { [assetId]: ... } } }. stubScanAssetSafe caught
that endpoint (via **/scan-asset**) but returned the single-asset shape, so the
recovery hook read results[id] as undefined. Previously that failed open
silently; now (correctly) it resolves to UNABLE_TO_SCAN, which made the 'Swap
shows no warning when scan confirms tokens are safe' test see a token warning.

Have the stub return the bulk shape (keyed by the requested asset_ids) for the
bulk endpoint and the single shape otherwise, so a safe scan resolves to SAFE.

Verified: blockaid swap E2E (safe/malicious/suspicious/unable) + swap.test.ts
pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CassioMG CassioMG force-pushed the feature/swap-to-new-token branch from 4aab554 to d568194 Compare July 2, 2026 23:16
Update the design doc to match what shipped: crypto amount as the single
source of truth in both display modes (§2.5), the fail-closed destination
verdict recovery (§3.9), the shared fontScale module (§3.3), the new
useSwapDestinationScan/fontScale test suites (§3.12), and corrected
facts/line-refs (priceValue→priceValueUsd, line count, SlideupModal refs,
MIN_TRENDING_VOLUME7D location, CONFIRM_AMOUNT step).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CassioMG CassioMG merged commit 7015e62 into master Jul 2, 2026
11 checks passed
@CassioMG CassioMG deleted the feature/swap-to-new-token branch July 2, 2026 23:46
@github-actions github-actions Bot mentioned this pull request Jul 7, 2026
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.

5 participants