Skip to main content

Deposits

This guide covers depositing tokens from your wallet into your private balance.

Basic Deposit

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

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

Deposit Parameters

interface DepositParams {
  tokenAddress: Hex;         // ERC-20 token address
  amount: bigint;            // Amount in wei
  recipientMpk?: bigint;     // Optional: deposit to another user
  onProgress?: OnProgress;   // Optional: progress callback
}
ParameterTypeRequiredDescription
tokenAddressHexYesERC-20 token contract address
amountbigintYesAmount in smallest unit (wei)
recipientMpkbigintNoDeposit to another user’s MPK
onProgressfunctionNoProgress callback function

Progress Tracking

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

Deposit Steps

StepDescription
wrappingWrapping ETH to WETH (if depositing ETH)
approvingApproving token spending
shieldingExecuting the deposit transaction
complianceWaiting for compliance verification
enum ShieldStep {
  wrapping = 'wrapping',
  approving = 'approving',
  shielding = 'shielding',
  compliance = 'compliance',
}

Deposit Result

interface ShieldResult {
  /** Hash of the shield transaction submitted on-chain. */
  txHash: Hex;
  /** Note commitment registered in the shielded pool. */
  commitment: Hex;
  /** Fee paid for this shield, in token wei. Not surfaced by the shield response, so usually undefined; query `vault.getFees()` for the schedule. */
  fee?: bigint;
  /** On-chain shield request ID, from the `ShieldRequested` event. Use this to track status via `sdk.transactions.getShieldStatus(requestId)`. */
  requestId: string;
  /** WETH wrap transaction hash, set only when ETH was wrapped to WETH as part of the flow. */
  wrapTxHash?: Hex;
}

Depositing ETH

To deposit native ETH, use the zero address:
const ETH_ADDRESS = '0x0000000000000000000000000000000000000000';

const result = await sdk.vault.shield({
  tokenAddress: ETH_ADDRESS,
  amount: 1000000000000000000n, // 1 ETH
  onProgress: ({ step, message, txHash }) => {
    if (step === 'wrapping') {
      console.log('Wrapping ETH to WETH...');
    }
  },
});

// Result includes wrap transaction
console.log('Wrap tx:', result.wrapTxHash);
console.log('Deposit tx:', result.txHash);
The SDK automatically:
  1. Wraps ETH to WETH
  2. Approves WETH spending
  3. Deposits WETH to shield

Deposit to Another User

You can deposit directly to another user’s private balance:
// Get recipient's MPK from their privacy address
import { extractMpkFromPrivacyAddress } from '@sunnyside-io/privacy-boost';

const recipientPrivacyAddress = '0x04...';
const recipientMpk = extractMpkFromPrivacyAddress(recipientPrivacyAddress);

const result = await sdk.vault.shield({
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
  recipientMpk,
});

Token Approval

The SDK handles token approval automatically. For large amounts, you may want to approve unlimited spending:
// The SDK checks current allowance and only approves if needed
// If allowance is sufficient, the approve step is skipped

Parsing Amounts

Use the standalone parseAmount / formatAmount helpers exported from the SDK package:
import { parseAmount, formatAmount } from '@sunnyside-io/privacy-boost';

// Parse human-readable amount to bigint (token has 18 decimals)
const amount = parseAmount('1.5', 18);
// Returns: 1500000000000000000n

// Format bigint to human-readable
const formatted = formatAmount(1500000000000000000n, 18);
// Returns: "1.5"

Error Handling

try {
  await sdk.vault.shield({ tokenAddress, amount });
} catch (error) {
  switch (error.code) {
    case 'INSUFFICIENT_BALANCE':
      console.log('Not enough tokens in wallet');
      break;
    case 'USER_REJECTED':
      console.log('User rejected transaction');
      break;
    case 'APPROVAL_FAILED':
      console.log('Token approval failed');
      break;
    case 'DEPOSIT_FAILED':
      console.log('Deposit transaction failed');
      break;
    case 'COMPLIANCE_REJECTED':
      console.log('Compliance check failed');
      break;
    default:
      console.log('Deposit error:', error.message);
  }
}

UI Example

import { useState } from 'react';

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

  const handleShield = async () => {
    setLoading(true);
    try {
      const decimals = 18; // token decimals (fetch via sdk.vault.getToken)
      const parsedAmount = parseAmount(amount, decimals);

      await sdk.vault.shield({
        tokenAddress,
        amount: parsedAmount,
        onProgress: ({ step, message }) => {
          setStep(message);
        },
      });

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

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

Best Practices

1. Always Validate Amounts

function validateDepositAmount(amount: bigint, walletBalance: bigint) {
  if (amount <= 0n) {
    throw new Error('Amount must be positive');
  }
  if (amount > walletBalance) {
    throw new Error('Insufficient wallet balance');
  }
}

2. Check Token Support

const supportedTokens = await sdk.vault.getTokens();
const isSupported = supportedTokens.some(
  (t) => t.address.toLowerCase() === tokenAddress.toLowerCase()
);

if (!isSupported) {
  throw new Error('Token not supported');
}

3. Handle Large Deposits

For large deposits, consider splitting into multiple transactions:
async function depositInChunks(tokenAddress: Hex, totalAmount: bigint) {
  const CHUNK_SIZE = 10000000000000000000n; // 10 tokens
  let remaining = totalAmount;

  while (remaining > 0n) {
    const chunk = remaining > CHUNK_SIZE ? CHUNK_SIZE : remaining;
    await sdk.vault.shield({ tokenAddress, amount: chunk });
    remaining -= chunk;
  }
}

Relayed Deposits (build calldata, relay yourself)

If your users don’t submit transactions directly — they hold funds at addresses you relay for (a gasless relayer, an EIP-7702 session key, a custom pipeline) — build the deposit calldata without submitting it, relay it through your own infrastructure, then finalize from the mined receipt.
import { finalizeShield } from '@sunnyside-io/privacy-boost';

// 1. Build the calldata (no wallet, nothing submitted)
const prepared = await sdk.vault.prepareShield({
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
  // recipient: '0x04...',  // omit to shield to yourself
});

// prepared.wrap? / prepared.approve? / prepared.shield are { to, value, data }
// calls — relay them in order (wrap?, approve?, shield).
const calls = [prepared.wrap, prepared.approve, prepared.shield].filter(Boolean);
const receipt = await myRelayer.submit(calls);

// 2. Finalize from the mined receipt to recover the request id. finalizeShield
// is a stateless top-level export — it parses the receipt and needs no session.
const result = await finalizeShield({
  receipt,
  shieldContract: prepared.finalize.shieldContract,
  commitment: prepared.commitment,
});
console.log('Request ID:', result.requestId);
Each call is { to, value, data }, so the array drops directly into an EIP-5792 / Porto wallet_prepareCalls bundle.

Server-side (no session)

A backend that holds only a recipient’s privacy address can build a deposit on their behalf with the stateless buildShieldPayload — no signed-in session and no private keys:
import { buildShieldPayload } from '@sunnyside-io/privacy-boost';

const prepared = await buildShieldPayload({
  tokenAddress: '0x...',
  amount: 1000000000000000000n,
  recipient: userPrivacyAddress,
  tokenId,                 // from the token catalog
  shieldContractAddress,
  teePublicKey,
  minShieldAmount,         // optional: reject below the per-token minimum
});
The note’s owner is bound cryptographically to the recipient’s privacy address, so only they can spend it; the ephemeral sender used to encrypt the deposit is generated and discarded internally.

Next Steps