diff options
| author | Adam Soltys <[email protected]> | 2026-07-30 08:51:36 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-30 08:51:36 -0700 |
| commit | add3ad93ff37f1daaedab5788f00663be299555c (patch) | |
| tree | de0523f7f6d13e57e58c26d8b03dabbdcb5790cb /routes | |
| parent | 81c3593f1eec7f94dc9cbc22edc3456fa57b95cb (diff) | |
| download | coinos-server-add3ad93ff37f1daaedab5788f00663be299555c.tar.gz coinos-server-add3ad93ff37f1daaedab5788f00663be299555c.zip | |
bolt12 zaps: pay offers, NWC pay/receive, per-user offers, NIP-44
- sendLightning fetches an invoice from lno1 offers (payer_note passthrough
for NIP-177 zap intents) and pays it through the existing bolt12 path
- NWC: new nwc#2 pay/receive methods (BIP-321 URIs, payer proofs stubbed
until CLN 26.06 createproof), nip44_v2 alongside nip04 with per-request
scheme detection, dual 23196/23197 notifications, 13194 refreshed when
its content or encryption tag goes stale
- standing per-user bolt12 offer via GET /offer; incoming bolt12 payments
record invreq_payer_note as memo
- parse/fetchinvoice routes handle offers; zap display parses kind 9736
receipts (amount tag + embedded intent) and drops a call to a sync()
helper that never existed
Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'routes')
| -rw-r--r-- | routes/nostr.ts | 69 | ||||
| -rw-r--r-- | routes/payments.ts | 40 |
2 files changed, 74 insertions, 35 deletions
diff --git a/routes/nostr.ts b/routes/nostr.ts index 0eb95ff6..4a7ce069 100644 --- a/routes/nostr.ts +++ b/routes/nostr.ts @@ -21,6 +21,36 @@ import { decode } from "nostr-tools/nip19"; import type { ProfilePointer } from "nostr-tools/nip19"; import { getZapEndpoint, makeZapRequest } from "nostr-tools/nip57"; +// Amount and payer of a zap receipt. NIP-57 kind 9735: amount comes from +// decoding the paid bolt11, payer from the embedded 9734 zap request in the +// description tag. NIP-177 kind 9736 (BOLT12 zaps): amount is an explicit +// msat tag, and the receipt is payer-published with the signed 9737 intent in +// the description tag. +const parseZap = async (ev) => { + const { kind, tags, pubkey } = ev; + const description = tags.find((t) => t[0] === "description")?.[1]; + + let payer; + try { + payer = JSON.parse(description).pubkey; + } catch (e) {} + payer ||= pubkey; + + let amount = 0; + if (kind === 9736) { + const msat = Number.parseInt(tags.find((t) => t[0] === "amount")?.[1]); + if (msat > 0) amount = Math.round(msat / 1000); + } else { + try { + const bolt11 = tags.find((t) => t[0] === "bolt11")?.[1]; + const { amount_msat } = await ln.decode(bolt11); + if (amount_msat) amount = Math.round(amount_msat / 1000); + } catch (e) {} + } + + return { amount, pubkey: payer }; +}; + export default { async event(req, res) { try { @@ -46,23 +76,14 @@ export default { ) .map(({ value }) => value.pubkey); - const zapEvents = await scan({ kinds: [9735], "#e": [id] }); + const zapEvents = await scan({ kinds: [9735, 9736], "#e": [id] }); const zaps = []; - for (const { tags } of zapEvents) { - const bolt11 = tags.find((t) => t[0] === "bolt11")?.[1]; - const description = tags.find((t) => t[0] === "description")?.[1]; - const { pubkey } = JSON.parse(description); - let amount = 0; - try { - const { amount_msat } = await ln.decode(bolt11); - if (amount_msat) amount = Math.round(amount_msat / 1000); - } catch (e) {} - - zaps.push({ amount, pubkey }); + for (const zapEvent of zapEvents) { + zaps.push(await parseZap(zapEvent)); } pubkeys.push(event.pubkey); - pubkeys.push(...zaps.map((z) => z.pubkey)); + pubkeys.push(...zaps.map((z) => z.pubkey).filter((p) => p)); pubkeys = [...new Set(pubkeys)]; const profiles = await scan({ kinds: [0], authors: pubkeys }); const found = profiles.map((p) => p.pubkey); @@ -167,24 +188,16 @@ export default { let { id } = req.params; if (id.startsWith("nevent")) id = decode(id).data.id; if (id.startsWith("note")) id = decode(id).data; - const filter = { kinds: [9735], "#e": [id] }; - let events = await scan(filter); - if (!events.length) { - await sync("wss://relay.primal.net", filter); - events = await scan(filter); - } + // q() scans the local strfry and falls back to primal (the old code + // called a `sync` helper that never existed and threw ReferenceError + // whenever the local relay had no receipts) + const filter = { kinds: [9735, 9736], "#e": [id] }; + const events = await q(filter); if (!events.length) return res.send([]); const zaps = []; - for (const { tags } of events) { - const bolt11 = tags.find((t) => t[0] === "bolt11")?.[1]; - const description = tags.find((t) => t[0] === "description")?.[1]; - const { pubkey } = JSON.parse(description); - let amount = 0; - try { - const { amount_msat } = await ln.decode(bolt11); - if (amount_msat) amount = Math.round(amount_msat / 1000); - } catch (e) {} + for (const zapEvent of events) { + const { amount, pubkey } = await parseZap(zapEvent); let user; if (pubkey) { diff --git a/routes/payments.ts b/routes/payments.ts index 9c62d2bb..4239a409 100644 --- a/routes/payments.ts +++ b/routes/payments.ts @@ -2,7 +2,7 @@ import config from "$config"; import api from "$lib/api"; import { requirePin } from "$lib/auth"; import { archive, db, g, gf, s, sa } from "$lib/db"; -import { generate } from "$lib/invoices"; +import { generate, getUserOffer } from "$lib/invoices"; import { replay } from "$lib/lightning"; import ln from "$lib/ln"; import { err, l, warn } from "$lib/logging"; @@ -219,21 +219,30 @@ export default { let amount_msat; let payee; - if (decoded.type.includes("bolt12")) { + if (decoded.type === "bolt12 offer") { + ({ offer_amount_msat: amount_msat } = decoded); + payee = decoded.offer_issuer_id || decoded.offer_node_id; + } else if (decoded.type.includes("bolt12")) { ({ invoice_amount_msat: amount_msat, invoice_node_id: payee } = decoded); } else ({ amount_msat, payee } = decoded); const node = nodes.find((n) => n.nodeid === payee); - const alias = node ? node.alias : payee.substr(0, 12); + const alias = node ? node.alias : (payee || "").substr(0, 12); - const amount = Math.round(amount_msat / 1000); + const amount = Math.round((amount_msat || 0) / 1000); let ourfee = Math.round(amount * config.fee[PaymentType.lightning]); const credit = await g(`credit:lightning:${user.id}`); const covered = Math.min(credit, ourfee) || 0; ourfee -= covered; - res.send({ alias, amount, ourfee }); + res.send({ + alias, + amount, + ourfee, + type: decoded.type, + description: decoded.offer_description || decoded.description, + }); } catch (e) { err("problem parsing", e.message); bail(res, e.message); @@ -716,9 +725,26 @@ export default { res.send(await ln.decode(bolt11)); }, + // The user's standing bolt12 offer (lno1...) — reusable receive code they + // can publish (e.g. in a nostr kind 10058 list for bolt12 zaps) + async offer(req, res) { + try { + res.send(await getUserOffer(req.user)); + } catch (e) { + bail(res, e.message); + } + }, + async fetchinvoice(req, res) { - const { amount, offer } = req.body; - res.send(await ln.fetchinvoice(offer, amount ? amount * 1000 : null)); + const { amount, offer, payer_note } = req.body; + res.send( + await ln.fetchinvoice({ + offer, + amount_msat: amount ? amount * 1000 : undefined, + payer_note, + timeout: 60, + }), + ); }, async auth(req, res) { |
