mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 00:17:02 +02:00
* fix(nwc): improve connection reliability and add health tracking - Add connection status observable (disconnected/connecting/connected/error) - Validate wallet connection on restore using support$ observable with 10s timeout - Add notification subscription error recovery with exponential backoff (5 retries) - Add retry logic for balance refresh (3 retries with backoff) - Use library's support$ observable for wallet capabilities (cached by applesauce) - Replace manual getInfo() calls with reactive support$ subscription - Add visual connection status indicator in WalletViewer header - Add reconnect button when connection is in error state - Store network info in cached connection for display https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * feat(wallet): add copy NWC connection string button to header Adds a copy button (Copy/Check icons) in the wallet header before the refresh button that copies the NWC connection string to clipboard for easy sharing or backup. https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * fix(wallet): use CopyCheck icon and fix transaction loading - Change Check to CopyCheck icon for copy NWC string button - Add walletMethods computed value that combines support$ observable with cached info fallback from initial connection - Fix transaction history not loading because support$ waits for kind 13194 events which many NWC wallets don't publish - The cached info from getInfo() RPC call is now used as fallback https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(nwc): simplify with derived state from observables Production-ready refactor of NWC implementation: nwc.ts: - Add wallet$ observable for reactive wallet instance access - Remove redundant subscribeToSupport() - only needed for validation - Cleaner code organization with clear sections useWallet.ts: - All state derived from observables (no useState for wallet) - Move walletMethods computation to hook (reusable) - isConnected derived from connectionStatus - Simplified from 240 to 170 lines WalletViewer.tsx: - Use walletMethods from hook instead of local useMemo - Simpler connection state tracking via connectionStatus - Remove redundant wallet variable from destructuring - No color change on copy NWC string (per feedback) https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(wallet): use useCopy hook for clipboard operations Replace manual clipboard state management with useCopy hook: - copyInvoice/invoiceCopied for generated invoice - copyRawTx/rawTxCopied for transaction JSON - copyNwc/nwcCopied for NWC connection string Benefits: - Cleaner code (removed manual setTimeout calls) - Automatic timeout cleanup on unmount - Consistent copy behavior across all clipboard operations https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * feat(wallet): implement lazy-loaded transactions observable - Add shared wallet types (Transaction, TransactionsState) in src/types/wallet.ts - Add transactionsState$ observable to NWC service for shared tx state - Implement loadTransactions, loadMoreTransactions, and retryLoadTransactions - Auto-refresh transactions on payment notifications - Simplify WalletViewer to use observable state instead of local state - Remove manual transaction loading logic from component https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * fix(wallet): update balance observable on initial connect - Call refreshBalance() in createWalletFromURI to fetch initial balance - Update balance$ directly when ConnectWalletDialog gets balance - Fixes issue where WalletViewer showed "-" after connecting while user menu showed correct balance (different data sources) https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(wallet): use single data source for balance across UI Remove fallback to nwcConnection.balance in user-menu - now both WalletViewer and user-menu use balance$ observable as the single source of truth for wallet balance. https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * fix(wallet): address code review issues and simplify user menu - Fix memory leak: track retry timeout and clear on disconnect - Add explicit WalletSupport type for support observable - Add comments explaining balance refresh error handling behavior - Add comment about restoreWallet not being awaited intentionally - User menu now uses connectionStatus observable (shows connecting/error states) - Remove wallet name display from user menu (simplifies UI) - Remove unused walletServiceProfile hook and getWalletName function https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ * refactor(wallet): extract WalletConnectionStatus component - Create reusable WalletConnectionStatus component for connection indicator - Remove rounded borders from indicator (now square) - Export getConnectionStatusColor helper for custom usage - Use component in both user-menu and WalletViewer - Supports size (sm/md), showLabel, and className props https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ --------- Co-authored-by: Claude <noreply@anthropic.com>
198 lines
5.4 KiB
TypeScript
198 lines
5.4 KiB
TypeScript
/**
|
|
* useWallet Hook
|
|
*
|
|
* Provides reactive access to the NWC wallet throughout the application.
|
|
* All state is derived from observables - no manual synchronization needed.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* function MyComponent() {
|
|
* const { wallet, balance, connectionStatus, walletMethods, 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...")} />;
|
|
* }
|
|
*
|
|
* return <div>Balance: {formatSats(balance)}</div>;
|
|
* }
|
|
* ```
|
|
*/
|
|
|
|
import { useEffect, useMemo, useRef } from "react";
|
|
import { use$ } from "applesauce-react/hooks";
|
|
import { useGrimoire } from "@/core/state";
|
|
import type { WalletSupport } from "applesauce-wallet-connect/helpers";
|
|
import {
|
|
wallet$,
|
|
restoreWallet,
|
|
clearWallet,
|
|
refreshBalance as refreshBalanceService,
|
|
reconnect as reconnectService,
|
|
balance$,
|
|
connectionStatus$,
|
|
lastError$,
|
|
transactionsState$,
|
|
loadTransactions as loadTransactionsService,
|
|
loadMoreTransactions as loadMoreTransactionsService,
|
|
retryLoadTransactions as retryLoadTransactionsService,
|
|
} from "@/services/nwc";
|
|
|
|
export function useWallet() {
|
|
const { state } = useGrimoire();
|
|
const nwcConnection = state.nwcConnection;
|
|
const restoreAttemptedRef = useRef(false);
|
|
|
|
// All state derived from observables
|
|
const wallet = use$(wallet$);
|
|
const balance = use$(balance$);
|
|
const connectionStatus = use$(connectionStatus$);
|
|
const lastError = use$(lastError$);
|
|
const transactionsState = use$(transactionsState$);
|
|
|
|
// Wallet support from library's support$ observable (cached by library for 60s)
|
|
const support: WalletSupport | null | undefined = use$(
|
|
() => wallet?.support$,
|
|
[wallet],
|
|
);
|
|
|
|
// Wallet methods - combines reactive support$ with cached info fallback
|
|
// The support$ waits for kind 13194 events which some wallets don't publish
|
|
const walletMethods = useMemo(() => {
|
|
return support?.methods ?? state.nwcConnection?.info?.methods ?? [];
|
|
}, [support?.methods, state.nwcConnection?.info?.methods]);
|
|
|
|
// Restore wallet on mount if connection exists
|
|
// Note: Not awaited intentionally - wallet is available synchronously from wallet$
|
|
// before validation completes. Any async errors are handled within restoreWallet.
|
|
useEffect(() => {
|
|
if (nwcConnection && !wallet && !restoreAttemptedRef.current) {
|
|
restoreAttemptedRef.current = true;
|
|
restoreWallet(nwcConnection);
|
|
}
|
|
}, [nwcConnection, wallet]);
|
|
|
|
// Reset restore flag when connection is cleared
|
|
useEffect(() => {
|
|
if (!nwcConnection) {
|
|
restoreAttemptedRef.current = false;
|
|
}
|
|
}, [nwcConnection]);
|
|
|
|
// Derived state
|
|
const isConnected = connectionStatus !== "disconnected";
|
|
|
|
// ============================================================================
|
|
// Wallet operations
|
|
// ============================================================================
|
|
|
|
async function payInvoice(invoice: string, amount?: number) {
|
|
if (!wallet) throw new Error("No wallet connected");
|
|
const result = await wallet.payInvoice(invoice, amount);
|
|
await refreshBalanceService();
|
|
return result;
|
|
}
|
|
|
|
async function makeInvoice(
|
|
amount: number,
|
|
options?: {
|
|
description?: string;
|
|
description_hash?: string;
|
|
expiry?: number;
|
|
},
|
|
) {
|
|
if (!wallet) throw new Error("No wallet connected");
|
|
return await wallet.makeInvoice(amount, options);
|
|
}
|
|
|
|
async function getInfo() {
|
|
if (!wallet) throw new Error("No wallet connected");
|
|
return await wallet.getInfo();
|
|
}
|
|
|
|
async function getBalance() {
|
|
if (!wallet) throw new Error("No wallet connected");
|
|
const result = await wallet.getBalance();
|
|
return result.balance;
|
|
}
|
|
|
|
async function listTransactions(options?: {
|
|
from?: number;
|
|
until?: number;
|
|
limit?: number;
|
|
offset?: number;
|
|
unpaid?: boolean;
|
|
type?: "incoming" | "outgoing";
|
|
}) {
|
|
if (!wallet) throw new Error("No wallet connected");
|
|
return await wallet.listTransactions(options);
|
|
}
|
|
|
|
async function lookupInvoice(paymentHash: string) {
|
|
if (!wallet) throw new Error("No wallet connected");
|
|
return await wallet.lookupInvoice(paymentHash);
|
|
}
|
|
|
|
async function payKeysend(pubkey: string, amount: number, preimage?: string) {
|
|
if (!wallet) throw new Error("No wallet connected");
|
|
const result = await wallet.payKeysend(pubkey, amount, preimage);
|
|
await refreshBalanceService();
|
|
return result;
|
|
}
|
|
|
|
function disconnect() {
|
|
clearWallet();
|
|
}
|
|
|
|
async function reconnect() {
|
|
await reconnectService();
|
|
}
|
|
|
|
async function refreshBalance() {
|
|
return await refreshBalanceService();
|
|
}
|
|
|
|
async function loadTransactions() {
|
|
await loadTransactionsService();
|
|
}
|
|
|
|
async function loadMoreTransactions() {
|
|
await loadMoreTransactionsService();
|
|
}
|
|
|
|
async function retryLoadTransactions() {
|
|
await retryLoadTransactionsService();
|
|
}
|
|
|
|
return {
|
|
// State (all derived from observables)
|
|
wallet,
|
|
balance,
|
|
isConnected,
|
|
connectionStatus,
|
|
lastError,
|
|
support,
|
|
walletMethods,
|
|
transactionsState,
|
|
|
|
// Operations
|
|
payInvoice,
|
|
makeInvoice,
|
|
getInfo,
|
|
getBalance,
|
|
refreshBalance,
|
|
listTransactions,
|
|
lookupInvoice,
|
|
payKeysend,
|
|
disconnect,
|
|
reconnect,
|
|
loadTransactions,
|
|
loadMoreTransactions,
|
|
retryLoadTransactions,
|
|
};
|
|
}
|