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;