--- name: nostr-wallet-connect description: Use when implementing Nostr Wallet Connect (NIP-47) wallet service - provides complete patterns for generating NWC connection strings, publishing info events, listening for requests, processing wallet commands, and sending encrypted responses. Client-side patterns included for reference. when_to_use: When building wallet services that expose Lightning functionality via NWC, generating NWC connection URIs for clients, implementing NIP-47 wallet service functionality, or handling encrypted wallet communication over Nostr relays --- # Nostr Wallet Connect (NIP-47) - Wallet Service Implementation ## Overview Complete implementation guide for Nostr Wallet Connect (NWC) following NIP-47, **focused on wallet service implementation** (the wallet that exposes functionality to clients). This guide provides production-ready patterns based on real-world implementations. **Core Capabilities (Wallet Service):** - Generate NWC connection strings for clients to connect - Publish info events (kind 13194) with wallet capabilities - Listen for request events (kind 23194) from clients - Handle encrypted request decryption (NIP-04/NIP-44) - Process wallet commands (pay_invoice, get_balance, make_invoice, etc.) - Send encrypted responses (kind 23195) - Manage connection allowances and quotas - Prevent duplicate command processing **Note:** Client-side patterns are included for reference only. The primary focus is implementing the wallet service. ## Prerequisites **IMPORTANT:** Before adding dependencies, review your project's `package.json` to check if any of these packages already exist. If they do, verify the versions are compatible with the requirements below. Only add packages that are missing or need version updates. **Required packages:** - `nostr-tools` - Nostr protocol operations and encryption - Nostr client library (e.g., `@nostr-dev-kit/ndk`, `@nostrify/nostrify`, or `@nostrify/react`) - Lightning node integration or wallet backend **Optional:** - `@getalby/sdk` - For client-side implementations (reference only) ## Implementation Checklist ### Wallet Service Implementation (Primary Focus) - [ ] Generate NWC connection strings with unique keypairs - [ ] Store connection data securely (wallet pubkey, connection secret, allowance) - [ ] Publish info events (kind 13194) with supported methods - [ ] Listen for request events (kind 23194) on specific relays - [ ] Handle encrypted request decryption (NIP-04 or NIP-44) - [ ] Implement command handlers (pay_invoice, get_balance, make_invoice, etc.) - [ ] Send encrypted responses (kind 23195) with proper error handling - [ ] Track processed commands to prevent duplicates - [ ] Implement allowance/quota management - [ ] Add UI for displaying connection strings (QR codes) ### Client Implementation (Reference Only) - [ ] Parse NWC connection strings - [ ] Connect to wallet services - [ ] Send payment requests - [ ] Handle responses and errors ## Part 1: Understanding NWC Architecture ### NWC Connection String Format NWC connection strings follow this format: ``` nostr+walletconnect://[pubkey]?relay=[relay_url]&secret=[32-byte_hex_secret]&lud16=[lightning_address] ``` **Components:** - `pubkey`: 32-byte hex-encoded public key of the wallet service (unique per connection) - `relay`: WebSocket URL of the Nostr relay (may be multiple, space-separated) - `secret`: 32-byte randomly generated hex-encoded secret key for encryption - `lud16`: (Optional) Lightning address for profile setup **Example:** ``` nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c ``` ### Event Kinds - **13194**: Info event (replaceable) - Wallet service capabilities - **23194**: Request event - Client requests to wallet service - **23195**: Response event - Wallet service responses - **23197**: Notification event (NIP-44) - Wallet service notifications - **23196**: Notification event (NIP-04, legacy) - Wallet service notifications ### Encryption - **NIP-04 (widely supported)**: Most common, use for maximum compatibility - **NIP-44 (preferred)**: Modern encryption, use if both client and service support it - Negotiation via `encryption` tag in info and request events ## Part 2: Wallet Service Implementation ### Connection Data Structure **Store connection information securely:** ```typescript // types/nwc.ts export interface NWCConnection { walletPublicKey: string; // Wallet service pubkey (64 hex chars) walletPrivateKey: string; // Wallet service private key (64 hex chars) connectionSecret: string; // Connection secret for encryption (64 hex chars) connectionPublicKey: string; // Connection pubkey derived from secret allowanceLeft: number; // Remaining spending allowance (in msats) } export interface NWCCommand { method: string; params: Record; } export interface NWCResult { result_type: string; result: Record | null; } export interface NWCError { result_type: string; error: { code: string; message: string; }; } // Event kind constants export const NWCKind = { NWCInfo: 13194, NWCRequest: 23194, NWCResponse: 23195, } as const; ``` ### Generating NWC Connection Strings **Generate NWC connection strings for clients to connect:** ```typescript // lib/nwcGenerator.ts import { generateSecretKey, getPublicKey } from 'nostr-tools'; import { bytesToHex } from '@noble/hashes/utils'; // or equivalent export interface NWCConnectionConfig { relay: string | string[]; lud16?: string; } /** * Generate a new NWC connection with unique keypair * @param config - Connection configuration * @returns NWC connection data and connection string */ export function generateNWCConnection(config: NWCConnectionConfig): { connection: NWCConnection; connectionString: string; } { // Generate wallet service keypair (unique per connection) const walletSecret = generateSecretKey(); // Uint8Array const walletPublicKey = getPublicKey(walletSecret); // hex string const walletPrivateKey = bytesToHex(walletSecret); // hex string // Generate connection secret (for encryption with clients) const connectionSecret = generateSecretKey(); const connectionPublicKey = getPublicKey(connectionSecret); const connectionSecretHex = bytesToHex(connectionSecret); // Format relay(s) - can be space-separated const relays = Array.isArray(config.relay) ? config.relay.join(' ') : config.relay; // Build connection string const url = new URL(`nostr+walletconnect://${walletPublicKey}`); url.searchParams.set('relay', relays); url.searchParams.set('secret', connectionSecretHex); if (config.lud16) { url.searchParams.set('lud16', config.lud16); } const connection: NWCConnection = { walletPublicKey, walletPrivateKey, connectionSecret: connectionSecretHex, connectionPublicKey, allowanceLeft: 1000000, // Default 1M sats allowance (adjust as needed) }; return { connection, connectionString: url.toString(), }; } ``` **Usage:** ```typescript const { connection, connectionString } = generateNWCConnection({ relay: ['wss://relay.damus.io', 'wss://relay.nostr.band'], lud16: 'user@getalby.com', }); // Store connection securely storeConnection(connection); // Display connectionString to user (QR code, copy button, etc.) console.log('NWC Connection String:', connectionString); ``` ### Publishing Info Event **Publish replaceable info event (kind 13194) with wallet capabilities:** ```typescript // lib/nwcInfo.ts import type { NostrEvent } from '@nostrify/nostrify'; // or your Nostr library export interface WalletServiceCapabilities { methods: string[]; // e.g., ['pay_invoice', 'get_balance', 'make_invoice'] notifications?: string[]; // e.g., ['payment_received', 'payment_sent'] encryption?: string[]; // e.g., ['nip04', 'nip44_v2'] } /** * Publish NWC info event (kind 13194) * This is a replaceable event - only the latest per pubkey is stored */ export async function publishNWCInfoEvent( walletPubkey: string, walletPrivateKey: string, capabilities: WalletServiceCapabilities, relays: string[], nostrClient: NostrClient // Your Nostr client instance ): Promise { // Check if info event already exists const existingEvents = await nostrClient.query([ { kinds: [13194], authors: [walletPubkey], limit: 1, } ], { relays }); // Only publish if it doesn't exist or capabilities changed if (existingEvents.length > 0) { const existing = existingEvents[0]; const existingMethods = existing.content.split(/\s+/).filter(Boolean); const methodsMatch = JSON.stringify(existingMethods.sort()) === JSON.stringify(capabilities.methods.sort()); if (methodsMatch) { console.log('NWC info event already published with same capabilities'); return; } } // Build event tags const tags: string[][] = []; // Add encryption tag (default to NIP-04 for compatibility) if (capabilities.encryption && capabilities.encryption.length > 0) { tags.push(['encryption', capabilities.encryption.join(' ')]); } else { tags.push(['encryption', 'nip04']); } // Add notifications tag if supported if (capabilities.notifications && capabilities.notifications.length > 0) { tags.push(['notifications', capabilities.notifications.join(' ')]); } // Content is space-separated list of supported methods const content = capabilities.methods.join(' '); // Create and publish event const event = await nostrClient.createEvent({ kind: 13194, content, tags, }, walletPrivateKey); await nostrClient.publish(event, relays); console.log('Published NWC info event:', event.id); } ``` **Usage:** ```typescript await publishNWCInfoEvent( connection.walletPublicKey, connection.walletPrivateKey, { methods: [ 'pay_invoice', 'get_balance', 'make_invoice', 'lookup_invoice', 'list_transactions', 'get_info', ], notifications: ['payment_received', 'payment_sent'], encryption: ['nip04'], // Use NIP-04 for maximum compatibility }, ['wss://relay.damus.io'], nostrClient ); ``` ### Listening for Request Events **Listen for and process NWC request events (kind 23194):** ```typescript // lib/nwcRequestHandler.ts import { nip04 } from 'nostr-tools'; // or nip44 for NIP-44 support import type { NostrEvent } from '@nostrify/nostrify'; export interface NWCRequest { method: string; params: Record; } export interface NWCResponse { result_type: string; error?: { code: string; message: string; } | null; result?: Record | null; } /** * Subscribe to NWC request events and handle them */ export function listenToNWCCommands( connection: NWCConnection, relays: string[], nostrClient: NostrClient, onCommand: (command: NWCCommand, event: NostrEvent) => Promise, seenCommandsUntil: number = 0 // Track last processed command timestamp ): () => void { // Subscribe to request events // Filter: kind 23194, author is connectionPublicKey, p tag is walletPublicKey const subscription = nostrClient.subscribe([ { kinds: [23194], authors: [connection.connectionPublicKey], '#p': [connection.walletPublicKey], since: seenCommandsUntil, // Only get new commands } ], { relays }); subscription.on('event', async (event: NostrEvent) => { try { // Check if NWC is enabled if (!isNWCEnabled()) { console.log('Received NWC command but NWC is disabled'); return; } // Check if we've already processed this command if (event.created_at <= seenCommandsUntil) { return; } // Update seen commands timestamp updateSeenCommandsUntil(event.created_at); console.log('NWC request received:', event.id); // Get client pubkey from p tag const clientPubkeyTag = event.tags.find(([name]) => name === 'p'); if (!clientPubkeyTag?.[1]) { console.error('Request missing client pubkey'); return; } const clientPubkey = clientPubkeyTag[1]; // Determine encryption scheme (default to NIP-04) const encryptionTag = event.tags.find(([name]) => name === 'encryption'); const encryption = encryptionTag?.[1] || 'nip04'; // Decrypt request let command: NWCCommand; try { if (encryption === 'nip44_v2' || encryption === 'nip44') { // NIP-44 decryption (if supported) const { nip44 } = await import('nostr-tools'); const decrypted = await nip44.v2.decrypt( event.content, connection.connectionSecret, clientPubkey ); command = JSON.parse(decrypted); } else { // NIP-04 decryption (default) const decrypted = await nip04.decrypt( connection.connectionSecret, clientPubkey, event.content ); command = JSON.parse(decrypted); } } catch (decryptError) { console.error('Failed to decrypt NWC request:', decryptError); await sendNWCResponse( event, clientPubkey, { result_type: command?.method || 'error', error: { code: 'UNSUPPORTED_ENCRYPTION', message: 'Failed to decrypt request', }, result: null, }, encryption, connection, nostrClient, relays ); return; } console.log('NWC command:', command.method, command.params); // Process command const response = await onCommand(command, event); // Send response await sendNWCResponse( event, clientPubkey, response, encryption, connection, nostrClient, relays ); } catch (error) { console.error('Error handling NWC request:', error); // Send error response const clientPubkeyTag = event.tags.find(([name]) => name === 'p'); const clientPubkey = clientPubkeyTag?.[1] || event.pubkey; const encryptionTag = event.tags.find(([name]) => name === 'encryption'); const encryption = encryptionTag?.[1] || 'nip04'; await sendNWCResponse( event, clientPubkey, { result_type: 'error', error: { code: 'INTERNAL', message: error instanceof Error ? error.message : 'Unknown error', }, result: null, }, encryption, connection, nostrClient, relays ); } }); // Return unsubscribe function return () => { subscription.close(); }; } /** * Send encrypted NWC response (kind 23195) */ async function sendNWCResponse( requestEvent: NostrEvent, clientPubkey: string, response: NWCResponse | NWCError, encryption: string, connection: NWCConnection, nostrClient: NostrClient, relays: string[] ): Promise { // Encrypt response let encryptedContent: string; try { if (encryption === 'nip44_v2' || encryption === 'nip44') { const { nip44 } = await import('nostr-tools'); encryptedContent = await nip44.v2.encrypt( JSON.stringify(response), connection.connectionSecret, clientPubkey ); } else { encryptedContent = await nip04.encrypt( connection.connectionSecret, clientPubkey, JSON.stringify(response) ); } } catch (encryptError) { console.error('Failed to encrypt NWC response:', encryptError); throw encryptError; } // Create response event const responseEvent = await nostrClient.createEvent({ kind: 23195, content: encryptedContent, tags: [ ['p', clientPubkey], ['e', requestEvent.id], ], }, connection.walletPrivateKey); // Publish response await nostrClient.publish(responseEvent, relays); console.log('Sent NWC response:', responseEvent.id); } ``` ### Processing Wallet Commands **Handle common NWC commands with real-world patterns:** ```typescript // lib/nwcCommands.ts import type { NWCCommand, NWCResponse, NWCError, NWCConnection } from './types'; export async function handleNWCCommand( command: NWCCommand, connection: NWCConnection, walletBackend: WalletBackend // Your wallet/lightning backend ): Promise { switch (command.method) { case 'get_info': { return { result_type: 'get_info', result: { alias: 'My Wallet', color: '#FF0000', pubkey: connection.walletPublicKey, network: 'mainnet', block_height: await walletBackend.getBlockHeight(), block_hash: await walletBackend.getBlockHash(), methods: [ 'pay_invoice', 'get_balance', 'make_invoice', 'lookup_invoice', 'list_transactions', 'get_info', ], }, error: null, }; } case 'get_balance': { try { const balance = await walletBackend.getBalance(); // Returns sats return { result_type: 'get_balance', result: { balance: balance * 1000, // Convert to msats }, error: null, }; } catch (error) { return { result_type: 'get_balance', error: { code: 'INTERNAL', message: error instanceof Error ? error.message : 'Failed to get balance', }, result: null, }; } } case 'pay_invoice': { const invoice = command.params.invoice as string; const amountMsat = command.params.amount as number | undefined; // Check allowance if (amountMsat && amountMsat > connection.allowanceLeft) { return { result_type: 'pay_invoice', error: { code: 'QUOTA_EXCEEDED', message: 'Spending quota exceeded', }, result: null, }; } try { // Decode and validate invoice const invoiceData = await walletBackend.decodeInvoice(invoice); // Calculate total amount (invoice amount + fees) const totalAmount = invoiceData.amount + invoiceData.feeReserve; // Check allowance again with fees if (totalAmount > connection.allowanceLeft) { return { result_type: 'pay_invoice', error: { code: 'QUOTA_EXCEEDED', message: 'Spending quota exceeded', }, result: null, }; } // Pay invoice const paymentResult = await walletBackend.payInvoice(invoice); // Deduct from allowance connection.allowanceLeft -= totalAmount; updateConnection(connection); return { result_type: 'pay_invoice', result: { preimage: paymentResult.preimage, // fees_paid can be included if available }, error: null, }; } catch (error) { return { result_type: 'pay_invoice', error: { code: 'PAYMENT_FAILED', message: error instanceof Error ? error.message : 'Payment failed', }, result: null, }; } } case 'make_invoice': { const amount = command.params.amount as number; // msats const description = command.params.description as string | undefined; const expiry = command.params.expiry as number | undefined; // seconds try { const invoice = await walletBackend.createInvoice({ amount: amount / 1000, // Convert msats to sats description, expiry, }); return { result_type: 'make_invoice', result: { type: 'incoming', invoice: invoice.bolt11, description: invoice.description, payment_hash: invoice.paymentHash, amount: amount, created_at: Math.floor(Date.now() / 1000), expires_at: expiry ? Math.floor(Date.now() / 1000) + expiry : null, }, error: null, }; } catch (error) { return { result_type: 'make_invoice', error: { code: 'INTERNAL', message: error instanceof Error ? error.message : 'Failed to create invoice', }, result: null, }; } } case 'list_transactions': { const from = command.params.from as number | undefined; const until = command.params.until as number | undefined; const limit = (command.params.limit as number) || 10; const offset = (command.params.offset as number) || 0; const unpaid = command.params.unpaid as boolean | undefined; const type = command.params.type as 'incoming' | 'outgoing' | undefined; try { const transactions = await walletBackend.listTransactions({ from, until: until || Math.floor(Date.now() / 1000), limit, offset, unpaid, type, }); // Transform to NWC format const nwcTransactions = transactions.map(tx => ({ type: tx.amount > 0 ? 'incoming' : 'outgoing', invoice: tx.bolt11, description: tx.description, preimage: tx.preimage, payment_hash: tx.paymentHash, amount: Math.abs(tx.amount) * 1000, // Convert to msats fees_paid: tx.feesPaid || 0, created_at: tx.createdAt, settled_at: tx.settledAt, expires_at: tx.expiresAt, })); // Sort by created_at descending (newest first) nwcTransactions.sort((a, b) => b.created_at - a.created_at); return { result_type: 'list_transactions', result: { transactions: nwcTransactions, }, error: null, }; } catch (error) { return { result_type: 'list_transactions', error: { code: 'INTERNAL', message: error instanceof Error ? error.message : 'Failed to list transactions', }, result: null, }; } } case 'lookup_invoice': { const paymentHash = command.params.payment_hash as string | undefined; const invoice = command.params.invoice as string | undefined; if (!paymentHash && !invoice) { return { result_type: 'lookup_invoice', error: { code: 'OTHER', message: 'invoice or payment_hash required', }, result: null, }; } try { let hash = paymentHash; if (!hash && invoice) { const decoded = await walletBackend.decodeInvoice(invoice); hash = decoded.paymentHash; } const tx = await walletBackend.lookupTransaction(hash!); if (!tx) { return { result_type: 'lookup_invoice', error: { code: 'NOT_FOUND', message: 'invoice not found', }, result: null, }; } return { result_type: 'lookup_invoice', result: { type: tx.amount > 0 ? 'incoming' : 'outgoing', invoice: tx.bolt11, description: tx.description, preimage: tx.preimage, payment_hash: tx.paymentHash, amount: Math.abs(tx.amount) * 1000, fees_paid: tx.feesPaid || 0, created_at: tx.createdAt, settled_at: tx.settledAt, expires_at: tx.expiresAt, }, error: null, }; } catch (error) { return { result_type: 'lookup_invoice', error: { code: 'NOT_FOUND', message: 'invoice not found', }, result: null, }; } } default: return { result_type: command.method, error: { code: 'NOT_IMPLEMENTED', message: `Method ${command.method} is not implemented`, }, result: null, }; } } ``` ### Complete Wallet Service Integration **Putting it all together:** ```typescript // stores/nwcStore.ts (example using Pinia/Vue, adapt to your framework) import { defineStore } from 'pinia'; import { generateNWCConnection } from '@/lib/nwcGenerator'; import { publishNWCInfoEvent } from '@/lib/nwcInfo'; import { listenToNWCCommands } from '@/lib/nwcRequestHandler'; import { handleNWCCommand } from '@/lib/nwcCommands'; import type { NWCConnection } from '@/types/nwc'; export const useNWCStore = defineStore('nwc', { state: () => ({ nwcEnabled: false, connections: [] as NWCConnection[], seenCommandsUntil: 0, subscriptions: [] as Array<() => void>, }), actions: { async generateNWCConnection(relays: string[], lud16?: string) { // Generate new connection const { connection, connectionString } = generateNWCConnection({ relay: relays, lud16, }); // Store connection this.connections.push(connection); // Publish info event await publishNWCInfoEvent( connection.walletPublicKey, connection.walletPrivateKey, { methods: [ 'pay_invoice', 'get_balance', 'make_invoice', 'lookup_invoice', 'list_transactions', 'get_info', ], encryption: ['nip04'], }, relays, nostrClient ); return { connection, connectionString }; }, async listenToNWCCommands() { if (!this.nwcEnabled || this.connections.length === 0) { return; } // Close existing subscriptions this.unsubscribeNWC(); // For each connection, start listening for (const connection of this.connections) { const unsubscribe = listenToNWCCommands( connection, relays, // Your relay URLs nostrClient, async (command, event) => { // Handle blocking for pay_invoice to prevent concurrent payments if (command.method === 'pay_invoice' && this.blocking) { return { result_type: 'pay_invoice', error: { code: 'INTERNAL', message: 'Already processing a payment', }, result: null, }; } if (command.method === 'pay_invoice') { this.blocking = true; } try { const response = await handleNWCCommand( command, connection, walletBackend ); return response; } finally { if (command.method === 'pay_invoice') { this.blocking = false; } } }, this.seenCommandsUntil ); this.subscriptions.push(unsubscribe); } }, unsubscribeNWC() { for (const unsubscribe of this.subscriptions) { unsubscribe(); } this.subscriptions = []; }, getConnectionString(connection: NWCConnection, relays: string[]): string { const url = new URL(`nostr+walletconnect://${connection.walletPublicKey}`); url.searchParams.set('relay', relays.join(' ')); url.searchParams.set('secret', connection.connectionSecret); return url.toString(); }, }, }); ``` ## Part 3: UI Components (Wallet Service) ### NWC Connection String Display **Display connection string with QR code:** ```typescript // components/NWCDialog.tsx import { useState } from 'react'; import { QRCode } from '@/components/QRCode'; // From qr-code-generator skill import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { useNWCStore } from '@/stores/nwcStore'; export function NWCDialog({ connection, connectionString, open, onOpenChange }: { connection: NWCConnection; connectionString: string; open: boolean; onOpenChange: (open: boolean) => void; }) { const handleCopy = async () => { await navigator.clipboard.writeText(connectionString); // Optional: User feedback - choose one: // Option 1: Console logging // console.log('Copied to clipboard'); // Option 2: Toast notification (if toast is available) // toast({ title: 'Copied', description: 'Connection string copied to clipboard' }); // Option 3: No notification (silent success) }; return ( NWC Connection String

Allowance: {connection.allowanceLeft / 1000} sats

); } ``` ## Part 4: Error Handling ### NWC Error Codes **Handle NWC error codes according to NIP-47:** ```typescript // lib/nwcErrors.ts export const NWC_ERROR_CODES = { RATE_LIMITED: 'RATE_LIMITED', NOT_IMPLEMENTED: 'NOT_IMPLEMENTED', INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE', QUOTA_EXCEEDED: 'QUOTA_EXCEEDED', RESTRICTED: 'RESTRICTED', UNAUTHORIZED: 'UNAUTHORIZED', INTERNAL: 'INTERNAL', UNSUPPORTED_ENCRYPTION: 'UNSUPPORTED_ENCRYPTION', PAYMENT_FAILED: 'PAYMENT_FAILED', NOT_FOUND: 'NOT_FOUND', OTHER: 'OTHER', } as const; export function getErrorMessage(code: string): string { const messages: Record = { RATE_LIMITED: 'Too many requests. Please wait a moment and try again.', NOT_IMPLEMENTED: 'This feature is not supported by the wallet.', INSUFFICIENT_BALANCE: 'Insufficient balance in wallet.', QUOTA_EXCEEDED: 'Spending quota exceeded.', RESTRICTED: 'This operation is not allowed.', UNAUTHORIZED: 'Wallet connection not authorized.', INTERNAL: 'An internal error occurred.', UNSUPPORTED_ENCRYPTION: 'Encryption method not supported.', PAYMENT_FAILED: 'Payment failed. Please try again.', NOT_FOUND: 'Requested item not found.', OTHER: 'An unknown error occurred.', }; return messages[code] || messages.OTHER; } ``` ## Part 5: Security Considerations ### Best Practices 1. **Never log secrets**: Connection strings contain private keys 2. **Validate all inputs**: Always validate NWC connection strings and command parameters 3. **Use NIP-04 for compatibility**: NIP-04 is widely supported; use NIP-44 only if both sides support it 4. **Unique keys per connection**: Generate unique keypairs for each connection 5. **Store securely**: Use secure storage for connection data (localStorage is acceptable for browser apps) 6. **Track processed commands**: Use `seenCommandsUntil` to prevent duplicate processing 7. **Implement blocking**: Prevent concurrent payment processing 8. **Allowance management**: Track and enforce spending limits per connection 9. **Error messages**: Don't expose sensitive information in error messages ### Connection String Security ```typescript // Never log full connection strings function logConnection(connection: NWCConnection) { // ❌ Bad console.log('Connection:', connection.connectionString); // ✅ Good console.log('Connection:', { pubkey: connection.walletPublicKey.substring(0, 8) + '...', allowanceLeft: connection.allowanceLeft, }); } ``` ## Part 6: Client Implementation (Reference Only) ### NWC Connection String Parsing **For reference - how clients parse connection strings:** ```typescript // lib/nwcParser.ts (CLIENT SIDE - REFERENCE ONLY) export interface ParsedNWC { pubkey: string; relay: string[]; secret: string; lud16?: string; } export function parseNWCConnectionString(uri: string): ParsedNWC | null { try { const normalized = uri.replace(/^nostrwalletconnect:\/\//, 'nostr+walletconnect://'); if (!normalized.startsWith('nostr+walletconnect://')) { return null; } const url = new URL(normalized); const pubkey = url.hostname; if (!/^[0-9a-f]{64}$/i.test(pubkey)) { return null; } const relay = url.searchParams.get('relay'); const secret = url.searchParams.get('secret'); const lud16 = url.searchParams.get('lud16') || undefined; if (!relay || !secret) { return null; } if (!/^[0-9a-f]{64}$/i.test(secret)) { return null; } const relays = relay.split(/\s+/).filter(Boolean); return { pubkey, relay: relays, secret, lud16 }; } catch (error) { return null; } } ``` ## Part 7: Troubleshooting ### Common Issues **Connection fails:** - Verify relay URL is correct and accessible - Check that connection string format is valid - Ensure wallet service has published info event (kind 13194) **Commands not received:** - Verify subscription filter matches connection pubkeys - Check that `since` parameter is not filtering out events - Ensure relays are connected and receiving events **Encryption errors:** - Ensure both client and service support same encryption scheme - Check that secret key is correct (64 hex characters) - Verify pubkey matches between connection string and info event **Duplicate command processing:** - Implement `seenCommandsUntil` tracking - Check event `created_at` before processing - Update timestamp after successful processing ## References - **NIP-47**: https://github.com/nostr-protocol/nips/blob/master/47.md - **NIP-44**: https://github.com/nostr-protocol/nips/blob/master/44.md - **NIP-04**: https://github.com/nostr-protocol/nips/blob/master/04.md - **cashu.me NWC implementation**: Reference implementation in Vue/Pinia