Get back into it

This commit is contained in:
jwinterm
2025-09-24 14:06:42 -04:00
parent bd97d9d848
commit 4ad063f735
16 changed files with 1751 additions and 328 deletions
+5 -4
View File
@@ -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
+31 -12
View File
@@ -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 ======
+101 -10
View File
@@ -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;
+21 -18
View File
@@ -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);
}
+448 -98
View File
@@ -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;
+1 -1
View File
@@ -18,7 +18,7 @@
},
"production": {
"enabled": true,
"console_logging": false,
"console_logging": true,
"debug_hotkeys": false,
"simulated_blocks": false
}
+352
View File
@@ -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;
+5 -5
View File
@@ -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;
+9 -1
View File
@@ -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)