aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-02 21:23:27 -0700
committerAdam Soltys <[email protected]>2026-06-02 21:23:27 -0700
commit6372d9185021aeb379e8c67386ba72f7b0aa4bdd (patch)
tree078e03c96c76b31a9465d2ae043081853a86cc6b
parenta664af99dfee8267a960fd22cbab852681198de2 (diff)
downloadcoinos-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.ts91
1 files changed, 65 insertions, 26 deletions
diff --git a/lib/nwc.ts b/lib/nwc.ts
index ad29f5dc..5677fea3 100644
--- a/lib/nwc.ts
+++ b/lib/nwc.ts
@@ -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;