diff --git a/components/EnrollmentAttestation.tsx b/components/EnrollmentAttestation.tsx index ff1ad9d55..66e71e004 100644 --- a/components/EnrollmentAttestation.tsx +++ b/components/EnrollmentAttestation.tsx @@ -7,10 +7,8 @@ import NetworkSwitchButton from './NetworkSwitchButton'; import { BASE_SEPOLIA_CHAIN_ID, EAS_CONTRACT_ADDRESS, SCHEMA_UID } from '../utils/constants'; import { BrowserProvider, - Contract, Interface, type Log, - type TransactionResponse, type TransactionReceipt } from 'ethers'; @@ -19,37 +17,39 @@ const easInterface = new Interface([ 'event Attested(address indexed recipient, address indexed attester, bytes32 indexed uid, bytes32 schema)', ]); -interface EnrollmentAttestationProps { - verifiedName: string; - poapVerified: boolean; - onAttestationComplete: (attestationUID: string) => void; -} - -type AttestationError = { - message: string; - code?: number; -}; - -interface SchemaData { - userAddress: string; - verifiedName: string; - poapVerified: boolean; - timestamp: number; -} +// Initialize EAS instance +const eas = new EAS(EAS_CONTRACT_ADDRESS); +// Types interface AttestationData { recipient: string; expirationTime: bigint; revocable: boolean; refUID: string; data: string; + value: bigint; +} + +interface PreviewData { + userAddress: string; + verifiedName: string; + poapVerified: boolean; + timestamp: number; } -interface AttestationTransaction extends TransactionResponse { - wait(): Promise; +interface EnrollmentAttestationProps { + verifiedName: string; + poapVerified: boolean; + timestamp: number; + onAttestationComplete: (uid: string) => void; } -export default function EnrollmentAttestation({ verifiedName, poapVerified, onAttestationComplete }: EnrollmentAttestationProps) { +export function EnrollmentAttestation({ + verifiedName, + poapVerified, + timestamp, + onAttestationComplete +}: EnrollmentAttestationProps) { const { address } = useAccount(); const chainId = useChainId(); const publicClient = usePublicClient(); @@ -57,12 +57,7 @@ export default function EnrollmentAttestation({ verifiedName, poapVerified, onAt const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [transactionHash, setTransactionHash] = useState(null); - const [previewData, setPreviewData] = useState<{ - userAddress: string; - verifiedName: string; - poapVerified: boolean; - timestamp: number; - } | null>(null); + const [previewData, setPreviewData] = useState(null); const handleAttestationError = (err: Error) => { console.error('Attestation error:', { @@ -101,45 +96,31 @@ export default function EnrollmentAttestation({ verifiedName, poapVerified, onAt const createAttestation = async () => { if (!address || !previewData) { - setError("Please connect your wallet and preview the attestation first"); - return; - } - - if (!publicClient) { - setError("Web3 provider not available"); - return; - } - - if (chainId !== BASE_SEPOLIA_CHAIN_ID) { - setError("Please switch to Base Sepolia network"); + handleAttestationError(new Error("Please connect your wallet and preview the attestation first")); return; } + setLoading(true); try { - setLoading(true); - setError(null); - setTransactionHash(null); + // Debounce the button click + if (loading) return; - // Initialize EAS with the correct contract address - const eas = new EAS(EAS_CONTRACT_ADDRESS); + console.log('Starting attestation creation...'); - // Ensure wallet is connected - if (!walletClient) throw new Error("Wallet client not available"); - if (!address) throw new Error("Wallet not connected"); + // Network check and switch with timeout + await Promise.race([ + handleNetworkSwitch(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Network switch timeout')), 10000) + ) + ]); - // Create an Ethers v6 provider and signer + // Create an Ethers provider and signer const provider = new BrowserProvider(window.ethereum); const signer = await provider.getSigner(); - // Connect the signer to EAS - await eas.connect(signer); - - // Ensure we're on the correct network - const network = await provider.getNetwork(); - if (network.chainId !== BigInt(BASE_SEPOLIA_CHAIN_ID)) { - await handleNetworkSwitch(); - return; // Exit and let the user try again after network switch - } + // Connect EAS with signer + eas.connect(signer); // Create Schema Encoder instance and encode data const schemaEncoder = new SchemaEncoder("address userAddress,string verifiedName,bool poapVerified,uint256 timestamp"); @@ -150,108 +131,116 @@ export default function EnrollmentAttestation({ verifiedName, poapVerified, onAt { name: "timestamp", value: previewData.timestamp, type: "uint256" } ]); - // Prepare the attestation request - const attestationData: AttestationData = { + const attestationData = { recipient: address, expirationTime: BigInt(0), revocable: true, refUID: '0x0000000000000000000000000000000000000000000000000000000000000000', - data: encodedData + data: encodedData, + value: BigInt(0), }; console.log('Creating attestation with data:', attestationData); - - // Submit the attestation console.log('Submitting attestation with schema:', SCHEMA_UID); - console.log('Attestation data:', attestationData); - // Cast the transaction to unknown first, then to AttestationTransaction - const rawTransaction = await eas.attest({ + // Create attestation with timeout + const attestationPromise = eas.attest({ schema: SCHEMA_UID, - data: attestationData + data: attestationData, }); - const transaction = rawTransaction as unknown as AttestationTransaction; - console.log('Transaction submitted:', transaction); + const rawTransaction = await Promise.race([ + attestationPromise, + new Promise((_, reject) => + setTimeout(() => reject(new Error('Attestation creation timeout')), 30000) + ) + ]) as { wait: () => Promise }; - // Wait for the transaction to be mined + console.log('Transaction submitted:', rawTransaction); console.log('Waiting for transaction confirmation...'); - const receipt = await transaction.wait(); - console.log('Transaction confirmed:', receipt); - if (!receipt || !receipt.logs) { - console.error('Invalid transaction receipt:', receipt); - throw new Error('Invalid transaction receipt'); - } + // Process transaction receipt with timeout + const receipt = await Promise.race([ + rawTransaction.wait(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Transaction confirmation timeout')), 60000) + ) + ]) as TransactionReceipt; - // Set transaction hash setTransactionHash(receipt.hash); - // Get the transaction logs - const logs = receipt.logs; - console.log('Processing transaction logs:', logs); - - // Find and parse the attestation event log - const attestationLog = logs.find((log: Log) => { - if (!log?.topics?.[0]) { - console.error('Invalid log format:', log); - return false; - } - try { - const event = easInterface.getEvent('Attested'); - if (!event) { - console.error('Failed to get Attested event from interface'); - return false; - } - return log.topics[0].toLowerCase() === event.topicHash.toLowerCase(); - } catch (e) { - console.error('Error processing log:', e); - return false; - } - }); + // Process logs asynchronously + const uid = await processAttestationLogs(receipt); + onAttestationComplete(uid); + } catch (error: any) { + console.error('Error creating attestation:', error); + handleAttestationError(error); + } finally { + setLoading(false); + } + }; - if (!attestationLog) { - console.error('No attestation event found in logs:', logs); - throw new Error('Unable to find attestation event in transaction logs'); - } + // Separate async function for processing logs + const processAttestationLogs = async (receipt: TransactionReceipt): Promise => { + const logs = receipt.logs; + console.log('Processing transaction logs:', logs); + // Find attestation log asynchronously + const attestationLog = await new Promise((resolve, reject) => { try { - // Parse the event using ethers Interface - const parsedLog = easInterface.parseLog({ - topics: attestationLog.topics as string[], - data: attestationLog.data + const log = logs.find((log: Log | undefined) => { + if (!log?.topics?.[0]) return false; + const event = easInterface.getEvent('Attested'); + return event && log.topics[0].toLowerCase() === event.topicHash.toLowerCase(); }); - - if (!parsedLog || !parsedLog.args) { - throw new Error('Failed to parse attestation event'); - } - - const uid = parsedLog.args.uid; - if (!uid) { - throw new Error('No UID found in parsed event'); + if (!log) { + reject(new Error('No attestation event found in logs')); + return; } - console.log('Successfully parsed attestation event. UID:', uid); + resolve(log); + } catch (e) { + reject(e); + } + }); - // Verify the attestation exists - const attestation = await eas.getAttestation(uid); - if (!attestation) { - throw new Error('Attestation verification failed'); + // Parse log with timeout + const parsedLog = await Promise.race([ + new Promise((resolve, reject) => { + try { + const result = easInterface.parseLog({ + topics: attestationLog.topics as string[], + data: attestationLog.data + }); + if (!result?.args?.uid) reject(new Error('Invalid parsed log')); + resolve(result); + } catch (e) { + reject(e); } - console.log('Attestation verified:', attestation); - - onAttestationComplete(uid); - setLoading(false); - return uid; - } catch (error) { - console.error('Error parsing attestation event:', error); - throw new Error('Failed to parse attestation event. Please check the transaction logs.'); - } - } catch (err: any) { - console.error('Error creating attestation:', err); - handleAttestationError(err); - setLoading(false); + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Log parsing timeout')), 5000) + ) + ]); + + const uid = (parsedLog as any).args.uid; + console.log('Successfully parsed attestation event. UID:', uid); + + // Verify attestation with timeout + const attestation = await Promise.race([ + eas.getAttestation(uid), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Attestation verification timeout')), 10000) + ) + ]); + + if (!attestation) { + throw new Error('Attestation verification failed'); } + + console.log('Attestation verified:', attestation); + return uid; }; + return ( @@ -261,79 +250,70 @@ export default function EnrollmentAttestation({ verifiedName, poapVerified, onAt {!address && ( - - Please connect your wallet to continue + + Please connect your wallet to create an attestation )} - {/* Preview Section */} - {previewData && ( - - Attestation Preview - User Address: {previewData?.userAddress} - Verified Name: {previewData?.verifiedName} - POAP Verified: {previewData?.poapVerified ? 'Yes' : 'No'} - Timestamp: {new Date(previewData?.timestamp * 1000).toLocaleString()} - - )} - - {chainId !== BASE_SEPOLIA_CHAIN_ID && address && ( + {address && chainId !== BASE_SEPOLIA_CHAIN_ID && ( - - - )} - - {error && ( - - {error} - - )} - - {/* Preview Button */} - {address && ( - - )} - - {/* Create Attestation Button */} - {address && previewData && ( - - )} - - {/* Transaction Hash Display */} - {transactionHash && ( - - - Transaction Hash: - {transactionHash} - + + Please switch to Base Sepolia network + )} + {address && chainId === BASE_SEPOLIA_CHAIN_ID && ( + <> + + + Click below to create your enrollment attestation + + + + + {error && ( + + {error} + + )} + + {transactionHash && ( + + + Transaction Hash:{' '} + + {transactionHash} + + + + )} + + )} );