mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-18 06:41:27 +02:00
Get back into it
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+5
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -18,7 +18,7 @@
|
||||
},
|
||||
"production": {
|
||||
"enabled": true,
|
||||
"console_logging": false,
|
||||
"console_logging": true,
|
||||
"debug_hotkeys": false,
|
||||
"simulated_blocks": false
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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)
|
||||
|
||||
@@ -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?
|
||||
Reference in New Issue
Block a user