lamassu-server/packages/server/lib/plugins/wallet/lnbits/lnbits.js
padreug 78840f115f 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.
2025-10-12 14:24:29 +02:00

278 lines
No EOL
7 KiB
JavaScript

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.bolt11) {
throw new Error('LNBits did not return a bolt11 invoice')
}
return result.bolt11
}
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 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
}
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 {
fundingPendingBalance: new BN(0),
fundingConfirmedBalance: walletBalance,
fundingAddress
}
}
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
}