Skip to content

Unshield (Withdraw)

Unshielding converts private notes back to public tokens at an EOA address.

Basic Unshield

typescript
import { parseEther } from 'viem';

await client.sync();

const txHash = await client.unshield({
  recipient: '0x...',  // Your EOA address
  amount: parseEther('100'),
});

How It Works

Your Private Notes                  ZK Proof                      Your EOA
┌────────────────┐                                               
│  Note          │     ┌────────────────────────────────────┐    ┌─────────────┐
│  500 USDC      │────▶│  Proves:                           │───▶│  100 USDC   │
└────────────────┘     │  1. I own this note                │    │  (public)   │
                       │  2. Nullifier is valid              │    └─────────────┘
                       │  3. Amount matches withdrawal       │    
                       └────────────────────────────────────┘    ┌─────────────┐
                                                                 │  Change     │
                                                                 │  400 USDC   │
                                                                 │  (private)  │
                                                                 └─────────────┘

Token Withdrawals

Specify the token to withdraw:

typescript
await client.unshield({
  recipient: '0x...',
  amount: parseUnits('100', 6),
  tokenId: BigInt(deployment.tokens.zkUSD),
});

Partial Withdrawals

Unshield less than your balance - the rest stays private:

typescript
const balance = client.getBalance();  // 500 USDC

await client.unshield({
  recipient: '0x...',
  amount: parseUnits('100', 6),  // Withdraw 100
  // 400 stays in privacy pool as change note
});

Full Withdrawal

To withdraw everything:

typescript
const balance = client.getBalance();

await client.unshield({
  recipient: '0x...',
  amount: balance,  // Withdraw full balance
});

Proof Generation

Like transfers, unshield requires proof generation:

  • Time: 20-40 seconds (simpler than transfer)
  • Circuit: Uses the unshield circuit

Recipient Address

The recipient must be a valid EOA or contract that can receive tokens:

typescript
// Your own address
const myAddress = await walletClient.getAddresses()[0];

await client.unshield({
  recipient: myAddress,
  amount: parseEther('100'),
});

WARNING

Tokens go to a public address. The withdrawal is visible on-chain.

With Relayer (Gasless)

When using a relayer, the unshield transaction is submitted without needing gas:

typescript
const client = new PrivacyClient({
  relayerUrl: 'https://relayer.zkprivacy.dev',
  // ...
});

// No ETH needed in your wallet
await client.unshield({
  recipient: '0x...',
  amount: parseEther('100'),
});

After Unshield

Tokens appear at the recipient address after confirmation:

typescript
const txHash = await client.unshield({ ... });

// Wait for confirmation
await publicClient.waitForTransactionReceipt({ hash: txHash });

// Check public balance
const publicBalance = await publicClient.readContract({
  address: tokenAddress,
  abi: erc20Abi,
  functionName: 'balanceOf',
  args: [recipientAddress],
});
console.log('Public balance:', publicBalance);

// Sync private balance
await client.sync();
console.log('Private balance:', client.getBalance());

Privacy Considerations

Unshielding creates a public record:

Visible On-ChainHidden
Recipient addressWhich note was spent
Amount withdrawnYour total balance
Token typeYour ZK address
TimestampPrevious transactions

For maximum privacy, consider:

  • Withdrawing to a fresh address
  • Using multiple smaller withdrawals
  • Waiting between unshield and using funds

Released under the MIT License.