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

14 KiB

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:

// 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:

// 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:

const query = {
  operationName: 'lnInvoicePaymentStatus',
  query: `query lnInvoicePaymentStatus($input: LnInvoicePaymentStatusInput!) {
    lnInvoicePaymentStatus(input: $input) {
      status
    }
  }`,
  variables: { input: { paymentRequest: address } }
}

LNBits Equivalent:

// REST API Call  
GET /api/v1/payments/{payment_hash}
// Returns: { "paid": true/false, "amount": satoshis, "fee": fee_amount }

3. Outgoing Payments

Current Galoy:

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:

// REST API Call
POST /api/v1/payments  
{
  "out": true,
  "bolt11": "lnbc...",
  "amount": cryptoAtoms  // For zero-amount invoices
}

4. Balance Inquiry

Current Galoy:

const balanceQuery = {
  operationName: 'me',
  query: `query me {
    me {
      defaultAccount {
        wallets {
          walletCurrency
          balance
        }
      }
    }
  }`
}

LNBits Equivalent:

// 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

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

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

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

const lnbits = require('./lnbits')

module.exports = {
  LNBits: lnbits
}

File: packages/server/lib/plugins/wallet/index.js (update)

module.exports = {
  // ... existing plugins
  LNBits: require('./lnbits')
}

Environment Variables

File: packages/server/.sample.env (add)

# 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

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

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

// 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.