Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
|
|
const MEMPOOL = "https://mempool.space/api";
|
|
const MIN_CONFIRMATIONS = 1;
|
|
|
|
function merchantAddress(): string {
|
|
return (
|
|
process.env.MERCHANT_BTC_ADDRESS ||
|
|
process.env.NEXT_PUBLIC_MERCHANT_BTC_ADDRESS ||
|
|
""
|
|
).trim();
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
const merchant = merchantAddress();
|
|
if (!merchant) {
|
|
return NextResponse.json(
|
|
{ ok: false, error: "Server not configured: set MERCHANT_BTC_ADDRESS" },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ ok: false, error: "Invalid JSON" }, { status: 400 });
|
|
}
|
|
|
|
const txid =
|
|
typeof body === "object" &&
|
|
body !== null &&
|
|
"txid" in body &&
|
|
typeof (body as { txid: unknown }).txid === "string"
|
|
? (body as { txid: string }).txid.trim()
|
|
: "";
|
|
|
|
if (!/^[a-fA-F0-9]{64}$/.test(txid)) {
|
|
return NextResponse.json({ ok: false, error: "Invalid transaction id" }, { status: 400 });
|
|
}
|
|
|
|
const txRes = await fetch(`${MEMPOOL}/tx/${txid}`, { cache: "no-store" });
|
|
if (!txRes.ok) {
|
|
return NextResponse.json(
|
|
{ ok: false, error: "Transaction not found on network yet" },
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
|
|
const tx = (await txRes.json()) as {
|
|
vout?: Array<{ value?: number; scriptpubkey_address?: string }>;
|
|
status?: { confirmed?: boolean; block_height?: number };
|
|
};
|
|
|
|
const merchantLower = merchant.toLowerCase();
|
|
let sats = 0;
|
|
for (const vout of tx.vout || []) {
|
|
const addr = vout.scriptpubkey_address;
|
|
if (addr && addr.toLowerCase() === merchantLower && typeof vout.value === "number") {
|
|
sats += vout.value;
|
|
}
|
|
}
|
|
|
|
if (sats <= 0) {
|
|
return NextResponse.json(
|
|
{
|
|
ok: false,
|
|
error: "This transaction does not pay your configured merchant Bitcoin address",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
let confirmations = 0;
|
|
if (tx.status?.confirmed && typeof tx.status.block_height === "number") {
|
|
const tipRes = await fetch(`${MEMPOOL}/blocks/tip/height`, { cache: "no-store" });
|
|
if (tipRes.ok) {
|
|
const tip = Number(await tipRes.text());
|
|
if (Number.isFinite(tip)) {
|
|
confirmations = tip - tx.status.block_height + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (confirmations < MIN_CONFIRMATIONS) {
|
|
return NextResponse.json({
|
|
ok: false,
|
|
error: `Need at least ${MIN_CONFIRMATIONS} confirmation(s); currently ${confirmations}`,
|
|
confirmations,
|
|
sats,
|
|
});
|
|
}
|
|
|
|
const priceRes = await fetch(
|
|
"https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
|
|
{ next: { revalidate: 60 } },
|
|
);
|
|
if (!priceRes.ok) {
|
|
return NextResponse.json(
|
|
{ ok: false, error: "Could not fetch BTC/USD rate; try again shortly" },
|
|
{ status: 502 },
|
|
);
|
|
}
|
|
const priceJson = (await priceRes.json()) as { bitcoin?: { usd?: number } };
|
|
const btcUsd = priceJson.bitcoin?.usd;
|
|
if (!btcUsd || !Number.isFinite(btcUsd)) {
|
|
return NextResponse.json({ ok: false, error: "Invalid rate response" }, { status: 502 });
|
|
}
|
|
|
|
const btc = sats / 1e8;
|
|
const creditedUsd = Math.round(btc * btcUsd * 100) / 100;
|
|
|
|
return NextResponse.json({
|
|
ok: true,
|
|
creditedUsd,
|
|
sats,
|
|
btc,
|
|
btcUsd,
|
|
confirmations,
|
|
});
|
|
}
|