Compare commits

..

10 Commits

Author SHA1 Message Date
Michilis
e2a13d009f Update README.md 2025-07-19 09:52:10 +02:00
Michilis
a5c32b574c Update README.md 2025-07-19 09:50:30 +02:00
Michilis
4685a5e42a Merge pull request #2 from Michilis/dev
Updates
2025-07-16 00:08:38 +02:00
Michilis
aa93c50341 docs: remove empty General category from Swagger tags
- Remove the General tag definition from swagger.config.js
- Clean up the tags array to only include active categories
- Eliminates empty General category from Swagger UI
2025-07-15 22:07:29 +00:00
Michilis
184f28fe5d docs: remove GET / endpoint from Swagger documentation
- Remove the General category and GET / endpoint from API docs
- Keep the endpoint functional but hidden from Swagger UI
- Clean up documentation to focus on core API endpoints
2025-07-15 22:05:42 +00:00
Michilis
bf1f368a24 fix: enhance CORS configuration for production with Nginx proxy
- Add enhanced CORS headers for Swagger UI compatibility
- Add OPTIONS method support for preflight requests
- Add additional middleware for CORS preflight handling
- Add debug endpoint /api/cors-test for CORS troubleshooting
- Improve Swagger UI configuration for production deployment
2025-07-15 21:56:55 +00:00
Michilis
91259e98c6 feat: add API_DOMAIN environment variable for production configuration
- Add API_DOMAIN env var to set domain/IP for API in production
- Update swagger.config.js to use dynamic server URLs
- Update server.js CORS configuration to use API_DOMAIN
- Update env.example with new API_DOMAIN field
- Add comprehensive documentation in README.md
- Fixes CORS issues in Swagger docs for production deployments
2025-07-15 21:45:40 +00:00
Michilis
05cdee8236 Merge pull request #1 from Michilis/dev
Fee calculation fix
2025-07-15 20:05:42 +02:00
Michilis
ba36f96f4c Remove redeemId field from redeem response
- Remove redeemId from successful redemption response
- Remove redeemId from error response
- Update swagger documentation to remove redeemId field
- Update README examples to remove redeemId field
- Clean up API response format for better simplicity
2025-07-15 18:03:56 +00:00
Michilis
52d4735712 Update package.json with improved metadata and version bump
- Bump version to 1.1.0
- Update description to be more comprehensive
- Update repository URLs to correct GitHub username
- Add funding information
- Add new keywords (bolt11, lightning-network, payment, redemption)
- Add docs script for API documentation
- Improve overall package metadata
2025-07-15 17:43:32 +00:00
6 changed files with 104 additions and 167 deletions

View File

@@ -1,6 +1,6 @@
# Cashu Redeem API 🪙⚡ # Cashu Redeem API 🪙⚡
A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol. API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol.
## 🚀 Features ## 🚀 Features
@@ -8,7 +8,6 @@ A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses
- **Redeem to Lightning addresses** - Convert ecash to Lightning payments via LNURLp - **Redeem to Lightning addresses** - Convert ecash to Lightning payments via LNURLp
- **Security features** - Domain restrictions, rate limiting, input validation - **Security features** - Domain restrictions, rate limiting, input validation
- **Robust error handling** - Comprehensive error messages - **Robust error handling** - Comprehensive error messages
- **In-memory caching** - Fast mint and wallet instances with connection pooling
- **Interactive API Documentation** - Complete Swagger/OpenAPI documentation at `/docs` - **Interactive API Documentation** - Complete Swagger/OpenAPI documentation at `/docs`
@@ -18,14 +17,6 @@ A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses
Example: `https://cashu-redeem.azzamo.net/docs/` Example: `https://cashu-redeem.azzamo.net/docs/`
The documentation includes:
- Complete endpoint specifications
- Request/response schemas
- Try-it-out functionality
- Example requests and responses
- Authentication requirements
- Error code documentation
## 📡 API Endpoints ## 📡 API Endpoints
### 1. `POST /api/decode` ### 1. `POST /api/decode`
@@ -76,7 +67,6 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
```json ```json
{ {
"success": true, "success": true,
"redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762",
"paid": true, "paid": true,
"amount": 21000, "amount": 21000,
"invoiceAmount": 20580, "invoiceAmount": 20580,
@@ -94,7 +84,6 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
```json ```json
{ {
"success": true, "success": true,
"redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762",
"paid": true, "paid": true,
"amount": 21000, "amount": 21000,
"invoiceAmount": 20580, "invoiceAmount": 20580,
@@ -178,6 +167,9 @@ Edit `.env` file:
PORT=3000 PORT=3000
NODE_ENV=development NODE_ENV=development
# API Domain/IP Configuration (for Swagger docs and CORS)
API_DOMAIN=localhost:3000
# Security Configuration # Security Configuration
ALLOW_REDEEM_DOMAINS=ln.tips,getalby.com,wallet.mutinywallet.com ALLOW_REDEEM_DOMAINS=ln.tips,getalby.com,wallet.mutinywallet.com
API_SECRET=your-secret-key-here API_SECRET=your-secret-key-here
@@ -201,7 +193,6 @@ npm run dev
npm start npm start
``` ```
The API will be available at `http://localhost:3000`
## 🔧 Configuration ## 🔧 Configuration
@@ -234,25 +225,6 @@ DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com
This allows users to redeem tokens without specifying a Lightning address - the tokens will automatically be sent to your configured default address. If no default is set, Lightning address becomes required for all redemption requests. This allows users to redeem tokens without specifying a Lightning address - the tokens will automatically be sent to your configured default address. If no default is set, Lightning address becomes required for all redemption requests.
## 🏗 Architecture
### Services
#### `services/cashu.js`
- Manages Cashu token parsing and validation
- Handles mint connections and wallet instances
- Performs token melting operations
- Caches mint/wallet connections for performance
#### `services/lightning.js`
- Validates Lightning address formats
- Resolves LNURLp endpoints
- Generates Lightning invoices
- Handles domain restrictions
#### `services/redemption.js`
- Manages redemption status tracking
- Handles duplicate token detection
### Data Flow ### Data Flow
@@ -280,16 +252,6 @@ This allows users to redeem tokens without specifying a Lightning address - the
| `paid` | Successfully paid and completed | | `paid` | Successfully paid and completed |
| `failed` | Redemption failed (see error details) | | `failed` | Redemption failed (see error details) |
## 📊 Monitoring
### Health Check
```bash
curl http://localhost:3000/api/health
```
### Logs
The server logs all requests and errors to console. In production, consider using a proper logging solution like Winston.
## 🧪 Testing ## 🧪 Testing
@@ -301,45 +263,6 @@ The easiest way to test the API is using the interactive Swagger documentation a
- Fill in the request parameters - Fill in the request parameters
- Execute the request directly from the browser - 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"}'
```
**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"
}'
```
## 🚀 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
## 🤝 Contributing ## 🤝 Contributing

View File

@@ -2,9 +2,11 @@
PORT=3000 PORT=3000
NODE_ENV=development NODE_ENV=development
# API Domain/IP Configuration (for Swagger docs and CORS)
API_DOMAIN=localhost:3000
# Security Configuration # Security Configuration
ALLOW_REDEEM_DOMAINS=* ALLOW_REDEEM_DOMAINS=*
API_SECRET=your-secret-key-here
# Default Lightning Address (used when no address is provided in redeem requests) # Default Lightning Address (used when no address is provided in redeem requests)
DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com

31
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cashu-redeem-api", "name": "cashu-redeem-api",
"version": "1.0.0", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cashu-redeem-api", "name": "cashu-redeem-api",
"version": "1.0.0", "version": "1.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@cashu/cashu-ts": "^1.1.0", "@cashu/cashu-ts": "^1.1.0",
@@ -15,6 +15,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-rate-limit": "^8.0.0",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"uuid": "^10.0.0" "uuid": "^10.0.0"
@@ -26,6 +27,10 @@
"engines": { "engines": {
"node": ">=18.0.0", "node": ">=18.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/Michilis"
} }
}, },
"node_modules/@apidevtools/json-schema-ref-parser": { "node_modules/@apidevtools/json-schema-ref-parser": {
@@ -1464,6 +1469,23 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.0.tgz",
"integrity": "sha512-FXEAp2ccTeN1ZSO+sPHRHWB0/CrTP5asFBjUaNeD9A0v3iPmgFbLu24vqPjiM9utszI58VGlMokjXQ0W9Dbmjw==",
"dependencies": {
"ip": "2.0.1"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -1961,6 +1983,11 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

View File

@@ -1,14 +1,15 @@
{ {
"name": "cashu-redeem-api", "name": "cashu-redeem-api",
"version": "1.0.0", "version": "1.1.0",
"description": "Redeem ecash (Cashu tokens) to Lightning Address using cashu-ts library and LNURLp", "description": "A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "nodemon server.js", "dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint .", "lint": "eslint .",
"lint:fix": "eslint . --fix" "lint:fix": "eslint . --fix",
"docs": "echo \"API documentation available at http://localhost:3000/docs\""
}, },
"keywords": [ "keywords": [
"cashu", "cashu",
@@ -19,10 +20,18 @@
"lnurl", "lnurl",
"lnurlp", "lnurlp",
"mint", "mint",
"satoshi" "satoshi",
"bolt11",
"lightning-network",
"payment",
"redemption"
], ],
"author": "", "author": "Michilis",
"license": "MIT", "license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/Michilis"
},
"dependencies": { "dependencies": {
"@cashu/cashu-ts": "^1.1.0", "@cashu/cashu-ts": "^1.1.0",
"axios": "^1.7.7", "axios": "^1.7.7",
@@ -44,10 +53,10 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/yourusername/cashu-redeem-api.git" "url": "git+https://github.com/Michilis/cashu-redeem-api.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/yourusername/cashu-redeem-api/issues" "url": "https://github.com/Michilis/cashu-redeem-api/issues"
}, },
"homepage": "https://github.com/yourusername/cashu-redeem-api#readme" "homepage": "https://github.com/Michilis/cashu-redeem-api#readme"
} }

View File

@@ -10,23 +10,56 @@ const redemptionService = require('./services/redemption');
const app = express(); const app = express();
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
// Get API domain for CORS configuration
const apiDomain = process.env.API_DOMAIN || 'localhost:3000';
const isProduction = process.env.NODE_ENV === 'production';
const protocol = isProduction ? 'https' : 'http';
// Middleware // Middleware
app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: '10mb' }));
// Enhanced CORS configuration for Swagger UI
app.use(cors({ app.use(cors({
origin: process.env.ALLOWED_ORIGINS origin: process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
: ['http://localhost:3000'], : [`${protocol}://${apiDomain}`],
methods: ['GET', 'POST'], methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'] allowedHeaders: ['Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With'],
credentials: true,
optionsSuccessStatus: 200
})); }));
// Additional middleware for Swagger UI preflight requests
app.use((req, res, next) => {
if (req.method === 'OPTIONS') {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Accept, Origin, X-Requested-With');
res.status(200).end();
return;
}
next();
});
// Debug endpoint to test CORS
app.get('/api/cors-test', (req, res) => {
res.json({
success: true,
message: 'CORS test successful',
timestamp: new Date().toISOString(),
origin: req.headers.origin,
host: req.headers.host
});
});
// Swagger Documentation // Swagger Documentation
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, {
customCss: '.swagger-ui .topbar { display: none }', customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'Cashu Redeem API Documentation', customSiteTitle: 'Cashu Redeem API Documentation',
swaggerOptions: { swaggerOptions: {
filter: true, filter: true,
showRequestHeaders: true showRequestHeaders: true,
tryItOutEnabled: true
} }
})); }));
@@ -79,52 +112,6 @@ function asyncHandler(fn) {
// API Routes // API Routes
/**
* @swagger
* /:
* get:
* summary: API Information
* description: Get basic information about the Cashu Redeem API
* tags: [General]
* responses:
* 200:
* description: API information
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* example: "Cashu Redeem API"
* version:
* type: string
* example: "1.0.0"
* description:
* type: string
* example: "A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses"
* documentation:
* type: string
* example: "/docs"
* endpoints:
* type: object
* properties:
* decode:
* type: string
* example: "POST /api/decode"
* redeem:
* type: string
* example: "POST /api/redeem"
* validate:
* type: string
* example: "POST /api/validate-address"
* health:
* type: string
* example: "GET /api/health"
* github:
* type: string
* example: "https://github.com/yourusername/cashu-redeem-api"
*/
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.json({ res.json({
name: 'Cashu Redeem API', name: 'Cashu Redeem API',
@@ -305,9 +292,6 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
* error: * error:
* type: string * type: string
* example: "This token has already been spent and cannot be redeemed again" * example: "This token has already been spent and cannot be redeemed again"
* redeemId:
* type: string
* format: uuid
* errorType: * errorType:
* type: string * type: string
* example: "token_already_spent" * example: "token_already_spent"
@@ -324,9 +308,6 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
* error: * error:
* type: string * type: string
* example: "Token amount is insufficient to cover the minimum fee" * example: "Token amount is insufficient to cover the minimum fee"
* redeemId:
* type: string
* format: uuid
* errorType: * errorType:
* type: string * type: string
* example: "insufficient_funds" * example: "insufficient_funds"
@@ -355,7 +336,6 @@ app.post('/api/redeem', asyncHandler(async (req, res) => {
if (result.success) { if (result.success) {
const response = { const response = {
success: true, success: true,
redeemId: result.redeemId,
paid: result.paid, paid: result.paid,
amount: result.amount, amount: result.amount,
invoiceAmount: result.invoiceAmount, invoiceAmount: result.invoiceAmount,
@@ -400,7 +380,6 @@ app.post('/api/redeem', asyncHandler(async (req, res) => {
res.status(statusCode).json({ res.status(statusCode).json({
success: false, success: false,
error: result.error, error: result.error,
redeemId: result.redeemId,
errorType: statusCode === 409 ? 'token_already_spent' : errorType: statusCode === 409 ? 'token_already_spent' :
statusCode === 422 ? 'insufficient_funds' : 'validation_error' statusCode === 422 ? 'insufficient_funds' : 'validation_error'
}); });

View File

@@ -1,5 +1,16 @@
require('dotenv').config();
const swaggerJsdoc = require('swagger-jsdoc'); const swaggerJsdoc = require('swagger-jsdoc');
// Get the API domain from environment variable, default to localhost:3000
const apiDomain = process.env.API_DOMAIN || 'localhost:3000';
const isProduction = process.env.NODE_ENV === 'production';
const protocol = isProduction ? 'https' : 'http';
// For production behind Nginx, we need to ensure the URL doesn't include the internal port
const serverUrl = isProduction
? `${protocol}://${apiDomain}`
: `${protocol}://${apiDomain}`;
const options = { const options = {
definition: { definition: {
openapi: '3.0.0', openapi: '3.0.0',
@@ -18,12 +29,8 @@ const options = {
}, },
servers: [ servers: [
{ {
url: 'http://localhost:3000', url: serverUrl,
description: 'Development server' description: isProduction ? 'Production server' : 'Development server'
},
{
url: 'https://api.example.com',
description: 'Production server'
} }
], ],
components: { components: {
@@ -138,12 +145,6 @@ const options = {
type: 'boolean', type: 'boolean',
example: true example: true
}, },
redeemId: {
type: 'string',
format: 'uuid',
description: 'Unique redemption ID for tracking',
example: '8e99101e-d034-4d2e-9ccf-dfda24d26762'
},
paid: { paid: {
type: 'boolean', type: 'boolean',
description: 'Whether the payment was successful', description: 'Whether the payment was successful',
@@ -329,10 +330,6 @@ const options = {
} }
}, },
tags: [ tags: [
{
name: 'General',
description: 'General API information and utilities'
},
{ {
name: 'Token Operations', name: 'Token Operations',
description: 'Operations for decoding and redeeming Cashu tokens' description: 'Operations for decoding and redeeming Cashu tokens'