diff --git a/src/game/gameModeManager.js b/src/game/gameModeManager.js index 65b28da..d3c9eb7 100644 --- a/src/game/gameModeManager.js +++ b/src/game/gameModeManager.js @@ -719,9 +719,7 @@ class GameModeManager { INSERT INTO credit_transactions (user_id, amount, reason, balance_after, transaction_type) VALUES ($1, $2, 'game_entry', $3, 'spend') `, [user.id, -creditsToSpend, remainingCredits]); - - // Emit credits update - try { this.io && this.io.to(socketId).emit('credits_update', { balance: remainingCredits }); } catch(_) {} + console.log(`šŸŽ« Deducted ${creditsToSpend} credit(s) from user ${user.id}, ${remainingCredits} remaining`); return { @@ -788,16 +786,64 @@ class GameModeManager { if (payoutEligibleMode && outcome === 'escaped' && game.payout_address) { const { amount: payoutAmount, multiplier } = this.calculatePayout(recordedMode, { treasureFound }); if (payoutAmount > 0) { - await this.walletService.processPayout({ - userId: game.user_id, - gameId, - address: game.payout_address, - amount: payoutAmount, - multiplier, - description: `Game ${gameId} payout` - }); - - console.log(`šŸ’ø Created payout: ${payoutAmount} atomic units for game ${gameId} (multiplier ${multiplier}x)`); + try { + const payoutResult = await this.walletService.processPayout({ + userId: game.user_id, + gameId, + address: game.payout_address, + amount: payoutAmount, + multiplier, + description: `Game ${gameId} payout` + }); + + // Record payout in database + const reason = treasureFound ? 'escape_with_treasure' : 'escape'; + await this.db.query(` + INSERT INTO payouts (user_id, game_id, payout_address, amount, multiplier, reason, status, tx_hash, fee, created_at, processed_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW()) + `, [ + game.user_id, + gameId, + game.payout_address, + payoutAmount, + multiplier, + reason, + payoutResult.success ? 'completed' : 'failed', + payoutResult.txHash || null, + payoutResult.fee || null + ]); + + // Update user's total_amount_won + if (payoutResult.success) { + await this.db.query(` + UPDATE users + SET total_amount_won = COALESCE(total_amount_won, 0) + $1, + total_payouts_received = COALESCE(total_payouts_received, 0) + 1 + WHERE id = $2 + `, [payoutAmount, game.user_id]); + } + + console.log(`šŸ’ø Payout ${payoutResult.success ? 'sent' : 'failed'}: ${payoutAmount} atomic units for game ${gameId} (multiplier ${multiplier}x)`); + } catch (payoutError) { + console.error(`āŒ Payout processing failed for game ${gameId}:`, payoutError.message); + // Record failed payout attempt + try { + await this.db.query(` + INSERT INTO payouts (user_id, game_id, payout_address, amount, multiplier, reason, status, error_message, created_at) + VALUES ($1, $2, $3, $4, $5, $6, 'failed', $7, NOW()) + `, [ + game.user_id, + gameId, + game.payout_address, + payoutAmount, + multiplier, + treasureFound ? 'escape_with_treasure' : 'escape', + payoutError.message + ]); + } catch (dbErr) { + console.error('Failed to record payout error:', dbErr.message); + } + } } } diff --git a/src/network/connectionHandler.js b/src/network/connectionHandler.js index 00ed1be..ec6a345 100644 --- a/src/network/connectionHandler.js +++ b/src/network/connectionHandler.js @@ -222,17 +222,28 @@ class ConnectionHandler { // If payout address already set, echo confirmation for UI convenience if (sessionInfo.user.payout_address) { - this.io.to(socket.id).emit('address_confirmed', { - address: sessionInfo.user.payout_address, - message: 'Payout address restored.' + this.io.to(socket.id).emit('address_confirmed', { + address: sessionInfo.user.payout_address, + message: 'Payout address restored.' }); } // Credits convenience push (include creditsPerGame for games remaining calculation) - this.io.to(socket.id).emit('credits_update', { + this.io.to(socket.id).emit('credits_update', { balance: sessionInfo.user.credits || 0, creditsPerGame: this.gameModeManager?.creditsPerGameCost || 1 }); + + // Notify user if credits were recovered from unprocessed payments + if (sessionInfo.recovered && sessionInfo.recovered.creditsRecovered > 0) { + this.broadcastManager.sendStatusUpdate(socket.id, 'success', + `šŸ’° Payment recovered! ${sessionInfo.recovered.creditsRecovered} credits have been added to your balance.`); + this.io.to(socket.id).emit('credits_recovered', { + creditsRecovered: sessionInfo.recovered.creditsRecovered, + paymentsProcessed: sessionInfo.recovered.paymentsProcessed, + newBalance: sessionInfo.user.credits || 0 + }); + } } else { // fallback legacy welcome this.io.to(socket.client.id).emit('welcome', socket.client.id); diff --git a/src/network/paymentHandlers.js b/src/network/paymentHandlers.js index c72b934..4408fd5 100644 --- a/src/network/paymentHandlers.js +++ b/src/network/paymentHandlers.js @@ -282,7 +282,7 @@ class PaymentHandlers { // Attempt immediate game start so user doesn't wait another full block const currentBlock = this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : null; if (currentBlock !== null) { - const started = this.queueManager.startGameImmediately(socket.id, currentBlock); + const started = await this.queueManager.startGameImmediately(socket.id, currentBlock); if (!started && this.debugManager.CONSOLE_LOGGING) { console.log(`[PaymentHandlers] Immediate start skipped (not queued or still unconfirmed) for ${socket.id}`); } @@ -306,33 +306,6 @@ class PaymentHandlers { this._expiryTimeouts.set(socket.id, expiryTimeout); } - async handlePaymentDetected(socketId, paymentRequest, paymentStatus) { - const currentUser = this.queueManager.getUserBySocket ? this.queueManager.getUserBySocket(socketId) : null; - if (!currentUser) return; - const existingIndex = this.queueManager.getPlayerIndex(socketId); - if (existingIndex !== -1) { this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Payment confirmed, already queued!'); return; } - if (this.queueManager.activeGames && this.queueManager.activeGames.has(socketId)) { this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Payment confirmed, but you are already in a game!'); return; } - this.queueManager.addPlayer({ serverId: socketId, clientId: currentUser.clientId, 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) { - this.broadcastManager.sendStatusUpdate(socketId, 'success', `šŸ’° PAYMENT DETECTED (MEMPOOL)\n\nāœ… Added to queue.\nšŸ•’ Starts at block ${nextBlock}.\nšŸ“¦ Current block: ${currentBlock}`); - this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Waiting for block confirmation...'); - } else if (paymentStatus.confirmed) { - this.broadcastManager.sendStatusUpdate(socketId, 'success', `šŸ’° PAYMENT CONFIRMED IN BLOCK\n\nāœ… Added to queue.\nšŸ•’ Starts at block ${nextBlock}.\nšŸ“¦ Current block: ${currentBlock}`); - } - const newBalRes = await this.db.query('SELECT credits FROM users WHERE id = $1', [currentUser.id]); - const remaining = newBalRes.rows[0] ? newBalRes.rows[0].credits : currentUser.credits; - if (this.io) { - this.io.to(socketId).emit('credits_update', { balance: remaining }); - } - if (!this.confirmedPayments.has(paymentRequest.id)) { - this.confirmedPayments.add(paymentRequest.id); - this.io.to(socketId).emit('payment_confirmed', { paymentId: paymentRequest.id, status: paymentStatus, nextBlock, currentBlock }); - this._confirmedTimestamps.set(paymentRequest.id, Date.now()); - } - } - /** * Stop monitoring (if any) for a given socket (disconnect/expiry) */ diff --git a/src/network/queueHandler.js b/src/network/queueHandler.js index f15cdf3..4134f60 100644 --- a/src/network/queueHandler.js +++ b/src/network/queueHandler.js @@ -106,8 +106,8 @@ class QueueHandler { /** * Start games for waiting players when a new block is found */ - startGamesForWaiting(blockHeight) { - return this.queueManager.startGamesForWaiting(blockHeight); + async startGamesForWaiting(blockHeight) { + return await this.queueManager.startGamesForWaiting(blockHeight); } /** diff --git a/src/network/queueManager.js b/src/network/queueManager.js index 4bdb4eb..67b2181 100644 --- a/src/network/queueManager.js +++ b/src/network/queueManager.js @@ -56,7 +56,7 @@ class QueueManager { getQueueLength() { return this._waitingPlayers.length; } getQueuePosition(serverId) { const i = this.getPlayerIndex(serverId); return i === -1 ? -1 : i + 1; } - startGamesForWaiting(blockHeight) { + async startGamesForWaiting(blockHeight) { if (this.CONSOLE_LOGGING) { console.log(`[QueueManager] Starting games for ${this._waitingPlayers.length} waiting players at block ${blockHeight}`); } @@ -91,6 +91,22 @@ class QueueManager { if (game.getProofCommitment) { gameState.proof = game.getProofCommitment(); } + + // Process game start (credits deduction / payment link) + if (this.gameModeManager) { + const startRes = await this.gameModeManager.processGameStart(serverId, game.id); + if (!startRes.success) { + // Abort game + if (this.activeGames) this.activeGames.delete(serverId); + this.io.to(serverId).emit('message', 'Error starting game: ' + (startRes.reason || 'Payment processing failed')); + continue; + } + // Emit credits_update if credits were spent + if (startRes.creditsRemaining !== undefined) { + this.io.to(serverId).emit('credits_update', { balance: startRes.creditsRemaining }); + } + } + this.io.to(serverId).emit('game_start', gameState); if (this.CONSOLE_LOGGING) console.log(`[QueueManager] Game started for ${serverId}`); } catch (error) { @@ -123,7 +139,7 @@ class QueueManager { * the block tick already processed). This prevents an additional full-block wait. * Returns true if a game was started. */ - startGameImmediately(serverId, blockHeight) { + async startGameImmediately(serverId, blockHeight) { const idx = this.getPlayerIndex(serverId); if (idx === -1) return false; // not queued const entry = this._waitingPlayers[idx]; @@ -144,6 +160,22 @@ class QueueManager { if (game.getProofCommitment) { gameState.proof = game.getProofCommitment(); } + + // Process game start (credits deduction / payment link) + if (this.gameModeManager) { + const startRes = await this.gameModeManager.processGameStart(serverId, game.id); + if (!startRes.success) { + // Abort game + if (this.activeGames) this.activeGames.delete(serverId); + this.io.to(serverId).emit('message', 'Error starting game: ' + (startRes.reason || 'Payment processing failed')); + return false; + } + // Emit credits_update if credits were spent + if (startRes.creditsRemaining !== undefined) { + this.io.to(serverId).emit('credits_update', { balance: startRes.creditsRemaining }); + } + } + this.io.to(serverId).emit('game_start', gameState); if (this.CONSOLE_LOGGING) console.log(`[QueueManager] (immediate) Game started for ${serverId} at block ${blockHeight}`); return true; @@ -189,12 +221,26 @@ class QueueManager { gameState.proof = game.getProofCommitment(); } + // Process game start (credits deduction / payment link) + if (this.gameModeManager) { + const startRes = await this.gameModeManager.processGameStart(serverId, game.id); + if (!startRes.success) { + // Abort game + if (this.activeGames) this.activeGames.delete(serverId); + return { success: false, reason: startRes.reason || 'Payment processing failed' }; + } + // Emit credits_update if credits were spent + if (startRes.creditsRemaining !== undefined) { + this.io.to(serverId).emit('credits_update', { balance: startRes.creditsRemaining }); + } + } + this.io.to(serverId).emit('game_start', gameState); - + if (this.CONSOLE_LOGGING) { console.log(`[QueueManager] ⚔ Early entry game started for ${serverId} at block ${blockHeight} (dies at ${blockHeight + 1})`); } - + return { success: true }; } catch (err) { const normalized = normalizeError(err, 'Failed to start early game'); diff --git a/src/network/sessionManager.js b/src/network/sessionManager.js index 60f27bf..8600c27 100644 --- a/src/network/sessionManager.js +++ b/src/network/sessionManager.js @@ -32,7 +32,7 @@ class SessionManager { if (result.rows.length > 0) { const user = result.rows[0]; - + // Update last seen and socket_id await this.db.query( 'UPDATE users SET socket_id = $1, last_seen = NOW() WHERE id = $2', @@ -41,7 +41,18 @@ class SessionManager { // Update cache this.sessions.set(socketId, user); - + + // Check for unprocessed payments (payment recovery) + const recovered = await this.recoverPendingPayments(user.id, socketId); + if (recovered.creditsRecovered > 0) { + // Refresh user data after recovery + const refreshed = await this.db.query('SELECT * FROM users WHERE id = $1', [user.id]); + if (refreshed.rows.length > 0) { + this.sessions.set(socketId, refreshed.rows[0]); + user.credits = refreshed.rows[0].credits; + } + } + if (this.debugManager.CONSOLE_LOGGING) console.log(`[SessionManager] Resumed session for user ${user.id}`); return { resumed: true, @@ -49,7 +60,8 @@ class SessionManager { user: { ...user, socket_id: socketId - } + }, + recovered }; } } @@ -133,6 +145,84 @@ class SessionManager { } } + /** + * Recover unprocessed confirmed payments for a user who disconnected + * This handles credits_package payments that were confirmed but credits weren't added + */ + async recoverPendingPayments(userId, socketId) { + const result = { creditsRecovered: 0, paymentsProcessed: 0 }; + + try { + // Find confirmed credits_package payments that have no credits_purchased or 0 + // These are payments where the user disconnected before confirmation was processed + const unprocessedPayments = await this.db.query(` + SELECT p.id, p.payment_type, p.description, p.expected_amount, p.confirmed_at + FROM payments p + WHERE p.user_id = $1 + AND p.status = 'confirmed' + AND p.payment_type = 'credits_package' + AND (p.credits_purchased IS NULL OR p.credits_purchased = 0) + AND p.confirmed_at > NOW() - INTERVAL '24 hours' + ORDER BY p.confirmed_at ASC + `, [userId]); + + if (unprocessedPayments.rows.length === 0) { + return result; + } + + for (const payment of unprocessedPayments.rows) { + try { + // Parse credits from description (e.g., "Wowngeon 10 credits package (WOW)") + let creditsToAdd = 10; // Default fallback + const desc = payment.description || ''; + const match = desc.match(/(\d+)\s*credits?/i); + if (match) { + creditsToAdd = parseInt(match[1], 10) || 10; + } + + // Add credits to user + const updateResult = await this.db.query(` + UPDATE users + SET credits = credits + $1, + total_credits_purchased = COALESCE(total_credits_purchased, 0) + $1 + WHERE id = $2 + RETURNING credits + `, [creditsToAdd, userId]); + + const newBalance = updateResult.rows[0]?.credits ?? 0; + + // Mark payment as processed + await this.db.query(` + UPDATE payments + SET credits_purchased = $1 + WHERE id = $2 + `, [creditsToAdd, payment.id]); + + // Record credit transaction + await this.db.query(` + INSERT INTO credit_transactions (user_id, amount, reason, balance_after, transaction_type) + VALUES ($1, $2, 'package_purchase_recovered', $3, 'purchase') + `, [userId, creditsToAdd, newBalance]); + + result.creditsRecovered += creditsToAdd; + result.paymentsProcessed++; + + console.log(`šŸ’° [PaymentRecovery] Recovered ${creditsToAdd} credits for user ${userId} from payment ${payment.id}`); + } catch (paymentError) { + console.error(`[PaymentRecovery] Failed to process payment ${payment.id}:`, paymentError.message); + } + } + + if (result.creditsRecovered > 0) { + console.log(`šŸ’° [PaymentRecovery] Total recovered for user ${userId}: ${result.creditsRecovered} credits from ${result.paymentsProcessed} payments`); + } + } catch (error) { + console.error('[PaymentRecovery] Error recovering payments:', error.message); + } + + return result; + } + async cleanupExpiredSessions() { try { // SECURE: Parameterized query for cleanup diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js index 9bdd8b3..eb2a46f 100644 --- a/src/network/socketHandlers.js +++ b/src/network/socketHandlers.js @@ -481,6 +481,10 @@ class SocketHandlers { this.broadcastManager.sendStatusUpdate(socket.id, 'error', startRes.reason || 'Failed to start game.'); return; } + // Emit credits_update if credits were spent + if (startRes.creditsRemaining !== undefined) { + this.io.to(socket.id).emit('credits_update', { balance: startRes.creditsRemaining }); + } } this.io.to(socket.id).emit('game_start', state); @@ -712,8 +716,8 @@ class SocketHandlers { /** * Start games for waiting players when a new block is found */ - startGamesForWaiting(blockHeight) { - return this.queueHandler.startGamesForWaiting(blockHeight); + async startGamesForWaiting(blockHeight) { + return await this.queueHandler.startGamesForWaiting(blockHeight); } /**