Replace remote with local repo
This commit is contained in:
24
app/api/credits/balance/route.ts
Normal file
24
app/api/credits/balance/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* GET /api/credits/balance?handle=<handle>
|
||||
* Returns the VOID credit balance and unlocked tool list for a handle.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { getHandleRecord } from "@/lib/serverLedger";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const handle = searchParams.get("handle")?.trim() ?? "";
|
||||
|
||||
if (!handle) {
|
||||
return NextResponse.json({ ok: false, error: "handle is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const rec = getHandleRecord(handle);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
handle: handle.toLowerCase(),
|
||||
voidCredits: rec.voidCredits,
|
||||
unlockedToolIds: rec.unlockedTools.map((t) => t.toolId),
|
||||
});
|
||||
}
|
||||
90
app/api/credits/claim/route.ts
Normal file
90
app/api/credits/claim/route.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* 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<string, unknown>) ?? {};
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user