lamassu-server/misc-aio/LNBITS_REPLACEMENT_PLAN.md
2025-10-12 14:24:29 +02:00

555 lines
No EOL
14 KiB
Markdown

# 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 <WalletConfigForm fields={fields} config={config} />
}
```
#### 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.