Add core-side synthetic destination for swap-to-address - #730
Draft
j0ntz wants to merge 3 commits into
Draft
Conversation
Contributor
Author
6 tasks
j0ntz
force-pushed
the
jon/stealth-send-swap
branch
from
July 3, 2026 03:13
edc8fbd to
c12ad45
Compare
j0ntz
force-pushed
the
jon/stealth-send-swap
branch
2 times, most recently
from
July 30, 2026 07:56
2814d2c to
ca9ed1d
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
Contributor
Author
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit ca9ed1d. Configure here.
j0ntz
force-pushed
the
jon/stealth-send-swap
branch
2 times, most recently
from
August 17, 2026 21:18
01110fe to
c914c85
Compare
- EdgeSwapRequest accepts an optional toAddressInfo descriptor (toPluginId, toAddress, toMemos) as an alternative to toWallet, with exactly one of the two required. The core builds a synthetic, bridgified destination wallet backed by the real currencyConfig, so swap plugins receive an EdgeCurrencyWallet unchanged. - Destination memos (e.g. an XRP destination tag) use the descriptor only as GUI-to-core transport; plugins read them off the synthetic wallet's getMemos method (EdgeSyntheticDestinationWallet). - EdgeTxActionSwap.payoutWalletId and EdgeTxSwap.payoutWalletId become optional, since a swap-to-address destination has no payout wallet. - The pasted destination address and memo values are redacted from swap-quote logs.
A swap-to-address send, a private same-asset send, and a private cross-asset send all settle through a swap provider and carry every other field of EdgeTxActionSwap, so they stay swaps to existing consumers. What differs is the flow the user ran, which a UI needs in order to title the transaction. The optional swapType field names it, and the saved-action cleaner carries it so it survives a round trip.
A privacy feature powered by one provider needs two things the swap API could not express: a quote that must be sender-unlinkable rather than merely routed through the provider, and a provider the user's swap settings cannot switch off for that one feature.
j0ntz
force-pushed
the
jon/stealth-send-swap
branch
from
August 25, 2026 22:24
c914c85 to
e2bef4e
Compare
2 tasks
j0ntz
marked this pull request as draft
August 27, 2026 23:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Technical Design Document
stealth-send-swap.md
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Asana task
The core half of swap-to-address, which is what lets a swap target a pasted destination instead of a wallet the user holds.
EdgeSwapRequestaccepts an optionaltoAddressInfodescriptor as an alternative totoWallet, exactly one of the two required; the core builds a synthetic, bridgified destination wallet from it, backed by the realcurrencyConfig, so swap plugins receive anEdgeCurrencyWalletunchanged. Nothing Houdini-specific lives here: the same mechanism powers provider-agnostic send-to-any-asset, verified in-app with a ChangeNOW-routed cross-chain send.Judgement calls, with their alternates:
EdgeSwapToAddressInfocarries onlytoPluginId,toAddress, and optionaltoMemos.toPluginIdselects thecurrencyConfigand is not derivable from the request;toAddressis the payload; the destination token comes from the request's owntoTokenId. A descriptor also carryingtoTokenIdwas rejected as a two-sources-of-truth hazard.getMemoson the synthetic wallet. For memo-required payout chains such as an XRP destination tag, the descriptor'stoMemosis only the GUI-to-core transport; plugins never read the descriptor. The synthetic wallet exposesgetMemos()(EdgeSyntheticDestinationWallet), so the plugin-facing mechanism stays wallet-shaped and plugins keep one code path. Plugins reading a tag field off the descriptor was rejected to keep the plugin interface generic. A no-descriptor design is not available: the value has to cross the GUI-core bridge somewhere, and the descriptor is the plain-data channel that crosses yaob cleanly.EdgeTxActionSwap.payoutWalletIdandEdgeTxSwap.payoutWalletIdare both optional. A swap-to-address destination has no payout wallet;payoutAddresscarries the destination. Making only the action type optional left a latent inconsistency inEdgeTxSwap. Keeping them required and writing thesynthetic://<pluginId>id was rejected: it embeds fake wallet ids in persistent transaction metadata.EdgeTxActionSwap.swapType(swapSend,stealthSend,stealthSwapSend) names the send-shaped flows so a UI can title a transaction by the flow the user ran instead of inferring it. These stay swaps to every existing consumer, which a separateactionTypewould not; absent for a normal wallet-to-wallet swap.EdgeSwapRequest.privacy.'required'restricts a quote to routes that keep the sender unlinkable from the recipient, and a plugin that cannot offer one must decline rather than answer with a transparent route. A quote carries no route type back to the caller, so a silent downgrade would be undetectable; making the caller state its requirement puts the decision where the intent lives.EdgeSwapRequestOptions.forceEnabledlets a caller query named plugins the user switched off in their swap settings, because a private send is a send feature that happens to be powered by a swap provider. An explicitdisabledentry still wins, so a caller that disables everything except one provider cannot have that restriction defeated.quote.request.toWallet. One wallet is built perfetchSwapQuotescall and shared by every quote that call returns, so it closes when the last of them closes, and immediately when none survives or all reject. Without that, every quote refresh on a swap-to-address screen left another wallet in the table for the life of the account. Closing it with the first quote was rejected: the other quotes from the same call still carry it.The plugin-selection rule that
forceEnabledanddisabledencode is a named predicate (isSwapPluginQueryable) rather than an inline condition insidefetchSwapQuotes, with its full truth table under test. The corner that matters is exactly what a stealth send does in one call: every other plugin disabled, this one force-enabled.Testing. 177 mocha tests pass,
tscand eslint clean,verify-repo.shPASSED. 12 of those tests are new: the yaob bridge-crossing proof covers descriptor-only construction, plugin-faithful reads, memo round-trip throughgetMemos, and the synthetic wallet surviving the wire format back to the GUI; the plugin-selection truth table covers all eight flag combinations; two more cover the synthetic wallet's release, including a double-close that must not free it twice. In-app, linked into edge-react-gui with the Stealth Send UI: a real cross-chain send-to-address executed on the iOS simulator through this mechanism, deposit broadcast on chain, swap success scene reached.Note
Medium Risk
Changes the public swap request shape and payout metadata (optional
payoutWalletId), and introduces synthetic wallets on the quote path; swap plugins are unchanged but GUIs must handle new fields and redacted logging expectations.Overview
Adds swap-to-address:
EdgeSwapRequestcan usetoAddressInfo(toPluginId, pastedtoAddress, optionaltoMemos) instead oftoWallet, with exactly one required.fetchSwapQuotesresolves this viaresolveSwapRequest, building a bridgified synthetic destination wallet (makeSyntheticDestinationWallet) backed by the account’s realcurrencyConfigso swap plugins still see a normaltoWallet(including optionalgetMemosonEdgeSyntheticDestinationWallet).Quote aggregation gains
EdgeSwapRequestOptions.forceEnabledandisSwapPluginQueryable(disabledbeatsforceEnabled). Swap quote logging redacts pasted addresses and memo values.EdgeSwapRequest.privacy: 'required'is typed for callers that must not get a silent transparent route.Transaction metadata types add optional
swapTypeonEdgeTxActionSwap(swapSend,stealthSend,stealthSwapSend) and makepayoutWalletIdoptional onEdgeTxActionSwapandEdgeTxSwapwhen the payout is address-only. On-disk cleaners andSwapCurrencyErrorare updated for optionaltoWallet/toAddressInfo.Reviewed by Cursor Bugbot for commit c914c85. Bugbot is set up for automated code reviews on this repo. Configure here.