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