@rsksmart/rsk-cli
Version:
CLI tool for Rootstock network using Viem
256 lines (255 loc) • 9.53 kB
JavaScript
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}`);
}
}
}