Skip to content

Add Stealth Send and Stealth Swap (Houdini privacy routing)#6066

Draft
j0ntz wants to merge 8 commits into
developfrom
jon/stealth-send-swap
Draft

Add Stealth Send and Stealth Swap (Houdini privacy routing)#6066
j0ntz wants to merge 8 commits into
developfrom
jon/stealth-send-swap

Conversation

@j0ntz

@j0ntz j0ntz commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

Draft until those publish and the version pins bump — CI cannot typecheck the toAddressInfo path against published edge-core-js 2.46.1. Locally this branch is verified against the linked dep branches (tsc clean, eslint clean, full jest suite green).

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

Description

Asana task

Production Stealth Send + Stealth Swap (UI Proposal A), replacing every prototype hack from #6054 with real wiring. The prototype PRs (#6054, #6031) stay open as reference.

Send (SendScene2 becomes a send-to-address swap). Judgement call: integrated INTO SendScene2 in place, no feature flag and no parallel scene — the spec's "SendScene2 becomes a send-to-address swap" reading. The new "Recipient receives" selector (variant A1: visible before address entry) and Stealth Send toggle card render only when the scene is an unconstrained plain send; callers that pre-lock or hide tiles, pre-fill an address (payment protocol, deep links), pay FIO requests, or take over broadcast/completion keep today's behavior untouched. Alternate (a feature-flagged parallel scene replacing the wallet Send button, the prototype's approach) rejected: it forks the send flow and leaves every other send entry point without the feature. The TransactionListTop reroute and HoudiniSendScene re-skin from the prototype are gone.

  • Stealth ON or a cross-asset recipient turns the send into a live account.fetchSwapQuotes({ toAddressInfo }) quote; plain same-asset sends keep the existing UI and behavior (regression-checked, including multi-recipient UTXO sends).
  • Linked "You send"/"Recipient gets" rows: the edited side is the guaranteed amount (green "Guaranteed"), the other tracks the live quote as ~ "Estimated". Editing "Recipient gets" issues quoteFor: 'to' (reverse quote).
  • Inline quote row: real rate, winning provider label, and the price delta REUSED from the swap confirmation scene via the new shared PriceImpactText (the prototype's recreated delta UI is deleted); real expiry countdown that re-quotes on expiry; real network fee from the quote.
  • Stealth restricts the request to Houdini via the shared makeStealthSwapRequestOptions (all other providers disabled per-request); a plain cross-asset send fans out to every enabled provider (verified live: ChangeNOW won the fan-out and the send executed to the success scene).
  • Destination tag row appears only when the destination chain is memoNeeded per the Houdini chain metadata; the entered tag rides toAddressInfo.toMemos to the provider (which forwards it as destinationTag on order creation).
  • Address entry reuses AddressTile2 + the existing AddressModal family. Cross-chain destination addresses cannot go through the source wallet's parseUri, so AddressTile2 gains a crossChainAddressValidation override that validates against the destination chain's own regex (Houdini's per-chain addressValidation).
  • Chain metadata (src/util/houdiniChains.ts) is a generated snapshot of Houdini GET /chains (2026-07-02) intersected with Edge currency pluginIds — 38 chains including 6 memo chains — replacing the prototype's 4-chain hardcode and hard-coded memoNeeded flags. Native-asset destinations only for now (token destinations are a follow-up); dynamic sourcing from the API through the swap plugin is a follow-up noted in the module.
  • Success: same-asset non-stealth keeps the Transaction Success modal + details; cross-asset or stealth lands on SwapSuccessScene.
  • Slide label reads "Slide to send stealthily" when stealth is on; the SafeSlider always submits (no separate confirmation scene).

Known conflict, multiple recipients (UTXO). Gated in both directions per the spec: stealth ON or a mismatched recipient hides "Add Another Address"; with multiple recipients present, the stealth toggle is disabled and the card expands with "Stealth Send and cross-asset recipients are not available when sending to multiple recipients," and the recipient-asset selector locks. Multi-recipient sends now also show a Total Amount row (the UI call the task left open) instead of only per-recipient amounts.

Other conflicts noticed by inspection (documented calls): max-spend is not offered in swap-send mode (the plain-mode max flow still works; a swap-send max via the plugins' getMaxSwappable is a clean follow-up); PIN spending limits do not gate stealth sends, consistent with the existing swap flow which they also do not gate; FIO sends are constrained-mode and keep today's flow; the SwapDetailsCard on transaction details requires a payout wallet, so a stealth send's tx detail omits the exchange card (follow-up: render it from the payout asset info alone).

Swap. Stealth Swap toggle card on the amount-entry scene (final copy, working inline "Learn more"); the quote request is restricted to Houdini and the restriction survives re-quotes on the confirmation scene; the powered-by card renders as a fixed provider (no chevron, no "tap to change provider") via a now-optional PoweredByCard.onPress.

Copy is the final verbatim spec copy; the prototype's houdini_incognito_* keys are gone (new keys are stealth_*). "Learn more" opens https://gist.github.com/j0ntz/b3f8101f0a1f79539150fc73511bff8b in the browser as the placeholder (final URL TBD).

savedAction sweep for the now-optional toWallet/payoutWalletId across swap scenes, SwapProviderRow, ExchangeQuoteComponent, CategoriesActions, TransactionDetailsScene, and SwapDetailsCard.

Testing (iOS sim, edge-funds, deps linked via updot): a REAL cross-asset send-to-address executed end to end — Base ETH → Ethereum address, live ChangeNOW quote from the provider fan-out, slider approved, deposit broadcast on-chain (txid 0xfd51a5c5d4ba44267d257506c977a8e88952f56987e653d2b812502fa739cf8b), SwapSuccessScene reached, transaction details render. Stealth Send/Swap quote paths verified live through the Houdini plugin (the shared dev API key's free-tier 10-exchanges/day cap blocked executing the stealth orders themselves this run; the quote retrieval, provider restriction, XRP destination-tag row, and tag-to-order threading are all verified — the last via the plugin's live-recorded fixture test). Multi-recipient gating, plain-send regression, and "Learn more" verified on-device; jest suite green (snapshots updated for the new rows); maestro walks added under maestro/14-stealth/.

j0ntz added 8 commits July 2, 2026 17:25
edge-core-js swap-to-address makes EdgeSwapRequest.toWallet optional
(an address-only toAddressInfo destination replaces it) and makes
EdgeTxActionSwap.payoutWalletId and EdgeTxSwap.payoutWalletId optional.
Sweep the consumers: wallet-to-wallet swap scenes throw if their
destination wallet is missing, savedAction and swap-details readers
tolerate a missing payout wallet.
Extract the swap confirmation scene's price-impact computation and the
quote card's colored percentage text into PriceImpactText, so the send
scene's quote row can reuse the same delta UI.
HOUDINI_CHAINS snapshots Houdini's GET /chains intersected with Edge
currency pluginIds (per-chain address validation regex + memoNeeded),
mirroring the edge-exchange-plugins chain mapping. Register the houdini
swap plugin through HOUDINI_INIT env config like other providers.
The send scene now offers a "Recipient receives" asset selector
(Houdini-served destination chains) and a Stealth Send toggle. Stealth
or a cross-asset recipient turns the send into a swap-to-address quote:
live quotes via account.fetchSwapQuotes with toAddressInfo, linked
"You send"/"Recipient gets" amount rows whose edited side is the
guaranteed amount, the shared price-impact indicator, a real expiry
countdown that re-quotes, the quote's network fee, and a destination
tag row on memoNeeded chains that rides toMemos to the provider.
Stealth restricts quotes to Houdini; a plain cross-asset send fans out
to every provider. The confirm slider approves the quote and lands on
the swap success scene.

Plain same-asset sends are unchanged, including multi-recipient UTXO
sends, which now also show a total-amount row. Multi-recipient and
stealth/cross-asset are mutually exclusive, gated in both directions.
Constrained callers (locked or hidden tiles, FIO requests, payment
protocol, custom broadcast/completion hooks) keep today's behavior.
Cross-chain destination addresses validate against the destination
chain's own rules via a new AddressTile2 validation override, since the
source wallet cannot parse them.
A Stealth Swap toggle card on the amount-entry scene restricts the
quote request to the Houdini privacy provider (all other providers
disabled for the request), with the final copy and a working
"Learn more" link. The confirmation scene keeps the restriction on
re-quotes and renders the powered-by card as a fixed provider: no
chevron and no "tap to change provider" hint, via a now-optional
PoweredByCard onPress.
UI walks for the new send-to-address swap surfaces: the recipient-asset
selector and toggle card on the send scene (including the destination
tag row on an XRP destination) and the Stealth Swap toggle on the swap
amount-entry scene. Neither flow requests a live quote.
The address tile's Enter/Myself/Scan/Paste entry buttons had no stable
selectors.
@j0ntz

j0ntz commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence (iOS sim, live wiring)

agent proof 1216251688512498 01 stealth send toggle

agent proof 1216251688512498 01 stealth send toggle

agent proof 1216251688512498 02 recipient chain picker

agent proof 1216251688512498 02 recipient chain picker

agent proof 1216251688512498 03 send to any quote

agent proof 1216251688512498 03 send to any quote

agent proof 1216251688512498 04 send to any success

agent proof 1216251688512498 04 send to any success

agent proof 1216251688512498 05 stealth swap toggle

agent proof 1216251688512498 05 stealth swap toggle

agent proof 1216251688512498 06 stealth swap houdini only

agent proof 1216251688512498 06 stealth swap houdini only

agent proof 1216251688512498 07 learn more browser

agent proof 1216251688512498 07 learn more browser

agent proof 1216251688512498 08 xrp destination tag

agent proof 1216251688512498 08 xrp destination tag

agent proof 1216251688512498 09 plain send regression

agent proof 1216251688512498 09 plain send regression

agent proof 1216251688512498 10 multi recipient gating

agent proof 1216251688512498 10 multi recipient gating

Captured by the agent's in-app test run (build-and-test).

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.

1 participant