mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-24 09:38:32 +02:00
First (untested) payment and db setup
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Wowgue Game Configuration
|
||||
|
||||
# Game Mode Selection (required)
|
||||
# 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
|
||||
|
||||
# Cryptocurrency Configuration
|
||||
CRYPTO_TYPE=XMR
|
||||
# Payment amounts in atomic units (piconeros for XMR, tacoshis for WOW)
|
||||
# XMR: 0.005 XMR = 5000000000 piconeros
|
||||
# WOW: 1 WOW = 100000000000 tacoshis
|
||||
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
|
||||
|
||||
# Blockchain RPC Configuration
|
||||
PRIMARY_RPC_ENDPOINT=http://jw-nodes:18081
|
||||
FALLBACK_RPC_ENDPOINT=http://jw-nodes:18081
|
||||
RPC_POLL_INTERVAL=2000
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=wowgue
|
||||
DB_USER=jw
|
||||
DB_PASSWORD=jw
|
||||
|
||||
# Server Configuration
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
|
||||
# Payout Configuration
|
||||
PAYOUT_BATCH_INTERVAL=300000
|
||||
MAX_PAYOUT_BATCH_SIZE=50
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Database Connection Manager
|
||||
* Handles PostgreSQL connection pool and migration management
|
||||
*/
|
||||
|
||||
const { Pool } = require('pg');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
class DatabaseManager {
|
||||
constructor() {
|
||||
this.pool = null;
|
||||
this.connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database connection and run migrations
|
||||
*/
|
||||
async initialize() {
|
||||
const connected = await this.init();
|
||||
if (connected) {
|
||||
await this.runMigrations();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database connection
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
this.pool = new Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'wowgue',
|
||||
user: process.env.DB_USER || 'jw',
|
||||
password: process.env.DB_PASSWORD || 'jw',
|
||||
max: 20, // Maximum number of clients in the pool
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
// Test connection
|
||||
const client = await this.pool.connect();
|
||||
await client.query('SELECT NOW()');
|
||||
client.release();
|
||||
|
||||
this.connected = true;
|
||||
console.log('✅ Database connected successfully');
|
||||
|
||||
// Run migrations
|
||||
await this.runMigrations();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Database connection failed:', error.message);
|
||||
this.connected = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run database migrations
|
||||
*/
|
||||
async runMigrations() {
|
||||
try {
|
||||
const migrationsPath = path.join(__dirname, '../migrations');
|
||||
const files = await fs.readdir(migrationsPath);
|
||||
const sqlFiles = files.filter(file => file.endsWith('.sql')).sort();
|
||||
|
||||
console.log(`📁 Found ${sqlFiles.length} migration files`);
|
||||
|
||||
for (const file of sqlFiles) {
|
||||
const filePath = path.join(migrationsPath, file);
|
||||
const sql = await fs.readFile(filePath, 'utf8');
|
||||
|
||||
console.log(`🔄 Running migration: ${file}`);
|
||||
await this.pool.query(sql);
|
||||
console.log(`✅ Migration completed: ${file}`);
|
||||
}
|
||||
|
||||
console.log('✅ All migrations completed successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query
|
||||
*/
|
||||
async query(text, params = []) {
|
||||
if (!this.connected) {
|
||||
throw new Error('Database not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
const start = Date.now();
|
||||
const result = await this.pool.query(text, params);
|
||||
const duration = Date.now() - start;
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('📊 Query executed:', { text: text.substring(0, 100), duration, rows: result.rowCount });
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ Database query error:', error.message);
|
||||
console.error('Query:', text);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client from the pool for transactions
|
||||
*/
|
||||
async getClient() {
|
||||
if (!this.connected) {
|
||||
throw new Error('Database not connected');
|
||||
}
|
||||
return await this.pool.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close database connection
|
||||
*/
|
||||
async close() {
|
||||
if (this.pool) {
|
||||
await this.pool.end();
|
||||
this.connected = false;
|
||||
console.log('📴 Database connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status
|
||||
*/
|
||||
isConnected() {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check
|
||||
*/
|
||||
async healthCheck() {
|
||||
try {
|
||||
const result = await this.query('SELECT 1 as status, NOW() as timestamp');
|
||||
return {
|
||||
status: 'healthy',
|
||||
timestamp: result.rows[0].timestamp,
|
||||
connected: this.connected
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
error: error.message,
|
||||
connected: false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DatabaseManager;
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* Game Mode Manager
|
||||
* Handles different game modes (FREE, PAID_SINGLE, PAID_CREDITS)
|
||||
*/
|
||||
|
||||
class GameModeManager {
|
||||
constructor(databaseManager, moneroPayService) {
|
||||
this.db = databaseManager;
|
||||
this.moneroPayService = moneroPayService;
|
||||
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
|
||||
this.creditsPackagePrice = parseInt(process.env.CREDITS_PACKAGE_PRICE) || 50000000000; // 0.05 XMR
|
||||
|
||||
console.log(`🎮 Game Mode Manager initialized: ${this.gameMode} mode`);
|
||||
console.log(`💰 Currency: ${this.cryptoType}`);
|
||||
console.log(`💵 Single game price: ${this.singleGamePrice} atomic units`);
|
||||
console.log(`🎫 Credits package price: ${this.creditsPackagePrice} atomic units`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can start a game
|
||||
*/
|
||||
async canUserStartGame(socketId) {
|
||||
try {
|
||||
const user = await this.getOrCreateUser(socketId);
|
||||
|
||||
switch (this.gameMode) {
|
||||
case 'FREE':
|
||||
return { allowed: true, reason: 'Free mode' };
|
||||
|
||||
case 'PAID_CREDITS':
|
||||
if (user.credits > 0) {
|
||||
return { allowed: true, reason: `${user.credits} credits remaining` };
|
||||
}
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'No credits remaining',
|
||||
action: 'purchase_credits'
|
||||
};
|
||||
|
||||
case 'PAID_SINGLE':
|
||||
// Check for pending payment
|
||||
const pendingPayment = await this.db.query(`
|
||||
SELECT * FROM payments
|
||||
WHERE socket_id = $1 AND status = 'confirmed'
|
||||
AND payment_type = 'single_game'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM games
|
||||
WHERE games.payment_id = payments.id
|
||||
)
|
||||
ORDER BY confirmed_at DESC
|
||||
LIMIT 1
|
||||
`, [socketId]);
|
||||
|
||||
if (pendingPayment.rows.length > 0) {
|
||||
return {
|
||||
allowed: true,
|
||||
reason: 'Payment confirmed',
|
||||
paymentId: pendingPayment.rows[0].id
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'Payment required',
|
||||
action: 'make_payment'
|
||||
};
|
||||
|
||||
default:
|
||||
return { allowed: false, reason: 'Invalid game mode' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error checking user game eligibility:', error.message);
|
||||
return { allowed: false, reason: 'Database error' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process game start (deduct credits or link payment)
|
||||
*/
|
||||
async processGameStart(socketId, gameId) {
|
||||
try {
|
||||
const user = await this.getOrCreateUser(socketId);
|
||||
|
||||
switch (this.gameMode) {
|
||||
case 'FREE':
|
||||
// No processing needed for free mode
|
||||
return { success: true };
|
||||
|
||||
case 'PAID_CREDITS':
|
||||
// Deduct one credit
|
||||
await this.db.query(`
|
||||
UPDATE users
|
||||
SET credits = credits - 1,
|
||||
total_games_played = total_games_played + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`, [user.id]);
|
||||
|
||||
console.log(`🎫 Deducted 1 credit from user ${user.id}, ${user.credits - 1} remaining`);
|
||||
return { success: true, creditsRemaining: user.credits - 1 };
|
||||
|
||||
case 'PAID_SINGLE':
|
||||
// Link game to payment
|
||||
const payment = await this.db.query(`
|
||||
SELECT * FROM payments
|
||||
WHERE socket_id = $1 AND status = 'confirmed'
|
||||
AND payment_type = 'single_game'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM games
|
||||
WHERE games.payment_id = payments.id
|
||||
)
|
||||
ORDER BY confirmed_at DESC
|
||||
LIMIT 1
|
||||
`, [socketId]);
|
||||
|
||||
if (payment.rows.length === 0) {
|
||||
return { success: false, reason: 'No valid payment found' };
|
||||
}
|
||||
|
||||
await this.db.query(`
|
||||
UPDATE games
|
||||
SET payment_id = $1
|
||||
WHERE id = $2
|
||||
`, [payment.rows[0].id, gameId]);
|
||||
|
||||
await this.db.query(`
|
||||
UPDATE users
|
||||
SET total_games_played = total_games_played + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`, [user.id]);
|
||||
|
||||
console.log(`💳 Linked game ${gameId} to payment ${payment.rows[0].id}`);
|
||||
return { success: true, paymentId: payment.rows[0].id };
|
||||
|
||||
default:
|
||||
return { success: false, reason: 'Invalid game mode' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Error processing game start:', error.message);
|
||||
return { success: false, reason: 'Database error' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process game completion (handle payouts)
|
||||
*/
|
||||
async processGameCompletion(gameId, outcome, treasureFound = false) {
|
||||
try {
|
||||
// Get game details
|
||||
const gameResult = await this.db.query(`
|
||||
SELECT g.*, u.payout_address, u.id as user_id
|
||||
FROM games g
|
||||
JOIN users u ON g.user_id = u.id
|
||||
WHERE g.id = $1
|
||||
`, [gameId]);
|
||||
|
||||
if (gameResult.rows.length === 0) {
|
||||
return { success: false, reason: 'Game not found' };
|
||||
}
|
||||
|
||||
const game = gameResult.rows[0];
|
||||
|
||||
// Update game status
|
||||
await this.db.query(`
|
||||
UPDATE games
|
||||
SET status = $1,
|
||||
outcome = $2,
|
||||
treasure_found = $3,
|
||||
completed_at = NOW()
|
||||
WHERE id = $4
|
||||
`, [outcome === 'escaped' ? 'won' : 'lost', outcome, treasureFound, gameId]);
|
||||
|
||||
// Handle payouts for PAID_SINGLE mode
|
||||
if (this.gameMode === 'PAID_SINGLE' && outcome === 'escaped' && game.payout_address) {
|
||||
let payoutAmount;
|
||||
|
||||
if (treasureFound) {
|
||||
payoutAmount = this.singleGamePrice * 3; // 3x for treasure
|
||||
} else {
|
||||
payoutAmount = this.singleGamePrice * 2; // 2x for escape
|
||||
}
|
||||
|
||||
// Create payout record
|
||||
await this.moneroPayService.createPayout(
|
||||
game.user_id,
|
||||
gameId,
|
||||
game.payout_address,
|
||||
payoutAmount
|
||||
);
|
||||
|
||||
console.log(`💸 Created payout: ${payoutAmount} atomic units for game ${gameId}`);
|
||||
}
|
||||
|
||||
// Update user statistics
|
||||
if (outcome === 'escaped') {
|
||||
await this.db.query(`
|
||||
UPDATE users
|
||||
SET total_games_won = total_games_won + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`, [game.user_id]);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
outcome,
|
||||
treasureFound,
|
||||
payoutCreated: this.gameMode === 'PAID_SINGLE' && outcome === 'escaped'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error processing game completion:', error.message);
|
||||
return { success: false, reason: 'Database error' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create payment request
|
||||
*/
|
||||
async createPaymentRequest(socketId, paymentType) {
|
||||
try {
|
||||
const user = await this.getOrCreateUser(socketId);
|
||||
|
||||
let amount, description;
|
||||
|
||||
switch (paymentType) {
|
||||
case 'single_game':
|
||||
amount = this.singleGamePrice;
|
||||
description = `Wowgue single game entry (${this.cryptoType})`;
|
||||
break;
|
||||
|
||||
case 'credits_package':
|
||||
amount = this.creditsPackagePrice;
|
||||
description = `Wowgue 10 credits package (${this.cryptoType})`;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Invalid payment type');
|
||||
}
|
||||
|
||||
return await this.moneroPayService.createPaymentRequest(
|
||||
user.id,
|
||||
socketId,
|
||||
paymentType,
|
||||
amount,
|
||||
description
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating payment request:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create user record
|
||||
*/
|
||||
async getOrCreateUser(socketId, ipAddress = null) {
|
||||
try {
|
||||
// Try to find existing user
|
||||
let userResult = await this.db.query(`
|
||||
SELECT * FROM users WHERE socket_id = $1
|
||||
`, [socketId]);
|
||||
|
||||
if (userResult.rows.length > 0) {
|
||||
// Update last active
|
||||
await this.db.query(`
|
||||
UPDATE users
|
||||
SET last_active = NOW()
|
||||
WHERE id = $1
|
||||
`, [userResult.rows[0].id]);
|
||||
|
||||
return userResult.rows[0];
|
||||
}
|
||||
|
||||
// Create new user
|
||||
userResult = await this.db.query(`
|
||||
INSERT INTO users (socket_id, ip_address)
|
||||
VALUES ($1, $2)
|
||||
RETURNING *
|
||||
`, [socketId, ipAddress]);
|
||||
|
||||
console.log(`👤 Created new user: ${socketId}`);
|
||||
return userResult.rows[0];
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting/creating user:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user payout address
|
||||
*/
|
||||
async setUserPayoutAddress(socketId, payoutAddress) {
|
||||
try {
|
||||
const user = await this.getOrCreateUser(socketId);
|
||||
|
||||
await this.db.query(`
|
||||
UPDATE users
|
||||
SET payout_address = $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
`, [payoutAddress, user.id]);
|
||||
|
||||
console.log(`💰 Set payout address for user ${user.id}: ${payoutAddress}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error setting payout address:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics
|
||||
*/
|
||||
async getUserStats(socketId) {
|
||||
try {
|
||||
const result = await this.db.query(`
|
||||
SELECT * FROM get_user_stats($1)
|
||||
`, [socketId]);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
return result.rows[0];
|
||||
}
|
||||
|
||||
return {
|
||||
total_games: 0,
|
||||
games_won: 0,
|
||||
win_rate: 0,
|
||||
total_paid: 0,
|
||||
total_won: 0,
|
||||
net_profit: 0,
|
||||
credits_remaining: 0
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error getting user stats:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get game mode info for frontend
|
||||
*/
|
||||
getGameModeInfo() {
|
||||
return {
|
||||
mode: this.gameMode,
|
||||
cryptoType: this.cryptoType,
|
||||
singleGamePrice: this.singleGamePrice,
|
||||
creditsPackagePrice: this.creditsPackagePrice,
|
||||
features: {
|
||||
paymentRequired: this.gameMode !== 'FREE',
|
||||
creditsSystem: this.gameMode === 'PAID_CREDITS',
|
||||
payouts: this.gameMode === 'PAID_SINGLE'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GameModeManager;
|
||||
+136
-5
@@ -5,16 +5,31 @@ var http = require('http').Server(app);
|
||||
var io = require('socket.io')(http);
|
||||
var db = require('./db/dbcalls.js');
|
||||
|
||||
// Load environment configuration
|
||||
require('dotenv').config();
|
||||
|
||||
// Import payment system components
|
||||
const DatabaseManager = require('./db/databaseManager');
|
||||
const MoneroPayService = require('./payments/moneroPayService');
|
||||
const GameModeManager = require('./game/gameModeManager');
|
||||
const RpcService = require('./rpc/rpcService');
|
||||
|
||||
// Import modular components
|
||||
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
|
||||
const broadcastManager = new BroadcastManager(io);
|
||||
const debugManager = new DebugManager(broadcastManager);
|
||||
const activeGames = new Map(); // Maps socketId to Game objects
|
||||
const socketHandlers = new SocketHandlers(io, activeGames, broadcastManager, debugManager);
|
||||
const socketHandlers = new SocketHandlers(io, activeGames, broadcastManager, debugManager, gameModeManager);
|
||||
// Configure static file serving
|
||||
const htmlPath = path.join(__dirname, '../html');
|
||||
app.use(express.static(htmlPath));
|
||||
@@ -33,6 +48,69 @@ app.post('/debug', (req, res) => {
|
||||
res.json({ received: true, timestamp: Date.now() });
|
||||
});
|
||||
|
||||
// Payment system API endpoints
|
||||
app.post('/api/payment/create', async (req, res) => {
|
||||
try {
|
||||
const { userId, gameMode } = req.body;
|
||||
if (!userId || !gameMode) {
|
||||
return res.status(400).json({ error: 'Missing userId or gameMode' });
|
||||
}
|
||||
|
||||
const payment = await moneroPayService.createPaymentRequest(userId, gameMode);
|
||||
res.json(payment);
|
||||
} catch (error) {
|
||||
console.error('Error creating payment:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/payment/callback', async (req, res) => {
|
||||
try {
|
||||
const result = await moneroPayService.processCallback(req.body);
|
||||
res.json({ status: 'success', result });
|
||||
} catch (error) {
|
||||
console.error('Error processing callback:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/payment/status/:paymentId', async (req, res) => {
|
||||
try {
|
||||
const status = await moneroPayService.checkPaymentStatus(req.params.paymentId);
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
console.error('Error checking payment status:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/user/:userId/credits', async (req, res) => {
|
||||
try {
|
||||
const credits = await gameModeManager.getUserCredits(req.params.userId);
|
||||
res.json({ credits });
|
||||
} catch (error) {
|
||||
console.error('Error getting user credits:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/game-modes', (req, res) => {
|
||||
res.json({
|
||||
FREE: { name: 'Free Play', cost: 0, payoutMultiplier: 0 },
|
||||
PAID_SINGLE: {
|
||||
name: 'Paid Single Game',
|
||||
cost: parseFloat(process.env.SINGLE_GAME_COST || '0.005'),
|
||||
payoutMultiplier: { escape: 2, treasure: 3 }
|
||||
},
|
||||
PAID_CREDITS: {
|
||||
name: 'Credits Package',
|
||||
cost: parseFloat(process.env.CREDITS_PACKAGE_COST || '0.03'),
|
||||
credits: parseInt(process.env.CREDITS_PER_PACKAGE || '10'),
|
||||
payoutMultiplier: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize debug manager with new block callbacks
|
||||
debugManager.onNewBlockCallback((blockHeight) => {
|
||||
// Start games for waiting players when new block detected
|
||||
@@ -41,6 +119,32 @@ debugManager.onNewBlockCallback((blockHeight) => {
|
||||
socketHandlers.checkGamesTimeout(blockHeight);
|
||||
});
|
||||
|
||||
// Initialize payment system
|
||||
async function initializePaymentSystem() {
|
||||
try {
|
||||
console.log('🔄 Initializing payment system...');
|
||||
|
||||
// Initialize database connection and run migrations
|
||||
await databaseManager.initialize();
|
||||
console.log('✅ Database initialized successfully');
|
||||
|
||||
// Test RPC service connectivity
|
||||
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) {
|
||||
console.error('❌ Failed to initialize payment system:', error);
|
||||
console.log('⚠️ Server will continue in FREE mode only');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start the debug/production system
|
||||
debugManager.initialize();
|
||||
|
||||
@@ -66,7 +170,34 @@ function debugRegisteredUsers() {
|
||||
setInterval(debugRegisteredUsers, 10000);
|
||||
|
||||
// Start server
|
||||
http.listen(3000, function() {
|
||||
console.log('🚀 Wowngeon server listening on *:3000');
|
||||
console.log(`🐛 Debug mode: ${debugManager.getDebugStatus().debugMode ? 'ENABLED' : 'DISABLED'}`);
|
||||
});
|
||||
async function startServer() {
|
||||
try {
|
||||
// Initialize payment system first
|
||||
const paymentSystemReady = await initializePaymentSystem();
|
||||
|
||||
// Start HTTP server
|
||||
http.listen(3000, function() {
|
||||
console.log('🚀 Wowngeon server listening on *:3000');
|
||||
console.log(`🐛 Debug mode: ${debugManager.getDebugStatus().debugMode ? 'ENABLED' : 'DISABLED'}`);
|
||||
console.log(`💰 Payment system: ${paymentSystemReady ? 'ENABLED' : 'FREE MODE ONLY'}`);
|
||||
console.log(`🎮 Available game modes: ${paymentSystemReady ? 'FREE, PAID_SINGLE, PAID_CREDITS' : 'FREE ONLY'}`);
|
||||
});
|
||||
|
||||
// Start batch payout processing if payment system is ready
|
||||
if (paymentSystemReady) {
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await moneroPayService.processBatchPayouts();
|
||||
} catch (error) {
|
||||
console.error('Error in batch payout processing:', error);
|
||||
}
|
||||
}, 300000); // Every 5 minutes
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
startServer();
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
-- Wowgue Database Schema
|
||||
-- PostgreSQL migration script
|
||||
|
||||
-- Drop existing tables if they exist (for clean setup)
|
||||
DROP TABLE IF EXISTS payouts CASCADE;
|
||||
DROP TABLE IF EXISTS payments CASCADE;
|
||||
DROP TABLE IF EXISTS games CASCADE;
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
|
||||
-- Create users table
|
||||
-- Tracks user accounts, credits, and statistics
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
socket_id VARCHAR(255),
|
||||
ip_address INET,
|
||||
username VARCHAR(50),
|
||||
payout_address VARCHAR(95), -- Monero/Wownero address for payouts
|
||||
credits INTEGER DEFAULT 0, -- For PAID_CREDITS mode
|
||||
total_games_played INTEGER DEFAULT 0,
|
||||
total_games_won INTEGER DEFAULT 0,
|
||||
total_amount_paid BIGINT DEFAULT 0, -- In atomic units
|
||||
total_amount_won BIGINT DEFAULT 0, -- In atomic units
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index on socket_id for quick lookups
|
||||
CREATE INDEX idx_users_socket_id ON users(socket_id);
|
||||
CREATE INDEX idx_users_ip_address ON users(ip_address);
|
||||
|
||||
-- Create games table
|
||||
-- Tracks individual game sessions and outcomes
|
||||
CREATE TABLE games (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
socket_id VARCHAR(255),
|
||||
game_mode VARCHAR(20) NOT NULL, -- PAID_SINGLE, PAID_CREDITS, FREE
|
||||
payment_id INTEGER, -- Reference to payment (for PAID_SINGLE)
|
||||
start_block_height INTEGER,
|
||||
end_block_height INTEGER,
|
||||
status VARCHAR(20) DEFAULT 'waiting', -- waiting, active, won, lost, expired
|
||||
outcome VARCHAR(20), -- escaped, caught_by_monster, expired, treasure_found
|
||||
treasure_found BOOLEAN DEFAULT FALSE,
|
||||
dungeon_seed VARCHAR(50),
|
||||
moves_made INTEGER DEFAULT 0,
|
||||
duration_seconds INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for games
|
||||
CREATE INDEX idx_games_user_id ON games(user_id);
|
||||
CREATE INDEX idx_games_socket_id ON games(socket_id);
|
||||
CREATE INDEX idx_games_status ON games(status);
|
||||
CREATE INDEX idx_games_start_block ON games(start_block_height);
|
||||
|
||||
-- Create payments table
|
||||
-- Tracks incoming payments from players
|
||||
CREATE TABLE payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
socket_id VARCHAR(255),
|
||||
subaddress VARCHAR(95) NOT NULL, -- MoneroPay subaddress
|
||||
expected_amount BIGINT NOT NULL, -- In atomic units
|
||||
received_amount BIGINT DEFAULT 0, -- In atomic units
|
||||
payment_type VARCHAR(20) NOT NULL, -- single_game, credits_package
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, confirmed, expired
|
||||
description TEXT,
|
||||
tx_hash VARCHAR(64),
|
||||
block_height INTEGER,
|
||||
confirmations INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
confirmed_at TIMESTAMP,
|
||||
expires_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP + INTERVAL '2 hours')
|
||||
);
|
||||
|
||||
-- Create indexes for payments
|
||||
CREATE INDEX idx_payments_subaddress ON payments(subaddress);
|
||||
CREATE INDEX idx_payments_user_id ON payments(user_id);
|
||||
CREATE INDEX idx_payments_status ON payments(status);
|
||||
CREATE INDEX idx_payments_tx_hash ON payments(tx_hash);
|
||||
|
||||
-- Create payouts table
|
||||
-- Tracks outgoing payments to winners
|
||||
CREATE TABLE payouts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
game_id INTEGER REFERENCES games(id),
|
||||
payout_address VARCHAR(95) NOT NULL,
|
||||
amount BIGINT NOT NULL, -- In atomic units
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending, batched, completed, failed
|
||||
batch_id VARCHAR(50), -- Groups payouts that are sent together
|
||||
tx_hash VARCHAR(64),
|
||||
fee BIGINT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes for payouts
|
||||
CREATE INDEX idx_payouts_user_id ON payouts(user_id);
|
||||
CREATE INDEX idx_payouts_game_id ON payouts(game_id);
|
||||
CREATE INDEX idx_payouts_status ON payouts(status);
|
||||
CREATE INDEX idx_payouts_batch_id ON payouts(batch_id);
|
||||
|
||||
-- Create triggers to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Create function to get user statistics
|
||||
CREATE OR REPLACE FUNCTION get_user_stats(user_socket_id VARCHAR(255))
|
||||
RETURNS TABLE (
|
||||
total_games INTEGER,
|
||||
games_won INTEGER,
|
||||
win_rate DECIMAL(5,2),
|
||||
total_paid BIGINT,
|
||||
total_won BIGINT,
|
||||
net_profit BIGINT,
|
||||
credits_remaining INTEGER
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
u.total_games_played,
|
||||
u.total_games_won,
|
||||
CASE
|
||||
WHEN u.total_games_played > 0
|
||||
THEN ROUND((u.total_games_won::DECIMAL / u.total_games_played) * 100, 2)
|
||||
ELSE 0.00
|
||||
END as win_rate,
|
||||
u.total_amount_paid,
|
||||
u.total_amount_won,
|
||||
(u.total_amount_won - u.total_amount_paid) as net_profit,
|
||||
u.credits
|
||||
FROM users u
|
||||
WHERE u.socket_id = user_socket_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Insert default admin user for testing
|
||||
INSERT INTO users (socket_id, username, ip_address)
|
||||
VALUES ('admin', 'admin', '127.0.0.1');
|
||||
|
||||
-- Create view for active games
|
||||
CREATE VIEW active_games AS
|
||||
SELECT
|
||||
g.id,
|
||||
g.socket_id,
|
||||
u.username,
|
||||
g.game_mode,
|
||||
g.status,
|
||||
g.start_block_height,
|
||||
g.treasure_found,
|
||||
g.moves_made,
|
||||
g.created_at
|
||||
FROM games g
|
||||
JOIN users u ON g.user_id = u.id
|
||||
WHERE g.status IN ('waiting', 'active')
|
||||
ORDER BY g.created_at DESC;
|
||||
|
||||
-- Create view for pending payouts
|
||||
CREATE VIEW pending_payouts AS
|
||||
SELECT
|
||||
p.id,
|
||||
p.user_id,
|
||||
u.username,
|
||||
p.payout_address,
|
||||
p.amount,
|
||||
p.status,
|
||||
p.created_at,
|
||||
g.outcome as game_outcome
|
||||
FROM payouts p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
LEFT JOIN games g ON p.game_id = g.id
|
||||
WHERE p.status = 'pending'
|
||||
ORDER BY p.created_at ASC;
|
||||
+288
-13
@@ -7,11 +7,12 @@ const user = require('../db/user');
|
||||
const Game = require('../game/game');
|
||||
|
||||
class SocketHandlers {
|
||||
constructor(io, activeGames, broadcastManager, debugManager) {
|
||||
constructor(io, activeGames, broadcastManager, debugManager, gameModeManager = null) {
|
||||
this.io = io;
|
||||
this.activeGames = activeGames;
|
||||
this.broadcastManager = broadcastManager;
|
||||
this.debugManager = debugManager;
|
||||
this.gameModeManager = gameModeManager;
|
||||
this.playerMoveTimestamps = new Map();
|
||||
this.clientSocketMap = new Map();
|
||||
this.MOVE_COOLDOWN = 100; // Minimum 100ms between moves
|
||||
@@ -58,6 +59,11 @@ class SocketHandlers {
|
||||
socket.on('debug_ping', (data) => this.handleDebugPing(socket, data));
|
||||
socket.on('register_client', (data) => this.handleRegisterClient(socket, data));
|
||||
socket.on('auto_start', () => this.handleAutoStart(socket)); // New handler for start button
|
||||
|
||||
// Payment system handlers
|
||||
socket.on('request_payment', (data) => this.handlePaymentRequest(socket, data));
|
||||
socket.on('check_payment_status', (data) => this.handleCheckPaymentStatus(socket, data));
|
||||
socket.on('get_user_credits', () => this.handleGetUserCredits(socket));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,12 +76,19 @@ class SocketHandlers {
|
||||
|
||||
const command = msg.toLowerCase();
|
||||
|
||||
// Check for XMR/WOW address in the message
|
||||
const xmrAddressMatch = this.detectXMRAddress(msg);
|
||||
if (xmrAddressMatch) {
|
||||
this.handleAddressDetection(socket, xmrAddressMatch);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle game commands
|
||||
switch (command) {
|
||||
case 'hello':
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'help',
|
||||
'Welcome! Type "enter" to join the queue for the next block, or use the START button for immediate entry. ' +
|
||||
'You must send between 10-100 WOW to enter. The more you send, the more you can win.');
|
||||
'Paste your XMR/WOW address in chat to set your payout address.');
|
||||
return;
|
||||
|
||||
case 'enter':
|
||||
@@ -83,9 +96,25 @@ class SocketHandlers {
|
||||
return;
|
||||
|
||||
case 'cancel':
|
||||
// Check if this is for address confirmation
|
||||
if (this.pendingAddresses && this.pendingAddresses.has(socket.id)) {
|
||||
this.handleAddressConfirmation(socket, false);
|
||||
return;
|
||||
}
|
||||
// Otherwise handle queue cancellation
|
||||
this.handleCancelEntry(socket);
|
||||
return;
|
||||
|
||||
case 'confirm':
|
||||
this.handleAddressConfirmation(socket, true);
|
||||
return;
|
||||
|
||||
case 'address':
|
||||
case 'payout':
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
|
||||
'Please paste your XMR/WOW address directly in chat. The system will automatically detect and confirm it.');
|
||||
return;
|
||||
|
||||
default:
|
||||
// Broadcast as chat message to all clients
|
||||
this.handleChatBroadcast(socket, msg);
|
||||
@@ -184,9 +213,9 @@ class SocketHandlers {
|
||||
/**
|
||||
* Handle auto start request (start button)
|
||||
*/
|
||||
handleAutoStart(socket) {
|
||||
async handleAutoStart(socket) {
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`Player ${socket.id} requested auto-start via start button - STARTING IMMEDIATELY`);
|
||||
console.log(`Player ${socket.id} requested auto-start via start button`);
|
||||
}
|
||||
|
||||
const currentUser = this.getUserBySocket(socket.id);
|
||||
@@ -201,6 +230,30 @@ class SocketHandlers {
|
||||
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) {
|
||||
@@ -231,6 +284,16 @@ class SocketHandlers {
|
||||
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`);
|
||||
}
|
||||
@@ -347,7 +410,7 @@ class SocketHandlers {
|
||||
/**
|
||||
* Handle game over scenarios
|
||||
*/
|
||||
handleGameOver(socket, game, status, reason, message, score = 0) {
|
||||
async handleGameOver(socket, game, status, reason, message, score = 0) {
|
||||
game.gameState = status;
|
||||
const gameStats = {
|
||||
score: score,
|
||||
@@ -356,11 +419,31 @@ class SocketHandlers {
|
||||
};
|
||||
game.endGame(status, gameStats);
|
||||
|
||||
// Process game completion with payment system
|
||||
let payoutInfo = null;
|
||||
if (this.gameModeManager) {
|
||||
try {
|
||||
payoutInfo = await this.gameModeManager.completeGame(
|
||||
socket.id,
|
||||
game.id,
|
||||
status === 'won',
|
||||
game.player.hasTreasure || false
|
||||
);
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING && payoutInfo) {
|
||||
console.log(`💰 Payout processed for ${socket.id}:`, payoutInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing game completion:', error);
|
||||
}
|
||||
}
|
||||
|
||||
this.io.to(socket.id).emit('game_over', {
|
||||
status: status,
|
||||
reason: reason,
|
||||
message: message,
|
||||
score: score
|
||||
score: score,
|
||||
payout: payoutInfo
|
||||
});
|
||||
|
||||
this.activeGames.delete(socket.id);
|
||||
@@ -561,16 +644,208 @@ class SocketHandlers {
|
||||
}
|
||||
|
||||
game.gameState = 'lost';
|
||||
this.io.to(socketId).emit('game_over', {
|
||||
status: 'lost',
|
||||
reason: 'timeout',
|
||||
message: 'You didn\'t escape before the block time limit!'
|
||||
});
|
||||
|
||||
this.activeGames.delete(socketId);
|
||||
this.handleGameOver(socket, game, 'lost', 'timeout', 'You didn\'t escape before the block time limit!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ====== PAYMENT SYSTEM HANDLERS ======
|
||||
|
||||
/**
|
||||
* Handle payment request from client
|
||||
*/
|
||||
async handlePaymentRequest(socket, data) {
|
||||
if (!this.gameModeManager) {
|
||||
this.io.to(socket.id).emit('payment_error', {
|
||||
error: 'Payment system not available'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { gameMode } = data;
|
||||
const payment = await this.gameModeManager.moneroPayService.createPaymentRequest(socket.id, gameMode);
|
||||
|
||||
this.io.to(socket.id).emit('payment_created', {
|
||||
paymentId: payment.id,
|
||||
address: payment.address,
|
||||
amount: payment.amount,
|
||||
confirmations: payment.confirmations,
|
||||
gameMode: gameMode
|
||||
});
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`💳 Payment request created for ${socket.id}: ${payment.amount} XMR`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating payment request:', error);
|
||||
this.io.to(socket.id).emit('payment_error', {
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle payment status check from client
|
||||
*/
|
||||
async handleCheckPaymentStatus(socket, data) {
|
||||
if (!this.gameModeManager) {
|
||||
this.io.to(socket.id).emit('payment_error', {
|
||||
error: 'Payment system not available'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { paymentId } = data;
|
||||
const status = await this.gameModeManager.moneroPayService.checkPaymentStatus(paymentId);
|
||||
|
||||
this.io.to(socket.id).emit('payment_status', status);
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`💳 Payment status checked for ${socket.id}: ${status.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking payment status:', error);
|
||||
this.io.to(socket.id).emit('payment_error', {
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle get user credits request
|
||||
*/
|
||||
async handleGetUserCredits(socket) {
|
||||
if (!this.gameModeManager) {
|
||||
this.io.to(socket.id).emit('credits_info', { credits: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const credits = await this.gameModeManager.getUserCredits(socket.id);
|
||||
|
||||
this.io.to(socket.id).emit('credits_info', { credits });
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`💰 Credits checked for ${socket.id}: ${credits}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting user credits:', error);
|
||||
this.io.to(socket.id).emit('credits_info', { credits: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// ====== ADDRESS DETECTION SYSTEM ======
|
||||
|
||||
/**
|
||||
* Detect XMR/WOW address in chat message using regex
|
||||
*/
|
||||
detectXMRAddress(message) {
|
||||
// XMR mainnet addresses start with 4, 8, A, or B and are 95 characters
|
||||
// WOW addresses start with W and are 97 characters
|
||||
const xmrRegex = /\b[48AB][1-9A-HJ-NP-Za-km-z]{94}\b/;
|
||||
const wowRegex = /\bW[1-9A-HJ-NP-Za-km-z]{96}\b/;
|
||||
|
||||
const xmrMatch = message.match(xmrRegex);
|
||||
const wowMatch = message.match(wowRegex);
|
||||
|
||||
if (xmrMatch) {
|
||||
return { address: xmrMatch[0], type: 'XMR' };
|
||||
}
|
||||
if (wowMatch) {
|
||||
return { address: wowMatch[0], type: 'WOW' };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle detected address and confirm with user
|
||||
*/
|
||||
async handleAddressDetection(socket, addressMatch) {
|
||||
const { address, type } = addressMatch;
|
||||
|
||||
// Store pending address for confirmation
|
||||
if (!this.pendingAddresses) {
|
||||
this.pendingAddresses = new Map();
|
||||
}
|
||||
|
||||
this.pendingAddresses.set(socket.id, {
|
||||
address: address,
|
||||
type: type,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Send confirmation request to user
|
||||
this.io.to(socket.id).emit('address_detected', {
|
||||
address: address,
|
||||
type: type,
|
||||
message: `⚠️ DETECTED ${type} ADDRESS: ${address}\n\n` +
|
||||
`⚠️ WARNING: Please verify this is YOUR address before confirming!\n` +
|
||||
`⚠️ Clipboard viruses can change addresses - double check carefully!\n\n` +
|
||||
`Type "confirm" to set this as your payout address, or "cancel" to reject.`,
|
||||
confirmationRequired: true
|
||||
});
|
||||
|
||||
// Auto-expire pending address after 5 minutes
|
||||
setTimeout(() => {
|
||||
if (this.pendingAddresses && this.pendingAddresses.has(socket.id)) {
|
||||
this.pendingAddresses.delete(socket.id);
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
|
||||
'Address confirmation expired. Please paste your address again if needed.');
|
||||
}
|
||||
}, 300000); // 5 minutes
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`💰 Address detected for ${socket.id}: ${type} ${address}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle address confirmation
|
||||
*/
|
||||
async handleAddressConfirmation(socket, confirmed) {
|
||||
if (!this.pendingAddresses || !this.pendingAddresses.has(socket.id)) {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
|
||||
'No pending address to confirm. Please paste your address first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingAddress = this.pendingAddresses.get(socket.id);
|
||||
this.pendingAddresses.delete(socket.id);
|
||||
|
||||
if (!confirmed) {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
|
||||
'Address rejected. Paste a new address if you want to set a payout address.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save address to payment system
|
||||
if (this.gameModeManager) {
|
||||
try {
|
||||
await this.gameModeManager.setUserPayoutAddress(socket.id, pendingAddress.address);
|
||||
|
||||
this.io.to(socket.id).emit('address_confirmed', {
|
||||
address: pendingAddress.address,
|
||||
type: pendingAddress.type,
|
||||
message: `✅ Payout address confirmed: ${pendingAddress.address}\n\n` +
|
||||
`Future winnings will be sent to this address.`
|
||||
});
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`✅ Address confirmed for ${socket.id}: ${pendingAddress.address}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving payout address:', error);
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
|
||||
'Error saving address. Please try again.');
|
||||
}
|
||||
} else {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
|
||||
`Address noted: ${pendingAddress.address} (Payment system not active)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SocketHandlers;
|
||||
|
||||
Generated
+2328
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -27,10 +27,13 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.9.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.17.1",
|
||||
"pg": "^8.15.5",
|
||||
"node-pg-migrate": "^7.0.2",
|
||||
"request": "^2.88.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-client": "^4.8.1"
|
||||
"socket.io-client": "^4.8.1",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* MoneroPay Integration Service
|
||||
* Handles payment requests, monitoring, and payouts
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
class MoneroPayService {
|
||||
constructor(databaseManager) {
|
||||
this.db = databaseManager;
|
||||
this.endpoint = process.env.MONEROPAY_ENDPOINT;
|
||||
this.callbackBaseUrl = process.env.MONEROPAY_CALLBACK_BASE_URL;
|
||||
this.initialized = false;
|
||||
|
||||
if (!this.endpoint) {
|
||||
throw new Error('MONEROPAY_ENDPOINT not configured');
|
||||
}
|
||||
|
||||
console.log(`💰 MoneroPay service initialized: ${this.endpoint}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize MoneroPay service
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
// Test connection to MoneroPay endpoint
|
||||
const response = await axios.get(`${this.endpoint}/health`, { timeout: 5000 });
|
||||
|
||||
if (response.status === 200) {
|
||||
this.initialized = true;
|
||||
console.log('✅ MoneroPay service connection verified');
|
||||
return true;
|
||||
} else {
|
||||
console.log('❌ MoneroPay service health check failed');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ MoneroPay service connection failed: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payment request for a user
|
||||
*/
|
||||
async createPaymentRequest(userId, socketId, paymentType, amount, description) {
|
||||
try {
|
||||
const callbackUrl = `${this.callbackBaseUrl}/${socketId}`;
|
||||
|
||||
const response = await axios.post(`${this.endpoint}/receive`, {
|
||||
amount: parseInt(amount),
|
||||
description: description || `Wowgue ${paymentType}`,
|
||||
callback_url: callbackUrl
|
||||
});
|
||||
|
||||
const { address, amount: expectedAmount, created_at } = response.data;
|
||||
|
||||
// Store payment request in database
|
||||
const paymentRecord = await this.db.query(`
|
||||
INSERT INTO payments (
|
||||
user_id, socket_id, subaddress, expected_amount,
|
||||
payment_type, description, status
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, 'pending')
|
||||
RETURNING *
|
||||
`, [userId, socketId, address, expectedAmount, paymentType, description]);
|
||||
|
||||
console.log(`💳 Payment request created: ${address} for ${expectedAmount} atomic units`);
|
||||
|
||||
return {
|
||||
id: paymentRecord.rows[0].id,
|
||||
address,
|
||||
amount: expectedAmount,
|
||||
paymentType,
|
||||
description,
|
||||
expiresAt: paymentRecord.rows[0].expires_at
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create payment request:', error.message);
|
||||
throw new Error('Failed to create payment request');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check payment status for a subaddress
|
||||
*/
|
||||
async checkPaymentStatus(subaddress) {
|
||||
try {
|
||||
const response = await axios.get(`${this.endpoint}/receive/${subaddress}`);
|
||||
const data = response.data;
|
||||
|
||||
return {
|
||||
complete: data.complete,
|
||||
expectedAmount: data.amount.expected,
|
||||
coveredAmount: data.amount.covered.total,
|
||||
unlockedAmount: data.amount.covered.unlocked,
|
||||
transactions: data.transactions || []
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to check payment status for ${subaddress}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process payment confirmation callback
|
||||
*/
|
||||
async processPaymentCallback(socketId, paymentData) {
|
||||
try {
|
||||
const payment = await this.db.query(`
|
||||
SELECT * FROM payments
|
||||
WHERE socket_id = $1 AND status = 'pending'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, [socketId]);
|
||||
|
||||
if (payment.rows.length === 0) {
|
||||
console.warn(`⚠️ No pending payment found for socket ${socketId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const paymentRecord = payment.rows[0];
|
||||
|
||||
// Update payment status
|
||||
await this.db.query(`
|
||||
UPDATE payments
|
||||
SET status = 'confirmed',
|
||||
received_amount = $1,
|
||||
confirmed_at = NOW(),
|
||||
tx_hash = $2,
|
||||
block_height = $3,
|
||||
confirmations = $4
|
||||
WHERE id = $5
|
||||
`, [
|
||||
paymentData.amount,
|
||||
paymentData.tx_hash,
|
||||
paymentData.height,
|
||||
paymentData.confirmations,
|
||||
paymentRecord.id
|
||||
]);
|
||||
|
||||
// Handle different payment types
|
||||
if (paymentRecord.payment_type === 'credits_package') {
|
||||
// Add 10 credits to user
|
||||
await this.db.query(`
|
||||
UPDATE users
|
||||
SET credits = credits + 10,
|
||||
total_amount_paid = total_amount_paid + $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
`, [paymentData.amount, paymentRecord.user_id]);
|
||||
|
||||
console.log(`💰 Added 10 credits to user ${paymentRecord.user_id}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Payment confirmed: ${paymentData.tx_hash} for ${paymentData.amount} atomic units`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to process payment callback:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payout for game winner
|
||||
*/
|
||||
async createPayout(userId, gameId, payoutAddress, amount) {
|
||||
try {
|
||||
const result = await this.db.query(`
|
||||
INSERT INTO payouts (
|
||||
user_id, game_id, payout_address, amount, status
|
||||
) VALUES ($1, $2, $3, $4, 'pending')
|
||||
RETURNING *
|
||||
`, [userId, gameId, payoutAddress, amount]);
|
||||
|
||||
console.log(`💸 Payout created: ${amount} atomic units to ${payoutAddress}`);
|
||||
return result.rows[0];
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create payout:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pending payouts in batches
|
||||
*/
|
||||
async processPendingPayouts() {
|
||||
try {
|
||||
const maxBatchSize = parseInt(process.env.MAX_PAYOUT_BATCH_SIZE) || 50;
|
||||
|
||||
// Get pending payouts
|
||||
const payouts = await this.db.query(`
|
||||
SELECT * FROM payouts
|
||||
WHERE status = 'pending'
|
||||
ORDER BY created_at ASC
|
||||
LIMIT $1
|
||||
`, [maxBatchSize]);
|
||||
|
||||
if (payouts.rows.length === 0) {
|
||||
return { processed: 0, message: 'No pending payouts' };
|
||||
}
|
||||
|
||||
// Group payouts by address to optimize transaction fees
|
||||
const payoutGroups = this.groupPayoutsByAddress(payouts.rows);
|
||||
const batchId = uuidv4();
|
||||
|
||||
// Prepare destinations for MoneroPay
|
||||
const destinations = payoutGroups.map(group => ({
|
||||
address: group.address,
|
||||
amount: group.totalAmount
|
||||
}));
|
||||
|
||||
// Send batch transfer
|
||||
const transferResponse = await axios.post(`${this.endpoint}/transfer`, {
|
||||
destinations
|
||||
});
|
||||
|
||||
const { tx_hash_list, fee, amount: totalAmount } = transferResponse.data;
|
||||
|
||||
// Update payout records
|
||||
const payoutIds = payouts.rows.map(p => p.id);
|
||||
await this.db.query(`
|
||||
UPDATE payouts
|
||||
SET status = 'completed',
|
||||
batch_id = $1,
|
||||
tx_hash = $2,
|
||||
fee = $3,
|
||||
processed_at = NOW()
|
||||
WHERE id = ANY($4)
|
||||
`, [batchId, tx_hash_list[0], fee, payoutIds]);
|
||||
|
||||
// Update user totals
|
||||
for (const payout of payouts.rows) {
|
||||
await this.db.query(`
|
||||
UPDATE users
|
||||
SET total_amount_won = total_amount_won + $1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
`, [payout.amount, payout.user_id]);
|
||||
}
|
||||
|
||||
console.log(`✅ Processed ${payouts.rows.length} payouts in batch ${batchId}`);
|
||||
console.log(`💸 Total amount: ${totalAmount}, Fee: ${fee}, TX: ${tx_hash_list.join(', ')}`);
|
||||
|
||||
return {
|
||||
processed: payouts.rows.length,
|
||||
batchId,
|
||||
totalAmount,
|
||||
fee,
|
||||
transactions: tx_hash_list
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to process payouts:', error.message);
|
||||
|
||||
// Mark failed payouts
|
||||
await this.db.query(`
|
||||
UPDATE payouts
|
||||
SET status = 'failed'
|
||||
WHERE status = 'pending' AND created_at < NOW() - INTERVAL '1 hour'
|
||||
`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group payouts by address to minimize transaction fees
|
||||
*/
|
||||
groupPayoutsByAddress(payouts) {
|
||||
const groups = {};
|
||||
|
||||
for (const payout of payouts) {
|
||||
if (!groups[payout.payout_address]) {
|
||||
groups[payout.payout_address] = {
|
||||
address: payout.payout_address,
|
||||
totalAmount: 0,
|
||||
payouts: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[payout.payout_address].totalAmount += parseInt(payout.amount);
|
||||
groups[payout.payout_address].payouts.push(payout);
|
||||
}
|
||||
|
||||
return Object.values(groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get wallet balance
|
||||
*/
|
||||
async getBalance() {
|
||||
try {
|
||||
const response = await axios.get(`${this.endpoint}/balance`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to get wallet balance:', error.message);
|
||||
return { total: 0, unlocked: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup expired payment requests
|
||||
*/
|
||||
async cleanupExpiredPayments() {
|
||||
try {
|
||||
const result = await this.db.query(`
|
||||
UPDATE payments
|
||||
SET status = 'expired'
|
||||
WHERE status = 'pending' AND expires_at < NOW()
|
||||
RETURNING id
|
||||
`);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
console.log(`🧹 Cleaned up ${result.rows.length} expired payments`);
|
||||
}
|
||||
|
||||
return result.rows.length;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to cleanup expired payments:', error.message);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MoneroPayService;
|
||||
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Enhanced RPC Service with Failover Support
|
||||
* Handles blockchain RPC calls with automatic failover and monitoring
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
const CONSOLE_LOGGING = process.env.NODE_ENV === 'debug' || process.env.NODE_ENV === 'development';
|
||||
|
||||
class RPCService {
|
||||
constructor() {
|
||||
this.primaryEndpoint = process.env.PRIMARY_RPC_ENDPOINT || 'http://jw-nodes:18081';
|
||||
this.fallbackEndpoint = process.env.FALLBACK_RPC_ENDPOINT || 'http://jw-nodes:18081';
|
||||
this.currentEndpoint = this.primaryEndpoint;
|
||||
this.failoverActive = false;
|
||||
this.lastBlockHeight = 0;
|
||||
this.consecutiveFailures = 0;
|
||||
this.maxFailures = 3;
|
||||
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.log(`🔗 RPC Service initialized`);
|
||||
console.log(`Primary: ${this.primaryEndpoint}`);
|
||||
console.log(`Fallback: ${this.fallbackEndpoint}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make RPC call with automatic failover
|
||||
*/
|
||||
async makeRPCCall(method, params = {}) {
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id: '0',
|
||||
method: method,
|
||||
params: params
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${this.currentEndpoint}/json_rpc`, request, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (response.data.error) {
|
||||
throw new Error(`RPC Error: ${response.data.error.message}`);
|
||||
}
|
||||
|
||||
// Reset failure counter on success
|
||||
this.consecutiveFailures = 0;
|
||||
|
||||
// Switch back to primary if we were using fallback
|
||||
if (this.failoverActive && this.currentEndpoint !== this.primaryEndpoint) {
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.log('🔄 Switching back to primary RPC endpoint');
|
||||
}
|
||||
this.currentEndpoint = this.primaryEndpoint;
|
||||
this.failoverActive = false;
|
||||
}
|
||||
|
||||
return response.data.result;
|
||||
|
||||
} catch (error) {
|
||||
this.consecutiveFailures++;
|
||||
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.error(`❌ RPC call failed (${this.consecutiveFailures}/${this.maxFailures}):`, error.message);
|
||||
}
|
||||
|
||||
// Try failover if we haven't exceeded max failures
|
||||
if (this.consecutiveFailures >= this.maxFailures && !this.failoverActive) {
|
||||
return await this.tryFailover(method, params);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt failover to backup endpoint
|
||||
*/
|
||||
async tryFailover(method, params) {
|
||||
if (this.currentEndpoint === this.fallbackEndpoint) {
|
||||
throw new Error('Both primary and fallback RPC endpoints failed');
|
||||
}
|
||||
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.log('🔄 Attempting RPC failover to backup endpoint');
|
||||
}
|
||||
|
||||
this.currentEndpoint = this.fallbackEndpoint;
|
||||
this.failoverActive = true;
|
||||
this.consecutiveFailures = 0;
|
||||
|
||||
try {
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
id: '0',
|
||||
method: method,
|
||||
params: params
|
||||
};
|
||||
|
||||
const response = await axios.post(`${this.currentEndpoint}/json_rpc`, request, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (response.data.error) {
|
||||
throw new Error(`RPC Error: ${response.data.error.message}`);
|
||||
}
|
||||
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.log('✅ Failover successful');
|
||||
}
|
||||
|
||||
return response.data.result;
|
||||
|
||||
} catch (error) {
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.error('❌ Failover also failed:', error.message);
|
||||
}
|
||||
throw new Error('Both primary and fallback RPC endpoints failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current block height
|
||||
*/
|
||||
async getBlockHeight() {
|
||||
try {
|
||||
const result = await this.makeRPCCall('get_block_count');
|
||||
const blockHeight = result.count;
|
||||
|
||||
if (blockHeight !== this.lastBlockHeight) {
|
||||
this.lastBlockHeight = blockHeight;
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.log(`📊 Block height updated: ${blockHeight}`);
|
||||
}
|
||||
}
|
||||
|
||||
return blockHeight;
|
||||
} catch (error) {
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.error('❌ Failed to get block height:', error.message);
|
||||
}
|
||||
return this.lastBlockHeight; // Return last known height on failure
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block information by height
|
||||
*/
|
||||
async getBlockByHeight(height) {
|
||||
try {
|
||||
return await this.makeRPCCall('get_block', { height });
|
||||
} catch (error) {
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.error(`❌ Failed to get block ${height}:`, error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network info
|
||||
*/
|
||||
async getNetworkInfo() {
|
||||
try {
|
||||
const [blockCount, networkInfo] = await Promise.all([
|
||||
this.makeRPCCall('get_block_count'),
|
||||
this.makeRPCCall('get_info')
|
||||
]);
|
||||
|
||||
return {
|
||||
blockHeight: blockCount.count,
|
||||
difficulty: networkInfo.difficulty,
|
||||
hashRate: networkInfo.difficulty / 120, // Approximate hash rate
|
||||
networkType: networkInfo.mainnet ? 'mainnet' : 'testnet',
|
||||
status: networkInfo.status
|
||||
};
|
||||
} catch (error) {
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.error('❌ Failed to get network info:', error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check for RPC endpoints
|
||||
*/
|
||||
async healthCheck() {
|
||||
const checks = {
|
||||
primary: { endpoint: this.primaryEndpoint, status: 'unknown', responseTime: 0 },
|
||||
fallback: { endpoint: this.fallbackEndpoint, status: 'unknown', responseTime: 0 },
|
||||
current: this.currentEndpoint,
|
||||
failoverActive: this.failoverActive,
|
||||
consecutiveFailures: this.consecutiveFailures
|
||||
};
|
||||
|
||||
// Test primary endpoint
|
||||
try {
|
||||
const start = Date.now();
|
||||
await axios.post(`${this.primaryEndpoint}/json_rpc`, {
|
||||
jsonrpc: '2.0',
|
||||
id: '0',
|
||||
method: 'get_block_count'
|
||||
}, { timeout: 5000 });
|
||||
checks.primary.status = 'healthy';
|
||||
checks.primary.responseTime = Date.now() - start;
|
||||
} catch (error) {
|
||||
checks.primary.status = 'unhealthy';
|
||||
checks.primary.error = error.message;
|
||||
}
|
||||
|
||||
// Test fallback endpoint (if different)
|
||||
if (this.fallbackEndpoint !== this.primaryEndpoint) {
|
||||
try {
|
||||
const start = Date.now();
|
||||
await axios.post(`${this.fallbackEndpoint}/json_rpc`, {
|
||||
jsonrpc: '2.0',
|
||||
id: '0',
|
||||
method: 'get_block_count'
|
||||
}, { timeout: 5000 });
|
||||
checks.fallback.status = 'healthy';
|
||||
checks.fallback.responseTime = Date.now() - start;
|
||||
} catch (error) {
|
||||
checks.fallback.status = 'unhealthy';
|
||||
checks.fallback.error = error.message;
|
||||
}
|
||||
} else {
|
||||
checks.fallback = checks.primary; // Same endpoint
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current endpoint status
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
currentEndpoint: this.currentEndpoint,
|
||||
failoverActive: this.failoverActive,
|
||||
consecutiveFailures: this.consecutiveFailures,
|
||||
lastBlockHeight: this.lastBlockHeight
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy methods for backward compatibility
|
||||
async daemonCall(method, params, callback) {
|
||||
try {
|
||||
const result = await this.makeRPCCall(method, params);
|
||||
if (callback) callback(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (callback) callback(null);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getBlockHeightLegacy() {
|
||||
return this.getBlockHeight();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RPCService;
|
||||
Reference in New Issue
Block a user