Private swap (v1) — tradeoffs

Private swap is the wedge: a user holding a private note of asset A ends up with a private note of asset B, with the amounts and the graph hidden, and both legs recoverable by scanning the announce bus. The swap venue is abstracted behind the SwapRouter seam so the privacy orchestration is identical regardless of where liquidity comes from.

Flow

planPrivateSwap (packages/blind-mode/src/private-swap.ts) returns four session-signed tx groups (1 wallet popup to fund the session, like every blind flow):

  1. prepare swap accounts — session in/out ATAs + the pool out ATA.
  2. unlock swap input (PUBLIC) — withdraw the whole input note A to the session's ATA.
  3. swap — router swaps A→B into the session's out ATA.
  4. make output private + record — deposit B as a fresh private note + announce both legs (an in for the output note, an out-receipt for the consumed input).

Routers

getSwapRouter(cluster, opts) (src/swap-router.ts) picks the venue:

clusterrouternotes
devnetDevnetStubRouterfixed-rate swap between two Token-2022 test mints, backed by operator reserves. No Jupiter liquidity on devnet. Requires opts.stub.
fork / mainnetJupiterRouterreal Jupiter v6 liquidity (lite-api.jup.ag), wrapAndUnwrapSol: false (the note legs already move wrapped tokens).

SWAP_ROUTER=stub|jupiter overrides the cluster default.

v1 tradeoffs (be honest)

  • Whole-note only. v1 swaps an entire input note — no partial split, so no transfer proof on the input leg. To swap a different amount, first send yourself a note of that size.
  • The swap input is PUBLIC. Step 2 withdraws note A to the session's ATA before the swap, so the input amount and asset are visible on-chain for that moment (labeled PUBLIC in the plan). Privacy is restored when B is re-deposited as a note. What stays hidden across the whole operation: the link between your wallet and the swap (the session key is ephemeral), and the output note.
  • The AMM sees the amount. Jupiter/Orca necessarily see the swapped amount and the two assets — that's inherent to using public liquidity. p15 hides who and the resulting balance, not the trade size from the venue.
  • Slippage. minOut is the floor; the router rejects a quote below it.

v2 (deferred)

Integrating the swap-auth circuit (circuits/swap-auth) would let a ZK proof authorize the swap without revealing the amount on the private leg. Designed separately; not wired into planPrivateSwap yet.

Verify

  • Unit: npm testtests/unit/private-swap.test.ts (tx grouping, signer tags, both announce legs, getSwapRouter selection). Runs offline.
  • Devnet e2e (stub): tsx scripts/12-private-swap-e2e.ts — full private-in → swap → private-out on devnet.
  • Mainnet-fork e2e (Jupiter): the full flow against cloned mainnet liquidity, no real SOL — manual two-phase (needs the agave 4.1 validator + the built note_vault.so):
    Shell
    tsx scripts/15-jupiter-private-swap-fork.ts collect
    bash /tmp/p15-pswap/validator.sh        # in another shell
    tsx scripts/15-jupiter-private-swap-fork.ts swap
    Asserts: input wSOL note consumed, output USDC note live, and scanning the announce bus recovers both legs (in + out-receipt).