Skip to content

React Integration

Complete React example with wallet connection and privacy operations.

Setup

bash
npm create vite@latest my-privacy-app -- --template react-ts
cd my-privacy-app
npm install @zkprivacy/sdk viem wagmi @rainbow-me/rainbowkit

Provider Setup

tsx
// src/providers.tsx
import { WagmiProvider, createConfig, http } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css';

const config = getDefaultConfig({
  appName: 'ZK Privacy App',
  projectId: 'YOUR_PROJECT_ID',  // Get from WalletConnect
  chains: [mainnet],
});

const queryClient = new QueryClient();

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>
          {children}
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Privacy Context

tsx
// src/context/privacy.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { 
  PrivacyClient, 
  createProver, 
  setGlobalProver,
  DEPLOYMENTS,
} from '@zkprivacy/sdk';
import { useWalletClient } from 'wagmi';

interface PrivacyContextType {
  client: PrivacyClient | null;
  isConnected: boolean;
  isLoading: boolean;
  balance: bigint;
  address: string | null;
  connect: () => Promise<void>;
  disconnect: () => void;
  sync: () => Promise<void>;
}

const PrivacyContext = createContext<PrivacyContextType | null>(null);

export function PrivacyProvider({ children }: { children: ReactNode }) {
  const { data: walletClient } = useWalletClient();
  const [client, setClient] = useState<PrivacyClient | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [balance, setBalance] = useState(0n);
  const [address, setAddress] = useState<string | null>(null);

  // Initialize prover on mount
  useEffect(() => {
    async function initProver() {
      const prover = await createProver({
        transferCircuit: '/circuits/transfer.json',
        unshieldCircuit: '/circuits/unshield.json',
      });
      setGlobalProver(prover);

      const newClient = new PrivacyClient({
        rpcUrl: DEPLOYMENTS.remote.rpcUrl,
        poolAddress: DEPLOYMENTS.remote.pool,
        relayerUrl: DEPLOYMENTS.remote.relayerUrl,
      });
      setClient(newClient);
      setIsLoading(false);
    }
    initProver();
  }, []);

  const connect = async () => {
    if (!client || !walletClient) return;
    
    setIsLoading(true);
    try {
      await client.connect({ mode: 'linked', walletClient });
      await client.sync();
      setIsConnected(true);
      setAddress(client.getAddress());
      setBalance(client.getBalance());
    } finally {
      setIsLoading(false);
    }
  };

  const disconnect = () => {
    client?.disconnect();
    setIsConnected(false);
    setAddress(null);
    setBalance(0n);
  };

  const sync = async () => {
    if (!client || !isConnected) return;
    await client.sync();
    setBalance(client.getBalance());
  };

  return (
    <PrivacyContext.Provider value={{
      client,
      isConnected,
      isLoading,
      balance,
      address,
      connect,
      disconnect,
      sync,
    }}>
      {children}
    </PrivacyContext.Provider>
  );
}

export function usePrivacy() {
  const context = useContext(PrivacyContext);
  if (!context) throw new Error('usePrivacy must be used within PrivacyProvider');
  return context;
}

Transfer Component

tsx
// src/components/Transfer.tsx
import { useState } from 'react';
import { parseEther, formatEther } from 'viem';
import { usePrivacy } from '../context/privacy';

export function Transfer() {
  const { client, isConnected, balance, sync } = usePrivacy();
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [isPending, setIsPending] = useState(false);
  const [txHash, setTxHash] = useState<string | null>(null);

  const handleTransfer = async () => {
    if (!client || !recipient || !amount) return;
    
    setIsPending(true);
    setTxHash(null);
    
    try {
      const hash = await client.transfer({
        recipient,
        amount: parseEther(amount),
      });
      setTxHash(hash);
      await sync();
      setRecipient('');
      setAmount('');
    } catch (error) {
      console.error('Transfer failed:', error);
      alert('Transfer failed: ' + (error as Error).message);
    } finally {
      setIsPending(false);
    }
  };

  if (!isConnected) {
    return <p>Connect wallet to transfer</p>;
  }

  return (
    <div>
      <h2>Transfer</h2>
      <p>Balance: {formatEther(balance)} ETH</p>
      
      <input
        type="text"
        placeholder="Recipient (zks1...)"
        value={recipient}
        onChange={(e) => setRecipient(e.target.value)}
        disabled={isPending}
      />
      
      <input
        type="text"
        placeholder="Amount"
        value={amount}
        onChange={(e) => setAmount(e.target.value)}
        disabled={isPending}
      />
      
      <button onClick={handleTransfer} disabled={isPending || !recipient || !amount}>
        {isPending ? 'Generating Proof...' : 'Transfer'}
      </button>
      
      {txHash && (
        <p>Success! TX: {txHash}</p>
      )}
    </div>
  );
}

App Component

tsx
// src/App.tsx
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { usePrivacy } from './context/privacy';
import { Transfer } from './components/Transfer';

function App() {
  const { isLoading, isConnected, address, connect, disconnect } = usePrivacy();

  return (
    <div>
      <h1>ZK Privacy App</h1>
      
      <ConnectButton />
      
      {isLoading ? (
        <p>Loading prover...</p>
      ) : isConnected ? (
        <>
          <p>ZK Address: {address}</p>
          <button onClick={disconnect}>Disconnect Privacy</button>
          <Transfer />
        </>
      ) : (
        <button onClick={connect}>Connect Privacy</button>
      )}
    </div>
  );
}

export default App;

Vite Config

typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  optimizeDeps: {
    exclude: ['@aztec/bb.js'],
  },
  assetsInclude: ['**/*.wasm'],
});

Released under the MIT License.