From 3c26a15ffecd94d847a33db9849706bbdc5d5b31 Mon Sep 17 00:00:00 2001 From: Rafael Taranto Date: Thu, 31 Jul 2025 09:18:26 +0100 Subject: [PATCH 01/10] chore: version bump --- package.json | 2 +- packages/admin-ui/package.json | 2 +- packages/server/package.json | 2 +- packages/typesafe-db/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9a0a59a1..eb117bdc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lamassu-server", "description": "bitcoin atm client server protocol module", - "version": "11.0.0", + "version": "11.0.1", "license": "./LICENSE", "author": "Lamassu (https://lamassu.is)", "packageManager": "pnpm@10.11.0", diff --git a/packages/admin-ui/package.json b/packages/admin-ui/package.json index cfe54695..6caa63a6 100644 --- a/packages/admin-ui/package.json +++ b/packages/admin-ui/package.json @@ -1,6 +1,6 @@ { "name": "lamassu-admin", - "version": "11.0.0", + "version": "11.0.1", "license": "../LICENSE", "type": "module", "dependencies": { diff --git a/packages/server/package.json b/packages/server/package.json index 9446a16d..3b8658c7 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -2,7 +2,7 @@ "name": "lamassu-server", "description": "bitcoin atm client server protocol module", "keywords": [], - "version": "11.0.0", + "version": "11.0.1", "license": "./LICENSE", "author": "Lamassu (https://lamassu.is)", "dependencies": { diff --git a/packages/typesafe-db/package.json b/packages/typesafe-db/package.json index d8894a3c..2699ebe8 100644 --- a/packages/typesafe-db/package.json +++ b/packages/typesafe-db/package.json @@ -1,6 +1,6 @@ { "name": "typesafe-db", - "version": "11.0.0", + "version": "11.0.1", "license": "../LICENSE", "type": "module", "dependencies": { From 577086b2ec676b3defebfd79d8d3d71aca5aea0c 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 2dca9633e24c791e010e84f80766d720c32b3cf2 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 19549ed3ff500463438a2c23a31890228b6039f8 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 b59b2f02694cfd2b1bc58c1a53d135a10dc81570 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 bf2c37409e2a66c133436a0099146d4d546949eb 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 349c8b3a8e67ad802bc6f7ad8f90c13379ceab99 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 7fe4add2abb3ef3c6458d8b4756573fc701b78f9 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 719f16b743e38570c047ab1864d8c6aeab8c7b6c 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 8cd38f6305b3ef6d514bbc67de28a84a2baf8dca 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 } }