From 838d4b094607c430ccc261145983ed2636abbb88 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 15:42:56 +0000 Subject: [PATCH] Add manual wallet unlock button and metadata display Improve NIP-60 wallet viewer UX: - Replace auto-decrypt with manual "Unlock Wallet" button: * Gives users control over when to decrypt sensitive data * Prevents automatic decryption on page load * Shows clear "locked" state until user clicks unlock - Add comprehensive wallet metadata display after unlock: * Wallet name and description * Unit (sat, usd, etc.) * Configured mints with full URLs * Configured relays * Wallet private key (truncated for security) * Warning about keeping private key secure - Enhance decryption error handling: * Add "Retry" button when decryption fails * Show loading state with spinner during decryption * Display detailed error messages * Console logging for debugging - Improve status indicators: * Clear locked/unlocked visual states * "Decrypting..." message with spinner * Better error display with retry option * Event count status grid This provides users with full visibility into their wallet configuration after unlocking, including all mints, relays, and the wallet private key used for Cashu operations. --- src/components/WalletViewer.tsx | 318 ++++++++++++++++++++++++-------- 1 file changed, 239 insertions(+), 79 deletions(-) diff --git a/src/components/WalletViewer.tsx b/src/components/WalletViewer.tsx index 297532b..b678683 100644 --- a/src/components/WalletViewer.tsx +++ b/src/components/WalletViewer.tsx @@ -10,7 +10,9 @@ import { ArrowUpRight, ArrowDownLeft, Zap, + RefreshCw, } from "lucide-react"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, @@ -19,7 +21,7 @@ import { CardTitle, } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { useMemo, useState, useEffect } from "react"; +import { useMemo, useState, useCallback } from "react"; import accountManager from "@/services/accounts"; import { decryptWalletConfig, @@ -108,73 +110,111 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { // Get active account from accountManager const activeAccount = use$(accountManager.active$); - // Decrypt wallet data when events are available (only for own wallet) - useEffect(() => { - if (!isOwnWallet || !activeAccount?.nip44) return; - if (!walletConfigEvent) return; + // Manual decrypt function + const decryptWalletData = useCallback(async () => { + if (!activeAccount?.nip44) { + setDecryptionError("No NIP-44 encryption support in active account"); + return; + } - const decryptWalletData = async () => { - setIsDecrypting(true); - setDecryptionError(null); + setIsDecrypting(true); + setDecryptionError(null); - try { - // Decrypt wallet config - if (walletConfigEvent) { - const config = await decryptWalletConfig( - walletConfigEvent, - activeAccount, - ); - if (config) { - setWalletConfig(config); - } - } + console.log("[WalletViewer] Starting decryption..."); + console.log( + "[WalletViewer] Wallet config event:", + walletConfigEvent ? "found" : "not found", + ); + console.log("[WalletViewer] Token events:", tokenEvents?.length || 0); + console.log("[WalletViewer] History events:", historyEvents?.length || 0); - // Decrypt token events - if (tokenEvents && tokenEvents.length > 0) { - const decryptedTokens: UnspentTokens[] = []; - for (const event of tokenEvents) { - const tokens = await decryptUnspentTokens(event, activeAccount); - if (tokens) { - decryptedTokens.push(tokens); - } - } - setUnspentTokens(decryptedTokens); - } - - // Decrypt history events - if (historyEvents && historyEvents.length > 0) { - const allTransactions: Transaction[] = []; - for (const event of historyEvents) { - const history = await decryptTransactionHistory( - event, - activeAccount, - ); - if (history && history.transactions) { - allTransactions.push(...history.transactions); - } - } - setTransactions(sortTransactions(allTransactions)); - } - } catch (error) { - console.error("Decryption error:", error); - setDecryptionError( - error instanceof Error - ? error.message - : "Failed to decrypt wallet data", + try { + // Decrypt wallet config + if (walletConfigEvent) { + console.log("[WalletViewer] Decrypting wallet config..."); + const config = await decryptWalletConfig( + walletConfigEvent, + activeAccount, ); - } finally { - setIsDecrypting(false); + if (config) { + console.log( + "[WalletViewer] Wallet config decrypted successfully:", + config, + ); + setWalletConfig(config); + } else { + console.warn("[WalletViewer] Wallet config decryption returned null"); + } } - }; - decryptWalletData(); - }, [ - isOwnWallet, - activeAccount, - walletConfigEvent, - tokenEvents, - historyEvents, - ]); + // Decrypt token events + if (tokenEvents && tokenEvents.length > 0) { + console.log( + `[WalletViewer] Decrypting ${tokenEvents.length} token event(s)...`, + ); + const decryptedTokens: UnspentTokens[] = []; + for (const event of tokenEvents) { + const tokens = await decryptUnspentTokens(event, activeAccount); + if (tokens) { + console.log("[WalletViewer] Token event decrypted:", tokens); + decryptedTokens.push(tokens); + } else { + console.warn( + "[WalletViewer] Token event decryption returned null:", + event.id, + ); + } + } + setUnspentTokens(decryptedTokens); + console.log( + `[WalletViewer] Total ${decryptedTokens.length} token event(s) decrypted`, + ); + } else { + console.log("[WalletViewer] No token events to decrypt"); + setUnspentTokens([]); + } + + // Decrypt history events + if (historyEvents && historyEvents.length > 0) { + console.log( + `[WalletViewer] Decrypting ${historyEvents.length} history event(s)...`, + ); + const allTransactions: Transaction[] = []; + for (const event of historyEvents) { + const history = await decryptTransactionHistory(event, activeAccount); + if (history && history.transactions) { + console.log( + `[WalletViewer] History event decrypted: ${history.transactions.length} transaction(s)`, + ); + allTransactions.push(...history.transactions); + } else { + console.warn( + "[WalletViewer] History event decryption returned null:", + event.id, + ); + } + } + setTransactions(sortTransactions(allTransactions)); + console.log( + `[WalletViewer] Total ${allTransactions.length} transaction(s) decrypted`, + ); + } else { + console.log("[WalletViewer] No history events to decrypt"); + setTransactions([]); + } + + console.log("[WalletViewer] Decryption completed successfully"); + } catch (error) { + console.error("[WalletViewer] Decryption error:", error); + setDecryptionError( + error instanceof Error + ? error.message + : "Failed to decrypt wallet data", + ); + } finally { + setIsDecrypting(false); + } + }, [activeAccount, walletConfigEvent, tokenEvents, historyEvents]); // Calculate balance const balanceByMint = useMemo(() => { @@ -292,9 +332,19 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { )} Wallet Status + {isOwnWallet && + !walletConfig && + !isDecrypting && + activeAccount?.nip44 && ( + + )} {isOwnWallet && walletConfig && ( -
- ✓ Unlocked +
+ + Unlocked
)}
@@ -303,6 +353,19 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { + {decryptionError && ( + + + + {decryptionError} + + + + )} + {isOwnWallet && walletConfig && ( @@ -314,13 +377,28 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { )} - {isOwnWallet && !walletConfig && !isDecrypting && ( + {isOwnWallet && + !walletConfig && + !isDecrypting && + !decryptionError && ( + + + + Wallet Locked: Your wallet data is encrypted + with NIP-44.{" "} + {activeAccount?.nip44 + ? "Click 'Unlock Wallet' to decrypt." + : "Sign in to decrypt."} + + + )} + + {isDecrypting && ( - + - Wallet Locked: Your wallet data is encrypted - with NIP-44.{" "} - {activeAccount?.nip44 ? "Decrypting..." : "Sign in to decrypt."} + Decrypting... Please wait while your wallet + data is being decrypted. )} @@ -335,6 +413,98 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { )} + {/* Wallet Metadata (shown when unlocked) */} + {isOwnWallet && walletConfig && ( +
+
+ Wallet Configuration +
+ + {/* Wallet Name */} + {walletConfig.name && ( +
+
Name
+
{walletConfig.name}
+
+ )} + + {/* Unit */} +
+
Unit
+
+ {walletConfig.unit || "sat"} +
+
+ + {/* Mints */} + {walletConfig.mints && walletConfig.mints.length > 0 && ( +
+
+ Configured Mints ({walletConfig.mints.length}) +
+
+ {walletConfig.mints.map((mint) => ( +
+ {mint} +
+ ))} +
+
+ )} + + {/* Relays */} + {walletConfig.relays && walletConfig.relays.length > 0 && ( +
+
+ Relays ({walletConfig.relays.length}) +
+
+ {walletConfig.relays.map((relay) => ( +
+ {relay} +
+ ))} +
+
+ )} + + {/* Private Key (show first 8 and last 8 chars) */} + {walletConfig.privkey && ( +
+
+ Wallet Private Key (for signing Cashu operations) +
+
+ {walletConfig.privkey.substring(0, 8)}... + {walletConfig.privkey.substring( + walletConfig.privkey.length - 8, + )} +
+
+ ⚠️ Keep this key private - it controls your Cashu wallet +
+
+ )} + + {/* Description */} + {walletConfig.description && ( +
+
+ Description +
+
{walletConfig.description}
+
+ )} +
+ )} + + {/* Event Status Grid */}
Config Event
@@ -368,16 +538,6 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { : "Unknown"}
- {walletConfig && walletConfig.mints && ( -
-
Configured Mints
-
- {walletConfig.mints.map((mint) => ( -
✓ {getMintDisplayName(mint)}
- ))} -
-
- )}