88 lines
3 KiB
JavaScript
88 lines
3 KiB
JavaScript
|
|
/**
|
||
|
|
* Public invoice endpoints.
|
||
|
|
*
|
||
|
|
* POST /api/invoice {listing_id} → create a fresh invoice
|
||
|
|
* GET /api/invoice/<id> → status
|
||
|
|
*
|
||
|
|
* No auth. The invoice contents are a payment request to the operator's own
|
||
|
|
* address — paying the exact amount shown settles the listing it was issued
|
||
|
|
* for. If somebody creates an unused invoice, they have an unused invoice;
|
||
|
|
* nothing moves until the mempool watcher (or an admin) sees the payment.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import {
|
||
|
|
createInvoice,
|
||
|
|
paymentsConfigured,
|
||
|
|
toPublic as invoiceToPublic,
|
||
|
|
reapInvoice,
|
||
|
|
} from './payments.js';
|
||
|
|
import { getInvoice, getListing } from './db.js';
|
||
|
|
|
||
|
|
export async function handleInvoiceCreate(req, res) {
|
||
|
|
if (req.method !== 'POST') return json(res, 405, { ok: false, reason: 'method not allowed' });
|
||
|
|
if (!paymentsConfigured()) {
|
||
|
|
return json(res, 503, { ok: false, reason: 'payments are not enabled on this instance' });
|
||
|
|
}
|
||
|
|
|
||
|
|
let body;
|
||
|
|
try { body = await readJson(req); }
|
||
|
|
catch (err) { return json(res, 400, { ok: false, reason: String(err?.message || err) }); }
|
||
|
|
|
||
|
|
const listingId = String(body?.listing_id || '').trim();
|
||
|
|
if (!listingId) return json(res, 400, { ok: false, reason: '`listing_id` required' });
|
||
|
|
const listing = getListing(listingId);
|
||
|
|
if (!listing) return json(res, 404, { ok: false, reason: 'no such listing' });
|
||
|
|
|
||
|
|
try {
|
||
|
|
const invoice = await createInvoice(listingId);
|
||
|
|
return json(res, 201, { ok: true, invoice: invoiceToPublic(invoice) });
|
||
|
|
} catch (err) {
|
||
|
|
return json(res, 500, { ok: false, reason: String(err?.message || err) });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function handleInvoiceGet(req, res, url) {
|
||
|
|
const parts = new URL(url, 'http://localhost').pathname.split('/').filter(Boolean);
|
||
|
|
// ['api', 'invoice', '<id>']
|
||
|
|
if (parts.length !== 3) return json(res, 400, { ok: false, reason: 'expected /api/invoice/<id>' });
|
||
|
|
const id = parts[2];
|
||
|
|
const invoice = getInvoice(id);
|
||
|
|
if (!invoice) return json(res, 404, { ok: false, reason: 'no such invoice' });
|
||
|
|
|
||
|
|
// A pending invoice past its deadline flips to 'expired' here lazily — the
|
||
|
|
// mempool watcher does not need a periodic sweeper if every status check
|
||
|
|
// takes care of its own invoice.
|
||
|
|
if (invoice.status === 'pending' && Date.parse(invoice.expires_at) < Date.now()) {
|
||
|
|
reapInvoice(id);
|
||
|
|
invoice.status = 'expired';
|
||
|
|
}
|
||
|
|
return json(res, 200, { ok: true, invoice: invoiceToPublic(invoice) });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------- helpers -------- */
|
||
|
|
|
||
|
|
const MAX_BODY = 32 * 1024;
|
||
|
|
|
||
|
|
async function readJson(req) {
|
||
|
|
const chunks = [];
|
||
|
|
let total = 0;
|
||
|
|
for await (const chunk of req) {
|
||
|
|
total += chunk.length;
|
||
|
|
if (total > MAX_BODY) throw new Error('body too large');
|
||
|
|
chunks.push(chunk);
|
||
|
|
}
|
||
|
|
const body = Buffer.concat(chunks).toString('utf8');
|
||
|
|
if (!body.trim()) throw new Error('empty request body');
|
||
|
|
return JSON.parse(body);
|
||
|
|
}
|
||
|
|
|
||
|
|
function json(res, status, body) {
|
||
|
|
const payload = JSON.stringify(body);
|
||
|
|
res.writeHead(status, {
|
||
|
|
'content-type': 'application/json; charset=utf-8',
|
||
|
|
'cache-control': 'no-store',
|
||
|
|
'content-length': Buffer.byteLength(payload),
|
||
|
|
});
|
||
|
|
res.end(payload);
|
||
|
|
}
|