From 2f0cc901eb601558d65521737613abc64a051822 Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 12 Oct 2025 14:24:20 +0200 Subject: [PATCH 01/10] miscellaneous files --- CLAUDE.md | 86 ++++ misc-aio/BTC_NODE_WALLET_CONFIGURATION.md | 169 +++++++ misc-aio/LIGHTNING_NETWORK_ANALYSIS.md | 543 +++++++++++++++++++++ misc-aio/LNBITS_CONFIG_CLARIFICATION.md | 95 ++++ misc-aio/LNBITS_REPLACEMENT_PLAN.md | 555 ++++++++++++++++++++++ misc-aio/lightning_analysis.pdf | Bin 0 -> 64317 bytes 6 files changed, 1448 insertions(+) create mode 100644 CLAUDE.md create mode 100644 misc-aio/BTC_NODE_WALLET_CONFIGURATION.md create mode 100644 misc-aio/LIGHTNING_NETWORK_ANALYSIS.md create mode 100644 misc-aio/LNBITS_CONFIG_CLARIFICATION.md create mode 100644 misc-aio/LNBITS_REPLACEMENT_PLAN.md create mode 100644 misc-aio/lightning_analysis.pdf diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..12701c68 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Bitcoin ATM server implementation built as a monorepo with three main packages: + +1. **packages/server** - Node.js backend with Express, GraphQL, PostgreSQL +2. **packages/admin-ui** - React frontend with Material-UI +3. **packages/typesafe-db** - TypeScript database layer using Kysely + +## Common Commands + +### Development Setup +- `pnpm install` - Install dependencies (Node.js 22+ required) +- `bash packages/server/tools/cert-gen.sh` - Generate SSL certificates +- `node packages/server/bin/lamassu-migrate` - Run database migrations +- `node packages/server/bin/lamassu-register admin@example.com superuser` - Create admin user + +### Development +- `pnpm run dev` - Start development servers (both admin UI and server) +- `pnpm run build` - Build all packages using Turbo +- `pnpm run test` - Run tests + +### TypeScript/Database +- `cd packages/typesafe-db && npm run generate-types` - Generate database types +- `cd packages/typesafe-db && npm run build` - Build TypeScript database layer + +### Individual Package Commands +- Server: `npm run dev` in packages/server (runs both server and admin-server with --watch) +- Admin UI: `npm run dev` in packages/admin-ui (Vite dev server) + +## Architecture + +**Monorepo Structure**: Uses Turbo for build orchestration and PNPM workspaces. + +### packages/server +- **bin/** - CLI tools and executables (lamassu-migrate, lamassu-register, etc.) +- **lib/** - Core business logic + - **blockchain/** - Cryptocurrency handling (bitcoin, ethereum, etc.) + - **cash-in/**, **cash-out/** - Transaction processing + - **compliance/** - KYC/AML and sanctions checking + - **new-admin/** - GraphQL API and admin server + - **plugins/** - Exchange, wallet, and service integrations + - **routes/** - Express API endpoints +- **migrations/** - Database schema migrations +- Express server with GraphQL API, PostgreSQL database + +### packages/admin-ui +- React app built with Vite +- **src/pages/** - Main application screens +- **src/components/** - Reusable UI components +- Material-UI design system +- Apollo Client for GraphQL + +### packages/typesafe-db +- Kysely for type-safe SQL queries +- Auto-generates TypeScript types from PostgreSQL schema +- Shared database layer across packages + +## Key Configuration + +### Environment Setup +- PostgreSQL database required +- Environment variables configured in `packages/server/.env` +- SSL certificates generated via `cert-gen.sh` + +### Build System +- **turbo.json** - Defines build pipeline and caching +- **pnpm-workspace.yaml** - Workspace package definitions +- **eslint.config.mjs** - ESLint configuration with React, TypeScript, Vitest rules +- Husky pre-commit hooks with lint-staged for code quality + +### Database +- PostgreSQL with extensive migration system +- Critical data: transactions, customers, machines, compliance records +- Use typesafe-db package for database operations + +## Development Guidelines + +### Code Quality +- TypeScript for database operations via typesafe-db package +- Follow existing patterns in GraphQL resolvers and Express routes +- Test database changes with migrations before deployment +- ESLint + Prettier enforced via pre-commit hooks \ No newline at end of file diff --git a/misc-aio/BTC_NODE_WALLET_CONFIGURATION.md b/misc-aio/BTC_NODE_WALLET_CONFIGURATION.md new file mode 100644 index 00000000..72de02f2 --- /dev/null +++ b/misc-aio/BTC_NODE_WALLET_CONFIGURATION.md @@ -0,0 +1,169 @@ +# Bitcoin Node and Wallet Configuration Guide + +## Overview + +The Lamassu server uses environment variables `BTC_NODE_LOCATION` and `BTC_WALLET_LOCATION` to control how it interacts with Bitcoin infrastructure. These variables determine whether Bitcoin node and wallet operations run locally or connect to remote services. + +## Configuration Values + +Both variables accept two values: +- **`local`** - Run Bitcoin Core node/wallet on the same server as Lamassu +- **`remote`** - Connect to external Bitcoin Core node/wallet services + +## Valid Configuration Combinations + +### 1. Local Node + Local Wallet ✅ +```bash +BTC_NODE_LOCATION=local +BTC_WALLET_LOCATION=local +``` +- **Use case**: Full local Bitcoin Core setup +- **Requirements**: Sufficient storage for blockchain, bandwidth for sync +- **Security**: Maximum control and security + +### 2. Remote Node + Remote Wallet ✅ +```bash +BTC_NODE_LOCATION=remote +BTC_WALLET_LOCATION=remote +``` +- **Use case**: Fully hosted Bitcoin infrastructure +- **Requirements**: All remote connection variables (see below) +- **Security**: Depends on remote service trust + +### 3. Remote Node + Local Wallet ✅ +```bash +BTC_NODE_LOCATION=remote +BTC_WALLET_LOCATION=local +``` +- **Use case**: Use remote blockchain data with local key management +- **Requirements**: Remote node connection variables +- **Security**: Local key control, remote blockchain dependency + +### 4. Local Node + Remote Wallet ❌ +```bash +BTC_NODE_LOCATION=local +BTC_WALLET_LOCATION=remote +``` +- **Status**: **FORBIDDEN** by Lamassu validation +- **Error**: "It's not possible to use a remote wallet without using a remote node!" +- **Reason**: Remote wallet services need their own blockchain access + +## Required Environment Variables + +### For Remote Node Configurations +When `BTC_NODE_LOCATION=remote`: +```bash +BTC_NODE_HOST= +BTC_NODE_PORT= # Usually 8333 for mainnet, 18333 for testnet +``` + +### For Remote Wallet Configurations +When `BTC_WALLET_LOCATION=remote`: +```bash +BTC_NODE_RPC_HOST= +BTC_NODE_RPC_PORT= # Usually 8332 for mainnet, 18332 for testnet +BTC_NODE_USER= +BTC_NODE_PASSWORD= +``` + +## Environment Setup Scripts + +### Development Environment +File: `packages/server/tools/build-dev-env.js` +```javascript +BTC_NODE_LOCATION=remote // Avoid full blockchain sync +BTC_WALLET_LOCATION=local // Local testing +``` + +### Production Environment +File: `packages/server/tools/build-prod-env.js` +```javascript +BTC_NODE_LOCATION=local // Full control +BTC_WALLET_LOCATION=local // Maximum security +``` + +## Validation Logic + +The configuration is validated in `packages/server/lib/blockchain/install.js`: + +```javascript +function isEnvironmentValid(crypto) { + // Prevent remote wallet + local node + if (isRemoteWallet(crypto) && !isRemoteNode(crypto)) + throw new Error( + `Invalid environment setup for ${crypto.display}: It's not possible to use a remote wallet without using a remote node!` + ) + + // Validate required variables for each configuration... +} +``` + +### Logic Breakdown +For `BTC_NODE_LOCATION=remote` + `BTC_WALLET_LOCATION=local`: +- `isRemoteWallet(crypto)` → `false` (wallet is local) +- `!isRemoteNode(crypto)` → `false` (node is remote, so `!remote` = `false`) +- `false && false` → `false` (validation passes) + +## Bitcoin Core Configuration Impact + +The variables affect the generated `bitcoin.conf`: + +### Local Node Configuration +```conf +rpcuser=lamassuserver +rpcpassword= +server=1 +rpcport=8333 +bind=0.0.0.0:8332 +``` + +### Remote Node Configuration +```conf +rpcuser=lamassuserver +rpcpassword= +server=1 +rpcport=8333 +bind=0.0.0.0:8332 +connect=: # Only connect to specified peer +``` + +## Multi-Cryptocurrency Support + +The same pattern applies to all supported cryptocurrencies: +- `BCH_NODE_LOCATION` / `BCH_WALLET_LOCATION` +- `LTC_NODE_LOCATION` / `LTC_WALLET_LOCATION` +- `DASH_NODE_LOCATION` / `DASH_WALLET_LOCATION` +- `ZEC_NODE_LOCATION` / `ZEC_WALLET_LOCATION` +- `XMR_NODE_LOCATION` / `XMR_WALLET_LOCATION` + +## Bitcoin Core Compatibility + +These configurations align with standard Bitcoin Core functionality: + +- **Local Node + Local Wallet**: Standard `bitcoind` with wallet enabled +- **Remote Node + Remote Wallet**: Client connecting to remote Bitcoin Core RPC +- **Remote Node + Local Wallet**: Local wallet connecting to remote node via RPC + +The restriction on "Local Node + Remote Wallet" is a Lamassu design choice, not a Bitcoin protocol limitation. + +## Security Considerations + +| Configuration | Storage | Bandwidth | Security | Trust | +|--------------|---------|-----------|----------|-------| +| Local + Local | High | High | Maximum | Self | +| Remote + Remote | Low | Low | Depends on service | Remote service | +| Remote + Local | Low | Medium | Keys local, blockchain remote | Remote node | +| Local + Remote | High | High | ❌ Not allowed | N/A | + +## Troubleshooting + +### Common Errors + +1. **"Environment variable BTC_NODE_LOCATION is not set!"** + - Solution: Set the required environment variable + +2. **"It's not possible to use a remote wallet without using a remote node!"** + - Solution: Use a valid configuration combination + +3. **Missing RPC connection variables** + - Solution: Set all required variables for remote configurations \ No newline at end of file diff --git a/misc-aio/LIGHTNING_NETWORK_ANALYSIS.md b/misc-aio/LIGHTNING_NETWORK_ANALYSIS.md new file mode 100644 index 00000000..267927cc --- /dev/null +++ b/misc-aio/LIGHTNING_NETWORK_ANALYSIS.md @@ -0,0 +1,543 @@ +# Lamassu Lightning Network Integration Analysis + +## Overview + +This document provides a comprehensive technical analysis of how the Lamassu Bitcoin ATM server integrates with the Lightning Network to process Bitcoin Lightning transactions. The analysis covers the complete flow from hardware interaction to Lightning invoice creation and payment processing. + +## Lightning Network Architecture + +Lamassu implements Lightning Network support through the **Galoy wallet plugin**, which acts as a Lightning service provider rather than running a local Lightning node. + +**Key Architecture Decision**: Despite the presence of a `LIGHTNING_NETWORK_DAEMON` environment variable, Lightning functionality is actually handled via **Galoy's hosted GraphQL API**, not a local Lightning daemon. + +### Core Components + +- **Transaction Coordinator**: Routes Lightning transactions through the cash-in/cash-out pipeline +- **Galoy Wallet Plugin**: Interfaces with Galoy's Lightning infrastructure via GraphQL +- **Invoice Management**: Creates, monitors, and processes Lightning invoices +- **Payment Probing**: Tests Lightning payment routes and limits + +## Lightning Invoice Creation Flow + +When a customer wants to purchase $100 of Bitcoin Lightning, the following sequence occurs: + +### 1. Machine Initiates Transaction Request + +The ATM machine sends a POST request to the server: + +```http +POST /tx +Content-Type: application/json + +{ + "direction": "cashIn", + "cryptoCode": "LN", + "fiat": 10000, + "cryptoAtoms": "546448", + "isLightning": true, + "deviceId": "", + "txVersion": 1 +} +``` + +**Parameters Explained**: +- `fiat`: Amount in cents ($100.00 = 10000 cents) +- `cryptoAtoms`: Equivalent amount in satoshis +- `cryptoCode`: "LN" indicates Lightning Network +- `isLightning`: Boolean flag for Lightning transactions + +### 2. Server Processing Pipeline + +#### Route Handler +**File**: `packages/server/lib/routes/txRoutes.js:13-49` + +```javascript +function postTx(req, res, next) { + const pi = plugins(req.settings, req.deviceId) + return Tx.post(_.set('deviceId', req.deviceId, req.body), pi) + .then(tx => res.json(tx)) + .catch(next) +} +``` + +#### Transaction Coordinator +**File**: `packages/server/lib/tx.js:13-18` + +```javascript +function process(tx, pi) { + const mtx = massage(tx) + if (mtx.direction === 'cashIn') return CashInTx.post(mtx, pi) // Lightning routing + if (mtx.direction === 'cashOut') return CashOutTx.post(mtx, pi) + return Promise.reject(new Error('No such tx direction: ' + mtx.direction)) +} +``` + +#### Cash-In Handler +**File**: `packages/server/lib/cash-in/cash-in-tx.js:154-155` + +```javascript +return pi + .newAddress(r.tx) // Creates Lightning invoice + .then(txObj => { + // Process the invoice response + if (txObj.batched) { + return txBatching.addUnconfirmedTx(r.tx) + } + // Handle immediate processing + }) +``` + +### 3. Lightning Invoice Generation + +#### Plugin System Routing +**File**: `packages/server/lib/plugins.js:427-437` + +```javascript +function newAddress(tx) { + const info = { + cryptoCode: tx.cryptoCode, // "LN" + label: 'TX ' + Date.now(), + hdIndex: tx.hdIndex, + cryptoAtoms: tx.cryptoAtoms, // Amount in satoshis + isLightning: tx.isLightning // true + } + return wallet.newAddress(settings, info, tx) // Calls Galoy plugin +} +``` + +#### Galoy Lightning Plugin +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:310-320` + +```javascript +function newAddress(account, info, tx) { + const { cryptoAtoms, cryptoCode } = tx + return checkCryptoCode(cryptoCode).then(() => + newInvoice( + account.walletId, + cryptoAtoms, // Amount in satoshis + account.apiSecret, + account.endpoint, + ), + ) +} +``` + +### 4. Galoy GraphQL Lightning Invoice Creation + +#### Invoice Creation Function +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:279-298` + +```javascript +function newInvoice(walletId, cryptoAtoms, token, endpoint) { + const createInvoice = { + operationName: 'lnInvoiceCreate', + query: `mutation lnInvoiceCreate($input: LnInvoiceCreateInput!) { + lnInvoiceCreate(input: $input) { + errors { + message + path + } + invoice { + paymentRequest // Lightning invoice string + } + } + }`, + variables: { + input: { + walletId, + amount: cryptoAtoms.toString() + } + } + } + + return request(createInvoice, token, endpoint).then(result => { + return result.data.lnInvoiceCreate.invoice.paymentRequest // Returns "lnbc..." + }) +} +``` + +#### GraphQL Request Handler +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:10-28` + +```javascript +function request(graphqlQuery, token, endpoint) { + const headers = { + 'content-type': 'application/json', + 'X-API-KEY': token, + } + return axios({ + method: 'post', + url: endpoint, + headers: headers, + data: graphqlQuery, + }) + .then(r => { + if (r.error) throw r.error + return r.data + }) +} +``` + +### 5. Response to Machine + +The server returns the Lightning invoice to the ATM machine: + +```json +{ + "id": "tx-uuid-12345", + "toAddress": "lnbc5464480n1pn2s...", // Lightning invoice + "layer2Address": null, + "cryptoCode": "LN", + "direction": "cashIn", + "fiat": 10000, + "cryptoAtoms": "546448", + "status": "authorized" +} +``` + +## Complete Machine-Server Interaction Flow + +``` +[ATM Machine] [Lamassu Server] [Galoy API] + │ │ │ + │ 1. POST /tx │ │ + │ {direction:"cashIn", LN, $100}│ │ + ├──────────────────────────────►│ │ + │ │ 2. Process transaction │ + │ │ - Route to cash-in │ + │ │ - Extract amount & type │ + │ │ │ + │ │ 3. GraphQL newInvoice │ + │ ├───────────────────────────►│ + │ │ lnInvoiceCreate mutation │ + │ │ │ + │ │ 4. Lightning invoice │ + │ │◄───────────────────────────┤ + │ │ "lnbc5464480n1..." │ + │ │ │ + │ 5. Transaction response │ │ + │ {toAddress: "lnbc..."} │ │ + │◄──────────────────────────────┤ │ + │ │ │ + │ 6. Display QR code to user │ │ + │ Show Lightning invoice │ │ + │ │ │ + │ 7. Poll for payment status │ │ + │ GET /tx/:id?status=confirmed │ │ + ├──────────────────────────────►│ │ + │ │ 8. Check invoice status │ + │ ├───────────────────────────►│ + │ │ lnInvoicePaymentStatus │ + │ │ │ + │ │ 9. Payment status │ + │ │◄───────────────────────────┤ + │ 10. Confirmation response │ "PAID" or "PENDING" │ + │ {status: "confirmed"} │ │ + │◄──────────────────────────────┤ │ + │ │ │ + │ 11. Dispense cash to user │ │ +``` + +## Lightning Payment Detection and Processing + +### Address Type Detection + +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:94-100` + +```javascript +function isLnInvoice(address) { + return address.toLowerCase().startsWith('lnbc') // Lightning mainnet invoices +} + +function isLnurl(address) { + return address.toLowerCase().startsWith('lnurl') // Lightning URL format +} +``` + +### Payment Status Monitoring + +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:322-339` + +```javascript +function getInvoiceStatus(token, endpoint, address) { + const query = { + operationName: 'lnInvoicePaymentStatus', + query: `query lnInvoicePaymentStatus($input: LnInvoicePaymentStatusInput!) { + lnInvoicePaymentStatus(input: $input) { + status // Returns "PENDING" or "PAID" + } + }`, + variables: { input: { paymentRequest: address } } + } + + return request(query, token, endpoint) + .then(r => r?.data?.lnInvoicePaymentStatus?.status) + .catch(err => { + throw new Error(err) + }) +} +``` + +### Status Polling Implementation + +The machine continuously polls the server to check if the Lightning payment has been received: + +**Route**: `/tx/:id?status=confirmed` +**Handler**: `packages/server/lib/routes/txRoutes.js:51-65` + +```javascript +function getTx(req, res, next) { + if (req.query.status) { + return helpers + .fetchStatusTx(req.params.id, req.query.status) + .then(r => res.json(r)) + .catch(err => { + if (err.name === 'HTTPError') { + return res.status(err.code).send(err.message) + } + next(err) + }) + } + return next(httpError('Not Found', 404)) +} +``` + +## Lightning Transaction Capabilities + +### Supported Operations + +1. **Invoice Creation** (`newAddress`) + - Generates Lightning invoices for incoming payments + - Specifies exact amount in satoshis + - Returns BOLT11 payment request string + +2. **Payment Sending** (`sendCoins`) + - Pays existing Lightning invoices + - Handles both amount-specified and no-amount invoices + - Supports LNURL payments + +3. **Status Checking** (`getStatus`) + - Monitors payment confirmation status + - Provides real-time payment updates + - Handles payment timeouts and failures + +4. **Balance Queries** (`balance`) + - Checks Lightning wallet balance + - Returns available satoshi amounts + - Accounts for pending transactions + +5. **Payment Probing** (`probeLN`) + - Tests Lightning payment routes + - Validates payment feasibility + - Determines supported amounts + +### Payment Route Probing + +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:244-254` + +```javascript +function probeLN(account, cryptoCode, invoice) { + const probeHardLimits = [200000, 1000000, 2000000] // Test limits in satoshis + const promises = probeHardLimits.map(limit => { + return sendProbeRequest( + account.walletId, + invoice, + limit, + account.apiSecret, + account.endpoint, + ).then(r => _.isEmpty(r.errors)) // Check if amount is routable + }) + return Promise.all(promises) // Return array of supported limits +} +``` + +**Probe Request Implementation**: +```javascript +function sendProbeRequest(walletId, invoice, cryptoAtoms, token, endpoint) { + const sendProbeNoAmount = { + operationName: 'lnNoAmountInvoiceFeeProbe', + query: `mutation lnNoAmountInvoiceFeeProbe($input: LnNoAmountInvoiceFeeProbeInput!) { + lnNoAmountInvoiceFeeProbe(input: $input) { + amount + errors { + message + path + } + } + }`, + variables: { + input: { + paymentRequest: invoice, + walletId, + amount: cryptoAtoms.toString(), + }, + }, + } + return request(sendProbeNoAmount, token, endpoint) +} +``` + +## Lightning Cash-Out (Payment) Flow + +For outgoing Lightning payments (when user wants to send Lightning Bitcoin): + +### Payment Processing +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:196-242` + +```javascript +function sendCoins(account, tx) { + const { toAddress, cryptoAtoms, cryptoCode } = tx + return checkCryptoCode(cryptoCode) + .then(() => { + if (isLnInvoice(toAddress)) { + return sendFundsLN( + account.walletId, + toAddress, // Lightning invoice to pay + cryptoAtoms, + account.apiSecret, + account.endpoint, + ) + } + + if (isLnurl(toAddress)) { + return sendFundsLNURL( + account.walletId, + toAddress, // LNURL to pay + cryptoAtoms, + account.apiSecret, + account.endpoint, + ) + } + + // Handle on-chain addresses + return sendFundsOnChain(...) + }) +} +``` + +### Lightning Payment Execution +```javascript +function sendFundsLN(walletId, invoice, cryptoAtoms, token, endpoint) { + const sendLnNoAmount = { + operationName: 'lnNoAmountInvoicePaymentSend', + query: `mutation lnNoAmountInvoicePaymentSend($input: LnNoAmountInvoicePaymentInput!) { + lnNoAmountInvoicePaymentSend(input: $input) { + errors { + message + path + } + status // Payment result status + } + }`, + variables: { + input: { + paymentRequest: invoice, + walletId, + amount: cryptoAtoms.toString(), + }, + }, + } + return request(sendLnNoAmount, token, endpoint) +} +``` + +## Configuration and Environment + +### Environment Variables + +**File**: `packages/server/.sample.env:38` +```bash +LIGHTNING_NETWORK_DAEMON= +``` + +**Note**: This variable exists but is not used. Lightning functionality is provided through Galoy's API configuration in the admin interface. + +### Galoy Account Configuration + +Required configuration parameters for Galoy integration: +- `walletId`: Galoy wallet identifier +- `apiSecret`: Authentication token for Galoy API +- `endpoint`: Galoy GraphQL API endpoint URL + +### Supported Cryptocurrency Codes + +**File**: `packages/server/lib/plugins/wallet/galoy/galoy.js:5-6` +```javascript +const NAME = 'LN' +const SUPPORTED_COINS = ['LN'] +``` + +## Error Handling and Edge Cases + +### Lightning-Specific Error Handling + +1. **Invoice Validation** + - Validates Lightning invoice format (starts with "lnbc") + - Checks invoice expiration + - Verifies payment request integrity + +2. **Payment Routing Failures** + - Handles insufficient Lightning liquidity + - Manages routing failures + - Provides fallback mechanisms + +3. **Network Connectivity Issues** + - Implements retry logic for Galoy API calls + - Handles temporary network failures + - Maintains transaction state during outages + +### Transaction State Management + +Lightning transactions follow these states: +- `pending`: Invoice created, awaiting payment +- `authorized`: Payment detected but not confirmed +- `confirmed`: Payment fully confirmed +- `expired`: Invoice expired without payment +- `failed`: Payment attempt failed + +## Security Considerations + +### API Security +- All Galoy API requests use authenticated connections +- API keys are securely stored and transmitted +- GraphQL queries are parameterized to prevent injection + +### Lightning Network Security +- Invoices have built-in expiration times +- Payment amounts are validated before processing +- Route probing prevents overpayment attempts + +### Transaction Integrity +- Database transactions ensure atomicity +- Serializable isolation prevents race conditions +- Transaction versioning prevents double-processing + +## Performance Characteristics + +### Lightning Advantages +- **Instant Payments**: Lightning payments confirm in seconds +- **Low Fees**: Minimal network fees compared to on-chain +- **Scalability**: High transaction throughput capability + +### System Performance +- **GraphQL Efficiency**: Single API calls for complex operations +- **Caching**: Invoice status results cached for performance +- **Batching**: Multiple probe requests processed in parallel + +### Monitoring and Observability +- Transaction logs include Lightning-specific metadata +- Payment routing information tracked for debugging +- API response times monitored for performance optimization + +## Conclusion + +The Lamassu Lightning Network integration provides a complete solution for instant Bitcoin transactions through ATMs. By leveraging Galoy's hosted Lightning infrastructure, the system achieves the benefits of Lightning Network payments without the operational complexity of running local Lightning nodes. + +Key strengths of this implementation: +- **Reliable Infrastructure**: Galoy provides enterprise-grade Lightning services +- **Simple Integration**: GraphQL API provides clean, documented interfaces +- **Real-time Processing**: Instant payment detection and confirmation +- **Comprehensive Features**: Supports full Lightning payment lifecycle +- **Robust Error Handling**: Graceful failure modes and recovery mechanisms + +This architecture enables Lamassu ATMs to offer Lightning Network functionality while maintaining the reliability and security required for financial hardware deployment. \ No newline at end of file diff --git a/misc-aio/LNBITS_CONFIG_CLARIFICATION.md b/misc-aio/LNBITS_CONFIG_CLARIFICATION.md new file mode 100644 index 00000000..94c514fd --- /dev/null +++ b/misc-aio/LNBITS_CONFIG_CLARIFICATION.md @@ -0,0 +1,95 @@ +# LNBits Configuration Clarification + +## Key Finding: walletId is NOT Required + +After reviewing the LNBits codebase, we can confirm that **walletId is not necessary** for the Lamassu-LNBits integration because **the adminKey is already wallet-specific**. + +## Evidence from LNBits Source Code + +### 1. Wallet Model Structure +```python +# packages/lnbits/lnbits/core/models/wallets.py +class Wallet(BaseModel): + id: str + user: str + name: str + adminkey: str # Each wallet has unique adminkey + inkey: str # Each wallet has unique inkey +``` + +### 2. Key-to-Wallet Lookup +```sql +-- packages/lnbits/lnbits/core/crud/wallets.py:194 +SELECT *, COALESCE(( + SELECT balance FROM balances WHERE wallet_id = wallets.id +), 0) +AS balance_msat FROM wallets +WHERE (adminkey = :key OR inkey = :key) AND deleted = false +``` + +**Key Point**: LNBits looks up wallets directly by the adminkey/inkey - no walletId parameter needed. + +### 3. Authentication Flow +```python +# packages/lnbits/lnbits/decorators.py:94 +wallet = await get_wallet_for_key(key_value) +``` + +The authentication system uses only the key to identify and authorize access to the specific wallet. + +### 4. Unique Key Generation +```python +# packages/lnbits/lnbits/core/crud/wallets.py:24-25 +adminkey=uuid4().hex, # Unique UUID for each wallet +inkey=uuid4().hex, # Unique UUID for each wallet +``` + +Each wallet gets unique keys generated as UUIDs, ensuring 1:1 key-to-wallet relationship. + +## Impact on Lamassu Integration + +### Simplified Configuration + +**Before (Incorrect Assumption)**: +```javascript +const config = { + endpoint: 'https://lnbits.example.com', + adminKey: 'abc123...', + walletId: 'wallet_xyz' // ❌ Not needed! +} +``` + +**After (Correct Implementation)**: +```javascript +const config = { + endpoint: 'https://lnbits.example.com', + adminKey: 'abc123...' // ✅ Admin key identifies wallet +} +``` + +### API Calls Simplified + +All LNBits API calls use the adminKey in the `X-API-KEY` header, which automatically identifies the target wallet: + +```javascript +// Create invoice - no walletId needed +POST /api/v1/payments +Headers: { 'X-API-KEY': 'abc123...' } +Body: { "out": false, "amount": 1000, "memo": "Purchase" } + +// Check balance - no walletId needed +GET /api/v1/wallet +Headers: { 'X-API-KEY': 'abc123...' } +``` + +## Updated Migration Benefits + +1. **Fewer Configuration Parameters** - Reduces setup complexity +2. **Eliminates Mismatch Errors** - No risk of wrong walletId/adminKey pairs +3. **Consistent with LNBits Design** - Follows intended architecture +4. **Simpler Admin UI** - Fewer fields in configuration forms +5. **Reduced Validation Logic** - Less error-prone configuration + +## Conclusion + +The walletId parameter should be **removed from the LNBits integration plan**. The adminKey serves as both authentication credential and wallet identifier, following LNBits' design principle of wallet-specific API keys. \ No newline at end of file diff --git a/misc-aio/LNBITS_REPLACEMENT_PLAN.md b/misc-aio/LNBITS_REPLACEMENT_PLAN.md new file mode 100644 index 00000000..9c58e421 --- /dev/null +++ b/misc-aio/LNBITS_REPLACEMENT_PLAN.md @@ -0,0 +1,555 @@ +# 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. \ No newline at end of file diff --git a/misc-aio/lightning_analysis.pdf b/misc-aio/lightning_analysis.pdf new file mode 100644 index 0000000000000000000000000000000000000000..30efa031507001c11d304af9767f29fd8540138c GIT binary patch literal 64317 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 LT&k+B{%%|VHp_P& literal 0 HcmV?d00001 From fc761844b712999830cfaa9ba2424ab8a1d2a216 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 21:06:00 +0200 Subject: [PATCH 02/10] feat: add LNBits wallet plugin integration - Introduced LNBits as a Lightning Network wallet provider for Lamassu ATMs. - Added configuration options for LNBits in the environment variables. - Implemented core functionalities including invoice creation, payment processing, balance monitoring, and payment status tracking. - Created unit tests for the LNBits plugin to ensure functionality and error handling. - Updated development environment setup to include LNBits configuration. --- packages/server/.sample.env | 6 + .../lib/plugins/wallet/lnbits/README.md | 144 ++++++++++ .../server/lib/plugins/wallet/lnbits/index.js | 1 + .../lib/plugins/wallet/lnbits/lnbits.js | 247 +++++++++++++++++ .../1750000000000-add-lnbits-config.js | 36 +++ .../server/tests/unit/test_lnbits_plugin.js | 257 ++++++++++++++++++ packages/server/tools/build-dev-env.js | 94 +++++-- 7 files changed, 757 insertions(+), 28 deletions(-) create mode 100644 packages/server/lib/plugins/wallet/lnbits/README.md create mode 100644 packages/server/lib/plugins/wallet/lnbits/index.js create mode 100644 packages/server/lib/plugins/wallet/lnbits/lnbits.js create mode 100644 packages/server/migrations/1750000000000-add-lnbits-config.js create mode 100644 packages/server/tests/unit/test_lnbits_plugin.js diff --git a/packages/server/.sample.env b/packages/server/.sample.env index db66efe7..4519e525 100644 --- a/packages/server/.sample.env +++ b/packages/server/.sample.env @@ -37,6 +37,12 @@ HOSTNAME= LOG_LEVEL= LIGHTNING_NETWORK_DAEMON= +## LNBits Configuration (Lightning Network) +# LNBits server URL (e.g., https://legend.lnbits.com) +LNBITS_ENDPOINT= +# Admin key for the LNBits wallet (wallet-specific, no separate walletId needed) +LNBITS_ADMIN_KEY= + # Crypto nodes related variables ## Location info (can be local or remote) diff --git a/packages/server/lib/plugins/wallet/lnbits/README.md b/packages/server/lib/plugins/wallet/lnbits/README.md new file mode 100644 index 00000000..8d29366b --- /dev/null +++ b/packages/server/lib/plugins/wallet/lnbits/README.md @@ -0,0 +1,144 @@ +# LNBits Wallet Plugin + +This plugin integrates LNBits as a Lightning Network wallet provider for Lamassu Bitcoin ATMs. + +## Features + +- **Lightning Invoice Creation**: Generate BOLT11 invoices for incoming payments +- **Payment Processing**: Send Lightning payments to any BOLT11 invoice +- **Balance Monitoring**: Check wallet balance in real-time +- **Payment Status Tracking**: Monitor invoice payment status +- **Route Probing**: Test payment feasibility before attempting +- **Network Detection**: Automatic mainnet/testnet/regtest detection + +## Configuration + +### Required Environment Variables + +```bash +LNBITS_ENDPOINT=https://your-lnbits-server.com +LNBITS_ADMIN_KEY=your_admin_key_here +``` + +**Important**: The admin key is wallet-specific in LNBits. No separate walletId is needed. + +### Database Configuration + +The plugin configuration is stored in the database and can be managed through the Lamassu admin interface. + +## API Functions + +### Core Functions + +#### `newAddress(account, info, tx)` +Creates a new Lightning invoice for receiving payments. + +**Parameters:** +- `account`: Object containing `endpoint` and `adminKey` +- `info`: Transaction info object +- `tx`: Transaction object with `cryptoAtoms` and `cryptoCode` + +**Returns:** BOLT11 Lightning invoice string + +#### `getStatus(account, tx)` +Checks the payment status of a Lightning invoice. + +**Parameters:** +- `account`: Configuration object +- `tx`: Transaction object with `toAddress` (Lightning invoice) + +**Returns:** Object with `status` field: 'confirmed', 'pending', or 'notSeen' + +#### `sendCoins(account, tx)` +Sends a Lightning payment to a BOLT11 invoice. + +**Parameters:** +- `account`: Configuration object +- `tx`: Transaction object with `toAddress` and optional `cryptoAtoms` + +**Returns:** Object with `txid` and `fee` + +#### `balance(account, cryptoCode)` +Gets the current wallet balance. + +**Parameters:** +- `account`: Configuration object +- `cryptoCode`: Must be 'LN' + +**Returns:** BigNumber balance in satoshis + +### Utility Functions + +#### `isLnInvoice(address)` +Checks if a string is a valid Lightning invoice. + +#### `isLnurl(address)` +Checks if a string is a valid LNURL. + +#### `probeLN(account, cryptoCode, invoice)` +Tests payment route feasibility for different amounts. + +#### `cryptoNetwork(account, cryptoCode)` +Detects network type from endpoint URL. + +## Error Handling + +The plugin provides detailed error messages for common issues: + +- **Configuration errors**: Missing endpoint or admin key +- **Network errors**: Connection timeouts or unreachable server +- **API errors**: Invalid requests or authentication failures +- **Invoice errors**: Invalid BOLT11 format or expired invoices + +## Testing + +Run unit tests: +```bash +npm test -- tests/unit/test_lnbits_plugin.js +``` + +## Migration from Galoy + +This plugin replaces the Galoy (Blink) integration with the following improvements: + +1. **Simpler Configuration**: Only requires endpoint and admin key +2. **RESTful API**: Easier to debug than GraphQL +3. **Self-Hosted Option**: Can run your own LNBits instance +4. **Better Error Messages**: More descriptive error handling +5. **Native Webhook Support**: Real-time payment notifications (future enhancement) + +## Security Considerations + +- Store the admin key securely (marked as `secret: true` in database) +- Use HTTPS endpoints in production +- Validate all Lightning invoices before processing +- Implement rate limiting for API calls (handled by LNBits) +- Monitor wallet balance and set alerts for low balance + +## Troubleshooting + +### Common Issues + +1. **"LNBits configuration missing"** + - Ensure both `endpoint` and `adminKey` are set + - Check environment variables or database configuration + +2. **"Invalid Lightning invoice"** + - Verify the invoice starts with 'lnbc', 'lntb', or 'lnbcrt' + - Check if the invoice has expired + +3. **"LNBits API Error: 404"** + - Payment hash not found (invoice may not exist) + - Check if using correct endpoint URL + +4. **"LNBits API Error: 401"** + - Invalid admin key + - Verify the admin key is correct and has proper permissions + +## Support + +For LNBits-specific issues, refer to: +- [LNBits Documentation](https://docs.lnbits.com) +- [LNBits GitHub](https://github.com/lnbits/lnbits) + +For Lamassu integration issues, contact Lamassu support. \ No newline at end of file diff --git a/packages/server/lib/plugins/wallet/lnbits/index.js b/packages/server/lib/plugins/wallet/lnbits/index.js new file mode 100644 index 00000000..c0209991 --- /dev/null +++ b/packages/server/lib/plugins/wallet/lnbits/index.js @@ -0,0 +1 @@ +module.exports = require('./lnbits') \ No newline at end of file diff --git a/packages/server/lib/plugins/wallet/lnbits/lnbits.js b/packages/server/lib/plugins/wallet/lnbits/lnbits.js new file mode 100644 index 00000000..e48ced7b --- /dev/null +++ b/packages/server/lib/plugins/wallet/lnbits/lnbits.js @@ -0,0 +1,247 @@ +const axios = require('axios') +const bolt11 = require('bolt11') +const _ = require('lodash/fp') +const BN = require('../../../bn') + +const NAME = 'LNBits' +const SUPPORTED_COINS = ['LN'] + +function checkCryptoCode(cryptoCode) { + if (!SUPPORTED_COINS.includes(cryptoCode)) { + return Promise.reject(new Error(`Unsupported crypto: ${cryptoCode}`)) + } + return Promise.resolve() +} + +function validateConfig(account) { + const required = ['endpoint', 'adminKey'] + for (const field of required) { + if (!account[field]) { + throw new Error(`LNBits configuration missing: ${field}`) + } + } + + try { + new URL(account.endpoint) + } catch (error) { + throw new Error(`Invalid LNBits endpoint URL: ${account.endpoint}`) + } +} + +async function request(endpoint, method = 'GET', data = null, apiKey) { + const config = { + method, + url: endpoint, + headers: { + 'Content-Type': 'application/json', + 'X-API-KEY': apiKey + }, + timeout: 30000 + } + + if (data) { + config.data = data + } + + try { + const response = await axios(config) + return response.data + } catch (error) { + if (error.response) { + const detail = error.response.data?.detail || error.response.statusText + throw new Error(`LNBits API Error: ${detail} (${error.response.status})`) + } + throw new Error(`LNBits Network Error: ${error.message}`) + } +} + +function extractPaymentHash(bolt11Invoice) { + try { + const decoded = bolt11.decode(bolt11Invoice) + return decoded.tagsObject.payment_hash + } catch (error) { + throw new Error(`Invalid Lightning invoice: ${error.message}`) + } +} + +function isLnInvoice(address) { + if (!address || typeof address !== 'string') return false + return address.toLowerCase().startsWith('lnbc') || + address.toLowerCase().startsWith('lntb') || + address.toLowerCase().startsWith('lnbcrt') +} + +function isLnurl(address) { + if (!address || typeof address !== 'string') return false + return address.toLowerCase().startsWith('lnurl') +} + +async function newAddress(account, info, tx) { + validateConfig(account) + const { cryptoAtoms, cryptoCode } = tx + + await checkCryptoCode(cryptoCode) + + const invoiceData = { + out: false, + amount: parseInt(cryptoAtoms.toString()), + unit: 'sat', + memo: `Lamassu ATM - ${new Date().toISOString()}`, + expiry: 3600 + } + + const endpoint = `${account.endpoint}/api/v1/payments` + const result = await request(endpoint, 'POST', invoiceData, account.adminKey) + + if (!result.payment_request) { + throw new Error('LNBits did not return a payment request') + } + + return result.payment_request +} + +async function getStatus(account, tx) { + validateConfig(account) + const { toAddress, cryptoCode } = tx + + await checkCryptoCode(cryptoCode) + + if (!isLnInvoice(toAddress)) { + return { status: 'notSeen' } + } + + const paymentHash = extractPaymentHash(toAddress) + const endpoint = `${account.endpoint}/api/v1/payments/${paymentHash}` + + try { + const result = await request(endpoint, 'GET', null, account.adminKey) + + if (result.paid === true) { + return { status: 'confirmed' } + } else if (result.paid === false && result.pending === true) { + return { status: 'pending' } + } else { + return { status: 'notSeen' } + } + } catch (error) { + if (error.message.includes('404')) { + return { status: 'notSeen' } + } + throw error + } +} + +async function sendCoins(account, tx) { + validateConfig(account) + const { toAddress, cryptoAtoms, cryptoCode } = tx + + await checkCryptoCode(cryptoCode) + + const paymentData = { + out: true, + bolt11: toAddress + } + + const decoded = bolt11.decode(toAddress) + const invoiceAmount = decoded.satoshis + + if (!invoiceAmount || invoiceAmount === 0) { + paymentData.amount = parseInt(cryptoAtoms.toString()) + } + + const endpoint = `${account.endpoint}/api/v1/payments` + const result = await request(endpoint, 'POST', paymentData, account.adminKey) + + if (!result.payment_hash) { + throw new Error('LNBits payment failed: No payment hash returned') + } + + return { + txid: result.payment_hash, + fee: result.fee_msat ? Math.ceil(result.fee_msat / 1000) : 0 + } +} + +async function balance(account, cryptoCode) { + validateConfig(account) + await checkCryptoCode(cryptoCode) + + const endpoint = `${account.endpoint}/api/v1/wallet` + const result = await request(endpoint, 'GET', null, account.adminKey) + + if (result.balance === undefined) { + throw new Error('LNBits did not return wallet balance') + } + + const balanceSats = Math.floor(result.balance / 1000) + return new BN(balanceSats) +} + +async function newFunding(account, cryptoCode) { + await checkCryptoCode(cryptoCode) + validateConfig(account) + + const promises = [ + balance(account, cryptoCode), + newAddress(account, { cryptoCode }, { cryptoCode, cryptoAtoms: new BN(100000) }) + ] + + const [walletBalance, fundingAddress] = await Promise.all(promises) + + return { + fundingAddress, + fundingAddressQr: fundingAddress, + confirmed: walletBalance.gte(0), + confirmedBalance: walletBalance.toString() + } +} + +async function probeLN(account, cryptoCode, invoice) { + validateConfig(account) + await checkCryptoCode(cryptoCode) + + if (!isLnInvoice(invoice)) { + throw new Error('Invalid Lightning invoice provided for probe') + } + + const endpoint = `${account.endpoint}/api/v1/payments/decode` + const decodeData = { data: invoice } + + try { + const result = await request(endpoint, 'POST', decodeData, account.adminKey) + + const amountSats = result.amount_msat ? Math.floor(result.amount_msat / 1000) : 0 + + const limits = [200000, 1000000, 2000000] + const probeResults = limits.map(limit => amountSats <= limit) + + return probeResults + } catch (error) { + console.error('LNBits probe error:', error.message) + return [false, false, false] + } +} + +function cryptoNetwork(account, cryptoCode) { + const endpoint = account.endpoint || '' + if (endpoint.includes('testnet') || endpoint.includes('test')) { + return 'test' + } + if (endpoint.includes('regtest') || endpoint.includes('local')) { + return 'regtest' + } + return 'main' +} + +module.exports = { + NAME, + balance, + sendCoins, + newAddress, + getStatus, + newFunding, + probeLN, + cryptoNetwork, + isLnInvoice, + isLnurl +} \ No newline at end of file diff --git a/packages/server/migrations/1750000000000-add-lnbits-config.js b/packages/server/migrations/1750000000000-add-lnbits-config.js new file mode 100644 index 00000000..6e9c34a0 --- /dev/null +++ b/packages/server/migrations/1750000000000-add-lnbits-config.js @@ -0,0 +1,36 @@ +const db = require('./db') + +exports.up = function (next) { + const sql = ` + INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) + VALUES + ('lnbitsEndpoint', 'LNBits Server URL', 'text', 'string', 'wallets', false, false, null), + ('lnbitsAdminKey', 'LNBits Admin Key', 'text', 'string', 'wallets', false, true, null) + ON CONFLICT (name) DO NOTHING; + + -- Add LNBits as a valid wallet option for Lightning Network + INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) + VALUES + ('LN_wallet', 'Lightning Network Wallet', 'text', 'string', 'wallets', true, false, + '[{"code": "lnbits", "display": "LNBits"}, {"code": "galoy", "display": "Galoy (Blink)"}, {"code": "bitcoind", "display": "Bitcoin Core"}]') + ON CONFLICT (name) + DO UPDATE SET options = EXCLUDED.options + WHERE user_config.options NOT LIKE '%lnbits%'; + ` + + db.multi(sql, next) +} + +exports.down = function (next) { + const sql = ` + DELETE FROM user_config + WHERE name IN ('lnbitsEndpoint', 'lnbitsAdminKey'); + + -- Remove LNBits from wallet options + UPDATE user_config + SET options = REPLACE(options, ', {"code": "lnbits", "display": "LNBits"}', '') + WHERE name = 'LN_wallet'; + ` + + db.multi(sql, next) +} \ No newline at end of file diff --git a/packages/server/tests/unit/test_lnbits_plugin.js b/packages/server/tests/unit/test_lnbits_plugin.js new file mode 100644 index 00000000..f265b8c9 --- /dev/null +++ b/packages/server/tests/unit/test_lnbits_plugin.js @@ -0,0 +1,257 @@ +const test = require('tape') +const sinon = require('sinon') +const axios = require('axios') +const BN = require('../../lib/bn') + +// Mock the module before requiring the plugin +const lnbits = require('../../lib/plugins/wallet/lnbits/lnbits') + +test('LNBits plugin - configuration validation', t => { + t.plan(3) + + const validAccount = { + endpoint: 'https://demo.lnbits.com', + adminKey: 'test_admin_key_123' + } + + const invalidAccount1 = { + endpoint: 'https://demo.lnbits.com' + // Missing adminKey + } + + const invalidAccount2 = { + adminKey: 'test_admin_key_123' + // Missing endpoint + } + + // Valid config should not throw + t.doesNotThrow(() => { + lnbits.balance(validAccount, 'LN').catch(() => {}) + }, 'Valid configuration passes validation') + + // Missing adminKey should throw + lnbits.balance(invalidAccount1, 'LN').catch(err => { + t.ok(err.message.includes('LNBits configuration missing: adminKey'), + 'Missing adminKey throws appropriate error') + }) + + // Missing endpoint should throw + lnbits.balance(invalidAccount2, 'LN').catch(err => { + t.ok(err.message.includes('LNBits configuration missing: endpoint'), + 'Missing endpoint throws appropriate error') + }) +}) + +test('LNBits plugin - crypto code validation', t => { + t.plan(2) + + const account = { + endpoint: 'https://demo.lnbits.com', + adminKey: 'test_admin_key' + } + + // Valid crypto code + lnbits.balance(account, 'LN').catch(() => { + // Expected to fail at API call, not validation + }) + t.pass('LN crypto code is accepted') + + // Invalid crypto code + lnbits.balance(account, 'BTC').catch(err => { + t.ok(err.message.includes('Unsupported crypto'), + 'Invalid crypto code throws appropriate error') + }) +}) + +test('LNBits plugin - invoice detection', t => { + t.plan(6) + + // Valid Lightning invoices + t.ok(lnbits.isLnInvoice('lnbc100n1p0example'), 'Detects mainnet Lightning invoice') + t.ok(lnbits.isLnInvoice('lntb100n1p0example'), 'Detects testnet Lightning invoice') + t.ok(lnbits.isLnInvoice('lnbcrt100n1p0example'), 'Detects regtest Lightning invoice') + + // Invalid invoices + t.notOk(lnbits.isLnInvoice('bc1qexample'), 'Rejects Bitcoin address') + t.notOk(lnbits.isLnInvoice('lnurl1234'), 'Rejects LNURL') + t.notOk(lnbits.isLnInvoice(''), 'Rejects empty string') +}) + +test('LNBits plugin - LNURL detection', t => { + t.plan(3) + + t.ok(lnbits.isLnurl('lnurl1dp68gurn8ghj7um9'), 'Detects LNURL') + t.notOk(lnbits.isLnurl('lnbc100n1p0example'), 'Rejects Lightning invoice') + t.notOk(lnbits.isLnurl(''), 'Rejects empty string') +}) + +test('LNBits plugin - network detection', t => { + t.plan(4) + + const mainnetAccount = { + endpoint: 'https://lnbits.com', + adminKey: 'test' + } + + const testnetAccount = { + endpoint: 'https://testnet.lnbits.com', + adminKey: 'test' + } + + const regtestAccount = { + endpoint: 'http://localhost:5000', + adminKey: 'test' + } + + const testAccount = { + endpoint: 'https://test.lnbits.com', + adminKey: 'test' + } + + t.equal(lnbits.cryptoNetwork(mainnetAccount, 'LN'), 'main', 'Detects mainnet') + t.equal(lnbits.cryptoNetwork(testnetAccount, 'LN'), 'test', 'Detects testnet') + t.equal(lnbits.cryptoNetwork(regtestAccount, 'LN'), 'regtest', 'Detects regtest/local') + t.equal(lnbits.cryptoNetwork(testAccount, 'LN'), 'test', 'Detects test environment') +}) + +test('LNBits plugin - newAddress creates invoice', async t => { + t.plan(3) + + const account = { + endpoint: 'https://demo.lnbits.com', + adminKey: 'test_admin_key' + } + + const tx = { + cryptoAtoms: new BN('100000'), + cryptoCode: 'LN' + } + + const info = { + cryptoCode: 'LN' + } + + // Mock the axios request + const stub = sinon.stub(axios, 'request') + stub.resolves({ + data: { + payment_request: 'lnbc1000n1p0example', + payment_hash: 'abc123' + } + }) + + try { + const invoice = await lnbits.newAddress(account, info, tx) + + t.equal(invoice, 'lnbc1000n1p0example', 'Returns Lightning invoice') + t.ok(stub.calledOnce, 'Makes one API request') + + const callArgs = stub.firstCall.args[0] + t.equal(callArgs.data.amount, 100000, 'Sends correct amount in satoshis') + } catch (err) { + t.fail(`Unexpected error: ${err.message}`) + } finally { + stub.restore() + } +}) + +test('LNBits plugin - balance returns BN', async t => { + t.plan(2) + + const account = { + endpoint: 'https://demo.lnbits.com', + adminKey: 'test_admin_key' + } + + // Mock the axios request + const stub = sinon.stub(axios, 'request') + stub.resolves({ + data: { + balance: 500000000, // 500000 sats in millisats + name: 'Test Wallet' + } + }) + + try { + const balance = await lnbits.balance(account, 'LN') + + t.ok(balance instanceof BN, 'Returns BigNumber instance') + t.equal(balance.toString(), '500000', 'Converts millisats to sats correctly') + } catch (err) { + t.fail(`Unexpected error: ${err.message}`) + } finally { + stub.restore() + } +}) + +test('LNBits plugin - getStatus checks payment', async t => { + t.plan(3) + + const account = { + endpoint: 'https://demo.lnbits.com', + adminKey: 'test_admin_key' + } + + const paidTx = { + toAddress: 'lnbc1000n1p3q7vlpp5example', + cryptoCode: 'LN' + } + + // Mock bolt11 decode + const bolt11 = require('bolt11') + const decodeStub = sinon.stub(bolt11, 'decode') + decodeStub.returns({ + tagsObject: { + payment_hash: 'abc123' + } + }) + + // Mock axios for paid invoice + const axiosStub = sinon.stub(axios, 'request') + axiosStub.onFirstCall().resolves({ + data: { + paid: true, + amount: 1000 + } + }) + + try { + const status = await lnbits.getStatus(account, paidTx) + t.equal(status.status, 'confirmed', 'Returns confirmed for paid invoice') + } catch (err) { + t.fail(`Unexpected error: ${err.message}`) + } + + // Test pending invoice + axiosStub.onSecondCall().resolves({ + data: { + paid: false, + pending: true + } + }) + + try { + const status = await lnbits.getStatus(account, paidTx) + t.equal(status.status, 'pending', 'Returns pending for unpaid but pending invoice') + } catch (err) { + t.fail(`Unexpected error: ${err.message}`) + } + + // Test not found invoice + axiosStub.onThirdCall().rejects({ + response: { + status: 404, + data: { detail: 'Payment not found' } + } + }) + + try { + const status = await lnbits.getStatus(account, paidTx) + t.equal(status.status, 'notSeen', 'Returns notSeen for not found invoice') + } catch (err) { + t.fail(`Unexpected error: ${err.message}`) + } finally { + decodeStub.restore() + axiosStub.restore() + } +}) \ No newline at end of file diff --git a/packages/server/tools/build-dev-env.js b/packages/server/tools/build-dev-env.js index 9a2b2d20..dc1849f0 100644 --- a/packages/server/tools/build-dev-env.js +++ b/packages/server/tools/build-dev-env.js @@ -1,38 +1,76 @@ +#!/usr/bin/env node + const fs = require('fs') const path = require('path') -const setEnvVariable = require('./set-env-var') +const envPath = path.resolve(__dirname, '../.env') +const sampleEnvPath = path.resolve(__dirname, '../.sample.env') -fs.copyFileSync( - path.resolve(__dirname, '../.sample.env'), - path.resolve(__dirname, '../.env'), -) +// Check if .env already exists +if (fs.existsSync(envPath)) { + console.log('.env file already exists. To rebuild, delete it first.') + process.exit(0) +} -setEnvVariable('NODE_ENV', 'development') +// Copy sample env +const sampleContent = fs.readFileSync(sampleEnvPath, 'utf8') -setEnvVariable('POSTGRES_USER', 'postgres') -setEnvVariable('POSTGRES_PASSWORD', 'postgres123') -setEnvVariable('POSTGRES_HOST', 'localhost') -setEnvVariable('POSTGRES_PORT', '5432') -setEnvVariable('POSTGRES_DB', 'lamassu') +// Development defaults +const devDefaults = { + NODE_ENV: 'development', + + // Database + POSTGRES_USER: 'lamassu', + POSTGRES_PASSWORD: 'lamassu', + POSTGRES_HOST: 'localhost', + POSTGRES_PORT: '5432', + POSTGRES_DB: 'lamassu', + + // Paths + CA_PATH: path.resolve(__dirname, '../Lamassu_CA.pem'), + CERT_PATH: path.resolve(__dirname, '../../../certs/Lamassu_LS.pem'), + KEY_PATH: path.resolve(__dirname, '../../../certs/Lamassu_LS.key'), + MNEMONIC_PATH: path.resolve(__dirname, '../../../mnemonic.txt'), + + // Directories + BLOCKCHAIN_DIR: path.resolve(__dirname, '../../../blockchain'), + OFAC_DATA_DIR: path.resolve(__dirname, '../../../ofac'), + ID_PHOTO_CARD_DIR: path.resolve(__dirname, '../../../photos/idcard'), + FRONT_CAMERA_DIR: path.resolve(__dirname, '../../../photos/front'), + OPERATOR_DATA_DIR: path.resolve(__dirname, '../../../operator-data'), + + // Misc + HOSTNAME: 'localhost', + LOG_LEVEL: 'debug', + + // Bitcoin (for development, use remote node to avoid full sync) + BTC_NODE_LOCATION: 'remote', + BTC_WALLET_LOCATION: 'local', + BTC_NODE_HOST: 'blockstream.info', + BTC_NODE_PORT: '8333', + + // LNBits development defaults + LNBITS_ENDPOINT: 'https://legend.lnbits.com', + LNBITS_ADMIN_KEY: '' // User needs to set this +} -setEnvVariable('CA_PATH', `${process.env.PWD}/certs/Lamassu_OP_Root_CA.pem`) -setEnvVariable('CERT_PATH', `${process.env.PWD}/certs/Lamassu_OP.pem`) -setEnvVariable('KEY_PATH', `${process.env.PWD}/certs/Lamassu_OP.key`) +// Build .env content +let envContent = sampleContent -setEnvVariable( - 'MNEMONIC_PATH', - `${process.env.PWD}/.lamassu/mnemonics/mnemonic.txt`, -) +// Replace empty values with dev defaults +Object.keys(devDefaults).forEach(key => { + const regex = new RegExp(`^${key}=.*$`, 'gm') + envContent = envContent.replace(regex, `${key}=${devDefaults[key]}`) +}) -setEnvVariable('BLOCKCHAIN_DIR', `${process.env.PWD}/blockchains`) -setEnvVariable('OFAC_DATA_DIR', `${process.env.PWD}/.lamassu/ofac`) -setEnvVariable('ID_PHOTO_CARD_DIR', `${process.env.PWD}/.lamassu/idphotocard`) -setEnvVariable('FRONT_CAMERA_DIR', `${process.env.PWD}/.lamassu/frontcamera`) -setEnvVariable('OPERATOR_DATA_DIR', `${process.env.PWD}/.lamassu/operatordata`) +// Write .env file +fs.writeFileSync(envPath, envContent) -setEnvVariable('BTC_NODE_LOCATION', 'remote') -setEnvVariable('BTC_WALLET_LOCATION', 'local') - -setEnvVariable('HOSTNAME', 'localhost') -setEnvVariable('LOG_LEVEL', 'debug') +console.log('Development .env file created with defaults.') +console.log('IMPORTANT: You still need to:') +console.log(' 1. Generate certificates using: bash tools/cert-gen.sh') +console.log(' 2. Create a mnemonic file at: ../../../mnemonic.txt') +console.log(' 3. Set up PostgreSQL database') +console.log(' 4. Configure LNBits admin key if using Lightning') +console.log('') +console.log('Run migrations with: node bin/lamassu-migrate') \ No newline at end of file From 67f62169d1f27ff818dd817673d86ad7f1397ddc Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 21:07:38 +0200 Subject: [PATCH 03/10] chore: add placeholder migration files for previously applied migrations - Introduced placeholder migration files to satisfy the migration system for various compliance triggers and machine groups. - Each file includes no-op functions for both 'up' and 'down' migrations, indicating that the migrations were already applied to the database. --- .../1749551637988-relational-compliance-triggers.js | 12 ++++++++++++ .../migrations/1751291688761-add-machine-groups.js | 12 ++++++++++++ .../1752141860742-compliance-triggers-sets.js | 12 ++++++++++++ .../migrations/1752599801402-add-coupon-fk-to-txs.js | 12 ++++++++++++ ...76-machine-groups-with-compliance-trigger-sets.js | 12 ++++++++++++ 5 files changed, 60 insertions(+) create mode 100644 packages/server/migrations/1749551637988-relational-compliance-triggers.js create mode 100644 packages/server/migrations/1751291688761-add-machine-groups.js create mode 100644 packages/server/migrations/1752141860742-compliance-triggers-sets.js create mode 100644 packages/server/migrations/1752599801402-add-coupon-fk-to-txs.js create mode 100644 packages/server/migrations/1753351586476-machine-groups-with-compliance-trigger-sets.js diff --git a/packages/server/migrations/1749551637988-relational-compliance-triggers.js b/packages/server/migrations/1749551637988-relational-compliance-triggers.js new file mode 100644 index 00000000..799a8de6 --- /dev/null +++ b/packages/server/migrations/1749551637988-relational-compliance-triggers.js @@ -0,0 +1,12 @@ +// This migration was already applied to the database +// This is a placeholder file to satisfy the migration system + +exports.up = function (next) { + // Migration already applied - no-op + next() +} + +exports.down = function (next) { + // No-op + next() +} \ No newline at end of file diff --git a/packages/server/migrations/1751291688761-add-machine-groups.js b/packages/server/migrations/1751291688761-add-machine-groups.js new file mode 100644 index 00000000..799a8de6 --- /dev/null +++ b/packages/server/migrations/1751291688761-add-machine-groups.js @@ -0,0 +1,12 @@ +// This migration was already applied to the database +// This is a placeholder file to satisfy the migration system + +exports.up = function (next) { + // Migration already applied - no-op + next() +} + +exports.down = function (next) { + // No-op + next() +} \ No newline at end of file diff --git a/packages/server/migrations/1752141860742-compliance-triggers-sets.js b/packages/server/migrations/1752141860742-compliance-triggers-sets.js new file mode 100644 index 00000000..799a8de6 --- /dev/null +++ b/packages/server/migrations/1752141860742-compliance-triggers-sets.js @@ -0,0 +1,12 @@ +// This migration was already applied to the database +// This is a placeholder file to satisfy the migration system + +exports.up = function (next) { + // Migration already applied - no-op + next() +} + +exports.down = function (next) { + // No-op + next() +} \ No newline at end of file diff --git a/packages/server/migrations/1752599801402-add-coupon-fk-to-txs.js b/packages/server/migrations/1752599801402-add-coupon-fk-to-txs.js new file mode 100644 index 00000000..799a8de6 --- /dev/null +++ b/packages/server/migrations/1752599801402-add-coupon-fk-to-txs.js @@ -0,0 +1,12 @@ +// This migration was already applied to the database +// This is a placeholder file to satisfy the migration system + +exports.up = function (next) { + // Migration already applied - no-op + next() +} + +exports.down = function (next) { + // No-op + next() +} \ No newline at end of file diff --git a/packages/server/migrations/1753351586476-machine-groups-with-compliance-trigger-sets.js b/packages/server/migrations/1753351586476-machine-groups-with-compliance-trigger-sets.js new file mode 100644 index 00000000..799a8de6 --- /dev/null +++ b/packages/server/migrations/1753351586476-machine-groups-with-compliance-trigger-sets.js @@ -0,0 +1,12 @@ +// This migration was already applied to the database +// This is a placeholder file to satisfy the migration system + +exports.up = function (next) { + // Migration already applied - no-op + next() +} + +exports.down = function (next) { + // No-op + next() +} \ No newline at end of file From abe45c49f18744fbc04587d8c92711186ef5a0fc Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 21:07:59 +0200 Subject: [PATCH 04/10] refactor: streamline LNBits migration SQL statements - Updated the migration script for LNBits configuration to use an array for SQL statements, improving readability and maintainability. - Consolidated the insertion and deletion operations for user configuration related to LNBits and Lightning Network wallet options. --- .../1750000000000-add-lnbits-config.js | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/server/migrations/1750000000000-add-lnbits-config.js b/packages/server/migrations/1750000000000-add-lnbits-config.js index 6e9c34a0..35900fad 100644 --- a/packages/server/migrations/1750000000000-add-lnbits-config.js +++ b/packages/server/migrations/1750000000000-add-lnbits-config.js @@ -1,36 +1,34 @@ const db = require('./db') exports.up = function (next) { - const sql = ` - INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) - VALUES - ('lnbitsEndpoint', 'LNBits Server URL', 'text', 'string', 'wallets', false, false, null), - ('lnbitsAdminKey', 'LNBits Admin Key', 'text', 'string', 'wallets', false, true, null) - ON CONFLICT (name) DO NOTHING; + const sql = [ + `INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) + VALUES + ('lnbitsEndpoint', 'LNBits Server URL', 'text', 'string', 'wallets', false, false, null), + ('lnbitsAdminKey', 'LNBits Admin Key', 'text', 'string', 'wallets', false, true, null) + ON CONFLICT (name) DO NOTHING`, - -- Add LNBits as a valid wallet option for Lightning Network - INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) - VALUES - ('LN_wallet', 'Lightning Network Wallet', 'text', 'string', 'wallets', true, false, - '[{"code": "lnbits", "display": "LNBits"}, {"code": "galoy", "display": "Galoy (Blink)"}, {"code": "bitcoind", "display": "Bitcoin Core"}]') - ON CONFLICT (name) - DO UPDATE SET options = EXCLUDED.options - WHERE user_config.options NOT LIKE '%lnbits%'; - ` + `INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) + VALUES + ('LN_wallet', 'Lightning Network Wallet', 'text', 'string', 'wallets', true, false, + '[{"code": "lnbits", "display": "LNBits"}, {"code": "galoy", "display": "Galoy (Blink)"}, {"code": "bitcoind", "display": "Bitcoin Core"}]') + ON CONFLICT (name) + DO UPDATE SET options = EXCLUDED.options + WHERE user_config.options NOT LIKE '%lnbits%'` + ] db.multi(sql, next) } exports.down = function (next) { - const sql = ` - DELETE FROM user_config - WHERE name IN ('lnbitsEndpoint', 'lnbitsAdminKey'); + const sql = [ + `DELETE FROM user_config + WHERE name IN ('lnbitsEndpoint', 'lnbitsAdminKey')`, - -- Remove LNBits from wallet options - UPDATE user_config - SET options = REPLACE(options, ', {"code": "lnbits", "display": "LNBits"}', '') - WHERE name = 'LN_wallet'; - ` + `UPDATE user_config + SET options = REPLACE(options, ', {"code": "lnbits", "display": "LNBits"}', '') + WHERE name = 'LN_wallet'` + ] db.multi(sql, next) } \ No newline at end of file From 0f64df1d694ffd78ba1afae35684f0e4c0ef3d0d Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 21:09:23 +0200 Subject: [PATCH 05/10] refactor: update LNBits migration to use configuration object - Replaced SQL statements with a configuration object for LNBits settings, enhancing code clarity and maintainability. - Simplified the migration process by utilizing the saveConfig function for applying configurations. - Marked the down migration as a no-op to prevent breaking existing configurations. --- .../1750000000000-add-lnbits-config.js | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/packages/server/migrations/1750000000000-add-lnbits-config.js b/packages/server/migrations/1750000000000-add-lnbits-config.js index 35900fad..351f0b13 100644 --- a/packages/server/migrations/1750000000000-add-lnbits-config.js +++ b/packages/server/migrations/1750000000000-add-lnbits-config.js @@ -1,34 +1,17 @@ -const db = require('./db') +const { saveConfig } = require('../lib/new-settings-loader') exports.up = function (next) { - const sql = [ - `INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) - VALUES - ('lnbitsEndpoint', 'LNBits Server URL', 'text', 'string', 'wallets', false, false, null), - ('lnbitsAdminKey', 'LNBits Admin Key', 'text', 'string', 'wallets', false, true, null) - ON CONFLICT (name) DO NOTHING`, - - `INSERT INTO user_config (name, display_name, type, data_type, config_type, enabled, secret, options) - VALUES - ('LN_wallet', 'Lightning Network Wallet', 'text', 'string', 'wallets', true, false, - '[{"code": "lnbits", "display": "LNBits"}, {"code": "galoy", "display": "Galoy (Blink)"}, {"code": "bitcoind", "display": "Bitcoin Core"}]') - ON CONFLICT (name) - DO UPDATE SET options = EXCLUDED.options - WHERE user_config.options NOT LIKE '%lnbits%'` - ] + const config = { + 'lnbits_endpoint': '', + 'lnbits_adminKey': '', + 'LN_wallet': 'lnbits' + } - db.multi(sql, next) + saveConfig(config).then(next).catch(next) } exports.down = function (next) { - const sql = [ - `DELETE FROM user_config - WHERE name IN ('lnbitsEndpoint', 'lnbitsAdminKey')`, - - `UPDATE user_config - SET options = REPLACE(options, ', {"code": "lnbits", "display": "LNBits"}', '') - WHERE name = 'LN_wallet'` - ] - - db.multi(sql, next) + // No-op - removing config entries is not typically done in down migrations + // as it could break existing configurations + next() } \ No newline at end of file From ee625a91e9d731afbc4859995a40c86b6d9b825a Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 21:55:22 +0200 Subject: [PATCH 06/10] feat: integrate LNBits wallet schema and configuration - Added LNBits wallet schema to the admin UI, including validation and input components. - Updated the services index to include LNBits in the available wallet options. - Enhanced the wallet selection component to handle LNBits configuration input. --- .../src/pages/Services/schemas/index.js | 2 ++ .../src/pages/Services/schemas/lnbits.js | 36 +++++++++++++++++++ .../Wizard/components/Wallet/ChooseWallet.jsx | 15 +++++++- .../server/lib/new-admin/config/accounts.js | 1 + 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 packages/admin-ui/src/pages/Services/schemas/lnbits.js diff --git a/packages/admin-ui/src/pages/Services/schemas/index.js b/packages/admin-ui/src/pages/Services/schemas/index.js index 695aa598..f6519a95 100644 --- a/packages/admin-ui/src/pages/Services/schemas/index.js +++ b/packages/admin-ui/src/pages/Services/schemas/index.js @@ -11,6 +11,7 @@ import inforu from './inforu' import infura from './infura' import _itbit from './itbit' import _kraken from './kraken' +import lnbits from './lnbits' import mailgun from './mailgun' import scorechain from './scorechain' import sumsub from './sumsub' @@ -31,6 +32,7 @@ const schemas = (markets = {}) => { return { [bitgo.code]: bitgo, [galoy.code]: galoy, + [lnbits.code]: lnbits, [bitstamp.code]: bitstamp, [blockcypher.code]: blockcypher, [elliptic.code]: elliptic, diff --git a/packages/admin-ui/src/pages/Services/schemas/lnbits.js b/packages/admin-ui/src/pages/Services/schemas/lnbits.js new file mode 100644 index 00000000..00d03e5a --- /dev/null +++ b/packages/admin-ui/src/pages/Services/schemas/lnbits.js @@ -0,0 +1,36 @@ +import * as Yup from 'yup' + +import { + SecretInput, + TextInput, +} from '../../../components/inputs/formik' + +import { secretTest } from './helper' + +export default { + code: 'lnbits', + name: 'LNBits', + title: 'LNBits (Wallet)', + elements: [ + { + code: 'endpoint', + display: 'LNBits Server URL', + component: TextInput, + }, + { + code: 'adminKey', + display: 'Admin Key', + component: SecretInput, + }, + ], + getValidationSchema: account => { + return Yup.object().shape({ + endpoint: Yup.string('The endpoint must be a string') + .max(200, 'The endpoint is too long') + .required('The endpoint is required'), + adminKey: Yup.string('The Admin Key must be a string') + .max(200, 'The Admin Key is too long') + .test(secretTest(account?.adminKey)), + }) + }, +} \ No newline at end of file diff --git a/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx b/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx index c99295b8..f46bb5f0 100644 --- a/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx +++ b/packages/admin-ui/src/pages/Wizard/components/Wallet/ChooseWallet.jsx @@ -36,7 +36,7 @@ const SAVE_ACCOUNTS = gql` ` const isConfigurable = it => - R.includes(it)(['infura', 'bitgo', 'trongrid', 'galoy']) + R.includes(it)(['infura', 'bitgo', 'trongrid', 'galoy', 'lnbits']) const isLocalHosted = it => R.includes(it)([ @@ -178,6 +178,19 @@ const ChooseWallet = ({ data: currentData, addData }) => { /> )} + {selected === 'lnbits' && ( + <> +

Enter wallet information

+ + + )} ) } diff --git a/packages/server/lib/new-admin/config/accounts.js b/packages/server/lib/new-admin/config/accounts.js index 87a3e35a..e955082b 100644 --- a/packages/server/lib/new-admin/config/accounts.js +++ b/packages/server/lib/new-admin/config/accounts.js @@ -103,6 +103,7 @@ const ALL_ACCOUNTS = [ cryptos: [BTC, ZEC, LTC, BCH, DASH], }, { code: 'galoy', display: 'Galoy', class: WALLET, cryptos: [LN] }, + { code: 'lnbits', display: 'LNBits', class: WALLET, cryptos: [LN] }, { code: 'bitstamp', display: 'Bitstamp', From 31f12080466958bbd6dd15f3470868719cc293e9 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 23:09:00 +0200 Subject: [PATCH 07/10] feat: add bolt11 library for Lightning Network invoice handling - Included the bolt11 library in the server package to facilitate the creation and parsing of Lightning Network invoices. --- packages/server/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/package.json b/packages/server/package.json index 3b8658c7..800e8313 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -32,6 +32,7 @@ "bchaddrjs": "^0.3.0", "bignumber.js": "9.0.1", "bip39": "^2.3.1", + "bolt11": "^1.4.1", "ccxt": "2.9.16", "compression": "^1.7.4", "connect-pg-simple": "^6.2.1", From 7ebd809abc77385812606e11bedccc9f0b87e52b Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 23:09:18 +0200 Subject: [PATCH 08/10] feat: implement LNURL payment handling in LNBits plugin - Added a new function to handle LNURL payments, allowing users to send payments via LNURL addresses. - Integrated LNURL payment processing into the existing sendCoins function, enhancing the wallet's capabilities for Lightning Network transactions. --- .../lib/plugins/wallet/lnbits/lnbits.js | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/server/lib/plugins/wallet/lnbits/lnbits.js b/packages/server/lib/plugins/wallet/lnbits/lnbits.js index e48ced7b..44a0099f 100644 --- a/packages/server/lib/plugins/wallet/lnbits/lnbits.js +++ b/packages/server/lib/plugins/wallet/lnbits/lnbits.js @@ -131,12 +131,44 @@ async function getStatus(account, tx) { } } +async function sendLNURL(account, lnurl, cryptoAtoms) { + validateConfig(account) + + const paymentData = { + lnurl: lnurl, + amount: parseInt(cryptoAtoms.toString()) * 1000, // Convert satoshis to millisatoshis + comment: `Lamassu ATM - ${new Date().toISOString()}` + } + + const endpoint = `${account.endpoint}/api/v1/payments/lnurl` + const result = await request(endpoint, 'POST', paymentData, account.adminKey) + + if (!result.payment_hash) { + throw new Error('LNBits LNURL payment failed: No payment hash returned') + } + + return { + txid: result.payment_hash, + fee: result.fee_msat ? Math.ceil(result.fee_msat / 1000) : 0 + } +} + async function sendCoins(account, tx) { validateConfig(account) const { toAddress, cryptoAtoms, cryptoCode } = tx await checkCryptoCode(cryptoCode) + // Handle LNURL addresses + if (isLnurl(toAddress)) { + return sendLNURL(account, toAddress, cryptoAtoms) + } + + // Handle bolt11 invoices + if (!isLnInvoice(toAddress)) { + throw new Error('Invalid Lightning address: must be bolt11 invoice or LNURL') + } + const paymentData = { out: true, bolt11: toAddress From c58e4e330ca0c3cab6ff32845ea15302ba7aca4d Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 23:26:14 +0200 Subject: [PATCH 09/10] fix: update LNBits payment request handling - Changed the response handling in the newAddress function to return the bolt11 invoice instead of the payment request. - Updated error message to reflect the change in response structure from LNBits. --- packages/server/lib/plugins/wallet/lnbits/lnbits.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/lib/plugins/wallet/lnbits/lnbits.js b/packages/server/lib/plugins/wallet/lnbits/lnbits.js index 44a0099f..93cdb4be 100644 --- a/packages/server/lib/plugins/wallet/lnbits/lnbits.js +++ b/packages/server/lib/plugins/wallet/lnbits/lnbits.js @@ -93,11 +93,11 @@ async function newAddress(account, info, tx) { const endpoint = `${account.endpoint}/api/v1/payments` const result = await request(endpoint, 'POST', invoiceData, account.adminKey) - if (!result.payment_request) { - throw new Error('LNBits did not return a payment request') + if (!result.bolt11) { + throw new Error('LNBits did not return a bolt11 invoice') } - return result.payment_request + return result.bolt11 } async function getStatus(account, tx) { From 78840f115f52f41c51da60d8c07f17959816a6f6 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 13 Sep 2025 17:57:39 +0200 Subject: [PATCH 10/10] fix: correct LNBits newFunding return format for funding page Updates the newFunding function to return the expected interface: - fundingPendingBalance: BN(0) for Lightning Network - fundingConfirmedBalance: actual wallet balance as BN object - fundingAddress: bolt11 invoice for funding This fixes the TypeError "Cannot read properties of undefined (reading 'minus')" that occurred when accessing the funding page in the admin UI. --- packages/server/lib/plugins/wallet/lnbits/lnbits.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/server/lib/plugins/wallet/lnbits/lnbits.js b/packages/server/lib/plugins/wallet/lnbits/lnbits.js index 93cdb4be..e0fa1b10 100644 --- a/packages/server/lib/plugins/wallet/lnbits/lnbits.js +++ b/packages/server/lib/plugins/wallet/lnbits/lnbits.js @@ -221,10 +221,9 @@ async function newFunding(account, cryptoCode) { const [walletBalance, fundingAddress] = await Promise.all(promises) return { - fundingAddress, - fundingAddressQr: fundingAddress, - confirmed: walletBalance.gte(0), - confirmedBalance: walletBalance.toString() + fundingPendingBalance: new BN(0), + fundingConfirmedBalance: walletBalance, + fundingAddress } }