As an app developer, seamless payment integration remains a significant challenge.
While you want to make it as easy as possible to pay for goods and services inside your app, the current landscape of payment options is fragmented and complex, especially when targeting an international audience.
In order for your app to improve sales and conversion rates, you’ll likely need to support multiple payment gateways, such as Stripe, PayPal, Google Pay, and Apple Pay. Even then, you’ll still need to consolidate all these purchases with their different payment methods.
It’s painful and adds to your admin load.
Having a globally accepted and widely available currency is a possible solution, but the payment process also needs to be on par with traditional payment rails. If you’re an app developer who wants to add Bitcoin and primarily Lightning payments to your app, then Nostr Wallet Connect (NWC) offers an elegant solution, providing a standardised protocol for connecting Lightning wallets to applications through the Nostr protocol.
Understanding the Nostr Wallet Connect Architecture
Before diving into implementation, it's essential to understand how NWC works:- Connection Establishment: Your app receives a connection string from the user's wallet
- Secure Channel: Communication occurs over Nostr relays using encrypted events
- Request-Response Pattern: Your app sends requests, and the connected wallet responds
- Permission System: Users authorise specific actions and spending limits
Prerequisites
To successfully implement NWC, you'll need:- Basic understanding of the Nostr protocol
- Development environment for your application
- Access to Nostr relays for testing
- Familiarity with Lightning Network concepts
Step 1: Setting Up Your Development Environment
Start by adding the necessary libraries to your project. For JavaScript/TypeScript applications:
npm install nostr-tools nwc-sdk
# or
yarn add nostr-tools nwc-sdk
For other languages, look for equivalent NWC client libraries or implement the protocol directly using Nostr libraries.
Step 2: Creating the NWC Client
Initialise the NWC client in your application:
import { NostrWalletConnectClient } from 'nwc-sdk';
class WalletConnectManager {
constructor() {
this.client = null;
this.connectionInfo = null;
}
async initialize(connectionString) {
try {
this.client = new NostrWalletConnectClient(connectionString);
// Test the connection by requesting wallet info
const info = await this.client.getInfo();
this.connectionInfo = info;
console.log('Successfully connected to wallet:', info.alias || 'Unknown');
return true;
} catch (error) {
console.error('Failed to initialize NWC client:', error);
return false;
}
}
}
// Create a singleton instance
export const walletConnect = new WalletConnectManager();
Step 3: Implementing the Connection UI
Create a user interface for connecting wallets. This typically involves:
// In your React component or equivalent
import { useState } from 'react';
import { walletConnect } from './services/wallet-connect';
function ConnectWalletModal({ onSuccess }) {
const [connectionString, setConnectionString] = useState('');
const [connecting, setConnecting] = useState(false);
const [error, setError] = useState(null);
async function handleConnect() {
setConnecting(true);
setError(null);
try {
const success = await walletConnect.initialize(connectionString);
if (success) {
onSuccess();
} else {
setError('Failed to connect. Please check your connection string.');
}
} catch (err) {
setError(`Connection error: ${err.message}`);
} finally {
setConnecting(false);
}
}
return (
<div className="modal">
<h2>Connect Lightning Wallet</h2>
<p>Paste your Nostr Wallet Connect string from your wallet:</p>
<textarea
value={connectionString}
onChange={(e) => setConnectionString(e.target.value)}
placeholder="nostr+walletconnect://..."
/>
{error && <div className="error">{error}</div>}
<button
onClick={handleConnect}
disabled={!connectionString || connecting}
>
{connecting ? 'Connecting...' : 'Connect Wallet'}
</button>
</div>
);
}
Step 4: Implementing Payment Functionality
Now that you have a connection, implement payment capabilities: // In your wallet service async function makePayment(invoice, amountSats, memo) { if (!walletConnect.client) { throw new Error('Wallet not connected'); } try { const paymentResponse = await walletConnect.client.sendPayment({ invoice, amount: amountSats, comment: memo }); return { success: true, preimage: paymentResponse.preimage, paymentHash: paymentResponse.payment_hash }; } catch (error) { console.error('Payment failed:', error); return { success: false, error: error.message }; } } // Create an invoice
async function createInvoice(amountSats, description) {
if (!walletConnect.client) {
throw new Error('Wallet not connected');
}
try {
const invoiceResponse = await walletConnect.client.makeInvoice({
amount: amountSats,
description
});
return {
success: true,
paymentRequest: invoiceResponse.payment_request,
paymentHash: invoiceResponse.payment_hash
};
} catch (error) {
console.error('Invoice creation failed:', error);
return {
success: false,
error: error.message
};
}
}
Step 5: Implementing Persistent Connections
To maintain connections across sessions, store the connection string securely: // Save connection
function saveConnection(connectionString) {
// Use secure storage when possible
localStorage.setItem('nwcConnection', connectionString);
}
// Restore connection on app start
async function restoreConnection() {
const savedConnection = localStorage.getItem('nwcConnection');
if (savedConnection) {
return walletConnect.initialize(savedConnection);
}
return false;
}
// Call this when your app initializes
document.addEventListener('DOMContentLoaded', () => {
restoreConnection().then(connected => {
if (connected) {
updateUIForConnectedState();
} else {
updateUIForDisconnectedState();
}
});
});
Step 6: Handling Connection Management
Implement functionality to manage connections: // Disconnect wallet
function disconnectWallet() {
walletConnect.client = null;
walletConnect.connectionInfo = null;
localStorage.removeItem('nwcConnection');
updateUIForDisconnectedState();
}
// Check if connected
function isWalletConnected() {
return walletConnect.client !== null;
}
// Get wallet info
function getWalletInfo() {
return walletConnect.connectionInfo;
}
Step 7: Error Handling and Retry Logic
Implement robust error handling:
async function executeWithRetry(operation, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
console.warn(`Operation failed (attempt ${attempt + 1}/${maxRetries}):`, error);
lastError = error;
// Check if we need to reconnect
if (error.message.includes('not connected') || error.message.includes('timeout')) {
const reconnected = await attemptReconnect();
if (!reconnected) {
throw new Error('Lost connection to wallet');
}
} else {
// Don't retry if it's a rejection or permission issue
if (error.message.includes('rejected') || error.message.includes('permission')) {
throw error;
}
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
throw lastError;
}
// Usage example
async function safeSendPayment(invoice, amount, memo) {
return executeWithRetry(() => makePayment(invoice, amount, memo));
}
Step 8: Implementing a User-Friendly Payment UI
Create a smooth payment experience:
function PaymentButton({ invoice, amount, onSuccess, onFailure }) {
const [status, setStatus] = useState('ready');
async function handlePayment() {
setStatus('processing');
try {
const result = await safeSendPayment(invoice, amount);
if (result.success) {
setStatus('success');
onSuccess(result);
} else {
setStatus('failed');
onFailure(result.error);
}
} catch (error) {
setStatus('failed');
onFailure(error.message);
}
}
return (
<button
onClick={handlePayment}
disabled={status === 'processing' || !isWalletConnected()}
className={`payment-button ${status}`}
>
{status === 'ready' && `Pay ${amount} sats`}
{status === 'processing' && 'Processing...'}
{status === 'success' && 'Payment Sent!'}
{status === 'failed' && 'Payment Failed'}
</button>
);
}
Step 9: Testing Your Integration
Before releasing your integration:- Test with multiple NWC-compatible wallets, such as Alby or Mutiny.
- Verify that reconnection works after network interruptions
- Test error scenarios (rejected payments, connection timeouts)
- Check persistent connections across app reloads
- Verify payment flows with different invoice types
Security Best Practices
For secure NWC implementation:- Store connection strings securely, preferably encrypted
- Implement proper error handling for all operations
- Show clear authorisation requests to users
- Don't store sensitive payment information
- Implement timeout handling for operations
- Use HTTPS endpoints to protect user data
- Verify all payment details before execution