Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/app/api/index/donation/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Loading