mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 00:17:02 +02:00
refactor: make NWC fully reactive with notifications and graceful balance handling
Changes: - Remove polling mechanism in favor of pure reactive notifications$ observable - Subscribe to wallet.notifications$ for real-time balance updates - Make balance display conditional (only show if available) - Fix TypeScript errors (notification.type access, unused variable) - Remove Jotai callback mechanism for balance updates - Use use$() directly for reactive balance subscription - Update comments to reflect reactive architecture (no polling) The wallet now updates balance automatically when payments are sent/received via NIP-47 notifications, with no polling overhead.
This commit is contained in:
@@ -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() {
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Balance */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Balance:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold">
|
||||
{formatBalance(nwcConnection.balance)}
|
||||
{(balance !== undefined ||
|
||||
nwcConnection.balance !== undefined) && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Balance:
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleRefreshBalance}
|
||||
title="Refresh balance"
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg font-semibold">
|
||||
{formatBalance(balance ?? nwcConnection.balance)}
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleRefreshBalance}
|
||||
title="Refresh balance"
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Wallet Name */}
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -279,9 +284,12 @@ export default function UserMenu() {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="size-4 text-yellow-500" />
|
||||
<span className="text-sm">
|
||||
{formatBalance(nwcConnection.balance)}
|
||||
</span>
|
||||
{balance !== undefined ||
|
||||
nwcConnection.balance !== undefined ? (
|
||||
<span className="text-sm">
|
||||
{formatBalance(balance ?? nwcConnection.balance)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{getWalletName()}
|
||||
|
||||
@@ -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 <div>Balance: {balance} sats</div>;
|
||||
* return <div>Balance: {balance ? Math.floor(balance / 1000) : 0} sats</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@@ -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<WalletConnect | null>(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,
|
||||
|
||||
@@ -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<number | undefined>(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<number | undefined> {
|
||||
if (!walletInstance) return undefined;
|
||||
@@ -164,11 +142,6 @@ export async function refreshBalance(): Promise<number | undefined> {
|
||||
const newBalance = result.balance;
|
||||
|
||||
balance$.next(newBalance);
|
||||
|
||||
if (onBalanceUpdate) {
|
||||
onBalanceUpdate(newBalance);
|
||||
}
|
||||
|
||||
return newBalance;
|
||||
} catch (error) {
|
||||
console.error("[NWC] Failed to refresh balance:", error);
|
||||
|
||||
Reference in New Issue
Block a user