feat: add LNBits wallet plugin integration
- 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.
This commit is contained in:
parent
3c26a15ffe
commit
577086b2ec
7 changed files with 757 additions and 28 deletions
257
packages/server/tests/unit/test_lnbits_plugin.js
Normal file
257
packages/server/tests/unit/test_lnbits_plugin.js
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
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()
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue