Private Swaps
Exchange tokens privately within the privacy pool.
Overview
Private swaps enable atomic token exchanges (e.g., zkUSD ↔ zkEUR) without revealing:
- Your identity
- Swap amounts
- Your total balance
┌─────────────────────────────────────────────────────────┐
│ Private Swap │
│ │
│ Taker Maker │
│ ┌─────────┐ ┌─────────┐ │
│ │ zkUSD │ ────────────────────► │ zkUSD │ │
│ │ note │ swap │ note │ │
│ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ zkEUR │ ◄──────────────────── │ zkEUR │ │
│ │ note │ swap │ note │ │
│ └─────────┘ └─────────┘ │
│ │
│ Atomic: Both transfers succeed or both fail │
└─────────────────────────────────────────────────────────┘Quick Start
Simple Swap (Recommended)
The easiest way to swap tokens:
typescript
import { PrivacyClient } from '@zkprivacy/sdk';
// Swap 100 zkUSD for zkEUR
const result = await client.swap({
fromToken: 'zkUSD',
toToken: 'zkEUR',
amount: parseUnits('100', 6),
});
console.log('Received:', result.receivedAmount);The SDK handles:
- Finding a market maker
- Negotiating the rate
- Creating the swap intent
- Generating proofs
- Executing atomically
How It Works
1. Taker Creates Intent
You (the taker) create a swap intent specifying what you want to trade:
typescript
const intent = await client.createSwapIntent({
fromToken: BigInt(DEPLOYMENTS.remote.tokens.zkUSD),
toToken: BigInt(DEPLOYMENTS.remote.tokens.zkEUR),
fromAmount: parseUnits('100', 6),
minToAmount: parseUnits('92', 6), // Minimum acceptable
expiry: Math.floor(Date.now() / 1000) + 3600, // 1 hour
});2. Intent Posted On-Chain
The encrypted intent is posted as an event:
typescript
await client.postSwapIntent(intent);3. Maker Fills the Swap
A market maker sees the intent, decrypts it, and fills if the rate is acceptable:
typescript
// Market maker's side
await makerClient.fillSwapIntent(intent, {
toAmount: parseUnits('93', 6), // Actual amount to give
});4. Atomic Execution
Both proofs are verified and executed atomically on-chain. Either both succeed or neither does.
Privacy Model
What's Hidden
| Observer | Can See | Cannot See |
|---|---|---|
| Public | "A swap happened" | Who, amounts, rates |
| Maker | Token pair, amounts | Taker's identity, history |
| Taker | Token pair, amounts | Maker's identity, history |
Per-Swap Unlinkability
Each swap uses fresh addresses:
- Taker: New NPK derived per swap
- Maker: Stealth address per swap
This means:
- Multiple swaps by same taker are unlinkable
- Multiple fills by same maker are unlinkable
Configuration
Setting Market Maker URL
typescript
const client = new PrivacyClient({
// ... other config
makerUrl: 'https://maker.zkprivacy.dev',
});Checking Available Pairs
typescript
const pairs = await client.getSwapPairs();
// [{ from: 'zkUSD', to: 'zkEUR', rate: 0.93, available: 50000n }]Getting a Quote
typescript
const quote = await client.getSwapQuote({
fromToken: 'zkUSD',
toToken: 'zkEUR',
amount: parseUnits('1000', 6),
});
console.log('Rate:', quote.rate); // e.g., 0.93
console.log('You get:', quote.toAmount); // e.g., 930 zkEUR
console.log('Fee:', quote.fee); // e.g., 0.3%Advanced Usage
Manual Swap Flow
For full control over the swap process:
typescript
// 1. Create intent with custom parameters
const intent = await client.createSwapIntent({
fromToken: BigInt(tokenA),
toToken: BigInt(tokenB),
fromAmount: amount,
minToAmount: minReceive,
expiry: expiry,
});
// 2. Get intent details for inspection
console.log('Intent hash:', intent.intentHash);
console.log('Encrypted to maker:', intent.encryptedIntent);
// 3. Post to chain
const tx = await client.postSwapIntent(intent);
await waitForTransactionReceipt(tx);
// 4. Wait for fill (or timeout)
const result = await client.waitForSwapFill(intent.intentHash, {
timeout: 60000, // 1 minute
});
if (result.filled) {
console.log('Swap complete! Received:', result.receivedAmount);
} else {
console.log('Swap expired, funds returned');
}Canceling a Swap
If a swap hasn't been filled:
typescript
await client.cancelSwapIntent(intent.intentHash);Error Handling
typescript
try {
await client.swap({ ... });
} catch (error) {
if (error.message.includes('No liquidity')) {
console.log('Market maker has insufficient balance');
} else if (error.message.includes('Rate expired')) {
console.log('Quote expired, try again');
} else if (error.message.includes('Slippage exceeded')) {
console.log('Rate moved too much');
}
}Supported Pairs
| Pair | Direction | Notes |
|---|---|---|
| zkUSD ↔ zkEUR | Both | Primary pair |
| zkUSD ↔ zkPLN | Both | Available |
| zkEUR ↔ zkPLN | Both | Available |
TIP
ETH swaps are not yet supported. Convert to zkUSD first via shield/unshield.