- 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.
257 lines
No EOL
6.5 KiB
JavaScript
257 lines
No EOL
6.5 KiB
JavaScript
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()
|
|
}
|
|
}) |