From 8c7fa95d44aabd38fadc3ef955d835ad3379ec6c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 18 Sep 2025 14:42:13 -0700 Subject: sendinvoice support --- index.ts | 1 + lib/invoices.ts | 29 +++++++++++++++++++---------- lib/lightning.ts | 10 ++++++---- lib/payments.ts | 2 +- routes/payments.ts | 29 +++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 15 deletions(-) diff --git a/index.ts b/index.ts index 0ca35ec5..d9538427 100644 --- a/index.ts +++ b/index.ts @@ -67,6 +67,7 @@ app.post("/zapRequest", auth, nostr.zapRequest); app.get("/thread/:id", nostr.thread); app.get("/info", payments.info); +app.post("/sendinvoice", auth, payments.sendinvoice); app.post("/payments", auth, payments.create); app.get("/payments", auth, payments.list); app.get("/payments/:hash", payments.get); diff --git a/lib/invoices.ts b/lib/invoices.ts index 45987ae1..1800b859 100644 --- a/lib/invoices.ts +++ b/lib/invoices.ts @@ -16,6 +16,7 @@ export const generate = async ({ invoice, user }) => { let { address_type, bolt11, + bolt12, aid, currency, expiry, @@ -35,8 +36,8 @@ export const generate = async ({ invoice, user }) => { secret, } = invoice; - amount = parseInt(amount || 0); - tip = tip == null ? null : parseInt(tip); + amount = Number.parseInt(amount || 0); + tip = tip == null ? null : Number.parseInt(tip); if (user) user = await getUser(user.username); if (!user) fail("user not provided"); @@ -95,18 +96,26 @@ export const generate = async ({ invoice, user }) => { text = r.bolt11; paymentHash = r.payment_hash; } else if (type === PaymentType.bolt12) { - const r = await ln.offer({ - amount: amount ? `${amount + tip}sat` : "any", - label: `${id} ${user.username} ${new Date()}`, - description: memo || id, - }); + let r; + if (bolt12) { + const { id: nodeid } = await ln.getinfo(); + r = await ln.decode(bolt12); + if (r.invoice_node_id !== nodeid) fail("invalid invoice"); + amount = Math.round(r.invoice_amount_msat / 1000); + r.bolt12 = bolt12; + } else { + const r = await ln.offer({ + amount: amount ? `${amount + tip}sat` : "any", + label: `${id} ${user.username} ${new Date()}`, + description: memo || id, + }); - if (await getInvoice(r.offer_id)) fail("Duplicate offer exists"); + if (await getInvoice(r.offer_id)) fail("Duplicate offer exists"); + await s(`invoice:${r.offer_id}`, id); + } hash = r.bolt12; text = r.bolt12; - - await s(`invoice:${r.offer_id}`, id); } else if (type === PaymentType.bitcoin) { address_type ||= "bech32"; hash = await bc.getNewAddress({ address_type }); diff --git a/lib/lightning.ts b/lib/lightning.ts index 8f5f44bc..f8d0f6a4 100644 --- a/lib/lightning.ts +++ b/lib/lightning.ts @@ -27,7 +27,7 @@ export async function listenForLightning() { try { if (!preimage) return; - const invoice = await getInvoice(bolt11 ?? local_offer_id); + const invoice = await getInvoice(bolt11 ?? local_offer_id ?? bolt12); if (!invoice) return warn("received lightning with no invoice", bolt11); const p = await getPayment(bolt11 || bolt12); @@ -75,10 +75,10 @@ export async function replay(index) { try { if (!preimage) return; - const invoice = await getInvoice(bolt11 ?? local_offer_id); + const invoice = await getInvoice(bolt11 ?? local_offer_id ?? bolt12); if (!invoice) return warn("received lightning with no invoice", bolt11); - const p = await getPayment(bolt11 || bolt12); + let p = await getPayment(bolt11 || bolt12); if (p) return warn("already processed", bolt11 || bolt12); if (invoice?.memo) { @@ -93,13 +93,15 @@ export async function replay(index) { } } - await credit({ + p = await credit({ hash: bolt11 || bolt12, amount: received, memo: invoice.memo, ref: preimage, type: bolt12 ? PaymentType.bolt12 : PaymentType.lightning, }); + + return p; } catch (e) { console.log(e); err("problem receiving lightning payment", e.message); diff --git a/lib/payments.ts b/lib/payments.ts index 807c87fa..9b7c807b 100644 --- a/lib/payments.ts +++ b/lib/payments.ts @@ -185,7 +185,7 @@ export const credit = async ({ amount = parseInt(amount) || 0; let inv; - if (type === PaymentType.bolt12) { + if (type === PaymentType.bolt12 && !hash.startsWith("lni")) { const { invoices } = await ln.listinvoices({ invstring: hash }); const { local_offer_id } = invoices[0]; inv = await getInvoice(local_offer_id); diff --git a/routes/payments.ts b/routes/payments.ts index 3d289a1d..9df20c76 100644 --- a/routes/payments.ts +++ b/routes/payments.ts @@ -3,6 +3,7 @@ import api from "$lib/api"; import { requirePin } from "$lib/auth"; import { db, g, s } from "$lib/db"; import { generate } from "$lib/invoices"; +import { replay } from "$lib/lightning"; import ln from "$lib/ln"; import { err, l, warn } from "$lib/logging"; import mqtt from "$lib/mqtt"; @@ -30,6 +31,7 @@ import { } from "$lib/utils"; import rpc from "@coinos/rpc"; import got from "got"; +import { v4 } from "uuid"; export default { async info(_, res) { @@ -704,4 +706,31 @@ export default { console.log(req.body); res.send(req.body); }, + + async sendinvoice(req, res) { + try { + const { user } = req; + const { invreq } = req.body; + + const { amount_msat, bolt12, pay_index } = await ln.sendinvoice({ + invreq, + label: v4(), + }); + + await generate({ + invoice: { + amount: Math.round(amount_msat / 1000), + type: "bolt12", + bolt12, + }, + user, + }); + + const p = await replay(pay_index); + + res.send(p); + } catch (e) { + bail(res, e.message); + } + }, }; -- cgit v1.2.3