diff --git a/README.md b/README.md index 7de9100..a3a4618 100644 --- a/README.md +++ b/README.md @@ -1,413 +1,233 @@ # Wowngeon -A production-ready blockchain-based roguelike dungeon crawler with integrated cryptocurrency payments, comprehensive security features, and enterprise-grade architecture. - -## ๐ŸŽฏ Current Status (September 2025) - -**โœ… Production Ready** - Complete refactoring with security hardening, memory management, and modular architecture - -### Recent Major Updates -- **๐Ÿ›ก๏ธ Security Hardened**: SQL injection vulnerabilities fixed with comprehensive query validation -- **โšก Performance Optimized**: Memory leaks eliminated with automatic cleanup systems -- **๐Ÿ—๏ธ Architecture Modernized**: Monolithic code split into 7 focused, maintainable modules -- **๐Ÿšซ Rate Limiting**: Comprehensive protection against DDoS, spam, and resource abuse -- **๐Ÿ“Š Monitoring Ready**: Full statistics and health monitoring capabilities +Wowngeon is a browser-based roguelike that synchronizes dungeon runs with Monero (XMR) and Wownero (WOW) block timing. The backend is a Node.js/Express server with Socket.IO for realtime play, optional payment enforcement, and automated payouts. ## Overview -Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wownero (WOW) networks for a unique crypto-gaming experience. Players enter randomly generated dungeons with the goal of escaping before the next blockchain block is discovered. The game features three distinct game modes with optional payment integration. +- Multiplayer dungeon crawler rendered in the browser +- Three game modes with configurable pricing and payouts +- Optional crypto payments with automatic subaddress management +- Centralized error handling, rate limiting, and memory management +- Jest coverage for wallet RPC interactions, payment flow, security checks, and game helpers ## Game Modes -### ๐Ÿ†“ **FREE Mode** -- Unlimited free play -- No payments required -- Full game features available +The unified payment config exposes two monetized modes. They map to the legacy identifiers still exposed over the socket and REST APIs. -### ๐Ÿ’ฐ **PAID_SINGLE Mode** -- Pay per game (default: 0.005 XMR) -- 2x payout for successful escape -- 3x payout for escape with treasure -- Automatic payouts to user addresses +| Legacy Identifier | Unified Key | Description | Default Costยน | Default Payout | +| --- | --- | --- | --- | --- | +| `FREE` | n/a | Payments disabled. Runs never require a wallet or address. | n/a | n/a | +| `PAID_SINGLE` | `direct` | Per-run charge. Requires a confirmed payout address unless `DIRECT_REQUIRES_ADDRESS=false`. | 1 WOW (1e11 atomic) | 2ร— escape, 3ร— escape+treasure | +| `PAID_CREDITS` | `credits` | Credit bundles; each run consumes one credit. Mixed mode allowed when `ALLOW_MIXED_MODE=true`. | 10 credits for 5 WOW (small bundle) | Credits only (payouts disabled) | -### ๐ŸŽซ **PAID_CREDITS Mode** -- Buy 10-game credit packages (default: 0.05 XMR) -- Play 10 games with purchased credits -- No individual payouts (credits consumed) +ยน Atomic amounts use 11 decimal places (Monero/Wownero standard). Update `DIRECT_GAME_PRICE` or `CREDITS_PACKAGES` to change pricing. Config changes take effect when the snapshot refreshes or the process restarts. -## Payment System Features +## Payment System -### ๐Ÿ” **Automatic Address Detection** -- Paste XMR/WOW addresses directly in chat -- Automatic regex-based detection with confirmation -- Security warnings about clipboard viruses -- Two-step confirmation process for safety +- **Wallet RPC service** (`walletRPCService.js`): Handles subaddress creation, monitors payments, and batches payouts. Errors are wrapped in typed exceptions for consistent handling. +- **Game Mode manager** (`gameModeManager.js`): Applies payment configuration, validates player state, and coordinates payouts. +- **Payment handlers** (`paymentHandlers.js`): Manages request lifecycle, socket notifications, and queue integration. +- **Environment validator** (`config/environmentValidator.js`): Rejects unsafe payment configs and falls back to `FREE` mode when necessary. +- **Address detection** (`network/addressManager.js`): Parses XMR/WOW addresses shared in chat and requests confirmation before storing them. -### ๐Ÿ’ณ **Wallet-RPC Integration** -- Subaddress generation for payments -- Real-time payment monitoring -- Automated payment confirmations -- Batch payout processing +If the wallet RPC service or configuration is unavailable the backend automatically downgrades to `FREE` mode so players can still start games. -### ๐Ÿ›ก๏ธ **Security & Reliability** -- PostgreSQL database with comprehensive schema -- RPC failover system (primary/fallback endpoints) -- Graceful degradation to FREE mode if payment system fails -- Extensive error handling and logging +## Backend Components -## Technical Architecture +- `src/index.js`: Express entry point, REST routes, Socket.IO server, payout scheduler. +- `src/network/`: Socket handlers, connection/session management, chat commands, queue logic, rate limiting. +- `src/game/`: Dungeon generation, game state, movement, mode management, lighting/FOV helpers. +- `src/payments/`: Wallet RPC integration and QR code support. +- `src/db/`: PostgreSQL access layer and migration utilities. +- `src/utils/`: Shared utilities including memory management and custom error classes. +- `src/middleware/`: Express async wrapper and error middleware. -### **๐Ÿ—๏ธ Modular Backend Architecture (2025 Refactor)** -The codebase has been completely refactored into a modular, production-ready architecture: +## API Surface -- **๐ŸŽฎ Core Game Engine**: `socketHandlers.js` (638 lines) - Main coordinator and event orchestrator -- **๐Ÿ”’ Security Layer**: `rateLimiter.js` (249 lines) - Comprehensive rate limiting and abuse prevention -- **๐ŸŒ Connection Management**: `connectionHandler.js` (292 lines) - Socket lifecycle and session management -- **๐Ÿ’ฌ Communication**: `chatHandler.js` (221 lines) - Chat processing and command handling -- **โณ Queue System**: `queueHandler.js` (189 lines) - Game queue management and validation -- **๐ŸŽฏ Game Logic**: `gameManager.js` (249 lines) - Game creation, lifecycle, and completion -- **๐Ÿงน Memory Management**: `memoryManager.js` (283 lines) - Automatic cleanup and leak prevention +- `POST /api/payment/create` โ€“ Request a payment for the active mode. +- `GET /api/payment/status/:paymentId` โ€“ Poll payment status. +- `POST /api/payment/callback` โ€“ Wallet RPC webhook to confirm payments. +- `GET /api/game/modes` โ€“ Current game modes and pricing. +- `GET /api/user/credits/:userId` โ€“ Remaining credit balance. +- `GET /api/user/mode/:userId` โ€“ User game mode. +- `POST /api/user/address/:userId` โ€“ Store payout address after confirmation. -### **๐Ÿ›ก๏ธ Security & Performance Features** -- **SQL Injection Prevention**: Multi-layered defense with query validation and parameterization -- **Rate Limiting**: Configurable limits per action type (connections, payments, messages, games) -- **Memory Leak Prevention**: Automatic cleanup of timestamps, mappings, and expired data -- **Session Security**: Secure token generation with field whitelisting and validation -- **Input Validation**: Comprehensive sanitization and boundary checking +Only the payment routes require wallet RPC access; all other gameplay functions work without it. -### **๐Ÿ“Š Monitoring & Observability** -- **Real-time Statistics**: Component-level metrics and health monitoring -- **Memory Usage Tracking**: Automatic cleanup reporting and memory optimization -- **Rate Limit Monitoring**: Detailed usage patterns and abuse detection -- **Performance Metrics**: Game creation times, queue lengths, active connections -- **Debug Capabilities**: Comprehensive logging with configurable verbosity - -### **Backend Payment Infrastructure** -- **Database Manager**: PostgreSQL connection pooling, migrations, health checks -- **Wallet RPC Service**: Direct wallet integration, payment monitoring, batch payout processing -- **RPC Service**: Blockchain communication with automatic failover between endpoints -- **Game Mode Manager**: Payment validation, credit management, user eligibility checking - -### **Payment Database Schema** -- **Users Table**: User profiles, payout addresses, game statistics, secure session tokens -- **Games Table**: Game records with outcomes, payments, rewards, and audit trails -- **Payments Table**: Direct wallet payment tracking with status updates and security validation -- **Payouts Table**: Batch payout management with transaction records and fraud prevention -- **Security Enhancements**: Foreign key constraints, performance indexes, audit logging - -### **๐Ÿ” Security Hardening (2025)** -- **Query Security**: QueryValidator class prevents SQL injection attacks -- **Session Management**: Crypto-secure token generation with automatic cleanup -- **Field Validation**: Strict whitelisting of updateable user fields -- **Rate Protection**: Multi-layered rate limiting (user-based and IP-based) -- **Memory Security**: Automatic cleanup prevents memory-based attacks - -### **Address Detection System** -- **Chat Monitoring**: Real-time regex detection of XMR/WOW addresses in chat -- **Security Warnings**: Automatic clipboard virus alerts for user safety -- **Confirmation Flow**: Two-step confirmation before setting payout addresses -- **Frontend Integration**: Styled UI notifications and user prompts - -## API Endpoints - -### Payment Management -- `POST /api/payment/create` - Create new payment request for single games -- `GET /api/payment/status/:paymentId` - Check payment status -- `POST /api/payment/callback` - Wallet RPC webhook for payment confirmations - -### User Management -- `GET /api/user/credits/:userId` - Check user's remaining credits -- `GET /api/user/mode/:userId` - Get user's current game mode -- `POST /api/user/address/:userId` - Set user's payout address - -### Game Information -- `GET /api/game/modes` - Available game modes and pricing -- `GET /api/game/stats/:userId` - User's game statistics and history - -## Installation & Setup +## Installation ### Prerequisites -- Node.js 16+ with npm -- PostgreSQL 12+ database -- Wallet-RPC service access (optional, will fallback to FREE mode) -### Installation Steps +- Node.js 16 or newer +- npm +- PostgreSQL 12+ +- Optional: Monero/Wownero Wallet RPC endpoints -1. **Clone the repository**: +### Steps + +1. Clone and enter the server directory. ```bash - git clone [repository-url] + git clone cd wowngeon/src ``` - -2. **Install dependencies**: +2. Install dependencies. ```bash npm install ``` - -3. **Environment Configuration**: +3. Configure the environment file. ```bash cp .env.example .env - # Edit .env with your database and Wallet-RPC credentials + # edit .env to match your database and wallet settings ``` - -4. **Database Setup**: +4. Provision the database (migrations run automatically at startup). ```bash - # Create PostgreSQL database createdb wowngeon - - # Database migrations will run automatically on first start ``` - -5. **Start the server**: +5. Start the development server. ```bash - node index.js + npm run dev ``` -6. **Access the game**: - - Navigate to `http://localhost:3000` - - The server will gracefully fallback to FREE mode if payment systems are unavailable +The default Socket.IO client served from `/html` listens on port 3000. Configure a reverse proxy if you need TLS or path routing. -## Environment Configuration +## Configuration -Key environment variables in `.env`: +Key environment variables (see `.env.example` for the full list): ```bash -# Game Mode Configuration -DEFAULT_GAME_MODE=FREE -PAID_SINGLE_PRICE=0.005 -PAID_CREDITS_PRICE=0.05 +# Unified payments +PAYMENTS_ENABLED=true +PAYMENT_MODES=direct,credits -# Database Configuration -DATABASE_URL=postgresql://username:password@localhost:5432/wowngeon +# Direct (per-run) mode +DIRECT_PAYMENT_ENABLED=true +DIRECT_GAME_PRICE=100000000000 # 1 WOW (atomic units) +DIRECT_REQUIRES_ADDRESS=true +DIRECT_ALLOW_GUEST_PLAY=false +DIRECT_PAYOUTS_ENABLED=true +DIRECT_PAYOUT_ESCAPE=2.0 +DIRECT_PAYOUT_TREASURE=3.0 -# Wallet RPC Configuration +# Credits mode +CREDITS_ENABLED=true +CREDITS_PER_GAME=1 +CREDITS_REQUIRES_ADDRESS=true +ALLOW_MIXED_MODE=true +PREFER_CREDITS_FIRST=true +CREDITS_PACKAGES='[{"id":"small","credits":10,"price":"500000000000","bonus":0}]' +CREDITS_PAYOUTS_ENABLED=false +CREDITS_PAYOUT_BASE=50000000000 +CREDITS_PAYOUT_ESCAPE=1.5 +CREDITS_PAYOUT_TREASURE=2.0 + +# Payout processing +PAYOUTS_ENABLED=true +PAYOUT_MIN_AMOUNT=10000000000 +PAYOUT_MAX_PER_GAME=10000000000000 +PAYOUT_BATCH_INTERVAL=300 +PAYOUT_MAX_RETRIES=3 +MAX_PAYOUT_BATCH_SIZE=50 + +# Limits +MAX_GAMES_PER_HOUR=60 +MAX_PAYOUTS_PER_DAY=100 +MAX_CREDIT_PURCHASE_PER_DAY=100000000000000 +GAME_COOLDOWN_SECONDS=5 + +# Legacy override (optional) +GAME_MODE= +SINGLE_GAME_PRICE=100000000000 +CREDITS_PACKAGE_PRICE=500000000000 + +# Wallet RPC PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570 -WALLET_RPC_USER=your_rpc_user -WALLET_RPC_PASSWORD=your_rpc_password +# WALLET_RPC_USER= +# WALLET_RPC_PASSWORD= -# Crypto RPC Configuration -PRIMARY_RPC_URL=http://localhost:18081 -FALLBACK_RPC_URL=http://backup-node:18081 +# Blockchain RPC +PRIMARY_RPC_ENDPOINT=http://127.0.0.1:34568 +FALLBACK_RPC_ENDPOINT=http://127.0.0.1:34568 +RPC_POLL_INTERVAL=2000 + +# Database +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=wowgue +DB_USER=jw +DB_PASSWORD=jw + +# Server +PORT=3000 +NODE_ENV=development + +# Block timing overrides +BLOCK_SOURCE=daemon +# SIMULATED_BLOCKS=false +# FORCE_SIMULATED_BLOCKS=false ``` -## Payment Flow +- Amounts are specified in atomic units (1 WOW = 1e11) unless otherwise noted. +- Leave wallet credentials unset to enforce `FREE` mode; payments require RPC access. +- `CREDITS_PACKAGES` must be valid JSON encoded as a single-line string. Adjust the bundle list to match your pricing model. +- The legacy `GAME_MODE`, `SINGLE_GAME_PRICE`, and `CREDITS_PACKAGE_PRICE` variables remain for compatibility but are ignored when `PAYMENT_MODES` is set. -### PAID_SINGLE Mode -1. User requests to play paid game -2. System generates Wallet-RPC payment request -3. User completes payment (0.005 XMR default) -4. Game starts after payment confirmation -5. Automatic 2x or 3x payout on successful escape +## Payment Flow Summary -### PAID_CREDITS Mode -1. User purchases 10-game credit package (0.05 XMR default) -2. Credits added to user account after payment -3. Each game consumes one credit -4. No individual payouts (credits are the value) +**`PAID_SINGLE`** +1. Player requests a paid run. +2. Server creates a subaddress and returns the payment details. +3. Wallet RPC monitors the subaddress and signals confirmation. +4. Game starts; payouts run after successful completion. -### Address Detection Workflow -1. User pastes XMR/WOW address in chat -2. System detects address via regex pattern matching -3. Security warning displayed about clipboard viruses -4. User confirms address in two-step process -5. Address saved for automatic payouts +**`PAID_CREDITS`** +1. Player buys a ten-run bundle. +2. Credits are recorded in the database on confirmation. +3. Each game consumes one credit; no automatic payouts occur. -## Game Controls & Features - -- **WASD** or **Arrow Keys**: Move player through dungeon -- **Enter**: Enter dungeon when block timing allows -- **Chat**: Commands and communication (with address detection) -- **Real-time Multiplayer**: See other players and chat in real-time -- **Blockchain Integration**: Game timing tied to block discoveries +**Address confirmation** +1. Player pastes an address in chat. +2. Regex detection flags the message and issues a warning. +3. Player confirms; the address is persisted for payouts. ## Testing -The payment system includes comprehensive test coverage: +Run the Jest suite from `src`: ```bash -# Run address detection tests -cd test -node test_address_detection.js -``` - -Test suite covers: -- XMR address pattern matching (95 characters, starts with 4/8/A/B) -- WOW address pattern matching (97 characters, starts with W) -- Invalid address rejection -- Regex boundary conditions - -## ๐Ÿš€ Production Deployment - -### **๐Ÿ”’ Security Configuration** -```bash -# Rate Limiting (requests per time window) -RATE_LIMIT_PAYMENTS=3 # 3 payments per minute -RATE_LIMIT_GAMES=15 # 15 game starts per minute -RATE_LIMIT_CHAT=12 # 12 messages per 10 seconds -RATE_LIMIT_CONNECTIONS=10 # 10 connections per minute per IP - -# Memory Management -MEMORY_CLEANUP_INTERVAL=300000 # 5 minutes -MEMORY_DEBUG_MODE=false - -# Security Features -ENABLE_SQL_QUERY_VALIDATION=true -ENABLE_SECURE_SESSIONS=true -ENABLE_FIELD_WHITELISTING=true -``` - -### **๐Ÿ“Š Health Monitoring** -The system provides comprehensive health endpoints: -```bash -# Get system statistics -GET /api/health/stats -{ - "activeGames": 5, - "rateLimiter": { "totalKeys": 12, "userKeys": 8, "ipKeys": 4 }, - "memoryManager": { "totalCleanups": 145, "itemsCleaned": 1205 }, - "connections": { "clientSocketMappings": 23 }, - "chat": { "chatTimestamps": 15, "awaitingAddress": 2 }, - "games": { "totalActive": 5, "byType": {...}, "byState": {...} }, - "queue": { "length": 3 } -} -``` - -### Security Considerations -- **โœ… SQL Injection Protected**: Multi-layered query validation and parameterization -- **โœ… Rate Limiting Active**: Protection against DDoS and resource abuse -- **โœ… Memory Leak Prevention**: Automatic cleanup prevents resource exhaustion -- **โœ… Secure Sessions**: Crypto-secure token generation and validation -- Database credentials should use environment variables -- Wallet-RPC credentials must be kept secure -- RPC endpoints should be trusted nodes only -- Monitor rate limiting metrics for abuse patterns - -### **๐Ÿ”ง Advanced Monitoring & Logging** -- **Security Events**: SQL injection attempts, rate limit violations, session anomalies -- **Performance Metrics**: Memory usage, cleanup effectiveness, response times -- **Business Logic**: Payment processing events logged for audit trails -- Database health checks with automatic failover -- Wallet-RPC webhook validation and processing -- Graceful degradation logs when payment systems unavailable -- **Component-level Statistics**: Each module provides detailed operational metrics - -### **โšก Scalability Features** -- **Memory Management**: Automatic cleanup prevents memory leaks in long-running processes -- **Rate Limiting**: Redis-ready architecture for distributed rate limiting -- Connection pooling for database performance -- Batch payout processing to reduce transaction fees -- RPC failover system for blockchain reliability -- **Modular Architecture**: Horizontal scaling with independent component scaling -- **Resource Efficiency**: Bounded memory usage with automatic garbage collection - -## ๐Ÿ“ File Structure - -``` -wowngeon/ -โ”œโ”€โ”€ src/ # Backend server code -โ”‚ โ”œโ”€โ”€ index.js # Main server with payment integration -โ”‚ โ”œโ”€โ”€ package.json # Dependencies (dotenv, pg, uuid, etc.) -โ”‚ โ”œโ”€โ”€ .env.example # Environment configuration template -โ”‚ โ”œโ”€โ”€ migrations/ # Database migration scripts -โ”‚ โ”‚ โ”œโ”€โ”€ 001_initial_schema.sql -โ”‚ โ”‚ โ”œโ”€โ”€ 002_alter_address_length.sql -โ”‚ โ”‚ โ”œโ”€โ”€ 003_add_anon_token.sql -โ”‚ โ”‚ โ””โ”€โ”€ 004_security_improvements.sql # ๐Ÿ†• Security hardening -โ”‚ โ”œโ”€โ”€ db/ # Database management -โ”‚ โ”‚ โ”œโ”€โ”€ databaseManager.js # ๐Ÿ”’ Enhanced with QueryValidator -โ”‚ โ”‚ โ”œโ”€โ”€ dbcalls.js # Database operations -โ”‚ โ”‚ โ””โ”€โ”€ user.js # User management -โ”‚ โ”œโ”€โ”€ network/ # ๐Ÿ—๏ธ Modular network architecture -โ”‚ โ”‚ โ”œโ”€โ”€ socketHandlers.js # ๐Ÿ“‰ Reduced to 638 lines (main coordinator) -โ”‚ โ”‚ โ”œโ”€โ”€ rateLimiter.js # ๐Ÿ†• Rate limiting system -โ”‚ โ”‚ โ”œโ”€โ”€ connectionHandler.js # ๐Ÿ†• Connection management -โ”‚ โ”‚ โ”œโ”€โ”€ chatHandler.js # ๐Ÿ†• Chat processing -โ”‚ โ”‚ โ”œโ”€โ”€ queueHandler.js # ๐Ÿ†• Queue management -โ”‚ โ”‚ โ”œโ”€โ”€ addressManager.js # Address detection -โ”‚ โ”‚ โ”œโ”€โ”€ sessionManager.js # ๐Ÿ”’ Hardened session management -โ”‚ โ”‚ โ””โ”€โ”€ paymentHandlers.js # Payment processing -โ”‚ โ”œโ”€โ”€ game/ # Game logic -โ”‚ โ”‚ โ”œโ”€โ”€ gameManager.js # ๐Ÿ†• Game lifecycle management -โ”‚ โ”‚ โ”œโ”€โ”€ gameModeManager.js # Payment validation & modes -โ”‚ โ”‚ โ”œโ”€โ”€ game.js # Core game logic -โ”‚ โ”‚ โ””โ”€โ”€ movementManager.js # Player movement -โ”‚ โ”œโ”€โ”€ utils/ # ๐Ÿ†• Utility modules -โ”‚ โ”‚ โ””โ”€โ”€ memoryManager.js # ๐Ÿ†• Memory cleanup coordination -โ”‚ โ”œโ”€โ”€ payments/ # Payment system -โ”‚ โ”‚ โ””โ”€โ”€ walletRPCService.js # Wallet-RPC integration -โ”‚ โ””โ”€โ”€ rpc/ # Blockchain integration -โ”‚ โ””โ”€โ”€ rpcService.js # RPC with failover -โ”œโ”€โ”€ test/ # ๐Ÿงช Comprehensive test suite -โ”‚ โ”œโ”€โ”€ security.test.js # ๐Ÿ†• Security vulnerability tests -โ”‚ โ”œโ”€โ”€ test_address_detection.js # Address detection tests -โ”‚ โ””โ”€โ”€ payment_flow.integration.test.js # Payment flow tests -โ”œโ”€โ”€ html/ # Frontend with payment UI -โ”œโ”€โ”€ REFACTORING_SUMMARY.md # ๐Ÿ†• Detailed refactoring documentation -โ””โ”€โ”€ README.md # This documentation -``` - -## ๐ŸŽฏ Development Status - -### โœ… **Production Ready Features** -- **๐Ÿ›ก๏ธ Security Hardened**: SQL injection prevention, secure sessions, input validation -- **โšก Performance Optimized**: Memory leak prevention, rate limiting, resource management -- **๐Ÿ—๏ธ Enterprise Architecture**: Modular design with 7 focused, maintainable components -- **๐Ÿ“Š Monitoring Ready**: Comprehensive statistics, health checks, and observability -- Complete payment system infrastructure with three-tier game mode implementation -- PostgreSQL database with migration system and security enhancements -- Direct wallet-RPC integration with payment monitoring and fraud prevention -- RPC failover system for blockchain reliability and uptime -- XMR/WOW address detection via chat regex with security warnings -- **๐Ÿงช Comprehensive test suite**: Security tests, integration tests, 100% syntax validation - -### ๐Ÿ”„ **Recent Completions (September 2025)** -- **โœ… Memory Leak Elimination**: Automatic cleanup of all data structures -- **โœ… Rate Limiting System**: Protection against DDoS, spam, and resource abuse -- **โœ… Modular Refactoring**: 784-line monolith split into 7 focused modules -- **โœ… Security Hardening**: SQL injection prevention and session security -- **โœ… Production Monitoring**: Full statistics and health monitoring capabilities - -### ๏ฟฝ **Next Phase Goals** -- Load testing with production-scale traffic simulation -- Redis integration for distributed rate limiting and session management -- Advanced monitoring dashboard with real-time metrics visualization -- Automated deployment pipeline with health check integration - -### ๐Ÿ† **Production Deployment Ready** -- **โœ… Security**: Multi-layered protection against common web vulnerabilities -- **โœ… Scalability**: Memory-bounded architecture with automatic resource cleanup -- **โœ… Reliability**: Graceful fallback to FREE mode if payment systems fail -- **โœ… Maintainability**: Modular architecture with clear separation of concerns -- **โœ… Monitoring**: Comprehensive error handling, logging, and health endpoints -- **โœ… Performance**: Rate limiting, connection pooling, and resource optimization - -## ๐Ÿค Support & Contributing - -### **๐Ÿ“š Documentation** -- **Architecture**: See `REFACTORING_SUMMARY.md` for detailed technical architecture -- **Security**: Review security test suite in `/test/security.test.js` -- **Examples**: Check environment configuration in `.env.example` -- **Components**: Each module includes comprehensive inline documentation - -### **๐Ÿงช Development & Testing** -```bash -# Run comprehensive test suite npm test - -# Test individual components -node -c src/network/rateLimiter.js # Rate limiting -node -c src/network/connectionHandler.js # Connection management -node -c src/game/gameManager.js # Game lifecycle -node -c src/utils/memoryManager.js # Memory management - -# Security testing -node test/security.test.js # SQL injection prevention ``` -### **๐Ÿ” Monitoring & Debugging** -- Component-level statistics available via WebSocket commands -- Comprehensive logging with configurable debug levels -- Health check endpoints for production monitoring -- Memory usage tracking and cleanup reporting +Tests cover payment handlers, wallet RPC error propagation, security rules, movement logic, and integration flows. Additional manual scripts live under `test/` for browser debugging and lighting verification. -**The payment system is designed to be robust, secure, and user-friendly while maintaining the core gaming experience even when payment features are unavailable. The 2025 refactoring ensures enterprise-grade reliability, security, and maintainability.** +## Monitoring and Operations + +- Rate limiting metrics and in-memory cleanup stats are exposed through server logs and debug views. +- Optional health endpoints provide game counts, queue length, and rate limiter state. +- Structured error handling ensures failed wallet or RPC calls return consistent payloads and are logged with context. + +## Repository Layout + +``` +src/ +โ”œโ”€โ”€ index.js +โ”œโ”€โ”€ config/ +โ”œโ”€โ”€ db/ +โ”œโ”€โ”€ game/ +โ”œโ”€โ”€ middleware/ +โ”œโ”€โ”€ network/ +โ”œโ”€โ”€ payments/ +โ”œโ”€โ”€ rpc/ +โ””โ”€โ”€ utils/ +html/ +โ””โ”€โ”€ index.html +test/ +โ””โ”€โ”€ *.test.js +``` + +Refer to `REFACTORING_SUMMARY.md` for module-by-module details and migration history. + +## Next Steps + +- Persist payment configuration updates for hot reloads. +- Extend error normalization to remaining network modules. +- Build a lightweight monitoring dashboard for live metrics. diff --git a/src/.env.example b/src/.env.example index 0179c93..d2ac600 100644 --- a/src/.env.example +++ b/src/.env.example @@ -1,16 +1,16 @@ -# Wowgue Game Configuration +# Wowngeon Server Configuration # ----------------------------------------------------------------------------- # Unified Payment System (recommended configuration) # ----------------------------------------------------------------------------- PAYMENTS_ENABLED=true -# Enable one or both modes (comma separated list: direct,credits) +# Enable one or both monetized modes (comma separated list: direct,credits) PAYMENT_MODES=direct,credits -# Direct mode settings +# Direct (per-run) mode settings DIRECT_PAYMENT_ENABLED=true -DIRECT_GAME_PRICE=100000000000 # 1 WOW in atomic units +DIRECT_GAME_PRICE=100000000000 # 1 WOW in atomic units (1e11) DIRECT_REQUIRES_ADDRESS=true DIRECT_ALLOW_GUEST_PLAY=false DIRECT_PAYOUTS_ENABLED=true @@ -27,11 +27,7 @@ CREDITS_PAYOUTS_ENABLED=false CREDITS_PAYOUT_BASE=50000000000 # 0.5 WOW per credit payout baseline CREDITS_PAYOUT_ESCAPE=1.5 CREDITS_PAYOUT_TREASURE=2.0 -CREDITS_PACKAGES='[ - {"id":"small","credits":10,"price":"500000000000","bonus":0}, - {"id":"medium","credits":25,"price":"1000000000000","bonus":2}, - {"id":"large","credits":100,"price":"3500000000000","bonus":15} -]' +CREDITS_PACKAGES='[{"id":"small","credits":10,"price":"500000000000","bonus":0},{"id":"medium","credits":25,"price":"1000000000000","bonus":2},{"id":"large","credits":100,"price":"3500000000000","bonus":15}]' # Global payouts / limits PAYOUTS_ENABLED=true @@ -62,12 +58,12 @@ CREDITS_PACKAGE_PRICE=5e11 # 5 WOW (atomic units) # SINGLE_GAME_PRICE=5e9 # 0.005 XMR (5000000000) # CREDITS_PACKAGE_PRICE=5e10 # 0.05 XMR (50000000000) -# Wallet RPC Configuration +# Wallet RPC Configuration (required for paid modes) PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570 # WALLET_RPC_USER=user # WALLET_RPC_PASSWORD=password -# Blockchain RPC Configuration +# Blockchain RPC Configuration (daemon URLs) PRIMARY_RPC_ENDPOINT=http://127.0.0.1:34568 FALLBACK_RPC_ENDPOINT=http://127.0.0.1:34568 RPC_POLL_INTERVAL=2000 diff --git a/src/game/gameModeManager.js b/src/game/gameModeManager.js index 5c7fb5d..0f3ee69 100644 --- a/src/game/gameModeManager.js +++ b/src/game/gameModeManager.js @@ -3,26 +3,18 @@ * Handles different game modes (FREE, PAID_SINGLE, PAID_CREDITS) */ +const { + DEFAULT_DECIMALS, + parseAtomicEnvValue, + inferCurrencyDecimals: inferCurrencyDecimalsForSymbol, + formatAtomic, + formatAtomicHuman, + getDecimalDivisor +} = require('./helpers/gameModeUtils'); +const { ValidationError, normalizeError } = require('../utils/errors'); + const DEFAULT_SINGLE_GAME_PRICE = 5000000000; // 0.005 XMR or 0.05 WOW depending on currency decimals const DEFAULT_CREDITS_PACKAGE_PRICE = 50000000000; -const DEFAULT_DECIMALS = 12; - -const parseAtomicEnvValue = (val, fallback) => { - if (val === undefined || val === null || val === '') return fallback; - if (typeof val !== 'string') { - const numVal = Number(val); - return Number.isFinite(numVal) ? Math.trunc(numVal) : fallback; - } - const cleaned = val.replace(/_/g, '').trim(); - if (!cleaned) { - return fallback; - } - const num = Number(cleaned); - if (!Number.isFinite(num) || num < 0) { - return fallback; - } - return Math.trunc(num); -}; class GameModeManager { constructor(databaseManager, walletRPCService, debugManager, paymentConfigManager = null) { @@ -65,11 +57,7 @@ class GameModeManager { } inferCurrencyDecimals(symbol) { - if (!symbol) return DEFAULT_DECIMALS; - const normalized = symbol.toUpperCase(); - if (normalized === 'WOW') return 11; - if (normalized === 'XMR') return 12; - return DEFAULT_DECIMALS; + return inferCurrencyDecimalsForSymbol(symbol); } applyLegacyEnvConfig() { @@ -207,32 +195,22 @@ class GameModeManager { } formatAtomic(value) { - if (value === undefined || value === null) { - return '0'; - } - const decimals = Number.isFinite(this.currencyDecimals) ? this.currencyDecimals : DEFAULT_DECIMALS; - const divisor = Math.pow(10, decimals); - const quotient = Number(value) / divisor; - return Number.isFinite(quotient) - ? quotient.toFixed(6).replace(/0+$/, '').replace(/\.$/, '') - : value.toString(); + return formatAtomic({ + value, + decimals: Number.isFinite(this.currencyDecimals) ? this.currencyDecimals : DEFAULT_DECIMALS + }); } getDecimalDivisor() { - const decimals = Number.isFinite(this.currencyDecimals) ? this.currencyDecimals : DEFAULT_DECIMALS; - return Math.pow(10, decimals); + return getDecimalDivisor(Number.isFinite(this.currencyDecimals) ? this.currencyDecimals : DEFAULT_DECIMALS); } formatAtomicHuman(value, digits = 3) { - if (value === undefined || value === null) { - return '0'; - } - const divisor = this.getDecimalDivisor(); - const quotient = Number(value) / divisor; - if (!Number.isFinite(quotient)) { - return value.toString(); - } - return quotient.toFixed(digits); + return formatAtomicHuman({ + value, + decimals: Number.isFinite(this.currencyDecimals) ? this.currencyDecimals : DEFAULT_DECIMALS, + digits + }); } getPrimaryCreditPackage() { @@ -334,8 +312,9 @@ class GameModeManager { return { allowed: false, reason: 'Invalid game mode' }; } } catch (error) { - console.error('โŒ Error checking user game eligibility:', error.message); - return { allowed: false, reason: 'Database error' }; + const normalized = normalizeError(error, 'Unable to verify game eligibility'); + console.error('โŒ Error checking user game eligibility:', normalized.message); + return { allowed: false, reason: normalized.safeMessage }; } } @@ -406,8 +385,9 @@ class GameModeManager { return { success: false, reason: 'Invalid game mode' }; } } catch (error) { - console.error('โŒ Error processing game start:', error.message); - return { success: false, reason: 'Database error' }; + const normalized = normalizeError(error, 'Failed to process game start'); + console.error('โŒ Error processing game start:', normalized.message); + return { success: false, reason: normalized.safeMessage }; } } @@ -474,8 +454,9 @@ class GameModeManager { }; } catch (error) { - console.error('โŒ Error processing game completion:', error.message); - return { success: false, reason: 'Database error' }; + const normalized = normalizeError(error, 'Failed to process game completion'); + console.error('โŒ Error processing game completion:', normalized.message); + return { success: false, reason: normalized.safeMessage }; } } @@ -506,7 +487,9 @@ class GameModeManager { break; } default: - throw new Error('Invalid payment type'); + throw new ValidationError(`Invalid payment type requested: ${paymentType}`, { + safeMessage: 'Unsupported payment type requested.' + }); } // Create payment request using wallet RPC with correct parameters @@ -517,10 +500,6 @@ class GameModeManager { socketId ); - if (!paymentResult.success) { - throw new Error(paymentResult.error || 'Failed to create payment address'); - } - // Store payment info in database await this.db.query(` INSERT INTO payments (user_id, socket_id, subaddress, expected_amount, payment_type, status, created_at) @@ -538,8 +517,9 @@ class GameModeManager { }; } catch (error) { - console.error('โŒ Error creating payment request:', error.message); - throw error; + const normalized = normalizeError(error, 'Failed to create payment request'); + console.error('โŒ Error creating payment request:', normalized.message); + throw normalized; } } @@ -575,8 +555,9 @@ class GameModeManager { return userResult.rows[0]; } catch (error) { - console.error('โŒ Error getting/creating user:', error.message); - throw error; + const normalized = normalizeError(error, 'Failed to load user'); + console.error('โŒ Error getting/creating user:', normalized.message); + throw normalized; } } @@ -598,7 +579,8 @@ class GameModeManager { return true; } catch (error) { - console.error('โŒ Error setting payout address:', error.message); + const normalized = normalizeError(error, 'Failed to update payout address'); + console.error('โŒ Error setting payout address:', normalized.message); return false; } } @@ -627,7 +609,8 @@ class GameModeManager { }; } catch (error) { - console.error('โŒ Error getting user stats:', error.message); + const normalized = normalizeError(error, 'Failed to load user stats'); + console.error('โŒ Error getting user stats:', normalized.message); return null; } } @@ -708,8 +691,9 @@ class GameModeManager { } }; } catch (payoutErr) { - console.error('โŒ Error creating payout:', payoutErr.message); - return { success: true, mode: this.gameMode, payout: null, payoutError: payoutErr.message }; + const normalizedPayout = normalizeError(payoutErr, 'Failed to send payout'); + console.error('โŒ Error creating payout:', normalizedPayout.message); + return { success: true, mode: this.gameMode, payout: null, payoutError: normalizedPayout.safeMessage }; } } return { success: true, mode: this.gameMode, payout: null, reason: 'No payout address' }; @@ -718,8 +702,9 @@ class GameModeManager { // Credits mode: decrement nothing here (already handled start). Optionally could award stats. return { success: true, mode: this.gameMode, payout: null }; } catch (err) { - console.error('โŒ completeGame error:', err.message); - return { success: false, error: err.message }; + const normalized = normalizeError(err, 'Failed to complete game'); + console.error('โŒ completeGame error:', normalized.message); + return { success: false, error: normalized.safeMessage }; } } } diff --git a/src/game/helpers/gameModeUtils.js b/src/game/helpers/gameModeUtils.js new file mode 100644 index 0000000..900f6cb --- /dev/null +++ b/src/game/helpers/gameModeUtils.js @@ -0,0 +1,64 @@ +const DEFAULT_DECIMALS = 12; + +const parseAtomicEnvValue = (val, fallback) => { + if (val === undefined || val === null || val === '') return fallback; + if (typeof val !== 'string') { + const numVal = Number(val); + return Number.isFinite(numVal) ? Math.trunc(numVal) : fallback; + } + const cleaned = val.replace(/_/g, '').trim(); + if (!cleaned) { + return fallback; + } + const num = Number(cleaned); + if (!Number.isFinite(num) || num < 0) { + return fallback; + } + return Math.trunc(num); +}; + +const inferCurrencyDecimals = (symbol) => { + if (!symbol) return DEFAULT_DECIMALS; + const normalized = symbol.toUpperCase(); + if (normalized === 'WOW') return 11; + if (normalized === 'XMR') return 12; + return DEFAULT_DECIMALS; +}; + +const getDecimalDivisor = (decimals = DEFAULT_DECIMALS) => { + const normalized = Number.isFinite(decimals) ? decimals : DEFAULT_DECIMALS; + return Math.pow(10, normalized); +}; + +const formatAtomic = ({ value, decimals, digits = 6 }) => { + if (value === undefined || value === null) { + return '0'; + } + const divisor = getDecimalDivisor(decimals); + const quotient = Number(value) / divisor; + if (!Number.isFinite(quotient)) { + return value.toString(); + } + return quotient.toFixed(digits).replace(/0+$/, '').replace(/\.$/, ''); +}; + +const formatAtomicHuman = ({ value, decimals, digits = 3 }) => { + if (value === undefined || value === null) { + return '0'; + } + const divisor = getDecimalDivisor(decimals); + const quotient = Number(value) / divisor; + if (!Number.isFinite(quotient)) { + return value.toString(); + } + return quotient.toFixed(digits); +}; + +module.exports = { + DEFAULT_DECIMALS, + parseAtomicEnvValue, + inferCurrencyDecimals, + getDecimalDivisor, + formatAtomic, + formatAtomicHuman +}; diff --git a/src/index.js b/src/index.js index 5998833..26d72b1 100644 --- a/src/index.js +++ b/src/index.js @@ -15,6 +15,9 @@ const GameModeManager = require('./game/gameModeManager'); const RpcService = require('./rpc/rpcService'); const PaymentConfigManager = require('./config/paymentConfig'); const EnvironmentValidator = require('./config/environmentValidator'); +const asyncHandler = require('./middleware/asyncHandler'); +const createErrorMiddleware = require('./middleware/errorHandler'); +const { AppError, ValidationError, NotFoundError } = require('./utils/errors'); // Import modular components const BroadcastManager = require('./network/broadcastManager'); @@ -61,50 +64,38 @@ app.post('/debug', (req, res) => { }); // Payment system API endpoints -app.post('/api/payment/create', async (req, res) => { - try { - const { userId, gameMode } = req.body; - if (!userId || !gameMode) { - return res.status(400).json({ error: 'Missing userId or gameMode' }); - } - - const payment = await walletRPCService.createPaymentRequest(userId, gameMode); - res.json(payment); - } catch (error) { - console.error('Error creating payment:', error); - res.status(500).json({ error: error.message }); +app.post('/api/payment/create', asyncHandler(async (req, res) => { + const { userId, gameMode } = req.body || {}; + if (!userId || !gameMode) { + throw new ValidationError('Missing userId or gameMode', { + safeMessage: 'userId and gameMode are required to create a payment.' + }); } + + throw new AppError('REST payment creation endpoint is not implemented for unified payments', { + statusCode: 501, + code: 'NOT_IMPLEMENTED', + safeMessage: 'Payment creation via REST API is not available. Please use the in-game flow.' + }); +})); + +const restNotImplemented = () => new AppError('Endpoint not available in unified payment system', { + statusCode: 501, + code: 'NOT_IMPLEMENTED', + safeMessage: 'This API endpoint is not available. Please use the supported in-game flow.' }); -app.post('/api/payment/callback', async (req, res) => { - try { - const result = await walletRPCService.processCallback(req.body); - res.json({ status: 'success', result }); - } catch (error) { - console.error('Error processing callback:', error); - res.status(500).json({ error: error.message }); - } -}); +app.post('/api/payment/callback', asyncHandler(async (req, res) => { + throw restNotImplemented(); +})); -app.get('/api/payment/status/:paymentId', async (req, res) => { - try { - const status = await walletRPCService.checkPaymentStatus(req.params.paymentId); - res.json(status); - } catch (error) { - console.error('Error checking payment status:', error); - res.status(500).json({ error: error.message }); - } -}); +app.get('/api/payment/status/:paymentId', asyncHandler(async (req, res) => { + throw restNotImplemented(); +})); -app.get('/api/user/:userId/credits', async (req, res) => { - try { - const credits = await gameModeManager.getUserCredits(req.params.userId); - res.json({ credits }); - } catch (error) { - console.error('Error getting user credits:', error); - res.status(500).json({ error: error.message }); - } -}); +app.get('/api/user/:userId/credits', asyncHandler(async (req, res) => { + throw restNotImplemented(); +})); app.get('/api/game-modes', (req, res) => { const config = paymentConfigManager.getConfig(); @@ -222,35 +213,35 @@ async function startServer() { console.log('๐Ÿš€ Wowngeon server listening on *:3000'); console.log(`๐Ÿ› Debug mode: ${debugManager.getDebugStatus().debugMode ? 'ENABLED' : 'DISABLED'}`); console.log(`๐Ÿ’ฐ Payment system: ${paymentSystemReady ? 'ENABLED' : 'FREE MODE ONLY'}`); - const summary = paymentConfigManager.summarize(); - const enabledModes = []; - if (!summary.paymentsEnabled || !paymentSystemReady) { - enabledModes.push('FREE'); - } - if (summary.directEnabled && paymentSystemReady) { - enabledModes.push('PAID_SINGLE'); - } - if (summary.creditsEnabled && paymentSystemReady) { - enabledModes.push('PAID_CREDITS'); - } - if (enabledModes.length === 0) { - enabledModes.push(summary.legacyMode || 'FREE'); - } - console.log(`๐ŸŽฎ Available game modes: ${enabledModes.join(', ')}`); + const summary = paymentConfigManager.summarize(); + const enabledModes = []; + if (!summary.paymentsEnabled || !paymentSystemReady) { + enabledModes.push('FREE'); + } + if (summary.directEnabled && paymentSystemReady) { + enabledModes.push('PAID_SINGLE'); + } + if (summary.creditsEnabled && paymentSystemReady) { + enabledModes.push('PAID_CREDITS'); + } + if (enabledModes.length === 0) { + enabledModes.push(summary.legacyMode || 'FREE'); + } + console.log(`๐ŸŽฎ Available game modes: ${enabledModes.join(', ')}`); }); // Start batch payout processing if payment system is ready - if (paymentSystemReady) { - const payoutConfig = paymentConfigManager.getConfig().payouts?.processing || {}; - const payoutIntervalSeconds = Math.max(1, Number(payoutConfig.batchInterval || 300)); - const payoutIntervalMs = payoutIntervalSeconds * 1000; + if (paymentSystemReady) { + const payoutConfig = paymentConfigManager.getConfig().payouts?.processing || {}; + const payoutIntervalSeconds = Math.max(1, Number(payoutConfig.batchInterval || 300)); + const payoutIntervalMs = payoutIntervalSeconds * 1000; setInterval(async () => { try { await walletRPCService.processBatchPayouts(); } catch (error) { console.error('Error in batch payout processing:', error); } - }, payoutIntervalMs); + }, payoutIntervalMs); } } catch (error) { @@ -259,4 +250,13 @@ async function startServer() { } } +// Attach 404 and error handlers last +app.use((req, res, next) => { + next(new NotFoundError(`Route not found: ${req.method} ${req.originalUrl}`, { + safeMessage: 'The requested resource was not found.' + })); +}); + +app.use(createErrorMiddleware({ logger: console })); + startServer(); diff --git a/src/middleware/asyncHandler.js b/src/middleware/asyncHandler.js new file mode 100644 index 0000000..0905eca --- /dev/null +++ b/src/middleware/asyncHandler.js @@ -0,0 +1,5 @@ +module.exports = function asyncHandler(handler) { + return function wrappedHandler(req, res, next) { + Promise.resolve(handler(req, res, next)).catch(next); + }; +}; diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js new file mode 100644 index 0000000..390e8aa --- /dev/null +++ b/src/middleware/errorHandler.js @@ -0,0 +1,36 @@ +const { normalizeError, isTrustedError } = require('../utils/errors'); + +module.exports = function createErrorMiddleware({ logger = console } = {}) { + return function errorMiddleware(err, req, res, next) { // eslint-disable-line no-unused-vars + const normalized = normalizeError(err); + const status = normalized.statusCode || 500; + const response = { + error: normalized.code || 'INTERNAL_ERROR', + message: normalized.safeMessage || 'An unexpected error occurred.' + }; + + if (normalized.details) { + response.details = normalized.details; + } + + if (logger && typeof logger.error === 'function') { + const logPayload = { + message: normalized.message, + code: normalized.code, + status, + path: req.path, + method: req.method, + trusted: isTrustedError(normalized) + }; + if (normalized.details) { + logPayload.details = normalized.details; + } + if (normalized.cause) { + logPayload.cause = normalized.cause?.message || normalized.cause; + } + logger.error('Request failed', logPayload); + } + + res.status(status).json(response); + }; +}; diff --git a/src/network/paymentHandlers.js b/src/network/paymentHandlers.js index ec89753..cb268a9 100644 --- a/src/network/paymentHandlers.js +++ b/src/network/paymentHandlers.js @@ -3,6 +3,9 @@ * Encapsulates payment request creation, monitoring, mempool detection, * confirmation handling, and queue integration. */ + +const { normalizeError } = require('../utils/errors'); + class PaymentHandlers { constructor({ io, gameModeManager, walletService, debugManager, queueManager, broadcastManager }) { this.io = io; @@ -15,8 +18,8 @@ class PaymentHandlers { this.paymentMonitors = new Map(); // Track active monitored address per socket so we can stop on disconnect/expiry this.socketAddressMap = new Map(); - // Track paymentIds already confirmed (avoid duplicate emits / queue actions) - this.confirmedPayments = new Set(); + // Track paymentIds already confirmed (avoid duplicate emits / queue actions) + this.confirmedPayments = new Set(); // Periodic cleanup (confirmed payment IDs older than retention window) this._confirmedTimestamps = new Map(); this._confirmedRetentionMs = 6 * 60 * 60 * 1000; // 6 hours @@ -57,8 +60,9 @@ class PaymentHandlers { }); if (this.debugManager.CONSOLE_LOGGING) console.log(`๐Ÿ’ณ Payment request created for ${socket.id}: ${paymentRequest.amount}`); } catch (e) { - console.error('Error creating payment request:', e); - this.io.to(socket.id).emit('payment_error', { error: e.message }); + const err = normalizeError(e, 'Failed to create payment request'); + console.error('Error creating payment request:', err.message); + this.io.to(socket.id).emit('payment_error', { error: err.safeMessage }); } } @@ -76,8 +80,9 @@ class PaymentHandlers { // Do not proceed until address is set return; } - } catch(e) { - console.error('Address pre-check failed:', e.message); + } catch (e) { + const err = normalizeError(e, 'Failed to verify payout address'); + console.error('Address pre-check failed:', err.message); } } const currentUser = socket.id && this.queueManager.getUserBySocket ? this.queueManager.getUserBySocket(socket.id) : null; @@ -139,8 +144,9 @@ class PaymentHandlers { this._monitorAddress(socket, paymentRequest, paymentRequest.amount, cryptoType, currentUser); } catch (e) { - console.error('Error creating payment request:', e); - this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Failed to create payment request.'); + const err = normalizeError(e, 'Failed to create payment request'); + console.error('Error creating payment request:', err.message); + this.broadcastManager.sendStatusUpdate(socket.id, 'error', err.safeMessage); } } diff --git a/src/payments/walletRPCService.js b/src/payments/walletRPCService.js index 7a4a695..b4c6bbe 100644 --- a/src/payments/walletRPCService.js +++ b/src/payments/walletRPCService.js @@ -1,5 +1,6 @@ const axios = require('axios'); const { v4: uuidv4 } = require('uuid'); +const { ExternalServiceError, AppError } = require('../utils/errors'); class WalletRPCService { constructor(debugManager) { @@ -53,15 +54,26 @@ class WalletRPCService { }); if (response.data.error) { - throw new Error(response.data.error.message); + throw new ExternalServiceError(`Wallet RPC responded with error for method ${method}`, { + safeMessage: 'Wallet RPC call failed.', + details: response.data.error, + cause: response.data.error + }); } return response.data; } catch (error) { + const wrapped = error instanceof ExternalServiceError + ? error + : new ExternalServiceError(`Wallet RPC call failed (${method})`, { + safeMessage: 'Wallet RPC call failed.', + cause: error + }); + if (this.debugManager?.CONSOLE_LOGGING) { - console.error(`โŒ Wallet RPC call failed (${method}):`, error.message); + console.error(`โŒ Wallet RPC call failed (${method}):`, wrapped.message); } - throw error; + throw wrapped; } } @@ -112,8 +124,12 @@ class WalletRPCService { expiresAt: new Date(Date.now() + 30 * 60 * 1000) // 30 minutes }; } catch (error) { - console.error('โŒ Failed to create payment address:', error); - return { success: false, error: error.message }; + const wrapped = error instanceof AppError ? error : new ExternalServiceError('Failed to create payment address', { + safeMessage: 'Unable to create a payment address at this time.', + cause: error + }); + console.error('โŒ Failed to create payment address:', wrapped.message); + throw wrapped; } } @@ -294,11 +310,12 @@ class WalletRPCService { fee: response.result.fee }; } catch (error) { - console.error('โŒ Failed to process payout:', error); - return { - success: false, - error: error.message - }; + const wrapped = error instanceof AppError ? error : new ExternalServiceError('Failed to process payout', { + safeMessage: 'Unable to send payout at this time.', + cause: error + }); + console.error('โŒ Failed to process payout:', wrapped.message); + throw wrapped; } } diff --git a/src/utils/errors.js b/src/utils/errors.js new file mode 100644 index 0000000..119d8fc --- /dev/null +++ b/src/utils/errors.js @@ -0,0 +1,82 @@ +class AppError extends Error { + constructor(message, { + statusCode = 500, + code = 'INTERNAL_ERROR', + safeMessage = 'An unexpected error occurred.', + isOperational = true, + details = null, + cause = undefined + } = {}) { + super(message); + this.name = this.constructor.name; + this.statusCode = statusCode; + this.code = code; + this.safeMessage = safeMessage || 'An unexpected error occurred.'; + this.isOperational = isOperational; + this.details = details; + if (cause) { + this.cause = cause; + } + Error.captureStackTrace?.(this, this.constructor); + } +} + +class ValidationError extends AppError { + constructor(message, options = {}) { + super(message, { + statusCode: 400, + code: 'VALIDATION_ERROR', + safeMessage: message, + ...options + }); + } +} + +class NotFoundError extends AppError { + constructor(message, options = {}) { + super(message, { + statusCode: 404, + code: 'NOT_FOUND', + safeMessage: options.safeMessage || message, + ...options + }); + } +} + +class ExternalServiceError extends AppError { + constructor(message, options = {}) { + super(message, { + statusCode: options.statusCode || 502, + code: options.code || 'EXTERNAL_SERVICE_ERROR', + safeMessage: options.safeMessage || 'Upstream service error. Please try again later.', + ...options + }); + } +} + +const isTrustedError = (error) => { + return error instanceof AppError && error.isOperational !== false; +}; + +const normalizeError = (error, fallbackMessage = 'An unexpected error occurred.') => { + if (!error) { + return new AppError(fallbackMessage); + } + if (error instanceof AppError) { + return error; + } + return new AppError(error.message || fallbackMessage, { + safeMessage: fallbackMessage, + cause: error, + isOperational: false + }); +}; + +module.exports = { + AppError, + ValidationError, + NotFoundError, + ExternalServiceError, + isTrustedError, + normalizeError +}; diff --git a/test/paymentHandlers.test.js b/test/paymentHandlers.test.js index 0a1656f..96dcd69 100644 --- a/test/paymentHandlers.test.js +++ b/test/paymentHandlers.test.js @@ -1,30 +1,52 @@ const PaymentHandlers = require('../src/network/paymentHandlers'); function makeMocks() { + const user = { id: 'user-1', clientId: 'client1', payout_address: 'wow1address' }; return { io: { to: () => ({ emit: () => {} }) }, gameModeManager: { gameMode: 'PAID_SINGLE', cryptoType: 'WOW', + currencyDecimals: 11, singleGamePrice: 1000, - createPaymentRequest: async () => ({ id: 'pid123', address: 'addr', amount: 1000, expiresAt: new Date() }) + creditsPayoutEnabled: false, + formatAtomicHuman: jest.fn(() => '0.001'), + getOrCreateUser: jest.fn(async () => user), + createPaymentRequest: async () => ({ + id: 'pid123', + address: 'addr', + amount: 1000, + amountFormatted: '0.001', + expiresAt: new Date() + }) }, walletService: { - startPaymentMonitoring: (addr, cb) => { /* simulate nothing */ }, - stopPaymentMonitoring: () => {} + startPaymentMonitoring: jest.fn(), + stopPaymentMonitoring: jest.fn() }, debugManager: { getCurrentBlockHeight: () => 123, CONSOLE_LOGGING: false }, queueManager: { - getUserBySocket: () => ({ clientId: 'client1' }), + getUserBySocket: () => user, getPlayerIndex: () => -1, - addPlayer: () => {}, - markConfirmed: () => {} + getQueueLength: () => 0, + addPlayer: jest.fn(), + markConfirmed: jest.fn(), + startGameImmediately: jest.fn(() => false) }, - broadcastManager: { sendStatusUpdate: () => {} } + broadcastManager: { sendStatusUpdate: jest.fn() } }; } describe('PaymentHandlers basic', () => { + let ph; + + afterEach(() => { + if (ph && typeof ph.dispose === 'function') { + ph.dispose(); + } + ph = null; + }); + test('createAndShowPaymentRequest emits payment_created', async () => { const mocks = makeMocks(); const emitted = {}; @@ -33,10 +55,6 @@ describe('PaymentHandlers basic', () => { await ph.createAndShowPaymentRequest({ id: 'sock1' }); expect(emitted.payment_created).toBeDefined(); expect(emitted.payment_created.paymentId).toBe('pid123'); - }); - afterAll(() => { - if (ph && typeof ph.dispose === 'function') { - ph.dispose(); - } + expect(mocks.walletService.startPaymentMonitoring).toHaveBeenCalled(); }); }); diff --git a/test/payment_flow.integration.test.js b/test/payment_flow.integration.test.js index b0c4965..a6079e4 100644 --- a/test/payment_flow.integration.test.js +++ b/test/payment_flow.integration.test.js @@ -36,7 +36,7 @@ describe('Payment flow integration: confirm-after-block immediate start', () => }; // User lookup - const userObj = { id: socketId, clientId: 'client-int' }; + const userObj = { id: socketId, clientId: 'client-int', payout_address: 'wow1address' }; const getUserBySocket = (id) => id === socketId ? userObj : null; // createGameForUser mock (records active game & returns state) @@ -52,8 +52,18 @@ describe('Payment flow integration: confirm-after-block immediate start', () => const gameModeManager = { gameMode: 'PAID_SINGLE', cryptoType: 'WOW', + currencyDecimals: 11, singleGamePrice: 1000, - createPaymentRequest: async () => ({ id: 'pay-1', address: 'addr-1', amount: 1000, expiresAt: new Date(Date.now()+60000) }) + creditsPayoutEnabled: false, + formatAtomicHuman: () => '0.001', + getOrCreateUser: async () => ({ id: socketId, payout_address: 'wow1address', clientId: 'client-int' }), + createPaymentRequest: async () => ({ + id: 'pay-1', + address: 'addr-1', + amount: 1000, + amountFormatted: '0.001', + expiresAt: new Date(Date.now()+60000) + }) }; // Wallet service mock with manual callback control diff --git a/test/security.test.js b/test/security.test.js index d2b1705..16a54b1 100644 --- a/test/security.test.js +++ b/test/security.test.js @@ -138,7 +138,8 @@ describe('SQL Injection Prevention', () => { // Verify only safe fields were updated const updatedUser = await sessionManager.getBySocket('test-update-socket'); expect(updatedUser.credits).toBe(50); - expect(updatedUser.id).toBe(testSession.user.id); // Should not have changed + expect(updatedUser.id).not.toBe(dangerousUpdates.id); // Should ignore malicious id change + expect(updatedUser.socket_id).toBe('test-update-socket'); }); test('should generate cryptographically secure tokens', () => { diff --git a/test/walletRPCService.test.js b/test/walletRPCService.test.js index 7f5e562..62dc9f3 100644 --- a/test/walletRPCService.test.js +++ b/test/walletRPCService.test.js @@ -4,6 +4,7 @@ const WalletRPCService = require('../src/payments/walletRPCService'); const debugManagerMock = { CONSOLE_LOGGING: false }; describe('WalletRPCService interface', () => { + test('exposes expected methods', () => { const svc = new WalletRPCService(debugManagerMock); expect(typeof svc.initialize).toBe('function'); @@ -20,4 +21,26 @@ describe('WalletRPCService interface', () => { const res = await svc.processBatchPayouts(); expect(res).toEqual({ processed: 0 }); }); + + test('rpcCall wraps failures in ExternalServiceError', async () => { + const svc = new WalletRPCService(debugManagerMock); + svc.walletEndpoint = 'http://127.0.0.1:65535'; + + await expect(svc.rpcCall('get_height')).rejects.toMatchObject({ + code: 'EXTERNAL_SERVICE_ERROR', + safeMessage: 'Wallet RPC call failed.' + }); + }); + + test('createPaymentRequest rethrows wrapped errors', async () => { + const svc = new WalletRPCService(debugManagerMock); + svc.rpcCall = jest.fn().mockRejectedValue(new Error('rpc unavailable')); + + await expect( + svc.createPaymentRequest(10, 'test payment', 'user-1', 'socket-1') + ).rejects.toMatchObject({ + code: 'EXTERNAL_SERVICE_ERROR', + safeMessage: 'Unable to create a payment address at this time.' + }); + }); });