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 🪙⚡
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
@@ -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
- **Security features** - Domain restrictions, rate limiting, input validation
- **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`
@@ -18,14 +17,6 @@ A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses
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
### 1. `POST /api/decode`
@@ -76,7 +67,6 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
```json
{
"success": true,
"redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762",
"paid": true,
"amount": 21000,
"invoiceAmount": 20580,
@@ -94,7 +84,6 @@ Redeem a Cashu token to a Lightning address. Lightning address is optional - if
```json
{
"success": true,
"redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762",
"paid": true,
"amount": 21000,
"invoiceAmount": 20580,
@@ -178,6 +167,9 @@ Edit `.env` file:
PORT=3000
NODE_ENV=development
# API Domain/IP Configuration (for Swagger docs and CORS)
API_DOMAIN=localhost:3000
# Security Configuration
ALLOW_REDEEM_DOMAINS=ln.tips,getalby.com,wallet.mutinywallet.com
API_SECRET=your-secret-key-here
@@ -201,7 +193,6 @@ npm run dev
npm start
```
The API will be available at `http://localhost:3000`
## 🔧 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.
## 🏗 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
@@ -280,16 +252,6 @@ This allows users to redeem tokens without specifying a Lightning address - the
| `paid` | Successfully paid and completed |
| `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
@@ -301,45 +263,6 @@ The easiest way to test the API is using the interactive Swagger documentation a
- 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"}'
```
**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
@@ -352,4 +275,4 @@ curl -X POST http://localhost:3000/api/redeem \
## 📝 License
MIT License - see LICENSE file for details.
MIT License - see LICENSE file for details.

View File

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

31
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "cashu-redeem-api",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cashu-redeem-api",
"version": "1.0.0",
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"@cashu/cashu-ts": "^1.1.0",
@@ -15,6 +15,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "^8.0.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"uuid": "^10.0.0"
@@ -26,6 +27,10 @@
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/Michilis"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
@@ -1464,6 +1469,23 @@
"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": {
"version": "3.1.3",
"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==",
"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": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

View File

@@ -1,14 +1,15 @@
{
"name": "cashu-redeem-api",
"version": "1.0.0",
"description": "Redeem ecash (Cashu tokens) to Lightning Address using cashu-ts library and LNURLp",
"version": "1.1.0",
"description": "A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"docs": "echo \"API documentation available at http://localhost:3000/docs\""
},
"keywords": [
"cashu",
@@ -19,10 +20,18 @@
"lnurl",
"lnurlp",
"mint",
"satoshi"
"satoshi",
"bolt11",
"lightning-network",
"payment",
"redemption"
],
"author": "",
"author": "Michilis",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/Michilis"
},
"dependencies": {
"@cashu/cashu-ts": "^1.1.0",
"axios": "^1.7.7",
@@ -44,10 +53,10 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/yourusername/cashu-redeem-api.git"
"url": "git+https://github.com/Michilis/cashu-redeem-api.git"
},
"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 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
app.use(express.json({ limit: '10mb' }));
// Enhanced CORS configuration for Swagger UI
app.use(cors({
origin: process.env.ALLOWED_ORIGINS
? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim())
: ['http://localhost:3000'],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
: [`${protocol}://${apiDomain}`],
methods: ['GET', 'POST', 'OPTIONS'],
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
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: 'Cashu Redeem API Documentation',
swaggerOptions: {
filter: true,
showRequestHeaders: true
showRequestHeaders: true,
tryItOutEnabled: true
}
}));
@@ -79,52 +112,6 @@ function asyncHandler(fn) {
// 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) => {
res.json({
name: 'Cashu Redeem API',
@@ -305,9 +292,6 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
* error:
* type: string
* example: "This token has already been spent and cannot be redeemed again"
* redeemId:
* type: string
* format: uuid
* errorType:
* type: string
* example: "token_already_spent"
@@ -324,9 +308,6 @@ app.post('/api/decode', asyncHandler(async (req, res) => {
* error:
* type: string
* example: "Token amount is insufficient to cover the minimum fee"
* redeemId:
* type: string
* format: uuid
* errorType:
* type: string
* example: "insufficient_funds"
@@ -355,7 +336,6 @@ app.post('/api/redeem', asyncHandler(async (req, res) => {
if (result.success) {
const response = {
success: true,
redeemId: result.redeemId,
paid: result.paid,
amount: result.amount,
invoiceAmount: result.invoiceAmount,
@@ -400,7 +380,6 @@ app.post('/api/redeem', asyncHandler(async (req, res) => {
res.status(statusCode).json({
success: false,
error: result.error,
redeemId: result.redeemId,
errorType: statusCode === 409 ? 'token_already_spent' :
statusCode === 422 ? 'insufficient_funds' : 'validation_error'
});

View File

@@ -1,5 +1,16 @@
require('dotenv').config();
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 = {
definition: {
openapi: '3.0.0',
@@ -18,12 +29,8 @@ const options = {
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server'
},
{
url: 'https://api.example.com',
description: 'Production server'
url: serverUrl,
description: isProduction ? 'Production server' : 'Development server'
}
],
components: {
@@ -138,12 +145,6 @@ const options = {
type: 'boolean',
example: true
},
redeemId: {
type: 'string',
format: 'uuid',
description: 'Unique redemption ID for tracking',
example: '8e99101e-d034-4d2e-9ccf-dfda24d26762'
},
paid: {
type: 'boolean',
description: 'Whether the payment was successful',
@@ -329,10 +330,6 @@ const options = {
}
},
tags: [
{
name: 'General',
description: 'General API information and utilities'
},
{
name: 'Token Operations',
description: 'Operations for decoding and redeeming Cashu tokens'