Updates fix endpoints.

This commit is contained in:
michilis
2025-05-31 15:30:07 +02:00
parent fc7927e1c8
commit 877d472e7c
5 changed files with 227 additions and 666 deletions

View File

@@ -214,21 +214,41 @@ class CashuService {
// Perform the melt operation using the quote and proofs
const meltResponse = await wallet.meltTokens(meltQuote, proofs);
// Verify payment was successful
if (!meltResponse.paid) {
throw new Error('Payment failed - token melted but Lightning payment was not successful');
// Debug: Log the melt response structure
console.log('Melt response:', JSON.stringify(meltResponse, null, 2));
// Verify payment was successful - check multiple possible indicators
const paymentSuccessful = meltResponse.paid === true ||
meltResponse.payment_preimage ||
meltResponse.preimage ||
(meltResponse.state && meltResponse.state === 'PAID');
if (!paymentSuccessful) {
console.warn('Payment verification failed. Response structure:', meltResponse);
// Don't throw error immediately - the payment might have succeeded
// but the response structure is different than expected
}
// Get the actual fee charged from the melt response
// The actual fee might be in meltResponse.fee_paid, meltResponse.fee, or calculated from change
const actualFeeCharged = meltResponse.fee_paid ||
meltResponse.fee ||
meltQuote.fee_reserve; // fallback to quote fee
// Calculate net amount based on actual fee charged
const actualNetAmount = parsed.totalAmount - actualFeeCharged;
return {
success: true,
paid: meltResponse.paid,
preimage: meltResponse.payment_preimage,
paid: paymentSuccessful,
preimage: meltResponse.payment_preimage || meltResponse.preimage,
change: meltResponse.change || [],
amount: meltQuote.amount,
fee: meltQuote.fee_reserve,
actualFee: expectedFee,
netAmount: parsed.totalAmount - meltQuote.fee_reserve,
quote: meltQuote.quote
fee: actualFeeCharged, // Use actual fee from melt response
actualFee: expectedFee, // Keep the calculated expected fee for comparison
netAmount: actualNetAmount, // Use net amount based on actual fee
quote: meltQuote.quote,
rawMeltResponse: meltResponse // Include raw response for debugging
};
} catch (error) {
// Check if it's a cashu-ts specific error
@@ -283,17 +303,92 @@ class CashuService {
const parsed = await this.parseToken(token);
const mint = await this.getMint(parsed.mint);
// Extract secrets from proofs
const secrets = parsed.proofs.map(proof => proof.secret);
// Log the attempt for debugging
console.log(`Checking spendability for ${secrets.length} proofs at mint: ${parsed.mint}`);
// Perform the check
const checkResult = await mint.check({ secrets });
console.log('Spendability check result:', checkResult);
return {
spendable: checkResult.spendable,
spendable: checkResult.spendable || [],
pending: checkResult.pending || [],
mintUrl: parsed.mint,
totalAmount: parsed.totalAmount
};
} catch (error) {
throw new Error(`Failed to check token spendability: ${error.message}`);
// Enhanced error logging
console.error('Spendability check error details:', {
errorType: error.constructor.name,
errorMessage: error.message,
errorCode: error.code,
errorStatus: error.status,
errorResponse: error.response,
errorData: error.data,
errorStack: error.stack,
errorString: String(error)
});
// Handle different types of errors
let errorMessage = 'Unknown error occurred';
// Handle cashu-ts HttpResponseError specifically
if (error.constructor.name === 'HttpResponseError') {
console.log('HttpResponseError detected, extracting details...');
// Try to extract useful information from the HTTP response error
if (error.response) {
const status = error.response.status || error.status;
const statusText = error.response.statusText;
if (status === 404) {
errorMessage = 'This mint does not support spendability checking (endpoint not found)';
} else if (status === 405) {
errorMessage = 'This mint does not support spendability checking (method not allowed)';
} else if (status === 501) {
errorMessage = 'This mint does not support spendability checking (not implemented)';
} else {
errorMessage = `Mint returned HTTP ${status}${statusText ? ': ' + statusText : ''}`;
}
} else if (error.message && error.message !== '[object Object]') {
errorMessage = error.message;
} else {
errorMessage = 'This mint does not support spendability checking or returned an invalid response';
}
} else if (error && typeof error === 'object') {
if (error.message && error.message !== '[object Object]') {
errorMessage = error.message;
} else if (error.toString && typeof error.toString === 'function') {
const stringError = error.toString();
if (stringError !== '[object Object]') {
errorMessage = stringError;
} else {
errorMessage = 'Invalid response from mint - spendability checking may not be supported';
}
} else {
errorMessage = 'Invalid response from mint - spendability checking may not be supported';
}
} else if (typeof error === 'string') {
errorMessage = error;
}
// Check if it's a known error pattern indicating unsupported operation
if (errorMessage.includes('not supported') ||
errorMessage.includes('404') ||
errorMessage.includes('405') ||
errorMessage.includes('501') ||
errorMessage.includes('Method not allowed') ||
errorMessage.includes('endpoint not found') ||
errorMessage.includes('not implemented') ||
errorMessage.includes('Invalid response from mint')) {
throw new Error('This mint does not support spendability checking. Token may still be valid.');
}
throw new Error(`Failed to check token spendability: ${errorMessage}`);
}
}
}

View File

@@ -169,12 +169,21 @@ class RedemptionService {
// 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
});
@@ -191,39 +200,55 @@ class RedemptionService {
}
// Step 2: Resolve Lightning address to invoice
// IMPORTANT: Create invoice for net amount (after subtracting expected fees)
this.updateRedemption(redeemId, { status: 'resolving_invoice' });
const invoiceData = await lightningService.resolveInvoice(
lightningAddressToUse,
tokenData.totalAmount,
`Cashu redemption ${redeemId.substring(0, 8)}`
netAmountAfterFee, // Use net amount instead of full token amount
'Cashu redemption'
);
this.updateRedemption(redeemId, {
bolt11: invoiceData.bolt11.substring(0, 50) + '...',
domain: invoiceData.domain
domain: invoiceData.domain,
invoiceAmount: netAmountAfterFee
});
// Step 3: Melt the token to pay the invoice
this.updateRedemption(redeemId, { status: 'melting_token' });
const meltResult = await cashuService.meltToken(token, invoiceData.bolt11);
// Log melt result for debugging
console.log(`Redemption ${redeemId}: Melt result:`, {
paid: meltResult.paid,
hasPreimage: !!meltResult.preimage,
amount: meltResult.amount,
fee: meltResult.fee
});
// Determine if payment was successful
// Consider it successful if we have a preimage, even if 'paid' flag is unclear
const paymentSuccessful = meltResult.paid || !!meltResult.preimage;
// Step 4: Update final status
this.updateRedemption(redeemId, {
status: meltResult.paid ? 'paid' : 'failed',
paid: meltResult.paid,
status: paymentSuccessful ? 'paid' : 'failed',
paid: paymentSuccessful,
preimage: meltResult.preimage,
fee: meltResult.fee,
actualFee: meltResult.actualFee,
netAmount: meltResult.netAmount,
change: meltResult.change,
paidAt: meltResult.paid ? new Date().toISOString() : null
paidAt: paymentSuccessful ? new Date().toISOString() : null,
rawMeltResponse: meltResult.rawMeltResponse // Store for debugging
});
return {
success: true,
redeemId,
paid: meltResult.paid,
paid: paymentSuccessful,
amount: tokenData.totalAmount,
invoiceAmount: netAmountAfterFee, // Amount actually sent in the invoice
to: lightningAddressToUse,
usingDefaultAddress: isUsingDefault,
fee: meltResult.fee,
@@ -327,27 +352,6 @@ class RedemptionService {
}
}
}
/**
* Get redemption statistics
* @returns {Object} Statistics
*/
getStats() {
const redemptions = Array.from(this.redemptions.values());
return {
total: redemptions.length,
paid: redemptions.filter(r => r.paid).length,
failed: redemptions.filter(r => r.status === 'failed').length,
processing: redemptions.filter(r => r.status === 'processing').length,
totalAmount: redemptions
.filter(r => r.paid && r.amount)
.reduce((sum, r) => sum + r.amount, 0),
totalFees: redemptions
.filter(r => r.paid && r.fee)
.reduce((sum, r) => sum + r.fee, 0)
};
}
}
module.exports = new RedemptionService();