+ {/* Header */}
+
+ {/* Left: Wallet Name + Status */}
+
+
+ {walletInfo?.alias || "Lightning Wallet"}
+
+
+
+
+ {/* Right: Info Dropdown, Refresh, Disconnect */}
+
+ {walletInfo && (
+
+
+
+
+
+
+
+
+
+
+
+ Wallet Information
+
+ {walletInfo.network && (
+
+ Network
+
+ {walletInfo.network}
+
+
+ )}
+ {state.nwcConnection?.relays &&
+ state.nwcConnection.relays.length > 0 && (
+
+ )}
+
+
+
+
Capabilities
+
+ {walletInfo.methods.map((method) => (
+
+ {method}
+
+ ))}
+
+
+
+ {walletInfo.notifications &&
+ walletInfo.notifications.length > 0 && (
+
+
+ Notifications
+
+
+ {walletInfo.notifications.map((notification) => (
+
+ {notification}
+
+ ))}
+
+
+ )}
+
+
+
+ )}
+
+
+
+
+
+
+
+ Refresh Balance
+
+
+
+
+ setDisconnectDialogOpen(true)}
+ className="flex items-center gap-1 text-destructive hover:text-destructive/80 transition-colors"
+ aria-label="Disconnect wallet"
+ >
+
+
+
+ Disconnect Wallet
+
+
+
+
+ {/* Big Centered Balance */}
+
+
+ {formatSats(balance)}
+
+
+
+ {/* Send / Receive Buttons */}
+ {walletInfo &&
+ (walletInfo.methods.includes("pay_invoice") ||
+ walletInfo.methods.includes("make_invoice")) && (
+
+
+ {walletInfo.methods.includes("make_invoice") && (
+ setReceiveDialogOpen(true)}
+ variant="outline"
+ >
+
+ Receive
+
+ )}
+ {walletInfo.methods.includes("pay_invoice") && (
+ setSendDialogOpen(true)}
+ variant="default"
+ >
+
+ Send
+
+ )}
+
+
+ )}
+
+ {/* Transaction History */}
+
+ {walletInfo?.methods.includes("list_transactions") ? (
+ loading ? (
+
+
+
+ ) : txLoadFailed ? (
+
+
+ Failed to load transaction history
+
+
+
+ Retry
+
+
+ ) : transactionsWithMarkers.length === 0 ? (
+
+
+ No transactions found
+
+
+ ) : (
+
{
+ if (item.type === "day-marker") {
+ return (
+
+
+ {item.data}
+
+
+ );
+ }
+
+ const tx = item.data;
+ const txLabel =
+ tx.description ||
+ (tx.type === "incoming" ? "Received" : "Payment");
+
+ return (
+ handleTransactionClick(tx)}
+ >
+
+ {tx.type === "incoming" ? (
+
+ ) : (
+
+ )}
+
{txLabel}
+
+
+
+ {formatSats(tx.amount)}
+
+
+
+ );
+ }}
+ components={{
+ Footer: () =>
+ loadingMore ? (
+
+
+
+ ) : !hasMore && transactions.length > 0 ? (
+
+ No more transactions
+
+ ) : null,
+ }}
+ />
+ )
+ ) : (
+
+
+ Transaction history not available
+
+
+ )}
+
+
+ {/* Disconnect Confirmation Dialog */}
+
+
+
+ Disconnect Wallet?
+
+ This will disconnect your Lightning wallet. You can reconnect at
+ any time.
+
+
+
+ setDisconnectDialogOpen(false)}
+ >
+ Cancel
+
+
+ Disconnect
+
+
+
+
+
+ {/* Transaction Detail Dialog */}
+
+
+
+ Transaction Details
+
+
+ {selectedTransaction && (
+
+
+ {selectedTransaction.type === "incoming" ? (
+
+ ) : (
+
+ )}
+
+
+ {selectedTransaction.type === "incoming"
+ ? "Received"
+ : "Sent"}
+
+
+ {formatSats(selectedTransaction.amount)} sats
+
+
+
+
+
+ {selectedTransaction.description && (
+
+
+ Description
+
+
{selectedTransaction.description}
+
+ )}
+
+
+
Date
+
+ {formatFullDate(selectedTransaction.created_at)}
+
+
+
+ {selectedTransaction.fees_paid !== undefined &&
+ selectedTransaction.fees_paid > 0 && (
+
+
+ Fees Paid
+
+
+ {formatSats(selectedTransaction.fees_paid)} sats
+
+
+ )}
+
+ {selectedTransaction.payment_hash && (
+
+
+ Payment Hash
+
+
+ {selectedTransaction.payment_hash}
+
+
+ )}
+
+ {selectedTransaction.preimage && (
+
+
+ Preimage
+
+
+ {selectedTransaction.preimage}
+
+
+ )}
+
+
+ )}
+
+
+ setDetailDialogOpen(false)}
+ >
+ Close
+
+
+
+
+
+ {/* Send Dialog */}
+
{
+ setSendDialogOpen(open);
+ if (!open) resetSendDialog();
+ }}
+ >
+
+
+ Send Payment
+
+ {sendStep === "input"
+ ? "Pay a Lightning invoice or Lightning address. Amount can be overridden if the invoice allows it."
+ : "Confirm payment details before sending."}
+
+
+
+ {sendStep === "input" ? (
+
+
+
+ Invoice or Lightning Address
+
+ handleInvoiceChange(e.target.value)}
+ className="font-mono text-xs"
+ />
+
+
+
+
+ Amount (sats, optional)
+
+
setSendAmount(e.target.value)}
+ />
+
+ Leave empty for invoices with fixed amounts
+
+
+
+
+ {sending ? (
+ <>
+
+ Resolving...
+ >
+ ) : (
+ "Continue"
+ )}
+
+
+ ) : (
+
+
+
+
Confirm Payment
+
+ {invoiceDetails?.amount && !sendAmount && (
+
+ Amount:
+
+ {Math.floor(invoiceDetails.amount).toLocaleString()}{" "}
+ sats
+
+
+ )}
+ {sendAmount && (
+
+ Amount:
+
+ {parseInt(sendAmount).toLocaleString()} sats
+
+
+ )}
+ {invoiceDetails?.description && (
+
+
+ Description:
+
+
+ {invoiceDetails.description}
+
+
+ )}
+
+
+
+
+
+
+ Back
+
+
+ {sending ? (
+ <>
+
+ Sending...
+ >
+ ) : (
+ <>
+
+ Send Payment
+ >
+ )}
+
+
+
+ )}
+
+
+
+ {/* Receive Dialog */}
+
{
+ setReceiveDialogOpen(open);
+ if (!open) resetReceiveDialog();
+ }}
+ >
+
+
+ Receive Payment
+
+ Generate a Lightning invoice to receive sats.
+ {checkingPayment && " Waiting for payment..."}
+
+
+
+
+ {!generatedInvoice ? (
+ <>
+
+ Amount (sats)
+ setReceiveAmount(e.target.value)}
+ disabled={generating}
+ />
+
+
+
+
+ Description (optional)
+
+ setReceiveDescription(e.target.value)}
+ disabled={generating}
+ />
+
+
+
+ {generating ? (
+ <>
+
+ Generating...
+ >
+ ) : (
+ <>
+
+ Generate Invoice
+ >
+ )}
+
+ >
+ ) : (
+ <>
+
+ {invoiceQR && (
+
+
+ {checkingPayment && (
+
+
+
+ )}
+
+ )}
+
+
+
+
+ {copied ? (
+ <>
+
+ Copied Invoice
+ >
+ ) : (
+ <>
+
+ Copy Invoice
+ >
+ )}
+
+
+
+
+ Invoice (tap to view)
+
+
+ {generatedInvoice}
+
+
+
+
+ Generate Another
+
+
+ >
+ )}
+
+
+
+
+ );
+}
diff --git a/src/components/WindowRenderer.tsx b/src/components/WindowRenderer.tsx
index 6ec02c5..4d4671f 100644
--- a/src/components/WindowRenderer.tsx
+++ b/src/components/WindowRenderer.tsx
@@ -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 =
diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx
index 0075e19..3b04c00 100644
--- a/src/components/nostr/user-menu.tsx
+++ b/src/components/nostr/user-menu.tsx
@@ -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() {
{/* Wallet Info Dialog */}
@@ -295,7 +300,7 @@ export default function UserMenu() {
- {account ? (
+ {account && (
<>
+ >
+ )}
- {/* Wallet Section */}
- {nwcConnection ? (
- setShowWalletInfo(true)}
- >
-
-
- {balance !== undefined ||
- nwcConnection.balance !== undefined ? (
-
- {formatBalance(balance ?? nwcConnection.balance)}
-
- ) : null}
-
-
-
-
- {getWalletName()}
-
-
-
- ) : (
- setShowConnectWallet(true)}
- >
-
- Connect Wallet
-
- )}
+ {/* Wallet Section - Always show */}
+ {nwcConnection ? (
+
+
+
+ {balance !== undefined ||
+ nwcConnection.balance !== undefined ? (
+
+ {formatBalance(balance ?? nwcConnection.balance)}
+
+ ) : null}
+
+
+
+
+ {getWalletName()}
+
+
+
+ ) : (
+ setShowConnectWallet(true)}
+ >
+
+ Connect Wallet
+
+ )}
+ {account && (
+ <>
{relays && relays.length > 0 && (
<>
@@ -398,64 +407,44 @@ export default function UserMenu() {
Log out
-
-
-
-
- Theme
-
-
- {availableThemes.map((theme) => (
- setTheme(theme.id)}
- >
-
- {theme.name}
-
- ))}
-
-
>
- ) : (
+ )}
+
+ {!account && (
<>
+
setShowLogin(true)}>
Log in
-
-
-
-
- Theme
-
-
- {availableThemes.map((theme) => (
- setTheme(theme.id)}
- >
-
- {theme.name}
-
- ))}
-
-
>
)}
+
+ {/* Theme Section - Always show */}
+
+
+
+
+ Theme
+
+
+ {availableThemes.map((theme) => (
+ setTheme(theme.id)}
+ >
+
+ {theme.name}
+
+ ))}
+
+
>
diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts
index 61ac112..07c5bdd 100644
--- a/src/hooks/useWallet.ts
+++ b/src/hooks/useWallet.ts
@@ -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,
};
diff --git a/src/types/app.ts b/src/types/app.ts
index 6630a38..681b678 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -21,6 +21,7 @@ export type AppId =
| "spells"
| "spellbooks"
| "blossom"
+ | "wallet"
| "win";
export interface WindowInstance {
diff --git a/src/types/man.ts b/src/types/man.ts
index ec0fa8b..6493e5e 100644
--- a/src/types/man.ts
+++ b/src/types/man.ts
@@ -785,4 +785,16 @@ export const manPages: Record
= {
},
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: {},
+ },
};