fix(nwc): keep support$ subscribed to prevent encryption cache expiry

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
This commit is contained in:
Claude
2026-02-13 07:07:16 +00:00
parent dd32dc4937
commit 9c9734f8bc

View File

@@ -38,6 +38,7 @@ WalletConnect.publishMethod = (relays, event) =>
// Internal state
let notificationSubscription: Subscription | null = null;
let notificationRetryTimeout: ReturnType<typeof setTimeout> | null = null;
let supportSubscription: Subscription | null = null;
/**
* Connection status for the NWC wallet
@@ -75,6 +76,22 @@ export const transactionsState$ = new BehaviorSubject<TransactionsState>(
// 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<void> {
connectionStatus$.next("connecting");
lastError$.next(null);
subscribeToSupport(wallet);
subscribeToNotifications(wallet);
await refreshBalance();
}