mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 16:37:06 +02:00
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
This commit is contained in:
@@ -396,12 +396,12 @@ export default function WalletViewer() {
|
||||
toggleWalletBalancesBlur,
|
||||
} = useGrimoire();
|
||||
const {
|
||||
wallet,
|
||||
balance,
|
||||
isConnected,
|
||||
connectionStatus,
|
||||
lastError,
|
||||
support, // Wallet capabilities from support$ observable (cached by library)
|
||||
support,
|
||||
walletMethods, // Combined support$ + cached info fallback
|
||||
refreshBalance,
|
||||
listTransactions,
|
||||
makeInvoice,
|
||||
@@ -420,8 +420,7 @@ export default function WalletViewer() {
|
||||
const [txLoadAttempted, setTxLoadAttempted] = useState(false);
|
||||
const [txLoadFailed, setTxLoadFailed] = useState(false);
|
||||
|
||||
// Use refs to track loading attempts without causing re-renders
|
||||
const lastConnectionStateRef = useRef(isConnected);
|
||||
// Rate limiting refs
|
||||
const lastBalanceRefreshRef = useRef(0);
|
||||
const lastTxLoadRef = useRef(0);
|
||||
|
||||
@@ -456,36 +455,27 @@ export default function WalletViewer() {
|
||||
// Copy NWC connection string state
|
||||
const [copiedNwc, setCopiedNwc] = useState(false);
|
||||
|
||||
// Wallet methods - use support$ observable if available, fallback to cached info
|
||||
// The support$ observable waits for kind 13194 events which some wallets don't publish
|
||||
// The cached info comes from getInfo() RPC call during initial connection
|
||||
const walletMethods = useMemo(() => {
|
||||
return support?.methods ?? state.nwcConnection?.info?.methods ?? [];
|
||||
}, [support?.methods, state.nwcConnection?.info?.methods]);
|
||||
|
||||
// Reset state when connection changes
|
||||
// Reset transaction state when wallet disconnects
|
||||
useEffect(() => {
|
||||
// Detect connection state changes
|
||||
if (isConnected !== lastConnectionStateRef.current) {
|
||||
lastConnectionStateRef.current = isConnected;
|
||||
|
||||
if (isConnected) {
|
||||
// Reset transaction loading flags when wallet connects
|
||||
setTxLoadAttempted(false);
|
||||
setTxLoadFailed(false);
|
||||
setTransactions([]);
|
||||
} else {
|
||||
// Clear all state when wallet disconnects
|
||||
setTxLoadAttempted(false);
|
||||
setTxLoadFailed(false);
|
||||
setTransactions([]);
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
setHasMore(true);
|
||||
}
|
||||
if (!isConnected) {
|
||||
setTxLoadAttempted(false);
|
||||
setTxLoadFailed(false);
|
||||
setTransactions([]);
|
||||
setLoading(false);
|
||||
setLoadingMore(false);
|
||||
setHasMore(true);
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
// Reset transaction load flag when wallet connects (to trigger reload)
|
||||
useEffect(() => {
|
||||
if (connectionStatus === "connected") {
|
||||
setTxLoadAttempted(false);
|
||||
setTxLoadFailed(false);
|
||||
setTransactions([]);
|
||||
}
|
||||
}, [connectionStatus]);
|
||||
|
||||
// Load transactions when wallet methods are available (only once)
|
||||
// walletMethods combines support$ observable with cached info fallback
|
||||
useEffect(() => {
|
||||
@@ -959,7 +949,7 @@ export default function WalletViewer() {
|
||||
return items;
|
||||
}, [transactions]);
|
||||
|
||||
if (!isConnected || !wallet) {
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-8">
|
||||
<Card className="max-w-md">
|
||||
@@ -1134,7 +1124,7 @@ export default function WalletViewer() {
|
||||
aria-label="Copy connection string"
|
||||
>
|
||||
{copiedNwc ? (
|
||||
<CopyCheck className="size-3 text-green-500" />
|
||||
<CopyCheck className="size-3" />
|
||||
) : (
|
||||
<Copy className="size-3" />
|
||||
)}
|
||||
|
||||
@@ -1,73 +1,67 @@
|
||||
/**
|
||||
* useWallet Hook
|
||||
*
|
||||
* Provides access to the NWC wallet throughout the application.
|
||||
* Fully reactive using observables - balance updates automatically via use$()
|
||||
* 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, support, payInvoice } = useWallet();
|
||||
* const { wallet, balance, connectionStatus, walletMethods, payInvoice } = useWallet();
|
||||
*
|
||||
* // Connection status: 'disconnected' | 'connecting' | 'connected' | 'error'
|
||||
* if (connectionStatus === 'error') {
|
||||
* return <div>Connection error - <button onClick={reconnect}>Retry</button></div>;
|
||||
* return <ErrorState onRetry={reconnect} />;
|
||||
* }
|
||||
*
|
||||
* // Wallet capabilities from support$ observable (cached by library)
|
||||
* const canPay = support?.methods?.includes('pay_invoice');
|
||||
*
|
||||
* async function handlePay() {
|
||||
* if (!wallet) return;
|
||||
* await payInvoice("lnbc...");
|
||||
* // Balance automatically updates via notifications!
|
||||
* // walletMethods combines support$ with cached info for reliability
|
||||
* if (walletMethods.includes('pay_invoice')) {
|
||||
* return <PayButton onClick={() => payInvoice("lnbc...")} />;
|
||||
* }
|
||||
*
|
||||
* return <div>Balance: {balance ? Math.floor(balance / 1000) : 0} sats</div>;
|
||||
* return <div>Balance: {formatSats(balance)}</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { use$ } from "applesauce-react/hooks";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import {
|
||||
getWallet,
|
||||
wallet$,
|
||||
restoreWallet,
|
||||
clearWallet as clearWalletService,
|
||||
clearWallet,
|
||||
refreshBalance as refreshBalanceService,
|
||||
reconnect as reconnectService,
|
||||
balance$,
|
||||
connectionStatus$,
|
||||
lastError$,
|
||||
} from "@/services/nwc";
|
||||
import type { WalletConnect } from "applesauce-wallet-connect";
|
||||
|
||||
export function useWallet() {
|
||||
const { state } = useGrimoire();
|
||||
const nwcConnection = state.nwcConnection;
|
||||
const [wallet, setWallet] = useState<WalletConnect | null>(getWallet());
|
||||
const restoreAttemptedRef = useRef(false);
|
||||
|
||||
// Subscribe to observables (fully reactive!)
|
||||
// All state derived from observables
|
||||
const wallet = use$(wallet$);
|
||||
const balance = use$(balance$);
|
||||
const connectionStatus = use$(connectionStatus$);
|
||||
const lastError = use$(lastError$);
|
||||
|
||||
// Subscribe to wallet support$ observable for cached capabilities
|
||||
// This replaces manual getInfo() calls
|
||||
// Wallet support from library's support$ observable (cached)
|
||||
const support = use$(() => wallet?.support$, [wallet]);
|
||||
|
||||
// Initialize wallet on mount if connection exists but no wallet instance
|
||||
// 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
|
||||
useEffect(() => {
|
||||
if (nwcConnection && !wallet && !restoreAttemptedRef.current) {
|
||||
restoreAttemptedRef.current = true;
|
||||
console.log("[useWallet] Restoring wallet from saved connection");
|
||||
|
||||
// restoreWallet is now async and validates the connection
|
||||
restoreWallet(nwcConnection).then((restoredWallet) => {
|
||||
setWallet(restoredWallet);
|
||||
});
|
||||
restoreWallet(nwcConnection);
|
||||
}
|
||||
}, [nwcConnection, wallet]);
|
||||
|
||||
@@ -78,33 +72,20 @@ export function useWallet() {
|
||||
}
|
||||
}, [nwcConnection]);
|
||||
|
||||
// Update local wallet ref when connection changes
|
||||
useEffect(() => {
|
||||
const currentWallet = getWallet();
|
||||
if (currentWallet !== wallet) {
|
||||
setWallet(currentWallet);
|
||||
}
|
||||
}, [nwcConnection, wallet]);
|
||||
// Derived state
|
||||
const isConnected = connectionStatus !== "disconnected";
|
||||
|
||||
// ============================================================================
|
||||
// Wallet operations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Pay a BOLT11 invoice
|
||||
* Balance will auto-update via notification subscription
|
||||
*/
|
||||
async function payInvoice(invoice: string, amount?: number) {
|
||||
if (!wallet) throw new Error("No wallet connected");
|
||||
|
||||
const result = await wallet.payInvoice(invoice, amount);
|
||||
|
||||
// Balance will update automatically via notifications
|
||||
// But we can also refresh immediately for instant feedback
|
||||
await refreshBalanceService();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new invoice
|
||||
*/
|
||||
async function makeInvoice(
|
||||
amount: number,
|
||||
options?: {
|
||||
@@ -114,40 +95,20 @@ export function useWallet() {
|
||||
},
|
||||
) {
|
||||
if (!wallet) throw new Error("No wallet connected");
|
||||
|
||||
return await wallet.makeInvoice(amount, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wallet info (capabilities, alias, etc.)
|
||||
*/
|
||||
async function getInfo() {
|
||||
if (!wallet) throw new Error("No wallet connected");
|
||||
|
||||
return await wallet.getInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current balance
|
||||
*/
|
||||
async function getBalance() {
|
||||
if (!wallet) throw new Error("No wallet connected");
|
||||
|
||||
const result = await wallet.getBalance();
|
||||
return result.balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually refresh the balance
|
||||
*/
|
||||
async function refreshBalance() {
|
||||
return await refreshBalanceService();
|
||||
}
|
||||
|
||||
/**
|
||||
* List recent transactions
|
||||
* @param options - Pagination and filter options
|
||||
*/
|
||||
async function listTransactions(options?: {
|
||||
from?: number;
|
||||
until?: number;
|
||||
@@ -157,84 +118,53 @@ export function useWallet() {
|
||||
type?: "incoming" | "outgoing";
|
||||
}) {
|
||||
if (!wallet) throw new Error("No wallet connected");
|
||||
|
||||
return await wallet.listTransactions(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up an invoice by payment hash
|
||||
* @param paymentHash - The payment hash to look up
|
||||
*/
|
||||
async function lookupInvoice(paymentHash: string) {
|
||||
if (!wallet) throw new Error("No wallet connected");
|
||||
|
||||
return await wallet.lookupInvoice(paymentHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pay to a node pubkey directly (keysend)
|
||||
* @param pubkey - The node pubkey to pay
|
||||
* @param amount - Amount in millisats
|
||||
* @param preimage - Optional preimage (hex string)
|
||||
*/
|
||||
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);
|
||||
|
||||
// Refresh balance after payment
|
||||
await refreshBalanceService();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the wallet
|
||||
*/
|
||||
function disconnect() {
|
||||
clearWalletService();
|
||||
setWallet(null);
|
||||
clearWallet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to reconnect the wallet after an error
|
||||
*/
|
||||
async function reconnect() {
|
||||
await reconnectService();
|
||||
}
|
||||
|
||||
async function refreshBalance() {
|
||||
return await refreshBalanceService();
|
||||
}
|
||||
|
||||
return {
|
||||
/** The wallet instance (null if not connected) */
|
||||
// State (all derived from observables)
|
||||
wallet,
|
||||
/** Current balance in millisats (auto-updates via observable!) */
|
||||
balance,
|
||||
/** Whether a wallet is connected */
|
||||
isConnected: !!wallet,
|
||||
/** Connection status: 'disconnected' | 'connecting' | 'connected' | 'error' */
|
||||
isConnected,
|
||||
connectionStatus,
|
||||
/** The last connection error (null if no error) */
|
||||
lastError,
|
||||
/** Wallet support info from support$ observable (methods, notifications, etc.) - cached by library */
|
||||
support,
|
||||
/** Pay a BOLT11 invoice */
|
||||
walletMethods,
|
||||
|
||||
// Operations
|
||||
payInvoice,
|
||||
/** Generate a new invoice */
|
||||
makeInvoice,
|
||||
/** Get wallet information - prefer using support instead */
|
||||
getInfo,
|
||||
/** Get current balance */
|
||||
getBalance,
|
||||
/** Manually refresh balance */
|
||||
refreshBalance,
|
||||
/** List recent transactions */
|
||||
listTransactions,
|
||||
/** Look up an invoice by payment hash */
|
||||
lookupInvoice,
|
||||
/** Pay to a node pubkey directly (keysend) */
|
||||
payKeysend,
|
||||
/** Disconnect wallet */
|
||||
disconnect,
|
||||
/** Attempt to reconnect after an error */
|
||||
reconnect,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
* Provides a singleton WalletConnect instance for the application using
|
||||
* applesauce-wallet-connect for NIP-47 Lightning wallet integration.
|
||||
*
|
||||
* Features:
|
||||
* - Maintains persistent wallet connection across app lifetime
|
||||
* - Subscribes to NIP-47 notifications (kind 23197) for balance updates
|
||||
* - Fully reactive using RxJS observables (no polling!)
|
||||
* - Components use use$() to reactively subscribe to balance changes
|
||||
* - Connection health tracking with automatic recovery
|
||||
* - Uses library's support$ observable for cached wallet capabilities
|
||||
* Architecture:
|
||||
* - All state is exposed via BehaviorSubject observables
|
||||
* - Components subscribe via use$() for automatic updates
|
||||
* - Notification subscription handles balance updates reactively
|
||||
* - Automatic retry with exponential backoff on failures
|
||||
*/
|
||||
|
||||
import { WalletConnect } from "applesauce-wallet-connect";
|
||||
@@ -18,19 +16,14 @@ import type { NWCConnection } from "@/types/app";
|
||||
import pool from "./relay-pool";
|
||||
import { BehaviorSubject, Subscription, firstValueFrom, timeout } from "rxjs";
|
||||
|
||||
// Set the pool for wallet connect to use
|
||||
// Configure the pool for wallet connect
|
||||
WalletConnect.pool = pool;
|
||||
|
||||
let walletInstance: WalletConnect | null = null;
|
||||
// Internal state
|
||||
let notificationSubscription: Subscription | null = null;
|
||||
let supportSubscription: Subscription | null = null;
|
||||
|
||||
/**
|
||||
* Connection status for the NWC wallet
|
||||
* - disconnected: No wallet connected
|
||||
* - connecting: Wallet is being restored/validated
|
||||
* - connected: Wallet is connected and responding
|
||||
* - error: Connection failed or lost
|
||||
*/
|
||||
export type NWCConnectionStatus =
|
||||
| "disconnected"
|
||||
@@ -38,29 +31,28 @@ export type NWCConnectionStatus =
|
||||
| "connected"
|
||||
| "error";
|
||||
|
||||
/**
|
||||
* Observable for connection status
|
||||
* Components can subscribe to this for real-time connection state using use$()
|
||||
*/
|
||||
// ============================================================================
|
||||
// Observables - All state is exposed reactively
|
||||
// ============================================================================
|
||||
|
||||
/** The current wallet instance (null if not connected) */
|
||||
export const wallet$ = new BehaviorSubject<WalletConnect | null>(null);
|
||||
|
||||
/** Connection status */
|
||||
export const connectionStatus$ = new BehaviorSubject<NWCConnectionStatus>(
|
||||
"disconnected",
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable for the last connection error
|
||||
* Components can use this to display error messages
|
||||
*/
|
||||
/** Last connection error (null if no error) */
|
||||
export const lastError$ = new BehaviorSubject<Error | null>(null);
|
||||
|
||||
/**
|
||||
* Observable for wallet balance updates
|
||||
* Components can subscribe to this for real-time balance changes using use$()
|
||||
*/
|
||||
/** Current balance in millisats */
|
||||
export const balance$ = new BehaviorSubject<number | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Helper to convert hex string to Uint8Array
|
||||
*/
|
||||
// ============================================================================
|
||||
// Internal helpers
|
||||
// ============================================================================
|
||||
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
@@ -70,50 +62,45 @@ function hexToBytes(hex: string): Uint8Array {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to wallet notifications with automatic retry on error
|
||||
* This enables real-time balance updates when transactions occur
|
||||
* Subscribe to wallet notifications with automatic retry on error.
|
||||
* Notifications trigger balance refresh for real-time updates.
|
||||
*/
|
||||
function subscribeToNotificationsWithRetry(wallet: WalletConnect) {
|
||||
function subscribeToNotifications(wallet: WalletConnect) {
|
||||
// Clean up existing subscription
|
||||
if (notificationSubscription) {
|
||||
notificationSubscription.unsubscribe();
|
||||
notificationSubscription = null;
|
||||
}
|
||||
notificationSubscription?.unsubscribe();
|
||||
notificationSubscription = null;
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 5;
|
||||
const baseDelay = 2000; // 2 seconds
|
||||
const baseDelay = 2000;
|
||||
|
||||
function subscribe() {
|
||||
console.log(
|
||||
`[NWC] Subscribing to wallet notifications (attempt ${retryCount + 1})`,
|
||||
);
|
||||
|
||||
notificationSubscription = wallet.notifications$.subscribe({
|
||||
next: (notification) => {
|
||||
console.log("[NWC] Notification received:", notification);
|
||||
retryCount = 0; // Reset retry count on success
|
||||
console.log(
|
||||
"[NWC] Notification received:",
|
||||
notification.notification_type,
|
||||
);
|
||||
retryCount = 0;
|
||||
|
||||
// Mark as connected if we were in error state
|
||||
// Recover from error state on successful notification
|
||||
if (connectionStatus$.value === "error") {
|
||||
connectionStatus$.next("connected");
|
||||
lastError$.next(null);
|
||||
}
|
||||
|
||||
// When we get a notification, refresh the balance
|
||||
// Refresh balance on any notification
|
||||
refreshBalance();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error("[NWC] Notification subscription error:", error);
|
||||
console.error("[NWC] Notification error:", error);
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
retryCount++;
|
||||
console.log(`[NWC] Retrying notification subscription in ${delay}ms`);
|
||||
connectionStatus$.next("connecting");
|
||||
setTimeout(subscribe, delay);
|
||||
} else {
|
||||
console.error("[NWC] Max notification retries reached");
|
||||
connectionStatus$.next("error");
|
||||
lastError$.next(
|
||||
error instanceof Error
|
||||
@@ -123,14 +110,10 @@ function subscribeToNotificationsWithRetry(wallet: WalletConnect) {
|
||||
}
|
||||
},
|
||||
complete: () => {
|
||||
console.log("[NWC] Notification subscription completed");
|
||||
// If subscription completes unexpectedly, try to reconnect
|
||||
if (walletInstance && retryCount < maxRetries) {
|
||||
// Reconnect if subscription completes unexpectedly
|
||||
if (wallet$.value && retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
retryCount++;
|
||||
console.log(
|
||||
`[NWC] Subscription completed, reconnecting in ${delay}ms`,
|
||||
);
|
||||
setTimeout(subscribe, delay);
|
||||
}
|
||||
},
|
||||
@@ -140,57 +123,29 @@ function subscribeToNotificationsWithRetry(wallet: WalletConnect) {
|
||||
subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the wallet's support$ observable for cached capabilities
|
||||
* This keeps connection alive and validates the wallet is responding
|
||||
*/
|
||||
function subscribeToSupport(wallet: WalletConnect) {
|
||||
// Clean up existing subscription
|
||||
if (supportSubscription) {
|
||||
supportSubscription.unsubscribe();
|
||||
supportSubscription = null;
|
||||
}
|
||||
|
||||
supportSubscription = wallet.support$.subscribe({
|
||||
next: (support) => {
|
||||
if (support) {
|
||||
console.log("[NWC] Wallet support info received:", support);
|
||||
// Mark as connected when we receive support info
|
||||
if (
|
||||
connectionStatus$.value === "connecting" ||
|
||||
connectionStatus$.value === "error"
|
||||
) {
|
||||
connectionStatus$.next("connected");
|
||||
lastError$.next(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
console.error("[NWC] Support subscription error:", error);
|
||||
// Don't set error state here as notifications subscription handles recovery
|
||||
},
|
||||
});
|
||||
}
|
||||
// ============================================================================
|
||||
// Public API
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Creates a new WalletConnect instance from a connection string
|
||||
* Automatically subscribes to notifications for balance updates
|
||||
* Creates a new wallet connection from a NWC URI.
|
||||
* Used when user connects a new wallet.
|
||||
*/
|
||||
export function createWalletFromURI(connectionString: string): WalletConnect {
|
||||
connectionStatus$.next("connecting");
|
||||
lastError$.next(null);
|
||||
|
||||
walletInstance = WalletConnect.fromConnectURI(connectionString);
|
||||
subscribeToSupport(walletInstance);
|
||||
subscribeToNotificationsWithRetry(walletInstance);
|
||||
const wallet = WalletConnect.fromConnectURI(connectionString);
|
||||
wallet$.next(wallet);
|
||||
|
||||
return walletInstance;
|
||||
subscribeToNotifications(wallet);
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a wallet from saved connection data
|
||||
* Used on app startup to reconnect to a previously connected wallet
|
||||
* Validates the connection using the support$ observable
|
||||
* Restores a wallet from saved connection data.
|
||||
* Validates the connection before marking as connected.
|
||||
*/
|
||||
export async function restoreWallet(
|
||||
connection: NWCConnection,
|
||||
@@ -198,123 +153,98 @@ export async function restoreWallet(
|
||||
connectionStatus$.next("connecting");
|
||||
lastError$.next(null);
|
||||
|
||||
walletInstance = new WalletConnect({
|
||||
const wallet = new WalletConnect({
|
||||
service: connection.service,
|
||||
relays: connection.relays,
|
||||
secret: hexToBytes(connection.secret),
|
||||
});
|
||||
|
||||
// Set initial balance from cache while we validate
|
||||
wallet$.next(wallet);
|
||||
|
||||
// Show cached balance immediately while validating
|
||||
if (connection.balance !== undefined) {
|
||||
balance$.next(connection.balance);
|
||||
}
|
||||
|
||||
// Subscribe to support$ for cached wallet capabilities
|
||||
subscribeToSupport(walletInstance);
|
||||
|
||||
// Validate connection by waiting for support info with timeout
|
||||
// Validate connection by waiting for support info
|
||||
try {
|
||||
console.log("[NWC] Validating wallet connection...");
|
||||
await firstValueFrom(
|
||||
walletInstance.support$.pipe(
|
||||
wallet.support$.pipe(
|
||||
timeout({
|
||||
first: 10000, // 10 second timeout for first value
|
||||
first: 10000,
|
||||
with: () => {
|
||||
throw new Error("Connection validation timeout");
|
||||
throw new Error("Connection timeout");
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
console.log("[NWC] Wallet connection validated");
|
||||
connectionStatus$.next("connected");
|
||||
} catch (error) {
|
||||
console.error("[NWC] Wallet validation failed:", error);
|
||||
console.error("[NWC] Validation failed:", error);
|
||||
connectionStatus$.next("error");
|
||||
lastError$.next(
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error("Connection validation failed"),
|
||||
error instanceof Error ? error : new Error("Connection failed"),
|
||||
);
|
||||
// Continue anyway - notifications subscription will retry
|
||||
// Continue anyway - notifications will retry
|
||||
}
|
||||
|
||||
// Subscribe to notifications with retry logic
|
||||
subscribeToNotificationsWithRetry(walletInstance);
|
||||
|
||||
// Refresh balance from wallet (not just cache)
|
||||
subscribeToNotifications(wallet);
|
||||
refreshBalance();
|
||||
|
||||
return walletInstance;
|
||||
return wallet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current wallet instance
|
||||
*/
|
||||
export function getWallet(): WalletConnect | null {
|
||||
return walletInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current wallet instance and stops notifications
|
||||
* Disconnects and clears the wallet.
|
||||
*/
|
||||
export function clearWallet(): void {
|
||||
if (notificationSubscription) {
|
||||
notificationSubscription.unsubscribe();
|
||||
notificationSubscription = null;
|
||||
}
|
||||
if (supportSubscription) {
|
||||
supportSubscription.unsubscribe();
|
||||
supportSubscription = null;
|
||||
}
|
||||
walletInstance = null;
|
||||
notificationSubscription?.unsubscribe();
|
||||
notificationSubscription = null;
|
||||
|
||||
wallet$.next(null);
|
||||
balance$.next(undefined);
|
||||
connectionStatus$.next("disconnected");
|
||||
lastError$.next(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually refresh the balance from the wallet
|
||||
* Useful for initial load or manual refresh button
|
||||
* Includes retry logic for reliability
|
||||
* Refreshes the balance from the wallet.
|
||||
* Includes retry logic for reliability.
|
||||
*/
|
||||
export async function refreshBalance(): Promise<number | undefined> {
|
||||
if (!walletInstance) return undefined;
|
||||
const wallet = wallet$.value;
|
||||
if (!wallet) return undefined;
|
||||
|
||||
const maxRetries = 3;
|
||||
const baseDelay = 1000;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
const result = await walletInstance.getBalance();
|
||||
const newBalance = result.balance;
|
||||
const result = await wallet.getBalance();
|
||||
balance$.next(result.balance);
|
||||
|
||||
balance$.next(newBalance);
|
||||
|
||||
// Mark as connected on successful balance fetch
|
||||
// Recover from error state on success
|
||||
if (connectionStatus$.value === "error") {
|
||||
connectionStatus$.next("connected");
|
||||
lastError$.next(null);
|
||||
}
|
||||
|
||||
return newBalance;
|
||||
return result.balance;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[NWC] Failed to refresh balance (attempt ${attempt + 1}):`,
|
||||
`[NWC] Balance refresh failed (attempt ${attempt + 1}):`,
|
||||
error,
|
||||
);
|
||||
|
||||
if (attempt < maxRetries - 1) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, baseDelay * Math.pow(2, attempt)),
|
||||
await new Promise((r) =>
|
||||
setTimeout(r, baseDelay * Math.pow(2, attempt)),
|
||||
);
|
||||
} else if (connectionStatus$.value !== "connected") {
|
||||
connectionStatus$.next("error");
|
||||
lastError$.next(
|
||||
error instanceof Error ? error : new Error("Failed to get balance"),
|
||||
);
|
||||
} else {
|
||||
// Only set error state on final failure if not already connected
|
||||
if (connectionStatus$.value !== "connected") {
|
||||
connectionStatus$.next("error");
|
||||
lastError$.next(
|
||||
error instanceof Error ? error : new Error("Failed to get balance"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,19 +253,15 @@ export async function refreshBalance(): Promise<number | undefined> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to reconnect the wallet
|
||||
* Call this when the user wants to manually retry after an error
|
||||
* Attempts to reconnect after an error.
|
||||
*/
|
||||
export async function reconnect(): Promise<void> {
|
||||
if (!walletInstance) return;
|
||||
const wallet = wallet$.value;
|
||||
if (!wallet) return;
|
||||
|
||||
connectionStatus$.next("connecting");
|
||||
lastError$.next(null);
|
||||
|
||||
// Re-subscribe to support and notifications
|
||||
subscribeToSupport(walletInstance);
|
||||
subscribeToNotificationsWithRetry(walletInstance);
|
||||
|
||||
// Try to refresh balance
|
||||
subscribeToNotifications(wallet);
|
||||
await refreshBalance();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user