Skip to content

Add core-side synthetic destination for swap-to-address#724

Open
j0ntz wants to merge 1 commit into
masterfrom
jon/swap-to-address-core
Open

Add core-side synthetic destination for swap-to-address#724
j0ntz wants to merge 1 commit into
masterfrom
jon/swap-to-address-core

Conversation

@j0ntz

@j0ntz j0ntz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Phase A of swap-to-address ("private send"): let a swap target a pasted destination address instead of a destination wallet, without touching swap plugins.

account.fetchSwapQuotes now accepts an optional toAddressInfo descriptor (toPluginId, toTokenId, toAddress) on EdgeSwapRequest as an alternative to toWallet. Exactly one of the two is required. When toAddressInfo is supplied, the core builds a synthetic, bridgified EdgeCurrencyWallet-shaped destination from it, backed by the real currencyConfig the core already holds: currencyInfo and currencyConfig.allTokens are authentic, and getAddresses / getReceiveAddress return the pasted address. Plugins receive an EdgeCurrencyWallet unchanged.

Building the destination core-side and bridgifying it there is what makes this work. A GUI-built fake wallet cannot cross the yaob bridge (its function properties pack as '?' and the receiver throws on argument unpack), which the Phase 1 spike proved. A plain-data descriptor crosses fine, and the synthetic, bridgified core-side, survives the wire format both ways.

EdgeTxActionSwap.payoutWalletId becomes optional, since a swap-to-address destination has no payout wallet (payoutAddress already carries the destination).

The mechanism is provider-agnostic; nothing here is Houdini-specific.

Test

  • test/core/synthetic-wallet.test.ts is the core acceptance: it stands up the production yaob wire format (makeLocalBridge with a JSON cloneMessage), has the GUI side pass only the descriptor, builds the synthetic destination core-side, and a plugin-faithful consumer reads a working address, token, and currency info through the bridge. It also calls the returned synthetic back across the bridge, the direct inverse of the Phase 1 failure.
  • Full suite green: tsc clean, mocha 164 passing.
  • Plugin-level end-to-end (deliverable 5): on edge-exchange-plugins PR Loosen constraint for checking tx confirmation status #461 (jon/houdini-swap), a BTC to ETH quote and exchange order drives through the actual HoudiniSwap plugin with an address-only synthetic destination (replayed from cached fixtures, no live API). The pasted address flows through to payoutAddress, with payoutWalletId set to the synthetic id.
  • In-app end-to-end on the iOS simulator: with this core linked into edge-react-gui, a real BTC to ETH swap to a pasted ETH address (~60 USD) was driven through the address-only path (toAddressInfo, no destination wallet). The core built the synthetic destination, HoudiniSwap quoted it, and the swap executed to the in-app success scene. Proof screenshots attached.

Follow-ups for a GUI integration

  • GUI passes the descriptor instead of fabricating a wallet (the spike's GUI fake is discarded).
  • payoutWalletId becoming optional touches savedAction consumers in edge-react-gui; sweep those before relying on it in production.
  • Per-provider audit of the methods a plugin calls on toWallet before enabling the synthetic destination for that provider.
  • Pasted-address validation (Houdini per-chain regex for the MVP; parseUri on EdgeCurrencyConfig is the post-MVP generalization).

Asana: https://app.asana.com/0/1215088146871429/1215645666041229


Note

Medium Risk
Changes the swap request contract and quote path; plugins still receive toWallet, but GUI and savedAction code must handle optional payoutWalletId and synthetic wallet ids.

Overview
Adds swap-to-address (e.g. private send): EdgeSwapRequest can use toAddressInfo (toPluginId, toAddress) instead of toWallet, with exactly one required. fetchSwapQuotes resolves the destination through resolveSwapRequest, which validates the plugin/token and substitutes a core-built, bridgified synthetic EdgeCurrencyWallet so existing swap plugins still see a normal toWallet.

New makeSyntheticDestinationWallet uses real currencyConfig, synthetic ids under synthetic://, and address-only getAddresses / getReceiveAddress. Swap logging redacts pasted destination addresses.

EdgeTxActionSwap.payoutWalletId is optional (cleaner updated); SwapCurrencyError resolves the destination plugin from toWallet or toAddressInfo. Tests cover bridge round-trip behavior for the synthetic wallet.

Reviewed by Cursor Bugbot for commit bfcfcbb. Bugbot is set up for automated code reviews on this repo. Configure here.

@j0ntz

j0ntz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence: in-app swap-to-address via synthetic destination

agent proof 1215645666041229 01 swap to address quote

agent proof 1215645666041229 01 swap to address quote

agent proof 1215645666041229 02 swap to address success

agent proof 1215645666041229 02 swap to address success

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

Comment thread src/core/swap/swap-api.ts
Comment thread src/core/swap/swap-api.ts
@j0ntz j0ntz force-pushed the jon/swap-to-address-core branch from 2f9a0fa to 0a8c8b9 Compare June 12, 2026 02:54
@j0ntz

j0ntz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Both addressed in 0a8c8b9:

  • resolveSwapRequest now rejects a toAddressInfo.toTokenId that does not match the request toTokenId, so a mismatch can no longer produce a wrong-asset quote.
  • The pasted destination address is redacted from the swap-request warning log.

Comment thread src/types/types.ts
Comment thread src/core/swap/swap-api.ts
@j0ntz j0ntz force-pushed the jon/swap-to-address-core branch from 0a8c8b9 to 5ee8085 Compare June 12, 2026 03:00
@j0ntz

j0ntz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Both addressed in 5ee8085:

  • asEdgeTxActionSwap cleaner now uses asOptional(asString) for payoutWalletId, matching the now-optional public type, so a saved action without it no longer fails when read from disk or validated in makeSpend.
  • The per-plugin quote.request warning log now redacts toAddressInfo.toAddress as well, closing the second logging path.

Let account.fetchSwapQuotes accept an optional toAddressInfo descriptor
(toPluginId, toTokenId, toAddress) as an alternative to a destination
toWallet. The core validates exactly one of the two, then builds a
synthetic, bridgified EdgeCurrencyWallet from the descriptor, backed by
the real currencyConfig it already holds. getAddresses and
getReceiveAddress return the pasted address; currencyInfo and
currencyConfig.allTokens come from the real plugin config. Swap plugins
receive an EdgeCurrencyWallet unchanged.

Make EdgeTxActionSwap.payoutWalletId optional, since a swap-to-address
destination has no payout wallet.

A bridge-crossing proof test stands up the production yaob wire format
(makeLocalBridge with a JSON cloneMessage): the GUI passes only the
descriptor, the core builds the synthetic destination, and a
plugin-faithful consumer reads a working address, token, and currency
info through the bridge. This inverts the Phase 1 failure where a
GUI-built fake wallet could not cross the bridge.
@j0ntz j0ntz force-pushed the jon/swap-to-address-core branch from 5ee8085 to bfcfcbb Compare June 16, 2026 18:05

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bfcfcbb. Configure here.

Comment thread src/core/swap/swap-api.ts
return {
...request,
toWallet: makeSyntheticDestinationWallet(currencyConfig, toAddress)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Resolved swap request breaks re-quote

Medium Severity

When toAddressInfo is resolved, the core adds a synthetic toWallet but leaves toAddressInfo on the same EdgeSwapRequest. Quotes that echo that request then carry both fields, so a later fetchSwapQuotes call with quote.request fails the “exactly one of toWallet or toAddressInfo” check.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bfcfcbb. Configure here.

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