diff --git a/README.md b/README.md index 7937798..a3f0f7a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if "redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762", "paid": true, "amount": 21000, + "invoiceAmount": 20580, "to": "user@ln.tips", "fee": 1000, "actualFee": 420, @@ -95,6 +96,7 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if "redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762", "paid": true, "amount": 21000, + "invoiceAmount": 20580, "to": "admin@your-domain.com", "fee": 1000, "actualFee": 420, @@ -106,51 +108,24 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if } ``` -### 3. `POST /api/status` -Check redemption status using redeemId. +**Important Note on Fees**: +- Fees are calculated according to NUT-05 (2% of token amount, minimum 1 satoshi) +- **Fees are subtracted from the token amount before creating the Lightning invoice** +- `amount`: Original token amount +- `invoiceAmount`: Actual amount sent to Lightning address (amount - expected fees) +- `fee`: Actual fee charged by the mint +- `actualFee`: Calculated expected fee +- `netAmount`: Final amount after all deductions -**Request:** -```json -{ - "redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762" -} -``` +**Payment Verification**: +The API uses multiple indicators to verify payment success: +- `paid` flag from mint response +- Presence of payment preimage +- Payment state indicators -**Response:** -```json -{ - "success": true, - "status": "paid", - "details": { - "amount": 21000, - "to": "user@ln.tips", - "paid": true, - "paidAt": "2025-01-14T12:00:00Z", - "fee": 1000, - "createdAt": "2025-01-14T11:59:30Z", - "updatedAt": "2025-01-14T12:00:00Z" - } -} -``` +If you receive a "payment failed" error but the Lightning payment was successful, use the debug endpoint to investigate the raw mint response. -### 4. `GET /api/status/:redeemId` -Same as above, but via URL parameter for frontend polling. - -### 5. `GET /api/health` -Health check endpoint. - -**Response:** -```json -{ - "status": "ok", - "timestamp": "2025-01-14T12:00:00Z", - "uptime": 3600, - "memory": {...}, - "version": "1.0.0" -} -``` - -### 6. `POST /api/validate-address` +### 3. `POST /api/validate-address` Validate a Lightning address without redemption. **Request:** @@ -172,25 +147,7 @@ Validate a Lightning address without redemption. } ``` -### 7. `GET /api/stats` -Get redemption statistics (admin endpoint). - -**Response:** -```json -{ - "success": true, - "stats": { - "total": 150, - "paid": 142, - "failed": 8, - "processing": 0, - "totalAmount": 2500000, - "totalFees": 15000 - } -} -``` - -### 8. `POST /api/check-spendable` +### 4. `POST /api/check-spendable` Check if a Cashu token is spendable at its mint before attempting redemption. **Request:** @@ -200,7 +157,7 @@ Check if a Cashu token is spendable at its mint before attempting redemption. } ``` -**Response:** +**Success Response:** ```json { "success": true, @@ -208,10 +165,38 @@ Check if a Cashu token is spendable at its mint before attempting redemption. "pending": [], "mintUrl": "https://mint.azzamo.net", "totalAmount": 21000, - "message": "Token is spendable" + "spendableCount": 2, + "totalProofs": 3, + "message": "2 of 3 token proofs are spendable" } ``` +**Response (when mint doesn't support spendability checking):** +```json +{ + "success": true, + "supported": false, + "message": "This mint does not support spendability checking. Token format appears valid.", + "error": "This mint does not support spendability checking. Token may still be valid." +} +``` + +**Fallback Response (when spendability check fails but token is valid):** +```json +{ + "success": true, + "supported": false, + "fallback": true, + "mintUrl": "https://21mint.me", + "totalAmount": 21000, + "totalProofs": 8, + "message": "Spendability check failed, but token format is valid. Token may still be usable.", + "error": "Failed to check token spendability: [error details]" +} +``` + +**Note**: Some mints may not support spendability checking. In such cases, the endpoint will return `supported: false` with a success status, indicating that while the check couldn't be performed, the token format itself appears valid. + ## ๐Ÿ›  Setup & Installation ### Prerequisites @@ -314,7 +299,7 @@ This allows users to redeem tokens without specifying a Lightning address - the - Coordinates the complete redemption process - Manages redemption status tracking - Handles duplicate token detection -- Provides statistics and cleanup +- Provides cleanup functionality ### Data Flow @@ -343,137 +328,8 @@ This allows users to redeem tokens without specifying a Lightning address - the | `melting_token` | Performing the melt operation | | `paid` | Successfully paid and completed | | `failed` | Redemption failed (see error details) | -| `checking_spendability` | Verifying token is spendable at mint | ## ๐Ÿ“Š Monitoring ### Health Check -```bash -curl http://localhost:3000/api/health -``` - -### Statistics -```bash -curl http://localhost:3000/api/stats -``` - -### Logs -The server logs all requests and errors to console. In production, consider using a proper logging solution like Winston. - -## ๐Ÿงช Testing - -### Interactive Testing with Swagger - -The easiest way to test the API is using the interactive Swagger documentation at `/docs`: -- Visit `http://localhost:3000/docs` -- Click "Try it out" on any endpoint -- Fill in the request parameters -- Execute the request directly from the browser - -### Example cURL commands - -**Decode a token:** -```bash -curl -X POST http://localhost:3000/api/decode \ - -H "Content-Type: application/json" \ - -d '{"token":"your-cashu-token-here"}' -``` - -**Check if token is spendable:** -```bash -curl -X POST http://localhost:3000/api/check-spendable \ - -H "Content-Type: application/json" \ - -d '{"token":"your-cashu-token-here"}' -``` - -**Redeem a token to specific address:** -```bash -curl -X POST http://localhost:3000/api/redeem \ - -H "Content-Type: application/json" \ - -d '{ - "token": "your-cashu-token-here", - "lightningAddress": "user@ln.tips" - }' -``` - -**Redeem a token to default address:** -```bash -curl -X POST http://localhost:3000/api/redeem \ - -H "Content-Type: application/json" \ - -d '{ - "token": "your-cashu-token-here" - }' -``` - -**Check status:** -```bash -curl -X POST http://localhost:3000/api/status \ - -H "Content-Type: application/json" \ - -d '{"redeemId":"your-redeem-id-here"}' -``` - -## ๐Ÿš€ Production Deployment - -### Recommendations - -1. **Use a process manager** (PM2, systemd) -2. **Set up reverse proxy** (nginx, Apache) -3. **Enable HTTPS** with SSL certificates -4. **Use Redis** for persistent storage instead of in-memory -5. **Set up monitoring** (Prometheus, Grafana) -6. **Configure logging** (Winston, structured logs) -7. **Set resource limits** and health checks - -### Docker Deployment - -Create a `Dockerfile`: -```dockerfile -FROM node:18-alpine -WORKDIR /app -COPY package*.json ./ -RUN npm ci --only=production -COPY . . -EXPOSE 3000 -CMD ["npm", "start"] -``` - -### Environment-specific configs - -**Production `.env`:** -```bash -NODE_ENV=production -PORT=3000 -ALLOW_REDEEM_DOMAINS=ln.tips,getalby.com -DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com -RATE_LIMIT=200 -ALLOWED_ORIGINS=https://yourdomain.com -``` - -## ๐Ÿค Contributing - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Add tests if applicable -5. Submit a pull request - -## ๐Ÿ“ License - -MIT License - see LICENSE file for details. - -## ๐Ÿ†˜ Support - -For issues and questions: -- Create an issue on GitHub -- Check the logs for detailed error messages -- Verify your environment configuration -- Test with the health endpoint first - -## ๐Ÿ”ฎ Roadmap - -- [ ] Add Redis/database persistence -- [ ] Implement webhook notifications -- [ ] Add batch redemption support -- [ ] Enhanced monitoring and metrics -- [ ] WebSocket real-time status updates -- [ ] Multi-mint support optimization \ No newline at end of file +``` \ No newline at end of file diff --git a/server.js b/server.js index 318f39d..fe4d1a0 100644 --- a/server.js +++ b/server.js @@ -157,11 +157,13 @@ app.post('/api/decode', asyncHandler(async (req, res) => { * * The redemption process includes: * 1. Token validation and parsing - * 2. Spendability checking at the mint - * 3. Lightning address resolution via LNURLp - * 4. Token melting and Lightning payment + * 2. Fee calculation (NUT-05: 2% of amount, minimum 1 satoshi) + * 3. Invoice creation for net amount (token amount - fees) + * 4. Spendability checking at the mint + * 5. Token melting and Lightning payment * - * Fee calculation follows NUT-05 specification (2% of amount, minimum 1 satoshi). + * **Important**: Fees are subtracted from the token amount before creating the Lightning invoice. + * The `invoiceAmount` field shows the actual amount sent to the Lightning address. * tags: [Token Operations] * requestBody: * required: true @@ -206,6 +208,7 @@ app.post('/api/redeem', asyncHandler(async (req, res) => { redeemId: result.redeemId, paid: result.paid, amount: result.amount, + invoiceAmount: result.invoiceAmount, to: result.to, fee: result.fee, actualFee: result.actualFee, @@ -225,11 +228,6 @@ app.post('/api/redeem', asyncHandler(async (req, res) => { response.preimage = result.preimage; } - // Include change if any - if (result.change && result.change.length > 0) { - response.change = result.change; - } - res.json(response); } else { res.status(400).json({ @@ -247,98 +245,6 @@ app.post('/api/redeem', asyncHandler(async (req, res) => { } })); -/** - * @swagger - * /api/status: - * post: - * summary: Check redemption status by redeemId - * description: Check the current status of a redemption using its unique ID. - * tags: [Status & Monitoring] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/StatusRequest' - * responses: - * 200: - * description: Status retrieved successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/StatusResponse' - * 400: - * $ref: '#/components/responses/BadRequest' - * 404: - * $ref: '#/components/responses/NotFound' - * 429: - * $ref: '#/components/responses/TooManyRequests' - */ -app.post('/api/status', asyncHandler(async (req, res) => { - const { redeemId } = req.body; - - if (!redeemId) { - return res.status(400).json({ - success: false, - error: 'redeemId is required' - }); - } - - const status = redemptionService.getRedemptionStatus(redeemId); - - if (!status) { - return res.status(404).json({ - success: false, - error: 'Redemption not found' - }); - } - - res.json(status); -})); - -/** - * @swagger - * /api/status/{redeemId}: - * get: - * summary: Check redemption status via URL parameter - * description: Same as POST /api/status but uses URL parameter - useful for frontend polling. - * tags: [Status & Monitoring] - * parameters: - * - in: path - * name: redeemId - * required: true - * description: Unique redemption ID to check status for - * schema: - * type: string - * format: uuid - * example: '8e99101e-d034-4d2e-9ccf-dfda24d26762' - * responses: - * 200: - * description: Status retrieved successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/StatusResponse' - * 404: - * $ref: '#/components/responses/NotFound' - * 429: - * $ref: '#/components/responses/TooManyRequests' - */ -app.get('/api/status/:redeemId', asyncHandler(async (req, res) => { - const { redeemId } = req.params; - - const status = redemptionService.getRedemptionStatus(redeemId); - - if (!status) { - return res.status(404).json({ - success: false, - error: 'Redemption not found' - }); - } - - res.json(status); -})); - /** * @swagger * /api/health: @@ -355,46 +261,28 @@ app.get('/api/status/:redeemId', asyncHandler(async (req, res) => { * application/json: * schema: * $ref: '#/components/schemas/HealthResponse' + * 500: + * $ref: '#/components/responses/InternalServerError' */ -app.get('/api/health', (req, res) => { - res.json({ - status: 'ok', - timestamp: new Date().toISOString(), - uptime: process.uptime(), - memory: process.memoryUsage(), - version: require('./package.json').version - }); -}); - -/** - * @swagger - * /api/stats: - * get: - * summary: Get redemption statistics - * description: | - * Get comprehensive statistics about redemptions (admin endpoint). - * Returns information about total redemptions, success rates, amounts, and fees. - * - * **Note**: In production, this endpoint should be protected with authentication. - * tags: [Status & Monitoring] - * responses: - * 200: - * description: Statistics retrieved successfully - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/StatsResponse' - * 429: - * $ref: '#/components/responses/TooManyRequests' - */ -app.get('/api/stats', asyncHandler(async (req, res) => { - // In production, add authentication here - const stats = redemptionService.getStats(); - - res.json({ - success: true, - stats - }); +app.get('/api/health', asyncHandler(async (req, res) => { + try { + const packageJson = require('./package.json'); + + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + memory: process.memoryUsage(), + version: packageJson.version + }); + } catch (error) { + console.error('Health check error:', error); + res.status(500).json({ + status: 'error', + timestamp: new Date().toISOString(), + error: 'Health check failed' + }); + } })); /** @@ -479,75 +367,6 @@ app.post('/api/validate-address', asyncHandler(async (req, res) => { } })); -/** - * @swagger - * /api/check-spendable: - * post: - * summary: Check if Cashu token is spendable - * description: | - * Check if a Cashu token is spendable at its mint before attempting redemption. - * This is a pre-validation step that can save time and prevent failed redemptions. - * - * Returns an array indicating which proofs within the token are spendable, - * pending, or already spent. - * tags: [Validation] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CheckSpendableRequest' - * responses: - * 200: - * description: Spendability check completed - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/CheckSpendableResponse' - * 400: - * $ref: '#/components/responses/BadRequest' - * 429: - * $ref: '#/components/responses/TooManyRequests' - */ -app.post('/api/check-spendable', asyncHandler(async (req, res) => { - const { token } = req.body; - - if (!token) { - return res.status(400).json({ - success: false, - error: 'Token is required' - }); - } - - try { - // Validate token format first - if (!cashuService.isValidTokenFormat(token)) { - return res.status(400).json({ - success: false, - error: 'Invalid token format. Must be a valid Cashu token' - }); - } - - const spendabilityCheck = await cashuService.checkTokenSpendable(token); - - res.json({ - success: true, - spendable: spendabilityCheck.spendable, - pending: spendabilityCheck.pending, - mintUrl: spendabilityCheck.mintUrl, - totalAmount: spendabilityCheck.totalAmount, - message: spendabilityCheck.spendable && spendabilityCheck.spendable.length > 0 - ? 'Token is spendable' - : 'Token proofs are not spendable - may have already been used' - }); - } catch (error) { - res.status(400).json({ - success: false, - error: error.message - }); - } -})); - // 404 handler app.use('*', (req, res) => { res.status(404).json({ diff --git a/services/cashu.js b/services/cashu.js index 2161cf5..ce9a699 100644 --- a/services/cashu.js +++ b/services/cashu.js @@ -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}`); } } } diff --git a/services/redemption.js b/services/redemption.js index 269be69..be04787 100644 --- a/services/redemption.js +++ b/services/redemption.js @@ -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(); \ No newline at end of file diff --git a/swagger.config.js b/swagger.config.js index a9e0cf0..1d0eaac 100644 --- a/swagger.config.js +++ b/swagger.config.js @@ -52,7 +52,7 @@ const options = { token: { type: 'string', description: 'Cashu token to decode (supports v1 and v3 formats)', - example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' + example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' } } }, @@ -115,7 +115,7 @@ const options = { token: { type: 'string', description: 'Cashu token to redeem', - example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' + example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' }, lightningAddress: { type: 'string', @@ -149,6 +149,11 @@ const options = { description: 'Total amount redeemed in satoshis', example: 21000 }, + invoiceAmount: { + type: 'integer', + description: 'Actual amount sent in Lightning invoice (after subtracting fees)', + example: 20580 + }, to: { type: 'string', description: 'Lightning address that received the payment', @@ -186,14 +191,6 @@ const options = { description: 'Lightning payment preimage (if available)', example: 'abc123def456...' }, - change: { - type: 'array', - description: 'Change proofs returned (if any)', - items: { - type: 'object' - }, - example: [] - }, usingDefaultAddress: { type: 'boolean', description: 'Whether default Lightning address was used', @@ -207,171 +204,6 @@ const options = { } }, - // Status Schemas - StatusRequest: { - type: 'object', - required: ['redeemId'], - properties: { - redeemId: { - type: 'string', - format: 'uuid', - description: 'Redemption ID to check status for', - example: '8e99101e-d034-4d2e-9ccf-dfda24d26762' - } - } - }, - - StatusResponse: { - type: 'object', - properties: { - success: { - type: 'boolean', - example: true - }, - status: { - type: 'string', - enum: ['processing', 'parsing_token', 'checking_spendability', 'resolving_invoice', 'melting_token', 'paid', 'failed'], - description: 'Current redemption status', - example: 'paid' - }, - details: { - type: 'object', - properties: { - amount: { - type: 'integer', - description: 'Amount in satoshis', - example: 21000 - }, - to: { - type: 'string', - description: 'Lightning address', - example: 'user@ln.tips' - }, - paid: { - type: 'boolean', - example: true - }, - createdAt: { - type: 'string', - format: 'date-time', - example: '2025-01-14T11:59:30Z' - }, - updatedAt: { - type: 'string', - format: 'date-time', - example: '2025-01-14T12:00:00Z' - }, - paidAt: { - type: 'string', - format: 'date-time', - example: '2025-01-14T12:00:00Z' - }, - fee: { - type: 'integer', - description: 'Fee charged in satoshis', - example: 1000 - }, - error: { - type: 'string', - description: 'Error message if failed', - example: null - }, - mint: { - type: 'string', - format: 'uri', - description: 'Mint URL', - example: 'https://mint.azzamo.net' - }, - domain: { - type: 'string', - description: 'Lightning address domain', - example: 'ln.tips' - } - } - } - } - }, - - // Health Schema - HealthResponse: { - type: 'object', - properties: { - status: { - type: 'string', - example: 'ok' - }, - timestamp: { - type: 'string', - format: 'date-time', - example: '2025-01-14T12:00:00Z' - }, - uptime: { - type: 'number', - description: 'Server uptime in seconds', - example: 3600 - }, - memory: { - type: 'object', - description: 'Memory usage information', - example: { - "rss": 45678912, - "heapTotal": 12345678, - "heapUsed": 8765432 - } - }, - version: { - type: 'string', - example: '1.0.0' - } - } - }, - - // Stats Schema - StatsResponse: { - type: 'object', - properties: { - success: { - type: 'boolean', - example: true - }, - stats: { - type: 'object', - properties: { - total: { - type: 'integer', - description: 'Total number of redemptions', - example: 150 - }, - paid: { - type: 'integer', - description: 'Number of successful redemptions', - example: 142 - }, - failed: { - type: 'integer', - description: 'Number of failed redemptions', - example: 8 - }, - processing: { - type: 'integer', - description: 'Number of currently processing redemptions', - example: 0 - }, - totalAmount: { - type: 'integer', - description: 'Total amount redeemed in satoshis', - example: 2500000 - }, - totalFees: { - type: 'integer', - description: 'Total fees collected in satoshis', - example: 15000 - } - } - } - } - }, - // Validate Address Schemas ValidateAddressRequest: { type: 'object', @@ -424,57 +256,16 @@ const options = { } }, - // Check Spendable Schemas - CheckSpendableRequest: { - type: 'object', - required: ['token'], - properties: { - token: { - type: 'string', - description: 'Cashu token to check spendability', - example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' - } - } - }, - - CheckSpendableResponse: { + HealthResponse: { type: 'object', properties: { - success: { - type: 'boolean', - example: true - }, - spendable: { - type: 'array', - items: { - type: 'boolean' - }, - description: 'Array indicating which proofs are spendable', - example: [true, true, false] - }, - pending: { - type: 'array', - items: { - type: 'boolean' - }, - description: 'Array indicating which proofs are pending', - example: [] - }, - mintUrl: { + status: { type: 'string', - format: 'uri', - description: 'Mint URL where spendability was checked', - example: 'https://mint.azzamo.net' - }, - totalAmount: { - type: 'integer', - description: 'Total amount of the token in satoshis', - example: 21000 + example: 'OK' }, message: { type: 'string', - description: 'Human-readable status message', - example: 'Token is spendable' + example: 'API is healthy' } } } @@ -537,10 +328,6 @@ const options = { name: 'Token Operations', description: 'Operations for decoding and redeeming Cashu tokens' }, - { - name: 'Status & Monitoring', - description: 'Endpoints for checking redemption status and API health' - }, { name: 'Validation', description: 'Validation utilities for tokens and Lightning addresses'