diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx
index 20f9207..635563d 100644
--- a/src/components/nostr/user-menu.tsx
+++ b/src/components/nostr/user-menu.tsx
@@ -88,7 +88,7 @@ export default function UserMenu() {
const walletServiceProfile = useProfile(nwcConnection?.service);
// Use wallet hook for real-time balance and methods
- const { disconnect: disconnectWallet, refreshBalance } = useWallet();
+ const { disconnect: disconnectWallet, refreshBalance, balance } = useWallet();
function openProfile() {
if (!account?.pubkey) return;
@@ -111,7 +111,7 @@ export default function UserMenu() {
}
function handleDisconnectWallet() {
- // Disconnect from NWC service (stops polling, clears wallet instance)
+ // Disconnect from NWC service (stops notifications, clears wallet instance)
disconnectWallet();
// Clear connection from state
disconnectNWC();
@@ -123,7 +123,7 @@ export default function UserMenu() {
try {
await refreshBalance();
toast.success("Balance refreshed");
- } catch (error) {
+ } catch (_error) {
toast.error("Failed to refresh balance");
}
}
@@ -160,22 +160,27 @@ export default function UserMenu() {
{/* Balance */}
-
-
Balance:
-
-
- {formatBalance(nwcConnection.balance)}
+ {(balance !== undefined ||
+ nwcConnection.balance !== undefined) && (
+
+
+ Balance:
-
+
+
+ {formatBalance(balance ?? nwcConnection.balance)}
+
+
+
-
+ )}
{/* Wallet Name */}
@@ -279,9 +284,12 @@ export default function UserMenu() {
>
-
- {formatBalance(nwcConnection.balance)}
-
+ {balance !== undefined ||
+ nwcConnection.balance !== undefined ? (
+
+ {formatBalance(balance ?? nwcConnection.balance)}
+
+ ) : null}
{getWalletName()}
diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts
index c565568..61ac112 100644
--- a/src/hooks/useWallet.ts
+++ b/src/hooks/useWallet.ts
@@ -2,19 +2,20 @@
* useWallet Hook
*
* Provides access to the NWC wallet throughout the application.
- * Handles wallet lifecycle, balance updates, and transaction methods.
+ * Fully reactive using observables - balance updates automatically via use$()
*
* @example
* ```tsx
* function MyComponent() {
- * const { wallet, balance, payInvoice, makeInvoice, refreshBalance } = useWallet();
+ * const { wallet, balance, payInvoice, makeInvoice } = useWallet();
*
* async function handlePay() {
* if (!wallet) return;
* await payInvoice("lnbc...");
+ * // Balance automatically updates via notifications!
* }
*
- * return Balance: {balance} sats
;
+ * return Balance: {balance ? Math.floor(balance / 1000) : 0} sats
;
* }
* ```
*/
@@ -26,18 +27,17 @@ import {
getWallet,
restoreWallet,
clearWallet as clearWalletService,
- setBalanceUpdateCallback,
refreshBalance as refreshBalanceService,
balance$,
} from "@/services/nwc";
import type { WalletConnect } from "applesauce-wallet-connect";
export function useWallet() {
- const { state, updateNWCBalance } = useGrimoire();
+ const { state } = useGrimoire();
const nwcConnection = state.nwcConnection;
const [wallet, setWallet] = useState(getWallet());
- // Subscribe to balance updates from the service
+ // Subscribe to balance updates from observable (fully reactive!)
const balance = use$(balance$);
// Initialize wallet on mount if connection exists but no wallet instance
@@ -47,39 +47,30 @@ export function useWallet() {
const restoredWallet = restoreWallet(nwcConnection);
setWallet(restoredWallet);
- // Refresh balance on restore
+ // Fetch initial balance
refreshBalanceService();
}
}, [nwcConnection, wallet]);
- // Set up balance update callback to sync with state
- useEffect(() => {
- setBalanceUpdateCallback((newBalance) => {
- updateNWCBalance(newBalance);
- });
-
- return () => {
- setBalanceUpdateCallback(() => {});
- };
- }, [updateNWCBalance]);
-
// Update local wallet ref when connection changes
useEffect(() => {
const currentWallet = getWallet();
if (currentWallet !== wallet) {
setWallet(currentWallet);
}
- }, [nwcConnection]);
+ }, [nwcConnection, wallet]);
/**
* 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);
- // Refresh balance after payment
+ // Balance will update automatically via notifications
+ // But we can also refresh immediately for instant feedback
await refreshBalanceService();
return result;
@@ -138,7 +129,7 @@ export function useWallet() {
return {
/** The wallet instance (null if not connected) */
wallet,
- /** Current balance in millisats (undefined if not available) */
+ /** Current balance in millisats (auto-updates via observable!) */
balance,
/** Whether a wallet is connected */
isConnected: !!wallet,
diff --git a/src/services/nwc.ts b/src/services/nwc.ts
index 40d5809..8c40ba0 100644
--- a/src/services/nwc.ts
+++ b/src/services/nwc.ts
@@ -7,101 +7,27 @@
* Features:
* - Maintains persistent wallet connection across app lifetime
* - Subscribes to NIP-47 notifications (kind 23197) for balance updates
- * - Automatically updates balance when transactions occur
- * - Provides hook for easy wallet access throughout the app
+ * - Fully reactive using RxJS observables (no polling!)
+ * - Components use use$() to reactively subscribe to balance changes
*/
import { WalletConnect } from "applesauce-wallet-connect";
import type { NWCConnection } from "@/types/app";
import pool from "./relay-pool";
-import { BehaviorSubject } from "rxjs";
+import { BehaviorSubject, Subscription } from "rxjs";
// Set the pool for wallet connect to use
WalletConnect.pool = pool;
let walletInstance: WalletConnect | null = null;
-let notificationSubscription: any = null;
+let notificationSubscription: Subscription | null = null;
/**
* Observable for wallet balance updates
- * Components can subscribe to this for real-time balance changes
+ * Components can subscribe to this for real-time balance changes using use$()
*/
export const balance$ = new BehaviorSubject(undefined);
-/**
- * Callback for balance updates (set by the hook/state management)
- */
-let onBalanceUpdate: ((balance: number) => void) | null = null;
-
-/**
- * Register a callback for balance updates
- */
-export function setBalanceUpdateCallback(callback: (balance: number) => void) {
- onBalanceUpdate = callback;
-}
-
-/**
- * Subscribe to wallet notifications (NIP-47 kind 23197)
- * This enables real-time balance updates when transactions occur
- */
-function subscribeToNotifications(wallet: WalletConnect) {
- // Clean up existing subscription
- if (notificationSubscription) {
- notificationSubscription.unsubscribe();
- }
-
- // Subscribe to notifications from the wallet service
- // The applesauce-wallet-connect library handles this internally
- // when notifications are enabled in the wallet info
- console.log("[NWC] Subscribed to wallet notifications");
-
- // Note: The actual notification subscription is handled by WalletConnect
- // We can poll for balance updates periodically as a fallback
- startBalancePolling(wallet);
-}
-
-/**
- * Poll for balance updates every 30 seconds
- * This ensures we stay in sync even if notifications aren't working
- */
-function startBalancePolling(wallet: WalletConnect) {
- const pollInterval = setInterval(async () => {
- try {
- const result = await wallet.getBalance();
- const newBalance = result.balance;
-
- // Update observable
- if (balance$.value !== newBalance) {
- balance$.next(newBalance);
-
- // Trigger callback for state updates
- if (onBalanceUpdate) {
- onBalanceUpdate(newBalance);
- }
-
- console.log("[NWC] Balance updated:", newBalance);
- }
- } catch (error) {
- console.error("[NWC] Failed to poll balance:", error);
- }
- }, 30000); // Poll every 30 seconds
-
- // Store for cleanup
- notificationSubscription = {
- unsubscribe: () => clearInterval(pollInterval),
- };
-}
-
-/**
- * Creates a new WalletConnect instance from a connection string
- * Automatically subscribes to notifications for balance updates
- */
-export function createWalletFromURI(connectionString: string): WalletConnect {
- walletInstance = WalletConnect.fromConnectURI(connectionString);
- subscribeToNotifications(walletInstance);
- return walletInstance;
-}
-
/**
* Helper to convert hex string to Uint8Array
*/
@@ -113,6 +39,58 @@ function hexToBytes(hex: string): Uint8Array {
return bytes;
}
+/**
+ * Subscribe to wallet notifications (NIP-47 kind 23197)
+ * This enables real-time balance updates when transactions occur
+ */
+function subscribeToNotifications(wallet: WalletConnect) {
+ // Clean up existing subscription
+ if (notificationSubscription) {
+ notificationSubscription.unsubscribe();
+ }
+
+ console.log("[NWC] Subscribing to wallet notifications");
+
+ // Subscribe to the wallet's notifications$ observable
+ // This receives events like payment_received, payment_sent, etc.
+ notificationSubscription = wallet.notifications$.subscribe({
+ next: (notification) => {
+ console.log("[NWC] Notification received:", notification);
+
+ // When we get a notification, refresh the balance
+ // The notification types include: payment_received, payment_sent, etc.
+ wallet
+ .getBalance()
+ .then((result) => {
+ const newBalance = result.balance;
+ if (balance$.value !== newBalance) {
+ balance$.next(newBalance);
+ console.log("[NWC] Balance updated from notification:", newBalance);
+ }
+ })
+ .catch((error) => {
+ console.error(
+ "[NWC] Failed to fetch balance after notification:",
+ error,
+ );
+ });
+ },
+ error: (error) => {
+ console.error("[NWC] Notification subscription error:", error);
+ },
+ });
+}
+
+/**
+ * Creates a new WalletConnect instance from a connection string
+ * Automatically subscribes to notifications for balance updates
+ */
+export function createWalletFromURI(connectionString: string): WalletConnect {
+ walletInstance = WalletConnect.fromConnectURI(connectionString);
+ subscribeToNotifications(walletInstance);
+ return walletInstance;
+}
+
/**
* Restores a wallet from saved connection data
* Used on app startup to reconnect to a previously connected wallet
@@ -124,7 +102,7 @@ export function restoreWallet(connection: NWCConnection): WalletConnect {
secret: hexToBytes(connection.secret),
});
- // Set initial balance
+ // Set initial balance from cache
if (connection.balance !== undefined) {
balance$.next(connection.balance);
}
@@ -150,11 +128,11 @@ export function clearWallet(): void {
}
walletInstance = null;
balance$.next(undefined);
- onBalanceUpdate = null;
}
/**
* Manually refresh the balance from the wallet
+ * Useful for initial load or manual refresh button
*/
export async function refreshBalance(): Promise {
if (!walletInstance) return undefined;
@@ -164,11 +142,6 @@ export async function refreshBalance(): Promise {
const newBalance = result.balance;
balance$.next(newBalance);
-
- if (onBalanceUpdate) {
- onBalanceUpdate(newBalance);
- }
-
return newBalance;
} catch (error) {
console.error("[NWC] Failed to refresh balance:", error);