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 }