First (untested) payment and db setup

This commit is contained in:
jwinterm
2025-06-01 14:17:29 -04:00
parent f02e16272e
commit bd97d9d848
14 changed files with 4525 additions and 264 deletions
+39
View File
@@ -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
+162
View File
@@ -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;
+365
View File
@@ -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
View File
@@ -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();
+186
View File
@@ -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
View File
@@ -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;
+2328
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -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"
}
}
+331
View File
@@ -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;
+266
View File
@@ -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;