diff --git a/package-lock.json b/package-lock.json
index 61b8536..05e8e20 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,6 +42,7 @@
"applesauce-relay": "^5.0.0",
"applesauce-signers": "^5.0.0",
"applesauce-wallet": "^5.0.0",
+ "applesauce-wallet-connect": "^5.0.1",
"blossom-client-sdk": "^4.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -5747,6 +5748,22 @@
"url": "lightning:nostrudel@geyser.fund"
}
},
+ "node_modules/applesauce-wallet-connect": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/applesauce-wallet-connect/-/applesauce-wallet-connect-5.0.1.tgz",
+ "integrity": "sha512-k/Gl2IIjfQelW4deN/0M9/I3uznUMZalGAP9/wPgwmAtUyaEHb8YJpOdxqLwCQ98vZMTAcwgK6hmXkAPqA6NTg==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.7.1",
+ "applesauce-common": "^5.0.0",
+ "applesauce-core": "^5.0.0",
+ "rxjs": "^7.8.1"
+ },
+ "funding": {
+ "type": "lightning",
+ "url": "lightning:nostrudel@geyser.fund"
+ }
+ },
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
diff --git a/package.json b/package.json
index 697c812..160f261 100644
--- a/package.json
+++ b/package.json
@@ -50,6 +50,7 @@
"applesauce-relay": "^5.0.0",
"applesauce-signers": "^5.0.0",
"applesauce-wallet": "^5.0.0",
+ "applesauce-wallet-connect": "^5.0.1",
"blossom-client-sdk": "^4.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/src/components/ConnectWalletDialog.tsx b/src/components/ConnectWalletDialog.tsx
index b916fe7..084bcb9 100644
--- a/src/components/ConnectWalletDialog.tsx
+++ b/src/components/ConnectWalletDialog.tsx
@@ -11,7 +11,7 @@ import {
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useGrimoire } from "@/core/state";
-import { parseNWCUri, NWCClient } from "@/services/nwc-client";
+import { createWalletFromURI } from "@/services/nwc";
interface ConnectWalletDialogProps {
open: boolean;
@@ -53,27 +53,31 @@ export default function ConnectWalletDialog({
setError(null);
try {
- // Parse the connection URI
- const connection = parseNWCUri(connectionString);
-
- // Create NWC client
- const client = new NWCClient(connection);
+ // Create wallet instance from connection string
+ const wallet = createWalletFromURI(connectionString);
// Test the connection by getting wallet info
- const info = await client.getInfo();
+ const info = await wallet.getInfo();
// Get initial balance
let balance: number | undefined;
try {
- balance = await client.getBalance();
+ const balanceResult = await wallet.getBalance();
+ balance = balanceResult.balance;
} catch (err) {
console.warn("[NWC] Failed to get balance:", err);
// Balance is optional, continue anyway
}
+ // Get connection details from the wallet instance
+ const serialized = wallet.toJSON();
+
// Save connection to state
setNWCConnection({
- ...connection,
+ service: serialized.service,
+ relays: serialized.relays,
+ secret: serialized.secret,
+ lud16: serialized.lud16,
balance,
info: {
alias: info.alias,
diff --git a/src/components/WalletButton.tsx b/src/components/WalletButton.tsx
new file mode 100644
index 0000000..06b82d3
--- /dev/null
+++ b/src/components/WalletButton.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+import { Wallet, Zap } from "lucide-react";
+import { useGrimoire } from "@/core/state";
+import { Button } from "@/components/ui/button";
+import ConnectWalletDialog from "@/components/ConnectWalletDialog";
+
+export default function WalletButton() {
+ const { state } = useGrimoire();
+ const nwcConnection = state.nwcConnection;
+ const [showConnectWallet, setShowConnectWallet] = useState(false);
+
+ function formatBalance(millisats?: number): string {
+ if (millisats === undefined) return "—";
+ const sats = Math.floor(millisats / 1000);
+ return sats.toLocaleString();
+ }
+
+ return (
+ <>
+
+
+ {nwcConnection ? (
+
+ ) : (
+
+ )}
+ >
+ );
+}
diff --git a/src/components/layouts/AppShell.tsx b/src/components/layouts/AppShell.tsx
index 3f82958..9b10dd0 100644
--- a/src/components/layouts/AppShell.tsx
+++ b/src/components/layouts/AppShell.tsx
@@ -10,6 +10,7 @@ import CommandLauncher from "../CommandLauncher";
import { GlobalAuthPrompt } from "../GlobalAuthPrompt";
import { SpellbookDropdown } from "../SpellbookDropdown";
import UserMenu from "../nostr/user-menu";
+import WalletButton from "../WalletButton";
import { AppShellContext } from "./AppShellContext";
interface AppShellProps {
@@ -76,7 +77,10 @@ export function AppShell({ children, hideBottomBar = false }: AppShellProps) {
-
+
+
+
+
{children}
diff --git a/src/components/nostr/user-menu.tsx b/src/components/nostr/user-menu.tsx
index 00fe4eb..4bb555e 100644
--- a/src/components/nostr/user-menu.tsx
+++ b/src/components/nostr/user-menu.tsx
@@ -1,4 +1,4 @@
-import { User, HardDrive, Palette, Wallet, Zap } from "lucide-react";
+import { User, HardDrive, Palette } from "lucide-react";
import accounts from "@/services/accounts";
import { useProfile } from "@/hooks/useProfile";
import { use$ } from "applesauce-react/hooks";
@@ -22,7 +22,6 @@ import Nip05 from "./nip05";
import { RelayLink } from "./RelayLink";
import SettingsDialog from "@/components/SettingsDialog";
import LoginDialog from "./LoginDialog";
-import ConnectWalletDialog from "@/components/ConnectWalletDialog";
import { useState } from "react";
import { useTheme } from "@/lib/themes";
@@ -57,13 +56,11 @@ function UserLabel({ pubkey }: { pubkey: string }) {
export default function UserMenu() {
const account = use$(accounts.active$);
- const { state, addWindow, disconnectNWC } = useGrimoire();
+ const { state, addWindow } = useGrimoire();
const relays = state.activeAccount?.relays;
const blossomServers = state.activeAccount?.blossomServers;
- const nwcConnection = state.nwcConnection;
const [showSettings, setShowSettings] = useState(false);
const [showLogin, setShowLogin] = useState(false);
- const [showConnectWallet, setShowConnectWallet] = useState(false);
const { themeId, setTheme, availableThemes } = useTheme();
function openProfile() {
@@ -80,48 +77,10 @@ export default function UserMenu() {
accounts.removeAccount(account);
}
- function handleDisconnectWallet() {
- disconnectNWC();
- }
-
- function formatBalance(millisats?: number): string {
- if (millisats === undefined) return "—";
- const sats = Math.floor(millisats / 1000);
- return sats.toLocaleString();
- }
-
return (
<>
-
-
- {/* Wallet Connection Button */}
- {nwcConnection ? (
-
- ) : (
-
- )}
-
)}
- {nwcConnection && (
- <>
-
-
-
-
- Wallet
-
-
-
- Balance:
-
- {formatBalance(nwcConnection.balance)} sats
-
-
- {nwcConnection.info?.alias && (
-
- Wallet:
-
- {nwcConnection.info.alias}
-
-
- )}
-
-
- Disconnect Wallet
-
-
- >
- )}
-
Log out
diff --git a/src/services/nwc-client.ts b/src/services/nwc-client.ts
deleted file mode 100644
index 7528edd..0000000
--- a/src/services/nwc-client.ts
+++ /dev/null
@@ -1,269 +0,0 @@
-/**
- * Nostr Wallet Connect (NIP-47) Client
- *
- * Implements the client side of NIP-47 protocol for connecting to remote Lightning wallets.
- * Uses NIP-04 encryption for secure communication over Nostr relays.
- *
- * @see https://github.com/nostr-protocol/nips/blob/master/47.md
- */
-
-import { finalizeEvent, getPublicKey } from "nostr-tools";
-import { nip04 } from "nostr-tools";
-import pool from "./relay-pool";
-import type { NWCConnection } from "@/types/app";
-
-/**
- * NIP-47 request/response types
- */
-export interface NWCRequest {
- method: string;
- params?: Record;
-}
-
-export interface NWCResponse {
- result_type: string;
- result?: any;
- error?: {
- code: string;
- message: string;
- };
-}
-
-/**
- * Parses a nostr+walletconnect:// URI into connection details
- * Format: nostr+walletconnect://[pubkey]?relay=[url]&secret=[hex]&lud16=[optional]
- */
-export function parseNWCUri(uri: string): {
- walletPubkey: string;
- relays: string[];
- secret: string;
-} {
- // Remove protocol prefix
- const withoutProtocol = uri.replace(/^nostr\+walletconnect:\/\//i, "");
-
- // Split pubkey from query params
- const [pubkey, queryString] = withoutProtocol.split("?");
-
- if (!pubkey || !queryString) {
- throw new Error("Invalid NWC URI format");
- }
-
- // Parse query parameters
- const params = new URLSearchParams(queryString);
- const relay = params.get("relay");
- const secret = params.get("secret");
-
- if (!relay || !secret) {
- throw new Error("Missing required parameters: relay and secret");
- }
-
- // Normalize relay URL
- const relayUrl = relay.startsWith("wss://")
- ? relay
- : relay.startsWith("ws://")
- ? relay
- : `wss://${relay}`;
-
- return {
- walletPubkey: pubkey,
- relays: [relayUrl],
- secret,
- };
-}
-
-/**
- * NWC Client Class
- * Manages encrypted communication with a NWC wallet service
- */
-export class NWCClient {
- private walletPubkey: string;
- private relays: string[];
- private secret: Uint8Array;
- private clientSecretKey: Uint8Array;
- private clientPubkey: string;
-
- constructor(connection: NWCConnection) {
- this.walletPubkey = connection.walletPubkey;
- this.relays = connection.relays;
- this.secret = this.hexToBytes(connection.secret);
-
- // Derive client keypair from secret
- this.clientSecretKey = this.secret;
- this.clientPubkey = getPublicKey(this.clientSecretKey);
- }
-
- /**
- * Sends a command to the wallet and waits for response
- */
- async sendRequest(
- method: string,
- params?: Record,
- ): Promise {
- const request: NWCRequest = {
- method,
- params,
- };
-
- // Encrypt the request using NIP-04
- const encryptedContent = await nip04.encrypt(
- this.clientSecretKey,
- this.walletPubkey,
- JSON.stringify(request),
- );
-
- // Create kind 23194 event (client request)
- const requestEvent = finalizeEvent(
- {
- kind: 23194,
- created_at: Math.floor(Date.now() / 1000),
- tags: [["p", this.walletPubkey]],
- content: encryptedContent,
- },
- this.clientSecretKey,
- );
-
- // Publish request to relays
- await pool.publish(this.relays, requestEvent);
-
- // Wait for response (kind 23195)
- return new Promise((resolve, reject) => {
- const timeout = setTimeout(() => {
- subscription.unsubscribe();
- reject(new Error("Request timeout"));
- }, 30000); // 30 second timeout
-
- // Capture client keys for use in subscription
- const clientSecretKey = this.clientSecretKey;
- const walletPubkey = this.walletPubkey;
-
- const observable = pool.subscription(this.relays, [
- {
- kinds: [23195],
- authors: [this.walletPubkey],
- "#p": [this.clientPubkey],
- since: requestEvent.created_at,
- },
- ]);
-
- const subscription = observable.subscribe({
- next: async (eventOrEose) => {
- // Skip EOSE markers
- if (typeof eventOrEose === "string") {
- return;
- }
-
- try {
- // Decrypt response
- const decryptedContent = await nip04.decrypt(
- clientSecretKey,
- walletPubkey,
- eventOrEose.content,
- );
- const response = JSON.parse(decryptedContent) as NWCResponse;
-
- clearTimeout(timeout);
- subscription.unsubscribe();
- resolve(response);
- } catch (error) {
- console.error("[NWC] Failed to decrypt response:", error);
- }
- },
- error: (err) => {
- clearTimeout(timeout);
- reject(err);
- },
- });
- });
- }
-
- /**
- * Get wallet info (capabilities, alias, etc.)
- */
- async getInfo(): Promise<{
- alias?: string;
- color?: string;
- pubkey?: string;
- network?: string;
- block_height?: number;
- block_hash?: string;
- methods: string[];
- notifications?: string[];
- }> {
- const response = await this.sendRequest("get_info");
-
- if (response.error) {
- throw new Error(response.error.message);
- }
-
- return response.result;
- }
-
- /**
- * Get current wallet balance in millisatoshis
- */
- async getBalance(): Promise {
- const response = await this.sendRequest("get_balance");
-
- if (response.error) {
- throw new Error(response.error.message);
- }
-
- return response.result.balance;
- }
-
- /**
- * Pay a BOLT11 invoice
- */
- async payInvoice(invoice: string): Promise<{
- preimage: string;
- }> {
- const response = await this.sendRequest("pay_invoice", { invoice });
-
- if (response.error) {
- throw new Error(response.error.message);
- }
-
- return response.result;
- }
-
- /**
- * Make a new invoice
- */
- async makeInvoice(params: {
- amount: number;
- description?: string;
- description_hash?: string;
- expiry?: number;
- }): Promise<{
- type: string;
- invoice: string;
- description?: string;
- description_hash?: string;
- preimage?: string;
- payment_hash: string;
- amount: number;
- fees_paid: number;
- created_at: number;
- expires_at?: number;
- metadata?: Record;
- }> {
- const response = await this.sendRequest("make_invoice", params);
-
- if (response.error) {
- throw new Error(response.error.message);
- }
-
- return response.result;
- }
-
- /**
- * Helper: Convert hex string to Uint8Array
- */
- private hexToBytes(hex: string): Uint8Array {
- const bytes = new Uint8Array(hex.length / 2);
- for (let i = 0; i < hex.length; i += 2) {
- bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
- }
- return bytes;
- }
-}
diff --git a/src/services/nwc.ts b/src/services/nwc.ts
new file mode 100644
index 0000000..9b25f31
--- /dev/null
+++ b/src/services/nwc.ts
@@ -0,0 +1,43 @@
+/**
+ * NWC (Nostr Wallet Connect) Service
+ *
+ * Provides a singleton WalletConnect instance for the application using
+ * applesauce-wallet-connect for NIP-47 Lightning wallet integration.
+ */
+
+import { WalletConnect } from "applesauce-wallet-connect";
+import pool from "./relay-pool";
+
+// Set the pool for wallet connect to use
+WalletConnect.pool = pool;
+
+let walletInstance: WalletConnect | null = null;
+
+/**
+ * Creates a new WalletConnect instance from a connection string
+ */
+export function createWalletFromURI(connectionString: string): WalletConnect {
+ walletInstance = WalletConnect.fromConnectURI(connectionString);
+ return walletInstance;
+}
+
+/**
+ * Gets the current wallet instance
+ */
+export function getWallet(): WalletConnect | null {
+ return walletInstance;
+}
+
+/**
+ * Clears the current wallet instance
+ */
+export function clearWallet(): void {
+ walletInstance = null;
+}
+
+/**
+ * Sets the wallet instance (used for restoring from saved state)
+ */
+export function setWallet(wallet: WalletConnect): void {
+ walletInstance = wallet;
+}
diff --git a/src/types/app.ts b/src/types/app.ts
index 0101e17..6630a38 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -84,11 +84,13 @@ export interface RelayInfo {
*/
export interface NWCConnection {
/** The wallet service's public key */
- walletPubkey: string;
+ service: string;
/** Relay URL(s) for communication */
relays: string[];
- /** Shared secret for encryption (32-byte hex) */
+ /** Shared secret for encryption */
secret: string;
+ /** Optional lightning address (lud16) */
+ lud16?: string;
/** Optional cached balance in millisats */
balance?: number;
/** Optional wallet info */