Withdraw and swap
A withdraw-and-swap spends notes and delivers a different token to the recipient: shielded USDC out as USDT, shielded ETH out as USDC on another chain. It is a withdrawal whose processor is RelaySwaps rather than PrivacyPoolRelay, and whose payout routing contains the full calldata for a DEX aggregator. Because that routing is bound in the proof's context, the relayer that submits the transaction can execute exactly the route you quoted or nothing at all.
The shipped aggregator is LiFi. The SDK abstracts it behind ISwapQuoteProvider, so another provider can be supplied; LiFiSwapQuoteProvider is the implementation in the box.
How it works
The flow is quote-heavy because the route has to be fixed before the proof is made, and the proof takes tens of seconds.
- Relayer fee estimate. Ask the relayer what it will charge to relay this asset and amount. The fee comes out of the withdrawn token before the swap. If you ask for
extraGas, the relayer adds an ETH top-up for the recipient (priced from gas, clamped at a per-chain ceiling) and folds it into the fee, so you pre-pay it and the relayer is reimbursed. - Swap quote. Fetch the aggregator's calldata for
withdrawAmount - feeAmountof the input token intooutputToken, from RelaySwaps to the recipient. The response includes a minimum output. - Committed relayer quote. Send the calldata back to the relayer, which ABI-encodes the complete
SwapRouting(target, approval address, output token, recipient, fee, output floor, gas top-up, dust recipient, calldata) and signs it with an expiry. - Validate. The SDK decodes the signed commitment once and checks every field against what you asked for, including that the embedded calldata equals yours, that
amountSent = amount + fee, that the fee is within the Entrypoint'smaxRelayFee, and that the chain matches the pool. - Prove. The transact proof's
amountOutisamountSent, and itscontextcommits to the routing, calldata included. - Relay. The relayer calls
RelaySwaps.execute(orexecuteUnwrappedfor a yield-share note). The contract re-readsmaxRelayFeeon-chain, pulls the withdrawal, pays the fee, runs the swap, requires the recipient's balance rose by at least the floor, forwards the gas top-up, and sweeps dust.
Quotes are short-lived and proof generation is not. If the quote expires while you are proving, nothing has failed; re-quote and prove again.
How to swap out
Configure the session with a swap quote provider, then run the two calls:
// At session build time:
// builder.withSwapQuoteProvider(new LiFiSwapQuoteProvider({ integrator, apiKey }))
const quote = await session.prepareSwapQuote({
relayerInfo,
amount: withdrawAmount, // Hex, in the input token
tokenId: usdcAddress, // the shielded asset you are spending
outputToken: usdtAddress, // what the recipient receives
recipientAddress: publicDestination,
slippage: 0.005, // optional, fraction
extraGas: false, // optional ETH top-up for the recipient
});
console.log("estimated output:", quote.estimatedOutput, "min:", quote.minOutputAmount);
const receipt = await session.relayWithdrawAndSwap({
inputCommitments: [noteToSpend.commitment],
amount: withdrawAmount,
tokenId: usdcAddress,
recipientAddress: publicDestination,
outputToken: usdtAddress,
minOutputAmount: quote.minOutputAmount,
swapCalldata: quote.swapCalldata,
selectedQuote: quote.selectedQuote,
swapTarget: quote.swapTarget,
approvalAddress: quote.approvalAddress,
dustRecipient: quote.dustRecipient,
});
console.log("swap tx:", receipt.txHash);
Pass toChainId to prepareSwapQuote for a cross-chain delivery. The aggregator then bridges, and because no output lands on the source chain the on-chain output check is skipped: the routing carries a zero recipient, and delivery is enforced by the aggregator's destination-side executor against destinationMinOutputAmount. Keep fallbackAddress set for cross-chain routes; it is where a failed bridge leg refunds.
Constraints
- The route is frozen at proving time. That is the guarantee, and it is also why a stale quote means re-proving rather than resubmitting.
- Allowlisted routers only.
swapTargetandapprovalAddressmust be on the RelaySwaps owner'sapprovedRouters. The SDK checks this before proving and refuses an address outside the list (UnallowedLiFiAddress); the contract checks again on-chain. - A real output floor is mandatory on same-chain swaps. The contract rejects a named recipient with a zero
minOutputAmount, because a zero floor would let the calldata route output anywhere. The SDK always sets one from the quote. - Yield-share notes unwrap first. Spending ppUSDC through this path uses
executeUnwrapped: the relayer supplies the wrapper's zap from an allowlist it derives on-chain at boot, the shares are redeemed to USDC after the vault pays them, and the swap runs on the underlying. Your call is the same; the relayer chooses the entry point from the asset. - Unsupported output tokens are withdrawals. Swapping into a token the pool does not list is simply a withdrawal that changes asset on the way out. There is no way to hold an unlisted asset privately; to stay in the pool, use reshield.
- Layout drift is fatal by design. The SDK and relayer assert the contract's
ROUTING_TYPEHASHagainst their ownSwapRoutingencoding (RelaySwapsLayoutMismatch), so an outdated client cannot produce a routing the contract decodes differently.
Behind the scenes
session.prepareSwapQuote(...) then session.relayWithdrawAndSwap(...). Requires withSwapQuoteProvider(...) at build time.RelaySwaps.execute(proof, transactParams, noteData), or .executeUnwrapped(..., zap) for a yield share; either calls PoolVault.transact and then the aggregator.POST /v1/quote/evm/{chainId}/swap for both the estimate and the committed quote, then POST /v1/relay/evm/{chainId}/relay-swap. The relayer publishes its RelaySwaps address as contracts.relaySwaps in /v1/details; it is absent on a chain without swaps.transact_NxM with amountOut = amountSent and context = keccak(processor, routing). Nothing swap-specific is proven; the binding does the work.What's next
- Reshield: swap and land back in the pool as a fresh note.
- Swap and deposit: the inbound sibling, from any wallet token into a shielded note.
- Relaying: quotes, expiry, and what a relayer can and cannot do.