Prerequisites
Before integrating Curate V2, ensure you have:- Node.js 18+ and npm/yarn
- React project with TypeScript (recommended)
- Familiarity with ethers.js v6
Setup
Install Dependencies
Copy
Ask AI
npm install ethers@6 @tanstack/react-query
Environment Configuration
Create a.env file:
Copy
Ask AI
# RPC Endpoints
NEXT_PUBLIC_RPC_ARBITRUM="https://arb1.arbitrum.io/rpc"
NEXT_PUBLIC_RPC_ARBITRUM_SEPOLIA="https://sepolia-rollup.arbitrum.io/rpc"
# Optional: For contract deployment
PRIVATE_KEY="your_private_key"
ARBISCAN_API_KEY="your_api_key"
Web3 Context Setup
Create Web3 Provider
Copy
Ask AI
// context/Web3Provider.tsx
import { createContext, useContext, useState, useCallback, ReactNode } from 'react';
import { BrowserProvider, JsonRpcSigner } from 'ethers';
interface Web3ContextType {
provider: BrowserProvider | null;
signer: JsonRpcSigner | null;
address: string | null;
chainId: number | null;
connect: () => Promise<void>;
disconnect: () => void;
}
const Web3Context = createContext<Web3ContextType | undefined>(undefined);
export const Web3Provider = ({ children }: { children: ReactNode }) => {
const [provider, setProvider] = useState<BrowserProvider | null>(null);
const [signer, setSigner] = useState<JsonRpcSigner | null>(null);
const [address, setAddress] = useState<string | null>(null);
const [chainId, setChainId] = useState<number | null>(null);
const connect = useCallback(async () => {
if (!window.ethereum) throw new Error("No wallet found");
const web3Provider = new BrowserProvider(window.ethereum);
const web3Signer = await web3Provider.getSigner();
const userAddress = await web3Signer.getAddress();
const network = await web3Provider.getNetwork();
setProvider(web3Provider);
setSigner(web3Signer);
setAddress(userAddress);
setChainId(Number(network.chainId));
}, []);
const disconnect = useCallback(() => {
setProvider(null);
setSigner(null);
setAddress(null);
setChainId(null);
}, []);
return (
<Web3Context.Provider value={{ provider, signer, address, chainId, connect, disconnect }}>
{children}
</Web3Context.Provider>
);
};
export const useWeb3 = () => {
const context = useContext(Web3Context);
if (!context) throw new Error('useWeb3 must be used within Web3Provider');
return context;
};
Contract Hooks
Generic Contract Hook
Copy
Ask AI
// hooks/useContract.ts
import { Contract } from 'ethers';
import { useMemo } from 'react';
import { useWeb3 } from '../context/Web3Provider';
export const useContract = <T extends Contract>(
address: string | undefined,
abi: any
): T | null => {
const { signer, provider } = useWeb3();
return useMemo(() => {
if (!address) return null;
const signerOrProvider = signer || provider;
return signerOrProvider
? new Contract(address, abi, signerOrProvider) as T
: null;
}, [address, abi, signer, provider]);
};
Registry Hook
Copy
Ask AI
// hooks/useRegistry.ts
import { useQuery } from '@tanstack/react-query';
import { useContract } from './useContract';
import CurateABI from '../abis/CurateV2.json';
import CurateViewABI from '../abis/CurateView.json';
import { formatEther } from 'ethers';
interface RegistryConfig {
governor: string;
relayer: string;
submissionDeposit: bigint;
removalDeposit: bigint;
submissionChallengeDeposit: bigint;
removalChallengeDeposit: bigint;
challengePeriod: bigint;
arbitrationCost: bigint;
}
export const useRegistry = (curateAddress: string, viewAddress: string) => {
const view = useContract(viewAddress, CurateViewABI);
return useQuery({
queryKey: ['registry', curateAddress],
queryFn: async (): Promise<RegistryConfig> => {
if (!view) throw new Error('View contract not ready');
const config = await view.fetchArbitrable(curateAddress);
return {
governor: config.governor,
relayer: config.relayerContract,
submissionDeposit: config.submissionBaseDeposit,
removalDeposit: config.removalBaseDeposit,
submissionChallengeDeposit: config.submissionChallengeBaseDeposit,
removalChallengeDeposit: config.removalChallengeBaseDeposit,
challengePeriod: config.challengePeriodDuration,
arbitrationCost: config.arbitrationCost,
};
},
enabled: !!view,
staleTime: 60000, // Cache for 1 minute
});
};
Item Hook
Copy
Ask AI
// hooks/useItem.ts
import { useQuery } from '@tanstack/react-query';
import { useContract } from './useContract';
import CurateViewABI from '../abis/CurateView.json';
enum Status {
Absent = 0,
Registered = 1,
RegistrationRequested = 2,
ClearingRequested = 3
}
interface ItemData {
status: Status;
statusName: string;
disputed: boolean;
sumDeposit: bigint;
requester: string;
}
export const useItem = (curateAddress: string, viewAddress: string, itemId: string) => {
const view = useContract(viewAddress, CurateViewABI);
return useQuery({
queryKey: ['item', curateAddress, itemId],
queryFn: async (): Promise<ItemData> => {
if (!view) throw new Error('View contract not ready');
const item = await view.getItem(curateAddress, itemId);
const statusNames = ['Absent', 'Registered', 'RegistrationRequested', 'ClearingRequested'];
return {
status: item.status,
statusName: statusNames[item.status],
disputed: item.disputed,
sumDeposit: item.sumDeposit,
requester: item.requester,
};
},
enabled: !!view && !!itemId,
refetchInterval: 30000, // Refresh every 30 seconds
});
};
Submission Hook
Submit Item
Copy
Ask AI
// hooks/useSubmitItem.ts
import { useState } from 'react';
import { keccak256, toUtf8Bytes, parseEther } from 'ethers';
import { useContract } from './useContract';
import { useRegistry } from './useRegistry';
import CurateABI from '../abis/CurateV2.json';
interface SubmitItemParams {
data: Record<string, any>;
}
export const useSubmitItem = (curateAddress: string, viewAddress: string) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const curate = useContract(curateAddress, CurateABI);
const { data: registry } = useRegistry(curateAddress, viewAddress);
const submitItem = async ({ data }: SubmitItemParams) => {
if (!curate || !registry) throw new Error('Contracts not ready');
setIsLoading(true);
setError(null);
try {
const itemString = JSON.stringify(data);
const itemId = keccak256(toUtf8Bytes(itemString));
// Calculate total cost
const totalCost = registry.submissionDeposit + registry.arbitrationCost;
// Submit item
const tx = await curate.addItem(itemString, { value: totalCost });
const receipt = await tx.wait();
return {
itemId,
transactionHash: receipt.hash,
};
} catch (err) {
const error = err instanceof Error ? err : new Error('Submission failed');
setError(error);
throw error;
} finally {
setIsLoading(false);
}
};
return { submitItem, isLoading, error };
};
Challenge Hook
Challenge Item
Copy
Ask AI
// hooks/useChallengeItem.ts
import { useState } from 'react';
import { useContract } from './useContract';
import { useItem } from './useItem';
import { useRegistry } from './useRegistry';
import CurateABI from '../abis/CurateV2.json';
interface ChallengeParams {
itemId: string;
evidence: {
name: string;
description: string;
supportingInfo?: string;
};
}
export const useChallengeItem = (curateAddress: string, viewAddress: string) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const curate = useContract(curateAddress, CurateABI);
const { data: registry } = useRegistry(curateAddress, viewAddress);
const challengeItem = async ({ itemId, evidence }: ChallengeParams) => {
if (!curate || !registry) throw new Error('Contracts not ready');
setIsLoading(true);
setError(null);
try {
// Get item to determine challenge deposit type
const itemInfo = await curate.getItemInfo(itemId);
const status = itemInfo[0];
// Status 2 = RegistrationRequested, Status 3 = ClearingRequested
const challengeDeposit = status === 2
? registry.submissionChallengeDeposit
: registry.removalChallengeDeposit;
const totalCost = challengeDeposit + registry.arbitrationCost;
const evidenceString = JSON.stringify(evidence);
const tx = await curate.challengeRequest(itemId, evidenceString, {
value: totalCost
});
const receipt = await tx.wait();
return {
transactionHash: receipt.hash,
};
} catch (err) {
const error = err instanceof Error ? err : new Error('Challenge failed');
setError(error);
throw error;
} finally {
setIsLoading(false);
}
};
return { challengeItem, isLoading, error };
};
Execute Request Hook
Execute Unchallenged Request
Copy
Ask AI
// hooks/useExecuteRequest.ts
import { useState } from 'react';
import { useContract } from './useContract';
import { useRegistry } from './useRegistry';
import CurateABI from '../abis/CurateV2.json';
export const useExecuteRequest = (curateAddress: string, viewAddress: string) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const curate = useContract(curateAddress, CurateABI);
const { data: registry } = useRegistry(curateAddress, viewAddress);
const canExecute = async (itemId: string): Promise<boolean> => {
if (!curate || !registry) return false;
try {
const [status, requestCount] = await curate.getItemInfo(itemId);
// Must be in a pending state
if (status !== 2 && status !== 3) return false;
const requestInfo = await curate.getRequestInfo(itemId, requestCount - 1);
const now = Math.floor(Date.now() / 1000);
return now > Number(requestInfo.submissionTime) + Number(registry.challengePeriod);
} catch {
return false;
}
};
const executeRequest = async (itemId: string) => {
if (!curate) throw new Error('Contract not ready');
const executable = await canExecute(itemId);
if (!executable) throw new Error('Request cannot be executed yet');
setIsLoading(true);
setError(null);
try {
const tx = await curate.executeRequest(itemId);
const receipt = await tx.wait();
return {
transactionHash: receipt.hash,
};
} catch (err) {
const error = err instanceof Error ? err : new Error('Execution failed');
setError(error);
throw error;
} finally {
setIsLoading(false);
}
};
return { executeRequest, canExecute, isLoading, error };
};
Event Monitoring
Event Listeners
Copy
Ask AI
// hooks/useEventListeners.ts
import { useEffect } from 'react';
import { useContract } from './useContract';
import CurateABI from '../abis/CurateV2.json';
interface EventCallbacks {
onNewItem?: (itemId: string, data: string, addedDirectly: boolean) => void;
onStatusChange?: (itemId: string, updatedDirectly: boolean) => void;
onDispute?: (disputeId: bigint, requestId: bigint) => void;
onRuling?: (disputeId: bigint, ruling: number) => void;
}
export const useEventListeners = (curateAddress: string, callbacks: EventCallbacks) => {
const curate = useContract(curateAddress, CurateABI);
useEffect(() => {
if (!curate) return;
// Listen for new items
if (callbacks.onNewItem) {
curate.on("NewItem", (itemID, data, addedDirectly) => {
callbacks.onNewItem!(itemID, data, addedDirectly);
});
}
// Listen for status changes
if (callbacks.onStatusChange) {
curate.on("ItemStatusChange", (itemID, updatedDirectly) => {
callbacks.onStatusChange!(itemID, updatedDirectly);
});
}
// Listen for disputes
if (callbacks.onDispute) {
curate.on("DisputeRequest", (arbitrator, disputeID, requestID, templateId) => {
callbacks.onDispute!(disputeID, requestID);
});
}
// Listen for rulings
if (callbacks.onRuling) {
curate.on("Ruling", (arbitrator, disputeID, ruling) => {
callbacks.onRuling!(disputeID, Number(ruling));
});
}
// Cleanup listeners on unmount
return () => {
curate.removeAllListeners();
};
}, [curate, callbacks]);
};
Usage Example
Copy
Ask AI
// components/RegistryMonitor.tsx
import { useEventListeners } from '../hooks/useEventListeners';
export function RegistryMonitor({ curateAddress }: { curateAddress: string }) {
useEventListeners(curateAddress, {
onNewItem: (itemId, data, addedDirectly) => {
console.log('New item:', {
itemId,
data: JSON.parse(data),
addedDirectly
});
},
onStatusChange: (itemId, updatedDirectly) => {
console.log(`Item ${itemId} status changed`);
},
onDispute: (disputeId, requestId) => {
console.log('Dispute created:', {
disputeId: disputeId.toString(),
requestId: requestId.toString()
});
},
onRuling: (disputeId, ruling) => {
const outcomes = ['None', 'Requester', 'Challenger'];
console.log(`Dispute ${disputeId} ruled: ${outcomes[ruling]}`);
},
});
return <div>Monitoring registry events...</div>;
}
Complete Integration Example
Copy
Ask AI
// pages/CuratePage.tsx
import { useState } from 'react';
import { Web3Provider, useWeb3 } from '../context/Web3Provider';
import { useRegistry, useItem, useSubmitItem, useChallengeItem } from '../hooks';
import { formatEther } from 'ethers';
const CURATE_ADDRESS = '0x...'; // Your registry address
const VIEW_ADDRESS = '0x...'; // CurateView address
function CurateContent() {
const { address, connect } = useWeb3();
const [itemId, setItemId] = useState<string>('');
const { data: registry, isLoading: registryLoading } = useRegistry(CURATE_ADDRESS, VIEW_ADDRESS);
const { data: item, isLoading: itemLoading } = useItem(CURATE_ADDRESS, VIEW_ADDRESS, itemId);
const { submitItem, isLoading: submitting } = useSubmitItem(CURATE_ADDRESS, VIEW_ADDRESS);
const { challengeItem, isLoading: challenging } = useChallengeItem(CURATE_ADDRESS, VIEW_ADDRESS);
if (!address) {
return (
<button onClick={connect}>Connect Wallet</button>
);
}
return (
<div>
<h1>Curate V2 Integration</h1>
{/* Registry Info */}
{registry && (
<section>
<h2>Registry Configuration</h2>
<p>Submission Deposit: {formatEther(registry.submissionDeposit)} ETH</p>
<p>Challenge Period: {Number(registry.challengePeriod) / 3600} hours</p>
</section>
)}
{/* Submit Item */}
<section>
<h2>Submit New Item</h2>
<button
onClick={async () => {
const result = await submitItem({
data: {
name: "Example Token",
address: "0x...",
symbol: "EXT"
}
});
console.log('Submitted:', result.itemId);
setItemId(result.itemId);
}}
disabled={submitting}
>
{submitting ? 'Submitting...' : 'Submit Item'}
</button>
</section>
{/* Item Details */}
{item && (
<section>
<h2>Item Details</h2>
<p>Status: {item.statusName}</p>
<p>Disputed: {item.disputed ? 'Yes' : 'No'}</p>
{item.status === 2 && !item.disputed && (
<button
onClick={async () => {
await challengeItem({
itemId,
evidence: {
name: "Invalid Submission",
description: "This item violates the policy."
}
});
}}
disabled={challenging}
>
{challenging ? 'Challenging...' : 'Challenge Item'}
</button>
)}
</section>
)}
</div>
);
}
export default function CuratePage() {
return (
<Web3Provider>
<CurateContent />
</Web3Provider>
);
}
Deployment
Deploy to Testnet
Copy
Ask AI
npx hardhat deploy --network arbitrumSepolia
Deploy to Mainnet
Copy
Ask AI
npx hardhat deploy --network arbitrum
Verify Contracts
Copy
Ask AI
npx hardhat verify --network arbitrum DEPLOYED_ADDRESS