Updates fix endpoints.
This commit is contained in:
242
README.md
242
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",
|
"redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762",
|
||||||
"paid": true,
|
"paid": true,
|
||||||
"amount": 21000,
|
"amount": 21000,
|
||||||
|
"invoiceAmount": 20580,
|
||||||
"to": "user@ln.tips",
|
"to": "user@ln.tips",
|
||||||
"fee": 1000,
|
"fee": 1000,
|
||||||
"actualFee": 420,
|
"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",
|
"redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762",
|
||||||
"paid": true,
|
"paid": true,
|
||||||
"amount": 21000,
|
"amount": 21000,
|
||||||
|
"invoiceAmount": 20580,
|
||||||
"to": "admin@your-domain.com",
|
"to": "admin@your-domain.com",
|
||||||
"fee": 1000,
|
"fee": 1000,
|
||||||
"actualFee": 420,
|
"actualFee": 420,
|
||||||
@@ -106,51 +108,24 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. `POST /api/status`
|
**Important Note on Fees**:
|
||||||
Check redemption status using redeemId.
|
- 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:**
|
**Payment Verification**:
|
||||||
```json
|
The API uses multiple indicators to verify payment success:
|
||||||
{
|
- `paid` flag from mint response
|
||||||
"redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762"
|
- Presence of payment preimage
|
||||||
}
|
- Payment state indicators
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
If you receive a "payment failed" error but the Lightning payment was successful, use the debug endpoint to investigate the raw mint 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. `GET /api/status/:redeemId`
|
### 3. `POST /api/validate-address`
|
||||||
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`
|
|
||||||
Validate a Lightning address without redemption.
|
Validate a Lightning address without redemption.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
@@ -172,25 +147,7 @@ Validate a Lightning address without redemption.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 7. `GET /api/stats`
|
### 4. `POST /api/check-spendable`
|
||||||
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`
|
|
||||||
Check if a Cashu token is spendable at its mint before attempting redemption.
|
Check if a Cashu token is spendable at its mint before attempting redemption.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
@@ -200,7 +157,7 @@ Check if a Cashu token is spendable at its mint before attempting redemption.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Success Response:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
@@ -208,10 +165,38 @@ Check if a Cashu token is spendable at its mint before attempting redemption.
|
|||||||
"pending": [],
|
"pending": [],
|
||||||
"mintUrl": "https://mint.azzamo.net",
|
"mintUrl": "https://mint.azzamo.net",
|
||||||
"totalAmount": 21000,
|
"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
|
## 🛠 Setup & Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
@@ -314,7 +299,7 @@ This allows users to redeem tokens without specifying a Lightning address - the
|
|||||||
- Coordinates the complete redemption process
|
- Coordinates the complete redemption process
|
||||||
- Manages redemption status tracking
|
- Manages redemption status tracking
|
||||||
- Handles duplicate token detection
|
- Handles duplicate token detection
|
||||||
- Provides statistics and cleanup
|
- Provides cleanup functionality
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|
||||||
@@ -343,137 +328,8 @@ This allows users to redeem tokens without specifying a Lightning address - the
|
|||||||
| `melting_token` | Performing the melt operation |
|
| `melting_token` | Performing the melt operation |
|
||||||
| `paid` | Successfully paid and completed |
|
| `paid` | Successfully paid and completed |
|
||||||
| `failed` | Redemption failed (see error details) |
|
| `failed` | Redemption failed (see error details) |
|
||||||
| `checking_spendability` | Verifying token is spendable at mint |
|
|
||||||
|
|
||||||
## 📊 Monitoring
|
## 📊 Monitoring
|
||||||
|
|
||||||
### Health Check
|
### 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
|
|
||||||
223
server.js
223
server.js
@@ -157,11 +157,13 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
|
|||||||
*
|
*
|
||||||
* The redemption process includes:
|
* The redemption process includes:
|
||||||
* 1. Token validation and parsing
|
* 1. Token validation and parsing
|
||||||
* 2. Spendability checking at the mint
|
* 2. Fee calculation (NUT-05: 2% of amount, minimum 1 satoshi)
|
||||||
* 3. Lightning address resolution via LNURLp
|
* 3. Invoice creation for net amount (token amount - fees)
|
||||||
* 4. Token melting and Lightning payment
|
* 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]
|
* tags: [Token Operations]
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
@@ -206,6 +208,7 @@ app.post('/api/redeem', asyncHandler(async (req, res) => {
|
|||||||
redeemId: result.redeemId,
|
redeemId: result.redeemId,
|
||||||
paid: result.paid,
|
paid: result.paid,
|
||||||
amount: result.amount,
|
amount: result.amount,
|
||||||
|
invoiceAmount: result.invoiceAmount,
|
||||||
to: result.to,
|
to: result.to,
|
||||||
fee: result.fee,
|
fee: result.fee,
|
||||||
actualFee: result.actualFee,
|
actualFee: result.actualFee,
|
||||||
@@ -225,11 +228,6 @@ app.post('/api/redeem', asyncHandler(async (req, res) => {
|
|||||||
response.preimage = result.preimage;
|
response.preimage = result.preimage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include change if any
|
|
||||||
if (result.change && result.change.length > 0) {
|
|
||||||
response.change = result.change;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({
|
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
|
* @swagger
|
||||||
* /api/health:
|
* /api/health:
|
||||||
@@ -355,46 +261,28 @@ app.get('/api/status/:redeemId', asyncHandler(async (req, res) => {
|
|||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/HealthResponse'
|
* $ref: '#/components/schemas/HealthResponse'
|
||||||
|
* 500:
|
||||||
|
* $ref: '#/components/responses/InternalServerError'
|
||||||
*/
|
*/
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', asyncHandler(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
memory: process.memoryUsage(),
|
memory: process.memoryUsage(),
|
||||||
version: require('./package.json').version
|
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'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
/**
|
|
||||||
* @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
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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
|
// 404 handler
|
||||||
app.use('*', (req, res) => {
|
app.use('*', (req, res) => {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
|
|||||||
@@ -214,21 +214,41 @@ class CashuService {
|
|||||||
// Perform the melt operation using the quote and proofs
|
// Perform the melt operation using the quote and proofs
|
||||||
const meltResponse = await wallet.meltTokens(meltQuote, proofs);
|
const meltResponse = await wallet.meltTokens(meltQuote, proofs);
|
||||||
|
|
||||||
// Verify payment was successful
|
// Debug: Log the melt response structure
|
||||||
if (!meltResponse.paid) {
|
console.log('Melt response:', JSON.stringify(meltResponse, null, 2));
|
||||||
throw new Error('Payment failed - token melted but Lightning payment was not successful');
|
|
||||||
|
// 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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
paid: meltResponse.paid,
|
paid: paymentSuccessful,
|
||||||
preimage: meltResponse.payment_preimage,
|
preimage: meltResponse.payment_preimage || meltResponse.preimage,
|
||||||
change: meltResponse.change || [],
|
change: meltResponse.change || [],
|
||||||
amount: meltQuote.amount,
|
amount: meltQuote.amount,
|
||||||
fee: meltQuote.fee_reserve,
|
fee: actualFeeCharged, // Use actual fee from melt response
|
||||||
actualFee: expectedFee,
|
actualFee: expectedFee, // Keep the calculated expected fee for comparison
|
||||||
netAmount: parsed.totalAmount - meltQuote.fee_reserve,
|
netAmount: actualNetAmount, // Use net amount based on actual fee
|
||||||
quote: meltQuote.quote
|
quote: meltQuote.quote,
|
||||||
|
rawMeltResponse: meltResponse // Include raw response for debugging
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Check if it's a cashu-ts specific error
|
// Check if it's a cashu-ts specific error
|
||||||
@@ -283,17 +303,92 @@ class CashuService {
|
|||||||
const parsed = await this.parseToken(token);
|
const parsed = await this.parseToken(token);
|
||||||
const mint = await this.getMint(parsed.mint);
|
const mint = await this.getMint(parsed.mint);
|
||||||
|
|
||||||
|
// Extract secrets from proofs
|
||||||
const secrets = parsed.proofs.map(proof => proof.secret);
|
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 });
|
const checkResult = await mint.check({ secrets });
|
||||||
|
|
||||||
|
console.log('Spendability check result:', checkResult);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
spendable: checkResult.spendable,
|
spendable: checkResult.spendable || [],
|
||||||
pending: checkResult.pending || [],
|
pending: checkResult.pending || [],
|
||||||
mintUrl: parsed.mint,
|
mintUrl: parsed.mint,
|
||||||
totalAmount: parsed.totalAmount
|
totalAmount: parsed.totalAmount
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} 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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,11 +170,20 @@ class RedemptionService {
|
|||||||
// Calculate expected fee according to NUT-05
|
// Calculate expected fee according to NUT-05
|
||||||
const expectedFee = cashuService.calculateFee(tokenData.totalAmount);
|
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, {
|
this.updateRedemption(redeemId, {
|
||||||
amount: tokenData.totalAmount,
|
amount: tokenData.totalAmount,
|
||||||
mint: tokenData.mint,
|
mint: tokenData.mint,
|
||||||
numProofs: tokenData.numProofs,
|
numProofs: tokenData.numProofs,
|
||||||
expectedFee: expectedFee,
|
expectedFee: expectedFee,
|
||||||
|
netAmountAfterFee: netAmountAfterFee,
|
||||||
format: tokenData.format
|
format: tokenData.format
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -191,39 +200,55 @@ class RedemptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Resolve Lightning address to invoice
|
// Step 2: Resolve Lightning address to invoice
|
||||||
|
// IMPORTANT: Create invoice for net amount (after subtracting expected fees)
|
||||||
this.updateRedemption(redeemId, { status: 'resolving_invoice' });
|
this.updateRedemption(redeemId, { status: 'resolving_invoice' });
|
||||||
const invoiceData = await lightningService.resolveInvoice(
|
const invoiceData = await lightningService.resolveInvoice(
|
||||||
lightningAddressToUse,
|
lightningAddressToUse,
|
||||||
tokenData.totalAmount,
|
netAmountAfterFee, // Use net amount instead of full token amount
|
||||||
`Cashu redemption ${redeemId.substring(0, 8)}`
|
'Cashu redemption'
|
||||||
);
|
);
|
||||||
|
|
||||||
this.updateRedemption(redeemId, {
|
this.updateRedemption(redeemId, {
|
||||||
bolt11: invoiceData.bolt11.substring(0, 50) + '...',
|
bolt11: invoiceData.bolt11.substring(0, 50) + '...',
|
||||||
domain: invoiceData.domain
|
domain: invoiceData.domain,
|
||||||
|
invoiceAmount: netAmountAfterFee
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 3: Melt the token to pay the invoice
|
// Step 3: Melt the token to pay the invoice
|
||||||
this.updateRedemption(redeemId, { status: 'melting_token' });
|
this.updateRedemption(redeemId, { status: 'melting_token' });
|
||||||
const meltResult = await cashuService.meltToken(token, invoiceData.bolt11);
|
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
|
// Step 4: Update final status
|
||||||
this.updateRedemption(redeemId, {
|
this.updateRedemption(redeemId, {
|
||||||
status: meltResult.paid ? 'paid' : 'failed',
|
status: paymentSuccessful ? 'paid' : 'failed',
|
||||||
paid: meltResult.paid,
|
paid: paymentSuccessful,
|
||||||
preimage: meltResult.preimage,
|
preimage: meltResult.preimage,
|
||||||
fee: meltResult.fee,
|
fee: meltResult.fee,
|
||||||
actualFee: meltResult.actualFee,
|
actualFee: meltResult.actualFee,
|
||||||
netAmount: meltResult.netAmount,
|
netAmount: meltResult.netAmount,
|
||||||
change: meltResult.change,
|
change: meltResult.change,
|
||||||
paidAt: meltResult.paid ? new Date().toISOString() : null
|
paidAt: paymentSuccessful ? new Date().toISOString() : null,
|
||||||
|
rawMeltResponse: meltResult.rawMeltResponse // Store for debugging
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
redeemId,
|
redeemId,
|
||||||
paid: meltResult.paid,
|
paid: paymentSuccessful,
|
||||||
amount: tokenData.totalAmount,
|
amount: tokenData.totalAmount,
|
||||||
|
invoiceAmount: netAmountAfterFee, // Amount actually sent in the invoice
|
||||||
to: lightningAddressToUse,
|
to: lightningAddressToUse,
|
||||||
usingDefaultAddress: isUsingDefault,
|
usingDefaultAddress: isUsingDefault,
|
||||||
fee: meltResult.fee,
|
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();
|
module.exports = new RedemptionService();
|
||||||
@@ -52,7 +52,7 @@ const options = {
|
|||||||
token: {
|
token: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Cashu token to decode (supports v1 and v3 formats)',
|
description: 'Cashu token to decode (supports v1 and v3 formats)',
|
||||||
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -115,7 +115,7 @@ const options = {
|
|||||||
token: {
|
token: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Cashu token to redeem',
|
description: 'Cashu token to redeem',
|
||||||
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
||||||
},
|
},
|
||||||
lightningAddress: {
|
lightningAddress: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -149,6 +149,11 @@ const options = {
|
|||||||
description: 'Total amount redeemed in satoshis',
|
description: 'Total amount redeemed in satoshis',
|
||||||
example: 21000
|
example: 21000
|
||||||
},
|
},
|
||||||
|
invoiceAmount: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Actual amount sent in Lightning invoice (after subtracting fees)',
|
||||||
|
example: 20580
|
||||||
|
},
|
||||||
to: {
|
to: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Lightning address that received the payment',
|
description: 'Lightning address that received the payment',
|
||||||
@@ -186,14 +191,6 @@ const options = {
|
|||||||
description: 'Lightning payment preimage (if available)',
|
description: 'Lightning payment preimage (if available)',
|
||||||
example: 'abc123def456...'
|
example: 'abc123def456...'
|
||||||
},
|
},
|
||||||
change: {
|
|
||||||
type: 'array',
|
|
||||||
description: 'Change proofs returned (if any)',
|
|
||||||
items: {
|
|
||||||
type: 'object'
|
|
||||||
},
|
|
||||||
example: []
|
|
||||||
},
|
|
||||||
usingDefaultAddress: {
|
usingDefaultAddress: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Whether default Lightning address was used',
|
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
|
// Validate Address Schemas
|
||||||
ValidateAddressRequest: {
|
ValidateAddressRequest: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@@ -424,57 +256,16 @@ const options = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Check Spendable Schemas
|
HealthResponse: {
|
||||||
CheckSpendableRequest: {
|
|
||||||
type: 'object',
|
|
||||||
required: ['token'],
|
|
||||||
properties: {
|
|
||||||
token: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'Cashu token to check spendability',
|
|
||||||
example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
CheckSpendableResponse: {
|
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
success: {
|
status: {
|
||||||
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: {
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'uri',
|
example: 'OK'
|
||||||
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
|
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Human-readable status message',
|
example: 'API is healthy'
|
||||||
example: 'Token is spendable'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -537,10 +328,6 @@ const options = {
|
|||||||
name: 'Token Operations',
|
name: 'Token Operations',
|
||||||
description: 'Operations for decoding and redeeming Cashu tokens'
|
description: 'Operations for decoding and redeeming Cashu tokens'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'Status & Monitoring',
|
|
||||||
description: 'Endpoints for checking redemption status and API health'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'Validation',
|
name: 'Validation',
|
||||||
description: 'Validation utilities for tokens and Lightning addresses'
|
description: 'Validation utilities for tokens and Lightning addresses'
|
||||||
|
|||||||
Reference in New Issue
Block a user