From d6cc4d541f580878353c7f3243624934a279e996 Mon Sep 17 00:00:00 2001 From: Dmitriy E Date: Thu, 16 Apr 2026 09:27:39 +0300 Subject: [PATCH] fix(nwc): stabilize relay subscription and wallet service info - Await initial relay subscription in subscribeNotifications before returning so callers do not miss early notifications. - Retry fetching kind 13194 wallet service info with backoff when the relay returns no event yet. Made-with: Cursor --- src/nwc/NWCClient.ts | 109 ++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 44 deletions(-) diff --git a/src/nwc/NWCClient.ts b/src/nwc/NWCClient.ts index cc171ffa..0fe27a4d 100644 --- a/src/nwc/NWCClient.ts +++ b/src/nwc/NWCClient.ts @@ -383,54 +383,63 @@ export class NWCClient { notifications: Nip47NotificationType[]; }> { await this._checkConnected(); - const events = await new Promise((resolve, reject) => { - const events: Event[] = []; - const sub = this.pool.subscribe( - this.relayUrls, - { - kinds: [13194], - limit: 1, - authors: [this.walletPubkey], - }, - { - eoseTimeout: 10000, - onevent: (event) => { - events.push(event); + const maxAttempts = 4; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const events = await new Promise((resolve) => { + const collected: Event[] = []; + const sub = this.pool.subscribe( + this.relayUrls, + { + kinds: [13194], + limit: 1, + authors: [this.walletPubkey], }, - oneose: () => { - sub.close(); - resolve(events); + { + eoseTimeout: 10000, + onevent: (event) => { + collected.push(event); + }, + oneose: () => { + sub.close(); + resolve(collected); + }, }, - }, - ); - }); + ); + }); - if (!events.length) { - throw new Error("no info event (kind 13194) returned from relay"); - } - const content = events[0].content; - const notificationsTag = events[0].tags.find( - (t) => t[0] === "notifications", - ); - // TODO: Remove version tag after 01-06-2025 - const versionsTag = events[0].tags.find((t) => t[0] === "v"); - const encryptionTag = events[0].tags.find((t) => t[0] === "encryption"); - - let encryptions: string[] = ["nip04" satisfies Nip47EncryptionType]; - // TODO: Remove version tag after 01-06-2025 - if (versionsTag && versionsTag[1].includes("1.0")) { - encryptions.push("nip44_v2" satisfies Nip47EncryptionType); - } - if (encryptionTag) { - encryptions = encryptionTag[1].split(" ") as Nip47EncryptionType[]; + if (!events.length) { + if (attempt < maxAttempts - 1) { + await new Promise((r) => setTimeout(r, 750 * (attempt + 1))); + } + continue; + } + + const content = events[0].content; + const notificationsTag = events[0].tags.find( + (t) => t[0] === "notifications", + ); + // TODO: Remove version tag after 01-06-2025 + const versionsTag = events[0].tags.find((t) => t[0] === "v"); + const encryptionTag = events[0].tags.find((t) => t[0] === "encryption"); + + let encryptions: string[] = ["nip04" satisfies Nip47EncryptionType]; + // TODO: Remove version tag after 01-06-2025 + if (versionsTag && versionsTag[1].includes("1.0")) { + encryptions.push("nip44_v2" satisfies Nip47EncryptionType); + } + if (encryptionTag) { + encryptions = encryptionTag[1].split(" ") as Nip47EncryptionType[]; + } + return { + encryptions, + // delimiter is " " per spec, but Alby NWC originally returned "," + capabilities: content.split(/[ |,]/g) as Nip47Method[], + notifications: (notificationsTag?.[1]?.split(" ") || + []) as Nip47NotificationType[], + }; } - return { - encryptions, - // delimiter is " " per spec, but Alby NWC originally returned "," - capabilities: content.split(/[ |,]/g) as Nip47Method[], - notifications: (notificationsTag?.[1]?.split(" ") || - []) as Nip47NotificationType[], - }; + + throw new Error("no info event (kind 13194) returned from relay"); } async getInfo(): Promise { @@ -713,7 +722,13 @@ export class NWCClient { let subscribed = true; let endPromise: (() => void) | undefined; let sub: SubCloser | undefined; + let pendingInitial: { resolve: () => void } | undefined; + const initialSubscriptionReady = new Promise((resolve) => { + pendingInitial = { resolve }; + }); (async () => { + // `subscribed` is cleared by the unsubscribe callback returned to callers. + // eslint-disable-next-line no-unmodified-loop-condition while (subscribed) { try { await this._checkConnected(); @@ -771,6 +786,10 @@ export class NWCClient { }, ); console.info("subscribed to relays"); + if (pendingInitial) { + pendingInitial.resolve(); + pendingInitial = undefined; + } await new Promise((resolve) => { endPromise = () => { @@ -792,6 +811,8 @@ export class NWCClient { } })(); + await initialSubscriptionReady; + return () => { subscribed = false; endPromise?.();