fix(nwc): prevent pay_invoice timeout when relay is in reconnect backoff

The root cause was that WalletConnect.pool was set directly, which caused
the library to use pool.subscription() and pool.publish() internally.
Both methods call pool.group(relays) with ignoreOffline=true by default,
which silently filters out relays whose ready state is false (i.e. relays
in reconnect backoff after a momentary disconnection).

This meant that when a user called payInvoice while the NWC relay was
briefly reconnecting:
1. The request event was "published" to zero relays (silently dropped)
2. The response subscription was also on zero relays
3. The 30-second timeout would fire with no recovery possible

The fix provides custom subscriptionMethod and publishMethod that use
pool.group(relays, false) — ignoreOffline=false — which passes relays
through to the underlying Relay instance. The Relay class has a proper
waitForReady() mechanism that queues operations until the relay
reconnects, rather than silently dropping them.

Also uses WalletConnect.fromJSON() for wallet restore (idiomatic
applesauce API) and removes the now-unused custom hexToBytes helper.

https://claude.ai/code/session_01LNdzz2qi4hvjCzKBjTK5Gy
This commit is contained in:
Claude
2026-02-12 22:49:52 +00:00
parent 62ce435043
commit dd32dc4937

View File

@@ -20,8 +20,20 @@ import {
import pool from "./relay-pool";
import { BehaviorSubject, Subscription, firstValueFrom, timeout } from "rxjs";
// Configure the pool for wallet connect
WalletConnect.pool = pool;
// Configure wallet connect with custom methods that don't filter offline relays.
//
// By default, pool.subscription() and pool.publish() call pool.group(relays)
// with ignoreOffline=true, which silently drops relays in reconnect backoff.
// This causes NWC requests to be published to ZERO relays if the NWC relay
// has a momentary disconnection, making pay_invoice appear to time out.
//
// Using ignoreOffline=false ensures the underlying Relay.waitForReady() is
// used instead, which queues operations until the relay reconnects rather
// than silently dropping them.
WalletConnect.subscriptionMethod = (relays, filters) =>
pool.group(relays, false).subscription(filters);
WalletConnect.publishMethod = (relays, event) =>
pool.group(relays, false).publish(event);
// Internal state
let notificationSubscription: Subscription | null = null;
@@ -63,14 +75,6 @@ export const transactionsState$ = new BehaviorSubject<TransactionsState>(
// Internal helpers
// ============================================================================
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}
/**
* Subscribe to wallet notifications with automatic retry on error.
* Notifications trigger balance refresh for real-time updates.
@@ -169,10 +173,10 @@ export async function restoreWallet(
connectionStatus$.next("connecting");
lastError$.next(null);
const wallet = new WalletConnect({
const wallet = WalletConnect.fromJSON({
service: connection.service,
relays: connection.relays,
secret: hexToBytes(connection.secret),
secret: connection.secret,
});
wallet$.next(wallet);