UNPKG

@rsksmart/rsk-cli

Version:

CLI tool for Rootstock network using Viem

256 lines (255 loc) 9.53 kB
import { isAddress } from 'viem'; import ViemProvider from '../viemProvider.js'; import fs from 'fs'; import path from 'path'; import { v4 } from 'uuid'; import chalk from 'chalk'; function logMessage(message, color = chalk.white) { console.log(color(message)); } function logError(message) { logMessage(`❌ ${message}`, chalk.red); } function logSuccess(message) { logMessage(`✅ ${message}`, chalk.green); } function logWarning(message) { logMessage(`⚠️ ${message}`, chalk.yellow); } function logInfo(message) { logMessage(`📊 ${message}`, chalk.blue); } export class MonitorManager { sessions = new Map(); pollingIntervals = new Map(); viemProvider; publicClient; stateFilePath; isInitialized = false; constructor(testnet = false) { this.viemProvider = new ViemProvider(testnet); this.stateFilePath = path.join(process.cwd(), '.rsk-monitoring.json'); } async initialize() { if (this.isInitialized) return; try { this.publicClient = await this.viemProvider.getPublicClient(); await this.loadState(); this.isInitialized = true; } catch (error) { logError(`Failed to initialize monitoring: ${error}`); throw error; } } async startTransactionMonitoring(txHash, confirmations = 12, testnet = false) { await this.initialize(); if (!txHash.startsWith('0x') || txHash.length !== 66) { throw new Error(`Invalid transaction hash format: ${txHash}. Expected 64 hex characters with 0x prefix.`); } const config = { type: 'transaction', txHash, confirmations, testnet }; const sessionId = v4(); const session = { id: sessionId, config, startTime: new Date(), isActive: true, lastCheck: new Date(), checkCount: 0 }; this.sessions.set(sessionId, session); this.startPolling(sessionId); await this.saveState(); logSuccess(`Started monitoring transaction: ${txHash}`); logInfo(`Session ID: ${sessionId}`); return sessionId; } async startAddressMonitoring(address, monitorBalance = true, monitorTransactions = false, testnet = false) { await this.initialize(); if (!isAddress(address)) { throw new Error(`Invalid address format: ${address}. Expected a valid Ethereum/Rootstock address.`); } const config = { type: 'address', address, monitorBalance, monitorTransactions, testnet }; const sessionId = v4(); const session = { id: sessionId, config, startTime: new Date(), isActive: true, lastCheck: new Date(), checkCount: 0 }; this.sessions.set(sessionId, session); this.startPolling(sessionId); await this.saveState(); logSuccess(`Started monitoring address: ${address}`); logInfo(`Session ID: ${sessionId}`); return sessionId; } async stopMonitoring(sessionId) { const session = this.sessions.get(sessionId); if (!session) { logError(`Session ${sessionId} not found`); return false; } session.isActive = false; this.sessions.set(sessionId, session); const interval = this.pollingIntervals.get(sessionId); if (interval) { clearInterval(interval); this.pollingIntervals.delete(sessionId); } await this.saveState(); logWarning(`Stopped monitoring session: ${sessionId}`); const activeSessions = this.getActiveSessions(); if (activeSessions.length === 0) { setTimeout(() => { process.exit(0); }, 100); } return true; } async stopAllMonitoring() { for (const [sessionId] of this.sessions) { await this.stopMonitoring(sessionId); } logWarning(`Stopped all monitoring sessions`); } getActiveSessions() { return Array.from(this.sessions.values()).filter(session => session.isActive); } startPolling(sessionId) { const session = this.sessions.get(sessionId); if (!session) return; const interval = setInterval(async () => { try { await this.checkSession(sessionId); } catch (error) { logError(`Error checking session ${sessionId}: ${error}`); const session = this.sessions.get(sessionId); if (session && session.checkCount > 10) { logWarning(`Too many errors, stopping session ${sessionId}`); await this.stopMonitoring(sessionId); } } }, 10000); this.pollingIntervals.set(sessionId, interval); } async checkSession(sessionId) { const session = this.sessions.get(sessionId); if (!session || !session.isActive) return; session.lastCheck = new Date(); session.checkCount++; if (session.config.type === 'transaction') { await this.checkTransaction(session); } else if (session.config.type === 'address') { await this.checkAddress(session); } this.sessions.set(sessionId, session); } async checkTransaction(session) { const config = session.config; try { const receipt = await this.publicClient.getTransactionReceipt({ hash: config.txHash }); const currentBlock = await this.publicClient.getBlockNumber(); const confirmations = receipt ? Number(currentBlock - receipt.blockNumber) : 0; const status = receipt ? (receipt.status === 'success' ? 'confirmed' : 'failed') : 'pending'; logInfo(`TX ${config.txHash.slice(0, 10)}... - Status: ${status}, Confirmations: ${confirmations}`); if (receipt && (status === 'failed' || confirmations >= (config.confirmations || 12))) { if (status === 'failed') { logError(`Transaction ${config.txHash.slice(0, 10)}... failed`); } else { logSuccess(`Transaction ${config.txHash.slice(0, 10)}... confirmed with ${confirmations} confirmations`); } await this.stopMonitoring(session.id); } } catch (error) { if (error.message?.includes('not found') || error.message?.includes('pending')) { logWarning(`Transaction ${config.txHash.slice(0, 10)}... not found or pending`); } else { logError(`Error checking transaction ${config.txHash.slice(0, 10)}...: ${error.message || error}`); if (session.checkCount > 10) { logWarning(`Too many errors, stopping monitoring`); await this.stopMonitoring(session.id); } } } } async checkAddress(session) { const config = session.config; try { if (config.monitorBalance) { const currentBalance = await this.publicClient.getBalance({ address: config.address }); logInfo(`Address ${config.address.slice(0, 10)}... - Balance: ${currentBalance} wei`); } if (config.monitorTransactions) { logInfo(`Checking transactions for ${config.address.slice(0, 10)}...`); } } catch (error) { if (error.message?.includes('Invalid address')) { logError(`Invalid address format: ${config.address}`); logWarning(`Stopping monitoring for invalid address`); await this.stopMonitoring(session.id); } else if (error.message?.includes('rate limit') || error.message?.includes('too many requests')) { logWarning(`Rate limited, will retry later`); } else { logError(`Error checking address ${config.address.slice(0, 10)}...: ${error.message || error}`); } } } async loadState() { try { if (fs.existsSync(this.stateFilePath)) { const data = fs.readFileSync(this.stateFilePath, 'utf8'); const state = JSON.parse(data); for (const session of state.sessions) { if (session.isActive) { session.isActive = false; } this.sessions.set(session.id, session); } } } catch (error) { logWarning(`Could not load monitoring state: ${error}`); } } async saveState() { try { const state = { sessions: Array.from(this.sessions.values()), globalSettings: { defaultPollingInterval: 10, maxConcurrentSessions: 10, defaultConfirmations: 12 } }; fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2)); } catch (error) { logError(`Could not save monitoring state: ${error}`); } } }