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
Copy
Ask AI
npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query
Configure Provider
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Copy
Ask AI
// 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
Input Validation
Input Validation
Copy
Ask AI
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);
Transaction Simulation
Transaction Simulation
Copy
Ask AI
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,
});
Metadata Security
Metadata Security
- Validate metadata size to prevent excessive gas costs
- Sanitize all user-provided metadata fields
- Use
extraDescriptionUrifor large documents - Ensure proper base64 encoding/decoding of data URIs
Complete Integration Example
Copy
Ask AI
// 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>
);
}