240 lines
7.7 KiB
Markdown
240 lines
7.7 KiB
Markdown
# NOWPayments IPN Listener
|
|
|
|
A TypeScript Express application for receiving and processing Instant Payment Notifications (IPN) from NOWPayments.
|
|
|
|
## Features
|
|
|
|
- ✅ Secure IPN signature validation using HMAC SHA512
|
|
- ✅ TypeScript for type safety
|
|
- ✅ Handles all NOWPayments payment statuses
|
|
- ✅ MySQL/MariaDB database integration
|
|
- ✅ Automatic payment processing: moves finished/confirmed payments from `pending_orders` to `sales`
|
|
- ✅ 10-minute reservation mechanism to prevent overselling
|
|
- ✅ Expiration checking and automatic cleanup of expired orders
|
|
- ✅ Final inventory validation before creating sales
|
|
- ✅ Transaction-safe database operations
|
|
- ✅ Idempotent IPN processing (handles duplicate callbacks)
|
|
- ✅ Error handling and logging
|
|
|
|
## Setup
|
|
|
|
### 1. Install Dependencies
|
|
|
|
```bash
|
|
npm install
|
|
```
|
|
|
|
### 2. Configure Environment Variables
|
|
|
|
Copy `.env.example` to `.env` and fill in your NOWPayments IPN Secret Key:
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
Edit `.env` and configure:
|
|
|
|
**NOWPayments IPN Secret Key:**
|
|
- Go to NOWPayments Dashboard → Store Settings → IPN Secret Key
|
|
- Generate a new key if you don't have one
|
|
- Add it to the `.env` file
|
|
|
|
**Database Configuration:**
|
|
- Set your MySQL/MariaDB connection details:
|
|
- `DB_HOST` - Database host (default: localhost)
|
|
- `DB_PORT` - Database port (default: 3306)
|
|
- `DB_USER` - Database username
|
|
- `DB_PASSWORD` - Database password
|
|
- `DB_NAME` - Database name (default: cbd420)
|
|
|
|
### 3. Database Migration
|
|
|
|
The application requires the `expires_at` column in the `pending_orders` table for the 10-minute reservation mechanism. Run the migration:
|
|
|
|
```bash
|
|
mysql -u your_user -p your_database < migrations/add_expires_at_to_pending_orders.sql
|
|
```
|
|
|
|
Or manually add the column:
|
|
|
|
```sql
|
|
ALTER TABLE `pending_orders`
|
|
ADD COLUMN `expires_at` datetime NOT NULL DEFAULT (DATE_ADD(NOW(), INTERVAL 10 MINUTE))
|
|
AFTER `created_at`;
|
|
|
|
ALTER TABLE `pending_orders`
|
|
ADD INDEX `idx_expires_at` (`expires_at`);
|
|
```
|
|
|
|
### 4. Build the Project
|
|
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
### 5. Run the Server
|
|
|
|
**Development mode (with auto-reload):**
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
**Production mode:**
|
|
```bash
|
|
npm start
|
|
```
|
|
|
|
The server will start on port 3000 (or the port specified in your `.env` file).
|
|
|
|
## Endpoints
|
|
|
|
### POST `/ipn`
|
|
Receives IPN notifications from NOWPayments. This endpoint:
|
|
- Validates the request signature
|
|
- Processes payment status updates
|
|
- Returns 200 OK to acknowledge receipt
|
|
|
|
### GET `/health`
|
|
Health check endpoint to verify the server is running.
|
|
|
|
## IPN Callback URL Setup
|
|
|
|
When creating a payment via the NOWPayments API, include the `ipn_callback_url` parameter:
|
|
|
|
```javascript
|
|
{
|
|
"price_amount": 100,
|
|
"price_currency": "usd",
|
|
"pay_currency": "btc",
|
|
"ipn_callback_url": "https://yourdomain.com/ipn",
|
|
// ... other parameters
|
|
}
|
|
```
|
|
|
|
## Payment Statuses
|
|
|
|
The listener handles the following payment statuses:
|
|
|
|
- `waiting` - Payment is waiting
|
|
- `confirming` - Payment is being confirmed on blockchain
|
|
- `confirmed` - Payment confirmed on blockchain
|
|
- `sending` - Payment is being sent
|
|
- `partially_paid` - Payment partially received
|
|
- `finished` - Payment completed successfully
|
|
- `failed` - Payment failed
|
|
- `refunded` - Payment refunded
|
|
- `expired` - Payment expired
|
|
|
|
## Database Integration
|
|
|
|
The application implements a **10-minute reservation mechanism** to prevent race conditions when multiple buyers attempt to purchase the last available units simultaneously.
|
|
|
|
### Payment Processing Flow
|
|
|
|
When a payment status is `finished` or `confirmed`, the system:
|
|
|
|
1. **Find Pending Order** - Looks up the pending order by `payment_id` or `invoice_id`
|
|
2. **Check Expiration** - Verifies the pending order hasn't expired (10-minute window)
|
|
3. **Validate Payment Status** - Processes based on status:
|
|
- `finished` or `confirmed` → Proceed to create sale
|
|
- `failed` or `expired` → Delete pending order
|
|
- `waiting`, `confirming` → Acknowledge and wait
|
|
4. **Final Inventory Check** - Validates inventory is still available before creating sale
|
|
5. **Create Sale Record** - Inserts into `sales` table and deletes from `pending_orders`
|
|
|
|
All operations are performed within a database transaction to ensure data consistency and prevent race conditions.
|
|
|
|
### Database Schema
|
|
|
|
The application expects the following tables (as defined in `cbd420(1).sql` + migration):
|
|
|
|
- **`pending_orders`** - Stores pending payment orders with 10-minute reservations
|
|
- `payment_id` (unique) - NOWPayments payment/invoice ID
|
|
- `order_id` (unique) - Internal order ID (format: SALE-{timestamp}-{drop_id}-{buyer_id})
|
|
- `drop_id` - Reference to drops table
|
|
- `buyer_id` - Reference to buyers table
|
|
- `size` - Order size (in grams)
|
|
- `price_amount` - Payment amount
|
|
- `price_currency` - Payment currency (default: 'chf')
|
|
- `created_at` - Order creation timestamp
|
|
- `expires_at` - Expiration timestamp (10 minutes from creation) - **REQUIRED**
|
|
|
|
- **`sales`** - Stores completed sales
|
|
- `drop_id` - Reference to drops table
|
|
- `buyer_id` - Reference to buyers table
|
|
- `size` - Sale size (in grams)
|
|
- `payment_id` - NOWPayments payment ID (matches pending_orders.payment_id)
|
|
- `created_at` - Sale creation timestamp
|
|
|
|
- **`drops`** - Product drop information
|
|
- `id` - Drop ID
|
|
- `size` - Available size
|
|
- `unit` - Unit of measurement ('g' or 'kg')
|
|
|
|
### Inventory Calculation
|
|
|
|
Available inventory is calculated as:
|
|
```
|
|
Available = drop.size - (SUM(sales.size) + SUM(pending_orders.size WHERE expires_at > NOW()))
|
|
```
|
|
|
|
The system automatically handles unit conversion (kg to grams) when calculating inventory.
|
|
|
|
### Key Features
|
|
|
|
- **Expiration Handling**: Pending orders expire after 10 minutes and are automatically excluded from inventory calculations
|
|
- **Inventory Validation**: Final inventory check ensures no overselling occurs between payment initiation and completion
|
|
- **Idempotency**: IPN callbacks can be processed multiple times safely (checks for existing sales)
|
|
- **Transaction Safety**: All database operations use transactions to ensure atomicity
|
|
|
|
## IPN Callback Processing
|
|
|
|
The IPN handler follows this flow for each callback:
|
|
|
|
1. **Find Pending Order** - Searches by `payment_id` or `invoice_id` (tries both)
|
|
2. **Check Expiration** - Verifies `expires_at > NOW()` - expired orders are rejected
|
|
3. **Payment Status Processing**:
|
|
- `finished` or `confirmed` → Creates sale (after inventory check)
|
|
- `failed` or `expired` → Deletes pending order
|
|
- Other statuses → Acknowledged, waiting for final status
|
|
4. **Final Inventory Check** - Re-validates inventory before creating sale (within transaction)
|
|
5. **Create Sale** - Atomically creates sale and deletes pending order
|
|
|
|
### Important Notes
|
|
|
|
- IPN callbacks may be sent multiple times - the system handles this with idempotency checks
|
|
- Expired pending orders are automatically excluded from inventory calculations
|
|
- Always returns HTTP 200 to NOWPayments (even on errors) to prevent retries
|
|
- Inventory is checked within a database transaction to prevent race conditions
|
|
|
|
## Security
|
|
|
|
- All IPN requests are validated using HMAC SHA512 signature verification
|
|
- Invalid signatures are rejected with 400 Bad Request
|
|
- The IPN secret key should never be committed to version control
|
|
- Database transactions ensure data consistency and prevent race conditions
|
|
|
|
## Testing
|
|
|
|
You can test the IPN endpoint using tools like:
|
|
- Postman
|
|
- curl
|
|
- NOWPayments test payments
|
|
|
|
Example curl command (with test signature):
|
|
```bash
|
|
curl -X POST http://localhost:3000/ipn \
|
|
-H "Content-Type: application/json" \
|
|
-H "x-nowpayments-sig: your_signature_here" \
|
|
-d '{"payment_id":"test123","payment_status":"waiting","price_amount":100,"price_currency":"usd"}'
|
|
```
|
|
|
|
## Documentation
|
|
|
|
For detailed implementation guide and database schema, refer to the **IPN Callback Integration Guide** provided with this application.
|
|
|
|
## License
|
|
|
|
ISC
|
|
|