aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/invoices.ts30
-rw-r--r--routes/preimages.ts33
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) {