Refactor: move services to components, add route modules

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-02-17 03:46:46 +00:00
parent e2a13d009f
commit 50e5787ec2
12 changed files with 902 additions and 1218 deletions

146
routes/redemption.js Normal file
View File

@@ -0,0 +1,146 @@
const express = require('express');
const router = express.Router();
const redemption = require('../components/redemption');
/**
* @swagger
* /api/redeem:
* post:
* summary: Redeem a Cashu token to Lightning address
* description: |
* Redeem a Cashu token to a Lightning address (optional - uses default if not provided).
*
* The redemption process includes:
* 1. Token validation and parsing
* 2. Getting exact melt quote from mint to determine precise fees
* 3. Invoice creation for net amount (token amount - exact fees)
* 4. Spendability checking at the mint
* 5. Token melting and Lightning payment
*
* **Important**: The system gets the exact fee from the mint before creating the invoice.
* The `invoiceAmount` field shows the actual amount sent to the Lightning address.
* No sats are lost to fee estimation errors.
* tags: [Token Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RedeemRequest'
* responses:
* 200:
* description: Token redeemed successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/RedeemResponse'
* 400:
* $ref: '#/components/responses/BadRequest'
* 409:
* description: Token already spent
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* error:
* type: string
* example: "This token has already been spent and cannot be redeemed again"
* errorType:
* type: string
* example: "token_already_spent"
* 422:
* description: Insufficient funds or unprocessable token
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: false
* error:
* type: string
* example: "Token amount is insufficient to cover the minimum fee"
* errorType:
* type: string
* example: "insufficient_funds"
* 429:
* $ref: '#/components/responses/TooManyRequests'
* 500:
* $ref: '#/components/responses/InternalServerError'
*/
router.post('/redeem', async (req, res) => {
const { token, lightningAddress } = req.body;
const validation = await redemption.validateRedemptionRequest(token, lightningAddress);
if (!validation.valid) {
return res.status(400).json({
success: false,
error: validation.errors.join(', ')
});
}
try {
const result = await redemption.performRedemption(token, lightningAddress);
if (result.success) {
const response = {
success: true,
paid: result.paid,
amount: result.amount,
invoiceAmount: result.invoiceAmount,
to: result.to,
fee: result.fee,
actualFee: result.actualFee,
netAmount: result.netAmount,
mint_url: result.mint,
format: result.format
};
if (result.usingDefaultAddress) {
response.usingDefaultAddress = true;
response.message = `Redeemed to default Lightning address: ${result.to}`;
}
if (result.preimage) {
response.preimage = result.preimage;
}
res.json(response);
} else {
let statusCode = 400;
if (result.error && (
result.error.includes('cannot be redeemed') ||
result.error.includes('already been used') ||
result.error.includes('not spendable') ||
result.error.includes('already spent') ||
result.error.includes('invalid proofs')
)) {
statusCode = 409;
} else if (result.error && result.error.includes('insufficient')) {
statusCode = 422;
}
res.status(statusCode).json({
success: false,
error: result.error,
errorType: statusCode === 409 ? 'token_already_spent' :
statusCode === 422 ? 'insufficient_funds' : 'validation_error'
});
}
} catch (error) {
console.error('Error in redemption:', error);
res.status(500).json({
success: false,
error: 'Internal server error during redemption'
});
}
});
module.exports = router;