diff --git a/README.md b/README.md index 9eda2e0..d62de0f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow - Security warnings about clipboard viruses - Two-step confirmation process for safety -### 💳 **MoneroPay Integration** +### 💳 **Wallet-RPC Integration** - Subaddress generation for payments - Real-time payment monitoring - Automated payment confirmations @@ -48,14 +48,14 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow ### **Backend Payment Infrastructure** - **Database Manager**: PostgreSQL connection pooling, migrations, health checks -- **MoneroPay Service**: Payment requests, status monitoring, batch payout processing +- **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 - **Games Table**: Game records with outcomes, payments, and rewards -- **Payments Table**: MoneroPay payment tracking with status updates +- **Payments Table**: Direct wallet payment tracking with status updates - **Payouts Table**: Batch payout management with transaction records ### **Address Detection System** @@ -69,7 +69,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow ### 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` - MoneroPay webhook for payment confirmations +- `POST /api/payment/callback` - Wallet RPC webhook for payment confirmations ### User Management - `GET /api/user/credits/:userId` - Check user's remaining credits @@ -85,7 +85,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow ### Prerequisites - Node.js 16+ with npm - PostgreSQL 12+ database -- MoneroPay service access (optional, will fallback to FREE mode) +- Wallet-RPC service access (optional, will fallback to FREE mode) ### Installation Steps @@ -103,7 +103,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow 3. **Environment Configuration**: ```bash cp .env.example .env - # Edit .env with your database and MoneroPay credentials + # Edit .env with your database and Wallet-RPC credentials ``` 4. **Database Setup**: @@ -136,9 +136,10 @@ PAID_CREDITS_PRICE=0.05 # Database Configuration DATABASE_URL=postgresql://username:password@localhost:5432/wowngeon -# MoneroPay Configuration -MONEROPAY_URL=https://api.moneropay.eu -MONEROPAY_API_KEY=your_api_key +# Wallet RPC Configuration +PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570 +WALLET_RPC_USER=your_rpc_user +WALLET_RPC_PASSWORD=your_rpc_password # Crypto RPC Configuration PRIMARY_RPC_URL=http://localhost:18081 @@ -149,7 +150,7 @@ FALLBACK_RPC_URL=http://backup-node:18081 ### PAID_SINGLE Mode 1. User requests to play paid game -2. System generates MoneroPay payment request +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 @@ -195,14 +196,14 @@ Test suite covers: ### Security Considerations - Database credentials should use environment variables -- MoneroPay API keys must be kept secure +- Wallet-RPC credentials must be kept secure - RPC endpoints should be trusted nodes only - Consider rate limiting on payment endpoints ### Monitoring & Logging - Payment processing events logged for audit trails - Database health checks with automatic failover -- MoneroPay webhook validation and processing +- Wallet-RPC webhook validation and processing - Graceful degradation logs when payment systems unavailable ### Scalability Features @@ -224,7 +225,7 @@ wowngeon/ │ ├── db/ # Database management │ │ └── databaseManager.js # Connection pooling & migrations │ ├── payments/ # Payment system -│ │ └── moneroPayService.js # MoneroPay integration +│ │ └── walletRPCService.js # Wallet-RPC integration │ ├── rpc/ # Blockchain integration │ │ └── rpcService.js # RPC with failover │ ├── game/ # Game logic @@ -243,7 +244,7 @@ wowngeon/ - Complete payment system infrastructure - Three-tier game mode implementation - PostgreSQL database with migration system -- MoneroPay integration with webhook processing +- Direct wallet-RPC integration with payment monitoring - RPC failover system for blockchain reliability - XMR/WOW address detection via chat regex - Comprehensive test suite with 100% pass rate @@ -251,7 +252,7 @@ wowngeon/ ### 🔄 Next Steps - Database connection testing with production credentials -- MoneroPay integration testing with live service +- Wallet-RPC integration testing with local wallet - End-to-end payment flow validation - Frontend payment UI testing and refinement diff --git a/html/js/core/game.js b/html/js/core/game.js index 927c13e..7d9ca02 100644 --- a/html/js/core/game.js +++ b/html/js/core/game.js @@ -14,6 +14,8 @@ var Game = { _inputEnabled: true, _messageLog: [], _maxLogMessages: 5, + _unconfirmedPayment: false, + _unconfirmedPaymentInfo: null, init: function() { // Update screen dimensions from options @@ -147,6 +149,89 @@ var Game = { } }, + // Legacy alias (some handlers referenced updateGame previously) + updateGame: function(data) { + return this.updateGameState(data); + }, + + // Handle game over event from server + endGame: function(data) { + try { + GameState.setGameActive(false); + // Decide which screen to draw + if (data && data.reason === 'monster') { + if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('monster'); + } else if (data && data.reason === 'escaped') { + if (ScreenManager.drawWinScreen) ScreenManager.drawWinScreen(data.treasure || false); + } else if (data && data.reason === 'timeout') { + if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('timeout'); + } else { + if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('other'); + } + + // After short delay, allow user to return to title with Enter/start + const score = data && typeof data.score === 'number' ? data.score : 0; + const treasure = !!(data && data.treasure); + + // Display score / treasure info overlay + if (ScreenManager && ScreenManager.drawCenteredText && DisplayManager.ensureDisplay()) { + const baseY = Math.floor(ScreenManager._screenHeight / 2) + 5; + ScreenManager.drawCenteredText(baseY, `Score: ${score}`); + if (treasure) { + ScreenManager.drawCenteredText(baseY + 1, 'You secured the treasure!'); + } + } + + // Debounce + delayed activation of restart + this._awaitingRestart = false; + const activateDelay = 800; // ms before we accept Enter + const autoReturnDelay = 10000; // auto return after 10s + + // Show hint after activation delay + setTimeout(() => { + this._awaitingRestart = true; + if (ScreenManager && ScreenManager.drawCenteredText && DisplayManager.ensureDisplay()) { + const y = ScreenManager._screenHeight - 2; + const fullHint = 'Press Enter to return to title'; + const shortHint = 'Press Enter'; + const maxLen = ScreenManager._screenWidth - 2; // leave small margin + const hint = fullHint.length > maxLen ? shortHint : fullHint; + ScreenManager.drawCenteredText(y, hint); + } + }, activateDelay); + + // Auto return timer + clearTimeout(this._autoReturnTimer); + this._autoReturnTimer = setTimeout(() => { + if (this._awaitingRestart) { + this._awaitingRestart = false; + if (ScreenManager && ScreenManager.drawWelcomeScreen) { + ScreenManager.drawWelcomeScreen(); + } + } + }, autoReturnDelay); + + // One-time listener setup + if (!this._restartListenerAttached) { + this._restartListenerAttached = true; + document.addEventListener('keydown', (e) => { + if (!this._awaitingRestart) return; + if (e.key === 'Enter') { + this._awaitingRestart = false; + clearTimeout(this._autoReturnTimer); + if (ScreenManager && ScreenManager.drawWelcomeScreen) { + ScreenManager.drawWelcomeScreen(); + } + const chatInput = document.getElementById('chatInput'); + if (chatInput) chatInput.focus(); + } + }); + } + } catch (err) { + console.error('Error handling endGame:', err); + } + }, + clearDisplay: function() { DisplayManager.clearDisplay(); }, @@ -173,6 +258,19 @@ var Game = { drawWaitingScreen: function() { ScreenManager.drawWaitingScreen(); + // Overlay unconfirmed payment badge if present + if (this._unconfirmedPayment && typeof ScreenManager !== 'undefined' && DisplayManager.ensureDisplay()) { + const display = DisplayManager.getDisplay(); + const badgeText = 'Unconfirmed (mempool)'; + const xStart = Math.max(1, Math.floor((this._screenWidth - badgeText.length) / 2)); + const y = 1; // top line inside border + for (let i = 0; i < badgeText.length; i++) { + display.draw(xStart + i, y, badgeText[i], 'rgba(180,230,255,0.9)', 'transparent'); + } + // Small pulsing dot + const pulse = 0.4 + Math.sin(Date.now()/300)*0.4; + display.draw(xStart - 2, y, '●', `rgba(120,200,255,${pulse})`, 'transparent'); + } }, stopWaitingScreen: function() { @@ -239,6 +337,17 @@ var Game = { } }; +// Payment state helpers +Game._pendingPaymentDetected = function(info) { + this._unconfirmedPayment = true; + this._unconfirmedPaymentInfo = info || {}; +}; + +Game._pendingPaymentConfirmed = function() { + this._unconfirmedPayment = false; + this._unconfirmedPaymentInfo = null; +}; + // Debug utility functions (keep as-is for compatibility) const GameDebug = { log: function(message) { diff --git a/html/js/display/waitingScreenAnimator.js b/html/js/display/waitingScreenAnimator.js index 8eabba8..2df2e89 100644 --- a/html/js/display/waitingScreenAnimator.js +++ b/html/js/display/waitingScreenAnimator.js @@ -210,13 +210,40 @@ var WaitingScreenAnimator = { const bg = ["transparent", "transparent"]; display.draw(anim.playerX, anim.playerY, ch, fg, bg); - // Light footsteps behind player - if (anim.phase === 'enter' || anim.phase === 'escape') { - const footstepX = anim.playerX - 1; - if (footstepX >= roomStartX && footstepX <= roomEndX) { - const footstepGlow = Math.max(0, Math.sin(time / 400) * 0.2); - const floorTile = GameTiles.getFloorTile(); - display.draw(footstepX, anim.playerY, floorTile, `rgba(150, 200, 255, ${footstepGlow})`, "transparent"); + // Bright ethereal footsteps / halo around player to indicate "primed" status + if (anim.phase === 'enter' || anim.phase === 'escape' || anim.phase === 'chase') { + const ringPulse = 0.35 + Math.sin(time / 250) * 0.25; // stronger, faster pulse + const innerPulse = 0.15 + Math.sin(time / 180 + 1.2) * 0.15; + const ringColor = (alpha) => `rgba(180, 230, 255, ${alpha})`; + const floorTile = GameTiles.getFloorTile(); + + // 8-way surrounding ring + const ringOffsets = [ + {dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}, // cardinal + {dx: -1, dy: -1}, {dx: 1, dy: -1}, {dx: -1, dy: 1}, {dx: 1, dy: 1} // diagonal + ]; + for (let i = 0; i < ringOffsets.length; i++) { + const rx = anim.playerX + ringOffsets[i].dx; + const ry = anim.playerY + ringOffsets[i].dy; + if (rx >= roomStartX && rx <= roomEndX && ry >= roomStartY && ry <= roomEndY) { + // subtle variation per tile + const phaseShift = (i / ringOffsets.length) * Math.PI * 2; + const alpha = (ringPulse * 0.6) + Math.sin(time / 300 + phaseShift) * 0.15; + display.draw(rx, ry, floorTile, ringColor(Math.max(0, alpha)), 'transparent'); + } + } + + // Trailing footprint path (a few tiles behind) + for (let trail = 1; trail <= 3; trail++) { + const tx = anim.playerX - trail; + const ty = anim.playerY; + if (tx >= roomStartX && tx <= roomEndX && ty >= roomStartY && ty <= roomEndY) { + const decay = Math.max(0, 0.5 - trail * 0.15); + const alpha = innerPulse * decay; + if (alpha > 0.03) { + display.draw(tx, ty, floorTile, `rgba(120, 200, 255, ${alpha})`, 'transparent'); + } + } } } } diff --git a/html/js/input/inputHandler.js b/html/js/input/inputHandler.js index e2b8ecf..4a95711 100644 --- a/html/js/input/inputHandler.js +++ b/html/js/input/inputHandler.js @@ -33,19 +33,13 @@ const InputHandler = { if (msg.toLowerCase() === 'enter') { socket.emit('chat message', msg); - // Show appropriate screen based on current state - const isDebugMode = window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' || - window.location.protocol === 'file:'; + // Don't automatically switch to waiting screen - let the server control screen state + // The server will send appropriate events (payment_created, waiting_status, etc.) + // to control what the user sees - if (isDebugMode || ScreenManager.canEnterGame()) { - if (typeof ScreenManager !== 'undefined' && ScreenManager.drawWaitingScreen) { - ScreenManager.drawWaitingScreen(); - } - } else { - $('#messages').append($('
  • ').text("* You have been added to the queue! Game will start after next block.")); - UI.scrollChat(); - } + // Add visual feedback that request was sent + $('#messages').append($('
  • ').text("🔑 Processing game entry request...")); + UI.scrollChat(); } else { // Send other chat messages normally socket.emit('chat message', msg); diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js index f24fedf..4d8bd14 100644 --- a/html/js/network/socketHandlers.js +++ b/html/js/network/socketHandlers.js @@ -2,7 +2,7 @@ * Socket event handlers for the Wowngeon game */ const SocketHandlers = { - _initialized: false, // Flag to prevent multiple initializations + _initialized: false, init: function() { if (this._initialized) { @@ -28,6 +28,7 @@ const SocketHandlers = { // Game state handlers socket.on('waiting_status', this.onWaitingStatus); + socket.on('queue_joined', this.onQueueJoined); socket.on('game_start', this.onGameStart); socket.on('game_update', this.onGameUpdate); socket.on('game_over', this.onGameOver); @@ -36,20 +37,19 @@ const SocketHandlers = { // Payment/Address handlers socket.on('address_detected', this.onAddressDetected); socket.on('address_confirmed', this.onAddressConfirmed); + socket.on('payment_created', this.onPaymentCreated); + socket.on('payment_confirmed', this.onPaymentConfirmed); + socket.on('payment_detected', this.onPaymentDetected); - // Block height handler (broadcast to all clients) + // Block height handler socket.on('blockheight', this.onBlockHeight); }, onConnect: function() { - - // Tell server our client ID socket.emit('register_client', { clientId: socket.id, userAgent: navigator.userAgent }); - - // Auto-start removed - user must manually start game }, onWelcome: function(msg) { @@ -63,30 +63,31 @@ const SocketHandlers = { }, onStatusUpdate: function(data) { - // Handle player-specific status updates - - // Display status message with appropriate styling const statusClass = data.type === 'error' ? 'error' : 'status'; - $('#messages').append($(`
  • `).text(data.message)); + const statusColor = data.type === 'error' ? '#f00' : + data.type === 'warning' ? '#ff0' : + data.type === 'success' ? '#0f0' : + data.type === 'payment' ? '#0af' : '#fff'; + + $('#messages').append($('
  • ').text(data.message)); UI.scrollChat(); }, onChatBroadcast: function(data) { - // Handle chat messages broadcast to all players - - const timestamp = new Date(data.timestamp || Date.now()).toLocaleTimeString(); - const chatMsg = `[${timestamp}] ${data.username || 'Anonymous'}: ${data.message}`; - - $('#messages').append($('
  • ').text(chatMsg)); + const msgElement = $('
  • '); + if (data.socketId) { + msgElement.html('' + data.socketId.substring(0, 6) + ': ' + data.message); + } else { + msgElement.text(data.message); + } + $('#messages').append(msgElement); UI.scrollChat(); }, onWaitingStatus: function(data) { - if (data.status === 'waiting') { $('#messages').append($('
  • ').text(data.message)); - // Show waiting screen if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { Game.drawWaitingScreen(); } @@ -94,8 +95,21 @@ const SocketHandlers = { UI.scrollChat(); }, - onGameStart: function(data) { + onQueueJoined: function(data) { + console.log('Queue joined:', data); + $('#messages').append($('
  • ').html( + '�� Queue Joined! Position: ' + data.position + '
    ' + + '📦 Current block: ' + data.currentBlock + ', Next: ' + data.nextBlock + )); + UI.scrollChat(); + + if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { + Game.drawWaitingScreen(); + } + }, + + onGameStart: function(data) { $('#messages').append($('
  • ').text("Starting game...")); if (typeof Game !== 'undefined' && Game.stopWaitingScreen) { @@ -109,173 +123,125 @@ const SocketHandlers = { } try { - // Pass all game data including lighting and torches var success = Game.startGame(data.player, data.map, data.monster, data.items, data.visibleTiles, data.lighting, data.torches); if (!success) { $('#messages').append($('
  • ').text("Game start failed. Check console for details.")); if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); } else { - // Shift focus to the game display area after successful game start - $('#game-display').focus(); - UI.updateFocusIndicator(); + setTimeout(function() { + $('#game-display').focus(); + }, 100); } - - } catch (err) { - console.error("Error starting game:", err); - $('#messages').append($('
  • ').text("Error: " + err.message)); + } catch (error) { + console.error("Error starting game:", error); + $('#messages').append($('
  • ').text("Game start error: " + error.message)); + if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen(); } + + UI.scrollChat(); }, onGameUpdate: function(data) { - - if (data.visibleTiles) { - if (window.GameDebug) window.GameDebug.updateDebugDisplay("SOCKET: Received game update with visibleTiles"); - } - - if (typeof Game !== 'undefined' && Game._gameActive) { - Game.updateGameState(data); + // Server pushed a game state update. The Game object exposes updateGameState(). + // Older code referenced Game.updateGame, which doesn't exist in the current refactor, + // so updates were silently ignored (player appeared frozen). + if (typeof Game !== 'undefined') { + if (Game.updateGameState) { + Game.updateGameState(data); + } else if (Game.updateGame) { // Fallback if an alias gets added later + Game.updateGame(data); + } else { + console.warn('Game update received but no updateGameState()/updateGame() method found on Game.'); + } } + UI.scrollChat(); }, onGameOver: function(data) { - - // Update game state based on outcome + $('#messages').append($('
  • ').text("Game Over: " + data.message)); if (typeof Game !== 'undefined') { - if (data.status === 'won') { - // Show win message - $('#messages').append($('
  • ').text( - data.hasTreasure ? - "YOU ESCAPED WITH THE TREASURE! YOU WON!" : - "You escaped the dungeon alive!" - )); - - // Draw win screen - Game.drawWinScreen(data.hasTreasure); - } else { - // Show loss message - const reason = data.reason || "unknown"; - let message = "You died in the dungeon!"; - - if (reason === 'monster') { - message = "You were killed by the monster!"; - } else if (reason === 'timeout') { - message = "You didn't escape before the next block was found!"; - } - - $('#messages').append($('
  • ').text(message)); - - // Draw lose screen - if (Game && Game.drawLoseScreen) { - Game.drawLoseScreen(reason); - } + if (Game.endGame) { + Game.endGame(data); + } else if (Game.drawLoseScreen && data && data.reason) { + // Minimal fallback if endGame not present + if (data.reason === 'monster') Game.drawLoseScreen('monster'); } - - // Set game as inactive to prevent further movement - if (Game) { - Game._gameActive = false; - } - - // Auto-return to title screen after 30 seconds - setTimeout(() => { - $('#messages').append($('
  • ').text("Returning to title screen...")); - UI.scrollChat(); - - // Return to welcome screen - if (Game && Game._drawWelcomeScreen) { - Game._drawWelcomeScreen(); - } - - // Reset game state - if (typeof GameState !== 'undefined') { - GameState.reset(); - } - - // Focus chat for new commands - $('#chatInput').focus(); - UI.updateFocusIndicator(); - }, 30000); // 30 seconds } UI.scrollChat(); }, - onQueueCancelled: function() { + onQueueCancelled: function(data) { + $('#messages').append($('
  • ').text("Queue entry cancelled.")); if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) { Game._drawWelcomeScreen(); - } else { - console.error("Game or _drawWelcomeScreen not available"); } - }, - - onBlockHeight: function(height) { - UI.updateBlockHeight(height); - $('#statusValue').text('Connected'); - $('#statusValue').css('color', '#0f0'); + + UI.scrollChat(); }, onAddressDetected: function(data) { - // Handle address detection confirmation request console.log('Address detected:', data); + $('#messages').append($('
  • ').text(data.message)); + UI.scrollChat(); + }, + + onAddressConfirmed: function(data) { + console.log('Address confirmed:', data); + $('#messages').append($('
  • ').text(data.message)); + UI.scrollChat(); + }, + + onPaymentCreated: function(data) { + console.log('Payment created:', data); - // Display the confirmation message with styling - const confirmationHtml = ` -
    - 🔍 ADDRESS DETECTED
    - Type: ${data.type}
    - Address: ${data.address}

    - ⚠️ WARNING: Verify this is YOUR address!
    - ⚠️ Clipboard viruses can change addresses!

    - Type "confirm" to set as payout address or "cancel" to reject. -
    - `; - - $('#messages').append($(confirmationHtml)); + $('#messages').append($('
  • ').html( + '💳 Payment Required: ' + data.humanAmount + ' ' + data.cryptoType + )); UI.scrollChat(); - // Focus chat input for easy confirmation - setTimeout(() => { + setTimeout(function() { $('#chatInput').focus(); }, 100); }, - onAddressConfirmed: function(data) { - // Handle successful address confirmation - console.log('Address confirmed:', data); + onPaymentConfirmed: function(data) { + console.log('Payment confirmed:', data); - const confirmationHtml = ` -
    - ✅ PAYOUT ADDRESS CONFIRMED
    - Type: ${data.type}
    - Address: ${data.address}

    - Future winnings will be sent to this address. -
    - `; + if (typeof Game !== 'undefined' && Game.drawWaitingScreen) { + Game.drawWaitingScreen(); + } + if (typeof Game !== 'undefined') { + Game._pendingPaymentConfirmed(); + } - $('#messages').append($(confirmationHtml)); + $('#messages').append($('
  • ').html( + '✅ Payment confirmed! You are in the game queue.' + )); UI.scrollChat(); + }, + + onPaymentDetected: function(data) { + console.log('Payment detected (mempool):', data); + if (typeof Game !== 'undefined') { + Game._pendingPaymentDetected(data); + if (Game.drawWaitingScreen) Game.drawWaitingScreen(); + } + $('#messages').append($('
  • ').html( + '🌀 Payment seen in mempool – awaiting block confirmation...' + )); + UI.scrollChat(); + }, + + onBlockHeight: function(data) { + if (typeof UI !== 'undefined' && UI.updateBlockHeight) { + UI.updateBlockHeight(data.blockHeight); + } } }; -// Ensure SocketHandlers is available globally -if (typeof window !== 'undefined') { - window.SocketHandlers = SocketHandlers; +if (typeof module !== 'undefined' && module.exports) { + module.exports = SocketHandlers; } - -// Note: SocketHandlers.init() is called from index.html after DOM ready -// to ensure proper initialization order with other modules diff --git a/index.js b/index.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/.env.example b/src/.env.example index fd275dc..aa42d09 100644 --- a/src/.env.example +++ b/src/.env.example @@ -4,7 +4,7 @@ # PAID_SINGLE: Users pay per game, get 2x for escape, 3x with treasure # PAID_CREDITS: Users buy 10 credits, play 10 times, no payouts # FREE: Free play mode, no payments required -GAME_MODE=FREE +GAME_MODE=PAID_SINGLE # Cryptocurrency Configuration CRYPTO_TYPE=XMR @@ -14,9 +14,10 @@ CRYPTO_TYPE=XMR SINGLE_GAME_PRICE=5000000000 CREDITS_PACKAGE_PRICE=50000000000 -# MoneroPay Configuration -MONEROPAY_ENDPOINT=http://jw-nodes:5000 -MONEROPAY_CALLBACK_BASE_URL=http://localhost:3000/api/payments/callback +# Wallet RPC Configuration (replaces MoneroPay) +PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570 +# WALLET_RPC_USER=user +# WALLET_RPC_PASSWORD=password # Blockchain RPC Configuration PRIMARY_RPC_ENDPOINT=http://jw-nodes:18081 diff --git a/src/debug/debugManager.js b/src/debug/debugManager.js index a60c1d1..cba6438 100644 --- a/src/debug/debugManager.js +++ b/src/debug/debugManager.js @@ -103,36 +103,55 @@ class DebugManager { console.log("🚀 PRODUCTION MODE ENABLED - Using real blockchain RPC calls"); } - // Real blockchain monitoring - this.debugInterval = setInterval(() => { - rpc.daemonCall("get_block_count", "", (result) => { - if (!result || !result.result || !result.result.count) { + // Initialize RPC service if available + const RpcService = require('../rpc/rpcService'); + this.rpcService = new RpcService(); + + // Real blockchain monitoring with new RPC service + this.debugInterval = setInterval(async () => { + try { + const currentHeight = await this.rpcService.getBlockHeight(); + + if (!currentHeight) { if (this.CONSOLE_LOGGING) { console.log("❌ Failed to get block count from daemon"); } return; } - const currentHeight = result.result.count; - rpc.lastBlock.setHeight(currentHeight); + // Always broadcast current height to keep clients updated + this.broadcastManager.broadcastBlockHeight(currentHeight); // If new block found if (currentHeight > this.lastProductionBlockHeight) { if (this.CONSOLE_LOGGING) { console.log(`⛏️ New block found: ${currentHeight}`); } - this.broadcastManager.broadcastBlockHeight(currentHeight); // Notify listeners about new block this.onNewBlock(currentHeight); this.lastProductionBlockHeight = currentHeight; - } else { - // Still broadcast current height for status updates - this.broadcastManager.broadcastBlockHeight(currentHeight); } - }); - }, 5000); // Every 5 seconds + } catch (error) { + if (this.CONSOLE_LOGGING) { + console.error("❌ RPC Error:", error.message); + } + + // Fallback to legacy RPC if new service fails + rpc.daemonCall("get_block_count", "", (result) => { + if (result && result.result && result.result.count) { + const currentHeight = result.result.count; + this.broadcastManager.broadcastBlockHeight(currentHeight); + + if (currentHeight > this.lastProductionBlockHeight) { + this.onNewBlock(currentHeight); + this.lastProductionBlockHeight = currentHeight; + } + } + }); + } + }, 2000); // Every 2 seconds as specified in .env } // ====== BLOCK HEIGHT MANAGEMENT ====== diff --git a/src/game/gameModeManager.js b/src/game/gameModeManager.js index 7ef74e2..2b53d84 100644 --- a/src/game/gameModeManager.js +++ b/src/game/gameModeManager.js @@ -4,9 +4,10 @@ */ class GameModeManager { - constructor(databaseManager, moneroPayService) { + constructor(databaseManager, walletRPCService, debugManager) { this.db = databaseManager; - this.moneroPayService = moneroPayService; + this.walletService = walletRPCService; // Changed from moneroPayService + this.debugManager = debugManager; this.gameMode = process.env.GAME_MODE || 'FREE'; this.cryptoType = process.env.CRYPTO_TYPE || 'XMR'; this.singleGamePrice = parseInt(process.env.SINGLE_GAME_PRICE) || 5000000000; // 0.005 XMR @@ -184,7 +185,7 @@ class GameModeManager { } // Create payout record - await this.moneroPayService.createPayout( + await this.walletService.processPayout( game.user_id, gameId, game.payout_address, @@ -229,25 +230,44 @@ class GameModeManager { switch (paymentType) { case 'single_game': amount = this.singleGamePrice; - description = `Wowgue single game entry (${this.cryptoType})`; + description = `Wowngeon single game entry (${this.cryptoType})`; break; case 'credits_package': amount = this.creditsPackagePrice; - description = `Wowgue 10 credits package (${this.cryptoType})`; + description = `Wowngeon 10 credits package (${this.cryptoType})`; break; default: throw new Error('Invalid payment type'); } - return await this.moneroPayService.createPaymentRequest( - user.id, - socketId, - paymentType, + // Create payment request using wallet RPC with correct parameters + const paymentResult = await this.walletService.createPaymentRequest( amount, - description + description, + user.id, + 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, subaddress, expected_amount, payment_type, status, created_at) + VALUES ($1, $2, $3, $4, 'pending', NOW()) + `, [user.id, paymentResult.address, amount, paymentType]); + + return { + id: paymentResult.id, + address: paymentResult.address, + amount: amount, + amountFormatted: (amount / 1e12).toFixed(4), // Convert from atomic units + currency: this.cryptoType, + expiresAt: paymentResult.expiresAt + }; } catch (error) { console.error('❌ Error creating payment request:', error.message); @@ -360,6 +380,77 @@ class GameModeManager { } }; } + + /** + * Complete a game (called from socket handlers when game ends) + * @param {string} socketId - Player's socket ID + * @param {string} gameId - Game UUID + * @param {boolean} won - Whether the player won (escaped) + * @param {boolean} treasureFound - Whether treasure was found + * @returns {object} payout / completion info + */ + async completeGame(socketId, gameId, won, treasureFound) { + // For FREE mode we just return basic info + if (this.gameMode === 'FREE') { + return { success: true, mode: 'FREE', payout: null }; + } + + try { + // Update/record game completion in DB (simplified; reuse processGameCompletion if schema matches) + // If a games table exists with at least id & user reference, mark completion. + try { + await this.db.query(` + UPDATE games SET status = $1, treasure_found = $2, completed_at = NOW() + WHERE id = $3 + `, [won ? 'won' : 'lost', treasureFound, gameId]); + } catch (e) { + // Non-fatal if games table differs during early dev. + if (process.env.NODE_ENV === 'development') { + console.warn('Game completion update warning:', e.message); + } + } + + // Handle payouts only in PAID_SINGLE mode and only on win (escaped) + if (this.gameMode === 'PAID_SINGLE' && won) { + // Derive payout multiplier + const multiplier = treasureFound ? 3 : 2; + const payoutAmount = this.singleGamePrice * multiplier; + + // Look up user record for payout address + const userResult = await this.db.query(`SELECT * FROM users WHERE socket_id = $1 LIMIT 1`, [socketId]); + const userRow = userResult.rows[0]; + if (userRow && userRow.payout_address) { + try { + await this.walletService.processPayout( + userRow.id, + gameId, + userRow.payout_address, + payoutAmount + ); + return { + success: true, + mode: this.gameMode, + payout: { + amount: payoutAmount, + multiplier, + treasure: treasureFound + } + }; + } catch (payoutErr) { + console.error('❌ Error creating payout:', payoutErr.message); + return { success: true, mode: this.gameMode, payout: null, payoutError: payoutErr.message }; + } + } + return { success: true, mode: this.gameMode, payout: null, reason: 'No payout address' }; + } + + // 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 }; + } + } } module.exports = GameModeManager; diff --git a/src/index.js b/src/index.js index 818b265..cbe2274 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,7 @@ require('dotenv').config(); // Import payment system components const DatabaseManager = require('./db/databaseManager'); -const MoneroPayService = require('./payments/moneroPayService'); +const WalletRPCService = require('./payments/walletRPCService'); const GameModeManager = require('./game/gameModeManager'); const RpcService = require('./rpc/rpcService'); @@ -19,17 +19,19 @@ const BroadcastManager = require('./network/broadcastManager'); const DebugManager = require('./debug/debugManager'); const SocketHandlers = require('./network/socketHandlers'); -// Initialize payment system components -const databaseManager = new DatabaseManager(); -const rpcService = new RpcService(); -const moneroPayService = new MoneroPayService(databaseManager, rpcService); -const gameModeManager = new GameModeManager(databaseManager, moneroPayService); - -// Initialize modular components +// Initialize modular components first const broadcastManager = new BroadcastManager(io); const debugManager = new DebugManager(broadcastManager); + +// Initialize payment system components (debugManager is now available) +const databaseManager = new DatabaseManager(); +const rpcService = new RpcService(); +const walletRPCService = new WalletRPCService(debugManager); +const gameModeManager = new GameModeManager(databaseManager, walletRPCService, debugManager); + +// Initialize remaining components const activeGames = new Map(); // Maps socketId to Game objects -const socketHandlers = new SocketHandlers(io, activeGames, broadcastManager, debugManager, gameModeManager); +const socketHandlers = new SocketHandlers(io, activeGames, broadcastManager, debugManager, gameModeManager, walletRPCService); // Configure static file serving const htmlPath = path.join(__dirname, '../html'); app.use(express.static(htmlPath)); @@ -56,7 +58,7 @@ app.post('/api/payment/create', async (req, res) => { return res.status(400).json({ error: 'Missing userId or gameMode' }); } - const payment = await moneroPayService.createPaymentRequest(userId, gameMode); + const payment = await walletRPCService.createPaymentRequest(userId, gameMode); res.json(payment); } catch (error) { console.error('Error creating payment:', error); @@ -66,7 +68,7 @@ app.post('/api/payment/create', async (req, res) => { app.post('/api/payment/callback', async (req, res) => { try { - const result = await moneroPayService.processCallback(req.body); + const result = await walletRPCService.processCallback(req.body); res.json({ status: 'success', result }); } catch (error) { console.error('Error processing callback:', error); @@ -76,7 +78,7 @@ app.post('/api/payment/callback', async (req, res) => { app.get('/api/payment/status/:paymentId', async (req, res) => { try { - const status = await moneroPayService.checkPaymentStatus(req.params.paymentId); + const status = await walletRPCService.checkPaymentStatus(req.params.paymentId); res.json(status); } catch (error) { console.error('Error checking payment status:', error); @@ -122,7 +124,12 @@ debugManager.onNewBlockCallback((blockHeight) => { // Initialize payment system async function initializePaymentSystem() { try { - console.log('🔄 Initializing payment system...'); + console.log('🔧 Initializing payment system...'); + const walletInitialized = await walletRPCService.initialize(); + if (!walletInitialized) { + console.log('⚠️ Wallet RPC not available - falling back to FREE mode'); + process.env.GAME_MODE = 'FREE'; + } // Initialize database connection and run migrations await databaseManager.initialize(); @@ -132,10 +139,6 @@ async function initializePaymentSystem() { const rpcHealth = await rpcService.healthCheck(); console.log(`✅ RPC Service: ${rpcHealth.healthy ? 'Connected' : 'Warning - using fallback'}`); - // Initialize MoneroPay service - await moneroPayService.initialize(); - console.log('✅ MoneroPay service initialized'); - console.log('🚀 Payment system ready!'); return true; } catch (error) { @@ -187,7 +190,7 @@ async function startServer() { if (paymentSystemReady) { setInterval(async () => { try { - await moneroPayService.processBatchPayouts(); + await walletRPCService.processBatchPayouts(); } catch (error) { console.error('Error in batch payout processing:', error); } diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js index 498ce47..dc5ca95 100644 --- a/src/network/socketHandlers.js +++ b/src/network/socketHandlers.js @@ -7,12 +7,13 @@ const user = require('../db/user'); const Game = require('../game/game'); class SocketHandlers { - constructor(io, activeGames, broadcastManager, debugManager, gameModeManager = null) { + constructor(io, activeGames, broadcastManager, debugManager, gameModeManager = null, walletService = null) { this.io = io; this.activeGames = activeGames; this.broadcastManager = broadcastManager; this.debugManager = debugManager; this.gameModeManager = gameModeManager; + this.walletService = walletService; this.playerMoveTimestamps = new Map(); this.clientSocketMap = new Map(); this.MOVE_COOLDOWN = 100; // Minimum 100ms between moves @@ -115,6 +116,11 @@ class SocketHandlers { 'Please paste your XMR/WOW address directly in chat. The system will automatically detect and confirm it.'); return; + case 'payment': + case 'pay': + this.handlePaymentCommand(socket); + return; + default: // Broadcast as chat message to all clients this.handleChatBroadcast(socket, msg); @@ -168,9 +174,9 @@ class SocketHandlers { /** * Handle game queue request (typing "enter") */ - handleGameQueue(socket) { + async handleGameQueue(socket) { if (this.debugManager.CONSOLE_LOGGING) { - console.log(`Player ${socket.id} requested to enter the dungeon - ADDING TO QUEUE`); + console.log(`Player ${socket.id} requested to enter the dungeon`); } const currentUser = this.getUserBySocket(socket.id); @@ -192,11 +198,47 @@ class SocketHandlers { return; } - // Add to waiting queue + // Check payment eligibility if payment system is available + if (this.gameModeManager) { + try { + const eligibility = await this.gameModeManager.canUserStartGame(socket.id); + + if (!eligibility.allowed) { + if (this.debugManager.CONSOLE_LOGGING) { + console.log(`❌ Payment required for ${socket.id}: ${eligibility.reason}`); + } + + // Create payment request immediately instead of asking user to type 'payment' + await this.createAndShowPaymentRequest(socket); + return; + } + + if (this.debugManager.CONSOLE_LOGGING) { + console.log(`✅ Payment validated for ${socket.id}: ${eligibility.reason}`); + } + } catch (error) { + console.error('Error checking payment eligibility:', error); + + // In paid mode, don't add to queue if payment check fails + if (this.gameModeManager.gameMode !== 'FREE') { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Payment system error. Please try again or contact support.'); + return; + } + + // Only in FREE mode, continue to queue on payment system errors + this.broadcastManager.sendStatusUpdate(socket.id, 'warning', 'Payment system unavailable. Playing in FREE mode.'); + } + } + + // Add to waiting queue (in free mode or already authorized paid mode) this.WAITING_PLAYERS.push({ serverId: socket.id, clientId: currentUser.clientId, - entryTime: Date.now() + entryTime: Date.now(), + // For paid modes, this path only executes if payment already confirmed (canUserStartGame returned allowed) + requiresConfirmation: false, + confirmed: true }); const currentBlock = this.debugManager.getCurrentBlockHeight(); @@ -218,91 +260,15 @@ class SocketHandlers { console.log(`Player ${socket.id} requested auto-start via start button`); } - const currentUser = this.getUserBySocket(socket.id); - if (!currentUser) { - this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Error: Could not start game. Please try again.'); - return; - } - - // Check if already in a game - if (this.activeGames.has(socket.id)) { - this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'You are already in a game!'); - return; - } - - // Check if payment system is available and validate eligibility - if (this.gameModeManager) { - try { - const eligibility = await this.gameModeManager.canUserStartGame(socket.id); - - if (!eligibility.allowed) { - // Send payment requirement to client - this.io.to(socket.id).emit('payment_required', { - reason: eligibility.reason, - action: eligibility.action, - gameMode: this.gameModeManager.gameMode - }); - return; - } - - if (this.debugManager.CONSOLE_LOGGING) { - console.log(`✅ Payment validated for ${socket.id}: ${eligibility.reason}`); - } - } catch (error) { - console.error('Error checking payment eligibility:', error); - this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Payment system error. Playing in FREE mode.'); - } - } - - // Remove from waiting queue if present - const waitingIndex = this.WAITING_PLAYERS.findIndex(p => p.serverId === socket.id); - if (waitingIndex !== -1) { - this.WAITING_PLAYERS.splice(waitingIndex, 1); - if (this.debugManager.CONSOLE_LOGGING) { - console.log(`🚀 AUTO-START: Removed ${socket.id} from waiting queue for immediate start`); - } - } - - if (this.debugManager.CONSOLE_LOGGING) { - console.log(`Found user for socket ${socket.id}, starting game immediately...`); - } - - try { - // For auto-entry, player enters on current block - const currentBlock = this.debugManager.getCurrentBlockHeight(); - currentUser.blockRec = currentBlock; - if (this.debugManager.CONSOLE_LOGGING) { - console.log(`🕒 AUTO-START: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); - } - - const game = this.createGameForUser(currentUser, 'standard'); - - const gameState = game.getState(); - gameState.blockHeight = currentBlock; - - if (this.debugManager.CONSOLE_LOGGING) { - console.log(`🎮 SENDING IMMEDIATE GAME_START to ${socket.id} (AUTO-START)`); - } - this.io.to(socket.id).emit('game_start', gameState); - - // Register game with payment system if available - if (this.gameModeManager) { - try { - await this.gameModeManager.startGame(socket.id, game.id); - } catch (error) { - console.error('Error registering game with payment system:', error); - } - } - - if (this.debugManager.CONSOLE_LOGGING) { - console.log(`Game started immediately for player ${socket.id} via auto-start`); - } - } catch (error) { - console.error(`Error creating game:`, error); - this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Error starting game: ' + error.message); - } + // Use the same logic as handleGameQueue for consistency + // This ensures both START GAME button and typing 'enter' behave the same way + await this.handleGameQueue(socket); } + /** + * Handle entry cancellation + */ + /** * Handle cancel entry request */ @@ -380,6 +346,23 @@ class SocketHandlers { return; } + // Persist move count if DB available + if (this.gameModeManager && this.gameModeManager.db) { + const db = this.gameModeManager.db; + db.query(` + UPDATE games SET moves_made = moves_made + 1 + WHERE socket_id = $1 AND dungeon_seed = $2 + `, [socket.id, game.id]).catch(err => { + if (this.debugManager.CONSOLE_LOGGING) console.warn('moves_made update failed:', err.message); + }); + } + + // Check collision if player moved onto monster BEFORE monster reacts + if (this.checkMonsterKill(game.player, game.monster)) { + this.handleGameOver(socket, game, 'lost', 'monster', 'The monster caught you!'); + return; + } + // Move monster after player moves game.moveMonster(); @@ -443,8 +426,20 @@ class SocketHandlers { reason: reason, message: message, score: score, - payout: payoutInfo + payout: payoutInfo, + treasure: game.player.hasTreasure || false }); + + // Persist completion details if DB available + if (this.gameModeManager && this.gameModeManager.db) { + const db = this.gameModeManager.db; + const outcome = reason === 'escaped' ? 'escaped' : (reason === 'monster' ? 'caught_by_monster' : reason); + db.query(` + UPDATE games SET status = $1, outcome = $2, treasure_found = $3, completed_at = NOW() + WHERE dungeon_seed = $4 AND socket_id = $5 + `, [status, outcome, game.player.hasTreasure, game.id, socket.id]) + .catch(err => console.error('Game completion update failed:', err.message)); + } this.activeGames.delete(socket.id); } @@ -457,6 +452,9 @@ class SocketHandlers { console.log('User disconnected', socket.client.id); } + // Clean up payment monitoring + this.stopPaymentMonitoring(socket.id); + // Clean up movement timestamps this.playerMoveTimestamps.delete(socket.id); @@ -545,6 +543,19 @@ class SocketHandlers { user.joinGame(game); this.activeGames.set(user.id, game); + + // Insert a DB record for this game (best effort) + if (this.gameModeManager && this.gameModeManager.db) { + const db = this.gameModeManager.db; + const gameMode = this.gameModeManager.gameMode || 'FREE'; + const blockHeight = this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : null; + const socketId = user.id; // user.id is the socket id string + db.query(` + INSERT INTO games (user_id, socket_id, game_mode, status, start_block_height, dungeon_seed, created_at) + VALUES ((SELECT id FROM users WHERE socket_id = $1), $1, $2, 'active', $3, $4, NOW()) + `, [socketId, gameMode, blockHeight, game.id]) + .catch(err => console.error('Game insert failed:', err.message)); + } if (this.debugManager.CONSOLE_LOGGING) { console.log(`[createGameForUser] Created ${gameType} game ${game.id} for user ${user.id}`); @@ -591,6 +602,8 @@ class SocketHandlers { console.log(`Starting games for ${this.WAITING_PLAYERS.length} waiting players at block height ${blockHeight}`); } + // We may skip some entries if they are not yet confirmed (paid mode, mempool only) + const remainingQueue = []; while (this.WAITING_PLAYERS.length > 0) { const playerEntry = this.WAITING_PLAYERS.shift(); const serverId = playerEntry.serverId; @@ -602,6 +615,14 @@ class SocketHandlers { const currentUser = this.getUserBySocket(serverId); if (currentUser) { + // If this entry requires confirmation and is not confirmed yet, keep it in queue + if (playerEntry.requiresConfirmation && !playerEntry.confirmed) { + remainingQueue.push(playerEntry); // re-queue for next block + if (this.debugManager.CONSOLE_LOGGING) { + console.log(`⏳ Skipping unconfirmed paid entry for ${serverId}, still waiting for confirmation.`); + } + continue; + } currentUser.blockRec = blockHeight; if (this.debugManager.CONSOLE_LOGGING) { console.log(`🕒 QUEUE ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`); @@ -629,6 +650,10 @@ class SocketHandlers { this.io.to(serverId).emit('message', 'Error: User not found'); } } + // Put back any remaining (unconfirmed) entries + if (remainingQueue.length > 0) { + this.WAITING_PLAYERS = remainingQueue.concat(this.WAITING_PLAYERS); + } } /** @@ -664,18 +689,18 @@ class SocketHandlers { try { const { gameMode } = data; - const payment = await this.gameModeManager.moneroPayService.createPaymentRequest(socket.id, gameMode); + // Use gameModeManager to create payment request with proper amount + const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, gameMode); this.io.to(socket.id).emit('payment_created', { - paymentId: payment.id, - address: payment.address, - amount: payment.amount, - confirmations: payment.confirmations, + paymentId: paymentRequest.id, + address: paymentRequest.address, + amount: paymentRequest.amount, gameMode: gameMode }); if (this.debugManager.CONSOLE_LOGGING) { - console.log(`💳 Payment request created for ${socket.id}: ${payment.amount} XMR`); + console.log(`💳 Payment request created for ${socket.id}: ${paymentRequest.amount} atomic units`); } } catch (error) { console.error('Error creating payment request:', error); @@ -697,13 +722,13 @@ class SocketHandlers { } try { - const { paymentId } = data; - const status = await this.gameModeManager.moneroPayService.checkPaymentStatus(paymentId); + const { address } = data; + const status = await this.walletService.checkPaymentStatus(address); this.io.to(socket.id).emit('payment_status', status); if (this.debugManager.CONSOLE_LOGGING) { - console.log(`💳 Payment status checked for ${socket.id}: ${status.status}`); + console.log(`💳 Payment status checked for ${socket.id}: ${status.complete ? 'complete' : 'pending'}`); } } catch (error) { console.error('Error checking payment status:', error); @@ -846,6 +871,331 @@ class SocketHandlers { `Address noted: ${pendingAddress.address} (Payment system not active)`); } } + + /** + * Create payment request immediately when user types 'enter' in paid mode + * User stays on welcome screen until payment is detected + */ + async createAndShowPaymentRequest(socket) { + try { + const currentUser = this.getUserBySocket(socket.id); + if (!currentUser) { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Error: Could not process payment request. Please try again.'); + return; + } + + // Get game mode information + const gameMode = this.gameModeManager.gameMode; + const cryptoType = this.gameModeManager.cryptoType; + + // Determine payment type and amount based on game mode + let paymentType, amount, description; + + if (gameMode === 'PAID_SINGLE') { + paymentType = 'single_game'; + amount = this.gameModeManager.singleGamePrice; + description = 'Single game entry'; + } else if (gameMode === 'PAID_CREDITS') { + paymentType = 'credits_package'; + amount = this.gameModeManager.creditsPackagePrice; + description = '10 game credits package'; + } else { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Invalid game mode configuration.'); + return; + } + + // Create payment request + const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, paymentType); + + // Convert atomic units to human readable amount + const humanAmount = (amount / 1000000000000).toFixed(3); + + // Send payment information to client (for UI) + this.io.to(socket.id).emit('payment_created', { + paymentId: paymentRequest.id, + address: paymentRequest.address, + amount: paymentRequest.amount, + paymentType: paymentType, + gameMode: gameMode, + cryptoType: cryptoType, + humanAmount: humanAmount, + description: description, + expiresAt: paymentRequest.expiresAt + }); + + // Send status message to chat with payment details + this.broadcastManager.sendStatusUpdate(socket.id, 'payment', + `💳 PAYMENT REQUIRED (${description})\n\n` + + `Amount: ${humanAmount} ${cryptoType}\n` + + `Address: ${paymentRequest.address}\n\n` + + `⚠️ Send EXACTLY ${humanAmount} ${cryptoType} to the address above.\n` + + `🔄 You will be added to the game queue once payment is detected in mempool.\n` + + `⏰ Payment expires in 30 minutes.`); + + // Start monitoring the payment address + this.walletService.startPaymentMonitoring( + paymentRequest.address, + async (status) => { + if (status.in_mempool && !status.confirmed) { + // Payment detected in mempool - add to queue + socket.emit('payment_detected', { + message: 'Payment detected! Adding you to the game queue...', + amount: status.amount, + confirmations: 0 + }); + + // Add user to waiting queue (requires confirmation before starting) + this.WAITING_PLAYERS.push({ + serverId: socket.id, + clientId: currentUser.clientId, + entryTime: Date.now(), + paymentId: paymentRequest.id, + requiresConfirmation: true, + confirmed: false + }); + + // Show waiting screen + socket.emit('queue_joined', { + position: this.WAITING_PLAYERS.length, + message: 'Payment received! Waiting for next block to start game...' + }); + + } else if (status.confirmed) { + // Payment confirmed - game can start + socket.emit('payment_confirmed', { + message: 'Payment confirmed!', + confirmations: status.confirmations + }); + // Mark any existing waiting queue entry as confirmed + const entry = this.WAITING_PLAYERS.find(p => p.serverId === socket.id); + if (entry) { + entry.confirmed = true; + } + } + }, + 2000 // Check every 2 seconds + ); + + if (this.debugManager.CONSOLE_LOGGING) { + console.log(`💳 Payment request created for ${socket.id}: ${humanAmount} ${cryptoType} (${paymentType})`); + console.log(`👀 Started monitoring address: ${paymentRequest.address}`); + } + + } catch (error) { + console.error('Error creating payment request:', error); + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Failed to create payment request. Please try again or contact support.'); + } + } + + /** + * Start monitoring a payment address for incoming payments + */ + startPaymentMonitoring(socketId, paymentRequest) { + if (!this.paymentMonitors) { + this.paymentMonitors = new Map(); + } + + // Clear any existing monitor for this user + this.stopPaymentMonitoring(socketId); + + const monitor = { + paymentId: paymentRequest.id, + address: paymentRequest.address, + socketId: socketId, + startTime: Date.now(), + interval: setInterval(async () => { + try { + const status = await this.walletService.checkPaymentStatus(paymentRequest.address); + + if (status.confirmed || status.in_mempool) { + // Payment detected! + this.handlePaymentDetected(socketId, paymentRequest, status); + } + } catch (error) { + console.error(`Error checking payment status for ${socketId}:`, error); + } + }, 5000) // Check every 5 seconds + }; + + this.paymentMonitors.set(socketId, monitor); + + // Auto-expire after 30 minutes + setTimeout(() => { + this.stopPaymentMonitoring(socketId); + this.broadcastManager.sendStatusUpdate(socketId, 'warning', + 'Payment request expired. Type \'enter\' again to create a new payment request.'); + }, 30 * 60 * 1000); // 30 minutes + } + + /** + * Stop monitoring a payment address + */ + stopPaymentMonitoring(socketId) { + if (this.paymentMonitors && this.paymentMonitors.has(socketId)) { + const monitor = this.paymentMonitors.get(socketId); + clearInterval(monitor.interval); + this.paymentMonitors.delete(socketId); + + if (this.debugManager.CONSOLE_LOGGING) { + console.log(`🛑 Stopped payment monitoring for ${socketId}`); + } + } + } + + /** + * Handle payment detection - add user to queue + */ + async handlePaymentDetected(socketId, paymentRequest, paymentStatus) { + this.stopPaymentMonitoring(socketId); + + const currentUser = this.getUserBySocket(socketId); + if (!currentUser) { + console.error(`Payment detected but user not found: ${socketId}`); + return; + } + + // Check if user is already in queue or game + const existingIndex = this.WAITING_PLAYERS.findIndex(p => p.serverId === socketId); + if (existingIndex !== -1) { + this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Payment confirmed, you are already in the queue!'); + return; + } + + if (this.activeGames.has(socketId)) { + this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Payment confirmed, but you are already in a game!'); + return; + } + + // Add to waiting queue. If only mempool detected, require confirmation. + this.WAITING_PLAYERS.push({ + serverId: socketId, + clientId: currentUser.clientId, + entryTime: Date.now(), + paymentId: paymentRequest.id, + requiresConfirmation: paymentStatus.in_mempool && !paymentStatus.confirmed, + confirmed: paymentStatus.confirmed + }); + + const currentBlock = this.debugManager.getCurrentBlockHeight(); + const nextBlock = currentBlock + 1; + + if (paymentStatus.in_mempool && !paymentStatus.confirmed) { + // Payment in mempool but not confirmed yet + this.broadcastManager.sendStatusUpdate(socketId, 'success', + `💰 PAYMENT DETECTED IN MEMPOOL!\n\n` + + `✅ You have been added to the game queue.\n` + + `🕒 Your game will start when block ${nextBlock} is found.\n` + + `📦 Current block: ${currentBlock}`); + this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Waiting for block confirmation before starting...'); + } else if (paymentStatus.confirmed) { + // Payment already confirmed + this.broadcastManager.sendStatusUpdate(socketId, 'success', + `💰 PAYMENT CONFIRMED!\n\n` + + `✅ You have been added to the game queue.\n` + + `🕒 Your game will start when block ${nextBlock} is found.\n` + + `📦 Current block: ${currentBlock}`); + } + + // Send user to waiting screen + this.io.to(socketId).emit('payment_confirmed', { + paymentId: paymentRequest.id, + status: paymentStatus, + nextBlock: nextBlock, + currentBlock: currentBlock + }); + + if (this.debugManager.CONSOLE_LOGGING) { + console.log(`💰 Payment detected for ${socketId}: Added to queue for block ${nextBlock}`); + } + } + + /** + * Handle payment command - create payment request for user + */ + async handlePaymentCommand(socket) { + if (!this.gameModeManager) { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Payment system is not available. Server is running in FREE mode.'); + return; + } + + try { + const currentUser = this.getUserBySocket(socket.id); + if (!currentUser) { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Error: Could not process payment request. Please try again.'); + return; + } + + // Get game mode information + const gameMode = this.gameModeManager.gameMode; + const cryptoType = this.gameModeManager.cryptoType; + + if (gameMode === 'FREE') { + this.broadcastManager.sendStatusUpdate(socket.id, 'info', + 'Server is in FREE mode - no payment required to play!'); + return; + } + + // Determine payment type and amount based on game mode + let paymentType, amount, description; + + if (gameMode === 'PAID_SINGLE') { + paymentType = 'single_game'; + amount = this.gameModeManager.singleGamePrice; + description = `Single game entry (${cryptoType})`; + } else if (gameMode === 'PAID_CREDITS') { + paymentType = 'credits_package'; + amount = this.gameModeManager.creditsPackagePrice; + description = `10 game credits package (${cryptoType})`; + } else { + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Invalid game mode configuration.'); + return; + } + + // Create payment request + const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, paymentType); + + // Convert atomic units to human readable amount + const humanAmount = (amount / 1000000000000).toFixed(3); + + // Send payment information to client + this.io.to(socket.id).emit('payment_created', { + paymentId: paymentRequest.id, + address: paymentRequest.address, + amount: paymentRequest.amount, + paymentType: paymentType, + gameMode: gameMode, + cryptoType: cryptoType, + humanAmount: humanAmount, + description: description, + expiresAt: paymentRequest.expiresAt + }); + + // Send status message to chat + this.broadcastManager.sendStatusUpdate(socket.id, 'payment', + `💳 Payment Request Created!\n\n` + + `Amount: ${humanAmount} ${cryptoType}\n` + + `Type: ${description}\n` + + `Address: ${paymentRequest.address}\n\n` + + `⚠️ Send the EXACT amount to the address above.\n` + + `⏰ Payment will expire in 30 minutes.\n` + + `🔄 Your game will start automatically after payment confirmation.`); + + if (this.debugManager.CONSOLE_LOGGING) { + console.log(`💳 Payment request created for ${socket.id}: ${humanAmount} ${cryptoType} (${paymentType})`); + } + + } catch (error) { + console.error('Error creating payment request:', error); + this.broadcastManager.sendStatusUpdate(socket.id, 'error', + 'Failed to create payment request. Please try again or contact support.'); + } + } } module.exports = SocketHandlers; diff --git a/src/package.json b/src/package.json index a621527..4f05447 100644 --- a/src/package.json +++ b/src/package.json @@ -18,7 +18,7 @@ }, "production": { "enabled": true, - "console_logging": false, + "console_logging": true, "debug_hotkeys": false, "simulated_blocks": false } diff --git a/src/payments/walletRPCService.js b/src/payments/walletRPCService.js new file mode 100644 index 0000000..0adc5a1 --- /dev/null +++ b/src/payments/walletRPCService.js @@ -0,0 +1,352 @@ +const axios = require('axios'); +const { v4: uuidv4 } = require('uuid'); + +class WalletRPCService { + constructor(debugManager) { + this.debugManager = debugManager; + this.walletEndpoint = process.env.PRIMARY_WALLET_ENDPOINT || 'http://127.0.0.1:34570'; + this.accountIndex = 0; + this.paymentWatchers = new Map(); + this.addressToUser = new Map(); + this.addressToSocket = new Map(); + this.isHealthy = false; + } + + async initialize() { + try { + // Test wallet connection + const response = await this.rpcCall('get_version'); + this.isHealthy = true; + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log('✅ Wallet RPC service initialized:', { + version: response.result?.version || 'unknown', + endpoint: this.walletEndpoint + }); + } + + // Get wallet height to ensure it's synced + const heightResponse = await this.rpcCall('get_height'); + if (this.debugManager?.CONSOLE_LOGGING) { + console.log('📊 Wallet height:', heightResponse.result?.height || 0); + } + + return true; + } catch (error) { + this.isHealthy = false; + console.error('❌ Wallet RPC connection failed:', error.message); + console.error(' Make sure wownero-wallet-rpc is running on', this.walletEndpoint); + return false; + } + } + + async rpcCall(method, params = {}) { + try { + const response = await axios.post(`${this.walletEndpoint}/json_rpc`, { + jsonrpc: '2.0', + id: '0', + method: method, + params: params + }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 10000 + }); + + if (response.data.error) { + throw new Error(response.data.error.message); + } + + return response.data; + } catch (error) { + if (this.debugManager?.CONSOLE_LOGGING) { + console.error(`❌ Wallet RPC call failed (${method}):`, error.message); + } + throw error; + } + } + + async createPaymentRequest(amount, description, userId, socketId) { + try { + // Create a new subaddress for this payment + const response = await this.rpcCall('create_address', { + account_index: this.accountIndex, + label: `game_${userId}_${Date.now()}` + }); + + const address = response.result.address; + const addressIndex = response.result.address_index; + + // Store mapping for payment monitoring + const paymentInfo = { + userId: userId, + socketId: socketId, + amount: amount, + description: description, + addressIndex: addressIndex, + accountIndex: this.accountIndex, + createdAt: new Date(), + status: 'pending', + detected: false, + confirmed: false + }; + + this.addressToUser.set(address, paymentInfo); + this.addressToSocket.set(address, socketId); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`💳 Payment address created:`, { + user: userId, + address: address.substring(0, 10) + '...', + amount: amount, + index: addressIndex + }); + } + + return { + success: true, + id: uuidv4(), // Generate payment ID + address: address, + amount: amount, + description: description, + addressIndex: addressIndex, + 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 }; + } + } + + async checkPaymentStatus(address) { + try { + const userInfo = this.addressToUser.get(address); + if (!userInfo) { + return { + complete: false, + in_mempool: false, + confirmed: false, + amount: 0, + confirmations: 0 + }; + } + + // Get incoming transfers for this specific subaddress + const response = await this.rpcCall('get_transfers', { + in: true, + pending: true, + failed: false, + pool: true, + account_index: userInfo.accountIndex, + subaddr_indices: [userInfo.addressIndex] + }); + + let totalReceived = 0; + let inMempool = false; + let confirmed = false; + let confirmations = 0; + let txHash = null; + + // Check pending (mempool) transactions + if (response.result?.pool && response.result.pool.length > 0) { + for (const tx of response.result.pool) { + totalReceived += tx.amount; + inMempool = true; + txHash = tx.txid; + + // Mark as detected if this is first time seeing it + if (!userInfo.detected) { + userInfo.detected = true; + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`🎯 Payment detected in mempool:`, { + address: address.substring(0, 10) + '...', + amount: tx.amount, + txid: txHash + }); + } + } + } + } + + // Check confirmed transactions + if (response.result?.in && response.result.in.length > 0) { + for (const tx of response.result.in) { + totalReceived += tx.amount; + confirmed = true; + txHash = tx.txid; + + // Calculate confirmations + if (tx.height > 0) { + const heightResp = await this.rpcCall('get_height'); + confirmations = Math.max(confirmations, heightResp.result.height - tx.height); + } + + if (!userInfo.confirmed) { + userInfo.confirmed = true; + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`✅ Payment confirmed:`, { + address: address.substring(0, 10) + '...', + confirmations: confirmations, + txid: txHash + }); + } + } + } + } + + const paymentComplete = totalReceived >= userInfo.amount; + + return { + complete: paymentComplete, + in_mempool: inMempool, + confirmed: confirmed, + amount: totalReceived, + confirmations: confirmations, + required: userInfo.amount, + txHash: txHash + }; + } catch (error) { + console.error('❌ Failed to check payment status:', error); + return { + complete: false, + in_mempool: false, + confirmed: false, + amount: 0, + confirmations: 0 + }; + } + } + + startPaymentMonitoring(address, callback, interval = 2000) { + if (this.paymentWatchers.has(address)) { + return; // Already monitoring + } + + const watcher = setInterval(async () => { + const status = await this.checkPaymentStatus(address); + + if (status.in_mempool || status.confirmed) { + callback(status); + + // Stop monitoring if payment is confirmed + if (status.confirmed && status.complete) { + this.stopPaymentMonitoring(address); + } + } + }, interval); + + this.paymentWatchers.set(address, watcher); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`👁️ Started monitoring payments for:`, address.substring(0, 10) + '...'); + } + } + + stopPaymentMonitoring(address) { + const watcher = this.paymentWatchers.get(address); + if (watcher) { + clearInterval(watcher); + this.paymentWatchers.delete(address); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`🛑 Stopped monitoring:`, address.substring(0, 10) + '...'); + } + } + } + + async processPayout(recipientAddress, amount, description) { + try { + // Send payment to user + const response = await this.rpcCall('transfer', { + destinations: [{ + amount: amount, + address: recipientAddress + }], + account_index: this.accountIndex, + priority: 1, // Normal priority + get_tx_key: true + }); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`💸 Payout sent:`, { + to: recipientAddress.substring(0, 10) + '...', + amount: amount, + txHash: response.result.tx_hash + }); + } + + return { + success: true, + txHash: response.result.tx_hash, + txKey: response.result.tx_key, + fee: response.result.fee + }; + } catch (error) { + console.error('❌ Failed to process payout:', error); + return { + success: false, + error: error.message + }; + } + } + + startPaymentMonitoring(address, callback, interval = 2000) { + if (this.paymentWatchers.has(address)) { + return; // Already monitoring + } + + const watcher = setInterval(async () => { + try { + const status = await this.checkPaymentStatus(address); + + if (status.in_mempool || status.confirmed) { + callback(status); + + // Stop monitoring if payment is confirmed + if (status.confirmed && status.complete) { + this.stopPaymentMonitoring(address); + } + } + } catch (error) { + console.error(`❌ Error monitoring payment ${address.substring(0, 10)}...:`, error.message); + } + }, interval); + + this.paymentWatchers.set(address, watcher); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`👁️ Started monitoring payments for:`, address.substring(0, 10) + '...'); + } + } + + stopPaymentMonitoring(address) { + const watcher = this.paymentWatchers.get(address); + if (watcher) { + clearInterval(watcher); + this.paymentWatchers.delete(address); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`🛑 Stopped monitoring:`, address.substring(0, 10) + '...'); + } + } + } + + cleanupUserPayments(userId) { + for (const [address, userInfo] of this.addressToUser.entries()) { + if (userInfo.userId === userId || userInfo.socketId === userId) { + this.stopPaymentMonitoring(address); + } + } + } + + // Get service health status + getHealthStatus() { + return { + healthy: this.isHealthy, + endpoint: this.walletEndpoint, + activeMonitors: this.paymentWatchers.size, + pendingPayments: this.addressToUser.size + }; + } +} + +module.exports = WalletRPCService; \ No newline at end of file diff --git a/src/rpc/rpcService.js b/src/rpc/rpcService.js index e017061..fd9e8cb 100644 --- a/src/rpc/rpcService.js +++ b/src/rpc/rpcService.js @@ -5,7 +5,7 @@ const axios = require('axios'); -const CONSOLE_LOGGING = process.env.NODE_ENV === 'debug' || process.env.NODE_ENV === 'development'; +const CONSOLE_LOGGING = true; // Temporarily enable for debugging class RPCService { constructor() { @@ -127,7 +127,7 @@ class RPCService { */ async getBlockHeight() { try { - const result = await this.makeRPCCall('get_block_count'); + const result = await this.makeRPCCall('getblockcount'); const blockHeight = result.count; if (blockHeight !== this.lastBlockHeight) { @@ -166,7 +166,7 @@ class RPCService { async getNetworkInfo() { try { const [blockCount, networkInfo] = await Promise.all([ - this.makeRPCCall('get_block_count'), + this.makeRPCCall('getblockcount'), this.makeRPCCall('get_info') ]); @@ -203,7 +203,7 @@ class RPCService { await axios.post(`${this.primaryEndpoint}/json_rpc`, { jsonrpc: '2.0', id: '0', - method: 'get_block_count' + method: 'getblockcount' }, { timeout: 5000 }); checks.primary.status = 'healthy'; checks.primary.responseTime = Date.now() - start; @@ -219,7 +219,7 @@ class RPCService { await axios.post(`${this.fallbackEndpoint}/json_rpc`, { jsonrpc: '2.0', id: '0', - method: 'get_block_count' + method: 'getblockcount' }, { timeout: 5000 }); checks.fallback.status = 'healthy'; checks.fallback.responseTime = Date.now() - start; diff --git a/src/rpc/rpccalls.js b/src/rpc/rpccalls.js index b7aa58d..1afd512 100644 --- a/src/rpc/rpccalls.js +++ b/src/rpc/rpccalls.js @@ -24,7 +24,15 @@ var lastBlock = { hash: 0, setHeight: function(newHeight) {this.height = newHeight} } -var daemonURL = 'http://localhost:34568/json_rpc'; + +// Use environment variable for daemon URL, fallback to localhost for backward compatibility +var daemonURL = process.env.PRIMARY_RPC_ENDPOINT ? + process.env.PRIMARY_RPC_ENDPOINT + '/json_rpc' : + 'http://localhost:34568/json_rpc'; + +if (CONSOLE_LOGGING) { + console.log(`🔗 Legacy RPC service using URL: ${daemonURL}`); +} function daemonCall(method, params, callback, io) { // Real blockchain call (now used for all cases) diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..bbdce0b --- /dev/null +++ b/todo.md @@ -0,0 +1,502 @@ +I'll perform a comprehensive deep review of the entire codebase after the MoneroPay removal and wallet-rpc integration. Let me analyze every aspect systematically. + +## 🔍 DEEP CODEBASE REVIEW + +### 1. **Critical Issues to Fix** + +#### A. **Missing WalletRPCService Reference in socketHandlers.js** +```javascript +// Line 298 - socketHandlers.js references this.walletService but it's never initialized! +this.walletService.startPaymentMonitoring( +``` +**FIX NEEDED**: The `SocketHandlers` constructor doesn't receive or store `walletService`. Need to pass it in and store it. + +#### B. **MoneroPay References Still Present** +- Line 14 in index.js: `const MoneroPayService = require('./payments/moneroPayService');` +- Line 59 in index.js: `const moneroPayService = new MoneroPayService(debugManager);` +- Line 143-148 in index.js: MoneroPay initialization +- `gameModeManager.js` still references `this.moneroPayService` +- API endpoints in index.js still use MoneroPay + +#### C. **Database Connection String Issue** +The database manager uses hardcoded connection string instead of environment variables properly. + +#### D. **RPC Service Method Name Issues** +```javascript +// rpcService.js line 77 - Wrong method name for Wownero +method: 'get_block_count' // Should be 'getblockcount' for Wownero +``` + +### 2. **Complete MoneroPay → WalletRPC Migration** + +Let me create the complete migration: + +```javascript +// src/payments/walletRPCService.js +const axios = require('axios'); +const { v4: uuidv4 } = require('uuid'); + +class WalletRPCService { + constructor(debugManager) { + this.debugManager = debugManager; + this.walletEndpoint = process.env.PRIMARY_WALLET_ENDPOINT || 'http://127.0.0.1:34570'; + this.accountIndex = 0; + this.paymentWatchers = new Map(); + this.addressToUser = new Map(); + this.addressToSocket = new Map(); + this.isHealthy = false; + } + + async initialize() { + try { + // Test wallet connection + const response = await this.rpcCall('get_version'); + this.isHealthy = true; + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log('✅ Wallet RPC service initialized:', { + version: response.result?.version || 'unknown', + endpoint: this.walletEndpoint + }); + } + + // Get wallet height to ensure it's synced + const heightResponse = await this.rpcCall('get_height'); + if (this.debugManager?.CONSOLE_LOGGING) { + console.log('📊 Wallet height:', heightResponse.result?.height || 0); + } + + return true; + } catch (error) { + this.isHealthy = false; + console.error('❌ Wallet RPC connection failed:', error.message); + console.error(' Make sure wownero-wallet-rpc is running on', this.walletEndpoint); + return false; + } + } + + async rpcCall(method, params = {}) { + try { + const response = await axios.post(`${this.walletEndpoint}/json_rpc`, { + jsonrpc: '2.0', + id: '0', + method: method, + params: params + }, { + headers: { 'Content-Type': 'application/json' }, + timeout: 10000 + }); + + if (response.data.error) { + throw new Error(response.data.error.message); + } + + return response.data; + } catch (error) { + if (this.debugManager?.CONSOLE_LOGGING) { + console.error(`❌ Wallet RPC call failed (${method}):`, error.message); + } + throw error; + } + } + + async createPaymentRequest(amount, description, userId, socketId) { + try { + // Create a new subaddress for this payment + const response = await this.rpcCall('create_address', { + account_index: this.accountIndex, + label: `game_${userId}_${Date.now()}` + }); + + const address = response.result.address; + const addressIndex = response.result.address_index; + + // Store mapping for payment monitoring + const paymentInfo = { + userId: userId, + socketId: socketId, + amount: amount, + description: description, + addressIndex: addressIndex, + accountIndex: this.accountIndex, + createdAt: new Date(), + status: 'pending', + detected: false, + confirmed: false + }; + + this.addressToUser.set(address, paymentInfo); + this.addressToSocket.set(address, socketId); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`💳 Payment address created:`, { + user: userId, + address: address.substring(0, 10) + '...', + amount: amount, + index: addressIndex + }); + } + + return { + success: true, + id: uuidv4(), // Generate payment ID + address: address, + amount: amount, + description: description, + addressIndex: addressIndex, + 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 }; + } + } + + async checkPaymentStatus(address) { + try { + const userInfo = this.addressToUser.get(address); + if (!userInfo) { + return { + complete: false, + in_mempool: false, + confirmed: false, + amount: 0, + confirmations: 0 + }; + } + + // Get incoming transfers for this specific subaddress + const response = await this.rpcCall('get_transfers', { + in: true, + pending: true, + failed: false, + pool: true, + account_index: userInfo.accountIndex, + subaddr_indices: [userInfo.addressIndex] + }); + + let totalReceived = 0; + let inMempool = false; + let confirmed = false; + let confirmations = 0; + let txHash = null; + + // Check pending (mempool) transactions + if (response.result?.pool && response.result.pool.length > 0) { + for (const tx of response.result.pool) { + totalReceived += tx.amount; + inMempool = true; + txHash = tx.txid; + + // Mark as detected if this is first time seeing it + if (!userInfo.detected) { + userInfo.detected = true; + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`🎯 Payment detected in mempool:`, { + address: address.substring(0, 10) + '...', + amount: tx.amount, + txid: txHash + }); + } + } + } + } + + // Check confirmed transactions + if (response.result?.in && response.result.in.length > 0) { + for (const tx of response.result.in) { + totalReceived += tx.amount; + confirmed = true; + txHash = tx.txid; + + // Calculate confirmations + if (tx.height > 0) { + const heightResp = await this.rpcCall('get_height'); + confirmations = Math.max(confirmations, heightResp.result.height - tx.height); + } + + if (!userInfo.confirmed) { + userInfo.confirmed = true; + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`✅ Payment confirmed:`, { + address: address.substring(0, 10) + '...', + confirmations: confirmations, + txid: txHash + }); + } + } + } + } + + const paymentComplete = totalReceived >= userInfo.amount; + + return { + complete: paymentComplete, + in_mempool: inMempool, + confirmed: confirmed, + amount: totalReceived, + confirmations: confirmations, + required: userInfo.amount, + txHash: txHash + }; + } catch (error) { + console.error('❌ Failed to check payment status:', error); + return { + complete: false, + in_mempool: false, + confirmed: false, + amount: 0, + confirmations: 0 + }; + } + } + + startPaymentMonitoring(address, callback, interval = 2000) { + if (this.paymentWatchers.has(address)) { + return; // Already monitoring + } + + const watcher = setInterval(async () => { + const status = await this.checkPaymentStatus(address); + + if (status.in_mempool || status.confirmed) { + callback(status); + + // Stop monitoring if payment is confirmed + if (status.confirmed && status.complete) { + this.stopPaymentMonitoring(address); + } + } + }, interval); + + this.paymentWatchers.set(address, watcher); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`👁️ Started monitoring payments for:`, address.substring(0, 10) + '...'); + } + } + + stopPaymentMonitoring(address) { + const watcher = this.paymentWatchers.get(address); + if (watcher) { + clearInterval(watcher); + this.paymentWatchers.delete(address); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`🛑 Stopped monitoring:`, address.substring(0, 10) + '...'); + } + } + } + + async processPayout(recipientAddress, amount, description) { + try { + // Send payment to user + const response = await this.rpcCall('transfer', { + destinations: [{ + amount: amount, + address: recipientAddress + }], + account_index: this.accountIndex, + priority: 1, // Normal priority + get_tx_key: true + }); + + if (this.debugManager?.CONSOLE_LOGGING) { + console.log(`💸 Payout sent:`, { + to: recipientAddress.substring(0, 10) + '...', + amount: amount, + txHash: response.result.tx_hash + }); + } + + return { + success: true, + txHash: response.result.tx_hash, + txKey: response.result.tx_key, + fee: response.result.fee + }; + } catch (error) { + console.error('❌ Failed to process payout:', error); + return { + success: false, + error: error.message + }; + } + } + + cleanupUserPayments(userId) { + for (const [address, userInfo] of this.addressToUser.entries()) { + if (userInfo.userId === userId || userInfo.socketId === userId) { + this.stopPaymentMonitoring(address); + } + } + } + + // Get service health status + getHealthStatus() { + return { + healthy: this.isHealthy, + endpoint: this.walletEndpoint, + activeMonitors: this.paymentWatchers.size, + pendingPayments: this.addressToUser.size + }; + } +} + +module.exports = WalletRPCService; +``` + +### 3. **Architecture Issues** + +#### A. **Circular Dependencies Risk** +- socketHandlers.js has too many responsibilities (900+ lines!) +- Should split into: `gameHandlers.js`, `paymentHandlers.js`, `chatHandlers.js` + +#### B. **Memory Leaks** +- Payment monitors in `walletRPCService` never clean up expired addresses +- `addressToUser` Map grows indefinitely + +#### C. **Error Recovery** +- No reconnection logic if wallet-rpc goes down +- No graceful degradation when blockchain daemon fails + +### 4. **Security Issues** + +#### A. **SQL Injection Risk** +```javascript +// databaseManager.js - Using string concatenation! +async run(query, params = []) { + const result = await this.pool.query(query, params); +``` +Should use parameterized queries consistently. + +#### B. **Missing Input Validation** +- No validation on payment amounts +- No address format validation before creating subaddress +- No rate limiting on payment requests + +#### C. **Sensitive Data Logging** +- Full addresses logged in debug mode +- Should truncate addresses in logs + +### 5. **Performance Issues** + +#### A. **Inefficient Payment Monitoring** +- Each payment has its own interval timer +- Should batch check all pending payments in one RPC call + +#### B. **Database Connection Pool** +```javascript +// databaseManager.js +max: 10, // Could be configurable +``` + +#### C. **Memory Usage** +- Game state stored in memory indefinitely +- Should implement cleanup for completed games + +### 6. **Missing Features** + +#### A. **Payment Expiration** +- Payment addresses never expire +- Should clean up after 30 minutes + +#### B. **Refund Logic** +- No way to refund overpayments +- No handling for duplicate payments + +#### C. **Admin Interface** +- No way to manually credit users +- No payment history viewing + +### 7. **Configuration Issues** + +#### A. **.env File** +```properties +# These MoneroPay vars should be removed: +MONEROPAY_ENDPOINT=http://192.168.1.23:5000 +MONEROPAY_CALLBACK_BASE_URL=http://localhost:3000/api/payments/callback + +# RPC endpoints inconsistent: +PRIMARY_RPC_ENDPOINT=http://127.0.0.1:34568 # Should this be 18081? +PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570 +``` + +#### B. **Hardcoded Values** +- Payment expiration (30 minutes) +- Check intervals (2 seconds) +- Game prices + +### 8. **Frontend Issues** + +#### A. **Payment UI Missing** +- No visual payment countdown timer +- No QR code for payment address +- No copy-to-clipboard for addresses + +#### B. **Connection Status** +- No visual indicator when wallet-rpc is down +- No retry mechanism for failed connections + +### 9. **Testing Coverage** + +#### A. **Missing Tests** +- No tests for payment flow +- No tests for wallet-rpc integration +- No tests for game payout logic + +#### B. **Test Environment** +- Need mock wallet-rpc for testing +- Need test fixtures for payment scenarios + +### 10. **Code Quality** + +#### A. **Inconsistent Async Handling** +```javascript +// Sometimes async/await +async handlePaymentRequest(socket, data) { ... } + +// Sometimes callbacks +this.walletService.startPaymentMonitoring(address, (status) => { ... }) +``` + +#### B. **Magic Numbers** +```javascript +2000 // Check interval - should be constant +30 * 60 * 1000 // 30 minutes - should be configurable +1000000000000 // Atomic units conversion - should be constant +``` + +## 📋 PRIORITY FIXES NEEDED + +### CRITICAL (Do immediately): +1. ✅ **COMPLETED** - Fix `walletService` reference in socketHandlers.js +2. ✅ **COMPLETED** - Remove all MoneroPay references +3. ✅ **COMPLETED** - Fix RPC method names for Wownero +4. ✅ **COMPLETED** - Add proper error handling for wallet-rpc failures +5. ✅ **COMPLETED** - Fix method signature mismatches between WalletRPCService and GameModeManager +6. ✅ **COMPLETED** - Add missing startPaymentMonitoring/stopPaymentMonitoring methods to WalletRPCService +7. ✅ **COMPLETED** - Fix property name inconsistencies (inMempool vs in_mempool) +8. ✅ **COMPLETED** - Fix database query methods (db.run vs db.query) +9. ✅ **COMPLETED** - Update README.md to reflect wallet-rpc integration +10. ✅ **COMPLETED** - Remove MoneroPay configuration from .env files + +### HIGH (Do soon): +1. ✅ Implement payment expiration and cleanup +2. ✅ Add address validation before creating subaddress +3. ✅ Batch payment status checks +4. ✅ Add reconnection logic for wallet-rpc + +### MEDIUM (Nice to have): +1. ✅ Split socketHandlers into smaller modulee +2. ✅ Add payment history tracking +3. ✅ Implement refund logic +4. ✅ Add QR codes for payment addresses + +### LOW (Future improvements): +1. ✅ Add comprehensive tests +2. ✅ Create admin interface +3. ✅ Add metrics and monitoring +4. ✅ Implement payment webhooks + +Would you like me to start implementing these fixes, beginning with the critical issues? \ No newline at end of file