# LNBits API Integration Plan ## Overview This document outlines the complete plan to replace the current Galoy (Blink API) Lightning Network integration with LNBits API in the Lamassu Bitcoin ATM server. ## Current State Analysis ### Galoy Plugin Structure - **Location**: `packages/server/lib/plugins/wallet/galoy/galoy.js` - **API Type**: GraphQL mutations and queries - **Authentication**: API secret token via `X-API-KEY` header - **Core Functions**: - `newInvoice()` - Create Lightning invoices - `getInvoiceStatus()` - Check payment status - `sendCoins()` - Pay Lightning invoices - `balance()` - Check wallet balance - `probeLN()` - Test payment routes ### LNBits API Structure - **API Type**: RESTful API with JSON responses - **Authentication**: Admin/Invoice keys via `X-API-KEY` header - **Endpoint**: `POST /api/v1/payments` (universal endpoint) - **Documentation**: Available at `/docs` endpoint ## API Mapping ### 1. Invoice Creation **Current Galoy**: ```javascript // GraphQL Mutation const createInvoice = { operationName: 'lnInvoiceCreate', query: `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { lnInvoiceCreate(input: $input) { errors { message path } invoice { paymentRequest } } }`, variables: { input: { walletId, amount: cryptoAtoms.toString() } } } ``` **LNBits Equivalent**: ```javascript // REST API Call POST /api/v1/payments { "out": false, "amount": cryptoAtoms, "unit": "sat", "memo": `Lamassu ATM Purchase - ${Date.now()}`, "webhook": "https://your-server.com/webhook/lnbits" } ``` ### 2. Payment Status Check **Current Galoy**: ```javascript const query = { operationName: 'lnInvoicePaymentStatus', query: `query lnInvoicePaymentStatus($input: LnInvoicePaymentStatusInput!) { lnInvoicePaymentStatus(input: $input) { status } }`, variables: { input: { paymentRequest: address } } } ``` **LNBits Equivalent**: ```javascript // REST API Call GET /api/v1/payments/{payment_hash} // Returns: { "paid": true/false, "amount": satoshis, "fee": fee_amount } ``` ### 3. Outgoing Payments **Current Galoy**: ```javascript const sendLnNoAmount = { operationName: 'lnNoAmountInvoicePaymentSend', query: `mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) { lnNoAmountInvoicePaymentSend(input: $input) { errors { message path } status } }`, variables: { input: { paymentRequest: invoice, walletId, amount: cryptoAtoms.toString() } } } ``` **LNBits Equivalent**: ```javascript // REST API Call POST /api/v1/payments { "out": true, "bolt11": "lnbc...", "amount": cryptoAtoms // For zero-amount invoices } ``` ### 4. Balance Inquiry **Current Galoy**: ```javascript const balanceQuery = { operationName: 'me', query: `query me { me { defaultAccount { wallets { walletCurrency balance } } } }` } ``` **LNBits Equivalent**: ```javascript // REST API Call GET /api/v1/wallet // Returns: { "name": "wallet_name", "balance": balance_msat } ``` ## Implementation Strategy ### Phase 1: Create LNBits Plugin **File**: `packages/server/lib/plugins/wallet/lnbits/lnbits.js` ```javascript const axios = require('axios') const { URL } = require('url') const BN = require('../../../bn') const NAME = 'LNBits' const SUPPORTED_COINS = ['LN'] // Configuration validation function validateConfig(account) { const required = ['endpoint', 'adminKey'] // No walletId needed for (const field of required) { if (!account[field]) { throw new Error(`LNBits configuration missing: ${field}`) } } } // HTTP request wrapper async function request(endpoint, method = 'GET', data = null, apiKey) { const config = { method, url: endpoint, headers: { 'Content-Type': 'application/json', 'X-API-KEY': apiKey } } if (data) config.data = data try { const response = await axios(config) return response.data } catch (error) { throw new Error(`LNBits API Error: ${error.response?.data?.detail || error.message}`) } } // Create Lightning invoice async function newAddress(account, info, tx) { validateConfig(account) const { cryptoAtoms } = tx const invoiceData = { out: false, amount: parseInt(cryptoAtoms.toString()), unit: 'sat', memo: `Lamassu ATM Purchase - ${Date.now()}`, expiry: 3600 // 1 hour expiration } const endpoint = `${account.endpoint}/api/v1/payments` const result = await request(endpoint, 'POST', invoiceData, account.adminKey) return result.payment_request // Returns BOLT11 invoice string } // Check payment status async function getStatus(account, tx) { validateConfig(account) const { toAddress } = tx // Extract payment hash from BOLT11 invoice const paymentHash = extractPaymentHash(toAddress) const endpoint = `${account.endpoint}/api/v1/payments/${paymentHash}` const result = await request(endpoint, 'GET', null, account.adminKey) return { status: result.paid ? 'confirmed' : 'pending' } } // Send Lightning payment async function sendCoins(account, tx) { validateConfig(account) const { toAddress, cryptoAtoms } = tx const paymentData = { out: true, bolt11: toAddress, amount: parseInt(cryptoAtoms.toString()) // For zero-amount invoices } const endpoint = `${account.endpoint}/api/v1/payments` const result = await request(endpoint, 'POST', paymentData, account.adminKey) return { txid: result.payment_hash, fee: result.fee || 0 } } // Get wallet balance async function balance(account) { validateConfig(account) const endpoint = `${account.endpoint}/api/v1/wallet` const result = await request(endpoint, 'GET', null, account.adminKey) return new BN(Math.floor(result.balance / 1000)) // Convert msat to sat } // Payment route probing async function probeLN(account, cryptoCode, invoice) { validateConfig(account) // Decode invoice to get payment info const endpoint = `${account.endpoint}/api/v1/payments/decode` const decodeData = { data: invoice } const result = await request(endpoint, 'POST', decodeData, account.adminKey) // Test different amounts for route feasibility const limits = [200000, 1000000, 2000000] return limits.map(limit => result.amount_msat <= limit * 1000) } // Utility functions function extractPaymentHash(bolt11) { // Use bolt11 decoder library to extract payment hash const decoded = require('bolt11').decode(bolt11) return decoded.paymentHash } function checkCryptoCode(cryptoCode) { if (!SUPPORTED_COINS.includes(cryptoCode)) { throw new Error(`Unsupported crypto: ${cryptoCode}`) } return Promise.resolve() } module.exports = { NAME, balance, sendCoins, newAddress, getStatus, probeLN, isLnInvoice: (address) => address.toLowerCase().startsWith('lnbc'), cryptoNetwork: () => 'main' } ``` ### Phase 2: Configuration Changes #### Admin UI Configuration Panel **File**: `packages/admin-ui/src/pages/Wallet/LNBitsConfig.jsx` ```javascript const LNBitsConfig = () => { const [config, setConfig] = useState({ endpoint: '', adminKey: '' // Admin key is wallet-specific, no walletId needed }) const fields = [ { name: 'endpoint', label: 'LNBits Server URL', placeholder: 'https://your-lnbits.com' }, { name: 'adminKey', label: 'Admin API Key', type: 'password', help: 'Wallet-specific key for all operations (no separate walletId needed)' } ] return } ``` #### Database Schema Updates **Migration File**: `packages/server/migrations/xxx-add-lnbits-config.js` ```javascript const up = ` INSERT INTO user_config (name, display_name, data_type, config_type, secret) VALUES ('lnbitsEndpoint', 'LNBits Server URL', 'string', 'wallets', false), ('lnbitsAdminKey', 'LNBits Admin Key', 'string', 'wallets', true); -- Note: No lnbitsWalletId needed - adminKey is wallet-specific ` ``` ### Phase 3: Integration Points #### Plugin Registration **File**: `packages/server/lib/plugins/wallet/lnbits/index.js` ```javascript const lnbits = require('./lnbits') module.exports = { LNBits: lnbits } ``` **File**: `packages/server/lib/plugins/wallet/index.js` (update) ```javascript module.exports = { // ... existing plugins LNBits: require('./lnbits') } ``` #### Environment Variables **File**: `packages/server/.sample.env` (add) ```bash # LNBits Configuration LNBITS_ENDPOINT=https://demo.lnbits.com LNBITS_ADMIN_KEY=your_admin_key_here # Note: No LNBITS_WALLET_ID needed - admin key identifies the wallet ``` ### Phase 4: Testing Strategy #### Unit Tests **File**: `packages/server/tests/unit/test_lnbits_plugin.js` ```javascript const test = require('tape') const lnbits = require('../../lib/plugins/wallet/lnbits/lnbits') const mockAccount = { endpoint: 'https://demo.lnbits.com', adminKey: 'test_admin_key' // No walletId needed } test('LNBits invoice creation', async t => { const tx = { cryptoAtoms: new BN(100000) } // 100k sats const invoice = await lnbits.newAddress(mockAccount, { cryptoCode: 'LN' }, tx) t.ok(invoice.startsWith('lnbc'), 'Generated valid BOLT11 invoice') t.end() }) test('LNBits balance check', async t => { const balance = await lnbits.balance(mockAccount, 'LN') t.ok(balance instanceof BN, 'Returns BigNumber balance') t.ok(balance.gte(0), 'Balance is non-negative') t.end() }) ``` #### Integration Tests **File**: `packages/server/tests/integration/test_lnbits_integration.js` ```javascript const test = require('tape') const request = require('supertest') const app = require('../../lib/app') test('Full Lightning transaction flow', async t => { // 1. Create transaction const createTx = await request(app) .post('/tx') .send({ direction: 'cashIn', cryptoCode: 'LN', fiat: 1000, // $10 cryptoAtoms: '39216', isLightning: true }) t.equal(createTx.status, 200, 'Transaction created successfully') t.ok(createTx.body.toAddress.startsWith('lnbc'), 'Valid Lightning invoice') // 2. Check transaction status const statusCheck = await request(app) .get(`/tx/${createTx.body.id}?status=confirmed`) t.equal(statusCheck.status, 200, 'Status check successful') t.end() }) ``` ### Phase 5: Deployment Strategy #### Rollout Plan 1. **Development Environment** - Deploy LNBits instance for testing - Configure test wallets with small amounts - Run comprehensive integration tests 2. **Staging Environment** - Mirror production configuration - Test with realistic transaction volumes - Validate error handling and edge cases 3. **Production Migration** - Feature flag to switch between Galoy and LNBits - Gradual rollout to subset of machines - Monitor transaction success rates - Full cutover after validation #### Configuration Migration **File**: `packages/server/tools/migrate-galoy-to-lnbits.js` ```javascript // Migration script to convert existing Galoy configurations const migrateWalletConfig = async () => { const galoyConfigs = await db.query(` SELECT * FROM user_config WHERE name IN ('galoyEndpoint', 'galoyApiSecret', 'galoyWalletId') `) // Convert to LNBits format const lnbitsConfigs = galoyConfigs.map(config => ({ ...config, name: config.name.replace('galoy', 'lnbits'), // Additional mapping logic })) // Insert new configurations await db.query(INSERT_LNBITS_CONFIG_SQL, lnbitsConfigs) } ``` ## Migration Checklist ### Pre-Migration - [ ] LNBits server deployed and configured - [ ] Test wallets created with sufficient balance - [ ] All unit tests passing - [ ] Integration tests validated - [ ] Staging environment fully tested ### During Migration - [ ] Feature flag enabled for LNBits - [ ] Monitor transaction success rates - [ ] Validate Lightning invoice generation - [ ] Test payment status polling - [ ] Confirm balance reporting accuracy ### Post-Migration - [ ] All machines successfully switched to LNBits - [ ] Transaction monitoring shows normal patterns - [ ] Error rates within acceptable bounds - [ ] Remove Galoy plugin code - [ ] Update documentation ## Key Differences & Considerations ### API Architecture - **Galoy**: Single GraphQL endpoint with mutations/queries - **LNBits**: RESTful endpoints with standard HTTP verbs ### Authentication - **Galoy**: Single API secret for all operations - **LNBits**: Wallet-specific admin key (no separate walletId needed) ### Error Handling - **Galoy**: GraphQL errors array in response - **LNBits**: HTTP status codes with JSON error details ### Webhook Support - **LNBits**: Native webhook support for payment notifications - **Galoy**: Requires polling for status updates ### Payment Hash Handling - **LNBits**: Exposes payment hashes directly - **Galoy**: Uses internal payment request tracking ### Key Management - **LNBits**: Admin key is wallet-specific (1:1 relationship) - **Galoy**: API secret can access multiple wallets within account ## Benefits of Migration 1. **Self-Hosted Control**: Run your own Lightning infrastructure 2. **Better Integration**: RESTful API easier to work with than GraphQL 3. **Enhanced Features**: Native webhook support, better payment tracking 4. **Simplified Configuration**: No separate walletId needed (admin key is wallet-specific) 5. **Cost Efficiency**: No external service fees 6. **Privacy**: Complete control over transaction data 7. **Extensibility**: Easy to add custom Lightning functionality ## Risks & Mitigation ### Technical Risks - **API Differences**: Thorough testing and validation required - **Payment Failures**: Comprehensive error handling needed - **Performance Issues**: Load testing with realistic volumes ### Business Risks - **Service Interruption**: Feature flag enables quick rollback - **Transaction Loss**: Extensive logging and monitoring - **Customer Impact**: Staged rollout minimizes exposure ### Mitigation Strategies - Feature flags for safe deployment - Comprehensive monitoring and alerting - Automated testing pipeline - Quick rollback procedures - 24/7 monitoring during migration ## Success Metrics - **Transaction Success Rate**: >99.5% - **Payment Confirmation Time**: <30 seconds average - **API Response Time**: <2 seconds for all endpoints - **Error Rate**: <0.5% - **Uptime**: >99.9% This migration plan provides a comprehensive path from Galoy's hosted Lightning service to a self-managed LNBits infrastructure, giving Lamassu operators full control over their Lightning Network integration.