From 497ec07bd815707c906d00e683c08b2d0a7bd879 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 23:13:26 +0000 Subject: [PATCH] feat: add NIP-60 Cashu wallet viewer with wallet subcommand Implements the "wallet nip-61" subcommand for viewing NIP-60 Cashu wallets: - Add wallet-parser.ts for subcommand parsing (nwc, nip-61, cashu, etc.) - Create shared wallet UI components for reuse across wallet types - Add useNip61Wallet hook for reactive wallet state via applesauce-wallet - Create Nip61WalletViewer showing balance, mints breakdown, and history - Update WindowRenderer to route wallet subcommands appropriately The viewer supports: - Unlocking encrypted wallet content - Displaying total balance and per-mint breakdown - Viewing transaction history (placeholders for send/receive) --- src/components/Nip61WalletViewer.tsx | 307 ++++++++++++++++++++ src/components/WindowRenderer.tsx | 11 +- src/components/wallet/TransactionRow.tsx | 58 ++++ src/components/wallet/WalletBalance.tsx | 54 ++++ src/components/wallet/WalletHeader.tsx | 53 ++++ src/components/wallet/WalletHistoryList.tsx | 209 +++++++++++++ src/components/wallet/WalletStates.tsx | 117 ++++++++ src/components/wallet/index.ts | 15 + src/hooks/useNip61Wallet.ts | 154 ++++++++++ src/lib/wallet-parser.ts | 57 ++++ src/services/nip61-wallet.ts | 52 ++++ src/types/man.ts | 27 +- 12 files changed, 1108 insertions(+), 6 deletions(-) create mode 100644 src/components/Nip61WalletViewer.tsx create mode 100644 src/components/wallet/TransactionRow.tsx create mode 100644 src/components/wallet/WalletBalance.tsx create mode 100644 src/components/wallet/WalletHeader.tsx create mode 100644 src/components/wallet/WalletHistoryList.tsx create mode 100644 src/components/wallet/WalletStates.tsx create mode 100644 src/components/wallet/index.ts create mode 100644 src/hooks/useNip61Wallet.ts create mode 100644 src/lib/wallet-parser.ts create mode 100644 src/services/nip61-wallet.ts diff --git a/src/components/Nip61WalletViewer.tsx b/src/components/Nip61WalletViewer.tsx new file mode 100644 index 0000000..2e17a31 --- /dev/null +++ b/src/components/Nip61WalletViewer.tsx @@ -0,0 +1,307 @@ +/** + * Nip61WalletViewer Component + * + * Displays NIP-60 Cashu wallet information: + * - Wallet balance (total and per-mint) + * - Transaction history + * - Placeholder send/receive buttons + */ + +import { useState, useMemo, useCallback } from "react"; +import { Send, Download, RefreshCw, Settings, Coins } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + WalletBalance, + WalletHeader, + WalletHistoryList, + TransactionRow, + NoWalletView, + WalletLockedView, + type HistoryItem, +} from "@/components/wallet"; +import { useNip61Wallet } from "@/hooks/useNip61Wallet"; +import { useGrimoire } from "@/core/state"; +import { useAccount } from "@/hooks/useAccount"; +import type { WalletHistory } from "applesauce-wallet/casts"; + +/** + * Component to display balance breakdown by mint + */ +function MintBalanceBreakdown({ + balance, + blurred, +}: { + balance: Record | undefined; + blurred: boolean; +}) { + if (!balance || Object.keys(balance).length === 0) { + return null; + } + + return ( +
+
+
+ + Balance by Mint +
+
+ {Object.entries(balance).map(([mint, amount]) => ( +
+ + {new URL(mint).hostname} + + + {blurred ? "✦✦✦" : amount.toLocaleString()} sats + +
+ ))} +
+
+
+ ); +} + +/** + * Transform WalletHistory into HistoryItem for the list + */ +function historyToItem(entry: WalletHistory): HistoryItem { + return { + id: entry.id, + timestamp: entry.event.created_at, + data: entry, + }; +} + +export default function Nip61WalletViewer() { + const { state, toggleWalletBalancesBlur } = useGrimoire(); + const { isLoggedIn } = useAccount(); + const { + hasWallet, + isUnlocked, + balance, + totalBalance, + history, + mints, + unlock, + unlocking, + error, + } = useNip61Wallet(); + + const [refreshing, setRefreshing] = useState(false); + + // Transform history for the list component + const historyItems = useMemo(() => { + if (!history) return []; + return history.map(historyToItem); + }, [history]); + + // Refresh wallet data + const handleRefresh = useCallback(async () => { + setRefreshing(true); + try { + // Re-unlock to refresh encrypted content + await unlock(); + toast.success("Wallet refreshed"); + } catch (err) { + console.error("Failed to refresh wallet:", err); + toast.error("Failed to refresh wallet"); + } finally { + setRefreshing(false); + } + }, [unlock]); + + // Render history entry + const renderHistoryEntry = useCallback( + (item: HistoryItem) => { + const entry = item.data as WalletHistory; + + // If not unlocked, show placeholder + if (!entry.unlocked) { + return ( + Locked + } + /> + ); + } + + // Get meta synchronously if available + // Note: In a real implementation, we'd need to handle the observable + // For now, we'll show a simplified view + return ( + + {entry.event.created_at + ? new Date(entry.event.created_at * 1000).toLocaleTimeString() + : "Transaction"} + + } + /> + ); + }, + [state.walletBalancesBlurred], + ); + + // Not logged in + if (!isLoggedIn) { + return ( + + ); + } + + // No wallet found + if (!hasWallet) { + return ( + + ); + } + + // Wallet locked + if (!isUnlocked) { + return ( + + ); + } + + // Determine wallet status + const walletStatus = unlocking || refreshing ? "loading" : "connected"; + + return ( +
+ {/* Header */} + 0 ? ( + + {mints.length} mint{mints.length !== 1 ? "s" : ""} + + ) : undefined + } + actions={ +
+ + + + + Refresh Wallet + + + + + + + Settings (coming soon) + +
+ } + /> + + {/* Error display */} + {error && ( +
+ {error} +
+ )} + + {/* Balance */} + + + {/* Balance by mint */} + + + {/* Send / Receive Buttons (Placeholders) */} +
+
+ + + + + Coming soon + + + + + + + Coming soon + +
+
+ + {/* Transaction History */} +
+
+ +
+
+
+ ); +} diff --git a/src/components/WindowRenderer.tsx b/src/components/WindowRenderer.tsx index ad307ef..53e97fb 100644 --- a/src/components/WindowRenderer.tsx +++ b/src/components/WindowRenderer.tsx @@ -43,6 +43,7 @@ const BlossomViewer = lazy(() => import("./BlossomViewer").then((m) => ({ default: m.BlossomViewer })), ); const WalletViewer = lazy(() => import("./WalletViewer")); +const Nip61WalletViewer = lazy(() => import("./Nip61WalletViewer")); const ZapWindow = lazy(() => import("./ZapWindow").then((m) => ({ default: m.ZapWindow })), ); @@ -232,9 +233,15 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) { /> ); break; - case "wallet": - content = ; + case "wallet": { + const walletSubcommand = window.props.subcommand || "nwc"; + if (walletSubcommand === "nip-61") { + content = ; + } else { + content = ; + } break; + } case "zap": content = ( void; +} + +/** + * Format satoshi amount with locale-aware thousands separator + */ +function formatSats(sats: number): string { + return sats.toLocaleString(); +} + +export function TransactionRow({ + direction, + amount, + blurred = false, + label, + onClick, +}: TransactionRowProps) { + return ( +
+
+ {direction === "in" ? ( + + ) : ( + + )} +
{label}
+
+
+

+ {blurred ? "✦✦✦✦" : formatSats(amount)} +

+
+
+ ); +} diff --git a/src/components/wallet/WalletBalance.tsx b/src/components/wallet/WalletBalance.tsx new file mode 100644 index 0000000..e319051 --- /dev/null +++ b/src/components/wallet/WalletBalance.tsx @@ -0,0 +1,54 @@ +/** + * WalletBalance Component + * + * Displays a large centered balance with privacy blur toggle. + * Shared between NWC and NIP-61 wallet viewers. + */ + +import { Eye, EyeOff } from "lucide-react"; + +interface WalletBalanceProps { + /** Balance in satoshis */ + balance: number | undefined; + /** Whether balance is blurred for privacy */ + blurred: boolean; + /** Callback to toggle blur */ + onToggleBlur: () => void; + /** Optional label shown below balance */ + label?: string; +} + +/** + * Format satoshi amount with locale-aware thousands separator + */ +function formatSats(sats: number | undefined): string { + if (sats === undefined) return "—"; + return sats.toLocaleString(); +} + +export function WalletBalance({ + balance, + blurred, + onToggleBlur, + label, +}: WalletBalanceProps) { + return ( +
+ + {label && ( + {label} + )} +
+ ); +} diff --git a/src/components/wallet/WalletHeader.tsx b/src/components/wallet/WalletHeader.tsx new file mode 100644 index 0000000..7c75217 --- /dev/null +++ b/src/components/wallet/WalletHeader.tsx @@ -0,0 +1,53 @@ +/** + * WalletHeader Component + * + * Displays wallet name, status indicator, and action buttons. + * Shared between NWC and NIP-61 wallet viewers. + */ + +import { ReactNode } from "react"; + +export type WalletStatus = "connected" | "locked" | "disconnected" | "loading"; + +interface WalletHeaderProps { + /** Wallet name/alias */ + name: string; + /** Connection/unlock status */ + status: WalletStatus; + /** Action buttons (refresh, settings, disconnect, etc.) */ + actions?: ReactNode; + /** Additional info content (dropdown, badges, etc.) */ + info?: ReactNode; +} + +function StatusIndicator({ status }: { status: WalletStatus }) { + const colors: Record = { + connected: "bg-green-500", + locked: "bg-yellow-500", + disconnected: "bg-red-500", + loading: "bg-blue-500 animate-pulse", + }; + + return
; +} + +export function WalletHeader({ + name, + status, + actions, + info, +}: WalletHeaderProps) { + return ( +
+ {/* Left: Wallet Name + Status */} +
+ {name} + + {info} +
+ + {/* Right: Actions */} + {actions &&
{actions}
} +
+ ); +} diff --git a/src/components/wallet/WalletHistoryList.tsx b/src/components/wallet/WalletHistoryList.tsx new file mode 100644 index 0000000..5d3af5e --- /dev/null +++ b/src/components/wallet/WalletHistoryList.tsx @@ -0,0 +1,209 @@ +/** + * WalletHistoryList Component + * + * Virtualized list of wallet transactions/history with day markers. + * Shared between NWC and NIP-61 wallet viewers. + */ + +import { ReactNode, useMemo } from "react"; +import { Virtuoso } from "react-virtuoso"; +import { RefreshCw } from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; + +export interface HistoryItem { + /** Unique identifier */ + id: string; + /** Unix timestamp in seconds */ + timestamp: number; + /** Custom data for rendering */ + data: unknown; +} + +interface WalletHistoryListProps { + /** History items to display */ + items: T[]; + /** Whether initial load is in progress */ + loading: boolean; + /** Whether more items are being loaded */ + loadingMore?: boolean; + /** Whether there are more items to load */ + hasMore?: boolean; + /** Whether loading failed */ + loadFailed?: boolean; + /** Callback to load more items */ + onLoadMore?: () => void; + /** Callback to retry loading */ + onRetry?: () => void; + /** Render function for each item */ + renderItem: (item: T, index: number) => ReactNode; + /** Empty state message */ + emptyMessage?: string; +} + +/** + * Format timestamp as a readable day marker + */ +function formatDayMarker(timestamp: number): string { + const date = new Date(timestamp * 1000); + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + // Reset time parts for comparison + const dateOnly = new Date( + date.getFullYear(), + date.getMonth(), + date.getDate(), + ); + const todayOnly = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate(), + ); + const yesterdayOnly = new Date( + yesterday.getFullYear(), + yesterday.getMonth(), + yesterday.getDate(), + ); + + if (dateOnly.getTime() === todayOnly.getTime()) { + return "Today"; + } else if (dateOnly.getTime() === yesterdayOnly.getTime()) { + return "Yesterday"; + } else { + // Format as "Jan 15" (short month, no year, respects locale) + return date.toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }); + } +} + +/** + * Check if two timestamps are on different days + */ +function isDifferentDay(timestamp1: number, timestamp2: number): boolean { + const date1 = new Date(timestamp1 * 1000); + const date2 = new Date(timestamp2 * 1000); + + return ( + date1.getFullYear() !== date2.getFullYear() || + date1.getMonth() !== date2.getMonth() || + date1.getDate() !== date2.getDate() + ); +} + +type ListItem = + | { type: "item"; data: T } + | { type: "day-marker"; label: string; timestamp: number }; + +export function WalletHistoryList({ + items, + loading, + loadingMore = false, + hasMore = false, + loadFailed = false, + onLoadMore, + onRetry, + renderItem, + emptyMessage = "No transactions found", +}: WalletHistoryListProps) { + // Process items to include day markers + const itemsWithMarkers = useMemo(() => { + if (!items || items.length === 0) return []; + + const result: ListItem[] = []; + + items.forEach((item, index) => { + // Add day marker if this is the first item or if day changed + if (index === 0) { + result.push({ + type: "day-marker", + label: formatDayMarker(item.timestamp), + timestamp: item.timestamp, + }); + } else if (isDifferentDay(items[index - 1].timestamp, item.timestamp)) { + result.push({ + type: "day-marker", + label: formatDayMarker(item.timestamp), + timestamp: item.timestamp, + }); + } + + result.push({ type: "item", data: item }); + }); + + return result; + }, [items]); + + // Loading state + if (loading) { + return ( +
+ +
+ ); + } + + // Error state + if (loadFailed) { + return ( +
+

+ Failed to load transaction history +

+ {onRetry && ( + + )} +
+ ); + } + + // Empty state + if (itemsWithMarkers.length === 0) { + return ( +
+

{emptyMessage}

+
+ ); + } + + return ( + { + if (item.type === "day-marker") { + return ( +
+ +
+ ); + } + + return renderItem(item.data, index); + }} + components={{ + Footer: () => + loadingMore ? ( +
+ +
+ ) : !hasMore && items.length > 0 ? ( +
+ No more transactions +
+ ) : null, + }} + /> + ); +} diff --git a/src/components/wallet/WalletStates.tsx b/src/components/wallet/WalletStates.tsx new file mode 100644 index 0000000..dbea384 --- /dev/null +++ b/src/components/wallet/WalletStates.tsx @@ -0,0 +1,117 @@ +/** + * Wallet State Components + * + * Shared components for common wallet states (no wallet, locked, etc.) + */ + +import { ReactNode } from "react"; +import { Wallet, Lock, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface NoWalletViewProps { + /** Title to display */ + title?: string; + /** Message to display */ + message: string; + /** Action button */ + action?: ReactNode; +} + +export function NoWalletView({ + title = "No Wallet Found", + message, + action, +}: NoWalletViewProps) { + return ( +
+ + + + + {title} + + + +

{message}

+ {action} +
+
+
+ ); +} + +interface WalletLockedViewProps { + /** Message to display */ + message?: string; + /** Whether unlock is in progress */ + loading: boolean; + /** Unlock button handler */ + onUnlock: () => void; +} + +export function WalletLockedView({ + message = "Your wallet is locked. Unlock it to view your balance and history.", + loading, + onUnlock, +}: WalletLockedViewProps) { + return ( +
+ + + + + Wallet Locked + + + +

{message}

+ +
+
+
+ ); +} + +interface WalletErrorViewProps { + /** Error message */ + message: string; + /** Retry handler */ + onRetry?: () => void; +} + +export function WalletErrorView({ message, onRetry }: WalletErrorViewProps) { + return ( +
+ + + + + Wallet Error + + + +

{message}

+ {onRetry && ( + + )} +
+
+
+ ); +} diff --git a/src/components/wallet/index.ts b/src/components/wallet/index.ts new file mode 100644 index 0000000..0d50b55 --- /dev/null +++ b/src/components/wallet/index.ts @@ -0,0 +1,15 @@ +/** + * Shared Wallet Components + * + * Reusable components for NWC and NIP-61 wallet viewers. + */ + +export { WalletBalance } from "./WalletBalance"; +export { WalletHeader, type WalletStatus } from "./WalletHeader"; +export { WalletHistoryList, type HistoryItem } from "./WalletHistoryList"; +export { TransactionRow } from "./TransactionRow"; +export { + NoWalletView, + WalletLockedView, + WalletErrorView, +} from "./WalletStates"; diff --git a/src/hooks/useNip61Wallet.ts b/src/hooks/useNip61Wallet.ts new file mode 100644 index 0000000..479bc86 --- /dev/null +++ b/src/hooks/useNip61Wallet.ts @@ -0,0 +1,154 @@ +/** + * useNip61Wallet Hook + * + * Provides access to the user's NIP-60 Cashu wallet state. + * Uses applesauce-wallet casts for reactive wallet data. + */ + +import { useMemo, useCallback, useState, useEffect, useRef } from "react"; +import { use$ } from "applesauce-react/hooks"; +import { castUser } from "applesauce-common/casts"; +import type { Subscription } from "rxjs"; +import { useAccount } from "@/hooks/useAccount"; +import eventStore from "@/services/event-store"; +import pool from "@/services/relay-pool"; +import { hub } from "@/services/hub"; +import { + couch, + UnlockWallet, + WALLET_KIND, + WALLET_TOKEN_KIND, + WALLET_HISTORY_KIND, +} from "@/services/nip61-wallet"; +import { kinds, relaySet } from "applesauce-core/helpers"; + +// Import casts to enable user.wallet$ property +import "applesauce-wallet/casts"; + +/** + * Hook to access the user's NIP-60 Cashu wallet + * + * @returns Wallet state and actions + */ +export function useNip61Wallet() { + const { pubkey, canSign } = useAccount(); + const [unlocking, setUnlocking] = useState(false); + const [error, setError] = useState(null); + const subscriptionRef = useRef(null); + + // Create User cast for the active pubkey + const user = useMemo( + () => (pubkey ? castUser(pubkey, eventStore) : undefined), + [pubkey], + ); + + // Get wallet observable from user cast + const wallet = use$(() => user?.wallet$, [user]); + + // Get wallet state observables + const balance = use$(() => wallet?.balance$, [wallet]); + const tokens = use$(() => wallet?.tokens$, [wallet]); + const history = use$(() => wallet?.history$, [wallet]); + const mints = use$(() => wallet?.mints$, [wallet]); + const relays = use$(() => wallet?.relays$, [wallet]); + const received = use$(() => wallet?.received$, [wallet]); + + // Get user's outbox relays for subscriptions + const outboxes = use$(() => user?.outboxes$, [user]); + + // Subscribe to wallet events when pubkey is available + useEffect(() => { + if (!pubkey) return; + + // Get all relevant relays + const walletRelays = relays || []; + const userOutboxes = outboxes || []; + const allRelays = relaySet(walletRelays, userOutboxes); + + if (allRelays.length === 0) return; + + // Subscribe to wallet-related events + const observable = pool.subscription( + allRelays, + [ + // Wallet events + { + kinds: [WALLET_KIND, WALLET_TOKEN_KIND, WALLET_HISTORY_KIND], + authors: [pubkey], + }, + // Token deletions + { + kinds: [kinds.EventDeletion], + "#k": [String(WALLET_TOKEN_KIND)], + authors: [pubkey], + }, + ], + { eventStore }, + ); + + const subscription = observable.subscribe(); + subscriptionRef.current = subscription; + + return () => { + subscription.unsubscribe(); + subscriptionRef.current = null; + }; + }, [pubkey, relays?.join(","), outboxes?.join(",")]); + + // Unlock wallet action + const unlock = useCallback(async () => { + if (!canSign) { + setError("Cannot unlock: no signer available"); + return false; + } + + setUnlocking(true); + setError(null); + + try { + await hub.run(UnlockWallet, { history: true, tokens: true }); + return true; + } catch (err) { + console.error("Failed to unlock wallet:", err); + setError(err instanceof Error ? err.message : "Failed to unlock wallet"); + return false; + } finally { + setUnlocking(false); + } + }, [canSign]); + + // Calculate total balance across all mints + const totalBalance = useMemo(() => { + if (!balance) return 0; + return Object.values(balance).reduce((sum, amount) => sum + amount, 0); + }, [balance]); + + return { + // Wallet state + wallet, + hasWallet: wallet !== undefined, + isUnlocked: wallet?.unlocked ?? false, + + // Balances + balance, // { [mint: string]: number } + totalBalance, // Total across all mints + + // Data + tokens, + history, + mints, + relays, + received, // Received nutzap IDs + + // Actions + unlock, + unlocking, + + // Error state + error, + + // Utilities + canSign, + couch, // For advanced operations + }; +} diff --git a/src/lib/wallet-parser.ts b/src/lib/wallet-parser.ts new file mode 100644 index 0000000..cb1e4c3 --- /dev/null +++ b/src/lib/wallet-parser.ts @@ -0,0 +1,57 @@ +/** + * Wallet Command Parser + * + * Parses arguments for the wallet command with subcommands: + * - nwc: NWC Lightning wallet (default) + * - nip-61: NIP-60 Cashu ecash wallet + */ + +export type WalletSubcommand = "nwc" | "nip-61"; + +export interface WalletCommandResult { + subcommand: WalletSubcommand; +} + +/** + * Parse wallet command arguments + * + * Usage: + * wallet - Open NWC Lightning wallet (default) + * wallet nwc - Open NWC Lightning wallet + * wallet nip-61 - Open NIP-60 Cashu ecash wallet + * wallet cashu - Alias for nip-61 + * wallet ecash - Alias for nip-61 + */ +export function parseWalletCommand(args: string[]): WalletCommandResult { + // Default to 'nwc' if no subcommand + if (args.length === 0) { + return { subcommand: "nwc" }; + } + + const subcommand = args[0].toLowerCase(); + + switch (subcommand) { + case "nwc": + case "lightning": + case "ln": + return { subcommand: "nwc" }; + + case "nip-61": + case "nip61": + case "cashu": + case "ecash": + case "nuts": + return { subcommand: "nip-61" }; + + default: + throw new Error( + `Unknown wallet type: ${subcommand} + +Available wallet types: + nwc NWC Lightning wallet (default) + nip-61 NIP-60 Cashu ecash wallet + cashu Alias for nip-61 + ecash Alias for nip-61`, + ); + } +} diff --git a/src/services/nip61-wallet.ts b/src/services/nip61-wallet.ts new file mode 100644 index 0000000..1faacd7 --- /dev/null +++ b/src/services/nip61-wallet.ts @@ -0,0 +1,52 @@ +/** + * NIP-61 Wallet Service + * + * Manages NIP-60 Cashu wallet operations using applesauce-wallet. + * Provides wallet unlocking, history, and token management. + */ + +import { IndexedDBCouch } from "applesauce-wallet/helpers"; + +// Re-export wallet constants for use throughout the app +export { + WALLET_KIND, + WALLET_TOKEN_KIND, + WALLET_HISTORY_KIND, + NUTZAP_KIND, +} from "applesauce-wallet/helpers"; + +// Re-export wallet actions +export { + UnlockWallet, + ReceiveToken, + ReceiveNutzaps, + ConsolidateTokens, + RecoverFromCouch, + SetWalletMints, + SetWalletRelays, +} from "applesauce-wallet/actions"; + +// Re-export casts - IMPORTANT: this enables user.wallet$ property +export type { + Wallet, + WalletToken, + WalletHistory, + Nutzap, +} from "applesauce-wallet/casts"; + +// Import casts to enable User extension with wallet$ property +import "applesauce-wallet/casts"; + +/** + * Singleton IndexedDB couch for safe token operations + * + * The "couch" is temporary storage for proofs during operations that could fail. + * This prevents losing proofs if the app crashes mid-operation. + */ +export const couch = new IndexedDBCouch(); + +/** + * NIP-60 Nutzap Info event kind (kind:10019) + * Used to advertise mints, relays, and P2PK pubkey for receiving nutzaps + */ +export const NUTZAP_INFO_KIND = 10019; diff --git a/src/types/man.ts b/src/types/man.ts index 7e30314..6794621 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -9,6 +9,7 @@ import { resolveNip05Batch, resolveDomainDirectoryBatch } from "@/lib/nip05"; import { parseChatCommand } from "@/lib/chat-parser"; import { parseBlossomCommand } from "@/lib/blossom-parser"; import { parseZapCommand } from "@/lib/zap-parser"; +import { parseWalletCommand } from "@/lib/wallet-parser"; export interface ManPageEntry { name: string; @@ -843,14 +844,32 @@ export const manPages: Record = { wallet: { name: "wallet", section: "1", - synopsis: "wallet", + synopsis: "wallet [nwc|nip-61]", description: - "View and manage your Nostr Wallet Connect (NWC) Lightning wallet. Display wallet balance, transaction history, send/receive payments, and view wallet capabilities. The wallet interface adapts based on the methods supported by your connected wallet provider.", - examples: ["wallet Open wallet viewer and manage Lightning payments"], + "View and manage your wallet. Supports NWC (Nostr Wallet Connect) for Lightning payments and NIP-61 for Cashu ecash. The wallet interface adapts based on the wallet type and supported capabilities.", + options: [ + { + flag: "nwc", + description: + "NWC Lightning wallet (default). Connect via nostr+walletconnect:// URI.", + }, + { + flag: "nip-61", + description: + "NIP-60 Cashu ecash wallet. View balance, history, and manage ecash tokens stored on Nostr relays.", + }, + ], + examples: [ + "wallet Open NWC Lightning wallet (default)", + "wallet nwc Open NWC Lightning wallet", + "wallet nip-61 Open Cashu ecash wallet", + "wallet cashu Alias for nip-61", + ], seeAlso: ["profile"], appId: "wallet", category: "Nostr", - defaultProps: {}, + argParser: (args: string[]) => parseWalletCommand(args), + defaultProps: { subcommand: "nwc" }, }, post: { name: "post",