aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-14 12:40:33 -0700
committerAdam Soltys <[email protected]>2026-06-17 07:47:49 -0700
commitceb003afb154560bb68b4252759fedc1fee75a0f (patch)
tree48ca17fe2d8295842e257eab03a3b4c37ecfca02
parent0987582546363935c5a388a9accb9b245c11a7c7 (diff)
downloadcoinos-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]>
-rw-r--r--lib/nwc.ts91
1 files changed, 65 insertions, 26 deletions
diff --git a/lib/nwc.ts b/lib/nwc.ts
index 49e2cfc2..b20d800b 100644
--- a/lib/nwc.ts
+++ b/lib/nwc.ts
@@ -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;