/** * Client-triggered claim: user polls their invoice status and calls this * when it shows "Settled". Server re-verifies with BTCPay before crediting. * * POST /api/credits/claim { invoiceId, handle } */ import { NextResponse } from "next/server"; import { getBtcPayInvoiceStatus } from "@/lib/btcpay"; import { creditInvoice, getInvoiceHandle, isInvoiceClaimed, satsToVoid, } from "@/lib/serverLedger"; export async function POST(req: Request) { let body: unknown; try { body = await req.json(); } catch { return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 }); } const { invoiceId, handle } = (body as Record) ?? {}; if (typeof invoiceId !== "string" || !invoiceId.trim()) { return NextResponse.json({ ok: false, error: "invoiceId is required" }, { status: 400 }); } if (typeof handle !== "string" || !handle.trim()) { return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 }); } const cleanInvoice = invoiceId.trim(); const cleanHandle = handle.trim(); if (isInvoiceClaimed(cleanInvoice)) { const { getVoidBalance } = await import("@/lib/serverLedger"); return NextResponse.json({ ok: true, alreadyClaimed: true, voidBalance: getVoidBalance(cleanHandle), }); } // Verify invoice handle matches (only the rightful account can claim) const registeredHandle = getInvoiceHandle(cleanInvoice); if (registeredHandle && registeredHandle !== cleanHandle.toLowerCase()) { return NextResponse.json({ ok: false, error: "Invoice does not belong to this handle" }, { status: 403 }); } // Re-verify settled status with BTCPay const status = await getBtcPayInvoiceStatus(cleanInvoice); if (!status.ok) { return NextResponse.json({ ok: false, error: status.error }, { status: 502 }); } if (status.status !== "Settled") { return NextResponse.json({ ok: false, error: `Invoice not settled yet (status: ${status.status})`, status: status.status, }); } // Convert USD → VOID credits let voidToCredit = 0; try { const priceRes = await fetch( "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", { next: { revalidate: 60 } }, ); const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } }; const btcUsd = priceJson.bitcoin?.usd ?? 0; if (btcUsd > 0) { const sats = Math.floor((status.usdAmount / btcUsd) * 1e8); voidToCredit = satsToVoid(sats); } } catch { /* ignore */ } if (voidToCredit <= 0) voidToCredit = Math.max(1, Math.floor(status.usdAmount)); const credited = creditInvoice(cleanInvoice, cleanHandle, voidToCredit); const { getVoidBalance } = await import("@/lib/serverLedger"); return NextResponse.json({ ok: true, credited, voidCredited: voidToCredit, voidBalance: getVoidBalance(cleanHandle), usdAmount: status.usdAmount, }); }