Fix fee calculation and improve token spendability detection

- Fix critical fee calculation bug: Now gets exact melt quote before creating invoice
- Improve spent token detection: Only marks as spent with clear indicators
- Add spent field to decode endpoint response (always boolean)
- Add informative root endpoint with API documentation
- Update documentation examples to use cashuB format
- Install bolt11 library for proper Lightning invoice verification
- Enhanced error handling and logging throughout

This fixes the issue where users lost sats due to fee estimation errors
and ensures accurate token spendability detection.
This commit is contained in:
Michilis
2025-07-15 17:41:57 +00:00
parent 4862196281
commit 961380dd88
8 changed files with 823 additions and 75 deletions

View File

@@ -1,4 +1,5 @@
const axios = require('axios');
const bolt11 = require('bolt11');
class LightningService {
constructor() {
@@ -220,11 +221,19 @@ class LightningService {
*/
async resolveInvoice(lightningAddress, amount, comment = 'Cashu token redemption') {
try {
console.log(`Resolving Lightning address: ${lightningAddress} for ${amount} sats`);
// Get LNURLp endpoint
const lnurlpUrl = this.getLNURLpEndpoint(lightningAddress);
console.log(`LNURLp endpoint: ${lnurlpUrl}`);
// Fetch LNURLp response
const lnurlpResponse = await this.fetchLNURLpResponse(lnurlpUrl);
console.log('LNURLp response:', {
callback: lnurlpResponse.callback,
minSendable: lnurlpResponse.minSendable,
maxSendable: lnurlpResponse.maxSendable
});
// Validate amount
if (!this.validateAmount(amount, lnurlpResponse)) {
@@ -235,7 +244,17 @@ class LightningService {
// Get invoice
const amountMsats = this.satsToMillisats(amount);
console.log(`Requesting invoice for ${amountMsats} millisats (${amount} sats)`);
console.log(`Using callback URL: ${lnurlpResponse.callback}`);
const invoiceResponse = await this.getInvoice(lnurlpResponse.callback, amountMsats, comment);
console.log('Invoice created successfully:', {
bolt11: invoiceResponse.bolt11.substring(0, 50) + '...',
lightningAddress,
amount,
amountMsats,
callback: lnurlpResponse.callback
});
return {
bolt11: invoiceResponse.bolt11,
@@ -247,6 +266,7 @@ class LightningService {
lnurlpResponse
};
} catch (error) {
console.error('Lightning address resolution failed:', error.message);
throw new Error(`Lightning address resolution failed: ${error.message}`);
}
}
@@ -271,6 +291,62 @@ class LightningService {
throw new Error(`Invoice parsing failed: ${error.message}`);
}
}
/**
* Verify that a Lightning invoice is valid and for the expected amount
* @param {string} bolt11Invoice - The Lightning invoice to verify
* @param {string} expectedLightningAddress - The expected Lightning address (for logging)
* @param {number} expectedAmount - Expected amount in satoshis (optional)
* @returns {boolean} Whether the invoice is valid
*/
verifyInvoiceDestination(bolt11Invoice, expectedLightningAddress, expectedAmount = null) {
try {
console.log(`Verifying invoice destination for: ${expectedLightningAddress}`);
console.log(`Invoice: ${bolt11Invoice.substring(0, 50)}...`);
// Decode the invoice using the bolt11 library
const decoded = bolt11.decode(bolt11Invoice);
// Basic validation checks
if (!decoded.complete) {
console.error('Invoice verification failed: Invoice is incomplete');
return false;
}
if (!decoded.paymentRequest) {
console.error('Invoice verification failed: No payment request found');
return false;
}
// Check if the invoice has expired
if (decoded.timeExpireDate && decoded.timeExpireDate < Date.now() / 1000) {
console.error('Invoice verification failed: Invoice has expired');
return false;
}
// Verify amount if provided
if (expectedAmount !== null) {
const invoiceAmount = decoded.satoshis || (decoded.millisatoshis ? Math.floor(decoded.millisatoshis / 1000) : 0);
if (invoiceAmount !== expectedAmount) {
console.error(`Invoice verification failed: Amount mismatch. Expected: ${expectedAmount} sats, Got: ${invoiceAmount} sats`);
return false;
}
}
console.log('Invoice verification: All checks passed');
console.log('Invoice details:', {
amount: decoded.satoshis || (decoded.millisatoshis ? Math.floor(decoded.millisatoshis / 1000) : 0),
timestamp: decoded.timestamp,
expiry: decoded.expiry,
description: decoded.tags?.find(tag => tag.tagName === 'description')?.data || 'No description'
});
return true;
} catch (error) {
console.error('Invoice verification failed:', error.message);
return false;
}
}
}
module.exports = new LightningService();