diff options
| author | Adam Soltys <[email protected]> | 2026-06-14 12:40:33 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-17 07:47:49 -0700 |
| commit | ceb003afb154560bb68b4252759fedc1fee75a0f (patch) | |
| tree | 48ca17fe2d8295842e257eab03a3b4c37ecfca02 /lib | |
| parent | 0987582546363935c5a388a9accb9b245c11a7c7 (diff) | |
| download | coinos-server-ceb003afb154560bb68b4252759fedc1fee75a0f.tar.gz coinos-server-ceb003afb154560bb68b4252759fedc1fee75a0f.zip | |
nwc: publish 13194 info event only when missing, recheck hourly
Port of master 6372d918, adapted to staging's nwc relay loop (no
heartbeat here). NWC clients fetch the kind 13194 info event before they
talk to the wallet; without it they show "no info event" and an unknown
balance. 13194 is replaceable and meant to persist, so instead of blindly
republishing on every connect, query the relay (ensureInfo) and publish
only the server pubkeys whose event is actually missing. Re-check hourly
so a relay restart that leaves our socket intact self-heals; interval is
cleared on close/reconnect. Verified [email protected] Relay exposes ws/
readyState, on("eose") and unsubscribe.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/nwc.ts | 91 |
1 files changed, 65 insertions, 26 deletions
@@ -44,44 +44,79 @@ const nwcRequestTimes: Map<string, number[]> = new Map(); export default () => { let r: any; + let infoCheckInterval: any; + + // Tracks which server pubkeys already have their kind 13194 info event on the + // relay, populated by the existence query in ensureInfo() below. + const infoSeen = new Set<string>(); + + function publishInfo(pk: string, sk: string) { + const info = finalizeEvent( + { + created_at: Math.floor(Date.now() / 1000), + kind: 13194, + tags: [ + ["p", pk], + ["notifications", "payment_received payment_sent"], + ], + content: methods.join(" "), + }, + hexToBytes(sk), + ); + r.send(["EVENT", info]); + l("nwc published 13194 info event", pk); + } + + // The kind 13194 info event (NWC capability advertisement) is REPLACEABLE, so + // it's meant to be published once and persist on the relay. NWC clients (Alby + // Go, Lightning Piggies, etc.) fetch it before they'll talk to the wallet — + // without it they report "no info event" and show an unknown balance. + // + // Rather than blindly republishing, query the relay on connect and publish + // only the pubkeys whose info event is missing. This is normally a no-op, but + // self-heals the case where the event went missing (e.g. the strfry restart on + // 2026-05-29 that left clients broken for days). The "infocheck" subscription + // is handled by the event/eose branches below. + function ensureInfo() { + if (!r?.ws || r.ws.readyState !== 1) return; + infoSeen.clear(); + r.subscribe("infocheck", { + kinds: [13194], + authors: [serverPubkey, serverPubkey2], + }); + } function connect() { + if (infoCheckInterval) clearInterval(infoCheckInterval); r = new Relay("ws://sf:7777", { reconnect: false }); r.on("open", async (_) => { l("nwc connected to strfry"); r.subscribe("nwc", { kinds: [23194], "#p": [serverPubkey, serverPubkey2], since: Math.floor(Date.now() / 1000) - nwcEventMaxAgeSeconds }); - const info = await finalizeEvent( - { - created_at: Math.floor(Date.now() / 1000), - kind: 13194, - tags: [ - ["p", serverPubkey], - ["notifications", "payment_received payment_sent"], - ], - content: methods.join(" "), - }, - hexToBytes(serverSecret), - ); - r.send(["EVENT", info]); - const info2 = await finalizeEvent( - { - created_at: Math.floor(Date.now() / 1000), - kind: 13194, - tags: [ - ["p", serverPubkey2], - ["notifications", "payment_received payment_sent"], - ], - content: methods.join(" "), - }, - hexToBytes(serverSecret2), - ); - r.send(["EVENT", info2]); + // Verify the 13194 info events exist on the relay, publish any missing. + ensureInfo(); + + // Re-check hourly so a relay restart that leaves our socket intact (or any + // other cause of the info event going missing) self-heals without waiting + // for the next app reconnect. + infoCheckInterval = setInterval(ensureInfo, 60 * 60 * 1000); + }); + + r.on("eose", (sub) => { + if (sub !== "infocheck") return; + for (const [pk, sk] of [ + [serverPubkey, serverSecret], + [serverPubkey2, serverSecret2], + ]) { + if (!infoSeen.has(pk)) publishInfo(pk, sk); + } + try { r.unsubscribe("infocheck"); } catch (_) {} }); r.on("close", () => { warn("nwc strfry connection lost, reconnecting in 5s"); + if (infoCheckInterval) clearInterval(infoCheckInterval); setTimeout(connect, 5000); }); @@ -89,6 +124,10 @@ export default () => { r.on("event", async (sub, ev) => { try { + if (sub === "infocheck") { + if (ev.kind === 13194) infoSeen.add(ev.pubkey); + return; + } if (sub !== "nwc") return; const now = Math.floor(Date.now() / 1000); if (ev.created_at && now - ev.created_at > nwcEventMaxAgeSeconds) return; |
