| 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 encryptionlud16: (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
encryptiontag 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
- Never log secrets: Connection strings contain private keys
- Validate all inputs: Always validate NWC connection strings and command parameters
- Use NIP-04 for compatibility: NIP-04 is widely supported; use NIP-44 only if both sides support it
- Unique keys per connection: Generate unique keypairs for each connection
- Store securely: Use secure storage for connection data (localStorage is acceptable for browser apps)
- Track processed commands: Use
seenCommandsUntilto prevent duplicate processing - Implement blocking: Prevent concurrent payment processing
- Allowance management: Track and enforce spending limits per connection
- 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
sinceparameter 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
seenCommandsUntiltracking - Check event
created_atbefore 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