aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/invoices.ts33
-rw-r--r--lib/payments.ts7
2 files changed, 32 insertions, 8 deletions
diff --git a/lib/invoices.ts b/lib/invoices.ts
index 434a2375..c3722ef5 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -13,6 +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
+export const parseEntry = (e) => {
+ try {
+ const parsed = JSON.parse(e);
+ if (parsed?.preimage) return { price: null, ...parsed };
+ } catch {}
+ return { preimage: e, price: null };
+};
+
export const generate = async ({ invoice, user }) => {
let {
address_type,
@@ -89,7 +99,6 @@ export const generate = async ({ invoice, user }) => {
expiry ||= 60 * 60 * 24 * 30;
const args = {
- amount_msat: amount ? `${amount + tip}sat` : "any",
label: `${id} ${user.username} ${Date.now()}`,
description: memo || "",
expiry,
@@ -103,10 +112,19 @@ export const generate = async ({ invoice, user }) => {
// merchant chose. Never fall back to a random preimage — a buyer would
// be paying for a secret that unlocks nothing.
const queue = `${user.id}:preimages`;
- let preimage;
- while ((preimage = await db.lPop(queue))) {
+ let entry;
+ while ((entry = await db.lPop(queue))) {
+ const { preimage, price } = 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
+ if (price) amount = price;
try {
- r = await ln.invoice({ ...args, preimage });
+ r = await ln.invoice({
+ ...args,
+ amount_msat: amount ? `${amount + tip}sat` : "any",
+ preimage,
+ });
// Keep the preimage recoverable for internal settlements, where
// cln never pays the invoice and so never reveals it — credit()
// picks this up as the payment ref
@@ -121,13 +139,16 @@ export const generate = async ({ invoice, user }) => {
warn("discarding unusable preimage", user.username, e.message);
continue;
}
- await db.lPush(queue, preimage);
+ await db.lPush(queue, entry);
throw e;
}
}
if (!r) fail("no preimages available");
} else {
- r = await ln.invoice(args);
+ r = await ln.invoice({
+ ...args,
+ amount_msat: amount ? `${amount + tip}sat` : "any",
+ });
}
}
diff --git a/lib/payments.ts b/lib/payments.ts
index 01de2eff..df2c8857 100644
--- a/lib/payments.ts
+++ b/lib/payments.ts
@@ -293,10 +293,13 @@ export const credit = async ({
// preimage:${paymentHash} (see generate()). Prefer it as the settlement
// ref: external settlements already carry the same value, but internal
// ones pass ref=sender.id, which would otherwise be surfaced as the
- // payment preimage and lose the secret the buyer paid for.
+ // payment preimage and lose the secret the buyer paid for. Only once the
+ // invoice is fully paid, though — internal sends can settle partial
+ // amounts, and an underpayment must not reveal the secret.
if (
inv.paymentHash &&
- ![PaymentType.bitcoin, PaymentType.liquid].includes(type)
+ ![PaymentType.bitcoin, PaymentType.liquid].includes(type) &&
+ inv.received + amount >= inv.amount
) {
const stored = await db.get(`preimage:${inv.paymentHash}`);
if (stored) ref = stored;