Skip to main content

Custom Hooks

This guide covers creating custom hooks that compose the built-in hooks.

Composing Hooks

usePrivacyWallet

Combine auth and balance data:
import { useAuth, useBalances } from '@testinprod-io/privacy-boost-react';

function usePrivacyWallet() {
  const auth = useAuth();
  const balances = useBalances();

  return {
    // Auth state
    isReady: auth.isAuthenticated,
    address: auth.address,
    privacyAddress: auth.privacyAddress,

    // Auth methods
    connect: auth.authenticateInjected,
    disconnect: auth.logout,

    // Balance data
    balances: balances.balances,
    totalShielded: balances.totalShielded,
    loading: balances.loading,
  };
}

// Usage
function Wallet() {
  const { isReady, connect, disconnect, balances } = usePrivacyWallet();
  // ...
}

useDepositFlow

Encapsulate deposit flow with state:
import { useState, useCallback } from 'react';
import { useVault, useAuth } from '@testinprod-io/privacy-boost-react';

interface UseDepositFlowResult {
  deposit: (tokenAddress: string, amount: string) => Promise<void>;
  loading: boolean;
  status: string;
  error: string | null;
  txHash: string | null;
}

function useDepositFlow(): UseDepositFlowResult {
  const { deposit: vaultDeposit } = useVault();
  const { isAuthenticated } = useAuth();

  const [loading, setLoading] = useState(false);
  const [status, setStatus] = useState('');
  const [error, setError] = useState<string | null>(null);
  const [txHash, setTxHash] = useState<string | null>(null);

  const deposit = useCallback(async (tokenAddress: string, amount: string) => {
    if (!isAuthenticated) {
      setError('Please connect wallet first');
      return;
    }

    setLoading(true);
    setError(null);
    setTxHash(null);
    setStatus('Starting deposit...');

    try {
      const result = await vaultDeposit({
        tokenAddress: tokenAddress as `0x${string}`,
        amount,
        onProgress: ({ step, message, txHash }) => {
          setStatus(message);
          if (txHash) setTxHash(txHash);
        },
      });

      setTxHash(result.txHash);
      setStatus('Deposit complete!');
    } catch (err: any) {
      if (err.code === 'WALLET_USER_REJECTED') {
        setStatus('');
      } else {
        setError(err.message);
        setStatus('');
      }
    } finally {
      setLoading(false);
    }
  }, [vaultDeposit, isAuthenticated]);

  return { deposit, loading, status, error, txHash };
}

// Usage
function DepositForm() {
  const { deposit, loading, status, error, txHash } = useDepositFlow();

  return (
    <div>
      <button onClick={() => deposit('0x...', '1.0')}>
        Deposit
      </button>
      {loading && <p>{status}</p>}
      {error && <p className="error">{error}</p>}
      {txHash && <a href={`https://etherscan.io/tx/${txHash}`}>View Tx</a>}
    </div>
  );
}

useTokenInfo

Fetch and cache token metadata:
import { useState, useEffect } from 'react';
import { usePrivacyBoost } from '@testinprod-io/privacy-boost-react';

interface TokenInfo {
  address: string;
  symbol: string;
  name: string;
  decimals: number;
  balance: bigint;
  formattedBalance: string;
}

function useTokenInfo(tokenAddress: string): {
  token: TokenInfo | null;
  loading: boolean;
  error: Error | null;
} {
  const sdk = usePrivacyBoost();
  const [token, setToken] = useState<TokenInfo | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    if (!sdk || !tokenAddress) return;

    async function fetchToken() {
      setLoading(true);
      try {
        const [metadata, balance] = await Promise.all([
          sdk.vault.getToken(tokenAddress),
          sdk.vault.getBalance(tokenAddress),
        ]);

        setToken({
          address: metadata.address,
          symbol: metadata.symbol,
          name: metadata.name,
          decimals: metadata.decimals,
          balance,
          formattedBalance: sdk.vault.formatAmount(balance, metadata.decimals),
        });
      } catch (err: any) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchToken();
  }, [sdk, tokenAddress]);

  return { token, loading, error };
}

useTransferForm

Form state management for transfers:
import { useState, useCallback } from 'react';
import { useVault, useContacts } from '@testinprod-io/privacy-boost-react';
import { isValidPrivacyAddress } from '@testinprod-io/privacy-boost';

function useTransferForm(tokenAddress: string) {
  const { send } = useVault();
  const { contacts, findByAddress } = useContacts();

  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Validation
  const isValidRecipient = isValidPrivacyAddress(recipient);
  const isValidAmount = amount !== '' && parseFloat(amount) > 0;
  const canSubmit = isValidRecipient && isValidAmount && !loading;

  // Get contact name if exists
  const recipientContact = findByAddress(recipient);
  const recipientName = recipientContact?.name || null;

  const submit = useCallback(async () => {
    if (!canSubmit) return;

    setLoading(true);
    setError(null);

    try {
      await send({
        to: recipient,
        tokenAddress: tokenAddress as `0x${string}`,
        amount,
      });
      // Reset form on success
      setRecipient('');
      setAmount('');
    } catch (err: any) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [send, recipient, tokenAddress, amount, canSubmit]);

  return {
    // State
    recipient,
    amount,
    loading,
    error,

    // Setters
    setRecipient,
    setAmount,

    // Validation
    isValidRecipient,
    isValidAmount,
    canSubmit,
    recipientName,

    // Actions
    submit,
    reset: () => {
      setRecipient('');
      setAmount('');
      setError(null);
    },
  };
}

usePollingBalance

Auto-refresh balances:
import { useEffect, useRef } from 'react';
import { useVault, useBalances } from '@testinprod-io/privacy-boost-react';

function usePollingBalance(intervalMs = 30000) {
  const { syncAllBalances } = useVault();
  const { balances, loading } = useBalances();
  const intervalRef = useRef<number | null>(null);

  useEffect(() => {
    // Initial sync
    syncAllBalances();

    // Set up polling
    intervalRef.current = window.setInterval(() => {
      syncAllBalances();
    }, intervalMs);

    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, [syncAllBalances, intervalMs]);

  return { balances, loading };
}

Best Practices

1. Handle Loading States

function useSafeOperation<T>(operation: () => Promise<T>) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  const [data, setData] = useState<T | null>(null);

  const execute = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const result = await operation();
      setData(result);
      return result;
    } catch (err: any) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [operation]);

  return { execute, loading, error, data };
}

2. Memoize Expensive Computations

function useFormattedBalances() {
  const { balances } = useBalances();

  return useMemo(() =>
    balances
      .filter(b => b.shielded > 0n)
      .sort((a, b) => Number(b.shielded - a.shielded))
      .map(b => ({
        ...b,
        displayAmount: `${b.formattedShielded} ${b.symbol}`,
      })),
    [balances]
  );
}

3. Clean Up Effects

function useAsyncEffect(effect: () => Promise<void>, deps: any[]) {
  useEffect(() => {
    let cancelled = false;

    effect().catch((err) => {
      if (!cancelled) {
        console.error(err);
      }
    });

    return () => {
      cancelled = true;
    };
  }, deps);
}

Next Steps