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):
prepare swap accounts — session in/out ATAs + the pool out ATA.
unlock swap input (PUBLIC) — withdraw the whole input note A to the
session's ATA.
swap — router swaps A→B into the session's out ATA.
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:
cluster
router
notes
devnet
DevnetStubRouter
fixed-rate swap between two Token-2022 test mints, backed by operator reserves. No Jupiter liquidity on devnet. Requires opts.stub.
fork / mainnet
JupiterRouter
real 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 test → tests/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 collectbash /tmp/p15-pswap/validator.sh # in another shelltsx 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).