Skip to main content

Withdrawals

This guide covers withdrawing tokens from your private balance to any public address.

Basic Withdrawal

const result = await sdk.vault.unshield({
  tokenAddress: '0x...token-address',
  amount: 1000000000000000000n, // 1 token
  recipientAddress: '0x...recipient-address',
});

console.log('Transaction hash:', result.txHash);

Withdrawal Parameters

interface UnshieldParams {
  tokenAddress: Hex;          // Token to unshield
  amount: bigint;             // Amount in wei
  recipientAddress: Hex;      // Destination address
  onProgress?: OnProgress;    // Progress callback
}
ParameterTypeRequiredDescription
tokenAddressHexYesToken contract address
amountbigintYesAmount in smallest unit
recipientAddressHexYesDestination public address
onProgressfunctionNoProgress callback

Progress Tracking

await sdk.vault.unshield({
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
  recipientAddress: '0x...',
  onProgress: ({ step, message, txHash }) => {
    console.log(`Step: ${step}`);
    console.log(`Message: ${message}`);
    if (txHash) {
      console.log(`Transaction: ${txHash}`);
    }
  },
});

Withdrawal Steps

StepDescription
preparingPreparing withdrawal data
signingSigning transaction data
provingGenerating zero-knowledge proof
unshieldingExecuting withdrawal transaction
enum UnshieldStep {
  preparing = 'preparing',
  signing = 'signing',
  proving = 'proving',
  unshielding = 'unshielding',
}

Withdrawal Result

interface UnshieldResult {
  txHash: Hex;          // Main withdrawal transaction
  amount: bigint;       // Amount withdrawn
}

Unwrapping WETH to ETH

WETH unwrapping is a separate step after the withdrawal settles on-chain. Call unwrapWeth() once getUnshieldStatus() confirms the withdrawal is complete:
const WETH_ADDRESS = '0x4200000000000000000000000000000000000006';

// Step 1: Unshield WETH from the privacy pool
const result = await sdk.vault.unshield({
  tokenAddress: WETH_ADDRESS,
  amount: 1000000000000000000n,
  recipientAddress: '0x...my-address',
});

// Step 2: Poll until the withdrawal settles on-chain
// (use getUnshieldStatus or the polling mechanism)

// Step 3: Unwrap WETH to ETH (connected wallet must match recipient)
const unwrapTxHash = await sdk.vault.unwrapWeth(
  1000000000000000000n,
  '0x...my-address',
);
console.log('Unwrap tx:', unwrapTxHash);

Withdraw to Self

Withdraw to your connected wallet:
const myAddress = sdk.auth.getAddress();

await sdk.vault.unshield({
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
  recipientAddress: myAddress,
});

Withdraw to Any Address

You can withdraw to any valid Ethereum address:
import { isEvmAddress } from '@testinprod-io/privacy-boost';

const recipientAddress = '0x...';

// Validate address
if (!isEvmAddress(recipientAddress)) {
  throw new Error('Invalid recipient address');
}

await sdk.vault.unshield({
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
  recipientAddress,
});

Partial Withdrawals

Withdraw any amount up to your shielded balance:
// Check available balance first
const balance = await sdk.vault.getBalance(tokenAddress);

if (amount > balance) {
  throw new Error('Insufficient shielded balance');
}

await sdk.vault.unshield({
  tokenAddress,
  amount, // Any amount <= balance
  recipientAddress,
});

Error Handling

try {
  await sdk.vault.unshield(params);
} catch (error) {
  switch (error.code) {
    case 'INSUFFICIENT_BALANCE':
      console.log('Not enough shielded balance');
      break;
    case 'INVALID_RECIPIENT':
      console.log('Invalid recipient address');
      break;
    case 'PROOF_GENERATION_FAILED':
      console.log('Failed to generate proof');
      break;
    case 'WITHDRAWAL_FAILED':
      console.log('Withdrawal transaction failed');
      break;
    default:
      console.log('Withdrawal error:', error.message);
  }
}

Estimating Fees

// The withdrawal fee is included in the amount
// No separate gas estimation needed as the prover pays gas

UI Example

import { useState } from 'react';

function UnshieldForm() {
  const [amount, setAmount] = useState('');
  const [recipient, setRecipient] = useState('');
  const [step, setStep] = useState('');
  const [loading, setLoading] = useState(false);

  const handleUnshield = async () => {
    setLoading(true);
    try {
      const parsedAmount = await sdk.vault.parseAmount(tokenAddress, amount);

      await sdk.vault.unshield({
        tokenAddress,
        amount: parsedAmount,
        recipientAddress: recipient as Hex,
        onProgress: ({ step, message }) => {
          setStep(message);
        },
      });

      alert('Withdrawal successful!');
    } catch (error) {
      alert(`Withdrawal failed: ${error.message}`);
    } finally {
      setLoading(false);
      setStep('');
    }
  };

  return (
    <div>
      <input
        type="text"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
        placeholder="Amount"
        disabled={loading}
      />
      <input
        type="text"
        value={recipient}
        onChange={(e) => setRecipient(e.target.value)}
        placeholder="Recipient address"
        disabled={loading}
      />
      <button onClick={handleUnshield} disabled={loading}>
        {loading ? step || 'Processing...' : 'Withdraw'}
      </button>
    </div>
  );
}

Best Practices

1. Validate Balance Before Withdrawal

async function safeUnshield(params: UnshieldParams) {
  const balance = await sdk.vault.getBalance(params.tokenAddress);

  if (params.amount > balance) {
    throw new Error(
      `Insufficient balance. Available: ${balance}, Requested: ${params.amount}`
    );
  }

  return sdk.vault.unshield(params);
}

2. Validate Recipient Address

import { isEvmAddress } from '@testinprod-io/privacy-boost';

function validateRecipient(address: string) {
  if (!isEvmAddress(address)) {
    throw new Error('Invalid Ethereum address');
  }
  if (address === '0x0000000000000000000000000000000000000000') {
    throw new Error('Cannot withdraw to zero address');
  }
}

3. Handle Long Operations

Withdrawals can take time due to proof generation:
const EXPECTED_DURATION = 30000; // 30 seconds

// Show estimated time
setStatus('Generating proof... This may take up to 30 seconds.');

await sdk.vault.unshield({
  ...params,
  onProgress: ({ step }) => {
    if (step === 'proving') {
      setStatus('Generating zero-knowledge proof...');
    } else if (step === 'unshielding') {
      setStatus('Submitting withdrawal transaction...');
    }
  },
});

Next Steps