From 9c9734f8bc174411f42ad2523c96cfcc758106e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 07:07:16 +0000 Subject: [PATCH] fix(nwc): keep support$ subscribed to prevent encryption cache expiry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The primary cause of pay_invoice timeouts was the library's support$ observable losing its ReplaySubject(1) cache after 60 seconds with zero subscribers. The support$ observable uses share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => timer(60000) }). When no UI component subscribes to wallet.support$ for 60s (e.g., user closes wallet window and browses the app), the cached kind 13194 wallet info is discarded. Since events$ is a hot shared observable that already received the info event from the relay (past EOSE), a fresh support$ subscription will never see a new info event. This causes genericCall's firstValueFrom(encryption$) to hang indefinitely — the defer() block never completes, so the request event is never created, never published, and the simpleTimeout on responses$ never even starts. The fix maintains a persistent subscription to wallet.support$ in the NWC service module for the lifetime of the wallet connection. This keeps the ReplaySubject(1) cache warm so encryption$ always resolves immediately when genericCall needs it. https://claude.ai/code/session_01LNdzz2qi4hvjCzKBjTK5Gy --- src/services/nwc.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/services/nwc.ts b/src/services/nwc.ts index 86d0e45..5aff3cf 100644 --- a/src/services/nwc.ts +++ b/src/services/nwc.ts @@ -38,6 +38,7 @@ WalletConnect.publishMethod = (relays, event) => // Internal state let notificationSubscription: Subscription | null = null; let notificationRetryTimeout: ReturnType | null = null; +let supportSubscription: Subscription | null = null; /** * Connection status for the NWC wallet @@ -75,6 +76,22 @@ export const transactionsState$ = new BehaviorSubject( // Internal helpers // ============================================================================ +/** + * Subscribe to wallet support info to keep the ReplaySubject(1) cache warm. + * + * The library's support$ uses share({ connector: () => new ReplaySubject(1), + * resetOnRefCountZero: () => timer(60000) }). If all subscribers drop for 60s, + * the cached wallet info (kind 13194) is lost. Since events$ is hot, the relay + * won't re-send the info event, so genericCall's firstValueFrom(encryption$) + * hangs indefinitely — causing every wallet operation to time out. + * + * This persistent subscription prevents the cache from expiring. + */ +function subscribeToSupport(wallet: WalletConnect) { + supportSubscription?.unsubscribe(); + supportSubscription = wallet.support$.subscribe(); +} + /** * Subscribe to wallet notifications with automatic retry on error. * Notifications trigger balance refresh for real-time updates. @@ -157,6 +174,7 @@ export function createWalletFromURI(connectionString: string): WalletConnect { const wallet = WalletConnect.fromConnectURI(connectionString); wallet$.next(wallet); + subscribeToSupport(wallet); subscribeToNotifications(wallet); refreshBalance(); // Fetch initial balance @@ -208,6 +226,7 @@ export async function restoreWallet( // Continue anyway - notifications will retry } + subscribeToSupport(wallet); subscribeToNotifications(wallet); refreshBalance(); @@ -218,7 +237,9 @@ export async function restoreWallet( * Disconnects and clears the wallet. */ export function clearWallet(): void { - // Clean up subscription and pending retry + // Clean up subscriptions and pending retry + supportSubscription?.unsubscribe(); + supportSubscription = null; notificationSubscription?.unsubscribe(); notificationSubscription = null; if (notificationRetryTimeout) { @@ -294,6 +315,7 @@ export async function reconnect(): Promise { connectionStatus$.next("connecting"); lastError$.next(null); + subscribeToSupport(wallet); subscribeToNotifications(wallet); await refreshBalance(); }