Claude Code Plugins

Community-maintained marketplace

Feedback

nostr-wallet-connect

@PerceptLabs/mvmnt
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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:

// 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<string, unknown>;
}

export interface NWCResult {
  result_type: string;
  result: Record<string, unknown> | 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:

// 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:

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:

// 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<void> {
  // 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:

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):

// 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<string, unknown>;
}

export interface NWCResponse {
  result_type: string;
  error?: {
    code: string;
    message: string;
  } | null;
  result?: Record<string, unknown> | null;
}

/**
 * Subscribe to NWC request events and handle them
 */
export function listenToNWCCommands(
  connection: NWCConnection,
  relays: string[],
  nostrClient: NostrClient,
  onCommand: (command: NWCCommand, event: NostrEvent) => Promise<NWCResponse | NWCError>,
  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<void> {
  // 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:

// 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<NWCResponse | NWCError> {
  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:

// 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:

// 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 (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>NWC Connection String</DialogTitle>
        </DialogHeader>
        <div className="space-y-4">
          <div className="flex justify-center p-4 bg-white rounded">
            <QRCode value={connectionString} size={256} />
          </div>
          <div>
            <Input 
              value={connectionString} 
              readOnly 
              className="font-mono text-xs"
            />
          </div>
          <div className="flex gap-2">
            <Button onClick={handleCopy} className="flex-1">
              Copy
            </Button>
            <Button variant="outline" onClick={() => onOpenChange(false)}>
              Close
            </Button>
          </div>
          <div className="text-sm text-muted-foreground">
            <p>Allowance: {connection.allowanceLeft / 1000} sats</p>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  );
}

Part 4: Error Handling

NWC Error Codes

Handle NWC error codes according to NIP-47:

// 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<string, string> = {
    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

// 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:

// 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