- 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.
279 lines
No EOL
7.1 KiB
JavaScript
279 lines
No EOL
7.1 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 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 {
|
|
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
|
|
} |