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-Chain | Hidden |
|---|---|
| Recipient address | Which note was spent |
| Amount withdrawn | Your total balance |
| Token type | Your ZK address |
| Timestamp | Previous transactions |
For maximum privacy, consider:
- Withdrawing to a fresh address
- Using multiple smaller withdrawals
- Waiting between unshield and using funds