diff options
| -rw-r--r-- | lib/invoices.ts | 30 | ||||
| -rw-r--r-- | routes/preimages.ts | 33 |
2 files changed, 49 insertions, 14 deletions
diff --git a/lib/invoices.ts b/lib/invoices.ts index c3722ef5..613287a3 100644 --- a/lib/invoices.ts +++ b/lib/invoices.ts @@ -13,14 +13,16 @@ import { PaymentType } from "$lib/types"; const bc = rpc(config.bitcoin); const lq = rpc(config.liquid); -// Preimage queue entries are {preimage, price} JSON, but entries queued -// before prices existed are bare hex strings — treat those as priceless +// Preimage queue entries are {preimage, price, fiat, currency} JSON, but +// entries queued before prices existed are bare hex strings — treat those +// as priceless export const parseEntry = (e) => { try { const parsed = JSON.parse(e); - if (parsed?.preimage) return { price: null, ...parsed }; + if (parsed?.preimage) + return { price: null, fiat: null, currency: null, ...parsed }; } catch {} - return { preimage: e, price: null }; + return { preimage: e, price: null, fiat: null, currency: null }; }; export const generate = async ({ invoice, user }) => { @@ -114,11 +116,27 @@ export const generate = async ({ invoice, user }) => { const queue = `${user.id}:preimages`; let entry; while ((entry = await db.lPop(queue))) { - const { preimage, price } = parseEntry(entry); + const { + preimage, + price, + fiat: entryFiat, + currency: entryCurrency, + } = parseEntry(entry); // The invoice creator is unauthenticated, so a merchant-set price // must win over the amount in the request — otherwise a buyer - // could name their own price for the secret + // could name their own price for the secret. Fiat prices convert + // at the current rate, and the invoice takes on the merchant's + // currency so receipts show the fiat price as set. if (price) amount = price; + else if (entryFiat) { + if (!rates[entryCurrency]) { + await db.lPush(queue, entry); + fail(`no rate available for ${entryCurrency}`); + } + currency = entryCurrency; + rate = rates[currency]; + amount = Math.round((SATS * entryFiat) / rate); + } try { r = await ln.invoice({ ...args, diff --git a/routes/preimages.ts b/routes/preimages.ts index f6632b38..49673d17 100644 --- a/routes/preimages.ts +++ b/routes/preimages.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import { db } from "$lib/db"; +import { db, g } from "$lib/db"; import { parseEntry } from "$lib/invoices"; import { bail, fail } from "$lib/utils"; @@ -7,9 +7,10 @@ import { bail, fail } from "$lib/utils"; // with usePreimage: true — each such invoice consumes the next queued preimage // so its payment hash is sha256(preimage). Once paid, the preimage is exposed // on the invoice record, letting a static storefront sell decryption secrets -// with no backend of its own. An optional per-batch price (sats) is enforced -// at invoice creation — invoice creation is unauthenticated, so without it a -// buyer could name their own price for the secret. +// with no backend of its own. An optional per-batch price — sats, or a fiat +// amount converted at invoice-creation time — is enforced at invoice creation; +// creation is unauthenticated, so without it a buyer could name their own +// price for the secret. const MAX_QUEUE = 1000; const key = (uid) => `${uid}:preimages`; @@ -19,7 +20,8 @@ const sha256 = (hex) => export default { async add(req, res) { try { - const { preimages, price = null } = req.body; + const { preimages, price = null, fiat = null } = req.body; + let { currency = null } = req.body; if (!Array.isArray(preimages) || !preimages.length) fail("preimages array required"); if ( @@ -28,8 +30,19 @@ export default { ) ) fail("preimages must be 64-character hex strings"); + if (price !== null && fiat !== null) + fail("specify price (sats) or fiat, not both"); if (price !== null && (!Number.isInteger(price) || price <= 0)) fail("price must be a positive integer amount of sats"); + if (fiat !== null) { + if (typeof fiat !== "number" || !(fiat > 0)) + fail("fiat must be a positive number"); + currency ||= req.user.currency; + const rates = await g("rates"); + if (!rates?.[currency]) fail(`unsupported currency ${currency}`); + } else { + currency = null; + } const k = key(req.user.id); const existing = await db.lRange(k, 0, -1); @@ -47,7 +60,9 @@ export default { if (fresh.length) await db.rPush( k, - fresh.map((preimage) => JSON.stringify({ preimage, price })), + fresh.map((preimage) => + JSON.stringify({ preimage, price, fiat, currency }), + ), ); res.send({ @@ -56,6 +71,8 @@ export default { preimage, hash: sha256(preimage), price, + fiat, + currency, })), }); } catch (e) { @@ -69,8 +86,8 @@ export default { res.send({ queued: entries.length, preimages: entries.map((e) => { - const { preimage, price } = parseEntry(e); - return { preimage, hash: sha256(preimage), price }; + const { preimage, price, fiat, currency } = parseEntry(e); + return { preimage, hash: sha256(preimage), price, fiat, currency }; }), }); } catch (e) { |
