From 0a189ae11d1991ffff832a841c49262467fd878d Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 19 Mar 2026 13:40:03 +0000 Subject: [PATCH] [#370] Add one-off backfill endpoint for trade_history user_address POST /api/backfill-user-address fetches trade_history rows with NULL user_address, reads tx receipts from chain, decodes Mint/Burn events to extract the user address, and updates the rows. Protected by CRON_SECRET auth. Groups by tx_hash to minimize RPC calls. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/backfill-user-address/route.ts | 123 +++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/app/api/backfill-user-address/route.ts diff --git a/src/app/api/backfill-user-address/route.ts b/src/app/api/backfill-user-address/route.ts new file mode 100644 index 00000000..778c7dd6 --- /dev/null +++ b/src/app/api/backfill-user-address/route.ts @@ -0,0 +1,123 @@ +import { NextResponse } from "next/server"; +import { decodeEventLog } from "viem"; +import { publicClient } from "../../../../lib/rpc"; +import { createServerClient } from "../../../../lib/supabase"; +import { mcv2BondEventAbi } from "../../../../lib/contracts/abi"; +import { MCV2_BOND } from "../../../../lib/contracts/constants"; + +/** Fail closed in production when CRON_SECRET is unset */ +function verifyCron(req: Request): boolean { + const secret = process.env.CRON_SECRET; + if (!secret) { + return process.env.NODE_ENV !== "production"; + } + const authHeader = req.headers.get("authorization"); + return authHeader === `Bearer ${secret}`; +} + +export async function POST(req: Request) { + if (!verifyCron(req)) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const supabase = createServerClient(); + if (!supabase) { + return NextResponse.json({ error: "Supabase not configured" }, { status: 500 }); + } + + // Fetch all trade_history rows missing user_address + const { data: rows, error: fetchError } = await supabase + .from("trade_history") + .select("id, tx_hash, log_index") + .is("user_address", null) + .order("id", { ascending: true }) + .limit(500); + + if (fetchError) { + return NextResponse.json({ error: fetchError.message }, { status: 500 }); + } + + if (!rows || rows.length === 0) { + return NextResponse.json({ message: "No rows to backfill", updated: 0 }); + } + + let updated = 0; + let errors = 0; + const errorDetails: { id: number; tx_hash: string; reason: string }[] = []; + + // Group by tx_hash to minimize RPC calls + const byTx = new Map(); + for (const row of rows) { + const existing = byTx.get(row.tx_hash) ?? []; + existing.push(row); + byTx.set(row.tx_hash, existing); + } + + for (const [txHash, txRows] of byTx) { + try { + const receipt = await publicClient.getTransactionReceipt({ + hash: txHash as `0x${string}`, + }); + + for (const row of txRows) { + const log = receipt.logs.find( + (l) => + l.logIndex === row.log_index && + l.address.toLowerCase() === MCV2_BOND.toLowerCase(), + ); + + if (!log) { + errorDetails.push({ id: row.id, tx_hash: txHash, reason: "Log not found in receipt" }); + errors++; + continue; + } + + try { + const decoded = decodeEventLog({ + abi: mcv2BondEventAbi, + data: log.data, + topics: log.topics, + }); + + const args = decoded.args as { user: `0x${string}` }; + const userAddress = args.user.toLowerCase(); + + const { error: updateError } = await supabase + .from("trade_history") + .update({ user_address: userAddress }) + .eq("id", row.id); + + if (updateError) { + errorDetails.push({ id: row.id, tx_hash: txHash, reason: updateError.message }); + errors++; + } else { + updated++; + } + } catch (decodeErr) { + errorDetails.push({ + id: row.id, + tx_hash: txHash, + reason: decodeErr instanceof Error ? decodeErr.message : String(decodeErr), + }); + errors++; + } + } + } catch (rpcErr) { + for (const row of txRows) { + errorDetails.push({ + id: row.id, + tx_hash: txHash, + reason: `RPC error: ${rpcErr instanceof Error ? rpcErr.message : String(rpcErr)}`, + }); + errors++; + } + } + } + + return NextResponse.json({ + total: rows.length, + updated, + errors, + ...(errorDetails.length > 0 ? { errorDetails } : {}), + }); +}