- 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.
247 lines
No EOL
6.2 KiB
JavaScript
247 lines
No EOL
6.2 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.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
|
|
} |