Skip to content

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

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

ObserverCan SeeCannot See
Public"A swap happened"Who, amounts, rates
MakerToken pair, amountsTaker's identity, history
TakerToken pair, amountsMaker'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

PairDirectionNotes
zkUSD ↔ zkEURBothPrimary pair
zkUSD ↔ zkPLNBothAvailable
zkEUR ↔ zkPLNBothAvailable

TIP

ETH swaps are not yet supported. Convert to zkUSD first via shield/unshield.

Released under the MIT License.