diff --git a/src/app/api/index/donation/route.ts b/src/app/api/index/donation/route.ts new file mode 100644 index 00000000..b783ae53 --- /dev/null +++ b/src/app/api/index/donation/route.ts @@ -0,0 +1,105 @@ +import { NextResponse } from "next/server"; +import { type Hex, decodeEventLog, encodeEventTopics } from "viem"; +import { publicClient } from "../../../../../lib/viem"; +import { createServerClient } from "../../../../../lib/supabase"; +import { + storyFactoryAbi, + donationEvent, +} from "../../../../../lib/contracts/abi"; +import type { Database } from "../../../../../lib/supabase"; + +/** Donation event topic0 */ +const DONATION_TOPIC = encodeEventTopics({ + abi: [donationEvent], + eventName: "Donation", +})[0]; + +function error(message: string, status = 400) { + return NextResponse.json({ error: message }, { status }); +} + +export async function POST(req: Request) { + const body = await req.json(); + const txHash = body.txHash as Hex | undefined; + + if (!txHash || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) { + return error("Missing or invalid txHash"); + } + + // 1. Fetch receipt + let receipt; + try { + receipt = await publicClient.getTransactionReceipt({ hash: txHash }); + } catch { + return error("Failed to fetch transaction receipt", 502); + } + + if (receipt.status !== "success") { + return error("Transaction failed"); + } + + // 2. Find Donation event log by event signature (topic0) + const donationLog = receipt.logs.find( + (log) => log.topics[0] === DONATION_TOPIC + ); + + if (!donationLog) { + return error("Donation event not found in receipt"); + } + + // 3. Decode event + let decoded; + try { + decoded = decodeEventLog({ + abi: storyFactoryAbi, + data: donationLog.data, + topics: donationLog.topics, + }); + } catch { + return error("Failed to decode Donation event"); + } + + if (decoded.eventName !== "Donation") { + return error("Unexpected event type"); + } + + const { storylineId, donor, amount } = decoded.args; + + // 4. Get block timestamp + let blockTimestamp: bigint; + try { + const block = await publicClient.getBlock({ + blockNumber: receipt.blockNumber, + }); + blockTimestamp = block.timestamp; + } catch { + return error("Failed to fetch block", 502); + } + + // 5. Upsert to Supabase + const supabase = createServerClient(); + if (!supabase) { + return error("Supabase not configured", 500); + } + + const row: Database["public"]["Tables"]["donations"]["Insert"] = { + storyline_id: Number(storylineId), + donor_address: donor.toLowerCase(), + amount: amount.toString(), // wei string to avoid precision loss + block_timestamp: new Date(Number(blockTimestamp) * 1000).toISOString(), + tx_hash: txHash.toLowerCase(), + log_index: donationLog.logIndex!, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { error: dbError } = await (supabase.from("donations") as any).upsert( + row, + { onConflict: "tx_hash,log_index" } + ); + + if (dbError) { + return error(`Database error: ${dbError.message}`, 500); + } + + return NextResponse.json({ success: true }); +}