From 81c3593f1eec7f94dc9cbc22edc3456fa57b95cb Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Tue, 14 Jul 2026 22:06:25 -0700 Subject: preimages: support fiat prices, converted at invoice-creation time POST /preimages now accepts fiat + optional currency (defaults to the merchant's) as an alternative to a sat price. The fiat amount is stored per entry and converted to sats at the moment a usePreimage invoice is created, so the price tracks the exchange rate instead of freezing at upload time. The invoice also takes on the entry's currency and rate so receipts display the fiat price exactly as the merchant set it. If no rate is available for the entry's currency, the preimage is pushed back onto the queue and creation fails rather than issuing an any-amount invoice. Co-Authored-By: Claude Fable 5 --- lib/invoices.ts | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) (limited to 'lib') 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, -- cgit v1.2.3