Repository navigation
Add core-side synthetic destination for swap-to-address - #730
Conversation
edc8fbd to
c12ad45
Compare
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. |
|
bugbot run |
01110fe to
c914c85
Compare
c914c85 to
e2bef4e
Compare
e2bef4e to
24613ad
Compare
peachbits
left a comment
There was a problem hiding this comment.
The synthetic destination wallet is the right seam: plugins receive a normal toWallet and need no changes. The account-facing types are where the design leaks, and the fixes are type-level rather than a rewrite. Three requests, detailed inline and below:
- Keep
EdgeSwapRequest.toWalletrequired and add a second request shape,EdgeSwapSendRequest, for send-to-address. The core already resolves every request to one with atoWalletbefore plugins orquote.requestsee it, so the optionality only exists at this boundary, and the GUI pays for it withrequireDestinationWalletin five places. - Give send-to-address transactions their own action type (
swapSend), written by the plugin at quote time, instead ofswapType?stamped by the GUI after broadcast pluspayoutWalletId?on two types. The plugin has every input it needs now thatprivacyis on the request. - Add
parseUritoEdgeCurrencyConfig, loading tools on demand the wayimportKeyandgetTokenDetailsalready do. That gives the GUI the plugins' own parsers for chains the user holds no wallet on.
privacy: 'required' on the request and forceEnabled on the options are fine as they are.
src/types/types.ts:1781 (EdgeCurrencyConfig)
Request: add parseUri to EdgeCurrencyConfig, loading tools on demand through getCurrencyTools the way importKey (line 1810) and getTokenDetails already do on this object.
readonly parseUri: (
uri: string,
currencyCode?: string,
customTokens?: EdgeMetaToken[]
) => Promise<EdgeParsedUri>Today parseUri exists only on EdgeCurrencyWallet, so a chain the user holds no wallet on has no reachable parser. The GUI PR fills that gap with a hand-written BIP-21/EIP-681 splitter plus a table of provider-supplied regexes, four of which had to be corrected. The plugins already implement all of it: BIP-21, EIP-681 pay- and @chainId, cashaddr and legacy forms, checksums, memos as uniqueIdentifier, amounts in native units. Tools are cached per session, so the cost is one constructor per chain the user pastes for.
Send-to-any is the immediate consumer. Two existing GUI paths could use the same method later, and don't have to now: the payment deep-link handler, which loops loaded wallets and falls back to a five-row scheme table to offer wallet creation, and the scan flow, which can only parse with the scene's wallet.
src/types/types.ts:888 (EdgeParsedUri)
Suggested optional field, so the label source for toAddresses has somewhere to live once currency plugins implement it:
/** Every `getAddresses` label that applies to `publicAddress`, for chains with more than one address format. Absent means `publicAddress` only. */
addressTypes?: string[]The plugin that validated the address is the one that knows its format: UTXO has verifyAddress and guessAddressTypeFromAddress in its keymanager, Zcash can go by prefix or the SDK's per-type validators. Nothing needs to implement it in this PR; the field lets the send flow drop the Houdini special case later without another core change.
|
All three requests are in: |
24613ad to
8866564
Compare
8866564 to
f283aaf
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.
A caller that has no wallet on a chain (such as a send to an address through a swap provider) still needs the chain's own URI parsing, so EdgeCurrencyConfig.parseUri reaches the plugin's currency tools the same way importKey does. EdgeParsedUri.addressTypes lets a chain with several address formats say which getAddresses labels a parsed address matches.
f283aaf to
a32a644
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a32a644. Configure here.

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. A quote's close releases its share even when the plugin's own close throws.EdgeCurrencyConfig.parseUrireaches the plugin's currency tools the same wayimportKeydoes, so a caller with no wallet on the destination chain (a send to a pasted address) still gets that chain's own URI parsing, destination tag included. It runs the same post-processing asEdgeCurrencyWallet.parseUrithrough one shared helper: the account's custom tokens go to the plugin, and acurrencyCodein the result is upgraded to atokenId.EdgeParsedUri.addressTypeslets a chain with several address formats say whichgetAddresseslabels a parsed address matches. Reimplementing URI parsing in the GUI was rejected: it would drift from the plugins that already own each chain's format.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. 188 mocha tests pass (1 pending) on the branch rebased onto current master,
tscand eslint clean,verify-repo.shPASSED. 18 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
High Risk
Changes swap quoting, bridged object lifetimes, and public swap/URI types used for send and stealth flows; mistakes could leak destinations, leave yaob leaks, or quote wrong providers.
Overview
Adds swap-to-address:
fetchSwapQuote/fetchSwapQuotesnow accept anEdgeSwapSendRequest(destination plugin, addresses, optional memos) in addition to wallet-to-wallet swaps. The core resolves send requests into a normalEdgeSwapRequestby building a bridgified synthetic destination wallet (makeSyntheticDestinationWallet) backed by the account’s realcurrencyConfig, so plugins still read payout data fromtoWallet. One synthetic wallet is shared per quote batch and released when the last wrapped quote closes, including when all quotes fail or a pluginclosethrows.Also extends swap quoting with
EdgeSwapRequest.privacy?: 'required'(plugins must decline if they cannot offer an unlinkable route) andEdgeSwapRequestOptions.forceEnabledplusisSwapPluginQueryable(explicitdisabledstill wins over user-off + force-on).Transaction metadata gains
EdgeTxActionSwapSend(swapSend) with validation in cleaners.EdgeCurrencyConfig.parseUriexposes chain URI parsing without a wallet via sharedparseCurrencyUri(custom tokens +currencyCode→tokenIdupgrade); walletparseUridelegates to the same helper.EdgeParsedUri.addressTypesis added for multi-format addresses.Reviewed by Cursor Bugbot for commit a32a644. Bugbot is set up for automated code reviews on this repo. Configure here.
Test evidence
24613adAdd swap route-privacy and force-enabled request options
agent proof 1216251688512498 03 send to any quote
agent proof 1216251688512498 04 send to any success