555 lines
No EOL
14 KiB
Markdown
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. |