diff --git a/README.md b/README.md
index 9eda2e0..d62de0f 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/html/js/core/game.js b/html/js/core/game.js
index 927c13e..7d9ca02 100644
--- a/html/js/core/game.js
+++ b/html/js/core/game.js
@@ -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) {
diff --git a/html/js/display/waitingScreenAnimator.js b/html/js/display/waitingScreenAnimator.js
index 8eabba8..2df2e89 100644
--- a/html/js/display/waitingScreenAnimator.js
+++ b/html/js/display/waitingScreenAnimator.js
@@ -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');
+ }
+ }
}
}
}
diff --git a/html/js/input/inputHandler.js b/html/js/input/inputHandler.js
index e2b8ecf..4a95711 100644
--- a/html/js/input/inputHandler.js
+++ b/html/js/input/inputHandler.js
@@ -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($('
').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($('').text("🔑 Processing game entry request..."));
+ UI.scrollChat();
} else {
// Send other chat messages normally
socket.emit('chat message', msg);
diff --git a/html/js/network/socketHandlers.js b/html/js/network/socketHandlers.js
index f24fedf..4d8bd14 100644
--- a/html/js/network/socketHandlers.js
+++ b/html/js/network/socketHandlers.js
@@ -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($(``).text(data.message));
+ const statusColor = data.type === 'error' ? '#f00' :
+ data.type === 'warning' ? '#ff0' :
+ data.type === 'success' ? '#0f0' :
+ data.type === 'payment' ? '#0af' : '#fff';
+
+ $('#messages').append($('').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($('').text(chatMsg));
+ const msgElement = $('');
+ if (data.socketId) {
+ msgElement.html('' + data.socketId.substring(0, 6) + ': ' + data.message);
+ } else {
+ msgElement.text(data.message);
+ }
+ $('#messages').append(msgElement);
UI.scrollChat();
},
onWaitingStatus: function(data) {
-
if (data.status === 'waiting') {
$('#messages').append($('').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($('').html(
+ '�� Queue Joined! Position: ' + data.position + '
' +
+ '📦 Current block: ' + data.currentBlock + ', Next: ' + data.nextBlock
+ ));
+ UI.scrollChat();
+
+ if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
+ Game.drawWaitingScreen();
+ }
+ },
+
+ onGameStart: function(data) {
$('#messages').append($('').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($('').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($('').text("Error: " + err.message));
+ } catch (error) {
+ console.error("Error starting game:", error);
+ $('#messages').append($('').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($('').text("Game Over: " + data.message));
if (typeof Game !== 'undefined') {
- if (data.status === 'won') {
- // Show win message
- $('#messages').append($('').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($('').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($('').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($('').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($('').text(data.message));
+ UI.scrollChat();
+ },
+
+ onAddressConfirmed: function(data) {
+ console.log('Address confirmed:', data);
+ $('#messages').append($('').text(data.message));
+ UI.scrollChat();
+ },
+
+ onPaymentCreated: function(data) {
+ console.log('Payment created:', data);
- // Display the confirmation message with styling
- const confirmationHtml = `
-
- 🔍 ADDRESS DETECTED
- Type: ${data.type}
- Address: ${data.address}
- ⚠️ WARNING: Verify this is YOUR address!
- ⚠️ Clipboard viruses can change addresses!
- Type "confirm" to set as payout address or "cancel" to reject.
-
- `;
-
- $('#messages').append($(confirmationHtml));
+ $('#messages').append($('').html(
+ '💳 Payment Required: ' + 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 = `
-
- ✅ PAYOUT ADDRESS CONFIRMED
- Type: ${data.type}
- Address: ${data.address}
- Future winnings will be sent to this address.
-
- `;
+ if (typeof Game !== 'undefined' && Game.drawWaitingScreen) {
+ Game.drawWaitingScreen();
+ }
+ if (typeof Game !== 'undefined') {
+ Game._pendingPaymentConfirmed();
+ }
- $('#messages').append($(confirmationHtml));
+ $('#messages').append($('').html(
+ '✅ Payment confirmed! 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($('').html(
+ '🌀 Payment seen in mempool – 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
diff --git a/index.js b/index.js
deleted file mode 100644
index e69de29..0000000
diff --git a/src/.env.example b/src/.env.example
index fd275dc..aa42d09 100644
--- a/src/.env.example
+++ b/src/.env.example
@@ -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
diff --git a/src/debug/debugManager.js b/src/debug/debugManager.js
index a60c1d1..cba6438 100644
--- a/src/debug/debugManager.js
+++ b/src/debug/debugManager.js
@@ -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 ======
diff --git a/src/game/gameModeManager.js b/src/game/gameModeManager.js
index 7ef74e2..2b53d84 100644
--- a/src/game/gameModeManager.js
+++ b/src/game/gameModeManager.js
@@ -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;
diff --git a/src/index.js b/src/index.js
index 818b265..cbe2274 100644
--- a/src/index.js
+++ b/src/index.js
@@ -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);
}
diff --git a/src/network/socketHandlers.js b/src/network/socketHandlers.js
index 498ce47..dc5ca95 100644
--- a/src/network/socketHandlers.js
+++ b/src/network/socketHandlers.js
@@ -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;
diff --git a/src/package.json b/src/package.json
index a621527..4f05447 100644
--- a/src/package.json
+++ b/src/package.json
@@ -18,7 +18,7 @@
},
"production": {
"enabled": true,
- "console_logging": false,
+ "console_logging": true,
"debug_hotkeys": false,
"simulated_blocks": false
}
diff --git a/src/payments/walletRPCService.js b/src/payments/walletRPCService.js
new file mode 100644
index 0000000..0adc5a1
--- /dev/null
+++ b/src/payments/walletRPCService.js
@@ -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;
\ No newline at end of file
diff --git a/src/rpc/rpcService.js b/src/rpc/rpcService.js
index e017061..fd9e8cb 100644
--- a/src/rpc/rpcService.js
+++ b/src/rpc/rpcService.js
@@ -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;
diff --git a/src/rpc/rpccalls.js b/src/rpc/rpccalls.js
index b7aa58d..1afd512 100644
--- a/src/rpc/rpccalls.js
+++ b/src/rpc/rpccalls.js
@@ -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)
diff --git a/todo.md b/todo.md
new file mode 100644
index 0000000..bbdce0b
--- /dev/null
+++ b/todo.md
@@ -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?
\ No newline at end of file