mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 22:47:02 +02:00
feat: add comprehensive NWC wallet viewer with dynamic UI (#135)
* feat: add comprehensive NWC wallet viewer with dynamic UI Implements a full-featured Lightning wallet interface using Nostr Wallet Connect (NWC/NIP-47) with method-based UI that adapts to wallet capabilities. **New Features:** - WalletViewer component with tabbed interface (Overview, Send, Receive, Transactions) - Real-time balance display with manual refresh - Send Lightning payments via BOLT11 invoices - Generate invoices with QR codes for receiving payments - Transaction history viewer (when supported by wallet) - Wallet info and capabilities display - Enhanced useWallet hook with additional NWC methods **Enhanced Methods:** - listTransactions() - View recent payment history - lookupInvoice() - Check invoice status by payment hash - payKeysend() - Direct keysend payments to node pubkeys **UI Features:** - Dynamic tabs based on wallet capabilities - QR code generation for invoices - Copy-to-clipboard for invoices - Error handling with user-friendly messages - Loading states for async operations - Empty states for no wallet connection **Command:** - New `wallet` command to open the wallet viewer **Technical Details:** - Integrates with existing NWC service singleton - Uses reactive balance$ observable for auto-updates - Proper TypeScript types aligned with applesauce-wallet-connect - Follows Grimoire patterns for window system integration - Lazy-loaded component for optimal bundle size All tests passing. Build verified. * refactor: redesign wallet UI to single-view layout with virtualized transactions Converts the tabbed wallet interface to a conventional single-view layout with improved UX and performance optimizations. **Layout Changes:** - Removed tabs in favor of single-page layout - Balance header at top with wallet name and refresh button - Side-by-side Send/Receive cards for quick access - Transaction history below with virtualized scrolling - Disconnect button at bottom of page **New Features:** - Connect Wallet button when no wallet is connected (opens dialog in-app) - Wallet capabilities shown in tooltip on info icon - Virtualized transaction list using react-virtuoso - Batched transaction loading (20 per batch) - Automatic "load more" when scrolling to bottom - Loading states for initial load and pagination - "No more transactions" message when exhausted **Performance Improvements:** - Virtualized list rendering for smooth scrolling with many transactions - Only renders visible transactions in viewport - Lazy loads additional batches on demand - Reduced initial load to 20 transactions instead of 50 **UX Improvements:** - More conventional wallet UI pattern - Send/Receive always visible (no tab switching) - QR code and invoice appear inline when generated - Info icon with tooltip for capabilities (cleaner than full card) - Disconnect option always accessible at bottom **Technical Details:** - Fixed transaction loading race condition with separate useEffect - Proper dependency tracking for loadMoreTransactions callback - Footer component in Virtuoso for loading/end states - Responsive grid layout for Send/Receive cards All tests passing. Build verified. * refactor: redesign wallet UI with ReqViewer-style header and dialogs Implements a cleaner, more consistent wallet UI matching Grimoire patterns. **Header Changes:** - ReqViewer-style compact header (border-b, px-4, py-2, font-mono, text-xs) - Left side: Wallet name · Balance with info tooltip - Right side: Icon-only action buttons (Receive, Send, Refresh) - All actions use tooltips for labels - Consistent spacing and typography with rest of app **Send/Receive as Dialogs:** - Removed always-visible Send/Receive cards - Converted to modal dialogs opened from header icons - Send dialog: Invoice input + optional amount override - Receive dialog: Two-step flow (generate → show QR + invoice) - "Generate Another" button to create multiple invoices without closing - Cleaner main view focused on transaction list **Transaction List Cleanup:** - Removed Card wrapper and extra padding - Direct virtualized list fills viewport - Removed "Transaction History" heading (redundant) - Added flex-shrink-0 to prevent transaction items from shrinking - Improved truncation handling for long descriptions - Hover effect for better interactivity - Footer messages use consistent border-b styling **Disconnect Button:** - Changed to destructive variant (red appearance) - Moved to footer with border-t - Full width for emphasis - Clearer visual indication of destructive action **UX Improvements:** - More screen space for transactions (primary content) - Consistent icon sizing (size-3 for header actions, size-4 for tx icons) - Better visual hierarchy with header actions - Dialog pattern matches other Grimoire components - Receive dialog stays open to show generated invoice **Technical Details:** - Proper dialog state management - Invoice QR with max-height scrolling for long invoices - Reset form state when closing dialogs - Maintained all existing functionality - Optimized for virtualized scrolling performance All tests passing. Build verified. * feat: final wallet UI improvements with prominent balance and enhanced UX Redesigns wallet viewer with cleaner layout and improved user experience. **Layout Changes:** - Big centered balance display (4xl font, prominent) - Large Send/Receive buttons below balance (side by side) - Single-line transaction items with better spacing - Info dropdown next to refresh button in header **Transaction List:** - Single-line compact design (description + time + amount) - No +/- signs on amounts (cleaner look) - Generic "Payment"/"Received" labels when description missing - Time displayed in compact format (HH:MM) - Day markers between days (Today/Yesterday/Jan 15) - Virtualized scrolling with batched loading **Info Dropdown:** - Wallet capabilities shown in dropdown (Info icon + ChevronDown) - Network information - Methods displayed as compact badges - Notifications support **User Menu Integration:** - Wallet option always visible (regardless of account status) - Clicking wallet opens wallet window (not info dialog) - Balance shown inline when connected - "Connect Wallet" option when not connected **Dialog Improvements:** - Send dialog with confirmation step - Receive dialog with payment detection - Auto-close on payment received - QR code with loading overlay during payment check **Visual Hierarchy:** - Header: Wallet name (left) | Info dropdown + Refresh (right) - Big centered balance with "sats" label - Prominent action buttons (Send default, Receive outline) - Clean transaction list with hover states - Destructive disconnect button in footer All tests passing ✅ Build verified ✅ * fix: replace AlertDialog with Dialog for disconnect confirmation - AlertDialog component doesn't exist in UI library - Use regular Dialog with custom footer buttons instead - All 929 tests passing, build successful * refine: wallet UI improvements based on feedback - Remove "sats" text from balance display - Swap send/receive button positions (receive left, send right) - Remove top border from transaction list - Remove timestamps from transaction list items - Add relay link to wallet info dropdown with external link icon - Change disconnect button to destructive color (always red) - Fix imports and remove unused formatTime function * feat: enhance send/receive flows with invoice parsing and auto-confirm Send flow improvements: - Parse BOLT11 invoices using light-bolt11-decoder - Auto-proceed to confirm step when valid invoice is entered - Show parsed amount and description in confirmation dialog - Validate invoice before allowing confirmation Receive flow improvements: - Fix invoice overflow with proper truncate display - Use nested div structure for single-line truncation All changes preserve type safety with proper Section type guards * feat: add Lightning address support and refine auto-confirm behavior Send flow enhancements: - Only auto-proceed to confirm if invoice has an amount (not for zero-amount invoices) - Add Lightning address (LNURL-pay) support with automatic resolution - Fetch invoice from Lightning address with amount validation - Show "Resolving..." loading state when processing Lightning addresses - Update UI labels and placeholders to indicate Lightning address support - Require amount field for Lightning address payments Lightning address flow: 1. Detect @ symbol in input (and not starting with "ln") 2. Validate amount is provided 3. Fetch LNURL-pay endpoint from .well-known/lnurlp/{username} 4. Check min/max sendable amounts 5. Request invoice from callback with specified amount 6. Parse and confirm invoice details Error handling: - Invalid Lightning address format - Failed to fetch Lightning address - Amount out of range (too small/too large) - Failed to generate invoice from callback * fix: UI improvements for wallet viewer and mobile receive flow Confirmation dialog improvements: - Remove yellow warning styles (border, background, icon) - Fix amount calculation: show proper sats conversion with Math.floor - Clean layout with key-value pairs instead of cluttered text - Show either invoice amount OR override amount (not both) Mobile receive flow improvements: - Large prominent "Copy Invoice" button (h-12, full width) - Invoice box now tappable to copy - Better touch targets for mobile users - Clearer label: "Invoice (tap to view)" Connection status: - Show green dot + "Connected" next to wallet name in header - Clear visual feedback that wallet is online Auto-launch wallet: - After successfully connecting wallet, automatically open wallet window - Added onConnected callback to ConnectWalletDialog - Wire up callback in user menu to call openWallet Code cleanup: - Remove unused AlertTriangle import * fix: correct amount handling and mobile invoice display Amount input fixes: - Changed from millisats to sats for user input - Updated label: "Amount (sats, optional)" - Fixed Lightning address resolution: no division, amount is already in sats - Fixed payInvoice: multiply by 1000 to convert sats to millisats for NWC - Fixed confirmation dialog: removed incorrect division by 1000 Before: User enters 1000 (meant as sats) → system treats as 1000000 millisats → sends 1000 sats After: User enters 1000 sats → system converts to 1000000 millisats → sends 1000 sats ✓ Invoice parsing was correct - already converting millisats to sats. The bug was in the confirmation display and amount submission. Mobile receive invoice fix: - Removed nested div with truncate (was causing overflow) - Changed to break-all + line-clamp-2 - Invoice now wraps properly on mobile (2 lines max) - Still tappable to copy * fix: convert sats to millisats when generating invoices Critical bug fix - we were generating nearly amountless invoices. Before: - User enters 1000 sats in UI - We passed 1000 to makeInvoice (thinking it's millisats) - Generated invoice for 1000 millisats = 1 sat After: - User enters 1000 sats in UI - We multiply by 1000 → 1,000,000 millisats - Generated invoice for 1,000,000 millisats = 1000 sats ✓ This matches the send flow where we also convert sats to millisats before passing to NWC protocol. * refactor: production-readiness improvements for WalletViewer Critical fixes: - Wrap loadWalletInfo and loadInitialTransactions in useCallback to prevent infinite loops - Fix dependency arrays in useEffect hooks - Fix early return bug in handleConfirmSend that left sending state stuck - Add async/await error handling to handleCopyInvoice - Change throw new Error instead of early return in Lightning address resolution Performance optimizations: - Removed duplicate function definitions (moved to useCallback hooks) - Proper memoization of wallet loading functions Code quality: - All functions properly handle errors with try/catch - Loading states properly reset in finally blocks - No memory leaks from useEffect dependencies * fix: prevent infinite transaction loading loop in WalletViewer Previously, the transaction list would try to load constantly and fail repeatedly due to a circular dependency in the useEffect hooks. The listTransactions function from useWallet wasn't wrapped in useCallback, causing loadInitialTransactions to be recreated on every render, which triggered the useEffect infinitely. Changes: - Add txLoadAttempted and txLoadFailed flags to prevent repeated attempts - Only attempt to load transactions once on wallet connection - Fail silently on initial load (no toast spam) - Show retry button when transaction loading fails - Reset flags when wallet connects/disconnects or after successful payments - Make transaction list truly optional - wallet still works if loading fails This ensures a better UX when wallets don't support list_transactions or when the method fails for any reason. * fix: remove circular dependency in loadInitialTransactions Removed txLoadFailed from the dependency array of loadInitialTransactions callback, which was causing a circular dependency: - loadInitialTransactions depended on txLoadFailed - Function sets txLoadFailed, triggering recreation - New function reference triggers useEffect again - Infinite loop The txLoadAttempted flag in the useEffect is sufficient to prevent repeated loads. No need to check txLoadFailed inside the callback. * fix: resolve React error #185 by removing callbacks from useEffect dependencies React error #185 occurs when state updates happen during render due to unstable function references in useEffect dependencies. The root cause was that functions from useWallet (getInfo, listTransactions) aren't memoized, so they create new references on every render, causing the useEffects that depend on them to run repeatedly. Changes: - Removed loadWalletInfo and loadInitialTransactions callbacks - Use refs (walletInfoLoadedRef, lastConnectionStateRef) to track state - Call async functions directly in useEffect without depending on them - Created reloadTransactions() helper that just resets flags - Simplified all reload logic to use the helper This ensures: - No circular dependencies in useEffect - Functions only run once when conditions are met - No state updates during render - Clean, predictable loading behavior * fix: properly clear wallet state on disconnect and update copy Changes: - Clear all wallet state when disconnecting (transactions, walletInfo, loading flags) - Previously only cleared state on connect, leaving stale data visible - Remove "Mutiny" mention from connection dialog copy - Update to generic "NWC wallet provider" text Now when you disconnect the wallet with the window open, it properly clears all data and returns to the "No Wallet Connected" state. * fix: prevent wallet from auto-reconnecting after disconnect The bug was in the useWallet hook which automatically restores the wallet if nwcConnection exists in state but the wallet instance is null. When disconnecting, it only cleared the wallet instance but left nwcConnection in state, causing an immediate reconnection. Fix: - Call disconnectNWCFromState() to clear nwcConnection from Grimoire state - Then call disconnect() to clear the wallet service - This prevents the auto-restore logic from triggering Now when you disconnect the wallet, it stays disconnected until you manually reconnect. * security: add critical production-ready security fixes Invoice Validation & Expiry Checks: - Validate BOLT11 invoice format (must start with 'ln') - Check invoice expiry before displaying/processing - Validate amount is reasonable (< 21M BTC) - Surface parse errors to user with toast notifications - Prevent processing of expired invoices Lightning Address Security: - Enforce HTTPS-only for LNURL-pay requests - Add 5-second timeout to all HTTP requests - Validate callback URLs use HTTPS - Proper AbortController cleanup on timeout - Better error messages for network failures Rate Limiting: - Balance refresh: minimum 2 seconds between calls - Transaction reload: minimum 5 seconds between reloads - User-friendly warning messages with countdown - Prevents spam to wallet service providers Storage Security Warning: - Add prominent security notice in ConnectWalletDialog - Warn users about browser storage implications - Advise to only connect on trusted devices Capability Detection: - Hide Send button if wallet doesn't support pay_invoice - Hide Receive button if wallet doesn't support make_invoice - Dynamic button rendering based on wallet capabilities - Prevents errors from unsupported operations Error Handling: - WindowErrorBoundary already wraps all windows (verified) - Proper error propagation with user-friendly messages - No silent failures on critical operations These changes significantly improve security and production-readiness without breaking existing functionality. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2, Wallet, AlertCircle } from "lucide-react";
|
||||
import { Loader2, Wallet, AlertCircle, AlertTriangle } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -16,11 +16,13 @@ import { createWalletFromURI } from "@/services/nwc";
|
||||
interface ConnectWalletDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConnected?: () => void;
|
||||
}
|
||||
|
||||
export default function ConnectWalletDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConnected,
|
||||
}: ConnectWalletDialogProps) {
|
||||
const [connectionString, setConnectionString] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -103,6 +105,9 @@ export default function ConnectWalletDialog({
|
||||
|
||||
// Close dialog
|
||||
onOpenChange(false);
|
||||
|
||||
// Call onConnected callback
|
||||
onConnected?.();
|
||||
} catch (err) {
|
||||
console.error("Wallet connection error:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to connect wallet");
|
||||
@@ -124,10 +129,24 @@ export default function ConnectWalletDialog({
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enter your wallet connection string. You can get this from your
|
||||
wallet provider (Alby, Mutiny, etc.)
|
||||
Enter your wallet connection string. You can get this from your NWC
|
||||
wallet provider.
|
||||
</p>
|
||||
|
||||
{/* Security warning */}
|
||||
<div className="flex items-start gap-2 rounded-md border border-yellow-500/50 bg-yellow-500/10 p-3 text-sm">
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-yellow-600 dark:text-yellow-500" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-yellow-900 dark:text-yellow-200">
|
||||
Security Notice
|
||||
</p>
|
||||
<p className="text-yellow-800 dark:text-yellow-300">
|
||||
Your wallet connection will be stored in browser storage. Only
|
||||
connect on trusted devices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||||
|
||||
1418
src/components/WalletViewer.tsx
Normal file
1418
src/components/WalletViewer.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ const SpellbooksViewer = lazy(() =>
|
||||
const BlossomViewer = lazy(() =>
|
||||
import("./BlossomViewer").then((m) => ({ default: m.BlossomViewer })),
|
||||
);
|
||||
const WalletViewer = lazy(() => import("./WalletViewer"));
|
||||
const CountViewer = lazy(() => import("./CountViewer"));
|
||||
|
||||
// Loading fallback component
|
||||
@@ -222,6 +223,9 @@ export function WindowRenderer({ window, onClose }: WindowRendererProps) {
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "wallet":
|
||||
content = <WalletViewer />;
|
||||
break;
|
||||
default:
|
||||
content = (
|
||||
<div className="p-4 text-muted-foreground">
|
||||
|
||||
@@ -99,6 +99,10 @@ export default function UserMenu() {
|
||||
);
|
||||
}
|
||||
|
||||
function openWallet() {
|
||||
addWindow("wallet", {}, "Wallet");
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
if (!account) return;
|
||||
accounts.removeAccount(account);
|
||||
@@ -155,6 +159,7 @@ export default function UserMenu() {
|
||||
<ConnectWalletDialog
|
||||
open={showConnectWallet}
|
||||
onOpenChange={setShowConnectWallet}
|
||||
onConnected={openWallet}
|
||||
/>
|
||||
|
||||
{/* Wallet Info Dialog */}
|
||||
@@ -295,7 +300,7 @@ export default function UserMenu() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-80" align="start">
|
||||
{account ? (
|
||||
{account && (
|
||||
<>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel
|
||||
@@ -307,43 +312,47 @@ export default function UserMenu() {
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Wallet Section */}
|
||||
{nwcConnection ? (
|
||||
<DropdownMenuItem
|
||||
className="cursor-crosshair flex items-center justify-between"
|
||||
onClick={() => setShowWalletInfo(true)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="size-4 text-muted-foreground" />
|
||||
{balance !== undefined ||
|
||||
nwcConnection.balance !== undefined ? (
|
||||
<span className="text-sm">
|
||||
{formatBalance(balance ?? nwcConnection.balance)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`size-1.5 rounded-full ${
|
||||
wallet ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{getWalletName()}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="cursor-crosshair"
|
||||
onClick={() => setShowConnectWallet(true)}
|
||||
>
|
||||
<Wallet className="size-4 text-muted-foreground mr-2" />
|
||||
<span className="text-sm">Connect Wallet</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/* Wallet Section - Always show */}
|
||||
{nwcConnection ? (
|
||||
<DropdownMenuItem
|
||||
className="cursor-crosshair flex items-center justify-between"
|
||||
onClick={openWallet}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="size-4 text-muted-foreground" />
|
||||
{balance !== undefined ||
|
||||
nwcConnection.balance !== undefined ? (
|
||||
<span className="text-sm">
|
||||
{formatBalance(balance ?? nwcConnection.balance)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`size-1.5 rounded-full ${
|
||||
wallet ? "bg-green-500" : "bg-red-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{getWalletName()}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="cursor-crosshair"
|
||||
onClick={() => setShowConnectWallet(true)}
|
||||
>
|
||||
<Wallet className="size-4 text-muted-foreground mr-2" />
|
||||
<span className="text-sm">Connect Wallet</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{account && (
|
||||
<>
|
||||
{relays && relays.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -398,64 +407,44 @@ export default function UserMenu() {
|
||||
<DropdownMenuItem onClick={logout} className="cursor-crosshair">
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-crosshair">
|
||||
<Palette className="size-4 mr-2" />
|
||||
Theme
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{availableThemes.map((theme) => (
|
||||
<DropdownMenuItem
|
||||
key={theme.id}
|
||||
className="cursor-crosshair"
|
||||
onClick={() => setTheme(theme.id)}
|
||||
>
|
||||
<span
|
||||
className={`size-2 rounded-full mr-2 ${
|
||||
themeId === theme.id
|
||||
? "bg-primary"
|
||||
: "bg-muted-foreground/30"
|
||||
}`}
|
||||
/>
|
||||
{theme.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{!account && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => setShowLogin(true)}>
|
||||
Log in
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-crosshair">
|
||||
<Palette className="size-4 mr-2" />
|
||||
Theme
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{availableThemes.map((theme) => (
|
||||
<DropdownMenuItem
|
||||
key={theme.id}
|
||||
className="cursor-crosshair"
|
||||
onClick={() => setTheme(theme.id)}
|
||||
>
|
||||
<span
|
||||
className={`size-2 rounded-full mr-2 ${
|
||||
themeId === theme.id
|
||||
? "bg-primary"
|
||||
: "bg-muted-foreground/30"
|
||||
}`}
|
||||
/>
|
||||
{theme.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Theme Section - Always show */}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="cursor-crosshair">
|
||||
<Palette className="size-4 mr-2" />
|
||||
Theme
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
{availableThemes.map((theme) => (
|
||||
<DropdownMenuItem
|
||||
key={theme.id}
|
||||
className="cursor-crosshair"
|
||||
onClick={() => setTheme(theme.id)}
|
||||
>
|
||||
<span
|
||||
className={`size-2 rounded-full mr-2 ${
|
||||
themeId === theme.id
|
||||
? "bg-primary"
|
||||
: "bg-muted-foreground/30"
|
||||
}`}
|
||||
/>
|
||||
{theme.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
|
||||
@@ -118,6 +118,50 @@ export function useWallet() {
|
||||
return await refreshBalanceService();
|
||||
}
|
||||
|
||||
/**
|
||||
* List recent transactions
|
||||
* @param options - Pagination and filter options
|
||||
*/
|
||||
async function listTransactions(options?: {
|
||||
from?: number;
|
||||
until?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
unpaid?: boolean;
|
||||
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
|
||||
*/
|
||||
@@ -143,6 +187,12 @@ export function useWallet() {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ export type AppId =
|
||||
| "spells"
|
||||
| "spellbooks"
|
||||
| "blossom"
|
||||
| "wallet"
|
||||
| "win";
|
||||
|
||||
export interface WindowInstance {
|
||||
|
||||
@@ -785,4 +785,16 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
},
|
||||
defaultProps: { subcommand: "servers" },
|
||||
},
|
||||
wallet: {
|
||||
name: "wallet",
|
||||
section: "1",
|
||||
synopsis: "wallet",
|
||||
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"],
|
||||
seeAlso: ["profile"],
|
||||
appId: "wallet",
|
||||
category: "Nostr",
|
||||
defaultProps: {},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user