Nostr Wallet Connect (NWC) is a protocol that allows Nostr clients to connect to and control external wallets. This integration enhances user experience by enabling them to manage their funds and sign transactions securely without leaving your app. If you're developing a Lightning wallet and want to support NWC, this guide will walk you through the integration process, covering everything from basic concepts to implementation details.

Understanding Nostr Wallet Connect

Nostr Wallet Connect leverages the Nostr protocol to create a secure communication channel between Lightning wallets and applications. For wallet developers, implementing NWC means your users can:
  • Connect securely to compatible applications
  • Authorise payment requests
  • Manage spending limits
  • Create invoices through connected applications
  • Maintain persistent connections with multiple apps

What does NWC offer wallet developers?

NWC facilitates communication between Nostr clients and wallets using a simple URI scheme. This allows for requests and responses to be passed securely and efficiently. The goal is to provide a seamless and secure way for users to interact with their wallets within the Nostr ecosystem. In order to support NWC your wallet will need to execute the following:
  • Set Up the NWC URI Handling: Your application needs to recognise and handle NWC URIs. These URIs typically start with `nostr+walletconnect://`. You'll need to implement a mechanism to parse these URIs and extract the relevant information, such as the relay URL and secret.
  • Establish a Connection: Once you have the URI details, your app will need to establish a WebSocket connection to the specified relay. This connection will be used to send requests to and receive responses from the connected wallet.
  • Implement Request Handling: Your app should be able to construct and send NWC requests, such as requests for balances, payments, or signing events. These requests need to be formatted according to the NWC protocol.
  • Process Wallet Responses: After sending a request, your app will receive a response from the wallet. You need to implement logic to parse these responses and handle them appropriately. This might involve displaying balances, confirming transactions, or handling errors.
  • Secure the Connection: Security is paramount when dealing with wallets. Ensure that the connection to the relay is secure (using WSS) and that the secret is handled securely within your application. Avoid exposing the secret to users or storing it insecurely.

Prerequisites for Implementation

Before beginning the integration process, you should have:
  • An existing Lightning wallet implementation
  • Basic understanding of the Nostr protocol
  • Knowledge of relays and event types in Nostr
  • Familiarity with cryptographic signing
  • A development environment for your wallet

The Architecture of NWC for Wallets

  • At a high level, implementing NWC in your wallet involves:
  • Generating a unique keypair for each application connection
  • Creating and managing NWC connection strings
  • Listening for requests on Nostr relays
  • Processing and responding to various request types
  • Managing user authorisation and permissions

1. Creating the Connection Infrastructure

First, set up the Nostr event handler in your wallet codebase: import { generatePrivateKey, getPublicKey, relayInit, finishEvent } from 'nostr-tools'; class NostrWalletConnectManager { constructor(relayUrls = ['wss://relay.damus.io', 'wss://nostr.fmt.wiz.biz']) { this.relays = relayUrls.map(url => relayInit(url)); this.connections = new Map(); // Store active connections this.initializeRelays(); } async initializeRelays() { for (const relay of this.relays) { await relay.connect(); // Set up event listeners relay.on('connect', () => { console.log(`Connected to ${relay.url}`); }); relay.on('error', () => { console.error(`Failed to connect to ${relay.url}`); }); } } // Additional methods will be added here }

2. Generating NWC Connection Strings

When a user wants to connect an application, your wallet needs to generate a connection string: generateConnectionString(appInfo, permissions) { const privateKey = generatePrivateKey(); const publicKey = getPublicKey(privateKey); // Create a unique connection ID const connectionId = crypto.randomUUID(); // Store connection details this.connections.set(connectionId, { privateKey, publicKey, appInfo, permissions, createdAt: Date.now() }); // Create the NWC URI const relayUrl = encodeURIComponent(this.relays[0].url); const secret = privateKey; return `nostr+walletconnect://${relayUrl}?secret=${secret}&required_commands=pay,make_invoice`; }

3. Implementing Request Listeners

Your wallet needs to listen for Nostr events from connected applications: subscribeToRequests(connectionId) { const connection = this.connections.get(connectionId); if (!connection) return; for (const relay of this.relays) { // Subscribe to events addressed to this connection's public key const subscription = relay.sub([ { kinds: [23194], // NWC request kind "#p": [connection.publicKey] } ]); subscription.on('event', event => { this.processRequest(connectionId, event); }); // Store subscription reference for cleanup connection.subscriptions = connection.subscriptions || []; connection.subscriptions.push(subscription); } }

4. Processing NWC Requests

The heart of NWC is processing different request types: async processRequest(connectionId, event) { const connection = this.connections.get(connectionId); if (!connection) return; try { // Parse the request content const content = JSON.parse(event.content); const { method, params, id } = content; // Check permissions if (!this.isMethodAllowed(connection, method)) { await this.sendErrorResponse(connection, id, "Permission denied"); return; } // Process based on method type switch (method) { case 'pay': await this.handlePayRequest(connection, params, id); break; case 'make_invoice': await this.handleInvoiceRequest(connection, params, id); break; case 'get_info': await this.handleGetInfoRequest(connection, id); break; default: await this.sendErrorResponse(connection, id, "Method not supported"); } } catch (error) { console.error("Error processing request:", error); } }

5. Implementing Request Handlers

For each request type, implement specific handlers: async handlePayRequest(connection, params, requestId) { const { invoice, amount, comment } = params; // Present payment request to user const userApproved = await this.requestUserApproval({ type: 'payment', appName: connection.appInfo.name, amount, invoice, comment }); if (userApproved) { try { // Process payment using your wallet's payment infrastructure const paymentResult = await this.walletService.sendPayment(invoice); // Send success response await this.sendResponse(connection, requestId, { preimage: paymentResult.preimage, paymentHash: paymentResult.paymentHash }); } catch (error) { await this.sendErrorResponse(connection, requestId, error.message); } } else { await this.sendErrorResponse(connection, requestId, "Payment rejected by user"); } } async handleInvoiceRequest(connection, params, requestId) { const { amount, description, expiry } = params; try { // Generate invoice using your wallet's invoice creation logic const invoice = await this.walletService.createInvoice({ amount, description, expiry: expiry || 3600 // Default 1 hour }); // Send response with invoice await this.sendResponse(connection, requestId, { invoice: invoice.paymentRequest, paymentHash: invoice.paymentHash }); } catch (error) { await this.sendErrorResponse(connection, requestId, error.message); } }

6. Sending Responses

Create methods to send responses back to applications: async sendResponse(connection, requestId, result) { const responseEvent = { kind: 23195, // NWC response kind created_at: Math.floor(Date.now() / 1000), tags: [ ["p", connection.publicKey], ["e", requestId] ], content: JSON.stringify({ result, id: requestId }) }; // Sign the event with the connection's private key const signedEvent = finishEvent(responseEvent, connection.privateKey); // Publish to all connected relays for (const relay of this.relays) { await relay.publish(signedEvent); } } async sendErrorResponse(connection, requestId, errorMessage) { const responseEvent = { kind: 23195, created_at: Math.floor(Date.now() / 1000), tags: [ ["p", connection.publicKey], ["e", requestId] ], content: JSON.stringify({ error: { message: errorMessage }, id: requestId }) }; const signedEvent = finishEvent(responseEvent, connection.privateKey); for (const relay of this.relays) { await relay.publish(signedEvent); } }

7. User Interface Considerations

Your wallet UI should include:
  • A connection management screen showing all connected apps
  • Permission settings for each connection
  • Connection string display/QR code for new connections
  • Real-time authorisation prompts for payment requests
  • Ability to revoke connections

8. Connection Management Features

Implement functionality to manage the connection lifecycle: disconnectApp(connectionId) { const connection = this.connections.get(connectionId); if (!connection) return; // Clean up subscriptions if (connection.subscriptions) { for (const subscription of connection.subscriptions) { subscription.unsub(); } } // Remove connection this.connections.delete(connectionId); } updatePermissions(connectionId, newPermissions) { const connection = this.connections.get(connectionId); if (!connection) return; connection.permissions = newPermissions; }

Testing Your Implementation

  • Test your NWC implementation thoroughly by:
  • Connecting to various NWC-compatible applications
  • Testing all request types (payments, invoices, etc.)
  • Verifying responses are correctly formatted
  • Testing connection persistence across app restarts
  • Checking that permission controls work as expected

Example Workflow

  • User clicks a "Connect Wallet" button in your app.
  • App generates an NWC URI or retrieves one from the user.
  • App establishes a WebSocket connection to the relay specified in the URI.
  • App sends a request to the wallet (e.g., a balance request).
  • App receives and processes the wallet's response.
  • App displays the wallet balance to the user.

Best Practices for Wallet Developers

  • Clear permissions: Make permission requests explicit to users
  • Connection information: Display detailed info about connected apps
  • Spending limits: Implement configurable spending limits per connection
  • Auto-approval options: Allow users to set auto-approval for trusted apps
  • Connection timeout: Add options for connections to expire
  • Relay redundancy: Use multiple relays for reliability

Troubleshooting Tips

  • Connection Issues: Check the relay URL and ensure your internet connection is stable.
  • Request Errors: Verify the format of your NWC requests and consult the NWC documentation.
  • Wallet Compatibility: Ensure the wallet you are trying to connect to supports the specific requests you are sending.

Security Considerations

  • Store private keys securely, preferably encrypted
  • Validate all incoming requests thoroughly
  • Implement rate limiting for requests
  • Consider timeouts for user authorisation prompts
  • Verify invoice amounts match requested amounts

Open your Lightning wallet to a growing network of apps

Implementing Nostr Wallet Connect in your Lightning wallet opens up integration possibilities with a growing ecosystem of applications. By following this guide, you've learned the core components needed to make your wallet NWC-compatible. The NWC protocol continues to evolve, so stay connected with the community for updates and enhancements. As more wallets implement NWC, we'll see increased interoperability across the Lightning Network ecosystem, benefiting developers and users alike. By adding NWC support to your wallet, you're contributing to a more connected and user-friendly Lightning Network experience, helping to accelerate Bitcoin adoption worldwide.

Resources

  1. https://docs.nwc.dev/bitcoin-lightning-wallets/getting-started