Get back into it

This commit is contained in:
jwinterm
2025-09-24 14:06:42 -04:00
parent bd97d9d848
commit 4ad063f735
16 changed files with 1751 additions and 328 deletions
+16 -15
View File
@@ -32,7 +32,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow
- Security warnings about clipboard viruses
- Two-step confirmation process for safety
### 💳 **MoneroPay Integration**
### 💳 **Wallet-RPC Integration**
- Subaddress generation for payments
- Real-time payment monitoring
- Automated payment confirmations
@@ -48,14 +48,14 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow
### **Backend Payment Infrastructure**
- **Database Manager**: PostgreSQL connection pooling, migrations, health checks
- **MoneroPay Service**: Payment requests, status monitoring, batch payout processing
- **Wallet RPC Service**: Direct wallet integration, payment monitoring, batch payout processing
- **RPC Service**: Blockchain communication with automatic failover between endpoints
- **Game Mode Manager**: Payment validation, credit management, user eligibility checking
### **Payment Database Schema**
- **Users Table**: User profiles, payout addresses, game statistics
- **Games Table**: Game records with outcomes, payments, and rewards
- **Payments Table**: MoneroPay payment tracking with status updates
- **Payments Table**: Direct wallet payment tracking with status updates
- **Payouts Table**: Batch payout management with transaction records
### **Address Detection System**
@@ -69,7 +69,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow
### Payment Management
- `POST /api/payment/create` - Create new payment request for single games
- `GET /api/payment/status/:paymentId` - Check payment status
- `POST /api/payment/callback` - MoneroPay webhook for payment confirmations
- `POST /api/payment/callback` - Wallet RPC webhook for payment confirmations
### User Management
- `GET /api/user/credits/:userId` - Check user's remaining credits
@@ -85,7 +85,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow
### Prerequisites
- Node.js 16+ with npm
- PostgreSQL 12+ database
- MoneroPay service access (optional, will fallback to FREE mode)
- Wallet-RPC service access (optional, will fallback to FREE mode)
### Installation Steps
@@ -103,7 +103,7 @@ Wowngeon is a web-based roguelike game that integrates with Monero (XMR) and Wow
3. **Environment Configuration**:
```bash
cp .env.example .env
# Edit .env with your database and MoneroPay credentials
# Edit .env with your database and Wallet-RPC credentials
```
4. **Database Setup**:
@@ -136,9 +136,10 @@ PAID_CREDITS_PRICE=0.05
# Database Configuration
DATABASE_URL=postgresql://username:password@localhost:5432/wowngeon
# MoneroPay Configuration
MONEROPAY_URL=https://api.moneropay.eu
MONEROPAY_API_KEY=your_api_key
# Wallet RPC Configuration
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570
WALLET_RPC_USER=your_rpc_user
WALLET_RPC_PASSWORD=your_rpc_password
# Crypto RPC Configuration
PRIMARY_RPC_URL=http://localhost:18081
@@ -149,7 +150,7 @@ FALLBACK_RPC_URL=http://backup-node:18081
### PAID_SINGLE Mode
1. User requests to play paid game
2. System generates MoneroPay payment request
2. System generates Wallet-RPC payment request
3. User completes payment (0.005 XMR default)
4. Game starts after payment confirmation
5. Automatic 2x or 3x payout on successful escape
@@ -195,14 +196,14 @@ Test suite covers:
### Security Considerations
- Database credentials should use environment variables
- MoneroPay API keys must be kept secure
- Wallet-RPC credentials must be kept secure
- RPC endpoints should be trusted nodes only
- Consider rate limiting on payment endpoints
### Monitoring & Logging
- Payment processing events logged for audit trails
- Database health checks with automatic failover
- MoneroPay webhook validation and processing
- Wallet-RPC webhook validation and processing
- Graceful degradation logs when payment systems unavailable
### Scalability Features
@@ -224,7 +225,7 @@ wowngeon/
│ ├── db/ # Database management
│ │ └── databaseManager.js # Connection pooling & migrations
│ ├── payments/ # Payment system
│ │ └── moneroPayService.js # MoneroPay integration
│ │ └── walletRPCService.js # Wallet-RPC integration
│ ├── rpc/ # Blockchain integration
│ │ └── rpcService.js # RPC with failover
│ ├── game/ # Game logic
@@ -243,7 +244,7 @@ wowngeon/
- Complete payment system infrastructure
- Three-tier game mode implementation
- PostgreSQL database with migration system
- MoneroPay integration with webhook processing
- Direct wallet-RPC integration with payment monitoring
- RPC failover system for blockchain reliability
- XMR/WOW address detection via chat regex
- Comprehensive test suite with 100% pass rate
@@ -251,7 +252,7 @@ wowngeon/
### 🔄 Next Steps
- Database connection testing with production credentials
- MoneroPay integration testing with live service
- Wallet-RPC integration testing with local wallet
- End-to-end payment flow validation
- Frontend payment UI testing and refinement
+109
View File
@@ -14,6 +14,8 @@ var Game = {
_inputEnabled: true,
_messageLog: [],
_maxLogMessages: 5,
_unconfirmedPayment: false,
_unconfirmedPaymentInfo: null,
init: function() {
// Update screen dimensions from options
@@ -147,6 +149,89 @@ var Game = {
}
},
// Legacy alias (some handlers referenced updateGame previously)
updateGame: function(data) {
return this.updateGameState(data);
},
// Handle game over event from server
endGame: function(data) {
try {
GameState.setGameActive(false);
// Decide which screen to draw
if (data && data.reason === 'monster') {
if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('monster');
} else if (data && data.reason === 'escaped') {
if (ScreenManager.drawWinScreen) ScreenManager.drawWinScreen(data.treasure || false);
} else if (data && data.reason === 'timeout') {
if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('timeout');
} else {
if (ScreenManager.drawLoseScreen) ScreenManager.drawLoseScreen('other');
}
// After short delay, allow user to return to title with Enter/start
const score = data && typeof data.score === 'number' ? data.score : 0;
const treasure = !!(data && data.treasure);
// Display score / treasure info overlay
if (ScreenManager && ScreenManager.drawCenteredText && DisplayManager.ensureDisplay()) {
const baseY = Math.floor(ScreenManager._screenHeight / 2) + 5;
ScreenManager.drawCenteredText(baseY, `Score: ${score}`);
if (treasure) {
ScreenManager.drawCenteredText(baseY + 1, 'You secured the treasure!');
}
}
// Debounce + delayed activation of restart
this._awaitingRestart = false;
const activateDelay = 800; // ms before we accept Enter
const autoReturnDelay = 10000; // auto return after 10s
// Show hint after activation delay
setTimeout(() => {
this._awaitingRestart = true;
if (ScreenManager && ScreenManager.drawCenteredText && DisplayManager.ensureDisplay()) {
const y = ScreenManager._screenHeight - 2;
const fullHint = 'Press Enter to return to title';
const shortHint = 'Press Enter';
const maxLen = ScreenManager._screenWidth - 2; // leave small margin
const hint = fullHint.length > maxLen ? shortHint : fullHint;
ScreenManager.drawCenteredText(y, hint);
}
}, activateDelay);
// Auto return timer
clearTimeout(this._autoReturnTimer);
this._autoReturnTimer = setTimeout(() => {
if (this._awaitingRestart) {
this._awaitingRestart = false;
if (ScreenManager && ScreenManager.drawWelcomeScreen) {
ScreenManager.drawWelcomeScreen();
}
}
}, autoReturnDelay);
// One-time listener setup
if (!this._restartListenerAttached) {
this._restartListenerAttached = true;
document.addEventListener('keydown', (e) => {
if (!this._awaitingRestart) return;
if (e.key === 'Enter') {
this._awaitingRestart = false;
clearTimeout(this._autoReturnTimer);
if (ScreenManager && ScreenManager.drawWelcomeScreen) {
ScreenManager.drawWelcomeScreen();
}
const chatInput = document.getElementById('chatInput');
if (chatInput) chatInput.focus();
}
});
}
} catch (err) {
console.error('Error handling endGame:', err);
}
},
clearDisplay: function() {
DisplayManager.clearDisplay();
},
@@ -173,6 +258,19 @@ var Game = {
drawWaitingScreen: function() {
ScreenManager.drawWaitingScreen();
// Overlay unconfirmed payment badge if present
if (this._unconfirmedPayment && typeof ScreenManager !== 'undefined' && DisplayManager.ensureDisplay()) {
const display = DisplayManager.getDisplay();
const badgeText = 'Unconfirmed (mempool)';
const xStart = Math.max(1, Math.floor((this._screenWidth - badgeText.length) / 2));
const y = 1; // top line inside border
for (let i = 0; i < badgeText.length; i++) {
display.draw(xStart + i, y, badgeText[i], 'rgba(180,230,255,0.9)', 'transparent');
}
// Small pulsing dot
const pulse = 0.4 + Math.sin(Date.now()/300)*0.4;
display.draw(xStart - 2, y, '●', `rgba(120,200,255,${pulse})`, 'transparent');
}
},
stopWaitingScreen: function() {
@@ -239,6 +337,17 @@ var Game = {
}
};
// Payment state helpers
Game._pendingPaymentDetected = function(info) {
this._unconfirmedPayment = true;
this._unconfirmedPaymentInfo = info || {};
};
Game._pendingPaymentConfirmed = function() {
this._unconfirmedPayment = false;
this._unconfirmedPaymentInfo = null;
};
// Debug utility functions (keep as-is for compatibility)
const GameDebug = {
log: function(message) {
+34 -7
View File
@@ -210,13 +210,40 @@ var WaitingScreenAnimator = {
const bg = ["transparent", "transparent"];
display.draw(anim.playerX, anim.playerY, ch, fg, bg);
// Light footsteps behind player
if (anim.phase === 'enter' || anim.phase === 'escape') {
const footstepX = anim.playerX - 1;
if (footstepX >= roomStartX && footstepX <= roomEndX) {
const footstepGlow = Math.max(0, Math.sin(time / 400) * 0.2);
const floorTile = GameTiles.getFloorTile();
display.draw(footstepX, anim.playerY, floorTile, `rgba(150, 200, 255, ${footstepGlow})`, "transparent");
// Bright ethereal footsteps / halo around player to indicate "primed" status
if (anim.phase === 'enter' || anim.phase === 'escape' || anim.phase === 'chase') {
const ringPulse = 0.35 + Math.sin(time / 250) * 0.25; // stronger, faster pulse
const innerPulse = 0.15 + Math.sin(time / 180 + 1.2) * 0.15;
const ringColor = (alpha) => `rgba(180, 230, 255, ${alpha})`;
const floorTile = GameTiles.getFloorTile();
// 8-way surrounding ring
const ringOffsets = [
{dx: -1, dy: 0}, {dx: 1, dy: 0}, {dx: 0, dy: -1}, {dx: 0, dy: 1}, // cardinal
{dx: -1, dy: -1}, {dx: 1, dy: -1}, {dx: -1, dy: 1}, {dx: 1, dy: 1} // diagonal
];
for (let i = 0; i < ringOffsets.length; i++) {
const rx = anim.playerX + ringOffsets[i].dx;
const ry = anim.playerY + ringOffsets[i].dy;
if (rx >= roomStartX && rx <= roomEndX && ry >= roomStartY && ry <= roomEndY) {
// subtle variation per tile
const phaseShift = (i / ringOffsets.length) * Math.PI * 2;
const alpha = (ringPulse * 0.6) + Math.sin(time / 300 + phaseShift) * 0.15;
display.draw(rx, ry, floorTile, ringColor(Math.max(0, alpha)), 'transparent');
}
}
// Trailing footprint path (a few tiles behind)
for (let trail = 1; trail <= 3; trail++) {
const tx = anim.playerX - trail;
const ty = anim.playerY;
if (tx >= roomStartX && tx <= roomEndX && ty >= roomStartY && ty <= roomEndY) {
const decay = Math.max(0, 0.5 - trail * 0.15);
const alpha = innerPulse * decay;
if (alpha > 0.03) {
display.draw(tx, ty, floorTile, `rgba(120, 200, 255, ${alpha})`, 'transparent');
}
}
}
}
}
+6 -12
View File
@@ -33,19 +33,13 @@ const InputHandler = {
if (msg.toLowerCase() === 'enter') {
socket.emit('chat message', msg);
// Show appropriate screen based on current state
const isDebugMode = window.location.hostname === 'localhost' ||
window.location.hostname === '127.0.0.1' ||
window.location.protocol === 'file:';
// Don't automatically switch to waiting screen - let the server control screen state
// The server will send appropriate events (payment_created, waiting_status, etc.)
// to control what the user sees
if (isDebugMode || ScreenManager.canEnterGame()) {
if (typeof ScreenManager !== 'undefined' && ScreenManager.drawWaitingScreen) {
ScreenManager.drawWaitingScreen();
}
} else {
$('#messages').append($('<li style="color: #ff0;">').text("* You have been added to the queue! Game will start after next block."));
UI.scrollChat();
}
// Add visual feedback that request was sent
$('#messages').append($('<li style="color: #0f0;">').text("🔑 Processing game entry request..."));
UI.scrollChat();
} else {
// Send other chat messages normally
socket.emit('chat message', msg);
+111 -145
View File
@@ -2,7 +2,7 @@
* Socket event handlers for the Wowngeon game
*/
const SocketHandlers = {
_initialized: false, // Flag to prevent multiple initializations
_initialized: false,
init: function() {
if (this._initialized) {
@@ -28,6 +28,7 @@ const SocketHandlers = {
// Game state handlers
socket.on('waiting_status', this.onWaitingStatus);
socket.on('queue_joined', this.onQueueJoined);
socket.on('game_start', this.onGameStart);
socket.on('game_update', this.onGameUpdate);
socket.on('game_over', this.onGameOver);
@@ -36,20 +37,19 @@ const SocketHandlers = {
// Payment/Address handlers
socket.on('address_detected', this.onAddressDetected);
socket.on('address_confirmed', this.onAddressConfirmed);
socket.on('payment_created', this.onPaymentCreated);
socket.on('payment_confirmed', this.onPaymentConfirmed);
socket.on('payment_detected', this.onPaymentDetected);
// Block height handler (broadcast to all clients)
// Block height handler
socket.on('blockheight', this.onBlockHeight);
},
onConnect: function() {
// Tell server our client ID
socket.emit('register_client', {
clientId: socket.id,
userAgent: navigator.userAgent
});
// Auto-start removed - user must manually start game
},
onWelcome: function(msg) {
@@ -63,30 +63,31 @@ const SocketHandlers = {
},
onStatusUpdate: function(data) {
// Handle player-specific status updates
// Display status message with appropriate styling
const statusClass = data.type === 'error' ? 'error' : 'status';
$('#messages').append($(`<li class="${statusClass}">`).text(data.message));
const statusColor = data.type === 'error' ? '#f00' :
data.type === 'warning' ? '#ff0' :
data.type === 'success' ? '#0f0' :
data.type === 'payment' ? '#0af' : '#fff';
$('#messages').append($('<li class="' + statusClass + '" style="color: ' + statusColor + '; white-space: pre-line;">').text(data.message));
UI.scrollChat();
},
onChatBroadcast: function(data) {
// Handle chat messages broadcast to all players
const timestamp = new Date(data.timestamp || Date.now()).toLocaleTimeString();
const chatMsg = `[${timestamp}] ${data.username || 'Anonymous'}: ${data.message}`;
$('#messages').append($('<li class="chat">').text(chatMsg));
const msgElement = $('<li style="color: #aaa;">');
if (data.socketId) {
msgElement.html('<strong>' + data.socketId.substring(0, 6) + ':</strong> ' + data.message);
} else {
msgElement.text(data.message);
}
$('#messages').append(msgElement);
UI.scrollChat();
},
onWaitingStatus: function(data) {
if (data.status === 'waiting') {
$('#messages').append($('<li style="color:#ff0;">').text(data.message));
// Show waiting screen
if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
Game.drawWaitingScreen();
}
@@ -94,8 +95,21 @@ const SocketHandlers = {
UI.scrollChat();
},
onGameStart: function(data) {
onQueueJoined: function(data) {
console.log('Queue joined:', data);
$('#messages').append($('<li class="queue-info" style="color:#0f0;">').html(
'�� <strong>Queue Joined!</strong> Position: ' + data.position + '<br>' +
'📦 Current block: ' + data.currentBlock + ', Next: ' + data.nextBlock
));
UI.scrollChat();
if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
Game.drawWaitingScreen();
}
},
onGameStart: function(data) {
$('#messages').append($('<li class="game-start">').text("Starting game..."));
if (typeof Game !== 'undefined' && Game.stopWaitingScreen) {
@@ -109,173 +123,125 @@ const SocketHandlers = {
}
try {
// Pass all game data including lighting and torches
var success = Game.startGame(data.player, data.map, data.monster, data.items, data.visibleTiles, data.lighting, data.torches);
if (!success) {
$('#messages').append($('<li class="error">').text("Game start failed. Check console for details."));
if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen();
} else {
// Shift focus to the game display area after successful game start
$('#game-display').focus();
UI.updateFocusIndicator();
setTimeout(function() {
$('#game-display').focus();
}, 100);
}
} catch (err) {
console.error("Error starting game:", err);
$('#messages').append($('<li class="error">').text("Error: " + err.message));
} catch (error) {
console.error("Error starting game:", error);
$('#messages').append($('<li class="error">').text("Game start error: " + error.message));
if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) Game._drawWelcomeScreen();
}
UI.scrollChat();
},
onGameUpdate: function(data) {
if (data.visibleTiles) {
if (window.GameDebug) window.GameDebug.updateDebugDisplay("SOCKET: Received game update with visibleTiles");
}
if (typeof Game !== 'undefined' && Game._gameActive) {
Game.updateGameState(data);
// Server pushed a game state update. The Game object exposes updateGameState().
// Older code referenced Game.updateGame, which doesn't exist in the current refactor,
// so updates were silently ignored (player appeared frozen).
if (typeof Game !== 'undefined') {
if (Game.updateGameState) {
Game.updateGameState(data);
} else if (Game.updateGame) { // Fallback if an alias gets added later
Game.updateGame(data);
} else {
console.warn('Game update received but no updateGameState()/updateGame() method found on Game.');
}
}
UI.scrollChat();
},
onGameOver: function(data) {
// Update game state based on outcome
$('#messages').append($('<li class="game-over">').text("Game Over: " + data.message));
if (typeof Game !== 'undefined') {
if (data.status === 'won') {
// Show win message
$('#messages').append($('<li style="color:#0f0; font-weight:bold;">').text(
data.hasTreasure ?
"YOU ESCAPED WITH THE TREASURE! YOU WON!" :
"You escaped the dungeon alive!"
));
// Draw win screen
Game.drawWinScreen(data.hasTreasure);
} else {
// Show loss message
const reason = data.reason || "unknown";
let message = "You died in the dungeon!";
if (reason === 'monster') {
message = "You were killed by the monster!";
} else if (reason === 'timeout') {
message = "You didn't escape before the next block was found!";
}
$('#messages').append($('<li style="color:#f00; font-weight:bold;">').text(message));
// Draw lose screen
if (Game && Game.drawLoseScreen) {
Game.drawLoseScreen(reason);
}
if (Game.endGame) {
Game.endGame(data);
} else if (Game.drawLoseScreen && data && data.reason) {
// Minimal fallback if endGame not present
if (data.reason === 'monster') Game.drawLoseScreen('monster');
}
// Set game as inactive to prevent further movement
if (Game) {
Game._gameActive = false;
}
// Auto-return to title screen after 30 seconds
setTimeout(() => {
$('#messages').append($('<li style="color:#888;">').text("Returning to title screen..."));
UI.scrollChat();
// Return to welcome screen
if (Game && Game._drawWelcomeScreen) {
Game._drawWelcomeScreen();
}
// Reset game state
if (typeof GameState !== 'undefined') {
GameState.reset();
}
// Focus chat for new commands
$('#chatInput').focus();
UI.updateFocusIndicator();
}, 30000); // 30 seconds
}
UI.scrollChat();
},
onQueueCancelled: function() {
onQueueCancelled: function(data) {
$('#messages').append($('<li style="color: #ff0;">').text("Queue entry cancelled."));
if (typeof Game !== 'undefined' && Game._drawWelcomeScreen) {
Game._drawWelcomeScreen();
} else {
console.error("Game or _drawWelcomeScreen not available");
}
},
onBlockHeight: function(height) {
UI.updateBlockHeight(height);
$('#statusValue').text('Connected');
$('#statusValue').css('color', '#0f0');
UI.scrollChat();
},
onAddressDetected: function(data) {
// Handle address detection confirmation request
console.log('Address detected:', data);
$('#messages').append($('<li class="address-detected" style="color: #ff0; white-space: pre-line;">').text(data.message));
UI.scrollChat();
},
onAddressConfirmed: function(data) {
console.log('Address confirmed:', data);
$('#messages').append($('<li class="address-confirmed" style="color: #0f0; white-space: pre-line;">').text(data.message));
UI.scrollChat();
},
onPaymentCreated: function(data) {
console.log('Payment created:', data);
// Display the confirmation message with styling
const confirmationHtml = `
<div class="address-confirmation" style="
background: #ffe066;
color: #333;
padding: 10px;
margin: 5px 0;
border-radius: 4px;
border-left: 4px solid #f0ad4e;
">
<strong>🔍 ADDRESS DETECTED</strong><br>
<strong>Type:</strong> ${data.type}<br>
<strong>Address:</strong> <code style="word-break: break-all; font-size: 11px;">${data.address}</code><br><br>
<strong style="color: #d9534f;">⚠️ WARNING: Verify this is YOUR address!</strong><br>
<strong style="color: #d9534f;">⚠️ Clipboard viruses can change addresses!</strong><br><br>
Type <strong>"confirm"</strong> to set as payout address or <strong>"cancel"</strong> to reject.
</div>
`;
$('#messages').append($(confirmationHtml));
$('#messages').append($('<li class="payment-info">').html(
'💳 <strong>Payment Required:</strong> ' + data.humanAmount + ' ' + data.cryptoType
));
UI.scrollChat();
// Focus chat input for easy confirmation
setTimeout(() => {
setTimeout(function() {
$('#chatInput').focus();
}, 100);
},
onAddressConfirmed: function(data) {
// Handle successful address confirmation
console.log('Address confirmed:', data);
onPaymentConfirmed: function(data) {
console.log('Payment confirmed:', data);
const confirmationHtml = `
<div class="address-confirmed" style="
background: #d4edda;
color: #155724;
padding: 10px;
margin: 5px 0;
border-radius: 4px;
border-left: 4px solid #28a745;
">
<strong>✅ PAYOUT ADDRESS CONFIRMED</strong><br>
<strong>Type:</strong> ${data.type}<br>
<strong>Address:</strong> <code style="word-break: break-all; font-size: 11px;">${data.address}</code><br><br>
Future winnings will be sent to this address.
</div>
`;
if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
Game.drawWaitingScreen();
}
if (typeof Game !== 'undefined') {
Game._pendingPaymentConfirmed();
}
$('#messages').append($(confirmationHtml));
$('#messages').append($('<li class="payment-success">').html(
'✅ <strong>Payment confirmed!</strong> You are in the game queue.'
));
UI.scrollChat();
},
onPaymentDetected: function(data) {
console.log('Payment detected (mempool):', data);
if (typeof Game !== 'undefined') {
Game._pendingPaymentDetected(data);
if (Game.drawWaitingScreen) Game.drawWaitingScreen();
}
$('#messages').append($('<li class="payment-mempool" style="color:#0af;">').html(
'🌀 <strong>Payment seen in mempool</strong> – awaiting block confirmation...'
));
UI.scrollChat();
},
onBlockHeight: function(data) {
if (typeof UI !== 'undefined' && UI.updateBlockHeight) {
UI.updateBlockHeight(data.blockHeight);
}
}
};
// Ensure SocketHandlers is available globally
if (typeof window !== 'undefined') {
window.SocketHandlers = SocketHandlers;
if (typeof module !== 'undefined' && module.exports) {
module.exports = SocketHandlers;
}
// Note: SocketHandlers.init() is called from index.html after DOM ready
// to ensure proper initialization order with other modules
View File
+5 -4
View File
@@ -4,7 +4,7 @@
# PAID_SINGLE: Users pay per game, get 2x for escape, 3x with treasure
# PAID_CREDITS: Users buy 10 credits, play 10 times, no payouts
# FREE: Free play mode, no payments required
GAME_MODE=FREE
GAME_MODE=PAID_SINGLE
# Cryptocurrency Configuration
CRYPTO_TYPE=XMR
@@ -14,9 +14,10 @@ CRYPTO_TYPE=XMR
SINGLE_GAME_PRICE=5000000000
CREDITS_PACKAGE_PRICE=50000000000
# MoneroPay Configuration
MONEROPAY_ENDPOINT=http://jw-nodes:5000
MONEROPAY_CALLBACK_BASE_URL=http://localhost:3000/api/payments/callback
# Wallet RPC Configuration (replaces MoneroPay)
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570
# WALLET_RPC_USER=user
# WALLET_RPC_PASSWORD=password
# Blockchain RPC Configuration
PRIMARY_RPC_ENDPOINT=http://jw-nodes:18081
+31 -12
View File
@@ -103,36 +103,55 @@ class DebugManager {
console.log("🚀 PRODUCTION MODE ENABLED - Using real blockchain RPC calls");
}
// Real blockchain monitoring
this.debugInterval = setInterval(() => {
rpc.daemonCall("get_block_count", "", (result) => {
if (!result || !result.result || !result.result.count) {
// Initialize RPC service if available
const RpcService = require('../rpc/rpcService');
this.rpcService = new RpcService();
// Real blockchain monitoring with new RPC service
this.debugInterval = setInterval(async () => {
try {
const currentHeight = await this.rpcService.getBlockHeight();
if (!currentHeight) {
if (this.CONSOLE_LOGGING) {
console.log("❌ Failed to get block count from daemon");
}
return;
}
const currentHeight = result.result.count;
rpc.lastBlock.setHeight(currentHeight);
// Always broadcast current height to keep clients updated
this.broadcastManager.broadcastBlockHeight(currentHeight);
// If new block found
if (currentHeight > this.lastProductionBlockHeight) {
if (this.CONSOLE_LOGGING) {
console.log(`⛏️ New block found: ${currentHeight}`);
}
this.broadcastManager.broadcastBlockHeight(currentHeight);
// Notify listeners about new block
this.onNewBlock(currentHeight);
this.lastProductionBlockHeight = currentHeight;
} else {
// Still broadcast current height for status updates
this.broadcastManager.broadcastBlockHeight(currentHeight);
}
});
}, 5000); // Every 5 seconds
} catch (error) {
if (this.CONSOLE_LOGGING) {
console.error("❌ RPC Error:", error.message);
}
// Fallback to legacy RPC if new service fails
rpc.daemonCall("get_block_count", "", (result) => {
if (result && result.result && result.result.count) {
const currentHeight = result.result.count;
this.broadcastManager.broadcastBlockHeight(currentHeight);
if (currentHeight > this.lastProductionBlockHeight) {
this.onNewBlock(currentHeight);
this.lastProductionBlockHeight = currentHeight;
}
}
});
}
}, 2000); // Every 2 seconds as specified in .env
}
// ====== BLOCK HEIGHT MANAGEMENT ======
+101 -10
View File
@@ -4,9 +4,10 @@
*/
class GameModeManager {
constructor(databaseManager, moneroPayService) {
constructor(databaseManager, walletRPCService, debugManager) {
this.db = databaseManager;
this.moneroPayService = moneroPayService;
this.walletService = walletRPCService; // Changed from moneroPayService
this.debugManager = debugManager;
this.gameMode = process.env.GAME_MODE || 'FREE';
this.cryptoType = process.env.CRYPTO_TYPE || 'XMR';
this.singleGamePrice = parseInt(process.env.SINGLE_GAME_PRICE) || 5000000000; // 0.005 XMR
@@ -184,7 +185,7 @@ class GameModeManager {
}
// Create payout record
await this.moneroPayService.createPayout(
await this.walletService.processPayout(
game.user_id,
gameId,
game.payout_address,
@@ -229,25 +230,44 @@ class GameModeManager {
switch (paymentType) {
case 'single_game':
amount = this.singleGamePrice;
description = `Wowgue single game entry (${this.cryptoType})`;
description = `Wowngeon single game entry (${this.cryptoType})`;
break;
case 'credits_package':
amount = this.creditsPackagePrice;
description = `Wowgue 10 credits package (${this.cryptoType})`;
description = `Wowngeon 10 credits package (${this.cryptoType})`;
break;
default:
throw new Error('Invalid payment type');
}
return await this.moneroPayService.createPaymentRequest(
user.id,
socketId,
paymentType,
// Create payment request using wallet RPC with correct parameters
const paymentResult = await this.walletService.createPaymentRequest(
amount,
description
description,
user.id,
socketId
);
if (!paymentResult.success) {
throw new Error(paymentResult.error || 'Failed to create payment address');
}
// Store payment info in database
await this.db.query(`
INSERT INTO payments (user_id, subaddress, expected_amount, payment_type, status, created_at)
VALUES ($1, $2, $3, $4, 'pending', NOW())
`, [user.id, paymentResult.address, amount, paymentType]);
return {
id: paymentResult.id,
address: paymentResult.address,
amount: amount,
amountFormatted: (amount / 1e12).toFixed(4), // Convert from atomic units
currency: this.cryptoType,
expiresAt: paymentResult.expiresAt
};
} catch (error) {
console.error('❌ Error creating payment request:', error.message);
@@ -360,6 +380,77 @@ class GameModeManager {
}
};
}
/**
* Complete a game (called from socket handlers when game ends)
* @param {string} socketId - Player's socket ID
* @param {string} gameId - Game UUID
* @param {boolean} won - Whether the player won (escaped)
* @param {boolean} treasureFound - Whether treasure was found
* @returns {object} payout / completion info
*/
async completeGame(socketId, gameId, won, treasureFound) {
// For FREE mode we just return basic info
if (this.gameMode === 'FREE') {
return { success: true, mode: 'FREE', payout: null };
}
try {
// Update/record game completion in DB (simplified; reuse processGameCompletion if schema matches)
// If a games table exists with at least id & user reference, mark completion.
try {
await this.db.query(`
UPDATE games SET status = $1, treasure_found = $2, completed_at = NOW()
WHERE id = $3
`, [won ? 'won' : 'lost', treasureFound, gameId]);
} catch (e) {
// Non-fatal if games table differs during early dev.
if (process.env.NODE_ENV === 'development') {
console.warn('Game completion update warning:', e.message);
}
}
// Handle payouts only in PAID_SINGLE mode and only on win (escaped)
if (this.gameMode === 'PAID_SINGLE' && won) {
// Derive payout multiplier
const multiplier = treasureFound ? 3 : 2;
const payoutAmount = this.singleGamePrice * multiplier;
// Look up user record for payout address
const userResult = await this.db.query(`SELECT * FROM users WHERE socket_id = $1 LIMIT 1`, [socketId]);
const userRow = userResult.rows[0];
if (userRow && userRow.payout_address) {
try {
await this.walletService.processPayout(
userRow.id,
gameId,
userRow.payout_address,
payoutAmount
);
return {
success: true,
mode: this.gameMode,
payout: {
amount: payoutAmount,
multiplier,
treasure: treasureFound
}
};
} catch (payoutErr) {
console.error('❌ Error creating payout:', payoutErr.message);
return { success: true, mode: this.gameMode, payout: null, payoutError: payoutErr.message };
}
}
return { success: true, mode: this.gameMode, payout: null, reason: 'No payout address' };
}
// Credits mode: decrement nothing here (already handled start). Optionally could award stats.
return { success: true, mode: this.gameMode, payout: null };
} catch (err) {
console.error('❌ completeGame error:', err.message);
return { success: false, error: err.message };
}
}
}
module.exports = GameModeManager;
+21 -18
View File
@@ -10,7 +10,7 @@ require('dotenv').config();
// Import payment system components
const DatabaseManager = require('./db/databaseManager');
const MoneroPayService = require('./payments/moneroPayService');
const WalletRPCService = require('./payments/walletRPCService');
const GameModeManager = require('./game/gameModeManager');
const RpcService = require('./rpc/rpcService');
@@ -19,17 +19,19 @@ const BroadcastManager = require('./network/broadcastManager');
const DebugManager = require('./debug/debugManager');
const SocketHandlers = require('./network/socketHandlers');
// Initialize payment system components
const databaseManager = new DatabaseManager();
const rpcService = new RpcService();
const moneroPayService = new MoneroPayService(databaseManager, rpcService);
const gameModeManager = new GameModeManager(databaseManager, moneroPayService);
// Initialize modular components
// Initialize modular components first
const broadcastManager = new BroadcastManager(io);
const debugManager = new DebugManager(broadcastManager);
// Initialize payment system components (debugManager is now available)
const databaseManager = new DatabaseManager();
const rpcService = new RpcService();
const walletRPCService = new WalletRPCService(debugManager);
const gameModeManager = new GameModeManager(databaseManager, walletRPCService, debugManager);
// Initialize remaining components
const activeGames = new Map(); // Maps socketId to Game objects
const socketHandlers = new SocketHandlers(io, activeGames, broadcastManager, debugManager, gameModeManager);
const socketHandlers = new SocketHandlers(io, activeGames, broadcastManager, debugManager, gameModeManager, walletRPCService);
// Configure static file serving
const htmlPath = path.join(__dirname, '../html');
app.use(express.static(htmlPath));
@@ -56,7 +58,7 @@ app.post('/api/payment/create', async (req, res) => {
return res.status(400).json({ error: 'Missing userId or gameMode' });
}
const payment = await moneroPayService.createPaymentRequest(userId, gameMode);
const payment = await walletRPCService.createPaymentRequest(userId, gameMode);
res.json(payment);
} catch (error) {
console.error('Error creating payment:', error);
@@ -66,7 +68,7 @@ app.post('/api/payment/create', async (req, res) => {
app.post('/api/payment/callback', async (req, res) => {
try {
const result = await moneroPayService.processCallback(req.body);
const result = await walletRPCService.processCallback(req.body);
res.json({ status: 'success', result });
} catch (error) {
console.error('Error processing callback:', error);
@@ -76,7 +78,7 @@ app.post('/api/payment/callback', async (req, res) => {
app.get('/api/payment/status/:paymentId', async (req, res) => {
try {
const status = await moneroPayService.checkPaymentStatus(req.params.paymentId);
const status = await walletRPCService.checkPaymentStatus(req.params.paymentId);
res.json(status);
} catch (error) {
console.error('Error checking payment status:', error);
@@ -122,7 +124,12 @@ debugManager.onNewBlockCallback((blockHeight) => {
// Initialize payment system
async function initializePaymentSystem() {
try {
console.log('🔄 Initializing payment system...');
console.log('🔧 Initializing payment system...');
const walletInitialized = await walletRPCService.initialize();
if (!walletInitialized) {
console.log('⚠️ Wallet RPC not available - falling back to FREE mode');
process.env.GAME_MODE = 'FREE';
}
// Initialize database connection and run migrations
await databaseManager.initialize();
@@ -132,10 +139,6 @@ async function initializePaymentSystem() {
const rpcHealth = await rpcService.healthCheck();
console.log(`✅ RPC Service: ${rpcHealth.healthy ? 'Connected' : 'Warning - using fallback'}`);
// Initialize MoneroPay service
await moneroPayService.initialize();
console.log('✅ MoneroPay service initialized');
console.log('🚀 Payment system ready!');
return true;
} catch (error) {
@@ -187,7 +190,7 @@ async function startServer() {
if (paymentSystemReady) {
setInterval(async () => {
try {
await moneroPayService.processBatchPayouts();
await walletRPCService.processBatchPayouts();
} catch (error) {
console.error('Error in batch payout processing:', error);
}
+448 -98
View File
@@ -7,12 +7,13 @@ const user = require('../db/user');
const Game = require('../game/game');
class SocketHandlers {
constructor(io, activeGames, broadcastManager, debugManager, gameModeManager = null) {
constructor(io, activeGames, broadcastManager, debugManager, gameModeManager = null, walletService = null) {
this.io = io;
this.activeGames = activeGames;
this.broadcastManager = broadcastManager;
this.debugManager = debugManager;
this.gameModeManager = gameModeManager;
this.walletService = walletService;
this.playerMoveTimestamps = new Map();
this.clientSocketMap = new Map();
this.MOVE_COOLDOWN = 100; // Minimum 100ms between moves
@@ -115,6 +116,11 @@ class SocketHandlers {
'Please paste your XMR/WOW address directly in chat. The system will automatically detect and confirm it.');
return;
case 'payment':
case 'pay':
this.handlePaymentCommand(socket);
return;
default:
// Broadcast as chat message to all clients
this.handleChatBroadcast(socket, msg);
@@ -168,9 +174,9 @@ class SocketHandlers {
/**
* Handle game queue request (typing "enter")
*/
handleGameQueue(socket) {
async handleGameQueue(socket) {
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`Player ${socket.id} requested to enter the dungeon - ADDING TO QUEUE`);
console.log(`Player ${socket.id} requested to enter the dungeon`);
}
const currentUser = this.getUserBySocket(socket.id);
@@ -192,11 +198,47 @@ class SocketHandlers {
return;
}
// Add to waiting queue
// Check payment eligibility if payment system is available
if (this.gameModeManager) {
try {
const eligibility = await this.gameModeManager.canUserStartGame(socket.id);
if (!eligibility.allowed) {
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`❌ Payment required for ${socket.id}: ${eligibility.reason}`);
}
// Create payment request immediately instead of asking user to type 'payment'
await this.createAndShowPaymentRequest(socket);
return;
}
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`✅ Payment validated for ${socket.id}: ${eligibility.reason}`);
}
} catch (error) {
console.error('Error checking payment eligibility:', error);
// In paid mode, don't add to queue if payment check fails
if (this.gameModeManager.gameMode !== 'FREE') {
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Payment system error. Please try again or contact support.');
return;
}
// Only in FREE mode, continue to queue on payment system errors
this.broadcastManager.sendStatusUpdate(socket.id, 'warning', 'Payment system unavailable. Playing in FREE mode.');
}
}
// Add to waiting queue (in free mode or already authorized paid mode)
this.WAITING_PLAYERS.push({
serverId: socket.id,
clientId: currentUser.clientId,
entryTime: Date.now()
entryTime: Date.now(),
// For paid modes, this path only executes if payment already confirmed (canUserStartGame returned allowed)
requiresConfirmation: false,
confirmed: true
});
const currentBlock = this.debugManager.getCurrentBlockHeight();
@@ -218,91 +260,15 @@ class SocketHandlers {
console.log(`Player ${socket.id} requested auto-start via start button`);
}
const currentUser = this.getUserBySocket(socket.id);
if (!currentUser) {
this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Error: Could not start game. Please try again.');
return;
}
// Check if already in a game
if (this.activeGames.has(socket.id)) {
this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'You are already in a game!');
return;
}
// Check if payment system is available and validate eligibility
if (this.gameModeManager) {
try {
const eligibility = await this.gameModeManager.canUserStartGame(socket.id);
if (!eligibility.allowed) {
// Send payment requirement to client
this.io.to(socket.id).emit('payment_required', {
reason: eligibility.reason,
action: eligibility.action,
gameMode: this.gameModeManager.gameMode
});
return;
}
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`✅ Payment validated for ${socket.id}: ${eligibility.reason}`);
}
} catch (error) {
console.error('Error checking payment eligibility:', error);
this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Payment system error. Playing in FREE mode.');
}
}
// Remove from waiting queue if present
const waitingIndex = this.WAITING_PLAYERS.findIndex(p => p.serverId === socket.id);
if (waitingIndex !== -1) {
this.WAITING_PLAYERS.splice(waitingIndex, 1);
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`🚀 AUTO-START: Removed ${socket.id} from waiting queue for immediate start`);
}
}
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`Found user for socket ${socket.id}, starting game immediately...`);
}
try {
// For auto-entry, player enters on current block
const currentBlock = this.debugManager.getCurrentBlockHeight();
currentUser.blockRec = currentBlock;
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`🕒 AUTO-START: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`);
}
const game = this.createGameForUser(currentUser, 'standard');
const gameState = game.getState();
gameState.blockHeight = currentBlock;
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`🎮 SENDING IMMEDIATE GAME_START to ${socket.id} (AUTO-START)`);
}
this.io.to(socket.id).emit('game_start', gameState);
// Register game with payment system if available
if (this.gameModeManager) {
try {
await this.gameModeManager.startGame(socket.id, game.id);
} catch (error) {
console.error('Error registering game with payment system:', error);
}
}
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`Game started immediately for player ${socket.id} via auto-start`);
}
} catch (error) {
console.error(`Error creating game:`, error);
this.broadcastManager.sendStatusUpdate(socket.id, 'error', 'Error starting game: ' + error.message);
}
// Use the same logic as handleGameQueue for consistency
// This ensures both START GAME button and typing 'enter' behave the same way
await this.handleGameQueue(socket);
}
/**
* Handle entry cancellation
*/
/**
* Handle cancel entry request
*/
@@ -380,6 +346,23 @@ class SocketHandlers {
return;
}
// Persist move count if DB available
if (this.gameModeManager && this.gameModeManager.db) {
const db = this.gameModeManager.db;
db.query(`
UPDATE games SET moves_made = moves_made + 1
WHERE socket_id = $1 AND dungeon_seed = $2
`, [socket.id, game.id]).catch(err => {
if (this.debugManager.CONSOLE_LOGGING) console.warn('moves_made update failed:', err.message);
});
}
// Check collision if player moved onto monster BEFORE monster reacts
if (this.checkMonsterKill(game.player, game.monster)) {
this.handleGameOver(socket, game, 'lost', 'monster', 'The monster caught you!');
return;
}
// Move monster after player moves
game.moveMonster();
@@ -443,8 +426,20 @@ class SocketHandlers {
reason: reason,
message: message,
score: score,
payout: payoutInfo
payout: payoutInfo,
treasure: game.player.hasTreasure || false
});
// Persist completion details if DB available
if (this.gameModeManager && this.gameModeManager.db) {
const db = this.gameModeManager.db;
const outcome = reason === 'escaped' ? 'escaped' : (reason === 'monster' ? 'caught_by_monster' : reason);
db.query(`
UPDATE games SET status = $1, outcome = $2, treasure_found = $3, completed_at = NOW()
WHERE dungeon_seed = $4 AND socket_id = $5
`, [status, outcome, game.player.hasTreasure, game.id, socket.id])
.catch(err => console.error('Game completion update failed:', err.message));
}
this.activeGames.delete(socket.id);
}
@@ -457,6 +452,9 @@ class SocketHandlers {
console.log('User disconnected', socket.client.id);
}
// Clean up payment monitoring
this.stopPaymentMonitoring(socket.id);
// Clean up movement timestamps
this.playerMoveTimestamps.delete(socket.id);
@@ -545,6 +543,19 @@ class SocketHandlers {
user.joinGame(game);
this.activeGames.set(user.id, game);
// Insert a DB record for this game (best effort)
if (this.gameModeManager && this.gameModeManager.db) {
const db = this.gameModeManager.db;
const gameMode = this.gameModeManager.gameMode || 'FREE';
const blockHeight = this.debugManager.getCurrentBlockHeight ? this.debugManager.getCurrentBlockHeight() : null;
const socketId = user.id; // user.id is the socket id string
db.query(`
INSERT INTO games (user_id, socket_id, game_mode, status, start_block_height, dungeon_seed, created_at)
VALUES ((SELECT id FROM users WHERE socket_id = $1), $1, $2, 'active', $3, $4, NOW())
`, [socketId, gameMode, blockHeight, game.id])
.catch(err => console.error('Game insert failed:', err.message));
}
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`[createGameForUser] Created ${gameType} game ${game.id} for user ${user.id}`);
@@ -591,6 +602,8 @@ class SocketHandlers {
console.log(`Starting games for ${this.WAITING_PLAYERS.length} waiting players at block height ${blockHeight}`);
}
// We may skip some entries if they are not yet confirmed (paid mode, mempool only)
const remainingQueue = [];
while (this.WAITING_PLAYERS.length > 0) {
const playerEntry = this.WAITING_PLAYERS.shift();
const serverId = playerEntry.serverId;
@@ -602,6 +615,14 @@ class SocketHandlers {
const currentUser = this.getUserBySocket(serverId);
if (currentUser) {
// If this entry requires confirmation and is not confirmed yet, keep it in queue
if (playerEntry.requiresConfirmation && !playerEntry.confirmed) {
remainingQueue.push(playerEntry); // re-queue for next block
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`⏳ Skipping unconfirmed paid entry for ${serverId}, still waiting for confirmation.`);
}
continue;
}
currentUser.blockRec = blockHeight;
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`🕒 QUEUE ENTRY: Player enters on block ${currentUser.blockRec}, will die when block ${currentUser.blockRec + 1} starts`);
@@ -629,6 +650,10 @@ class SocketHandlers {
this.io.to(serverId).emit('message', 'Error: User not found');
}
}
// Put back any remaining (unconfirmed) entries
if (remainingQueue.length > 0) {
this.WAITING_PLAYERS = remainingQueue.concat(this.WAITING_PLAYERS);
}
}
/**
@@ -664,18 +689,18 @@ class SocketHandlers {
try {
const { gameMode } = data;
const payment = await this.gameModeManager.moneroPayService.createPaymentRequest(socket.id, gameMode);
// Use gameModeManager to create payment request with proper amount
const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, gameMode);
this.io.to(socket.id).emit('payment_created', {
paymentId: payment.id,
address: payment.address,
amount: payment.amount,
confirmations: payment.confirmations,
paymentId: paymentRequest.id,
address: paymentRequest.address,
amount: paymentRequest.amount,
gameMode: gameMode
});
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`💳 Payment request created for ${socket.id}: ${payment.amount} XMR`);
console.log(`💳 Payment request created for ${socket.id}: ${paymentRequest.amount} atomic units`);
}
} catch (error) {
console.error('Error creating payment request:', error);
@@ -697,13 +722,13 @@ class SocketHandlers {
}
try {
const { paymentId } = data;
const status = await this.gameModeManager.moneroPayService.checkPaymentStatus(paymentId);
const { address } = data;
const status = await this.walletService.checkPaymentStatus(address);
this.io.to(socket.id).emit('payment_status', status);
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`💳 Payment status checked for ${socket.id}: ${status.status}`);
console.log(`💳 Payment status checked for ${socket.id}: ${status.complete ? 'complete' : 'pending'}`);
}
} catch (error) {
console.error('Error checking payment status:', error);
@@ -846,6 +871,331 @@ class SocketHandlers {
`Address noted: ${pendingAddress.address} (Payment system not active)`);
}
}
/**
* Create payment request immediately when user types 'enter' in paid mode
* User stays on welcome screen until payment is detected
*/
async createAndShowPaymentRequest(socket) {
try {
const currentUser = this.getUserBySocket(socket.id);
if (!currentUser) {
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Error: Could not process payment request. Please try again.');
return;
}
// Get game mode information
const gameMode = this.gameModeManager.gameMode;
const cryptoType = this.gameModeManager.cryptoType;
// Determine payment type and amount based on game mode
let paymentType, amount, description;
if (gameMode === 'PAID_SINGLE') {
paymentType = 'single_game';
amount = this.gameModeManager.singleGamePrice;
description = 'Single game entry';
} else if (gameMode === 'PAID_CREDITS') {
paymentType = 'credits_package';
amount = this.gameModeManager.creditsPackagePrice;
description = '10 game credits package';
} else {
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Invalid game mode configuration.');
return;
}
// Create payment request
const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, paymentType);
// Convert atomic units to human readable amount
const humanAmount = (amount / 1000000000000).toFixed(3);
// Send payment information to client (for UI)
this.io.to(socket.id).emit('payment_created', {
paymentId: paymentRequest.id,
address: paymentRequest.address,
amount: paymentRequest.amount,
paymentType: paymentType,
gameMode: gameMode,
cryptoType: cryptoType,
humanAmount: humanAmount,
description: description,
expiresAt: paymentRequest.expiresAt
});
// Send status message to chat with payment details
this.broadcastManager.sendStatusUpdate(socket.id, 'payment',
`💳 PAYMENT REQUIRED (${description})\n\n` +
`Amount: ${humanAmount} ${cryptoType}\n` +
`Address: ${paymentRequest.address}\n\n` +
`⚠️ Send EXACTLY ${humanAmount} ${cryptoType} to the address above.\n` +
`🔄 You will be added to the game queue once payment is detected in mempool.\n` +
`⏰ Payment expires in 30 minutes.`);
// Start monitoring the payment address
this.walletService.startPaymentMonitoring(
paymentRequest.address,
async (status) => {
if (status.in_mempool && !status.confirmed) {
// Payment detected in mempool - add to queue
socket.emit('payment_detected', {
message: 'Payment detected! Adding you to the game queue...',
amount: status.amount,
confirmations: 0
});
// Add user to waiting queue (requires confirmation before starting)
this.WAITING_PLAYERS.push({
serverId: socket.id,
clientId: currentUser.clientId,
entryTime: Date.now(),
paymentId: paymentRequest.id,
requiresConfirmation: true,
confirmed: false
});
// Show waiting screen
socket.emit('queue_joined', {
position: this.WAITING_PLAYERS.length,
message: 'Payment received! Waiting for next block to start game...'
});
} else if (status.confirmed) {
// Payment confirmed - game can start
socket.emit('payment_confirmed', {
message: 'Payment confirmed!',
confirmations: status.confirmations
});
// Mark any existing waiting queue entry as confirmed
const entry = this.WAITING_PLAYERS.find(p => p.serverId === socket.id);
if (entry) {
entry.confirmed = true;
}
}
},
2000 // Check every 2 seconds
);
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`💳 Payment request created for ${socket.id}: ${humanAmount} ${cryptoType} (${paymentType})`);
console.log(`👀 Started monitoring address: ${paymentRequest.address}`);
}
} catch (error) {
console.error('Error creating payment request:', error);
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Failed to create payment request. Please try again or contact support.');
}
}
/**
* Start monitoring a payment address for incoming payments
*/
startPaymentMonitoring(socketId, paymentRequest) {
if (!this.paymentMonitors) {
this.paymentMonitors = new Map();
}
// Clear any existing monitor for this user
this.stopPaymentMonitoring(socketId);
const monitor = {
paymentId: paymentRequest.id,
address: paymentRequest.address,
socketId: socketId,
startTime: Date.now(),
interval: setInterval(async () => {
try {
const status = await this.walletService.checkPaymentStatus(paymentRequest.address);
if (status.confirmed || status.in_mempool) {
// Payment detected!
this.handlePaymentDetected(socketId, paymentRequest, status);
}
} catch (error) {
console.error(`Error checking payment status for ${socketId}:`, error);
}
}, 5000) // Check every 5 seconds
};
this.paymentMonitors.set(socketId, monitor);
// Auto-expire after 30 minutes
setTimeout(() => {
this.stopPaymentMonitoring(socketId);
this.broadcastManager.sendStatusUpdate(socketId, 'warning',
'Payment request expired. Type \'enter\' again to create a new payment request.');
}, 30 * 60 * 1000); // 30 minutes
}
/**
* Stop monitoring a payment address
*/
stopPaymentMonitoring(socketId) {
if (this.paymentMonitors && this.paymentMonitors.has(socketId)) {
const monitor = this.paymentMonitors.get(socketId);
clearInterval(monitor.interval);
this.paymentMonitors.delete(socketId);
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`🛑 Stopped payment monitoring for ${socketId}`);
}
}
}
/**
* Handle payment detection - add user to queue
*/
async handlePaymentDetected(socketId, paymentRequest, paymentStatus) {
this.stopPaymentMonitoring(socketId);
const currentUser = this.getUserBySocket(socketId);
if (!currentUser) {
console.error(`Payment detected but user not found: ${socketId}`);
return;
}
// Check if user is already in queue or game
const existingIndex = this.WAITING_PLAYERS.findIndex(p => p.serverId === socketId);
if (existingIndex !== -1) {
this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Payment confirmed, you are already in the queue!');
return;
}
if (this.activeGames.has(socketId)) {
this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Payment confirmed, but you are already in a game!');
return;
}
// Add to waiting queue. If only mempool detected, require confirmation.
this.WAITING_PLAYERS.push({
serverId: socketId,
clientId: currentUser.clientId,
entryTime: Date.now(),
paymentId: paymentRequest.id,
requiresConfirmation: paymentStatus.in_mempool && !paymentStatus.confirmed,
confirmed: paymentStatus.confirmed
});
const currentBlock = this.debugManager.getCurrentBlockHeight();
const nextBlock = currentBlock + 1;
if (paymentStatus.in_mempool && !paymentStatus.confirmed) {
// Payment in mempool but not confirmed yet
this.broadcastManager.sendStatusUpdate(socketId, 'success',
`💰 PAYMENT DETECTED IN MEMPOOL!\n\n` +
`✅ You have been added to the game queue.\n` +
`🕒 Your game will start when block ${nextBlock} is found.\n` +
`📦 Current block: ${currentBlock}`);
this.broadcastManager.sendStatusUpdate(socketId, 'info', 'Waiting for block confirmation before starting...');
} else if (paymentStatus.confirmed) {
// Payment already confirmed
this.broadcastManager.sendStatusUpdate(socketId, 'success',
`💰 PAYMENT CONFIRMED!\n\n` +
`✅ You have been added to the game queue.\n` +
`🕒 Your game will start when block ${nextBlock} is found.\n` +
`📦 Current block: ${currentBlock}`);
}
// Send user to waiting screen
this.io.to(socketId).emit('payment_confirmed', {
paymentId: paymentRequest.id,
status: paymentStatus,
nextBlock: nextBlock,
currentBlock: currentBlock
});
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`💰 Payment detected for ${socketId}: Added to queue for block ${nextBlock}`);
}
}
/**
* Handle payment command - create payment request for user
*/
async handlePaymentCommand(socket) {
if (!this.gameModeManager) {
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Payment system is not available. Server is running in FREE mode.');
return;
}
try {
const currentUser = this.getUserBySocket(socket.id);
if (!currentUser) {
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Error: Could not process payment request. Please try again.');
return;
}
// Get game mode information
const gameMode = this.gameModeManager.gameMode;
const cryptoType = this.gameModeManager.cryptoType;
if (gameMode === 'FREE') {
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
'Server is in FREE mode - no payment required to play!');
return;
}
// Determine payment type and amount based on game mode
let paymentType, amount, description;
if (gameMode === 'PAID_SINGLE') {
paymentType = 'single_game';
amount = this.gameModeManager.singleGamePrice;
description = `Single game entry (${cryptoType})`;
} else if (gameMode === 'PAID_CREDITS') {
paymentType = 'credits_package';
amount = this.gameModeManager.creditsPackagePrice;
description = `10 game credits package (${cryptoType})`;
} else {
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Invalid game mode configuration.');
return;
}
// Create payment request
const paymentRequest = await this.gameModeManager.createPaymentRequest(socket.id, paymentType);
// Convert atomic units to human readable amount
const humanAmount = (amount / 1000000000000).toFixed(3);
// Send payment information to client
this.io.to(socket.id).emit('payment_created', {
paymentId: paymentRequest.id,
address: paymentRequest.address,
amount: paymentRequest.amount,
paymentType: paymentType,
gameMode: gameMode,
cryptoType: cryptoType,
humanAmount: humanAmount,
description: description,
expiresAt: paymentRequest.expiresAt
});
// Send status message to chat
this.broadcastManager.sendStatusUpdate(socket.id, 'payment',
`💳 Payment Request Created!\n\n` +
`Amount: ${humanAmount} ${cryptoType}\n` +
`Type: ${description}\n` +
`Address: ${paymentRequest.address}\n\n` +
`⚠️ Send the EXACT amount to the address above.\n` +
`⏰ Payment will expire in 30 minutes.\n` +
`🔄 Your game will start automatically after payment confirmation.`);
if (this.debugManager.CONSOLE_LOGGING) {
console.log(`💳 Payment request created for ${socket.id}: ${humanAmount} ${cryptoType} (${paymentType})`);
}
} catch (error) {
console.error('Error creating payment request:', error);
this.broadcastManager.sendStatusUpdate(socket.id, 'error',
'Failed to create payment request. Please try again or contact support.');
}
}
}
module.exports = SocketHandlers;
+1 -1
View File
@@ -18,7 +18,7 @@
},
"production": {
"enabled": true,
"console_logging": false,
"console_logging": true,
"debug_hotkeys": false,
"simulated_blocks": false
}
+352
View File
@@ -0,0 +1,352 @@
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
class WalletRPCService {
constructor(debugManager) {
this.debugManager = debugManager;
this.walletEndpoint = process.env.PRIMARY_WALLET_ENDPOINT || 'http://127.0.0.1:34570';
this.accountIndex = 0;
this.paymentWatchers = new Map();
this.addressToUser = new Map();
this.addressToSocket = new Map();
this.isHealthy = false;
}
async initialize() {
try {
// Test wallet connection
const response = await this.rpcCall('get_version');
this.isHealthy = true;
if (this.debugManager?.CONSOLE_LOGGING) {
console.log('✅ Wallet RPC service initialized:', {
version: response.result?.version || 'unknown',
endpoint: this.walletEndpoint
});
}
// Get wallet height to ensure it's synced
const heightResponse = await this.rpcCall('get_height');
if (this.debugManager?.CONSOLE_LOGGING) {
console.log('📊 Wallet height:', heightResponse.result?.height || 0);
}
return true;
} catch (error) {
this.isHealthy = false;
console.error('❌ Wallet RPC connection failed:', error.message);
console.error(' Make sure wownero-wallet-rpc is running on', this.walletEndpoint);
return false;
}
}
async rpcCall(method, params = {}) {
try {
const response = await axios.post(`${this.walletEndpoint}/json_rpc`, {
jsonrpc: '2.0',
id: '0',
method: method,
params: params
}, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
});
if (response.data.error) {
throw new Error(response.data.error.message);
}
return response.data;
} catch (error) {
if (this.debugManager?.CONSOLE_LOGGING) {
console.error(`❌ Wallet RPC call failed (${method}):`, error.message);
}
throw error;
}
}
async createPaymentRequest(amount, description, userId, socketId) {
try {
// Create a new subaddress for this payment
const response = await this.rpcCall('create_address', {
account_index: this.accountIndex,
label: `game_${userId}_${Date.now()}`
});
const address = response.result.address;
const addressIndex = response.result.address_index;
// Store mapping for payment monitoring
const paymentInfo = {
userId: userId,
socketId: socketId,
amount: amount,
description: description,
addressIndex: addressIndex,
accountIndex: this.accountIndex,
createdAt: new Date(),
status: 'pending',
detected: false,
confirmed: false
};
this.addressToUser.set(address, paymentInfo);
this.addressToSocket.set(address, socketId);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`💳 Payment address created:`, {
user: userId,
address: address.substring(0, 10) + '...',
amount: amount,
index: addressIndex
});
}
return {
success: true,
id: uuidv4(), // Generate payment ID
address: address,
amount: amount,
description: description,
addressIndex: addressIndex,
expiresAt: new Date(Date.now() + 30 * 60 * 1000) // 30 minutes
};
} catch (error) {
console.error('❌ Failed to create payment address:', error);
return { success: false, error: error.message };
}
}
async checkPaymentStatus(address) {
try {
const userInfo = this.addressToUser.get(address);
if (!userInfo) {
return {
complete: false,
in_mempool: false,
confirmed: false,
amount: 0,
confirmations: 0
};
}
// Get incoming transfers for this specific subaddress
const response = await this.rpcCall('get_transfers', {
in: true,
pending: true,
failed: false,
pool: true,
account_index: userInfo.accountIndex,
subaddr_indices: [userInfo.addressIndex]
});
let totalReceived = 0;
let inMempool = false;
let confirmed = false;
let confirmations = 0;
let txHash = null;
// Check pending (mempool) transactions
if (response.result?.pool && response.result.pool.length > 0) {
for (const tx of response.result.pool) {
totalReceived += tx.amount;
inMempool = true;
txHash = tx.txid;
// Mark as detected if this is first time seeing it
if (!userInfo.detected) {
userInfo.detected = true;
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`🎯 Payment detected in mempool:`, {
address: address.substring(0, 10) + '...',
amount: tx.amount,
txid: txHash
});
}
}
}
}
// Check confirmed transactions
if (response.result?.in && response.result.in.length > 0) {
for (const tx of response.result.in) {
totalReceived += tx.amount;
confirmed = true;
txHash = tx.txid;
// Calculate confirmations
if (tx.height > 0) {
const heightResp = await this.rpcCall('get_height');
confirmations = Math.max(confirmations, heightResp.result.height - tx.height);
}
if (!userInfo.confirmed) {
userInfo.confirmed = true;
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`✅ Payment confirmed:`, {
address: address.substring(0, 10) + '...',
confirmations: confirmations,
txid: txHash
});
}
}
}
}
const paymentComplete = totalReceived >= userInfo.amount;
return {
complete: paymentComplete,
in_mempool: inMempool,
confirmed: confirmed,
amount: totalReceived,
confirmations: confirmations,
required: userInfo.amount,
txHash: txHash
};
} catch (error) {
console.error('❌ Failed to check payment status:', error);
return {
complete: false,
in_mempool: false,
confirmed: false,
amount: 0,
confirmations: 0
};
}
}
startPaymentMonitoring(address, callback, interval = 2000) {
if (this.paymentWatchers.has(address)) {
return; // Already monitoring
}
const watcher = setInterval(async () => {
const status = await this.checkPaymentStatus(address);
if (status.in_mempool || status.confirmed) {
callback(status);
// Stop monitoring if payment is confirmed
if (status.confirmed && status.complete) {
this.stopPaymentMonitoring(address);
}
}
}, interval);
this.paymentWatchers.set(address, watcher);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`👁️ Started monitoring payments for:`, address.substring(0, 10) + '...');
}
}
stopPaymentMonitoring(address) {
const watcher = this.paymentWatchers.get(address);
if (watcher) {
clearInterval(watcher);
this.paymentWatchers.delete(address);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`🛑 Stopped monitoring:`, address.substring(0, 10) + '...');
}
}
}
async processPayout(recipientAddress, amount, description) {
try {
// Send payment to user
const response = await this.rpcCall('transfer', {
destinations: [{
amount: amount,
address: recipientAddress
}],
account_index: this.accountIndex,
priority: 1, // Normal priority
get_tx_key: true
});
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`💸 Payout sent:`, {
to: recipientAddress.substring(0, 10) + '...',
amount: amount,
txHash: response.result.tx_hash
});
}
return {
success: true,
txHash: response.result.tx_hash,
txKey: response.result.tx_key,
fee: response.result.fee
};
} catch (error) {
console.error('❌ Failed to process payout:', error);
return {
success: false,
error: error.message
};
}
}
startPaymentMonitoring(address, callback, interval = 2000) {
if (this.paymentWatchers.has(address)) {
return; // Already monitoring
}
const watcher = setInterval(async () => {
try {
const status = await this.checkPaymentStatus(address);
if (status.in_mempool || status.confirmed) {
callback(status);
// Stop monitoring if payment is confirmed
if (status.confirmed && status.complete) {
this.stopPaymentMonitoring(address);
}
}
} catch (error) {
console.error(`❌ Error monitoring payment ${address.substring(0, 10)}...:`, error.message);
}
}, interval);
this.paymentWatchers.set(address, watcher);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`👁️ Started monitoring payments for:`, address.substring(0, 10) + '...');
}
}
stopPaymentMonitoring(address) {
const watcher = this.paymentWatchers.get(address);
if (watcher) {
clearInterval(watcher);
this.paymentWatchers.delete(address);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`🛑 Stopped monitoring:`, address.substring(0, 10) + '...');
}
}
}
cleanupUserPayments(userId) {
for (const [address, userInfo] of this.addressToUser.entries()) {
if (userInfo.userId === userId || userInfo.socketId === userId) {
this.stopPaymentMonitoring(address);
}
}
}
// Get service health status
getHealthStatus() {
return {
healthy: this.isHealthy,
endpoint: this.walletEndpoint,
activeMonitors: this.paymentWatchers.size,
pendingPayments: this.addressToUser.size
};
}
}
module.exports = WalletRPCService;
+5 -5
View File
@@ -5,7 +5,7 @@
const axios = require('axios');
const CONSOLE_LOGGING = process.env.NODE_ENV === 'debug' || process.env.NODE_ENV === 'development';
const CONSOLE_LOGGING = true; // Temporarily enable for debugging
class RPCService {
constructor() {
@@ -127,7 +127,7 @@ class RPCService {
*/
async getBlockHeight() {
try {
const result = await this.makeRPCCall('get_block_count');
const result = await this.makeRPCCall('getblockcount');
const blockHeight = result.count;
if (blockHeight !== this.lastBlockHeight) {
@@ -166,7 +166,7 @@ class RPCService {
async getNetworkInfo() {
try {
const [blockCount, networkInfo] = await Promise.all([
this.makeRPCCall('get_block_count'),
this.makeRPCCall('getblockcount'),
this.makeRPCCall('get_info')
]);
@@ -203,7 +203,7 @@ class RPCService {
await axios.post(`${this.primaryEndpoint}/json_rpc`, {
jsonrpc: '2.0',
id: '0',
method: 'get_block_count'
method: 'getblockcount'
}, { timeout: 5000 });
checks.primary.status = 'healthy';
checks.primary.responseTime = Date.now() - start;
@@ -219,7 +219,7 @@ class RPCService {
await axios.post(`${this.fallbackEndpoint}/json_rpc`, {
jsonrpc: '2.0',
id: '0',
method: 'get_block_count'
method: 'getblockcount'
}, { timeout: 5000 });
checks.fallback.status = 'healthy';
checks.fallback.responseTime = Date.now() - start;
+9 -1
View File
@@ -24,7 +24,15 @@ var lastBlock = {
hash: 0,
setHeight: function(newHeight) {this.height = newHeight}
}
var daemonURL = 'http://localhost:34568/json_rpc';
// Use environment variable for daemon URL, fallback to localhost for backward compatibility
var daemonURL = process.env.PRIMARY_RPC_ENDPOINT ?
process.env.PRIMARY_RPC_ENDPOINT + '/json_rpc' :
'http://localhost:34568/json_rpc';
if (CONSOLE_LOGGING) {
console.log(`🔗 Legacy RPC service using URL: ${daemonURL}`);
}
function daemonCall(method, params, callback, io) {
// Real blockchain call (now used for all cases)
+502
View File
@@ -0,0 +1,502 @@
I'll perform a comprehensive deep review of the entire codebase after the MoneroPay removal and wallet-rpc integration. Let me analyze every aspect systematically.
## 🔍 DEEP CODEBASE REVIEW
### 1. **Critical Issues to Fix**
#### A. **Missing WalletRPCService Reference in socketHandlers.js**
```javascript
// Line 298 - socketHandlers.js references this.walletService but it's never initialized!
this.walletService.startPaymentMonitoring(
```
**FIX NEEDED**: The `SocketHandlers` constructor doesn't receive or store `walletService`. Need to pass it in and store it.
#### B. **MoneroPay References Still Present**
- Line 14 in index.js: `const MoneroPayService = require('./payments/moneroPayService');`
- Line 59 in index.js: `const moneroPayService = new MoneroPayService(debugManager);`
- Line 143-148 in index.js: MoneroPay initialization
- `gameModeManager.js` still references `this.moneroPayService`
- API endpoints in index.js still use MoneroPay
#### C. **Database Connection String Issue**
The database manager uses hardcoded connection string instead of environment variables properly.
#### D. **RPC Service Method Name Issues**
```javascript
// rpcService.js line 77 - Wrong method name for Wownero
method: 'get_block_count' // Should be 'getblockcount' for Wownero
```
### 2. **Complete MoneroPay → WalletRPC Migration**
Let me create the complete migration:
```javascript
// src/payments/walletRPCService.js
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
class WalletRPCService {
constructor(debugManager) {
this.debugManager = debugManager;
this.walletEndpoint = process.env.PRIMARY_WALLET_ENDPOINT || 'http://127.0.0.1:34570';
this.accountIndex = 0;
this.paymentWatchers = new Map();
this.addressToUser = new Map();
this.addressToSocket = new Map();
this.isHealthy = false;
}
async initialize() {
try {
// Test wallet connection
const response = await this.rpcCall('get_version');
this.isHealthy = true;
if (this.debugManager?.CONSOLE_LOGGING) {
console.log('✅ Wallet RPC service initialized:', {
version: response.result?.version || 'unknown',
endpoint: this.walletEndpoint
});
}
// Get wallet height to ensure it's synced
const heightResponse = await this.rpcCall('get_height');
if (this.debugManager?.CONSOLE_LOGGING) {
console.log('📊 Wallet height:', heightResponse.result?.height || 0);
}
return true;
} catch (error) {
this.isHealthy = false;
console.error('❌ Wallet RPC connection failed:', error.message);
console.error(' Make sure wownero-wallet-rpc is running on', this.walletEndpoint);
return false;
}
}
async rpcCall(method, params = {}) {
try {
const response = await axios.post(`${this.walletEndpoint}/json_rpc`, {
jsonrpc: '2.0',
id: '0',
method: method,
params: params
}, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000
});
if (response.data.error) {
throw new Error(response.data.error.message);
}
return response.data;
} catch (error) {
if (this.debugManager?.CONSOLE_LOGGING) {
console.error(`❌ Wallet RPC call failed (${method}):`, error.message);
}
throw error;
}
}
async createPaymentRequest(amount, description, userId, socketId) {
try {
// Create a new subaddress for this payment
const response = await this.rpcCall('create_address', {
account_index: this.accountIndex,
label: `game_${userId}_${Date.now()}`
});
const address = response.result.address;
const addressIndex = response.result.address_index;
// Store mapping for payment monitoring
const paymentInfo = {
userId: userId,
socketId: socketId,
amount: amount,
description: description,
addressIndex: addressIndex,
accountIndex: this.accountIndex,
createdAt: new Date(),
status: 'pending',
detected: false,
confirmed: false
};
this.addressToUser.set(address, paymentInfo);
this.addressToSocket.set(address, socketId);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`💳 Payment address created:`, {
user: userId,
address: address.substring(0, 10) + '...',
amount: amount,
index: addressIndex
});
}
return {
success: true,
id: uuidv4(), // Generate payment ID
address: address,
amount: amount,
description: description,
addressIndex: addressIndex,
expiresAt: new Date(Date.now() + 30 * 60 * 1000) // 30 minutes
};
} catch (error) {
console.error('❌ Failed to create payment address:', error);
return { success: false, error: error.message };
}
}
async checkPaymentStatus(address) {
try {
const userInfo = this.addressToUser.get(address);
if (!userInfo) {
return {
complete: false,
in_mempool: false,
confirmed: false,
amount: 0,
confirmations: 0
};
}
// Get incoming transfers for this specific subaddress
const response = await this.rpcCall('get_transfers', {
in: true,
pending: true,
failed: false,
pool: true,
account_index: userInfo.accountIndex,
subaddr_indices: [userInfo.addressIndex]
});
let totalReceived = 0;
let inMempool = false;
let confirmed = false;
let confirmations = 0;
let txHash = null;
// Check pending (mempool) transactions
if (response.result?.pool && response.result.pool.length > 0) {
for (const tx of response.result.pool) {
totalReceived += tx.amount;
inMempool = true;
txHash = tx.txid;
// Mark as detected if this is first time seeing it
if (!userInfo.detected) {
userInfo.detected = true;
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`🎯 Payment detected in mempool:`, {
address: address.substring(0, 10) + '...',
amount: tx.amount,
txid: txHash
});
}
}
}
}
// Check confirmed transactions
if (response.result?.in && response.result.in.length > 0) {
for (const tx of response.result.in) {
totalReceived += tx.amount;
confirmed = true;
txHash = tx.txid;
// Calculate confirmations
if (tx.height > 0) {
const heightResp = await this.rpcCall('get_height');
confirmations = Math.max(confirmations, heightResp.result.height - tx.height);
}
if (!userInfo.confirmed) {
userInfo.confirmed = true;
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`✅ Payment confirmed:`, {
address: address.substring(0, 10) + '...',
confirmations: confirmations,
txid: txHash
});
}
}
}
}
const paymentComplete = totalReceived >= userInfo.amount;
return {
complete: paymentComplete,
in_mempool: inMempool,
confirmed: confirmed,
amount: totalReceived,
confirmations: confirmations,
required: userInfo.amount,
txHash: txHash
};
} catch (error) {
console.error('❌ Failed to check payment status:', error);
return {
complete: false,
in_mempool: false,
confirmed: false,
amount: 0,
confirmations: 0
};
}
}
startPaymentMonitoring(address, callback, interval = 2000) {
if (this.paymentWatchers.has(address)) {
return; // Already monitoring
}
const watcher = setInterval(async () => {
const status = await this.checkPaymentStatus(address);
if (status.in_mempool || status.confirmed) {
callback(status);
// Stop monitoring if payment is confirmed
if (status.confirmed && status.complete) {
this.stopPaymentMonitoring(address);
}
}
}, interval);
this.paymentWatchers.set(address, watcher);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`👁️ Started monitoring payments for:`, address.substring(0, 10) + '...');
}
}
stopPaymentMonitoring(address) {
const watcher = this.paymentWatchers.get(address);
if (watcher) {
clearInterval(watcher);
this.paymentWatchers.delete(address);
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`🛑 Stopped monitoring:`, address.substring(0, 10) + '...');
}
}
}
async processPayout(recipientAddress, amount, description) {
try {
// Send payment to user
const response = await this.rpcCall('transfer', {
destinations: [{
amount: amount,
address: recipientAddress
}],
account_index: this.accountIndex,
priority: 1, // Normal priority
get_tx_key: true
});
if (this.debugManager?.CONSOLE_LOGGING) {
console.log(`💸 Payout sent:`, {
to: recipientAddress.substring(0, 10) + '...',
amount: amount,
txHash: response.result.tx_hash
});
}
return {
success: true,
txHash: response.result.tx_hash,
txKey: response.result.tx_key,
fee: response.result.fee
};
} catch (error) {
console.error('❌ Failed to process payout:', error);
return {
success: false,
error: error.message
};
}
}
cleanupUserPayments(userId) {
for (const [address, userInfo] of this.addressToUser.entries()) {
if (userInfo.userId === userId || userInfo.socketId === userId) {
this.stopPaymentMonitoring(address);
}
}
}
// Get service health status
getHealthStatus() {
return {
healthy: this.isHealthy,
endpoint: this.walletEndpoint,
activeMonitors: this.paymentWatchers.size,
pendingPayments: this.addressToUser.size
};
}
}
module.exports = WalletRPCService;
```
### 3. **Architecture Issues**
#### A. **Circular Dependencies Risk**
- socketHandlers.js has too many responsibilities (900+ lines!)
- Should split into: `gameHandlers.js`, `paymentHandlers.js`, `chatHandlers.js`
#### B. **Memory Leaks**
- Payment monitors in `walletRPCService` never clean up expired addresses
- `addressToUser` Map grows indefinitely
#### C. **Error Recovery**
- No reconnection logic if wallet-rpc goes down
- No graceful degradation when blockchain daemon fails
### 4. **Security Issues**
#### A. **SQL Injection Risk**
```javascript
// databaseManager.js - Using string concatenation!
async run(query, params = []) {
const result = await this.pool.query(query, params);
```
Should use parameterized queries consistently.
#### B. **Missing Input Validation**
- No validation on payment amounts
- No address format validation before creating subaddress
- No rate limiting on payment requests
#### C. **Sensitive Data Logging**
- Full addresses logged in debug mode
- Should truncate addresses in logs
### 5. **Performance Issues**
#### A. **Inefficient Payment Monitoring**
- Each payment has its own interval timer
- Should batch check all pending payments in one RPC call
#### B. **Database Connection Pool**
```javascript
// databaseManager.js
max: 10, // Could be configurable
```
#### C. **Memory Usage**
- Game state stored in memory indefinitely
- Should implement cleanup for completed games
### 6. **Missing Features**
#### A. **Payment Expiration**
- Payment addresses never expire
- Should clean up after 30 minutes
#### B. **Refund Logic**
- No way to refund overpayments
- No handling for duplicate payments
#### C. **Admin Interface**
- No way to manually credit users
- No payment history viewing
### 7. **Configuration Issues**
#### A. **.env File**
```properties
# These MoneroPay vars should be removed:
MONEROPAY_ENDPOINT=http://192.168.1.23:5000
MONEROPAY_CALLBACK_BASE_URL=http://localhost:3000/api/payments/callback
# RPC endpoints inconsistent:
PRIMARY_RPC_ENDPOINT=http://127.0.0.1:34568 # Should this be 18081?
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570
```
#### B. **Hardcoded Values**
- Payment expiration (30 minutes)
- Check intervals (2 seconds)
- Game prices
### 8. **Frontend Issues**
#### A. **Payment UI Missing**
- No visual payment countdown timer
- No QR code for payment address
- No copy-to-clipboard for addresses
#### B. **Connection Status**
- No visual indicator when wallet-rpc is down
- No retry mechanism for failed connections
### 9. **Testing Coverage**
#### A. **Missing Tests**
- No tests for payment flow
- No tests for wallet-rpc integration
- No tests for game payout logic
#### B. **Test Environment**
- Need mock wallet-rpc for testing
- Need test fixtures for payment scenarios
### 10. **Code Quality**
#### A. **Inconsistent Async Handling**
```javascript
// Sometimes async/await
async handlePaymentRequest(socket, data) { ... }
// Sometimes callbacks
this.walletService.startPaymentMonitoring(address, (status) => { ... })
```
#### B. **Magic Numbers**
```javascript
2000 // Check interval - should be constant
30 * 60 * 1000 // 30 minutes - should be configurable
1000000000000 // Atomic units conversion - should be constant
```
## 📋 PRIORITY FIXES NEEDED
### CRITICAL (Do immediately):
1. ✅ **COMPLETED** - Fix `walletService` reference in socketHandlers.js
2. ✅ **COMPLETED** - Remove all MoneroPay references
3. ✅ **COMPLETED** - Fix RPC method names for Wownero
4. ✅ **COMPLETED** - Add proper error handling for wallet-rpc failures
5. ✅ **COMPLETED** - Fix method signature mismatches between WalletRPCService and GameModeManager
6. ✅ **COMPLETED** - Add missing startPaymentMonitoring/stopPaymentMonitoring methods to WalletRPCService
7. ✅ **COMPLETED** - Fix property name inconsistencies (inMempool vs in_mempool)
8. ✅ **COMPLETED** - Fix database query methods (db.run vs db.query)
9. ✅ **COMPLETED** - Update README.md to reflect wallet-rpc integration
10. ✅ **COMPLETED** - Remove MoneroPay configuration from .env files
### HIGH (Do soon):
1. ✅ Implement payment expiration and cleanup
2. ✅ Add address validation before creating subaddress
3. ✅ Batch payment status checks
4. ✅ Add reconnection logic for wallet-rpc
### MEDIUM (Nice to have):
1. ✅ Split socketHandlers into smaller modulee
2. ✅ Add payment history tracking
3. ✅ Implement refund logic
4. ✅ Add QR codes for payment addresses
### LOW (Future improvements):
1. ✅ Add comprehensive tests
2. ✅ Create admin interface
3. ✅ Add metrics and monitoring
4. ✅ Implement payment webhooks
Would you like me to start implementing these fixes, beginning with the critical issues?