diff options
| author | Adam Soltys <[email protected]> | 2026-06-02 21:23:27 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-02 21:23:27 -0700 |
| commit | 6372d9185021aeb379e8c67386ba72f7b0aa4bdd (patch) | |
| tree | 078e03c96c76b31a9465d2ae043081853a86cc6b | |
| parent | a664af99dfee8267a960fd22cbab852681198de2 (diff) | |
| download | coinos-server-6372d9185021aeb379e8c67386ba72f7b0aa4bdd.tar.gz coinos-server-6372d9185021aeb379e8c67386ba72f7b0aa4bdd.zip | |
nwc: publish 13194 info event only when missing, recheck hourly
NWC clients (Alby Go, Lightning Piggies) fetch the kind 13194 info event
(capability advertisement) before they'll talk to the wallet; without it
they report "no info event" and show an unknown balance. The events went
missing after strfry restarted on 2026-05-29 and stayed gone for days,
because the app only published them once in the relay's on-open handler
and nothing re-asserted them.
13194 is a replaceable event meant to persist, so instead of blindly
republishing, query the relay on connect and publish only the server
pubkeys whose info event is actually missing (ensureInfo). Re-check
hourly so a relay restart that leaves our socket intact also self-heals.
Interval is cleared on close/reconnect alongside the heartbeat to avoid
leaks.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
| -rw-r--r-- | lib/nwc.ts | 91 |
1 files changed, 65 insertions, 26 deletions
@@ -50,9 +50,51 @@ const nwcRequestTimes: Map<string, number[]> = new Map(); export default () => { let r: any; let heartbeatInterval: 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 (heartbeatInterval) clearInterval(heartbeatInterval); + if (infoCheckInterval) clearInterval(infoCheckInterval); r = new Relay("ws://sf:7777", { reconnect: false }); r.on("open", async (_) => { @@ -74,38 +116,31 @@ export default () => { } }, 30000); 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 (heartbeatInterval) clearInterval(heartbeatInterval); + if (infoCheckInterval) clearInterval(infoCheckInterval); setTimeout(connect, 5000); }); @@ -113,6 +148,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; |
