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.withdraw({
  tokenAddress: '0x...token-address',
  amount: 1000000000000000000n, // 1 token
  recipientAddress: '0x...recipient-address',
});

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

Withdrawal Parameters

interface WithdrawParams {
  tokenAddress: Hex;          // Token to withdraw
  amount: bigint;             // Amount in wei
  recipientAddress: Hex;      // Destination address
  unwrapWeth?: boolean;       // Unwrap WETH to ETH
  onProgress?: OnProgress;    // Progress callback
}
ParameterTypeRequiredDescription
tokenAddressHexYesToken contract address
amountbigintYesAmount in smallest unit
recipientAddressHexYesDestination public address
unwrapWethbooleanNoConvert WETH to ETH
onProgressfunctionNoProgress callback

Progress Tracking

await sdk.vault.withdraw({
  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
unwrappingUnwrapping WETH to ETH (if applicable)
enum WithdrawStep {
  preparing = 'preparing',
  signing = 'signing',
  proving = 'proving',
  unshielding = 'unshielding',
  unwrapping = 'unwrapping',
}

Withdrawal Result

interface WithdrawResult {
  txHash: Hex;          // Main withdrawal transaction
  amount: bigint;       // Amount withdrawn
  unwrapTxHash?: Hex;   // WETH unwrap transaction (if applicable)
}

Withdrawing as ETH

To receive native ETH instead of WETH:
const WETH_ADDRESS = '0x4200000000000000000000000000000000000006';

const result = await sdk.vault.withdraw({
  tokenAddress: WETH_ADDRESS,
  amount: 1000000000000000000n,
  recipientAddress: '0x...',
  unwrapWeth: true, // Unwrap to ETH
});

console.log('Withdraw tx:', result.txHash);
console.log('Unwrap tx:', result.unwrapTxHash);

Withdraw to Self

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

await sdk.vault.withdraw({
  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.withdraw({
  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.withdraw({
  tokenAddress,
  amount, // Any amount <= balance
  recipientAddress,
});

Error Handling

try {
  await sdk.vault.withdraw(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;
    case 'UNWRAP_FAILED':
      console.log('WETH unwrap 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 WithdrawForm() {
  const [amount, setAmount] = useState('');
  const [recipient, setRecipient] = useState('');
  const [unwrapWeth, setUnwrapWeth] = useState(false);
  const [step, setStep] = useState('');
  const [loading, setLoading] = useState(false);

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

      await sdk.vault.withdraw({
        tokenAddress,
        amount: parsedAmount,
        recipientAddress: recipient,
        unwrapWeth,
        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}
      />
      <label>
        <input
          type="checkbox"
          checked={unwrapWeth}
          onChange={(e) => setUnwrapWeth(e.target.checked)}
          disabled={loading}
        />
        Unwrap WETH to ETH
      </label>
      <button onClick={handleWithdraw} disabled={loading}>
        {loading ? step || 'Processing...' : 'Withdraw'}
      </button>
    </div>
  );
}

Best Practices

1. Validate Balance Before Withdrawal

async function safeWithdraw(params: WithdrawParams) {
  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.withdraw(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.withdraw({
  ...params,
  onProgress: ({ step }) => {
    if (step === 'proving') {
      setStatus('Generating zero-knowledge proof...');
    } else if (step === 'unshielding') {
      setStatus('Submitting withdrawal transaction...');
    }
  },
});

Next Steps