refactor: simplify NWC wallet by removing ensureWalletReady wrapper

Following the applesauce-wallet-connect example pattern:
- The use$ subscription to wallet.support$ in useWallet hook keeps
  the observable alive, preventing ReplaySubject reset
- Wallet methods now call directly without wrapper
- Keep persistent subscription in waitForSupport for service-layer
  operations before React hooks mount

This simplifies the code while maintaining the fix for the 60-second
ReplaySubject reset issue.

https://claude.ai/code/session_018fU3rYmjFPEKz3ot1itLZL
This commit is contained in:
Claude
2026-01-30 10:30:21 +00:00
parent aa32bb535b
commit 305b69fb7e
2 changed files with 25 additions and 56 deletions

View File

@@ -4,19 +4,20 @@
* Provides reactive access to the NWC wallet throughout the application.
* All state is derived from observables - no manual synchronization needed.
*
* IMPORTANT: The use$ subscription to wallet.support$ keeps the observable alive,
* preventing the library's ReplaySubject from resetting after 60 seconds.
* Always check that `support` is defined before calling wallet methods.
*
* @example
* ```tsx
* function MyComponent() {
* const { wallet, balance, connectionStatus, walletMethods, payInvoice } = useWallet();
* const { wallet, support, balance, payInvoice } = useWallet();
*
* if (connectionStatus === 'error') {
* return <ErrorState onRetry={reconnect} />;
* }
*
* // walletMethods combines support$ with cached info for reliability
* if (walletMethods.includes('pay_invoice')) {
* return <PayButton onClick={() => payInvoice("lnbc...")} />;
* }
* // Always check support before operations
* const handlePay = async () => {
* if (!wallet || !support) return;
* await payInvoice("lnbc...");
* };
*
* return <div>Balance: {formatSats(balance)}</div>;
* }
@@ -40,7 +41,6 @@ import {
loadTransactions as loadTransactionsService,
loadMoreTransactions as loadMoreTransactionsService,
retryLoadTransactions as retryLoadTransactionsService,
ensureWalletReady,
} from "@/services/nwc";
export function useWallet() {
@@ -93,17 +93,11 @@ export function useWallet() {
/**
* Pay a Lightning invoice via NWC.
*
* IMPORTANT: This first calls ensureWalletReady() to ensure the wallet's
* support$ observable has emitted. Without this, wallet.payInvoice() can
* hang forever because the applesauce-wallet-connect library's genericCall
* waits for encryption$ (derived from support$) without any timeout.
* The use$ subscription to support$ keeps it alive, so encryption$ will work.
*/
async function payInvoice(invoice: string, amount?: number) {
// Ensure wallet is ready - this waits for support$ with a timeout
// instead of letting it hang forever
const readyWallet = await ensureWalletReady();
const result = await readyWallet.payInvoice(invoice, amount);
if (!wallet) throw new Error("No wallet connected");
const result = await wallet.payInvoice(invoice, amount);
await refreshBalanceService();
return result;
}
@@ -116,18 +110,18 @@ export function useWallet() {
expiry?: number;
},
) {
const readyWallet = await ensureWalletReady();
return await readyWallet.makeInvoice(amount, options);
if (!wallet) throw new Error("No wallet connected");
return await wallet.makeInvoice(amount, options);
}
async function getInfo() {
const readyWallet = await ensureWalletReady();
return await readyWallet.getInfo();
if (!wallet) throw new Error("No wallet connected");
return await wallet.getInfo();
}
async function getBalance() {
const readyWallet = await ensureWalletReady();
const result = await readyWallet.getBalance();
if (!wallet) throw new Error("No wallet connected");
const result = await wallet.getBalance();
return result.balance;
}
@@ -139,18 +133,18 @@ export function useWallet() {
unpaid?: boolean;
type?: "incoming" | "outgoing";
}) {
const readyWallet = await ensureWalletReady();
return await readyWallet.listTransactions(options);
if (!wallet) throw new Error("No wallet connected");
return await wallet.listTransactions(options);
}
async function lookupInvoice(paymentHash: string) {
const readyWallet = await ensureWalletReady();
return await readyWallet.lookupInvoice(paymentHash);
if (!wallet) throw new Error("No wallet connected");
return await wallet.lookupInvoice(paymentHash);
}
async function payKeysend(pubkey: string, amount: number, preimage?: string) {
const readyWallet = await ensureWalletReady();
const result = await readyWallet.payKeysend(pubkey, amount, preimage);
if (!wallet) throw new Error("No wallet connected");
const result = await wallet.payKeysend(pubkey, amount, preimage);
await refreshBalanceService();
return result;
}

View File

@@ -223,31 +223,6 @@ async function waitForSupport(
}
}
/**
* Ensures the wallet is ready before performing operations.
* This must be called before any wallet method that uses genericCall internally.
*
* If the wallet is already connected with an active support subscription, returns immediately.
* Otherwise, waits for support$ to emit with a timeout.
*
* @throws Error if wallet not connected or not ready
*/
export async function ensureWalletReady(): Promise<WalletConnect> {
const wallet = wallet$.value;
if (!wallet) {
throw new Error("No wallet connected");
}
// If we already have an active support subscription, wallet is ready
if (supportSubscription && !supportSubscription.closed) {
return wallet;
}
// Otherwise, wait for support to be available
await waitForSupport(wallet);
return wallet;
}
// ============================================================================
// Public API
// ============================================================================