Skip to content

feat(send): support muxed contract (W) destinations - #1036

Merged
CassioMG merged 40 commits into
mainfrom
cg-p30-send-w
Nov 7, 2026
Merged

CassioMG merged 40 commits into
mainfrom
cg-p30-send-w

Conversation

@CassioMG

@CassioMG CassioMG commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR

Users can now send native XLM and classic assets, such as USDC, to a muxed contract address (W…, CAP-84). The app sends it as a transfer on the asset's Stellar Asset Contract, to that exact W, and with no memo, because the mux id is already in the address. It is never sent as a classic payment. This matches the extension's stellar/freighter#3047.

As in stellar/freighter#3047, a custom token or a collectible can't go to a W. The amount and collectible screens show "Muxed address not supported", keep Review disabled and build nothing, and the transaction service refuses those sends as well. The memo is disabled and hidden for a W; a memo that still reaches the builder fails the build rather than being dropped. G and C sends behave as before.

An M that the token or collection can't take is now blocked on screen the same way, as in the extension. For a custom token or collectible whose contract doesn't support muxed addresses, the amount and collectible screens show the banner with the M explanation, keep Review disabled and build nothing. Before, they built the transaction and only the review sheet warned. Review stays disabled until the contract check answers.

Real W sends of XLM and USDC went through on testnet after its Protocol 30 upgrade on 2026-11-03 (see Verification). Simulating one needs freighter-backend#329, which so far is only on backend v1's staging. That environment is private (testing against it locally needs sshuttle), and backend v1 has no public staging, so #329 goes public when it's deployed to the production backend that release builds, PR previews and, by default, dev builds call. Until then, a debug-build-only "W Address Simulation Stub" lets the e2e flows and reviewers reach the review sheet.

Closes stellar/wallet-eng-monorepo#107

Known limitations

Changed lines

Files Added Removed Share of changed lines
Source code 24 832 141 23%
Tests 23 3,135 36 75%
Other 4 41 20 1%
Total 51 4,008 197

Source code is the app code under src/, translations included. It also counts the DEV-only simulation stub that the XLM-to-W e2e flow switches on. Tests are the unit tests, the e2e flows and the e2e runner script. Other is the e2e docs and .env.example.

Screenshots and Videos

Choosing a W recipient

Before (#1034, as on main) After
A W address rejected as an invalid Stellar address The W address offered as a recipient

XLM → W (USDC behaves the same)

Flow (time-lapse) Memo disabled Review Review rows: no memo
XLM to W flow Settings with the memo disabled Review send to the W Review rows with no memo row

Custom token or collectible → W: blocked

Flow (time-lapse) Custom token Explanation Collectible
Custom token to W blocked Banner and disabled Review for a custom token Muxed address not supported sheet Collectible to W blocked

Custom token or collectible → M without muxed support: now blocked on screen (before, only the review sheet warned)

Amount screen Collectible Dark mode Dark mode
TZT to an M: banner, Review disabled, the M explanation Collectible to an M blocked on iOS TZT to an M blocked on Android Collectible to an M blocked on Android
Implementation details (for agents)

Every link is a permalink at 70908faaff9077c7e4afdaf523ec7d25bd51243c, except item 10's, at 04e4863333c3c7659b5d59c0c718bde79a3eb09c. The branch has 16 commits of its own, and merges cg-p30-sdk-bump in to keep up with #1034.

Decisions (from the Protocol 30 design spec)

  • D5, parity with feat(send): support muxed contract (W) destinations freighter#3047: block a W when the asset is a custom token (a balance with a contractId) or a collectible; allow native XLM and classic assets, sent through their SAC. This replaces the issue's "SAC only" (isAssetSac) check.
  • D11, no remote flag: there is no feature flag.
  • D12, W-only: no change to how G/M/C memos fold, or to other G/M/C behaviour. The one exception came after the parity review: an M that the token or collection can't take is now blocked on screen (item 9).

What changed

  1. A W is a valid destination (1b6fba77).
  2. Building and simulating (ce6c27c4).
  3. No account lookups or unfunded warnings (67131a42).
  4. Settings and review sheets (d97c6ea6).
  5. The D5 block on screen (66dc3653).
  6. DEV-only simulation stub, e2e flows and test hooks (d66c24d6, 274b6aac, ee12e241, cce653d2, d2761943). See the sections below.
  7. Small screens (dde064cb, cbc0f46a). See "Small-screen fixes".
  8. Review follow-ups (0d11e57a): the collectible screen's warning sheet is readable by VoiceOver (see "Beyond the issue's scope"), plus tests, doc comments and a clearer internal error message for a custom token. No user-visible change otherwise.
  9. The M block on screen (1740fb16, 70908faa), for parity with feat(send): support muxed contract (W) destinations freighter#3047 after it merged.
    • Amount screen: isRecipientUnsupported covers an M whose token's contract doesn't support muxed addresses (isMuxedAddressWithoutMemoSupport, which the review sheet already used) as well as a custom token to a W. It drives the banner, the disabled Continue, the hidden percentage buttons and prepareTransaction's early return. The banner's testID names the case: muxed-address-not-supported-banner for an M, muxed-contract-not-supported-banner for a W.
    • The M check reads the contract's spec from freighter-backend, so it answers after the screen renders. Until it does (isMuxedSupportPending), nothing is built and Continue stays off; otherwise the 300 ms fee estimate would build the send first. The backend client times out, and a failed check counts as no support.
    • The support check starts each run from "unknown" and ignores an answer that arrives after the token or recipient changed, so a stale answer can't show or hide the banner.
    • Collectible screen: the same for an M whose collection doesn't support muxed addresses, including the image hidden on small screens.
    • The review sheet's M warning is gone (e54b5ec2, from review): Continue is off whenever it would have shown, and the sheet only opens from Continue.
  10. Muxed support is looked up only for an M (04e48633), from a review comment. The amount screen fetched the token contract's spec for every custom-token recipient, but only an M reads the answer: a G or C needs no muxed support, and a custom token sent to a W is blocked without it. The screen now skips the request for anything but an M, as the collectible screen already did.
    // Only an M reads the answer. A G or C needs no muxed support, and a W
    // is blocked for a custom token without any lookup.
    if (
    !isRecipientMuxed ||
    !isCustomToken ||
    !recipientAddress ||
    !network ||
    !contractId
    ) {
    return;
    }

Memo rules for a W

  • Settings: getMemoDisabledState disables the memo for an M or W first, before any contract lookup, with "Memo is disabled for this transaction."
  • Stale memo: the amount screen clears a stored memo when the recipient becomes an M or a W.
  • Review and details: no memo row for a W, even if a memo is stored.
  • Builder: a W is simulated with an empty memo; the transaction service refuses a non-empty memo for a W with the same message, so a memo is never dropped silently.
  • Unchanged: memos for G, M and C, and how memos fold into an M.

Parity with stellar/freighter#3047

Extension (stellar/freighter#3047) This PR
Custom token or collectible to a W blocked: "Muxed address not supported" banner with the W copy, Continue disabled blocked the same way on the amount and collectible screens, and the service refuses the build too
Native XLM or a classic asset to a W allowed, through the asset's SAC allowed, a SAC transfer to the exact W
Memo for a W disabled ("Memo is disabled for this transaction") disabled with the same message, hidden on review, refused if it reaches the builder
Recipient lookups for a W skipped, as a contract destination skipped: no account lookup and no unfunded notice
M recipient, with a custom token or collectible whose contract doesn't support muxed addresses blocked on the amount screen: the banner with the M copy, Continue disabled, no simulation blocked the same way on the amount and collectible screens; nothing is built until the contract check answers

DEV-only simulation stub

  • stubMuxedContractSimulation in the debug store, off by default, toggled from Home's debug sheet (toggle, "W Address Simulation Stub", en and pt).
  • With it on, in a debug build, buildTransaction skips both simulate endpoints when the final destination is a W and keeps the device-built XDR, so the review sheet opens. That XDR has no Soroban resources, so the network rejects it if it is ever submitted. C and G destinations always simulate.
  • Debug builds only: the builder checks __DEV__ before it reads the flag, so the stub never applies in a release build. A prod-bundle build gets the no-op debug store anyway. A dev-bundle release build, such as a PR preview, gets the persisted store and could keep a value an earlier debug build saved on the same install, but it simulates a W as normal regardless, so a PR preview can't inherit the stub.

Beyond the issue's scope, on purpose

  • accessible={false} on 5 bottom sheets: the debug sheet, the amount screen's settings and muxed-address-warning sheets, the collectible screen's muxed-address-warning sheet (the one its banner opens), and Add Token's add sheet. @gorhom/bottom-sheet 5.2.6 makes each sheet a single "Bottom Sheet, adjustable" accessibility element, so VoiceOver can't reach anything inside it. Turning that off fixes it for these sheets, as the send review sheet already does, and lets the iOS e2e assertions see their content. Other sheets are unchanged.
  • e2e runner: run-e2e-tests.sh loads E2E_TEST_CUSTOM_TOKEN_CONTRACT and E2E_TEST_CUSTOM_TOKEN_CODE from .env and passes them to Maestro. .env.example, e2e/docs/local-setup-and-env.md, e2e/docs/creating-tests.md and the e2e/README.md flow table list them. CI's env-file creator treats E2E_TEST* keys as optional.
  • Keyboard helper: waitForKeyboardDismiss takes an optional timeoutMs. With it, the wait also ends after that long without a hide event; on Android it races keyboard-controller's dismiss against the same cap. Only the amount screen's banner passes 500 ms, so its sheet opens even if the hide event never comes. Other callers are unchanged.
  • testIDs: send-settings-button (through a new optional testID on useRightHeaderButton), add-token-search-input, debug-stub-muxed-contract-simulation, debug-sheet-close-button, muxed-address-warning-close-button, muxed-contract-not-supported-banner, muxed-address-not-supported-banner, send-collectible-image and send-collectible-image-section.
  • Flows:
    • transactions/SendXlmToMuxedContract.yaml turns the stub on and sends XLM to a W up to the review sheet. It checks Settings says the memo is disabled and the review has no memo row, then cancels; it never confirms. On iOS each review row is one accessibility element whose label joins its texts ("Memo, None"), so the row check matches "Memo.*", after a "Fee.*" check that shows the pattern matches a row that is there (d2761943). It needs a debug build, so it isn't in the CI matrix.
    • transactions/SendCustomTokenToMuxedContractBlocked.yaml adds a SEP-41 token and checks the block. Banner, Continue disabled with an amount typed, no build or simulation error toast on arrival or after typing, and the explanation sheet. After typing, it also fails on the amount screen's "amount too high" and "XLM insufficient for transaction fees" errors, which would disable Continue too (d2761943). On Android it closes the keyboard before checking the banner and Continue (ee12e241).
    • onboarding/ImportFundedWallet.yaml retries the "Allow Paste" tap, which the iOS 26.5 paste prompt sometimes ignores.

Small-screen fixes

They apply while the recipient blocks the send: a W, or, since item 9, an M the token or collection can't take. Other layouts are unchanged.

Screen Before Fix After
Amount screen, iPhone SE, keyboard up the banner covered the 25% / 50% / 75% / Max row the percentage buttons are hidden while the send is blocked (dde064cb) banner y 298-342 pt and disabled Continue y 354-402 pt above the keyboard
Collectible screen, iPhone SE Review pushed past the fold first a 160 pt image on small screens (dde064cb) see the next row
Collectible screen, 360x640 dp Android and iPhone SE with the 160 pt image the list touched the banner, and on Android Review was cut off at the bottom edge the image section is hidden on small screens (max-xs) while the recipient blocks the send; the details list still names the collectible (cbc0f46a) Android: banner y 480-524 dp, Review y 536-581 dp of 640; SE: banner y 527-571 pt, Review y 583-631 pt of 667

Wider screens keep the 240 pt image with the banner. A G recipient's layout is identical before and after.

Tests

+114 tests and 2 new suites: MuxedAddressWarningBottomSheet.test.tsx and TransactionDetailsBottomSheet.test.tsx. The main additions:

  • stellar, qrValidation, soroban and muxedAddress helpers: W accepted (including a federation name that starts with W), Soroban routing, memo disabled first, W returned unchanged.
  • buildPaymentTransactionContractTarget and transactionService: a SAC transfer to the exact W, including mux ids 0 and u64 max; refusals for a custom token, a memo, and a W wrapping the token's own SAC; the collectible refusal before any I/O.
  • transactionBuilder: W routing, empty-memo simulation (a custom token sent to a G keeps its memo), and the stub (W only, both simulation paths; C still simulates; with __DEV__ false, a W simulates as normal even with the flag on).
  • sendRecipient, SendScreen/helpers and SendSearchContacts: no getAccount for a W on either path, no unfunded notice.
  • TransactionSettingsBottomSheet, SendReviewBottomSheet and TransactionDetailsBottomSheet: the memo off for a W without a contract lookup, the inclusion-fee label, no memo row for a W, Save after the W memo clear doesn't bring the old memo back, and G/M/C unchanged.
  • TransactionAmountScreen and SendCollectibleReview: banner, disabled button, nothing built, the stale-memo clear (a G or C keeps its memo), the percentage buttons and the collectible image, each with G/C controls. The collectible banner test gives each sheet its own present() and checks that the one it opens is the W warning.
  • TransactionAmountScreen and SendCollectibleReview, M: blocked when the contract lacks muxed support (the M banner and explanation, Review disabled, nothing built, the percentage buttons or the image hidden), not blocked when it has it, and nothing built until the check answers. The amount screen also checks that XLM to an M is unaffected, that a token switch doesn't reuse an earlier token's answer, and that a late answer is ignored. Removing any one of the last three guards fails its test.
  • TransactionAmountScreen: no muxed-support lookup for a custom token sent to a G, C or W; only an M gets one.
  • TransactionProcessingScreen: a W (or C) recipient with nothing submitting and no result shows "unsupported"; a G stays on "sending".
  • keyboard: the timeout on iOS and Android, and that no listener or timer is left behind.
  • useRightHeader: the forwarded testID.

Verification

  • Suites:
    • yarn test at 674cfb78: 336 suites passed (1 skipped), 4,945 tests passed (6 skipped).
    • yarn lint:ts at 674cfb78: clean. ESLint and Prettier are clean on the two screens and their tests. The next three results are from 0d11e57a.
    • yarn check still exits 1 on lint debt already on main. ESLint reports the same 111 errors in 18 files; the 30 in __tests__/helpers/soroban.test.ts are the same quote errors it has on the base branch. Prettier flags the same 9 tracked files as on main, none of them changed here.
    • yarn lint:translations: no missing translations; its 9 errors are the import/order and no-continue errors in 4 untouched files that main has too.
    • No file of feat(signing): render W addresses and fail closed on unreadable dApp requests #1037 or feat(history): show muxed contract (W) destinations #1035, mock-dapp, package.json, yarn.lock or .yarnrc.yml changed.
  • Maestro, iPhone SE (3rd generation) simulator, iOS 26.5: both flows pass at 0d11e57a, run with exactly the environment arguments run-e2e-tests.sh builds. Retries inside the shared steps recovered first-attempt misses that predate this PR: the "Allow Paste" prompt, and the manage-accounts sheet in SwitchToTestnet. On several earlier runs that manage-accounts step missed all three of its tries right after the import, and the run was repeated.
  • The flows' checks can fail (iPhone SE):
    • With a temporary local change that shows the memo row for a W (reverted, never committed), SendXlmToMuxedContract fails at its "Memo.*" check, right after the "Fee.*" check passes, and it passes again after the revert. The old "Memo" check still passed on that same screen.
    • Typing 1000 of a token the account holds 25 of makes SendCustomTokenToMuxedContractBlocked's post-amount check fail on the "The amount entered is greater than the available balance" toast.
  • Maestro, Pixel_10_Pro emulator, API 37:
    • Both flows pass at 0d11e57a, on a fresh devDebug build.
    • During development, SendCustomTokenToMuxedContractBlocked failed on its first Android run: the open keyboard covers the bottom of the amount screen, where the banner sits, as it covers Continue on any send. ee12e241 closes the keyboard on Android first.
    • This shared emulator has a fingerprint enrolled, so the app offers fingerprint unlock after the import; these runs used a local copy of the import step that taps "Skip for now". CI emulators have no fingerprint.
  • Device check of the M block at 70908faa (dev build, testnet, iPhone SE (3rd generation) and Pixel_10_Pro), through throwaway Maestro flows:
    • TZT, the e2e custom token, whose transfer takes no to_muxed, sent to an M; and a temporary local fake collectible (never committed) sent to the same M.
    • On both platforms: the M banner, Review disabled (on the amount screen, also after typing an amount), no build or simulation error, and the M explanation from the banner.
    • TZT to a W still shows the W banner and explanation on both.
    • SendCustomTokenToMuxedContractBlocked and SendXlmToMuxedContract weren't re-run after these two commits.
  • Device checks at cbc0f46a (dev build, testnet before its Protocol 30 upgrade, the stub on unless noted). The later commits add the stub's debug-build check, which a dev build passes, the collectible warning sheet's accessibility flag, tests and comments:
Check iPhone SE Pixel_10_Pro
Typed, pasted and Recents W: one suggestion row, no unfunded notice (an unfunded G control does show it) PASS. Recents used a seeded entry not run
XLM to W: Settings show the memo disabled and "Inclusion fee"; the review has no memo row; the fee breakdown shows Inclusion, Resource and Total PASS PASS through the flow (memo note, no Memo row)
USDC to W (a fresh account from provision-test-account.mjs --with-usdc-balance) PASS not run
Custom token to W: banner and disabled Review; the sheet shows the W copy PASS. Both sit above the keyboard, no overlap PASS at 720x1280 / 320 dpi with the keyboard closed. With it open, the keyboard covers them, as it covers Continue on every Android send
Collectible to W (local fake): banner and disabled Review PASS after cbc0f46a FAIL before, PASS after cbc0f46a at 720x1280 / 320 dpi
Stub off: XLM to W PASS: "Request failed with status code 400" on arrival (the amount-0 fee estimate); Review re-runs the simulation (another 400 in the app's network log); no sheet opens; nothing signed; the account's sequence number is unchanged not run
  • Stub off, toast detail: Review doesn't show a second toast, because the amount screen shows a builder error only when its text changes, and both failures read the same. That has always been the case on this screen, for any recipient.

  • Testnet W sends, 2026-11-03, after the Protocol 30 upgrade: stub off, dev build of this branch, backend v1 pointed at its private staging environment through sshuttle (a local, uncommitted change to the app's STG URL), the only deployment with freighter-backend#329 that day, from GAAP…SHK6 to WA7QYNF7…E2LLEM (base CA7QYNF7…UWDA, mux id 1234).

Send Device Tx
0.000001 XLM Pixel_10_Pro 9c972b68…
0.000001 XLM iPhone SE (3rd generation) 9156eafa…
0.01 USDC (GBBD47…) Pixel_10_Pro cd08ca68…
  • On chain, each succeeded as one invoke_host_function on the asset's SAC, transfer(from, to = the W, amount) (10, or 100000 for USDC), with no memo. Its transfer event pays the base contract with to_muxed_id: 1234. The success screen reads "… was sent to WA7Q…LLEM".

Follow-ups / out of scope

  • Accessibility: other sheets are still single accessibility elements, among them the fee breakdown. Making accessible: false the default in the shared BottomSheet wrapper would cover them all.
  • Android keyboard on the amount screen: the open keyboard covers the bottom of the screen on every send, so on Android the W banner shows once the keyboard closes.
  • Collectible screen on 360x640 dp Android: with a G recipient, the 240 pt image already pushes Review off the screen. This is unrelated to W, and this PR leaves that layout as it is.
  • e2e: the runner can't target a simulator by UDID (it takes the first booted one). An unfiltered yarn test:e2e also collects SendXlmToMuxedContract, which needs a debug build.
  • e2e, shared OpenManageAccountsSheet step: right after an import, a tap on the sheet's settings button can land while the sheet is still opening, and Settings never opens; the same tap a few seconds later works. Waiting for the sheet to settle before that tap may steady SwitchToTestnet for every flow that uses it.

🤖 Generated with Claude Code

CassioMG and others added 24 commits October 29, 2026 22:57
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Cover the Protocol 30 SDK surface the wallet depends on: the SEP-23
muxed contract (W) strkey vector, ScAddress arm 5 round-trips, the
CAP-87 ML-DSA cost types (86-94) and the CAP-88 millisecond
StellarValue arms. All four fail against the 17.0.1 SDK.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Pin which transaction memos fold into a muxed (M...) address on stellar-sdk 17.4.0. Decimal uint64 ids fold; hex, padded, empty and out-of-range values do not. The tests run against the real SDK, with only services/backend mocked.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
fast-text-encoding, the app's global TextDecoder, throws when it is asked
for { fatal: true }. SDK 17.4.0 asks for it in its bounded-fetch adapter, so
federation addresses did not resolve and stellar.toml asset icons did not
load on device. The SDK also asks for it in XdrString.toStringStrict(), so
scValToNative returned raw bytes instead of strings for symbols and strings.

Wrap the global TextDecoder so a UTF-8 decoder built with { fatal: true }
validates its input with a WHATWG-conformant check and throws a TypeError on
malformed bytes. Every other decoder is built and used as before, and the
wrapper does nothing where TextDecoder already supports fatal, as on Node
and in Jest.

The polyfill requires fast-text-encoding itself and installs Buffer first,
as shim.js does later, so the decoder keeps decoding through Buffer.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A TextDecoder built with { fatal: true } and the default ignoreBOM now drops
one leading U+FEFF from its output, as the native decoder does.
fast-text-encoding never strips it, so a stellar.toml or federation response
that starts with a BOM failed to parse on device: smol-toml and JSON.parse
both reject the BOM.

The SDK's bounded fetch builds its decoder with the default ignoreBOM, so it
gets the stripping. XdrString.toStringStrict() and the WASM name parser pass
ignoreBOM: true and keep the BOM. A strict decoder now reports the ignoreBOM
option it honors. Non-fatal decoders are unchanged and still keep a BOM.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A TextDecoder built with { fatal: true } now reads only the window of a
DataView or non-Uint8Array typed array that byteOffset and byteLength
describe, as the native decoder does. It used to validate and decode the
whole backing buffer, as fast-text-encoding does. Every caller in the app
passes a Uint8Array, which is unchanged, so this brings the strict path in
line with the native decoder rather than changing what the app decodes.
Non-fatal decoders still hand their input straight to the base.

The UTF8_LABEL comment now says which labels the strict path treats as
UTF-8, and that the Buffer-backed base accepts only utf-8 and utf8. The
tests gain a view-window check against the native decoder, a parity check for
decode() with no input on a non-fatal decoder, and a JSDoc on the hex helper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The decodeUtf8At comment said React Native's TextDecoder throws on fatal,
which stopped being true with the strict TextDecoder polyfill. It now says
why the function exists: the escape renderer needs one code point and its
width at an index, which a decoder of a whole buffer cannot give.

The strkey scrubbing tests add a letter before, a letter after and one more
base32 character after a W key, so the leading anchor, the trailing anchor
and the exact length are each pinned on their own. The memo folding tests
split the non-folding memos by reason: not a canonical decimal id, and not a
uint64. The SDK compatibility suite's header and top-level title now cover
SDK 17 and Protocols 27 and 30. No production code changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A W is now a valid send destination. isValidStellarAddress accepts it, isSorobanTransaction treats a send to it as Soroban, getMemoDisabledState always disables the memo for it (the mux id is in the address), and determineMuxedDestination returns it unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A W now goes through the token's SAC transfer, never Operation.payment, and is simulated like a C. buildPaymentTransaction and the transaction builder duck treat it as a contract destination, and getSacContractIdForBalance names the SAC that moves a balance. A W refuses a custom token (new muxedContractNotSupported copy), a memo that would otherwise be dropped, and a W whose base C is the token contract. buildSendCollectibleTransaction refuses a W before any network call. The duck simulates a W with an empty memo.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A W is now a contract destination in the recipient flow, exactly like a C. searchAddress and setDestinationAddress never call getAccount for a C or a W and record isDestinationFunded: false, the value a C already got because looking up a contract address finds no account. buildUnfundedContext flags a W as a contract destination, so the unfunded-destination warning and the unfunded notice stay hidden, and the processing screen treats a W as a contract recipient. The Blockaid helper's logic is unchanged, only its comments now name W.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A muxed contract (W) address carries its mux id, so a W transfer never
carries a memo.

TransactionSettingsBottomSheet now counts a W as a Soroban recipient, which
shows the inclusion-fee label, and resolves a W send's contract to the
selected asset's SAC instead of the recipient. A collectible's collection and
a custom token's own contract still win, and a balance with no SAC leaves the
contract unresolved rather than crashing the sheet. The memo field stays
disabled, with its message, through getMemoDisabledState.

SendReviewBottomSheet and TransactionDetailsBottomSheet hide the memo row for
a W as they do for an M, even when a memo is still stored.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A muxed contract (W) address is only sent through an asset's Stellar Asset
Contract, so a custom token or a collectible sent to one is blocked. The
builder refuses these sends as a fail-closed backstop, but a refused build
never reaches the review sheet where the M warning lives, so the block sits
on the screen itself.

TransactionAmountScreen (a custom token to a W) and SendCollectibleReview (a
collectible to a W) show a "Muxed address not supported" banner above
Continue/Review and disable the button. prepareTransaction returns before it
builds, so the auto-simulation, a settings change and Continue all build
nothing. Tapping the banner opens the explanation sheet. Native XLM and
classic assets still go to a W, and the M flow is unchanged.

MuxedAddressWarningBottomSheet takes a variant. "account", the default, keeps
the M copy, and "contract" explains the W with the new
transactionAmountScreen.errors.muxedContractWarning.description key, added in
en and pt. The sheet's close button gets a testID.

TransactionAmountScreen also clears a stale memo for a W recipient, as it
already did for an M.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Testnet can't simulate a send to a muxed contract (W) address until it runs
Protocol 30 and the backend ships freighter-backend#329. A DEV-only debug
store override, stubMuxedContractSimulation, makes buildTransaction skip both
simulate endpoints when the final destination is a W, so the review sheet
opens with the device-built XDR, which can't be submitted. A C or G
destination still simulates, and the debug store's prod path keeps the stub
off. Home's debug sheet toggles it ("W Address Simulation Stub", en and pt).

Two Maestro flows run on testnet:
- SendXlmToMuxedContract turns the stub on and sends XLM to a W up to the
  review sheet. It checks the memo is disabled in Settings and absent from
  the review, then cancels. It never confirms.
- SendCustomTokenToMuxedContractBlocked adds a SEP-41 token and checks that
  sending it to a W is blocked: the banner, a disabled Continue, the
  explanation sheet, and no build.

They need testIDs: send-settings-button (useRightHeaderButton now takes a
testID), add-token-search-input, debug-stub-muxed-contract-simulation and
debug-sheet-close-button. On iOS, gorhom makes each sheet one accessibility
element by default, which hides its contents from XCUITest and VoiceOver.
So the debug, transaction settings, muxed address warning and add token
sheets pass accessible: false, as the review sheet already does.

ImportFundedWallet retries its "Allow Paste" tap, which the iOS 26.5 paste
prompt sometimes ignores right after it appears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an iPhone SE with the keyboard up, the amount screen's "Muxed address
not supported" banner covered the percentage buttons. Nothing can be sent
while a custom token goes to a muxed contract (W) address, so the buttons
are hidden then. XLM to a W, and a custom token to a G or a C, keep them.

The collectible send screen doesn't scroll, so on the SE the W banner
pushed Review past the fold. While a W is the recipient, the collectible
image shrinks from 240 to 160 pt on small screens (max-xs), and the banner
and the disabled Review button both fit. Other recipients keep the layout.

waitForKeyboardDismiss takes an optional timeoutMs. With it, the wait also
ends once that much time has passed without a hide event, and the listener
and the timer are released on either path. On Android it races
keyboard-controller's dismiss against the same timeout. Without it, nothing
changes. The amount screen's W banner passes 500 ms, so its explanation
sheet can't stay shut if the hide event never comes.

The stub comments now say what each build does: a prod-bundle build has
the no-op debug store, so the stub is always off there, and a dev-bundle
release build keeps any value an earlier debug build persisted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SendCustomTokenToMuxedContractBlocked's checks couldn't fail. Continue was
checked with no amount, when it's disabled anyway, and the build-error
check came last, long after any 5-second toast was gone. assertNotVisible
also waits for a visible element to go away, so it can't catch a toast.

The flow now types an amount within the account's balance (1) and checks
that Continue stays disabled. Right after arriving, and again after typing,
it waits a few seconds for a build or simulation error toast and fails if
one shows up. It matches the texts those toasts carry: transactionService's
"Failed to build payment transaction: ..." (here the reason would be
transaction.errors.muxedContractNotSupported), the builder's "Failed to
build transaction" and "Simulation returned no prepared transaction XDR",
and the API client's "Request failed with status code ...". It waits for
the screen to settle before tapping the banner, and the bounded retry is
documented as a test-timing mitigation. The header now says the account
must hold the custom token.

run-e2e-tests.sh loads E2E_TEST_CUSTOM_TOKEN_CONTRACT and
E2E_TEST_CUSTOM_TOKEN_CODE from .env, like the recovery phrases, and passes
them to Maestro. .env.example, the e2e docs and the README's flow table
document them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Android the open keyboard covers the bottom of the amount screen, where
the "Muxed address not supported" banner and Continue sit. It covers
Continue on any send, which is why the other send flows hide it before
tapping Review. SendCustomTokenToMuxedContractBlocked asserted the banner,
and then the disabled Continue, while the keyboard was still up, so on a
Pixel emulator it failed at the banner check.

The flow now hides the keyboard on Android before each of those checks.
iOS runs the same steps as before, with the keyboard up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collectible send screen doesn't scroll. With a muxed contract (W)
recipient, the 160 pt image from the earlier fix still left no room on a
360x640 dp Android screen: the banner touched the details list, and Review
was pushed to the screen edge, half under the navigation handle. On an
iPhone SE the list and the banner touched too.

While a W is the recipient, small screens (max-xs) now hide the image
section instead of shrinking it. The details list still names the
collectible. The banner and the disabled Review button sit at the bottom
with room above them: on that Android screen Review ends at 581 dp of 640,
and on the SE at 631 pt of 667. Wider screens, and G, M and C recipients,
keep the 240 pt image and the layout they had before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The W simulation stub checked only the debug store's flag. A PR preview
is a dev-bundle release build: __DEV__ is false, but it gets the
persisted debug store. Installed over a debug build, it could inherit
the stub turned on, with no debug sheet to turn it off, and skip the
simulation of every W send.

The builder now checks __DEV__ before it reads the flag, so the stub only
ever applies in a debug build. A release build simulates a W as normal,
whatever the store holds. The comments in the builder and the debug store
say so, and a new test runs a W build with __DEV__ false and the flag on
and expects the normal simulation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SendXlmToMuxedContract checked that no "Memo" text was on the review
sheet. On iOS each review row is one accessible element, whose label
joins its texts ("Memo, None"), and Maestro's text is a full-match
regex, so "Memo" matched nothing even when the row was there. With a
local change that showed the memo row for a W, the flow still passed.
It now checks "Memo.*", after a "Fee.*" check that shows the same kind
of pattern matches a row that is there. With the same local change, the
flow now fails at the memo step, and it passes without it.

SendCustomTokenToMuxedContractBlocked types 1 of the token and expects
Continue to stay disabled because of the W. If the account held less
than that, the amount error would disable Continue too, and the check
would pass for the wrong reason. The error check after the amount now
also fails on the amount screen's two amount errors, "The amount entered
is greater than the available balance" and "XLM insufficient for
transaction fees". On an iPhone SE simulator, typing 1000 of a token the
account holds 25 of makes that check fail on the first toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accessibility: the collectible screen's muxed address warning, the sheet
the W banner opens, now sets accessible={false} on its modal, as its
twin on the amount screen does. gorhom otherwise makes the whole sheet
one accessibility element, which hides the explanation from VoiceOver.

Tests:
- TransactionProcessingScreen: a W (and a C) recipient with nothing
  submitting and no result shows "unsupported"; a G stays on "sending".
  Device runs never reach that branch, so it had no coverage.
- The amount screen keeps a stored memo for a G or a C recipient, so a
  change that cleared every recipient's memo would now fail the suite.
- The builder keeps the memo when it simulates a custom token sent to a G.
- The settings sheet: Save after a W clears the memo doesn't save the old
  one, and the memo check is never handed the W as the contract.
- The review sheet's memo-row tests serve their recipient on every render.
- The collectible banner test gives each sheet its own present() and
  checks that the one it opened is the W warning.
- JSDoc on the amount screen tests' routeFor and isContinueDisabled.

Wording, no behaviour change:
- determineMuxedDestination: an M with a memo is kept as is; the re-mux
  branch is never reached for a real M.
- getSacContractIdForBalance's error now says a custom token is sent
  through its own contract, not a derived SAC.
- buildPaymentTransaction returns the final destination on both paths,
  and buildTransaction stores a result only for the latest request.
- isDestinationFunded documents that false also means a contract
  destination, and setDestinationAddress reads the network only when it
  looks up an account.
- Comments and test titles state the rule instead of a design-spec
  decision id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stellarSdkV15.test.ts has covered more than SDK 15 since it gained the
Protocol 27 (CAP-71) block and the Protocol 30 block, so it becomes
stellarSdkCompat.test.ts, with a version-free top-level describe.

It also gains a block for the Horizon submission errors of the SDK 16+
HTTP client. SDK 16 replaced axios with fetch, and 16.1.0 / 17.0.0 made
submitTransaction reject with TransactionFailedError or BadResponseError.
The app reads those errors through isHorizonError: response.status for
the 504 retry and the log-severity split, and
response.data.extras.result_codes in the transaction builder and
useManageTokens. Every app test built that error by hand, so nothing
pinned it to the real SDK. The new tests drive the real Horizon.Server
against a stubbed fetch for a 400 with result codes, a 429 without them,
and a 504. If the SDK stops exposing response.data, the first two fail.

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

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Android Emulator preview APK is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-f08ddf3a12a93b1443ed
Backend: V1 prod + V2 staging (no sandbox configured for @CassioMG). SDF collaborators only — install instructions in the release description.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

iOS Simulator preview build is ready: https://github.com/stellar/freighter-mobile/releases/tag/untagged-96e6e2e57b2fe13f9a05
Backend: V1 prod + V2 staging (no sandbox configured for @CassioMG). SDF collaborators only — install instructions in the release description.

CassioMG and others added 2 commits October 30, 2026 17:51
A custom token whose contract doesn't support muxed addresses can't go
to an M. The amount screen now blocks that send the way it blocks a
custom token to a W, as the extension does: the "Muxed address not
supported" banner with the M explanation, Continue disabled, and nothing
built or simulated. Before, the screen built the transaction and only
the review sheet showed the warning.

The M check reads the contract's spec, so it answers after the screen
renders. Until it does, nothing is built and Continue stays off. Each
check starts from "unknown", and a late answer for a token or recipient
the user has since changed is ignored, so a stale answer can't show or
hide the banner.

The review sheet keeps its M warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The collectible screen now blocks an M whose collection doesn't support
muxed addresses the way the amount screen does since the previous
commit, and the way it already blocked a W: the "Muxed address not
supported" banner with the M explanation, Review disabled, nothing built
or simulated, and the image hidden on small screens so the banner and
Review fit. Before, the screen built the transfer and only the review
sheet warned.

As on the amount screen, nothing is built and Review stays off until the
collection check answers, and each check starts from "unknown". The
review sheet keeps its M warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
17.4.0 is now older than the 7-day npmMinimalAgeGate, so the
temporary npmPreapprovedPackages entry is no longer needed.

Co-Authored-By: Claude Opus 5.5 <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 finding remains because custom-token W sends can still trigger an unnecessary contract lookup and backend request, introducing avoidable failure or race behavior while the screen is disabled.

1 open issue
What changed in this PR

Adds support for sending native XLM and classic assets to muxed contract (W…) destinations through Stellar Asset Contract transfers, while blocking unsupported custom-token and collectible sends.

Changes:

  • Accepts, validates, routes, and displays W… destinations without account lookups or memo handling.
  • Adds SAC resolution, transaction-builder simulation behavior, UI blocking states, warnings, and debug simulation controls.
  • Expands unit, integration, and Maestro coverage for W… and unsupported M… flows.
File Description
src/​services/​transactionService.ts Updated as part of this pull request.
src/​services/​blockaid/​helper.ts Updated as part of this pull request.
src/​i18n/​locales/​pt/​translations.json Updated as part of this pull request.
src/​i18n/​locales/​en/​translations.json Updated as part of this pull request.
src/​hooks/​useRightHeader.tsx Updated as part of this pull request.
src/​helpers/​stellar.ts Updated as part of this pull request.
src/​helpers/​soroban.ts Updated as part of this pull request.
src/​helpers/​qrValidation.ts Updated as part of this pull request.
src/​helpers/​muxedAddress.ts Updated as part of this pull request.
src/​helpers/​keyboard.ts Updated as part of this pull request.
src/​ducks/​transactionBuilder.ts Updated as part of this pull request.
src/​ducks/​sendRecipient.ts Updated as part of this pull request.
src/​ducks/​debug.ts Updated as part of this pull request.
src/​components/​TransactionSettingsBottomSheet.tsx Updated as part of this pull request.
src/​components/​TransactionDetailsBottomSheet.tsx Updated as part of this pull request.
src/​components/​screens/​SendScreen/​SendSearchContacts.tsx Updated as part of this pull request.
src/​components/​screens/​SendScreen/​screens/​TransactionProcessingScreen.tsx Updated as part of this pull request.
src/​components/​screens/​SendScreen/​screens/​TransactionAmountScreen.tsx Updated as part of this pull request.
src/​components/​screens/​SendScreen/​screens/​SendCollectibleReview.tsx Updated as part of this pull request.
src/​components/​screens/​SendScreen/​helpers/​index.tsx Updated as part of this pull request.
src/​components/​screens/​SendScreen/​components/​SendReviewBottomSheet.tsx Updated as part of this pull request.
src/​components/​screens/​AddTokenScreen/​AddTokenScreen.tsx Updated as part of this pull request.
src/​components/​MuxedAddressWarningBottomSheet.tsx Updated as part of this pull request.
src/​components/​analytics/​DebugBottomSheet.tsx Updated as part of this pull request.
scripts/​run-e2e-tests.sh Updated as part of this pull request.
e2e/​README.md Updated as part of this pull request.
e2e/​flows/​transactions/​SendXlmToMuxedContract.yaml Updated as part of this pull request.
e2e/​flows/​transactions/​SendCustomTokenToMuxedContractBlocked.yaml Updated as part of this pull request.
e2e/​flows/​onboarding/​ImportFundedWallet.yaml Updated as part of this pull request.
e2e/​docs/​local-setup-and-env.md Updated as part of this pull request.
e2e/​docs/​creating-tests.md Updated as part of this pull request.
.env.example Updated as part of this pull request.
tests/​services/​transactionService.test.ts Updated as part of this pull request.
tests/​services/​buildPaymentTransactionContractTarget.test.ts Updated as part of this pull request.
tests/​hooks/​useRightHeader.test.tsx Updated as part of this pull request.
tests/​helpers/​stellar.test.ts Updated as part of this pull request.
tests/​helpers/​soroban.test.ts Updated as part of this pull request.
tests/​helpers/​qrValidation.test.ts Updated as part of this pull request.
tests/​helpers/​muxedAddress.test.ts Updated as part of this pull request.
tests/​helpers/​keyboard.test.ts Updated as part of this pull request.
tests/​ducks/​transactionBuilder.test.ts Updated as part of this pull request.
tests/​ducks/​sendRecipient.test.ts Updated as part of this pull request.
tests/​components/​TransactionSettingsBottomSheet.test.tsx Updated as part of this pull request.
tests/​components/​TransactionDetailsBottomSheet.test.tsx Updated as part of this pull request.
tests/​components/​screens/​SendScreen/​SendSearchContacts.test.tsx Updated as part of this pull request.
tests/​components/​screens/​SendScreen/​screens/​TransactionProcessingScreen.test.tsx Updated as part of this pull request.
tests/​components/​screens/​SendScreen/​screens/​TransactionAmountScreen.test.tsx Updated as part of this pull request.
tests/​components/​screens/​SendScreen/​screens/​SendCollectibleReview.test.tsx Updated as part of this pull request.
tests/​components/​screens/​SendScreen/​helpers/​index.test.tsx Updated as part of this pull request.
tests/​components/​screens/​SendScreen/​components/​SendReviewBottomSheet.test.tsx Updated as part of this pull request.
tests/​components/​MuxedAddressWarningBottomSheet.test.tsx Updated as part of this pull request.

Review effort: Lite

CassioMG and others added 5 commits November 2, 2026 15:35
Brings in b4489ad, which re-comments the stellar-sdk age-gate bypass in
.yarnrc.yml, and 9e5e0d2, which renames the SDK compatibility tests.
This branch started before both, so it still had the bypass on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The amount screen asked the backend for the token contract's spec on
every custom-token send, whatever the recipient. Only an M reads that
answer: a G or C needs no muxed support, and a custom token sent to a W
is blocked without a lookup. The screen now skips the request for
anything but an M.

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

The scrubber tests checked a W with one extra character but not one with
a character missing: a 68-character run must stay untouched, since only a
69-character M or W is a key. buildMuxedContract's comment says it drops
leading zeros from the id; nothing asserted it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md and the dependencies guide still said Node 20, but
package.json's engines field asks for >= 22.12.0 and every workflow runs
Node 22. Prettier, run by the pre-commit hook, also re-pads the Best
Practices table in AGENTS.md, which wasn't formatted before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in 900fd03 (two missing tests: a too-short W in the scrubber,
buildMuxedContract's leading zeros) and b80ca26 (Node 22 in the docs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CassioMG and others added 3 commits November 4, 2026 13:54
getMemoDisabledState turns the memo off for any M or W before it reads
the contract, so the SAC the sheet resolved for a W never changed the
result. A W now falls through to the C case and is passed as its own
contractId, which the memo check never reads. The
getSacContractIdForBalance and logger imports and point 3 of the doc
comment go with it, the two tests that only covered the lookup are
dropped, and the W tests now check the memo state instead of which
contract the check was handed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The amount screen and the collectible review block an M whose token or
collection doesn't support muxed addresses: Continue (Review) stays off
and nothing is built, both while the contract check runs and after it
says no. The review sheet only opens after a build, so its M banner,
and the footer's handling of the same flag, could no longer show. They
go; the screens' own banner and explanation sheet stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The W comment still said the review sheet carries an M banner, which
e54b5ec removed. The screen's JSDoc still said only a W is blocked,
though an M whose collection doesn't support muxed addresses is blocked
the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from cg-p30-sdk-bump to main November 6, 2026 23:18
CassioMG and others added 5 commits November 6, 2026 15:22
Brings in #1034 (squash-merged as 3999960) now that this PR targets
main. package.json and yarn.lock take main's versions: this PR doesn't
change them, and main's carry #1034's later typescript-wallet-sdk-km
6.0.0 bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #1037 (squash-merged as fa85619). Both PRs added fixture
imports at the top of soroban.test.ts; the resolution keeps all of
them. The translation files merged cleanly.

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

* Refuse a Soroban send that would drop its mux id or memo

A Soroban transfer carries no memo, so the only id it can carry is the
one in a muxed destination. When the destination the user entered
couldn't be built, the transfer went somewhere else instead: an M whose
contract doesn't support muxed addresses went to its base G without the
id, and a memo that couldn't become an M's id (the contract doesn't
support muxed addresses, or the memo isn't a decimal uint64) was
dropped. The transfer now throws a translated error instead, so nothing
is built or signed.

The amount and collectible screens already block an M the contract
doesn't support, so that refusal only fires when the build's support
check disagrees with the screen's. For a custom token, a memo that can't
be carried used to reach the backend's simulation, which rejects a
Soroban transaction with a memo, so Review failed with a generic
"Request failed with status code 400". It now fails with a clear message
as soon as the token is chosen. A memo for an M is refused too, since the
M's own id is the only one the transfer can carry.

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

* Show a failed collectible build's error as a toast

A failed collectible build was only logged, so tapping Review did
nothing and the user never learned why. The collectible screen now shows
the build error as a toast, as the amount screen does. This includes the
refusals from the previous commit, such as a memo the collection can't
carry, and any other build or simulation error.

Each failed build shows it, since a new build clears the error first.

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

* Look up an M recipient's funding through its base G

Horizon can't look up a muxed account (M) itself: /accounts/M... returns
400, which getAccount turns into null, so the send flow recorded an M as
unfunded. Typing or picking an M of a funded account then showed "The
destination account doesn't exist", and where Blockaid can scan the
transaction (mainnet), the review also warned that it was expected to
fail. An M is its base G plus an id, so its funding is now looked up
through the G. Contract destinations (C and W) still get no lookup.

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

* Block a memo a Soroban transfer can't carry before building anything

A Soroban transfer carries a memo only as the id of an M built from a G.
With a memo it can't carry (a federation memo, or one saved while XLM
was selected, then a custom token without muxed support, a memo that
isn't a decimal uint64, or a C recipient), every build threw: the
300 ms fee estimate failed before the user did anything, and Continue
looked dead, because the screen doesn't toast the same error twice.

Both send screens now block it up front, the way they block an M or a
W the token can't take: a "Memo not supported" banner that opens an
explanation, Continue (Review) off, the percentage buttons or the
collectible image hidden, and nothing built. A memo for a G waits for
the same contract lookup an M takes; a memo for a C is refused without
one. The build-time refusal stays as the backstop.

getSorobanMemoRefusal holds the rules. Its tests run every case through
determineMuxedDestination as well, so the screens and the build refuse
the same memos. Unlike createMuxedAccount it logs nothing, since the
screens check every memo the user saves. prepareTransaction checks the
freshly saved memo too, because Settings asks for a rebuild before the
screen re-renders with it.

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

* Throw away the earlier build when the new settings can't be built

On both send screens the review sheet's gear opens Settings, so the
memo can change while the review is open. When Settings saved a memo
the transfer can't carry, prepareTransaction returned without building,
but the transaction built for the earlier memo stayed in the store. The
review came back showing the new memo, and Confirm signed the old
transaction, without that memo or with the earlier mux id. A second
save while the memo blocked the send did the same, since it still runs
with the blocked render's state.

Every early return in prepareTransaction now resets the built
transaction and closes the review sheet, so nothing built for other
settings can be confirmed, and the screen's banner says why. Tests save
such a memo from Settings on each screen, save again while blocked,
and pin that XLM with a text memo still builds on Continue.

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

* Refuse a memo longer than 22 digits, as the SDK does

getSorobanMemoRefusal let a zero-padded numeric memo of 23 to 28
digits through, but the SDK refuses a mux id longer than 22 digits even
when its value fits a uint64. The screens didn't block such a memo, the
fee estimate then failed with one toast, and Continue looked dead. The
check now caps the length at 22 digits too, and the parity table pins
both sides of the limit through the real SDK.

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

* Name the memo block in the send screens' doc comments

The collectible screen's JSDoc and the amount screen's banner opener
now say they cover a memo the transfer can't carry too. The comment on
the collectible screen's memo check said an M "is handled above"; it
now says what happens: a W is blocked there, and Settings disables the
memo for an M.

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

* Say "collectible" in the collectible screen's numeric-memo sheet

A memo that isn't a number, for a collection that supports muxed
addresses, opened the token's explanation: "This token only accepts a
numeric memo". The collectible screen now opens its own variant: "This
collectible only accepts a numeric memo, from 0 to
18446744073709551615.", in en and pt.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings in 483fe24, the squash merge of #1038 into this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #1035 (squash-merged as 8e78952). Its soroban.ts change
(history attributes, near the end of the file) and this PR's (contract
destinations, near the top) don't overlap, so the file merged cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CassioMG
CassioMG merged commit db4a005 into main Nov 7, 2026
37 checks passed
@CassioMG
CassioMG deleted the cg-p30-send-w branch November 7, 2026 00:31
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.

3 participants