From fc761844b712999830cfaa9ba2424ab8a1d2a216 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 12 Sep 2025 21:06:00 +0200 Subject: [PATCH] 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