aboutsummaryrefslogtreecommitdiffstats
path: root/names
diff options
context:
space:
mode:
Diffstat (limited to 'names')
-rw-r--r--names/server.js43
1 files changed, 39 insertions, 4 deletions
diff --git a/names/server.js b/names/server.js
index 3072c8d..4b606a1 100644
--- a/names/server.js
+++ b/names/server.js
@@ -305,13 +305,27 @@ const arkParamOf = (uri) => {
return m ? decodeURIComponent(m[1]) : null;
};
+// A float arkoor delivery is indistinguishable from any other ark receive on
+// the wallet's side (the mailbox protocol carries no memo), so remember each
+// LN settlement we forwarded that way: the wallet asks /lnreceipts and
+// matches by amount + delivery time to put the ⚡ on the right history rows.
+// Bounded per name; entries age out on the invoice-retention horizon. Only
+// float deliveries are recorded — a viaClink delivery runs the wallet's own
+// LN-receive flow, which tags itself.
+function recordLnReceipt(name, sat, hash) {
+ const all = state.lnReceipts ||= {};
+ const cutoff = Date.now() - 7 * 86400_000;
+ all[name] = [...(all[name] || []).filter((r) => r.ts > cutoff),
+ { sat, ts: Date.now(), ...(hash ? { hash } : {}) }].slice(-50);
+}
+
// Deliver a settled payment to the name's owner. Two ways, and we try the
// one that works while they're asleep first:
// 1. an arkoor send from the float — instant, free, needs no cooperation
// 2. ask their wallet for an invoice and pay it over Lightning — needs no
// float at all, but only works while something of theirs is listening
// Failing both, it queues and retries; nothing is ever dropped silently.
-async function forward(name, sat) {
+async function forward(name, sat, hash) {
const rec = state.names[name];
const dest = rec && arkParamOf(rec.uri);
const clink = clinkDest(rec);
@@ -320,6 +334,8 @@ async function forward(name, sat) {
const viaFloat = async () => {
if (!dest || !fwd || fwd.balance().spendableSat < sat) return false;
await fwd.send(dest, sat);
+ recordLnReceipt(name, sat, hash);
+ persist();
log(`forwarded ${sat} sat to ${name}`);
return true;
};
@@ -463,11 +479,11 @@ async function settleLoop() {
persist();
}
try {
- await forward(name, sat);
+ await forward(name, sat, inv.payment_hash);
} catch (e) {
log(`forward failed for ${name} (${sat} sat): ${e.message} — queued`);
state.pending = state.pending || [];
- state.pending.push({ name, sat, ts: Date.now() });
+ state.pending.push({ name, sat, hash: inv.payment_hash, ts: Date.now() });
persist();
}
}
@@ -579,7 +595,7 @@ setInterval(async () => {
const still = [];
for (const p of q) {
if (p.next && Date.now() < p.next) { still.push(p); continue; }
- try { await forward(p.name, p.sat); }
+ try { await forward(p.name, p.sat, p.hash); }
catch (e) {
log(`forward retry failed for ${p.name} (${p.sat} sat, try ${(p.tries || 0) + 1}): ${e.message}`);
p.tries = (p.tries || 0) + 1;
@@ -870,6 +886,25 @@ Bun.serve({
}
}
+ // Recent LN settlements forwarded to the caller's name(s) as float
+ // arkoors — the wallet matches them against its receive movements to
+ // mark which ones actually arrived over Lightning. NIP-98 authed and
+ // owner-only: this is a feed of incoming amounts.
+ if (url.pathname === '/lnreceipts' && req.method === 'GET') {
+ if (!rateOk(ip, 30)) return json({ error: 'rate limited' }, 429);
+ const a = await checkNip98(req, url, '');
+ if (a.error) return json({ error: a.error }, 401);
+ const receipts = [];
+ for (const [key, rec] of Object.entries(state.names)) {
+ if (rec.pubkey !== a.pubkey && rec.manager !== a.pubkey) continue;
+ for (const r of (state.lnReceipts || {})[key] || []) {
+ receipts.push({ name: key, sat: r.sat, ts: r.ts, hash: r.hash || null });
+ }
+ }
+ receipts.sort((x, y) => y.ts - x.ts);
+ return json({ receipts: receipts.slice(0, 100) });
+ }
+
// Prefix search over registered names — powers recipient search in the
// wallet's DMs. Public data (every name is a public DNS record).
if (url.pathname === '/search' && req.method === 'GET') {