Skip to content

feat(swap): Soroban tokens in the swap picker (C3) - #1048

Open
piyalbasu wants to merge 35 commits into
feat/swap-backend-txfrom
feat/swap-soroban-tokens
Open

piyalbasu wants to merge 35 commits into
feat/swap-backend-txfrom
feat/swap-soroban-tokens

Conversation

@piyalbasu

@piyalbasu piyalbasu commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What

The swap token picker now offers Soroban contract tokens (SEP-41) alongside classic assets. You can find them through search, trending, a pasted contract ID, or your balances, and swap them through LI.FI on mainnet. The Swap button on a Soroban token's details screen now works too.

  • Decimals: each token's decimals are respected end to end: spendable, Max, balance checks, the amount sold, and the minimum received. For a token you don't hold, decimals come from /token-details; if they can't be read, the pick is refused.
  • Row subtitle: a Soroban-only token shows a shortened contract ID, so a lookalike still looks different.
  • Trustline: a Soroban destination never triggers the classic trustline or reserve logic. The review accepts a trust() on a token bought by its contract ID, since the backend adds one for a SAC.
  • Off mainnet: Soroban tokens you don't hold are hidden.

Draft, for diff review. Stacked on C2 (feat/swap-backend-tx, which is stacked on C1, feat/swap-backend-quotes): C2 is #1050, C1 is #1049. Needs stellar/freighter-backend-v2#178 live first.

Why

Swap routing spec, client item C3: stellar/wallet-eng-monorepo#131.

Implementation details (for agents)

What changed

  • Picker (useSwapTokenLookup, SwapToScreen, useTrendingTokenDetail): Soroban records allowed. A held token is matched by contract ID (canonicalId), so the same-token checks work. A pasted contract ID is looked up for the active account. A failed lookup shows the "search unavailable" notice, and the notice follows each lookup's own outcome. A pick waiting on /token-details ignores repeat taps and drops late results, in the picker and the trending sheet (including after the sheet closes). A no-code token is named by its shortened contract ID.
  • Shared formatter: formatSorobanSearchRecord is shared with Add token, and each caller keeps its own icon behaviour.
  • Amounts: a held SEP-41 source's spendable amount, Max and balance checks use the token's decimals. The available label and "Maximum spendable" are formatted once, at the token's decimals.
  • throwOnFailure: opt-in, added to getTokenDetails and handleContractLookup so a failed request can be told apart from "not a token". It defaults off, so no other caller changes.
  • Icons: the picker's write to the shared icon cache is removed, and getIconUrl is back to its pre-C3 behaviour.
  • Token details: the Swap button shows for SEP-41 tokens.

Verification

  • Each fix has a test that was run against the code before the fix and failed. Mutations were applied as exact edits, undone, and caught.
  • Includes a build test where both sides have decimals other than 7 (6 → 18).
  • jest: 341 suites, ~4840 tests pass. tsc, eslint and prettier are clean on the changed files.
  • /code-review at high, then medium on each fix round.

Known follow-ups

  • Send's balance check compares against a SEP-41 balance's raw total. That's existing behaviour.
  • The review's rate shows 0 for very low-value pairs, because of toFixed(7).
  • freighter-backend's /token-details answers 400 for every failure, so a lookup outage reads as "not a token" and a pasted ID shows "No tokens match" instead of the unavailable notice (both platforms). Needs 404 for not-a-token and 5xx for upstream failures.
  • Trending "Proceed anyway" on a slow decimals read has no pending state; closing another trending sheet meanwhile drops the pick silently.

Screenshots

pr1048-soroban-picker-quote-review

iOS simulator, mainnet, a local freighter-backend-v2 (stellar/freighter-backend-v2#178) with LI.FI enabled. Pasting deJTRSY's contract ID lists it with its shortened contract ID; LI.FI quotes 10 XLM → 1.4189969 deJTRSY; the review is decoded from the built Soroban transaction (not submitted).

🤖 Generated with Claude Code

piyalbasu and others added 27 commits November 9, 2026 12:11
decodeSwapTx accepted only XLM and CODE:ISSUER and read every authorized
transfer at 7 decimals. A `C…` wire asset is now its own contract, and the
amount sold is read at the sold token's decimals, passed in from the source
balance. A classic path payment still needs classic assets on both sides,
and a trust() on a Soroban-only token bought is refused: it opens no
trustline, so review must not show the reserve notice for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cimals

findSwapPath refused any contract token before asking the backend, which
now quotes them through LI.FI (and answers "no route" where no source can).
A Soroban token goes on the wire as its contract ID, and the minOut sent to
/swap/tx is rounded down to the bought token's decimals: the backend
rejects a minOut with more fraction digits than the token has, so the old
fixed 7 dp broke tokens with fewer decimals and truncated those with more.
The destination shim carries a Soroban descriptor's contract ID and
decimals for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parseDisplayNumber parsed through parseFloat, so an amount with more than
~15 significant digits (any 18-decimal Soroban token amount with a whole
part) lost its tail: typing 1234.123456789012345678 stored
1234.1234567890124. It now parses the normalized string with BigNumber and
falls back to parseFloat only for input BigNumber can't read.

Callers checked: the token-fiat converter (reducer SET_TOKEN_DISPLAY_FROM_TEXT,
SET_FIAT_DISPLAY_FROM_TEXT, SET_DISPLAY_AMOUNT_FROM_TEXT), the transaction
settings sheet and the slippage/fee/timeout validators. For inputs that fit
a double, BigNumber#toString matches Number#toString (checked against
"", "1.", ".5", "0.0000001", "1e5", "-1", "007", "1.50", "-0", "0."); only
inputs past double precision change, and they now keep their digits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25/50/75%/Max formatted the amount at 7 dp. For an 18-decimal Soroban
balance of 1.234567891234567891, Max set 1.2345679, above the balance; for
a token with fewer than 7 decimals it sent more fraction digits than the
backend accepts. They now format at the source balance's own decimals, as
Send's percentage buttons already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The picker dropped Soroban contract tokens from every source. It now keeps
them, from the same sources:
- held Soroban balances, in "Your tokens" (both pickers) and in held
  search matches, with their decimals; a held token also matches its
  exact contract ID;
- stellar.expert search and trending records whose asset is a contract
  ID, formatted with the contract ID as issuer, CUSTOM_TOKEN type and
  stellar.expert's decimals (trending still requires the verified lists,
  which key contract tokens by contract ID);
- a pasted contract ID, now looked up through freighter-backend
  (handleContractLookup, as Add token does) instead of stellar.expert.

Blockaid scans them through the existing bulk scan (SYMBOL-C… ids, which
/scan-asset-bulk accepts). A Soroban destination never requires a
trustline, so the reserve deduction and reserve sheet stay classic-only.
The "Soroban tokens aren't supported" empty state is gone; a contract ID
nothing resolves shows the normal no-results line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ubtitle

Picker and trending rows showed the domain, then the name, for every
non-held token. A Soroban-only token now shows its contract ID shortened to
the first and last four characters (e.g. CBI7…IHRV), per the design doc's
row-subtitle rule, so a token that copies another's symbol and domain still
looks different. Classic rows keep the issuer's home domain.

The row's context menu also gets the contract ID, so "View on
stellar.expert" opens /contract/C… instead of an /asset/CODE-C… page that
doesn't exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns up in the asset lists

Icons follow the design doc's order: the user's asset lists, then the
issuer's stellar.toml, with the first hit cached so the token looks the
same across the wallet.

- getIconUrl read a Soroban token's contract ID (which icon-store keys put
  in the issuer slot) as a G… issuer, so the asset-list lookup, which
  matches contract tokens by contract ID, never hit, and the toml lookup
  rejected the key. It now passes such an issuer as the contract ID.
- A non-held picker or trending row now caches its icon if the store has
  none (the asset lists' icons are cached up front): stellar.expert's copy
  of the issuer's toml image when the record has one, else fetchIconUrl,
  which tries the lists and then the toml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The no-route message still said "No path found between these tokens", a
Horizon path-finding term. The design doc's wording is "no quote
available", which also covers a Soroban pair that classic can't serve
(unsupported) and LI.FI has no route for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ole tokens

A SorobanBalance's total/available are in the token's smallest unit, but
the swap compared them to whole-token amounts: spendable (so Max and the
percentage buttons) was the raw integer, the insufficient-balance check
compared a whole-token amount against raw units and never tripped, and
validateSwapTransactionParams did the same. Spendable is now scaled by the
token's decimals, the spendable check scales the amount to raw units, and
the build-time check uses getBalanceInWholeUnits. Classic balances are
unchanged.

The Max test's fixture stored a whole-token total on a Soroban balance,
which is why it passed; it now stores the raw integer.

Sibling left alone: Send's validateTransactionParams compares amount with
balance.total the same way; Send behaviour is out of scope here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the search-unavailable notice

The picker's contract-ID lookup sent an empty pub_key to /token-details,
so it always came back empty and the picker said "No tokens match". The
hook now takes the active account's public key from SwapToScreen and
passes it to handleContractLookup.

The lookup branch also cleared stellarExpertDown, so a failed lookup hid
the "search unavailable" notice. The lookup doesn't involve stellar.expert,
so it no longer touches that flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
formatSearchRecord's Soroban branch copied useTokenLookup's Path 2. Both
now call helpers/formatSorobanSearchRecord, which leaves the icon to the
caller: Add token keeps the icon cache first and then tomlInfo.image, and
swap keeps tomlInfo.image (plus price). No behaviour change; the existing
useTokenLookup contract-record tests cover the Add token side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row subtitle built its own CBI7…IHRV; it now uses the existing
truncateAddress helper (CBI7...IHRV), as the rest of the app does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adcb113 had non-held picker rows write their icon into the shared token
icon store (cacheIconIfMissing). By decision, the picker shows a row's icon
through the existing lookup (store first, then the record's stellar.toml
image) but never writes to the shared cache. The row effect and the store
action, used only by it, are removed; tokenIcons.ts is back to its state
before C3. getIconUrl's contract-ID handling from that commit stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LI.FI quotes mainnet only, so off mainnet a Soroban token the account
doesn't hold could only ever answer "No quote available". Search and
trending now drop such records when the network isn't mainnet; held
Soroban tokens still list everywhere. The filter joins the hidden-token
filter applied to the hook's outputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ails, or refuse it

A Soroban search or trending record can come without decimals
(stellar.expert omits them sometimes), and descriptorFromSearchRecord then
assumed 7, so minOut could be rounded at the wrong precision and a token
with fewer decimals failed at build. Picking such a token from the picker or
from trending now reads its decimals through /token-details for the active
account first. If they stay unknown the pick is refused with a "Couldn't
read this token's details" toast and a logger.error (Sentry).
descriptorFromSearchRecord no longer falls back to 7 for a Soroban record:
it throws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch token

Covers a build where neither side has 7 decimals: the request carries the
amount sold at 6 dp and minOut rounded down at 18 dp, and the decoded
amount sold is read at the sold token's 6 decimals. Mutation-checked:
hardcoding 7 for the decoder's fromDecimals reads 0.1123456; hardcoding 7
for minOut sends 2.1022222.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…twice

e5a722f made spendableAmount whole tokens for a Soroban source, but the
Sell card's "available" line passes it to formatBalanceAmount, which scales
a Soroban balance's amount from raw units again: 100 of a 6-decimal token
read "0.0001 TKN available". buildSourceBalanceRight now hands
formatBalanceAmount the raw-unit amount it expects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s decimals

The message capped spendable at 7 decimal places, so a small 18-decimal
balance (0.000000000001) read "Maximum spendable: 0". It now formats at the
source balance's own decimals; classic sources are still 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adcb113 made getIconUrl read a C… issuer as a contract ID. getIconUrl is
shared by the token-icon batch refresh and fetchIconUrl, so that changed
icons outside the swap (Add token, balances). The picker doesn't need it:
asset-list icons for contract tokens are cached up front under
CODE:CONTRACT, which is the key the picker row reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… never stellar.expert

withTokenDecimals skipped /token-details whenever a record already had
decimals, and a stellar.expert record's decimals can come from the issuer's
stellar.toml. An issuer could then set the precision minOut is rounded to:
too many places and every build is rejected, too few and minOut is coarser
than it should be. For a Soroban token the account doesn't hold the pick
now always reads decimals through /token-details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
handleRecordPress waits on /token-details for an unheld Soroban token
before it dismisses the picker. Nothing stopped a second tap, a different
pick or closing the picker in the meantime, so a late result could call
goBack again (popping the amount screen) or overwrite a newer pick. A
repeat tap on the pending row is now ignored, and the result applies only
if no other row was picked and the picker is still open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trending "Swap to" and "Confirm anyway" actions applied the
/token-details result whenever it returned, with the source captured at the
press. A pick made in the picker meanwhile was overwritten, and the
same-token check ran against a stale source. The result (and its error
toast) now applies only if the swap's source and destination are what they
were at the press.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The picker matched records to held balances by CODE:ISSUER, but a Soroban
balance id uses the contract's on-chain symbol and a stellar.expert record
uses stellar.expert's code, which can differ. Such a held token then read
as not held: hidden off mainnet, and on mainnet listed as a new token whose
descriptor id (ABC:C…) matched no balance. Verified reachable before the
fix: with the source SYM:C… held, picking the ABC:C… row set the
destination to ABC:C… without clearing the source, and
validateSwapTransactionParams' id check passed, so the token could be
swapped into itself.

canonicalId now keys a Soroban token by its contract ID (held set, hidden
set, dedupe), and the picker and trending map a record to the held balance
with the same contract ID, so the pick uses the balance id and the
same-token checks apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The details screen's Swap button was gated to classic assets, with a
comment saying the swap flow didn't support Soroban tokens. Now that it
does, the button shows for every token except liquidity-pool shares,
matching the extension's user-approved decision (freighter 1f3685dbd).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not "No tokens match"

getTokenDetails answers null both for a contract that isn't a token (400)
and for a failed request (timeout, 5xx), so the picker showed a valid
contract ID as not found when /token-details was down. getTokenDetails and
handleContractLookup take an opt-in throwOnFailure (default off, so every
other caller is unchanged); the swap picker passes it and turns a failure
into the "search unavailable" notice. While that notice shows, the picker
no longer also says "No tokens match", which applies to the stellar.expert
outage path too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… both ways

90c6d76 only ever turned the notice on from the contract-ID branch, so a
notice left by an earlier failed lookup, or by a stellar.expert or trending
outage, stayed up after a lookup that answered. With "No tokens match"
hidden while the notice shows, a non-token paste then read as "search
unavailable", and a later successful paste kept the notice above its
result. The branch now sets the notice from its own outcome.

The earlier "keeps the notice when a lookup fails" test modelled a failure
as a null answer, which since 90c6d76 means "not a token"; it now rejects,
as a failed request does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-fe2dbee7871a0222db3b
Backend: sandbox (piyalbasu). SDF collaborators only — install instructions in the release description.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-3bd4644370bbb47c66cd
Backend: sandbox (piyalbasu). SDF collaborators only — install instructions in the release description.

@piyalbasu
piyalbasu marked this pull request as ready for review November 11, 2026 22:38
The decoder refused a trust() whenever the bought token was given as a
contract ID, on the reasoning that a Soroban-only token has no trustline.
A SAC held by its contract ID does, and the backend adds the trust() for
it: its toClassic (lifi.go) reads the contract instance to tell a SAC
from any other contract before building. The extension accepts the same
transaction; mobile blocked it with "didn't match your request".

Drift review on #1048 / #3073.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
piyalbasu and others added 2 commits November 11, 2026 19:43
A stellar.expert record with neither a code nor a toml code was named
by its full 56-character contract ID in the swap picker, which
overflowed the row, repeated the shortened-ID subtitle, and went out as
the asset_code analytics property. The extension names the same token by
the shortened ID; the two platforms now agree.

Applied in the swap picker's formatter only: Add token's lookup shares
formatSorobanSearchRecord and keeps the full ID as a custom token's code
(pinned by its own test).

Drift review on #1048 / #3073.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An active async-selection issue can let a late token-details response select a token after the picker is dismissed or changed, while repeated taps may also trigger duplicate requests.

1 open issue
What changed in this PR

Adds SEP-41 Soroban token support to the swap picker and transaction flow, including contract-ID lookup, network-specific visibility, decimal-aware balances and transaction validation, and Soroban token details actions.

Changes:

  • Supports Soroban tokens across search, trending, held balances, and pasted contract-ID lookup.
  • Propagates token decimals through spendable amounts, balance validation, quote construction, transaction decoding, and minimum-output calculation.
  • Refactors shared token formatting and updates swap UI, translations, and comprehensive tests.
File Description
src/​services/​transactionService.ts Validates swap balances in whole-token units.
src/​services/​backend.ts Adds optional failure propagation for token-detail lookups.
src/​i18n/​locales/​pt/​translations.json Updates Portuguese swap messages.
src/​i18n/​locales/​en/​translations.json Updates English swap messages.
src/​hooks/​useTokenLookup.ts Reuses Soroban search-record formatting.
src/​helpers/​formatSorobanSearchRecord.ts Adds shared Soroban search-record formatting.
src/​helpers/​formatAmount.ts Preserves high-precision input and exposes token decimal helpers.
src/​ducks/​transactionBuilder.ts Supplies source-token decimals while decoding swap transactions.
src/​ducks/​swap.ts Supports Soroban wire assets and decimal-aware minimum output.
src/​config/​types.ts Extends token-detail request parameters with failure behavior.
src/​components/​screens/​TokenDetailsScreen/​TokenDetailsScreen.tsx Enables Swap for Soroban tokens.
src/​components/​screens/​SwapScreen/​screens/​SwapToScreen.tsx Handles asynchronous Soroban selection and decimal lookup.
src/​components/​screens/​SwapScreen/​screens/​SwapAmountScreen.tsx Applies token-specific decimals to spendable amounts and Max.
src/​components/​screens/​SwapScreen/​hooks/​useTrendingTokenDetail.ts Resolves decimals for trending Soroban selections.
src/​components/​screens/​SwapScreen/​hooks/​useSwapTransaction.ts Builds swaps with destination-token precision.
src/​components/​screens/​SwapScreen/​hooks/​useSwapTokenLookup.ts Lists and filters Soroban tokens in swap search and trending results.
src/​components/​screens/​SwapScreen/​hooks/​useSwapToEmptyStates.ts Simplifies swap picker empty states.
src/​components/​screens/​SwapScreen/​hooks/​useSwapAmountError.ts Performs decimal-aware source balance checks.
src/​components/​screens/​SwapScreen/​helpers/​types.ts Documents Soroban destination descriptor fields.
src/​components/​screens/​SwapScreen/​helpers/​tokenDecimals.ts Resolves missing Soroban decimals through the backend.
src/​components/​screens/​SwapScreen/​helpers/​swapDisplayStrings.ts Formats Soroban spendable balances correctly.
src/​components/​screens/​SwapScreen/​helpers/​recordPredicates.ts Defines swappable classic and Soroban records.
src/​components/​screens/​SwapScreen/​helpers/​index.ts Exports the new swap helpers.
src/​components/​screens/​SwapScreen/​helpers/​formatSearchRecord.ts Formats both classic and Soroban search records.
src/​components/​screens/​SwapScreen/​helpers/​formatClassicRecord.ts Replaces the classic-only search-record formatter.
src/​components/​screens/​SwapScreen/​helpers/​descriptors.ts Builds Soroban descriptors without trustline requirements.
src/​components/​screens/​SwapScreen/​helpers/​decodeSwapTx.ts Decodes Soroban swaps using source-token decimals.
src/​components/​screens/​SwapScreen/​helpers/​computeTrendingIntersection.ts Includes verified Soroban records in trending results.
src/​components/​screens/​SwapScreen/​helpers/​canonicalId.ts Canonicalizes Soroban records by contract ID.
src/​components/​screens/​SwapScreen/​components/​SwapTokenRow.tsx Displays shortened Soroban contract identifiers.
tests/​services/​transactionService.test.ts Tests decimal-aware swap balance validation.
tests/​services/​backend.test.ts Tests propagated token lookup failures.
tests/​hooks/​useTokenFiatConverter.reducer.test.ts Tests preservation of high-precision token input.
tests/​helpers/​formatAmount.test.ts Tests high-precision display parsing.
tests/​ducks/​transactionBuilder.test.ts Tests decimal-aware Soroban transaction decoding.
tests/​ducks/​swap.test.ts Tests Soroban descriptor path projections.
tests/​ducks/​swap.swapMinOut.test.ts Tests decimal-aware minimum output rounding.
tests/​ducks/​swap.findSwapPath.test.ts Tests Soroban quote requests and unavailable routes.
tests/​components/​screens/​TokenDetailsScreen/​TokenDetailsScreen.test.tsx Tests Swap availability for Soroban and LP tokens.
tests/​components/​screens/​SwapScreen/​useSwapTransaction.test.ts Tests Soroban wire assets and precision.
tests/​components/​screens/​SwapScreen/​SwapToScreen.test.tsx Tests lookup, selection races, and decimal failures.
tests/​components/​screens/​SwapScreen/​SwapAmountScreen.test.tsx Tests Soroban Max amount handling.
tests/​components/​screens/​SwapScreen/​hooks/​useTrendingTokenDetail.test.ts Tests trending Soroban selection and stale results.
tests/​components/​screens/​SwapScreen/​hooks/​useSwapTokenLookup.test.ts Tests Soroban search, verification, filtering, and lookup states.
tests/​components/​screens/​SwapScreen/​hooks/​useSwapAmountError.test.ts Tests decimal-aware amount validation and messaging.
tests/​components/​screens/​SwapScreen/​helpers/​tokenDecimals.test.ts Tests backend decimal resolution and refusal behavior.
tests/​components/​screens/​SwapScreen/​helpers/​swapDisplayStrings.test.ts Tests whole-token spendable display formatting.
tests/​components/​screens/​SwapScreen/​helpers/​descriptors.test.ts Tests Soroban descriptor construction.
tests/​components/​screens/​SwapScreen/​helpers/​decodeSwapTx.test.ts Tests Soroban transaction decoding and mismatch handling.
tests/​components/​screens/​SwapScreen/​helpers/​computeTrendingIntersection.test.ts Tests verified Soroban trending intersections.
tests/​components/​screens/​SwapScreen/​helpers/​canonicalId.test.ts Tests contract-ID canonicalization.
tests/​components/​screens/​SwapScreen/​components/​SwapTokenRow.test.tsx Tests Soroban subtitles and context-menu data.

Review effort: Lite

piyalbasu and others added 2 commits November 11, 2026 19:52
…he sheet closes

The trending detail sheet guarded a late /token-details result only by
comparing the swap store with a snapshot taken before the request.
Closing the sheet, or confirming again, changes nothing in that
snapshot, so a late result still selected the token after the user had
dismissed the sheet, and repeated confirms issued duplicate reads.

The hook now tracks the pending pick the way SwapToScreen does: each
confirm takes a pick id, closing the sheet invalidates it, and a confirm
for the token already pending is ignored. The store comparison stays,
since a pick made through the regular picker doesn't go through this
hook.

Copilot review on #1048.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The no-code fallback used ??, so a record with code "" kept an empty
name; the extension's fallback uses ||. The two now take the same
branch. The shortened ID still renders with each platform's own
separator (CCW6...MI75 here, CCW6…MI75 on the extension), from the
shared truncate helpers each app uses everywhere.

Medium review on the C3 fix round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants