/** * AlertService - Email alerts for admin monitoring * * Sends alerts via Resend for: * - Low wallet balance * - Failed payouts * - Wallet disconnection * - High pending payout queue */ class AlertService { constructor({ walletService, db, debugManager }) { this.walletService = walletService; this.db = db; this.debugManager = debugManager; // Resend configuration this.resendApiKey = process.env.RESEND_API_KEY; this.adminEmail = process.env.ADMIN_EMAIL; this.fromEmail = process.env.ALERT_FROM_EMAIL || 'alerts@monerogue.app'; // Balance thresholds (atomic units) // BALANCE_WARN: sends email alert // BALANCE_CRITICAL: sends email AND halts new games this.balanceWarnThreshold = parseInt(process.env.BALANCE_WARN) || parseInt(process.env.LOW_BALANCE_THRESHOLD) || 100000000000; // 0.1 XMR default this.balanceCriticalThreshold = parseInt(process.env.BALANCE_CRITICAL) || 10000000000; // 0.01 XMR default this.highPendingThreshold = parseInt(process.env.HIGH_PENDING_THRESHOLD) || 10; // Track critical balance state for game halt this.isBalanceCritical = false; this.lastBalanceCheck = null; // Cooldowns (prevent alert spam) this.alertCooldown = parseInt(process.env.ALERT_COOLDOWN_MS) || 3600000; // 1 hour this.lastAlertSent = new Map(); // Track wallet disconnect duration this.walletDisconnectedSince = null; this.walletDisconnectAlertThreshold = 300000; // 5 minutes this.enabled = !!(this.resendApiKey && this.adminEmail); if (this.enabled) { console.log('📧 Alert service enabled - alerts will be sent to', this.adminEmail); } else { console.log('📧 Alert service disabled - set RESEND_API_KEY and ADMIN_EMAIL to enable'); } } /** * Check if an alert type is on cooldown */ isOnCooldown(alertType) { const lastSent = this.lastAlertSent.get(alertType) || 0; return Date.now() - lastSent < this.alertCooldown; } /** * Send an email alert via Resend */ async sendAlert(type, { subject, html }) { if (!this.enabled) { return { sent: false, reason: 'Alert service not configured' }; } if (this.isOnCooldown(type)) { return { sent: false, reason: 'On cooldown' }; } try { // Dynamic import to avoid requiring resend if not configured const { Resend } = require('resend'); const resend = new Resend(this.resendApiKey); const result = await resend.emails.send({ from: this.fromEmail, to: this.adminEmail, subject: subject, html: html }); this.lastAlertSent.set(type, Date.now()); console.log(`📧 Alert sent: ${type} - ${subject}`); return { sent: true, id: result.data?.id }; } catch (error) { console.error(`❌ Failed to send alert: ${error.message}`); return { sent: false, error: error.message }; } } /** * Check wallet balance and alert if low or critical * BALANCE_WARN: sends email alert * BALANCE_CRITICAL: sends email AND sets isBalanceCritical flag (halts new games) */ async checkWalletBalance() { if (!this.walletService?.isHealthy) { return; } try { const balance = await this.walletService.getBalance(); const unlockedBalance = balance.unlocked_balance || 0; this.lastBalanceCheck = Date.now(); const cryptoType = process.env.CRYPTO_TYPE || 'XMR'; const decimals = cryptoType === 'WOW' ? 11 : 12; const divisor = Math.pow(10, decimals); const balanceFormatted = (unlockedBalance / divisor).toFixed(6); // Check CRITICAL threshold first (more severe) if (unlockedBalance < this.balanceCriticalThreshold) { // Set critical flag - this will halt new games if (!this.isBalanceCritical) { this.isBalanceCritical = true; console.warn(`🚨 CRITICAL: Wallet balance is critically low (${balanceFormatted} ${cryptoType}). New games are HALTED.`); } const thresholdFormatted = (this.balanceCriticalThreshold / divisor).toFixed(6); await this.sendAlert('balance_critical', { subject: `🚨 CRITICAL: Wallet Balance Depleted - ${balanceFormatted} ${cryptoType}`, html: `
NEW GAMES HAVE BEEN HALTED.
The payout wallet balance is critically low:
Users cannot start new paid games until the wallet is topped up.
Please add funds immediately to restore service.
Server: ${process.env.NODE_ENV || 'development'}
Time: ${new Date().toISOString()}
The payout wallet balance is running low:
Please top up the wallet soon to avoid service interruption.
If balance drops below the critical threshold, new games will be automatically halted.
Server: ${process.env.NODE_ENV || 'development'}
Time: ${new Date().toISOString()}
The wallet RPC has been unreachable for ${minutes} minutes.
Payouts cannot be processed while the wallet is disconnected.
Please check:
Endpoint: ${process.env.PRIMARY_WALLET_ENDPOINT || 'not configured'}
Time: ${new Date().toISOString()}
There are ${pendingCount} pending or failed payouts in the queue.
This may indicate:
Please check the admin dashboard for details.
Threshold: ${this.highPendingThreshold}
Time: ${new Date().toISOString()}
A payout has failed after all retry attempts:
Manual intervention may be required.
Time: ${new Date().toISOString()}
` }); } /** * Run all checks (call this periodically) */ async runChecks() { await this.checkWalletBalance(); await this.checkWalletConnection(); await this.checkPendingPayouts(); } /** * Start periodic checking */ startPeriodicChecks(intervalMs = 300000) { // Default 5 minutes if (!this.enabled) { return; } // Run initial check after a short delay setTimeout(() => this.runChecks(), 10000); // Then run periodically this.checkInterval = setInterval(() => { this.runChecks(); }, intervalMs); console.log(`📧 Alert checks will run every ${intervalMs / 60000} minutes`); } /** * Stop periodic checking */ stopPeriodicChecks() { if (this.checkInterval) { clearInterval(this.checkInterval); this.checkInterval = null; } } } module.exports = AlertService;