From 8bec6596ed0c97397765f074e1b7000c0fc7984e Mon Sep 17 00:00:00 2001 From: pranavkonde Date: Thu, 19 Jun 2025 17:13:18 +0530 Subject: [PATCH 1/7] feat: add transaction and address monitoring with real-time updates --- bin/index.ts | 56 +++++ package-lock.json | 24 ++- package.json | 2 + src/commands/monitor.ts | 202 ++++++++++++++++++ src/utils/monitoring/MonitorManager.ts | 278 +++++++++++++++++++++++++ src/utils/types.ts | 38 ++++ 6 files changed, 599 insertions(+), 1 deletion(-) create mode 100644 src/commands/monitor.ts create mode 100644 src/utils/monitoring/MonitorManager.ts diff --git a/bin/index.ts b/bin/index.ts index cf1b32f..8afbd40 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -14,6 +14,7 @@ import { bridgeCommand } from "../src/commands/bridge.js"; import { batchTransferCommand } from "../src/commands/batchTransfer.js"; import { historyCommand } from "../src/commands/history.js"; import { selectAddress } from "../src/commands/selectAddress.js"; +import { monitorCommand, listMonitoringSessions, stopMonitoringSession } from "../src/commands/monitor.js"; interface CommandOptions { testnet?: boolean; @@ -33,6 +34,12 @@ interface CommandOptions { file?: string; interactive?: boolean; token?: Address; + tx?: string; + confirmations?: number; + balance?: boolean; + transactions?: boolean; + list?: boolean; + stop?: string; } const orange = chalk.rgb(255, 165, 0); @@ -225,4 +232,53 @@ program } }); +program + .command("monitor") + .description("Monitor transactions and addresses with real-time updates") + .option("-t, --testnet", "Monitor on the testnet") + .option("--tx ", "Transaction hash to monitor") + .option("-a, --address
", "Address to monitor") + .option("--confirmations ", "Required confirmations for transaction monitoring (default: 12)") + .option("--balance", "Monitor address balance changes") + .option("--transactions", "Monitor address transaction history") + .option("--list", "List active monitoring sessions") + .option("--stop ", "Stop a specific monitoring session") + .action(async (options: CommandOptions) => { + try { + if (options.list) { + await listMonitoringSessions(!!options.testnet); + return; + } + + if (options.stop) { + await stopMonitoringSession(options.stop, !!options.testnet); + return; + } + + if (!options.tx && !options.address) { + console.log(chalk.yellow("šŸ“Š No monitoring target specified. Showing active sessions:")); + await listMonitoringSessions(!!options.testnet); + return; + } + + const address = options.address + ? (`0x${options.address.replace(/^0x/, "")}` as `0x${string}`) + : undefined; + + await monitorCommand( + !!options.testnet, + options.tx, + address, + options.confirmations ? parseInt(options.confirmations.toString()) : undefined, + options.balance !== false, + !!options.transactions + ); + } catch (error: any) { + console.error( + chalk.red("Error during monitoring:"), + error.message || error + ); + } + }); + program.parse(process.argv); diff --git a/package-lock.json b/package-lock.json index 6b7c164..873041b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,14 +19,16 @@ "fs-extra": "^11.2.0", "inquirer": "^12.1.0", "ora": "^8.0.1", + "uuid": "^9.0.1", "viem": "^2.19.4" }, "bin": { "rsk-cli": "dist/bin/index.js" }, "devDependencies": { - "@types/bun": "*", + "@types/bun": "latest", "@types/figlet": "^1.5.8", + "@types/uuid": "^10.0.0", "solc": "0.8.28", "typescript": "^5.0.0" }, @@ -474,6 +476,13 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, "node_modules/abitype": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", @@ -1217,6 +1226,19 @@ "node": ">= 10.0.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/viem": { "version": "2.28.2", "resolved": "https://registry.npmjs.org/viem/-/viem-2.28.2.tgz", diff --git a/package.json b/package.json index b073441..890e9f8 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "devDependencies": { "@types/bun": "latest", "@types/figlet": "^1.5.8", + "@types/uuid": "^10.0.0", "solc": "0.8.28", "typescript": "^5.0.0" }, @@ -54,6 +55,7 @@ "fs-extra": "^11.2.0", "inquirer": "^12.1.0", "ora": "^8.0.1", + "uuid": "^9.0.1", "viem": "^2.19.4" } } diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts new file mode 100644 index 0000000..a7382d4 --- /dev/null +++ b/src/commands/monitor.ts @@ -0,0 +1,202 @@ +import { MonitorManager } from "../utils/monitoring/MonitorManager.js"; +import chalk from "chalk"; +import { Address, isAddress } from "viem"; +import Table from "cli-table3"; + +export async function monitorCommand( + testnet: boolean, + txHash?: string, + address?: Address, + confirmations?: number, + monitorBalance: boolean = true, + monitorTransactions: boolean = false +): Promise { + try { + const monitorManager = new MonitorManager(testnet); + await monitorManager.initialize(); + + if (txHash) { + + if (!txHash.startsWith('0x') || txHash.length !== 66) { + console.error(chalk.red('āŒ Invalid transaction hash format.')); + console.log(chalk.gray('Expected: 64 hex characters with 0x prefix (e.g., 0x1234...)')); + console.log(chalk.gray(`Received: ${txHash} (length: ${txHash.length})`)); + return; + } + + console.log(chalk.blue(`šŸ” Starting transaction monitoring...`)); + console.log(chalk.gray(`Network: ${testnet ? 'Testnet' : 'Mainnet'}`)); + console.log(chalk.gray(`Transaction: ${txHash}`)); + console.log(chalk.gray(`Required confirmations: ${confirmations || 12}`)); + console.log(''); + + const sessionId = await monitorManager.startTransactionMonitoring( + txHash, + confirmations || 12, + testnet + ); + + console.log(chalk.green(`\nšŸŽÆ Monitoring started successfully!`)); + console.log(chalk.blue(`Press Ctrl+C to stop monitoring`)); + console.log(''); + + process.on('SIGINT', async () => { + console.log(chalk.yellow(`\nā¹ļø Stopping monitoring...`)); + await monitorManager.stopMonitoring(sessionId); + process.exit(0); + }); + + setInterval(() => {}, 1000); + + } else if (address) { + + if (!isAddress(address)) { + console.error(chalk.red('āŒ Invalid address format.')); + console.log(chalk.gray('Expected: Valid Ethereum/Rootstock address (40 hex characters with 0x prefix)')); + console.log(chalk.gray(`Received: ${address} (length: ${String(address).length})`)); + return; + } + + console.log(chalk.blue(`šŸ” Starting address monitoring...`)); + console.log(chalk.gray(`Network: ${testnet ? 'Testnet' : 'Mainnet'}`)); + console.log(chalk.gray(`Address: ${address}`)); + console.log(chalk.gray(`Monitor balance: ${monitorBalance ? 'Yes' : 'No'}`)); + console.log(chalk.gray(`Monitor transactions: ${monitorTransactions ? 'Yes' : 'No'}`)); + console.log(''); + + const sessionId = await monitorManager.startAddressMonitoring( + address, + monitorBalance, + monitorTransactions, + testnet + ); + + console.log(chalk.green(`\nšŸŽÆ Monitoring started successfully!`)); + console.log(chalk.blue(`Press Ctrl+C to stop monitoring`)); + console.log(''); + + process.on('SIGINT', async () => { + console.log(chalk.yellow(`\nā¹ļø Stopping monitoring...`)); + await monitorManager.stopMonitoring(sessionId); + process.exit(0); + }); + + setInterval(() => {}, 1000); + + } else { + const activeSessions = monitorManager.getActiveSessions(); + + if (activeSessions.length === 0) { + console.log(chalk.yellow(`šŸ“Š No active monitoring sessions found.`)); + console.log(chalk.gray(`Use --tx or --address
to start monitoring.`)); + return; + } + + console.log(chalk.blue(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`)); + console.log(''); + + const table = new Table({ + head: ['Session ID', 'Type', 'Target', 'Status', 'Checks', 'Started'], + colWidths: [36, 12, 42, 10, 8, 20], + }); + + for (const session of activeSessions) { + const target = session.config.type === 'transaction' + ? (session.config as any).txHash.slice(0, 20) + '...' + : (session.config as any).address; + + table.push([ + session.id.slice(0, 8) + '...', + session.config.type, + target, + session.isActive ? 'Active' : 'Stopped', + session.checkCount.toString(), + session.startTime.toLocaleTimeString() + ]); + } + + console.log(table.toString()); + } + + } catch (error: any) { + if (error.message?.includes('Invalid address format')) { + console.error(chalk.red('āŒ Invalid address format provided.')); + console.log(chalk.gray('Please provide a valid Ethereum/Rootstock address.')); + } else if (error.message?.includes('Invalid transaction hash format')) { + console.error(chalk.red('āŒ Invalid transaction hash format provided.')); + console.log(chalk.gray('Please provide a valid transaction hash.')); + } else if (error.message?.includes('Failed to initialize monitoring')) { + console.error(chalk.red('āŒ Failed to connect to the network.')); + console.log(chalk.gray('Please check your internet connection and try again.')); + } else { + console.error(chalk.red('🚨 Error in monitoring:'), error.message || error); + } + } +} + +export async function listMonitoringSessions(testnet: boolean): Promise { + try { + const monitorManager = new MonitorManager(testnet); + await monitorManager.initialize(); + + const activeSessions = monitorManager.getActiveSessions(); + + if (activeSessions.length === 0) { + console.log(chalk.yellow(`šŸ“Š No active monitoring sessions found.`)); + return; + } + + console.log(chalk.blue(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`)); + console.log(''); + + const table = new Table({ + head: ['Session ID', 'Type', 'Target', 'Status', 'Checks', 'Started'], + colWidths: [36, 12, 42, 10, 8, 20], + }); + + for (const session of activeSessions) { + const target = session.config.type === 'transaction' + ? (session.config as any).txHash.slice(0, 20) + '...' + : (session.config as any).address; + + table.push([ + session.id.slice(0, 8) + '...', + session.config.type, + target, + session.isActive ? 'Active' : 'Stopped', + session.checkCount.toString(), + session.startTime.toLocaleTimeString() + ]); + } + + console.log(table.toString()); + + } catch (error: any) { + console.error(chalk.red('🚨 Error listing sessions:'), error.message || error); + } +} + +export async function stopMonitoringSession(sessionId: string, testnet: boolean): Promise { + try { + if (!sessionId || sessionId.length < 8) { + console.error(chalk.red('āŒ Invalid session ID provided.')); + console.log(chalk.gray('Please provide a valid session ID (at least 8 characters).')); + return; + } + + const monitorManager = new MonitorManager(testnet); + await monitorManager.initialize(); + + const success = await monitorManager.stopMonitoring(sessionId); + + if (success) { + console.log(chalk.green(`āœ… Successfully stopped monitoring session: ${sessionId}`)); + } else { + console.log(chalk.red(`āŒ Failed to stop monitoring session: ${sessionId}`)); + console.log(chalk.gray('Session not found or already stopped.')); + } + + } catch (error: any) { + console.error(chalk.red('🚨 Error stopping session:'), error.message || error); + } +} \ No newline at end of file diff --git a/src/utils/monitoring/MonitorManager.ts b/src/utils/monitoring/MonitorManager.ts new file mode 100644 index 0000000..5d63561 --- /dev/null +++ b/src/utils/monitoring/MonitorManager.ts @@ -0,0 +1,278 @@ +import { PublicClient, TransactionReceipt, Address, isAddress } from 'viem'; +import ViemProvider from '../viemProvider.js'; +import { + MonitoringConfig, + MonitoringSession, + TransactionMonitoringConfig, + AddressMonitoringConfig, + MonitoringState +} from '../types.js'; +import fs from 'fs'; +import path from 'path'; +import chalk from 'chalk'; +import { v4 as uuidv4 } from 'uuid'; + +export class MonitorManager { + private sessions: Map = new Map(); + private pollingIntervals: Map = new Map(); + private viemProvider: ViemProvider; + private publicClient!: PublicClient; + private stateFilePath: string; + private isInitialized = false; + + constructor(testnet: boolean = false) { + this.viemProvider = new ViemProvider(testnet); + this.stateFilePath = path.join(process.cwd(), '.rsk-monitoring.json'); + } + + async initialize(): Promise { + if (this.isInitialized) return; + + try { + this.publicClient = await this.viemProvider.getPublicClient(); + await this.loadState(); + this.isInitialized = true; + } catch (error) { + console.error(chalk.red('āŒ Failed to initialize monitoring:'), error); + throw error; + } + } + + async startTransactionMonitoring( + txHash: string, + confirmations: number = 12, + testnet: boolean = false + ): Promise { + 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: TransactionMonitoringConfig = { + type: 'transaction', + txHash, + confirmations, + testnet + }; + + const sessionId = uuidv4(); + const session: MonitoringSession = { + id: sessionId, + config, + startTime: new Date(), + isActive: true, + lastCheck: new Date(), + checkCount: 0 + }; + + this.sessions.set(sessionId, session); + this.startPolling(sessionId); + await this.saveState(); + + console.log(chalk.green(`āœ… Started monitoring transaction: ${txHash}`)); + console.log(chalk.blue(`šŸ“Š Session ID: ${sessionId}`)); + + return sessionId; + } + + async startAddressMonitoring( + address: Address, + monitorBalance: boolean = true, + monitorTransactions: boolean = false, + testnet: boolean = false + ): Promise { + await this.initialize(); + + + if (!isAddress(address)) { + throw new Error(`Invalid address format: ${address}. Expected a valid Ethereum/Rootstock address.`); + } + + const config: AddressMonitoringConfig = { + type: 'address', + address, + monitorBalance, + monitorTransactions, + testnet + }; + + const sessionId = uuidv4(); + const session: MonitoringSession = { + id: sessionId, + config, + startTime: new Date(), + isActive: true, + lastCheck: new Date(), + checkCount: 0 + }; + + this.sessions.set(sessionId, session); + this.startPolling(sessionId); + await this.saveState(); + + console.log(chalk.green(`āœ… Started monitoring address: ${address}`)); + console.log(chalk.blue(`šŸ“Š Session ID: ${sessionId}`)); + + return sessionId; + } + + async stopMonitoring(sessionId: string): Promise { + const session = this.sessions.get(sessionId); + if (!session) { + console.log(chalk.red(`āŒ 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(); + console.log(chalk.yellow(`ā¹ļø Stopped monitoring session: ${sessionId}`)); + return true; + } + + async stopAllMonitoring(): Promise { + for (const [sessionId] of this.sessions) { + await this.stopMonitoring(sessionId); + } + console.log(chalk.yellow(`ā¹ļø Stopped all monitoring sessions`)); + } + + getActiveSessions(): MonitoringSession[] { + return Array.from(this.sessions.values()).filter(session => session.isActive); + } + + private startPolling(sessionId: string): void { + const session = this.sessions.get(sessionId); + if (!session) return; + + const interval = setInterval(async () => { + try { + await this.checkSession(sessionId); + } catch (error) { + console.error(chalk.red(`āŒ Error checking session ${sessionId}:`), error); + + + const session = this.sessions.get(sessionId); + if (session && session.checkCount > 10) { + console.log(chalk.yellow(`āš ļø Too many errors, stopping session ${sessionId}`)); + await this.stopMonitoring(sessionId); + } + } + }, 10000); + + this.pollingIntervals.set(sessionId, interval); + } + + private async checkSession(sessionId: string): Promise { + 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); + } + + private async checkTransaction(session: MonitoringSession): Promise { + const config = session.config as TransactionMonitoringConfig; + + try { + const tx = await this.publicClient.getTransaction({ hash: config.txHash as `0x${string}` }); + const receipt = await this.publicClient.getTransactionReceipt({ hash: config.txHash as `0x${string}` }); + const currentBlock = await this.publicClient.getBlockNumber(); + + const confirmations = receipt ? Number(currentBlock - receipt.blockNumber) : 0; + const status = receipt ? (receipt.status === 'success' ? 'confirmed' : 'failed') : 'pending'; + + console.log(chalk.blue(`šŸ“Š TX ${config.txHash.slice(0, 10)}... - Status: ${status}, Confirmations: ${confirmations}`)); + + if (confirmations >= (config.confirmations || 12)) { + console.log(chalk.green(`āœ… Transaction ${config.txHash.slice(0, 10)}... confirmed with ${confirmations} confirmations`)); + await this.stopMonitoring(session.id); + } + + } catch (error: any) { + if (error.message?.includes('not found') || error.message?.includes('pending')) { + console.log(chalk.yellow(`āš ļø Transaction ${config.txHash.slice(0, 10)}... not found or pending`)); + } else { + console.error(chalk.red(`āŒ Error checking transaction ${config.txHash.slice(0, 10)}...:`), error.message || error); + } + } + } + + private async checkAddress(session: MonitoringSession): Promise { + const config = session.config as AddressMonitoringConfig; + + try { + if (config.monitorBalance) { + const currentBalance = await this.publicClient.getBalance({ address: config.address }); + console.log(chalk.blue(`šŸ’° Address ${config.address.slice(0, 10)}... - Balance: ${currentBalance} wei`)); + } + + if (config.monitorTransactions) { + console.log(chalk.blue(`šŸ“ Checking transactions for ${config.address.slice(0, 10)}...`)); + } + + } catch (error: any) { + if (error.message?.includes('Invalid address')) { + console.error(chalk.red(`āŒ Invalid address format: ${config.address}`)); + console.log(chalk.yellow(`āš ļø Stopping monitoring for invalid address`)); + await this.stopMonitoring(session.id); + } else if (error.message?.includes('rate limit') || error.message?.includes('too many requests')) { + console.log(chalk.yellow(`āš ļø Rate limited, will retry later`)); + } else { + console.error(chalk.red(`āŒ Error checking address ${config.address.slice(0, 10)}...:`), error.message || error); + } + } + } + + private async loadState(): Promise { + try { + if (fs.existsSync(this.stateFilePath)) { + const data = fs.readFileSync(this.stateFilePath, 'utf8'); + const state: MonitoringState = JSON.parse(data); + + for (const session of state.sessions) { + if (session.isActive) { + session.isActive = false; + } + this.sessions.set(session.id, session); + } + } + } catch (error) { + console.log(chalk.yellow(`āš ļø Could not load monitoring state: ${error}`)); + } + } + + private async saveState(): Promise { + try { + const state: MonitoringState = { + sessions: Array.from(this.sessions.values()), + globalSettings: { + defaultPollingInterval: 10, + maxConcurrentSessions: 10, + defaultConfirmations: 12 + } + }; + + fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2)); + } catch (error) { + console.error(chalk.red(`āŒ Could not save monitoring state: ${error}`)); + } + } +} \ No newline at end of file diff --git a/src/utils/types.ts b/src/utils/types.ts index a20fbab..c26a7ef 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -15,3 +15,41 @@ export type FileTx = { to: Address; value: bigint; }; + + +export type MonitoringType = 'transaction' | 'address'; + +export type TransactionMonitoringConfig = { + type: 'transaction'; + txHash: string; + confirmations?: number; + testnet: boolean; +}; + +export type AddressMonitoringConfig = { + type: 'address'; + address: Address; + monitorBalance: boolean; + monitorTransactions: boolean; + testnet: boolean; +}; + +export type MonitoringConfig = TransactionMonitoringConfig | AddressMonitoringConfig; + +export type MonitoringSession = { + id: string; + config: MonitoringConfig; + startTime: Date; + isActive: boolean; + lastCheck: Date; + checkCount: number; +}; + +export type MonitoringState = { + sessions: MonitoringSession[]; + globalSettings: { + defaultPollingInterval: number; + maxConcurrentSessions: number; + defaultConfirmations: number; + }; +}; From 91c93b53a2e7af1d6bf087e5d0015a72a1945147 Mon Sep 17 00:00:00 2001 From: pranavkonde Date: Mon, 7 Jul 2025 00:02:07 +0530 Subject: [PATCH 2/7] refactor: move tx monitoring from monitor to tx command --- bin/index.ts | 20 ++++++----- src/commands/monitor.ts | 43 ++---------------------- src/commands/tx.ts | 46 +++++++++++++++++++++++++- src/utils/monitoring/MonitorManager.ts | 6 ++-- 4 files changed, 62 insertions(+), 53 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index 8afbd40..d1fcba4 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -40,6 +40,7 @@ interface CommandOptions { transactions?: boolean; list?: boolean; stop?: string; + monitor?: boolean; } const orange = chalk.rgb(255, 165, 0); @@ -124,12 +125,19 @@ program .description("Check the status of a transaction") .option("-t, --testnet", "Check the transaction status on the testnet") .requiredOption("-i, --txid ", "Transaction ID") + .option("--monitor", "Keep monitoring the transaction until confirmation") + .option("--confirmations ", "Required confirmations for monitoring (default: 12)") .action(async (options: CommandOptions) => { const formattedTxId = options.txid!.startsWith("0x") ? options.txid : `0x${options.txid}`; - await txCommand(!!options.testnet, formattedTxId as `0x${string}`); + await txCommand( + !!options.testnet, + formattedTxId as `0x${string}`, + !!options.monitor, + options.confirmations ? parseInt(options.confirmations.toString()) : undefined + ); }); program @@ -234,11 +242,9 @@ program program .command("monitor") - .description("Monitor transactions and addresses with real-time updates") + .description("Monitor addresses with real-time updates") .option("-t, --testnet", "Monitor on the testnet") - .option("--tx ", "Transaction hash to monitor") .option("-a, --address
", "Address to monitor") - .option("--confirmations ", "Required confirmations for transaction monitoring (default: 12)") .option("--balance", "Monitor address balance changes") .option("--transactions", "Monitor address transaction history") .option("--list", "List active monitoring sessions") @@ -255,7 +261,7 @@ program return; } - if (!options.tx && !options.address) { + if (!options.address) { console.log(chalk.yellow("šŸ“Š No monitoring target specified. Showing active sessions:")); await listMonitoringSessions(!!options.testnet); return; @@ -267,9 +273,7 @@ program await monitorCommand( !!options.testnet, - options.tx, - address, - options.confirmations ? parseInt(options.confirmations.toString()) : undefined, + address as Address, options.balance !== false, !!options.transactions ); diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index a7382d4..b0475ac 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -5,9 +5,7 @@ import Table from "cli-table3"; export async function monitorCommand( testnet: boolean, - txHash?: string, address?: Address, - confirmations?: number, monitorBalance: boolean = true, monitorTransactions: boolean = false ): Promise { @@ -15,41 +13,7 @@ export async function monitorCommand( const monitorManager = new MonitorManager(testnet); await monitorManager.initialize(); - if (txHash) { - - if (!txHash.startsWith('0x') || txHash.length !== 66) { - console.error(chalk.red('āŒ Invalid transaction hash format.')); - console.log(chalk.gray('Expected: 64 hex characters with 0x prefix (e.g., 0x1234...)')); - console.log(chalk.gray(`Received: ${txHash} (length: ${txHash.length})`)); - return; - } - - console.log(chalk.blue(`šŸ” Starting transaction monitoring...`)); - console.log(chalk.gray(`Network: ${testnet ? 'Testnet' : 'Mainnet'}`)); - console.log(chalk.gray(`Transaction: ${txHash}`)); - console.log(chalk.gray(`Required confirmations: ${confirmations || 12}`)); - console.log(''); - - const sessionId = await monitorManager.startTransactionMonitoring( - txHash, - confirmations || 12, - testnet - ); - - console.log(chalk.green(`\nšŸŽÆ Monitoring started successfully!`)); - console.log(chalk.blue(`Press Ctrl+C to stop monitoring`)); - console.log(''); - - process.on('SIGINT', async () => { - console.log(chalk.yellow(`\nā¹ļø Stopping monitoring...`)); - await monitorManager.stopMonitoring(sessionId); - process.exit(0); - }); - - setInterval(() => {}, 1000); - - } else if (address) { - + if (address) { if (!isAddress(address)) { console.error(chalk.red('āŒ Invalid address format.')); console.log(chalk.gray('Expected: Valid Ethereum/Rootstock address (40 hex characters with 0x prefix)')); @@ -88,7 +52,7 @@ export async function monitorCommand( if (activeSessions.length === 0) { console.log(chalk.yellow(`šŸ“Š No active monitoring sessions found.`)); - console.log(chalk.gray(`Use --tx or --address
to start monitoring.`)); + console.log(chalk.gray(`Use --address
to start monitoring an address.`)); return; } @@ -122,9 +86,6 @@ export async function monitorCommand( if (error.message?.includes('Invalid address format')) { console.error(chalk.red('āŒ Invalid address format provided.')); console.log(chalk.gray('Please provide a valid Ethereum/Rootstock address.')); - } else if (error.message?.includes('Invalid transaction hash format')) { - console.error(chalk.red('āŒ Invalid transaction hash format provided.')); - console.log(chalk.gray('Please provide a valid transaction hash.')); } else if (error.message?.includes('Failed to initialize monitoring')) { console.error(chalk.red('āŒ Failed to connect to the network.')); console.log(chalk.gray('Please check your internet connection and try again.')); diff --git a/src/commands/tx.ts b/src/commands/tx.ts index 1b71101..e4d132c 100644 --- a/src/commands/tx.ts +++ b/src/commands/tx.ts @@ -1,11 +1,25 @@ import ViemProvider from "../utils/viemProvider.js"; import chalk from "chalk"; import Table from "cli-table3"; +import { MonitorManager } from "../utils/monitoring/MonitorManager.js"; -export async function txCommand(testnet: boolean, txid: string): Promise { +export async function txCommand( + testnet: boolean, + txid: string, + monitor: boolean = false, + confirmations: number = 12 +): Promise { try { const formattedTxId = txid.startsWith("0x") ? txid : `0x${txid}`; const txidWithCorrectType = formattedTxId as `0x${string}`; + + if (!txidWithCorrectType.startsWith('0x') || txidWithCorrectType.length !== 66) { + console.error(chalk.red('āŒ Invalid transaction hash format.')); + console.log(chalk.gray('Expected: 64 hex characters with 0x prefix (e.g., 0x1234...)')); + console.log(chalk.gray(`Received: ${txidWithCorrectType} (length: ${txidWithCorrectType.length})`)); + return; + } + const provider = new ViemProvider(testnet); const client = await provider.getPublicClient(); @@ -37,6 +51,36 @@ export async function txCommand(testnet: boolean, txid: string): Promise { { "šŸ“„ To": txReceipt.to } ); console.log(table.toString()); + + // If monitoring is requested, start monitoring the transaction + if (monitor) { + console.log(chalk.blue(`\nšŸ” Starting transaction monitoring...`)); + console.log(chalk.gray(`Network: ${testnet ? 'Testnet' : 'Mainnet'}`)); + console.log(chalk.gray(`Transaction: ${txidWithCorrectType}`)); + console.log(chalk.gray(`Required confirmations: ${confirmations}`)); + console.log(''); + + const monitorManager = new MonitorManager(testnet); + await monitorManager.initialize(); + + const sessionId = await monitorManager.startTransactionMonitoring( + txidWithCorrectType, + confirmations, + testnet + ); + + console.log(chalk.green(`\nšŸŽÆ Monitoring started successfully!`)); + console.log(chalk.blue(`Press Ctrl+C to stop monitoring`)); + console.log(''); + + process.on('SIGINT', async () => { + console.log(chalk.yellow(`\nā¹ļø Stopping monitoring...`)); + await monitorManager.stopMonitoring(sessionId); + process.exit(0); + }); + + setInterval(() => {}, 1000); + } } catch (error) { if (error instanceof Error) { console.error(chalk.red('🚨 Error checking transaction status:'), chalk.yellow(`Error checking transaction status: Invalid transaction hash`)); diff --git a/src/utils/monitoring/MonitorManager.ts b/src/utils/monitoring/MonitorManager.ts index 5d63561..348f8a0 100644 --- a/src/utils/monitoring/MonitorManager.ts +++ b/src/utils/monitoring/MonitorManager.ts @@ -10,7 +10,7 @@ import { import fs from 'fs'; import path from 'path'; import chalk from 'chalk'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 } from 'uuid'; export class MonitorManager { private sessions: Map = new Map(); @@ -57,7 +57,7 @@ export class MonitorManager { testnet }; - const sessionId = uuidv4(); + const sessionId = v4(); const session: MonitoringSession = { id: sessionId, config, @@ -98,7 +98,7 @@ export class MonitorManager { testnet }; - const sessionId = uuidv4(); + const sessionId = v4(); const session: MonitoringSession = { id: sessionId, config, From f01ea99efc26114524d4a847e968ffcea799b5b6 Mon Sep 17 00:00:00 2001 From: pranavkonde Date: Wed, 23 Jul 2025 00:56:29 +0530 Subject: [PATCH 3/7] Update README.md file --- README.md | 16 ++++++++++++++++ src/utils/monitoring/MonitorManager.ts | 20 ++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c49d79..1963c23 100644 --- a/README.md +++ b/README.md @@ -262,13 +262,21 @@ The `tx` command allows you to check the status of a specific transaction on the #### Mainnet ```bash +# Check transaction status rsk-cli tx --txid 0x86deb77e1d666ae6848630496d672da8b5f48292681bda33f8f04245c55dde26 + +# Monitor transaction until confirmation +rsk-cli tx --txid 0x86deb77e1d666ae6848630496d672da8b5f48292681bda33f8f04245c55dde26 --monitor ``` #### Testnet ```bash +# Check transaction status rsk-cli tx --testnet --txid 0x86deb77e1d666ae6848630496d672da8b5f48292681bda33f8f04245c55dde26 + +# Monitor transaction until confirmation +rsk-cli tx --testnet --txid 0x86deb77e1d666ae6848630496d672da8b5f48292681bda33f8f04245c55dde26 --monitor ``` Output example: @@ -278,8 +286,16 @@ Output example: 🌐 Network: Rootstock Testnet šŸ’° Current Balance: 0.5015859620415593 RBTC šŸ”— Ensure that transactions are being conducted on the correct network. + +# When using --monitor flag: +šŸ“Š Monitoring transaction: 0x86deb77e1d666ae6848630496d672da8b5f48292681bda33f8f04245c55dde26 +ā³ Current status: pending, confirmations: 0 +ā³ Current status: pending, confirmations: 2 +āœ… Transaction confirmed with 12 confirmations! ``` +The `--monitor` flag keeps listening to the transaction status until it reaches the required number of confirmations (default: 12) or fails. The monitoring will automatically stop once the transaction is confirmed or fails. + ### 5. Deploy Smart Contract The deploy command allows you to deploy a smart contract on the Rootstock blockchain. This command supports deployment on both the mainnet and testnet. diff --git a/src/utils/monitoring/MonitorManager.ts b/src/utils/monitoring/MonitorManager.ts index 348f8a0..fc54092 100644 --- a/src/utils/monitoring/MonitorManager.ts +++ b/src/utils/monitoring/MonitorManager.ts @@ -136,6 +136,14 @@ export class MonitorManager { await this.saveState(); console.log(chalk.yellow(`ā¹ļø Stopped monitoring session: ${sessionId}`)); + + const activeSessions = this.getActiveSessions(); + if (activeSessions.length === 0) { + setTimeout(() => { + process.exit(0); + }, 100); + } + return true; } @@ -201,8 +209,12 @@ export class MonitorManager { console.log(chalk.blue(`šŸ“Š TX ${config.txHash.slice(0, 10)}... - Status: ${status}, Confirmations: ${confirmations}`)); - if (confirmations >= (config.confirmations || 12)) { - console.log(chalk.green(`āœ… Transaction ${config.txHash.slice(0, 10)}... confirmed with ${confirmations} confirmations`)); + if (receipt && (status === 'failed' || confirmations >= (config.confirmations || 12))) { + if (status === 'failed') { + console.log(chalk.red(`āŒ Transaction ${config.txHash.slice(0, 10)}... failed`)); + } else { + console.log(chalk.green(`āœ… Transaction ${config.txHash.slice(0, 10)}... confirmed with ${confirmations} confirmations`)); + } await this.stopMonitoring(session.id); } @@ -211,6 +223,10 @@ export class MonitorManager { console.log(chalk.yellow(`āš ļø Transaction ${config.txHash.slice(0, 10)}... not found or pending`)); } else { console.error(chalk.red(`āŒ Error checking transaction ${config.txHash.slice(0, 10)}...:`), error.message || error); + if (session.checkCount > 10) { + console.log(chalk.yellow(`āš ļø Too many errors, stopping monitoring`)); + await this.stopMonitoring(session.id); + } } } } From 2bb5d5b9db62890e4d94e79ba5a3b9ff79bf648f Mon Sep 17 00:00:00 2001 From: pranavkonde Date: Thu, 28 Aug 2025 00:03:46 +0530 Subject: [PATCH 4/7] New Structure CLI --- bin/index.ts | 58 +++----------- src/commands/monitor.ts | 169 +++++++++++++++++++++++++++++----------- 2 files changed, 134 insertions(+), 93 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index ddb7d04..e649bd9 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -307,56 +307,22 @@ program return; } - // Handle transaction monitoring - if (options.tx) { - const formattedTxId = options.tx.startsWith("0x") ? options.tx : `0x${options.tx}`; - const confirmations = options.confirmations ? parseInt(options.confirmations.toString()) : 12; - - console.log(chalk.blue(`šŸ” Starting transaction monitoring...`)); - console.log(chalk.gray(`Network: ${options.testnet ? 'Testnet' : 'Mainnet'}`)); - console.log(chalk.gray(`Transaction: ${formattedTxId}`)); - console.log(chalk.gray(`Required confirmations: ${confirmations}`)); - console.log(''); - - const monitorManager = new MonitorManager(!!options.testnet); - await monitorManager.initialize(); - - const sessionId = await monitorManager.startTransactionMonitoring( - formattedTxId as `0x${string}`, - confirmations, - !!options.testnet - ); - - console.log(chalk.green(`\nšŸŽÆ Monitoring started successfully!`)); - console.log(chalk.blue(`Press Ctrl+C to stop monitoring`)); - console.log(''); - - process.on('SIGINT', async () => { - console.log(chalk.yellow(`\nā¹ļø Stopping monitoring...`)); - await monitorManager.stopMonitoring(sessionId); - process.exit(0); - }); - - setInterval(() => {}, 1000); - return; - } - - if (!options.address) { - console.log(chalk.yellow("šŸ“Š No monitoring target specified. Showing active sessions:")); - await listMonitoringSessions(!!options.testnet); - return; - } - const address = options.address ? (`0x${options.address.replace(/^0x/, "")}` as `0x${string}`) : undefined; - await monitorCommand( - !!options.testnet, - address as Address, - options.balance !== false, - !!options.transactions - ); + const tx = options.tx + ? (options.tx.startsWith("0x") ? options.tx : `0x${options.tx}`) as `0x${string}` + : undefined; + + await monitorCommand({ + testnet: !!options.testnet, + address: address as Address | undefined, + monitorBalance: options.balance !== false, + monitorTransactions: !!options.transactions, + tx, + confirmations: options.confirmations ? parseInt(options.confirmations.toString()) : undefined + }); } catch (error: any) { console.error( chalk.red("Error during monitoring:"), diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index b0475ac..8f7cccc 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -2,45 +2,78 @@ import { MonitorManager } from "../utils/monitoring/MonitorManager.js"; import chalk from "chalk"; import { Address, isAddress } from "viem"; import Table from "cli-table3"; +import ora from "ora"; + +type MonitorCommandOptions = { + testnet: boolean; + address?: Address; + monitorBalance?: boolean; + monitorTransactions?: boolean; + tx?: `0x${string}`; + confirmations?: number; +}; + +function logMessage(message: string, color: any = chalk.white) { + console.log(color(message)); +} -export async function monitorCommand( - testnet: boolean, - address?: Address, - monitorBalance: boolean = true, - monitorTransactions: boolean = false -): Promise { +function logError(message: string) { + logMessage(`āŒ ${message}`, chalk.red); +} + +function logSuccess(message: string) { + logMessage(message, chalk.green); +} + +function logWarning(message: string) { + logMessage(message, chalk.yellow); +} + +function logInfo(message: string) { + logMessage(message, chalk.blue); +} + +export async function monitorCommand(options: MonitorCommandOptions): Promise { try { - const monitorManager = new MonitorManager(testnet); + const spinner = ora('Initializing monitor...').start(); + const monitorManager = new MonitorManager(options.testnet); await monitorManager.initialize(); + spinner.stop(); - if (address) { - if (!isAddress(address)) { - console.error(chalk.red('āŒ Invalid address format.')); - console.log(chalk.gray('Expected: Valid Ethereum/Rootstock address (40 hex characters with 0x prefix)')); - console.log(chalk.gray(`Received: ${address} (length: ${String(address).length})`)); + if (options.tx) { + return await handleTransactionMonitoring(options, monitorManager); + } + + if (options.address) { + if (!isAddress(options.address)) { + logError('Invalid address format.'); + logMessage('Expected: Valid Ethereum/Rootstock address (40 hex characters with 0x prefix)', chalk.gray); + logMessage(`Received: ${options.address} (length: ${String(options.address).length})`, chalk.gray); return; } - console.log(chalk.blue(`šŸ” Starting address monitoring...`)); - console.log(chalk.gray(`Network: ${testnet ? 'Testnet' : 'Mainnet'}`)); - console.log(chalk.gray(`Address: ${address}`)); - console.log(chalk.gray(`Monitor balance: ${monitorBalance ? 'Yes' : 'No'}`)); - console.log(chalk.gray(`Monitor transactions: ${monitorTransactions ? 'Yes' : 'No'}`)); - console.log(''); + logInfo(`šŸ” Starting address monitoring...`); + logMessage(`Network: ${options.testnet ? 'Testnet' : 'Mainnet'}`, chalk.gray); + logMessage(`Address: ${options.address}`, chalk.gray); + logMessage(`Monitor balance: ${options.monitorBalance ? 'Yes' : 'No'}`, chalk.gray); + logMessage(`Monitor transactions: ${options.monitorTransactions ? 'Yes' : 'No'}`, chalk.gray); + logMessage(''); + spinner.start('Starting monitoring...'); const sessionId = await monitorManager.startAddressMonitoring( - address, - monitorBalance, - monitorTransactions, - testnet + options.address, + options.monitorBalance ?? true, + options.monitorTransactions ?? false, + options.testnet ); + spinner.stop(); - console.log(chalk.green(`\nšŸŽÆ Monitoring started successfully!`)); - console.log(chalk.blue(`Press Ctrl+C to stop monitoring`)); - console.log(''); + logSuccess(`\nšŸŽÆ Monitoring started successfully!`); + logInfo(`Press Ctrl+C to stop monitoring`); + logMessage(''); process.on('SIGINT', async () => { - console.log(chalk.yellow(`\nā¹ļø Stopping monitoring...`)); + logWarning(`\nā¹ļø Stopping monitoring...`); await monitorManager.stopMonitoring(sessionId); process.exit(0); }); @@ -51,13 +84,13 @@ export async function monitorCommand( const activeSessions = monitorManager.getActiveSessions(); if (activeSessions.length === 0) { - console.log(chalk.yellow(`šŸ“Š No active monitoring sessions found.`)); - console.log(chalk.gray(`Use --address
to start monitoring an address.`)); + logWarning(`šŸ“Š No active monitoring sessions found.`); + logMessage(`Use --address
to start monitoring an address.`, chalk.gray); return; } - console.log(chalk.blue(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`)); - console.log(''); + logInfo(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`); + logMessage(''); const table = new Table({ head: ['Session ID', 'Type', 'Target', 'Status', 'Checks', 'Started'], @@ -79,36 +112,76 @@ export async function monitorCommand( ]); } - console.log(table.toString()); + logMessage(table.toString()); } } catch (error: any) { if (error.message?.includes('Invalid address format')) { - console.error(chalk.red('āŒ Invalid address format provided.')); - console.log(chalk.gray('Please provide a valid Ethereum/Rootstock address.')); + logError('Invalid address format provided.'); + logMessage('Please provide a valid Ethereum/Rootstock address.', chalk.gray); } else if (error.message?.includes('Failed to initialize monitoring')) { - console.error(chalk.red('āŒ Failed to connect to the network.')); - console.log(chalk.gray('Please check your internet connection and try again.')); + logError('Failed to connect to the network.'); + logMessage('Please check your internet connection and try again.', chalk.gray); } else { - console.error(chalk.red('🚨 Error in monitoring:'), error.message || error); + logError(`Error in monitoring: ${error.message || error}`); } } } +async function handleTransactionMonitoring( + options: MonitorCommandOptions, + monitorManager: MonitorManager +): Promise { + if (!options.tx) { + logError('Transaction ID is required for transaction monitoring.'); + return; + } + + const confirmations = options.confirmations ?? 12; + + logInfo(`šŸ” Starting transaction monitoring...`); + logMessage(`Network: ${options.testnet ? 'Testnet' : 'Mainnet'}`, chalk.gray); + logMessage(`Transaction: ${options.tx}`, chalk.gray); + logMessage(`Required confirmations: ${confirmations}`, chalk.gray); + logMessage(''); + + const spinner = ora('Starting transaction monitoring...').start(); + const sessionId = await monitorManager.startTransactionMonitoring( + options.tx, + confirmations, + options.testnet + ); + spinner.stop(); + + logSuccess(`\nšŸŽÆ Monitoring started successfully!`); + logInfo(`Press Ctrl+C to stop monitoring`); + logMessage(''); + + process.on('SIGINT', async () => { + logWarning(`\nā¹ļø Stopping monitoring...`); + await monitorManager.stopMonitoring(sessionId); + process.exit(0); + }); + + setInterval(() => {}, 1000); +} + export async function listMonitoringSessions(testnet: boolean): Promise { try { + const spinner = ora('Initializing monitor...').start(); const monitorManager = new MonitorManager(testnet); await monitorManager.initialize(); + spinner.stop(); const activeSessions = monitorManager.getActiveSessions(); if (activeSessions.length === 0) { - console.log(chalk.yellow(`šŸ“Š No active monitoring sessions found.`)); + logWarning(`šŸ“Š No active monitoring sessions found.`); return; } - console.log(chalk.blue(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`)); - console.log(''); + logInfo(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`); + logMessage(''); const table = new Table({ head: ['Session ID', 'Type', 'Target', 'Status', 'Checks', 'Started'], @@ -130,34 +203,36 @@ export async function listMonitoringSessions(testnet: boolean): Promise { ]); } - console.log(table.toString()); + logMessage(table.toString()); } catch (error: any) { - console.error(chalk.red('🚨 Error listing sessions:'), error.message || error); + logError(`Error listing sessions: ${error.message || error}`); } } export async function stopMonitoringSession(sessionId: string, testnet: boolean): Promise { try { if (!sessionId || sessionId.length < 8) { - console.error(chalk.red('āŒ Invalid session ID provided.')); - console.log(chalk.gray('Please provide a valid session ID (at least 8 characters).')); + logError('Invalid session ID provided.'); + logMessage('Please provide a valid session ID (at least 8 characters).', chalk.gray); return; } + const spinner = ora('Initializing monitor...').start(); const monitorManager = new MonitorManager(testnet); await monitorManager.initialize(); + spinner.stop(); const success = await monitorManager.stopMonitoring(sessionId); if (success) { - console.log(chalk.green(`āœ… Successfully stopped monitoring session: ${sessionId}`)); + logSuccess(`āœ… Successfully stopped monitoring session: ${sessionId}`); } else { - console.log(chalk.red(`āŒ Failed to stop monitoring session: ${sessionId}`)); - console.log(chalk.gray('Session not found or already stopped.')); + logError(`Failed to stop monitoring session: ${sessionId}`); + logMessage('Session not found or already stopped.', chalk.gray); } } catch (error: any) { - console.error(chalk.red('🚨 Error stopping session:'), error.message || error); + logError(`Error stopping session: ${error.message || error}`); } } \ No newline at end of file From b49b71b30f7f12741f3c06709ef67ef5da32900c Mon Sep 17 00:00:00 2001 From: Pranav Konde Date: Sat, 6 Sep 2025 13:58:40 +0530 Subject: [PATCH 5/7] feat(monitor): improve spinner management and error handling --- bin/index.ts | 2 -- src/commands/monitor.ts | 78 +++++++++++++++++++++++++++-------------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index de9cea2..4494d7e 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -15,9 +15,7 @@ import { batchTransferCommand } from "../src/commands/batchTransfer.js"; import { historyCommand } from "../src/commands/history.js"; import { selectAddress } from "../src/commands/selectAddress.js"; import { monitorCommand, listMonitoringSessions, stopMonitoringSession } from "../src/commands/monitor.js"; -import { transactionCommand } from "../src/commands/transaction.js"; import { parseEther } from "viem"; -import { MonitorManager } from "../src/utils/monitoring/MonitorManager.js"; interface CommandOptions { testnet?: boolean; diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index 8f7cccc..5afe9f4 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -33,12 +33,25 @@ function logInfo(message: string) { logMessage(message, chalk.blue); } +function startSpinner(spinner: any, message: string) { + spinner.start(message); +} + +function stopSpinner(spinner: any) { + spinner.stop(); +} + +function succeedSpinner(spinner: any, message: string) { + spinner.succeed(message); +} + export async function monitorCommand(options: MonitorCommandOptions): Promise { try { - const spinner = ora('Initializing monitor...').start(); + const spinner = ora(); + startSpinner(spinner, 'ā³ Initializing monitor...'); const monitorManager = new MonitorManager(options.testnet); await monitorManager.initialize(); - spinner.stop(); + succeedSpinner(spinner, 'āœ… Monitor initialized successfully'); if (options.tx) { return await handleTransactionMonitoring(options, monitorManager); @@ -59,14 +72,14 @@ export async function monitorCommand(options: MonitorCommandOptions): Promise { - logWarning(`\nā¹ļø Stopping monitoring...`); - await monitorManager.stopMonitoring(sessionId); - process.exit(0); - }); + process.on('SIGINT', async () => { + logWarning(`\nā¹ļø Stopping monitoring...`); + await monitorManager.stopMonitoring(sessionId); + process.exit(0); + }); - setInterval(() => {}, 1000); + setInterval(() => {}, 1000); + } catch (error: any) { + stopSpinner(spinner); + logError(`Error in monitoring: ${error.message || error}`); + return; + } } export async function listMonitoringSessions(testnet: boolean): Promise { try { - const spinner = ora('Initializing monitor...').start(); + const spinner = ora(); + startSpinner(spinner, 'ā³ Initializing monitor...'); const monitorManager = new MonitorManager(testnet); await monitorManager.initialize(); - spinner.stop(); + succeedSpinner(spinner, 'āœ… Monitor initialized successfully'); const activeSessions = monitorManager.getActiveSessions(); @@ -218,16 +239,19 @@ export async function stopMonitoringSession(sessionId: string, testnet: boolean) return; } - const spinner = ora('Initializing monitor...').start(); + const spinner = ora(); + startSpinner(spinner, 'ā³ Initializing monitor...'); const monitorManager = new MonitorManager(testnet); await monitorManager.initialize(); - spinner.stop(); + succeedSpinner(spinner, 'āœ… Monitor initialized successfully'); + startSpinner(spinner, 'ā³ Stopping monitoring session...'); const success = await monitorManager.stopMonitoring(sessionId); - if (success) { - logSuccess(`āœ… Successfully stopped monitoring session: ${sessionId}`); + succeedSpinner(spinner, 'āœ… Monitoring session stopped successfully'); + logSuccess(`Session ${sessionId} stopped successfully`); } else { + stopSpinner(spinner); logError(`Failed to stop monitoring session: ${sessionId}`); logMessage('Session not found or already stopped.', chalk.gray); } From 9fc990f3d46ca35a5d98b12edb71524707bfa3c2 Mon Sep 17 00:00:00 2001 From: Pranav Konde Date: Thu, 11 Sep 2025 00:59:08 +0530 Subject: [PATCH 6/7] fix(monitor): Improve logging for external interactions --- package-lock.json | 38 ++++++------- package.json | 6 ++- src/utils/monitoring/MonitorManager.ts | 75 ++++++++++++++++---------- 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ecc007..a9df376 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@rsksmart/rsk-cli", - "version": "1.2.1", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rsksmart/rsk-cli", - "version": "1.2.1", + "version": "1.3.1", "license": "MIT", "dependencies": { "@openzeppelin/contracts": "^5.0.2", "@rsksmart/rsk-precompiled-abis": "^6.0.0-ARROWHEAD", "@types/fs-extra": "^11.0.4", + "@types/zxcvbn": "^4.4.5", "chalk": "^5.3.0", "cli-table3": "^0.6.5", "commander": "^13.1.0", @@ -19,8 +20,8 @@ "fs-extra": "^11.2.0", "inquirer": "^12.1.0", "ora": "^8.0.1", - "uuid": "^9.0.1", - "viem": "^2.19.4" + "viem": "^2.19.4", + "zxcvbn": "^4.4.2" }, "bin": { "rsk-cli": "dist/bin/index.js" @@ -28,7 +29,6 @@ "devDependencies": { "@types/bun": "latest", "@types/figlet": "^1.5.8", - "@types/uuid": "^10.0.0", "solc": "0.8.28", "typescript": "^5.0.0" }, @@ -476,11 +476,10 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", - "dev": true, + "node_modules/@types/zxcvbn": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.5.tgz", + "integrity": "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==", "license": "MIT" }, "node_modules/abitype": { @@ -1226,19 +1225,6 @@ "node": ">= 10.0.0" } }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/viem": { "version": "2.28.2", "resolved": "https://registry.npmjs.org/viem/-/viem-2.28.2.tgz", @@ -1315,6 +1301,12 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zxcvbn": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz", + "integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 420b82a..70c3d84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rsksmart/rsk-cli", - "version": "1.2.1", + "version": "1.3.1", "description": "CLI tool for Rootstock network using Viem", "repository": { "type": "git", @@ -48,6 +48,7 @@ "@openzeppelin/contracts": "^5.0.2", "@rsksmart/rsk-precompiled-abis": "^6.0.0-ARROWHEAD", "@types/fs-extra": "^11.0.4", + "@types/zxcvbn": "^4.4.5", "chalk": "^5.3.0", "cli-table3": "^0.6.5", "commander": "^13.1.0", @@ -56,6 +57,7 @@ "inquirer": "^12.1.0", "ora": "^8.0.1", "uuid": "^9.0.1", - "viem": "^2.19.4" + "viem": "^2.19.4", + "zxcvbn": "^4.4.2" } } diff --git a/src/utils/monitoring/MonitorManager.ts b/src/utils/monitoring/MonitorManager.ts index fc54092..c305607 100644 --- a/src/utils/monitoring/MonitorManager.ts +++ b/src/utils/monitoring/MonitorManager.ts @@ -1,7 +1,6 @@ -import { PublicClient, TransactionReceipt, Address, isAddress } from 'viem'; +import { PublicClient, Address, isAddress } from 'viem'; import ViemProvider from '../viemProvider.js'; import { - MonitoringConfig, MonitoringSession, TransactionMonitoringConfig, AddressMonitoringConfig, @@ -9,8 +8,28 @@ import { } from '../types.js'; import fs from 'fs'; import path from 'path'; -import chalk from 'chalk'; import { v4 } from 'uuid'; +import chalk from 'chalk'; + +function logMessage(message: string, color: any = chalk.white) { + console.log(color(message)); +} + +function logError(message: string) { + logMessage(`āŒ ${message}`, chalk.red); +} + +function logSuccess(message: string) { + logMessage(`āœ… ${message}`, chalk.green); +} + +function logWarning(message: string) { + logMessage(`āš ļø ${message}`, chalk.yellow); +} + +function logInfo(message: string) { + logMessage(`šŸ“Š ${message}`, chalk.blue); +} export class MonitorManager { private sessions: Map = new Map(); @@ -33,7 +52,7 @@ export class MonitorManager { await this.loadState(); this.isInitialized = true; } catch (error) { - console.error(chalk.red('āŒ Failed to initialize monitoring:'), error); + logError(`Failed to initialize monitoring: ${error}`); throw error; } } @@ -71,8 +90,8 @@ export class MonitorManager { this.startPolling(sessionId); await this.saveState(); - console.log(chalk.green(`āœ… Started monitoring transaction: ${txHash}`)); - console.log(chalk.blue(`šŸ“Š Session ID: ${sessionId}`)); + logSuccess(`Started monitoring transaction: ${txHash}`); + logInfo(`Session ID: ${sessionId}`); return sessionId; } @@ -112,8 +131,8 @@ export class MonitorManager { this.startPolling(sessionId); await this.saveState(); - console.log(chalk.green(`āœ… Started monitoring address: ${address}`)); - console.log(chalk.blue(`šŸ“Š Session ID: ${sessionId}`)); + logSuccess(`Started monitoring address: ${address}`); + logInfo(`Session ID: ${sessionId}`); return sessionId; } @@ -121,7 +140,7 @@ export class MonitorManager { async stopMonitoring(sessionId: string): Promise { const session = this.sessions.get(sessionId); if (!session) { - console.log(chalk.red(`āŒ Session ${sessionId} not found`)); + logError(`Session ${sessionId} not found`); return false; } @@ -135,7 +154,7 @@ export class MonitorManager { } await this.saveState(); - console.log(chalk.yellow(`ā¹ļø Stopped monitoring session: ${sessionId}`)); + logWarning(`Stopped monitoring session: ${sessionId}`); const activeSessions = this.getActiveSessions(); if (activeSessions.length === 0) { @@ -151,7 +170,7 @@ export class MonitorManager { for (const [sessionId] of this.sessions) { await this.stopMonitoring(sessionId); } - console.log(chalk.yellow(`ā¹ļø Stopped all monitoring sessions`)); + logWarning(`Stopped all monitoring sessions`); } getActiveSessions(): MonitoringSession[] { @@ -166,12 +185,11 @@ export class MonitorManager { try { await this.checkSession(sessionId); } catch (error) { - console.error(chalk.red(`āŒ Error checking session ${sessionId}:`), error); + logError(`Error checking session ${sessionId}: ${error}`); - const session = this.sessions.get(sessionId); if (session && session.checkCount > 10) { - console.log(chalk.yellow(`āš ļø Too many errors, stopping session ${sessionId}`)); + logWarning(`Too many errors, stopping session ${sessionId}`); await this.stopMonitoring(sessionId); } } @@ -200,31 +218,30 @@ export class MonitorManager { const config = session.config as TransactionMonitoringConfig; try { - const tx = await this.publicClient.getTransaction({ hash: config.txHash as `0x${string}` }); const receipt = await this.publicClient.getTransactionReceipt({ hash: config.txHash as `0x${string}` }); const currentBlock = await this.publicClient.getBlockNumber(); const confirmations = receipt ? Number(currentBlock - receipt.blockNumber) : 0; const status = receipt ? (receipt.status === 'success' ? 'confirmed' : 'failed') : 'pending'; - console.log(chalk.blue(`šŸ“Š TX ${config.txHash.slice(0, 10)}... - Status: ${status}, Confirmations: ${confirmations}`)); + logInfo(`TX ${config.txHash.slice(0, 10)}... - Status: ${status}, Confirmations: ${confirmations}`); if (receipt && (status === 'failed' || confirmations >= (config.confirmations || 12))) { if (status === 'failed') { - console.log(chalk.red(`āŒ Transaction ${config.txHash.slice(0, 10)}... failed`)); + logError(`Transaction ${config.txHash.slice(0, 10)}... failed`); } else { - console.log(chalk.green(`āœ… Transaction ${config.txHash.slice(0, 10)}... confirmed with ${confirmations} confirmations`)); + logSuccess(`Transaction ${config.txHash.slice(0, 10)}... confirmed with ${confirmations} confirmations`); } await this.stopMonitoring(session.id); } } catch (error: any) { if (error.message?.includes('not found') || error.message?.includes('pending')) { - console.log(chalk.yellow(`āš ļø Transaction ${config.txHash.slice(0, 10)}... not found or pending`)); + logWarning(`Transaction ${config.txHash.slice(0, 10)}... not found or pending`); } else { - console.error(chalk.red(`āŒ Error checking transaction ${config.txHash.slice(0, 10)}...:`), error.message || error); + logError(`Error checking transaction ${config.txHash.slice(0, 10)}...: ${error.message || error}`); if (session.checkCount > 10) { - console.log(chalk.yellow(`āš ļø Too many errors, stopping monitoring`)); + logWarning(`Too many errors, stopping monitoring`); await this.stopMonitoring(session.id); } } @@ -237,22 +254,22 @@ export class MonitorManager { try { if (config.monitorBalance) { const currentBalance = await this.publicClient.getBalance({ address: config.address }); - console.log(chalk.blue(`šŸ’° Address ${config.address.slice(0, 10)}... - Balance: ${currentBalance} wei`)); + logInfo(`Address ${config.address.slice(0, 10)}... - Balance: ${currentBalance} wei`); } if (config.monitorTransactions) { - console.log(chalk.blue(`šŸ“ Checking transactions for ${config.address.slice(0, 10)}...`)); + logInfo(`Checking transactions for ${config.address.slice(0, 10)}...`); } } catch (error: any) { if (error.message?.includes('Invalid address')) { - console.error(chalk.red(`āŒ Invalid address format: ${config.address}`)); - console.log(chalk.yellow(`āš ļø Stopping monitoring for 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')) { - console.log(chalk.yellow(`āš ļø Rate limited, will retry later`)); + logWarning(`Rate limited, will retry later`); } else { - console.error(chalk.red(`āŒ Error checking address ${config.address.slice(0, 10)}...:`), error.message || error); + logError(`Error checking address ${config.address.slice(0, 10)}...: ${error.message || error}`); } } } @@ -271,7 +288,7 @@ export class MonitorManager { } } } catch (error) { - console.log(chalk.yellow(`āš ļø Could not load monitoring state: ${error}`)); + logWarning(`Could not load monitoring state: ${error}`); } } @@ -288,7 +305,7 @@ export class MonitorManager { fs.writeFileSync(this.stateFilePath, JSON.stringify(state, null, 2)); } catch (error) { - console.error(chalk.red(`āŒ Could not save monitoring state: ${error}`)); + logError(`Could not save monitoring state: ${error}`); } } } \ No newline at end of file From 161a2b4492ae67932b892bca334d895c358558a9 Mon Sep 17 00:00:00 2001 From: Pranav Konde Date: Wed, 1 Oct 2025 22:33:12 +0530 Subject: [PATCH 7/7] fix: implement TX command monitoring and improve error handling --- bin/index.ts | 5 +- src/commands/monitor.ts | 188 +++++++++++++++++++++++----------------- src/commands/tx.ts | 113 +++++++++++++++++++++++- 3 files changed, 222 insertions(+), 84 deletions(-) diff --git a/bin/index.ts b/bin/index.ts index f2b842a..a265a3f 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -162,6 +162,8 @@ program testnet: !!options.testnet, txid: formattedTxId as `0x${string}`, isExternal: false, + monitor: !!options.monitor, + confirmations: options.confirmations ? parseInt(options.confirmations.toString()) : undefined, }); }); @@ -319,7 +321,8 @@ program monitorBalance: options.balance !== false, monitorTransactions: !!options.transactions, tx, - confirmations: options.confirmations ? parseInt(options.confirmations.toString()) : undefined + confirmations: options.confirmations ? parseInt(options.confirmations.toString()) : undefined, + isExternal: false }); } catch (error: any) { console.error( diff --git a/src/commands/monitor.ts b/src/commands/monitor.ts index 5afe9f4..1aab6ef 100644 --- a/src/commands/monitor.ts +++ b/src/commands/monitor.ts @@ -11,47 +11,68 @@ type MonitorCommandOptions = { monitorTransactions?: boolean; tx?: `0x${string}`; confirmations?: number; + isExternal?: boolean; }; -function logMessage(message: string, color: any = chalk.white) { - console.log(color(message)); +function logMessage( + params: MonitorCommandOptions, + message: string, + color: any = chalk.white +) { + if (!params.isExternal) { + console.log(color(message)); + } } -function logError(message: string) { - logMessage(`āŒ ${message}`, chalk.red); +function logError(params: MonitorCommandOptions, message: string) { + logMessage(params, `āŒ ${message}`, chalk.red); } -function logSuccess(message: string) { - logMessage(message, chalk.green); +function logSuccess(params: MonitorCommandOptions, message: string) { + logMessage(params, message, chalk.green); } -function logWarning(message: string) { - logMessage(message, chalk.yellow); +function logWarning(params: MonitorCommandOptions, message: string) { + logMessage(params, message, chalk.yellow); } -function logInfo(message: string) { - logMessage(message, chalk.blue); +function logInfo(params: MonitorCommandOptions, message: string) { + logMessage(params, message, chalk.blue); } -function startSpinner(spinner: any, message: string) { - spinner.start(message); +function startSpinner( + params: MonitorCommandOptions, + spinner: any, + message: string +) { + if (!params.isExternal) { + spinner.start(message); + } } -function stopSpinner(spinner: any) { - spinner.stop(); +function stopSpinner(params: MonitorCommandOptions, spinner: any) { + if (!params.isExternal) { + spinner.stop(); + } } -function succeedSpinner(spinner: any, message: string) { - spinner.succeed(message); +function succeedSpinner( + params: MonitorCommandOptions, + spinner: any, + message: string +) { + if (!params.isExternal) { + spinner.succeed(message); + } } export async function monitorCommand(options: MonitorCommandOptions): Promise { try { - const spinner = ora(); - startSpinner(spinner, 'ā³ Initializing monitor...'); + const spinner = options.isExternal ? ora({isEnabled: false}) : ora(); + startSpinner(options, spinner, 'ā³ Initializing monitor...'); const monitorManager = new MonitorManager(options.testnet); await monitorManager.initialize(); - succeedSpinner(spinner, 'āœ… Monitor initialized successfully'); + succeedSpinner(options, spinner, 'āœ… Monitor initialized successfully'); if (options.tx) { return await handleTransactionMonitoring(options, monitorManager); @@ -59,34 +80,34 @@ export async function monitorCommand(options: MonitorCommandOptions): Promise { - logWarning(`\nā¹ļø Stopping monitoring...`); + logWarning(options, `\nā¹ļø Stopping monitoring...`); await monitorManager.stopMonitoring(sessionId); process.exit(0); }); @@ -97,13 +118,13 @@ export async function monitorCommand(options: MonitorCommandOptions): Promise to start monitoring an address.`, chalk.gray); + logWarning(options, `šŸ“Š No active monitoring sessions found.`); + logMessage(options, `Use --address
to start monitoring an address.`, chalk.gray); return; } - logInfo(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`); - logMessage(''); + logInfo(options, `šŸ“Š Active Monitoring Sessions (${activeSessions.length})`); + logMessage(options, ''); const table = new Table({ head: ['Session ID', 'Type', 'Target', 'Status', 'Checks', 'Started'], @@ -125,18 +146,18 @@ export async function monitorCommand(options: MonitorCommandOptions): Promise { if (!options.tx) { - logError('Transaction ID is required for transaction monitoring.'); + logError(options, 'Transaction ID is required for transaction monitoring.'); return; } const confirmations = options.confirmations ?? 12; - logInfo(`šŸ” Starting transaction monitoring...`); - logMessage(`Network: ${options.testnet ? 'Testnet' : 'Mainnet'}`, chalk.gray); - logMessage(`Transaction: ${options.tx}`, chalk.gray); - logMessage(`Required confirmations: ${confirmations}`, chalk.gray); - logMessage(''); + logInfo(options, `šŸ” Starting transaction monitoring...`); + logMessage(options, `Network: ${options.testnet ? 'Testnet' : 'Mainnet'}`, chalk.gray); + logMessage(options, `Transaction: ${options.tx}`, chalk.gray); + logMessage(options, `Required confirmations: ${confirmations}`, chalk.gray); + logMessage(options, ''); - const spinner = ora(); + const spinner = options.isExternal ? ora({isEnabled: false}) : ora(); try { - startSpinner(spinner, 'ā³ Starting transaction monitoring...'); + startSpinner(options, spinner, 'ā³ Starting transaction monitoring...'); const sessionId = await monitorManager.startTransactionMonitoring( options.tx, confirmations, options.testnet ); - succeedSpinner(spinner, 'āœ… Transaction monitoring started successfully'); + succeedSpinner(options, spinner, 'āœ… Transaction monitoring started successfully'); - logSuccess(`\nšŸŽÆ Monitoring started successfully!`); - logInfo(`Press Ctrl+C to stop monitoring`); - logMessage(''); + logSuccess(options, `\nšŸŽÆ Monitoring started successfully!`); + logInfo(options, `Press Ctrl+C to stop monitoring`); + logMessage(options, ''); process.on('SIGINT', async () => { - logWarning(`\nā¹ļø Stopping monitoring...`); + logWarning(options, `\nā¹ļø Stopping monitoring...`); await monitorManager.stopMonitoring(sessionId); process.exit(0); }); setInterval(() => {}, 1000); } catch (error: any) { - stopSpinner(spinner); - logError(`Error in monitoring: ${error.message || error}`); + stopSpinner(options, spinner); + logError(options, `Error in monitoring: ${error.message || error}`); return; } } -export async function listMonitoringSessions(testnet: boolean): Promise { +export async function listMonitoringSessions(testnet: boolean, isExternal: boolean = false): Promise { try { - const spinner = ora(); - startSpinner(spinner, 'ā³ Initializing monitor...'); + const options: MonitorCommandOptions = { testnet, isExternal }; + const spinner = isExternal ? ora({isEnabled: false}) : ora(); + startSpinner(options, spinner, 'ā³ Initializing monitor...'); const monitorManager = new MonitorManager(testnet); await monitorManager.initialize(); - succeedSpinner(spinner, 'āœ… Monitor initialized successfully'); + succeedSpinner(options, spinner, 'āœ… Monitor initialized successfully'); const activeSessions = monitorManager.getActiveSessions(); if (activeSessions.length === 0) { - logWarning(`šŸ“Š No active monitoring sessions found.`); + logWarning(options, `šŸ“Š No active monitoring sessions found.`); return; } - logInfo(`šŸ“Š Active Monitoring Sessions (${activeSessions.length})`); - logMessage(''); + logInfo(options, `šŸ“Š Active Monitoring Sessions (${activeSessions.length})`); + logMessage(options, ''); const table = new Table({ head: ['Session ID', 'Type', 'Target', 'Status', 'Checks', 'Started'], @@ -224,39 +246,43 @@ export async function listMonitoringSessions(testnet: boolean): Promise { ]); } - logMessage(table.toString()); + logMessage(options, table.toString()); } catch (error: any) { - logError(`Error listing sessions: ${error.message || error}`); + const options: MonitorCommandOptions = { testnet, isExternal }; + logError(options, `Error listing sessions: ${error.message || error}`); } } -export async function stopMonitoringSession(sessionId: string, testnet: boolean): Promise { +export async function stopMonitoringSession(sessionId: string, testnet: boolean, isExternal: boolean = false): Promise { try { + const options: MonitorCommandOptions = { testnet, isExternal }; + if (!sessionId || sessionId.length < 8) { - logError('Invalid session ID provided.'); - logMessage('Please provide a valid session ID (at least 8 characters).', chalk.gray); + logError(options, 'Invalid session ID provided.'); + logMessage(options, 'Please provide a valid session ID (at least 8 characters).', chalk.gray); return; } - const spinner = ora(); - startSpinner(spinner, 'ā³ Initializing monitor...'); + const spinner = isExternal ? ora({isEnabled: false}) : ora(); + startSpinner(options, spinner, 'ā³ Initializing monitor...'); const monitorManager = new MonitorManager(testnet); await monitorManager.initialize(); - succeedSpinner(spinner, 'āœ… Monitor initialized successfully'); + succeedSpinner(options, spinner, 'āœ… Monitor initialized successfully'); - startSpinner(spinner, 'ā³ Stopping monitoring session...'); + startSpinner(options, spinner, 'ā³ Stopping monitoring session...'); const success = await monitorManager.stopMonitoring(sessionId); if (success) { - succeedSpinner(spinner, 'āœ… Monitoring session stopped successfully'); - logSuccess(`Session ${sessionId} stopped successfully`); + succeedSpinner(options, spinner, 'āœ… Monitoring session stopped successfully'); + logSuccess(options, `Session ${sessionId} stopped successfully`); } else { - stopSpinner(spinner); - logError(`Failed to stop monitoring session: ${sessionId}`); - logMessage('Session not found or already stopped.', chalk.gray); + stopSpinner(options, spinner); + logError(options, `Failed to stop monitoring session: ${sessionId}`); + logMessage(options, 'Session not found or already stopped.', chalk.gray); } } catch (error: any) { - logError(`Error stopping session: ${error.message || error}`); + const options: MonitorCommandOptions = { testnet, isExternal }; + logError(options, `Error stopping session: ${error.message || error}`); } } \ No newline at end of file diff --git a/src/commands/tx.ts b/src/commands/tx.ts index 165487b..626c5d2 100644 --- a/src/commands/tx.ts +++ b/src/commands/tx.ts @@ -2,11 +2,15 @@ import ViemProvider from "../utils/viemProvider.js"; import chalk from "chalk"; import Table from "cli-table3"; import { DataTx, TxResult } from "../utils/types.js"; +import { MonitorManager } from "../utils/monitoring/MonitorManager.js"; +import ora from "ora"; type TxCommandOptions = { testnet: boolean; txid: string; isExternal?: boolean; + monitor?: boolean; + confirmations?: number; }; function logMessage( @@ -23,6 +27,44 @@ function logError(params: TxCommandOptions, message: string) { logMessage(params, `āŒ ${message}`, chalk.red); } +function logSuccess(params: TxCommandOptions, message: string) { + logMessage(params, message, chalk.green); +} + +function logInfo(params: TxCommandOptions, message: string) { + logMessage(params, message, chalk.blue); +} + +function logWarning(params: TxCommandOptions, message: string) { + logMessage(params, message, chalk.yellow); +} + +function startSpinner( + params: TxCommandOptions, + spinner: any, + message: string +) { + if (!params.isExternal) { + spinner.start(message); + } +} + +function stopSpinner(params: TxCommandOptions, spinner: any) { + if (!params.isExternal) { + spinner.stop(); + } +} + +function succeedSpinner( + params: TxCommandOptions, + spinner: any, + message: string +) { + if (!params.isExternal) { + spinner.succeed(message); + } +} + export async function txCommand( params: TxCommandOptions ): Promise { @@ -37,7 +79,9 @@ export async function txCommand( }); if (!txReceipt) { - const errorMessage = "Transaction not found. Please check the transaction ID and try again."; + const network = params.testnet ? "testnet" : "mainnet"; + const oppositeNetwork = params.testnet ? "mainnet" : "testnet"; + const errorMessage = `Transaction not found on ${network}. Please check the transaction ID and network. Try with --testnet flag if the transaction is on ${oppositeNetwork}.`; logError(params, errorMessage); return { error: errorMessage, @@ -74,12 +118,18 @@ export async function txCommand( console.log(table.toString()); } + if (params.monitor) { + return await handleTransactionMonitoring(params, txidWithCorrectType); + } + return { success: true, data: txData, }; } catch (error) { - const errorMessage = "Error checking transaction status, please check the transaction ID."; + const network = params.testnet ? "testnet" : "mainnet"; + const oppositeNetwork = params.testnet ? "mainnet" : "testnet"; + const errorMessage = `Error checking transaction status on ${network}. Please check the transaction ID and network. Try with --testnet flag if the transaction is on ${oppositeNetwork}.`; logError(params, errorMessage); @@ -89,3 +139,62 @@ export async function txCommand( }; } } + +async function handleTransactionMonitoring( + params: TxCommandOptions, + txHash: `0x${string}` +): Promise { + const spinner = params.isExternal ? ora({isEnabled: false}) : ora(); + + try { + const confirmations = params.confirmations ?? 12; + + logInfo(params, `šŸ” Starting transaction monitoring...`); + logMessage(params, `Network: ${params.testnet ? 'Testnet' : 'Mainnet'}`, chalk.gray); + logMessage(params, `Transaction: ${txHash}`, chalk.gray); + logMessage(params, `Required confirmations: ${confirmations}`, chalk.gray); + logMessage(params, ''); + + startSpinner(params, spinner, 'ā³ Initializing monitor...'); + const monitorManager = new MonitorManager(params.testnet); + await monitorManager.initialize(); + succeedSpinner(params, spinner, 'āœ… Monitor initialized successfully'); + + startSpinner(params, spinner, 'ā³ Starting transaction monitoring...'); + const sessionId = await monitorManager.startTransactionMonitoring( + txHash, + confirmations, + params.testnet + ); + succeedSpinner(params, spinner, 'āœ… Transaction monitoring started successfully'); + + logSuccess(params, `\nšŸŽÆ Monitoring started successfully!`); + logInfo(params, `Press Ctrl+C to stop monitoring`); + logMessage(params, ''); + + process.on('SIGINT', async () => { + logWarning(params, `\nā¹ļø Stopping monitoring...`); + await monitorManager.stopMonitoring(sessionId); + process.exit(0); + }); + + setInterval(() => {}, 1000); + + return { + success: true, + data: { + txId: txHash, + network: params.testnet ? "Rootstock Testnet" : "Rootstock Mainnet", + monitoring: true, + sessionId: sessionId + } as any, + }; + } catch (error: any) { + stopSpinner(params, spinner); + logError(params, `Error in monitoring: ${error.message || error}`); + return { + error: error.message || error, + success: false, + }; + } +}