Skip to main content

Prerequisites

Before integrating Escrow V2, ensure you have:
  • Node.js 18+ and npm/yarn
  • React 18+ project with TypeScript
  • Familiarity with Wagmi v2 and Viem

Web3 Provider Setup

Install Dependencies

npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query

Configure Provider

// providers/Web3Provider.tsx
import { arbitrum, arbitrumSepolia, mainnet } from '@reown/appkit/networks';
import { createAppKit } from '@reown/appkit/react';
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi';
import { http, fallback, webSocket } from 'wagmi';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const isProduction = process.env.NODE_ENV === 'production';

// Alchemy transport helper
const alchemyTransport = (chainId: number) => 
  fallback([
    http(`https://arb-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`),
    webSocket(`wss://arb-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`)
  ]);

const chains = [
  isProduction ? arbitrum : arbitrumSepolia,
  mainnet, // Always included for ENS resolution
] as const;

const wagmiAdapter = new WagmiAdapter({
  networks: chains,
  projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID!,
  transports: {
    [arbitrum.id]: alchemyTransport(arbitrum.id),
    [arbitrumSepolia.id]: alchemyTransport(arbitrumSepolia.id),
    [mainnet.id]: alchemyTransport(mainnet.id),
  },
});

createAppKit({
  adapters: [wagmiAdapter],
  networks: chains,
  defaultNetwork: isProduction ? arbitrum : arbitrumSepolia,
  projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID!,
  allowUnsupportedChain: true,
  themeVariables: {
    '--w3m-color-mix': '#6134FE',
    '--w3m-color-mix-strength': 20,
    '--w3m-z-index': 10000,
  },
});

const queryClient = new QueryClient();

export default function Web3Provider({ children }: { children: React.ReactNode }) {
  return (
    <WagmiProvider config={wagmiAdapter.wagmiConfig}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </WagmiProvider>
  );
}

Contract Configuration

Addresses and ABIs

// config/contracts.ts
import { Address } from 'viem';

export const ESCROW_ADDRESSES = {
  arbitrum: {
    escrow: '0x79530E7Bb3950A3a4b5a167816154715681F2f6c' as Address,
    view: '0x3Fed94ee4FA1B5665DB84489f913E2c7e1290459' as Address,
  },
  arbitrumSepolia: {
    escrow: '0x5ef185810BCe41c03c9E5ca271B8C91F1024F953' as Address,
    view: '0x6451046caB9291a919FCba045bf6Bb8E0Bb71467' as Address,
  },
} as const;

// Import ABIs from the escrow-v2 package or copy from GitHub
import EscrowUniversalABI from './abis/EscrowUniversal.json';
import EscrowViewABI from './abis/EscrowView.json';

export { EscrowUniversalABI, EscrowViewABI };

Get Contract Address Hook

// hooks/useEscrowAddress.ts
import { useAccount } from 'wagmi';
import { ESCROW_ADDRESSES } from '../config/contracts';

export function useEscrowAddress() {
  const { chainId } = useAccount();
  
  return chainId === 42161 
    ? ESCROW_ADDRESSES.arbitrum 
    : ESCROW_ADDRESSES.arbitrumSepolia;
}

Transaction Creation

Create Transaction Hook

// hooks/useCreateTransaction.ts
import { useWriteContract, useWaitForTransactionReceipt, useAccount, useReadContract } from 'wagmi';
import { parseEther, parseUnits, Address } from 'viem';
import { useEscrowAddress } from './useEscrowAddress';
import { EscrowUniversalABI } from '../config/contracts';

interface TransactionMetadata {
  title: string;
  description: string;
  extraDescriptionUri?: string;
  // For crypto swaps
  otherChain?: string;
  otherChainAddress?: string;
  otherAsset?: string;
  otherAmount?: string;
}

interface CreateTransactionParams {
  seller: Address;
  amount: string;
  token: {
    address: Address | 'native';
    decimals: number;
    symbol: string;
  };
  deadline: Date;
  metadata: TransactionMetadata;
}

// Helper function to create data URI from metadata
function createMetadataUri(metadata: TransactionMetadata): string {
  const jsonString = JSON.stringify(metadata);
  const base64Encoded = btoa(jsonString);
  return `data:application/json;base64,${base64Encoded}`;
}

export function useCreateTransaction() {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, data: hash } = useWriteContract();
  const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash });

  const createTransaction = async (params: CreateTransactionParams) => {
    const deadlineTimestamp = BigInt(Math.floor(params.deadline.getTime() / 1000));
    const metadataUri = createMetadataUri(params.metadata);
    
    const amount = params.token.address === 'native'
      ? parseEther(params.amount)
      : parseUnits(params.amount, params.token.decimals);
    
    if (params.token.address === 'native') {
      return await writeContractAsync({
        address: escrow,
        abi: EscrowUniversalABI,
        functionName: 'createNativeTransaction',
        args: [deadlineTimestamp, metadataUri, params.seller],
        value: amount,
      });
    } else {
      // For ERC20, ensure approval is granted first
      return await writeContractAsync({
        address: escrow,
        abi: EscrowUniversalABI,
        functionName: 'createERC20Transaction',
        args: [
          amount,
          params.token.address,
          deadlineTimestamp,
          metadataUri,
          params.seller,
        ],
      });
    }
  };

  return { 
    createTransaction, 
    isLoading, 
    isSuccess,
    transactionHash: hash 
  };
}

Usage Example

// components/CreateTransactionForm.tsx
import { useCreateTransaction } from '../hooks/useCreateTransaction';
import { Address } from 'viem';

export function CreateTransactionForm() {
  const { createTransaction, isLoading, isSuccess } = useCreateTransaction();

  const handleSubmit = async () => {
    try {
      const hash = await createTransaction({
        seller: '0x...' as Address,
        amount: '0.1',
        token: { address: 'native', decimals: 18, symbol: 'ETH' },
        deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
        metadata: {
          title: 'Website Development',
          description: 'Build a responsive landing page',
        },
      });
      console.log('Transaction created:', hash);
    } catch (error) {
      console.error('Failed to create transaction:', error);
    }
  };

  return (
    <button onClick={handleSubmit} disabled={isLoading}>
      {isLoading ? 'Creating...' : 'Create Transaction'}
    </button>
  );
}

Transaction Management

Payment and Reimbursement Hooks

// hooks/useTransactionActions.ts
import { useWriteContract } from 'wagmi';
import { useEscrowAddress } from './useEscrowAddress';
import { EscrowUniversalABI } from '../config/contracts';

export function usePay() {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, isPending } = useWriteContract();

  const pay = async (transactionId: bigint, amount: bigint) => {
    return await writeContractAsync({
      address: escrow,
      abi: EscrowUniversalABI,
      functionName: 'pay',
      args: [transactionId, amount],
    });
  };

  return { pay, isPending };
}

export function useReimburse() {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, isPending } = useWriteContract();

  const reimburse = async (transactionId: bigint, amount: bigint) => {
    return await writeContractAsync({
      address: escrow,
      abi: EscrowUniversalABI,
      functionName: 'reimburse',
      args: [transactionId, amount],
    });
  };

  return { reimburse, isPending };
}

export function useExecuteTransaction() {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, isPending } = useWriteContract();

  const executeTransaction = async (transactionId: bigint) => {
    return await writeContractAsync({
      address: escrow,
      abi: EscrowUniversalABI,
      functionName: 'executeTransaction',
      args: [transactionId],
    });
  };

  return { executeTransaction, isPending };
}

Settlement Hooks

// hooks/useSettlement.ts
import { useWriteContract } from 'wagmi';
import { useEscrowAddress } from './useEscrowAddress';
import { EscrowUniversalABI } from '../config/contracts';

export function useProposeSettlement() {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, isPending } = useWriteContract();

  const proposeSettlement = async (transactionId: bigint, amount: bigint) => {
    return await writeContractAsync({
      address: escrow,
      abi: EscrowUniversalABI,
      functionName: 'proposeSettlement',
      args: [transactionId, amount],
    });
  };

  return { proposeSettlement, isPending };
}

export function useAcceptSettlement() {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, isPending } = useWriteContract();

  const acceptSettlement = async (transactionId: bigint) => {
    return await writeContractAsync({
      address: escrow,
      abi: EscrowUniversalABI,
      functionName: 'acceptSettlement',
      args: [transactionId],
    });
  };

  return { acceptSettlement, isPending };
}

Reading Transaction Data

Transaction Query Hooks

// hooks/useTransactionData.ts
import { useReadContract } from 'wagmi';
import { useEscrowAddress } from './useEscrowAddress';
import { EscrowUniversalABI } from '../config/contracts';

export function useTransaction(transactionId: bigint) {
  const { escrow } = useEscrowAddress();

  const { data: transaction, isLoading, error, refetch } = useReadContract({
    address: escrow,
    abi: EscrowUniversalABI,
    functionName: 'transactions',
    args: [transactionId],
    query: {
      enabled: transactionId !== undefined,
      refetchInterval: 30000, // Refresh every 30 seconds
    },
  });

  return { transaction, isLoading, error, refetch };
}

export function useTransactionCount() {
  const { escrow } = useEscrowAddress();

  const { data: count, isLoading, error } = useReadContract({
    address: escrow,
    abi: EscrowUniversalABI,
    functionName: 'getTransactionCount',
  });

  return { count, isLoading, error };
}

export function usePayouts(transactionId: bigint, winningParty: number) {
  const { escrow } = useEscrowAddress();

  const { data: payouts, isLoading, error } = useReadContract({
    address: escrow,
    abi: EscrowUniversalABI,
    functionName: 'getPayouts',
    args: [transactionId, winningParty],
    query: {
      enabled: transactionId !== undefined && winningParty !== undefined,
    },
  });

  return { payouts, isLoading, error };
}

export function useAmountCap(tokenAddress: Address) {
  const { escrow } = useEscrowAddress();

  const { data: cap, isLoading, error } = useReadContract({
    address: escrow,
    abi: EscrowUniversalABI,
    functionName: 'amountCaps',
    args: [tokenAddress],
    query: {
      enabled: !!tokenAddress,
    },
  });

  return { cap, isLoading, error };
}

Decode Metadata Hook

// hooks/useTransactionMetadata.ts
import { useState, useEffect } from 'react';

interface TransactionMetadata {
  title: string;
  description: string;
  extraDescriptionUri?: string;
  otherChain?: string;
  otherChainAddress?: string;
  otherAsset?: string;
  otherAmount?: string;
}

export function useTransactionMetadata(transactionUri: string) {
  const [metadata, setMetadata] = useState<TransactionMetadata | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    if (!transactionUri) return;
    
    setIsLoading(true);
    try {
      if (transactionUri.startsWith('data:application/json;base64,')) {
        const base64Data = transactionUri.split(',')[1];
        const jsonString = atob(base64Data);
        const decoded = JSON.parse(jsonString);
        setMetadata(decoded);
      }
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Failed to decode metadata'));
    } finally {
      setIsLoading(false);
    }
  }, [transactionUri]);
  
  return { metadata, isLoading, error };
}

Dispute Management

Arbitration Fee Hooks

// hooks/useDispute.ts
import { useWriteContract, useReadContract } from 'wagmi';
import { useEscrowAddress } from './useEscrowAddress';
import { EscrowUniversalABI } from '../config/contracts';

export function useArbitrationCost() {
  const { escrow } = useEscrowAddress();
  
  // Get arbitrator address
  const { data: arbitrator } = useReadContract({
    address: escrow,
    abi: EscrowUniversalABI,
    functionName: 'arbitrator',
  });

  const { data: extraData } = useReadContract({
    address: escrow,
    abi: EscrowUniversalABI,
    functionName: 'arbitratorExtraData',
  });

  // This would need the arbitrator ABI to get the actual cost
  // Simplified for this example
  return { arbitrator, extraData };
}

export function usePayArbitrationFee(isBuyer: boolean) {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, isPending } = useWriteContract();

  const payFee = async (transactionId: bigint, feeAmount: bigint) => {
    const functionName = isBuyer 
      ? 'payArbitrationFeeByBuyer' 
      : 'payArbitrationFeeBySeller';
    
    return await writeContractAsync({
      address: escrow,
      abi: EscrowUniversalABI,
      functionName,
      args: [transactionId],
      value: feeAmount,
    });
  };

  return { payFee, isPending };
}

export function useTimeout(isBuyer: boolean) {
  const { escrow } = useEscrowAddress();
  const { writeContractAsync, isPending } = useWriteContract();

  const timeout = async (transactionId: bigint) => {
    const functionName = isBuyer ? 'timeOutByBuyer' : 'timeOutBySeller';
    
    return await writeContractAsync({
      address: escrow,
      abi: EscrowUniversalABI,
      functionName,
      args: [transactionId],
    });
  };

  return { timeout, isPending };
}

Security Considerations

Token Compatibility

  • Only use standard ERC20 tokens without transfer hooks
  • Avoid tokens with fee-on-transfer, rebasing, or pausable mechanisms
  • The contract uses SafeERC20 but assumes standard behavior

Frontend Security Best Practices

import { isAddress } from 'viem';

// Validate addresses
if (!isAddress(sellerAddress)) {
  throw new Error('Invalid seller address');
}

// Check token decimals for accurate amount parsing
const amount = parseUnits(userInput, token.decimals);
import { simulateContract } from 'wagmi/actions';

// Preview transaction outcomes before confirmation
const { request } = await simulateContract(config, {
  address: escrow,
  abi: EscrowUniversalABI,
  functionName: 'createNativeTransaction',
  args: [deadline, metadataUri, seller],
  value: amount,
});
  • Validate metadata size to prevent excessive gas costs
  • Sanitize all user-provided metadata fields
  • Use extraDescriptionUri for large documents
  • Ensure proper base64 encoding/decoding of data URIs

Complete Integration Example

// pages/EscrowPage.tsx
import { useState } from 'react';
import { useAccount } from 'wagmi';
import { parseEther, formatEther, Address } from 'viem';
import { 
  useCreateTransaction, 
  useTransaction, 
  usePay, 
  useExecuteTransaction 
} from '../hooks';

export function EscrowPage() {
  const { address, isConnected } = useAccount();
  const [transactionId, setTransactionId] = useState<bigint | null>(null);
  
  const { createTransaction, isLoading: isCreating } = useCreateTransaction();
  const { transaction, isLoading: isLoadingTx } = useTransaction(transactionId!);
  const { pay, isPending: isPaying } = usePay();
  const { executeTransaction, isPending: isExecuting } = useExecuteTransaction();

  if (!isConnected) {
    return <div>Please connect your wallet</div>;
  }

  return (
    <div>
      <h1>Escrow V2 Integration</h1>
      
      {/* Create Transaction */}
      <section>
        <h2>Create New Escrow</h2>
        <button
          onClick={async () => {
            const hash = await createTransaction({
              seller: '0x...' as Address,
              amount: '0.1',
              token: { address: 'native', decimals: 18, symbol: 'ETH' },
              deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
              metadata: {
                title: 'Test Transaction',
                description: 'Testing escrow integration',
              },
            });
            console.log('Created:', hash);
          }}
          disabled={isCreating}
        >
          {isCreating ? 'Creating...' : 'Create Escrow'}
        </button>
      </section>

      {/* View Transaction */}
      {transactionId && transaction && (
        <section>
          <h2>Transaction Details</h2>
          <p>Amount: {formatEther(transaction.amount)} ETH</p>
          <p>Status: {transaction.status}</p>
          <p>Deadline: {new Date(Number(transaction.deadline) * 1000).toLocaleString()}</p>
        </section>
      )}
    </div>
  );
}

Resources