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

@@ -167,23 +167,10 @@ class RedemptionService {
this.updateRedemption(redeemId, { status: 'parsing_token' });
const tokenData = await cashuService.parseToken(token);
// Calculate expected fee according to NUT-05
const expectedFee = cashuService.calculateFee(tokenData.totalAmount);
// Calculate net amount after subtracting fees
const netAmountAfterFee = tokenData.totalAmount - expectedFee;
// Ensure we have enough for the minimum payment after fees
if (netAmountAfterFee <= 0) {
throw new Error(`Token amount (${tokenData.totalAmount} sats) is insufficient to cover the minimum fee (${expectedFee} sats)`);
}
this.updateRedemption(redeemId, {
amount: tokenData.totalAmount,
mint: tokenData.mint,
numProofs: tokenData.numProofs,
expectedFee: expectedFee,
netAmountAfterFee: netAmountAfterFee,
format: tokenData.format
});
@@ -203,26 +190,58 @@ class RedemptionService {
// This is likely an already-spent token - fail the redemption with clear message
throw new Error('This token has already been spent and cannot be redeemed again');
}
// Log but don't fail for other errors - some mints might not support this check
// For other errors, log but continue - some mints might not support this check
console.warn('Spendability check failed:', spendError.message);
console.log('Continuing with redemption despite spendability check failure...');
}
// Step 2: Resolve Lightning address to invoice
// IMPORTANT: Create invoice for net amount (after subtracting expected fees)
// Step 2: Get melt quote first to determine exact fees
this.updateRedemption(redeemId, { status: 'getting_melt_quote' });
// Create a temporary invoice to get the melt quote (we'll create the real one after)
console.log(`Getting melt quote for ${tokenData.totalAmount} sats to determine exact fees`);
const tempInvoiceData = await lightningService.resolveInvoice(
lightningAddressToUse,
tokenData.totalAmount, // Use full amount initially
'Cashu redemption'
);
// Get melt quote to determine exact fee
const meltQuote = await cashuService.getMeltQuote(token, tempInvoiceData.bolt11);
const exactFee = meltQuote.fee_reserve;
const finalInvoiceAmount = tokenData.totalAmount - exactFee;
console.log(`Melt quote: amount=${meltQuote.amount}, fee=${exactFee}, net to user=${finalInvoiceAmount}`);
// Step 3: Create final invoice for the correct amount (total - exact fee)
this.updateRedemption(redeemId, { status: 'resolving_invoice' });
if (finalInvoiceAmount <= 0) {
throw new Error(`Token amount (${tokenData.totalAmount} sats) is insufficient to cover the fee (${exactFee} sats)`);
}
console.log(`Creating final invoice for ${finalInvoiceAmount} sats (${tokenData.totalAmount} - ${exactFee} fee)`);
const invoiceData = await lightningService.resolveInvoice(
lightningAddressToUse,
netAmountAfterFee, // Use net amount instead of full token amount
finalInvoiceAmount, // Use amount minus exact fee
'Cashu redemption'
);
this.updateRedemption(redeemId, {
bolt11: invoiceData.bolt11.substring(0, 50) + '...',
domain: invoiceData.domain,
invoiceAmount: netAmountAfterFee
invoiceAmount: finalInvoiceAmount,
exactFee: exactFee
});
// Step 3: Melt the token to pay the invoice
// Verify the invoice is valid and for the correct amount
const invoiceVerified = lightningService.verifyInvoiceDestination(invoiceData.bolt11, lightningAddressToUse, finalInvoiceAmount);
if (!invoiceVerified) {
throw new Error('Invoice verification failed - invalid invoice or amount mismatch');
}
// Step 4: Melt the token to pay the invoice
this.updateRedemption(redeemId, { status: 'melting_token' });
const meltResult = await cashuService.meltToken(token, invoiceData.bolt11);
@@ -256,12 +275,12 @@ class RedemptionService {
redeemId,
paid: paymentSuccessful,
amount: tokenData.totalAmount,
invoiceAmount: netAmountAfterFee, // Amount actually sent in the invoice
invoiceAmount: finalInvoiceAmount, // Amount actually sent in the invoice
to: lightningAddressToUse,
usingDefaultAddress: isUsingDefault,
fee: meltResult.fee,
fee: exactFee, // Use the exact fee from the melt quote
actualFee: meltResult.actualFee,
netAmount: meltResult.netAmount,
netAmount: finalInvoiceAmount, // This is the net amount the user receives
preimage: meltResult.preimage,
change: meltResult.change,
mint: tokenData.mint,