/ correlate-tx-monitor.js
correlate-tx-monitor.js
1 #!/usr/bin/env node 2 3 /** 4 * Transaction Correlation Monitor 5 * 6 * Monitors Espresso testnet and Caff Node over 10-minute windows 7 * Analyzes transaction correlation with 5s-3min delay tolerance 8 * 9 * Usage: 10 * node correlate-tx-monitor.js [duration_minutes] 11 * 12 * Example: 13 * node correlate-tx-monitor.js 10 # Monitor for 10 minutes 14 * node correlate-tx-monitor.js # Default 10 minutes 15 */ 16 17 const ESPRESSO_API = 'https://query.decaf.testnet.espresso.network/v0'; 18 const CAFF_NODE_RPC = 'https://rari.caff.testnet.espresso.network'; 19 const RARI_NAMESPACE = 1380012617; 20 21 // Configuration 22 const DELAY_MIN = 5; // 5 seconds minimum delay 23 const DELAY_MAX = 180; // 3 minutes maximum delay 24 const POLL_INTERVAL = 5000; // Poll every 5 seconds 25 26 // State 27 const espressoTransactions = new Map(); 28 const caffNodeTransactions = new Map(); 29 const correlations = []; 30 let startTime = Date.now(); 31 32 // Colors for console output 33 const colors = { 34 reset: '\x1b[0m', 35 bright: '\x1b[1m', 36 green: '\x1b[32m', 37 yellow: '\x1b[33m', 38 blue: '\x1b[34m', 39 cyan: '\x1b[36m', 40 red: '\x1b[31m' 41 }; 42 43 function log(message, color = 'reset') { 44 console.log(`${colors[color]}${message}${colors.reset}`); 45 } 46 47 function formatTime(timestamp) { 48 return new Date(timestamp * 1000).toISOString().replace('T', ' ').slice(0, -5); 49 } 50 51 function formatHash(hash, length = 12) { 52 if (!hash) return 'N/A'; 53 if (hash.startsWith('TX~')) { 54 return 'TX~' + hash.slice(3, 3 + length); 55 } 56 if (hash.startsWith('0x')) { 57 return '0x' + hash.slice(2, 2 + length); 58 } 59 return hash.slice(0, length); 60 } 61 62 /** 63 * Fetch Espresso block 64 */ 65 async function fetchEspressoBlock(height) { 66 try { 67 const response = await fetch(`${ESPRESSO_API}/availability/block/${height}`); 68 if (!response.ok) return null; 69 70 const data = await response.json(); 71 return { 72 height: data.header?.fields?.height || height, 73 timestamp: data.header?.fields?.timestamp || 0, 74 hash: data.hash || 'unknown', 75 size: data.size || 0, 76 num_transactions: data.num_transactions || 0 77 }; 78 } catch (error) { 79 return null; 80 } 81 } 82 83 /** 84 * Fetch Espresso namespace transactions 85 */ 86 async function fetchEspressoNamespaceTransactions(height, namespace) { 87 try { 88 const response = await fetch(`${ESPRESSO_API}/availability/block/${height}/namespace/${namespace}`); 89 if (!response.ok) return []; 90 91 const data = await response.json(); 92 if (!data.transactions || data.transactions.length === 0) return []; 93 94 return data.transactions.map((tx, index) => ({ 95 hash: tx.commit || `TX~unknown_${index}`, 96 block_height: height, 97 index: index, 98 namespace: namespace, 99 size: tx.payload ? Buffer.from(tx.payload, 'base64').length : 0, 100 payload: tx.payload ? tx.payload.slice(0, 20) + '...' : null 101 })); 102 } catch (error) { 103 return []; 104 } 105 } 106 107 /** 108 * Fetch Caff Node block 109 */ 110 async function caffNodeCall(method, params) { 111 try { 112 const response = await fetch(CAFF_NODE_RPC, { 113 method: 'POST', 114 headers: { 'Content-Type': 'application/json' }, 115 body: JSON.stringify({ 116 jsonrpc: '2.0', 117 method, 118 params, 119 id: 1 120 }) 121 }); 122 123 if (!response.ok) return null; 124 const data = await response.json(); 125 return data.error ? null : data.result; 126 } catch (error) { 127 return null; 128 } 129 } 130 131 async function fetchCaffNodeBlock(number) { 132 const blockParam = `0x${number.toString(16)}`; 133 const block = await caffNodeCall('eth_getBlockByNumber', [blockParam, true]); 134 135 if (!block) return null; 136 137 return { 138 number: parseInt(block.number, 16), 139 timestamp: parseInt(block.timestamp, 16), 140 hash: block.hash, 141 transactions: (block.transactions || []).map((tx, index) => ({ 142 hash: tx.hash, 143 blockNumber: parseInt(tx.blockNumber, 16), 144 transactionIndex: parseInt(tx.transactionIndex, 16), 145 from: tx.from, 146 to: tx.to, 147 value: tx.value, 148 input: tx.input, 149 gas: parseInt(tx.gas, 16), 150 size: tx.input ? tx.input.length / 2 : 0 151 })) 152 }; 153 } 154 155 /** 156 * Calculate correlation confidence 157 */ 158 function calculateConfidence(espressoTx, caffTx, timeDiff) { 159 let confidence = 1.0; 160 161 // Timestamp penalty (40% weight) 162 if (timeDiff > 60) confidence -= 0.15; // > 1 min 163 if (timeDiff > 120) confidence -= 0.15; // > 2 min 164 if (timeDiff > 180) confidence -= 0.10; // > 3 min 165 166 // Index match (20% weight) 167 if (espressoTx.index !== caffTx.transactionIndex) { 168 const indexDiff = Math.abs(espressoTx.index - caffTx.transactionIndex); 169 if (indexDiff > 0) confidence -= 0.10; 170 if (indexDiff > 2) confidence -= 0.10; 171 } 172 173 // Size similarity (15% weight) 174 if (espressoTx.size && caffTx.size) { 175 const sizeDiff = Math.abs(espressoTx.size - caffTx.size); 176 if (sizeDiff > 100) confidence -= 0.05; 177 if (sizeDiff > 1000) confidence -= 0.10; 178 } 179 180 // Namespace correctness (15% weight) 181 if (espressoTx.namespace !== RARI_NAMESPACE) { 182 confidence -= 0.15; 183 } 184 185 return Math.max(0, Math.min(1, confidence)); 186 } 187 188 /** 189 * Try to correlate transactions 190 */ 191 function correlateTransactions() { 192 const newCorrelations = []; 193 194 // For each Caff Node transaction 195 caffNodeTransactions.forEach((caffTx, caffHash) => { 196 const caffTimestamp = caffTx.timestamp; 197 198 // Search Espresso transactions within delay window 199 espressoTransactions.forEach((espressoTx, espressoHash) => { 200 const espressoTimestamp = espressoTx.timestamp; 201 202 // Calculate time difference 203 const timeDiff = Math.abs(caffTimestamp - espressoTimestamp); 204 205 // Check if within delay tolerance 206 if (timeDiff >= DELAY_MIN && timeDiff <= DELAY_MAX) { 207 const confidence = calculateConfidence(espressoTx, caffTx, timeDiff); 208 209 // Only consider matches with >40% confidence 210 if (confidence >= 0.4) { 211 newCorrelations.push({ 212 espresso_tx: espressoHash, 213 espresso_block: espressoTx.block_height, 214 espresso_index: espressoTx.index, 215 espresso_timestamp: espressoTimestamp, 216 caff_tx: caffHash, 217 caff_block: caffTx.blockNumber, 218 caff_index: caffTx.transactionIndex, 219 caff_timestamp: caffTimestamp, 220 time_diff: timeDiff, 221 confidence: confidence, 222 matched_at: Date.now() 223 }); 224 } 225 } 226 }); 227 }); 228 229 // Sort by confidence and remove duplicates 230 newCorrelations.sort((a, b) => b.confidence - a.confidence); 231 232 // Add new correlations (avoid duplicates) 233 newCorrelations.forEach(corr => { 234 const exists = correlations.some(c => 235 c.espresso_tx === corr.espresso_tx && c.caff_tx === corr.caff_tx 236 ); 237 if (!exists) { 238 correlations.push(corr); 239 logCorrelation(corr); 240 } 241 }); 242 } 243 244 function logCorrelation(corr) { 245 const confidenceColor = 246 corr.confidence >= 0.8 ? 'green' : 247 corr.confidence >= 0.6 ? 'yellow' : 'red'; 248 249 log(`\n${'='.repeat(80)}`, 'cyan'); 250 log('š NEW CORRELATION FOUND', 'bright'); 251 log(`${'='.repeat(80)}`, 'cyan'); 252 253 log(`\n Espresso TX: ${formatHash(corr.espresso_tx, 20)}`, 'blue'); 254 log(` āā Block: #${corr.espresso_block} (index: ${corr.espresso_index})`, 'blue'); 255 log(` āā Time: ${formatTime(corr.espresso_timestamp)}`, 'blue'); 256 257 log(`\n Caff Node TX: ${formatHash(corr.caff_tx, 20)}`, 'yellow'); 258 log(` āā Block: #${corr.caff_block} (index: ${corr.caff_index})`, 'yellow'); 259 log(` āā Time: ${formatTime(corr.caff_timestamp)}`, 'yellow'); 260 261 log(`\n Time Diff: ${corr.time_diff}s`, 'cyan'); 262 log(` Confidence: ${(corr.confidence * 100).toFixed(1)}%`, confidenceColor); 263 log(`${'='.repeat(80)}\n`, 'cyan'); 264 } 265 266 /** 267 * Monitor Espresso network 268 */ 269 async function monitorEspresso(startBlock, endBlock) { 270 log(`\nš” Monitoring Espresso blocks ${startBlock} to ${endBlock}...`, 'blue'); 271 272 let txCount = 0; 273 274 for (let height = startBlock; height <= endBlock; height++) { 275 const block = await fetchEspressoBlock(height); 276 if (!block) continue; 277 278 // Get namespace transactions 279 const txs = await fetchEspressoNamespaceTransactions(height, RARI_NAMESPACE); 280 281 if (txs.length > 0) { 282 log(` Block #${height}: ${txs.length} RARI transactions`, 'blue'); 283 284 txs.forEach(tx => { 285 tx.timestamp = block.timestamp; 286 espressoTransactions.set(tx.hash, tx); 287 txCount++; 288 }); 289 } 290 291 // Small delay to avoid rate limiting 292 await new Promise(resolve => setTimeout(resolve, 100)); 293 } 294 295 log(`ā Espresso: Collected ${txCount} transactions from ${espressoTransactions.size} blocks\n`, 'green'); 296 } 297 298 /** 299 * Monitor Caff Node 300 */ 301 async function monitorCaffNode(startBlock, endBlock) { 302 log(`\nš” Monitoring Caff Node blocks ${startBlock} to ${endBlock}...`, 'yellow'); 303 304 let txCount = 0; 305 306 for (let number = startBlock; number <= endBlock; number++) { 307 const block = await fetchCaffNodeBlock(number); 308 if (!block || !block.transactions || block.transactions.length === 0) { 309 continue; 310 } 311 312 log(` Block #${number}: ${block.transactions.length} transactions`, 'yellow'); 313 314 block.transactions.forEach(tx => { 315 tx.timestamp = block.timestamp; 316 caffNodeTransactions.set(tx.hash, tx); 317 txCount++; 318 }); 319 320 // Small delay to avoid rate limiting 321 await new Promise(resolve => setTimeout(resolve, 100)); 322 } 323 324 log(`ā Caff Node: Collected ${txCount} transactions from ${caffNodeTransactions.size} blocks\n`, 'green'); 325 } 326 327 /** 328 * Get latest block heights 329 */ 330 async function getLatestBlocks() { 331 // Espresso 332 const espressoResponse = await fetch(`${ESPRESSO_API}/status/block-height`); 333 const espressoHeight = await espressoResponse.text(); 334 335 // Caff Node 336 const caffHeight = await caffNodeCall('eth_blockNumber', []); 337 const caffNumber = parseInt(caffHeight, 16); 338 339 return { 340 espresso: parseInt(espressoHeight), 341 caff: caffNumber 342 }; 343 } 344 345 /** 346 * Calculate block range for time window 347 */ 348 function calculateBlockRange(currentBlock, durationMinutes, avgBlockTime) { 349 const blocksToCheck = Math.ceil((durationMinutes * 60) / avgBlockTime); 350 return { 351 start: Math.max(1, currentBlock - blocksToCheck), 352 end: currentBlock 353 }; 354 } 355 356 /** 357 * Generate report 358 */ 359 function generateReport() { 360 const fs = require('fs'); 361 const timestamp = Date.now(); 362 363 log(`\n${'ā'.repeat(80)}`, 'bright'); 364 log('š CORRELATION ANALYSIS REPORT', 'bright'); 365 log(`${'ā'.repeat(80)}\n`, 'bright'); 366 367 // Summary 368 log('š Data Collection Summary:', 'cyan'); 369 log(` Espresso Transactions: ${espressoTransactions.size}`, 'blue'); 370 log(` Caff Node Transactions: ${caffNodeTransactions.size}`, 'yellow'); 371 log(` Total Correlations Found: ${correlations.length}`, 'green'); 372 373 // Calculate stats even if no correlations 374 let highConf = 0, medConf = 0, lowConf = 0; 375 let avgDelay = 0, minDelay = 0, maxDelay = 0; 376 377 if (correlations.length > 0) { 378 highConf = correlations.filter(c => c.confidence >= 0.8).length; 379 medConf = correlations.filter(c => c.confidence >= 0.6 && c.confidence < 0.8).length; 380 lowConf = correlations.filter(c => c.confidence < 0.6).length; 381 382 log(`\nš Confidence Distribution:`, 'cyan'); 383 log(` High (ā„80%): ${highConf} (${((highConf/correlations.length)*100).toFixed(1)}%)`, 'green'); 384 log(` Medium (60-79%): ${medConf} (${((medConf/correlations.length)*100).toFixed(1)}%)`, 'yellow'); 385 log(` Low (<60%): ${lowConf} (${((lowConf/correlations.length)*100).toFixed(1)}%)`, 'red'); 386 387 // Time delay analysis 388 const timeDiffs = correlations.map(c => c.time_diff); 389 avgDelay = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length; 390 minDelay = Math.min(...timeDiffs); 391 maxDelay = Math.max(...timeDiffs); 392 393 log(`\nā±ļø Time Delay Analysis:`, 'cyan'); 394 log(` Average Delay: ${avgDelay.toFixed(1)}s`, 'cyan'); 395 log(` Min Delay: ${minDelay}s`, 'cyan'); 396 log(` Max Delay: ${maxDelay}s`, 'cyan'); 397 398 // Top correlations 399 log(`\nš Top 5 Correlations (by confidence):`, 'cyan'); 400 correlations.slice(0, 5).forEach((corr, i) => { 401 const color = corr.confidence >= 0.8 ? 'green' : corr.confidence >= 0.6 ? 'yellow' : 'red'; 402 log(`\n ${i + 1}. Confidence: ${(corr.confidence * 100).toFixed(1)}%`, color); 403 log(` Espresso: ${formatHash(corr.espresso_tx, 16)} (block #${corr.espresso_block})`, 'blue'); 404 log(` Caff: ${formatHash(corr.caff_tx, 16)} (block #${corr.caff_block})`, 'yellow'); 405 log(` Delay: ${corr.time_diff}s`, 'cyan'); 406 }); 407 } else { 408 log(`\nā ļø No correlations found. This could mean:`, 'yellow'); 409 log(` - No transactions in the time window`, 'yellow'); 410 log(` - Delay is outside 5s-3min range`, 'yellow'); 411 log(` - Networks are out of sync`, 'yellow'); 412 } 413 414 // === SAVE ALL DATA TO FILES === 415 416 // 1. JSON Report with FULL transaction data 417 const jsonReport = { 418 metadata: { 419 generated_at: new Date().toISOString(), 420 timestamp: timestamp, 421 duration_minutes: Math.floor((Date.now() - startTime) / 60000), 422 config: { 423 delay_min: DELAY_MIN, 424 delay_max: DELAY_MAX, 425 namespace: RARI_NAMESPACE 426 } 427 }, 428 summary: { 429 espresso_transactions: espressoTransactions.size, 430 caff_transactions: caffNodeTransactions.size, 431 correlations_found: correlations.length, 432 high_confidence: highConf, 433 medium_confidence: medConf, 434 low_confidence: lowConf 435 }, 436 delay_stats: correlations.length > 0 ? { 437 average: avgDelay, 438 min: minDelay, 439 max: maxDelay 440 } : null, 441 espresso_transactions: Array.from(espressoTransactions.entries()).map(([hash, tx]) => ({ 442 hash: hash, 443 block_height: tx.block_height, 444 index: tx.index, 445 namespace: tx.namespace, 446 timestamp: tx.timestamp, 447 timestamp_iso: formatTime(tx.timestamp), 448 size: tx.size, 449 payload_preview: tx.payload 450 })), 451 caff_transactions: Array.from(caffNodeTransactions.entries()).map(([hash, tx]) => ({ 452 hash: hash, 453 block_number: tx.blockNumber, 454 transaction_index: tx.transactionIndex, 455 timestamp: tx.timestamp, 456 timestamp_iso: formatTime(tx.timestamp), 457 from: tx.from, 458 to: tx.to, 459 value: tx.value, 460 gas: tx.gas, 461 size: tx.size, 462 input_preview: tx.input ? tx.input.slice(0, 20) + '...' : null 463 })), 464 correlations: correlations.map(c => ({ 465 espresso_tx: c.espresso_tx, 466 espresso_block: c.espresso_block, 467 espresso_index: c.espresso_index, 468 espresso_timestamp: c.espresso_timestamp, 469 espresso_timestamp_iso: formatTime(c.espresso_timestamp), 470 caff_tx: c.caff_tx, 471 caff_block: c.caff_block, 472 caff_index: c.caff_index, 473 caff_timestamp: c.caff_timestamp, 474 caff_timestamp_iso: formatTime(c.caff_timestamp), 475 time_diff_seconds: c.time_diff, 476 confidence: c.confidence, 477 confidence_percent: (c.confidence * 100).toFixed(2) 478 })) 479 }; 480 481 const jsonFile = `correlation-full-${timestamp}.json`; 482 fs.writeFileSync(jsonFile, JSON.stringify(jsonReport, null, 2)); 483 log(`\nš¾ Full JSON report: ${jsonFile}`, 'green'); 484 485 // 2. CSV - Espresso Transactions 486 const espressoCsv = [ 487 'Hash,Block,Index,Namespace,Timestamp,Timestamp_ISO,Size', 488 ...Array.from(espressoTransactions.entries()).map(([hash, tx]) => 489 `"${hash}",${tx.block_height},${tx.index},${tx.namespace},${tx.timestamp},"${formatTime(tx.timestamp)}",${tx.size}` 490 ) 491 ].join('\n'); 492 493 const espressoCsvFile = `espresso-transactions-${timestamp}.csv`; 494 fs.writeFileSync(espressoCsvFile, espressoCsv); 495 log(`š¾ Espresso CSV: ${espressoCsvFile}`, 'blue'); 496 497 // 3. CSV - Caff Node Transactions 498 const caffCsv = [ 499 'Hash,Block,Index,Timestamp,Timestamp_ISO,From,To,Value,Gas,Size', 500 ...Array.from(caffNodeTransactions.entries()).map(([hash, tx]) => 501 `"${hash}",${tx.blockNumber},${tx.transactionIndex},${tx.timestamp},"${formatTime(tx.timestamp)}","${tx.from}","${tx.to}","${tx.value}",${tx.gas},${tx.size}` 502 ) 503 ].join('\n'); 504 505 const caffCsvFile = `caff-transactions-${timestamp}.csv`; 506 fs.writeFileSync(caffCsvFile, caffCsv); 507 log(`š¾ Caff Node CSV: ${caffCsvFile}`, 'yellow'); 508 509 // 4. CSV - Correlations 510 if (correlations.length > 0) { 511 const correlationCsv = [ 512 'Espresso_TX,Espresso_Block,Espresso_Index,Espresso_Time,Caff_TX,Caff_Block,Caff_Index,Caff_Time,Time_Diff_Seconds,Confidence,Confidence_Percent', 513 ...correlations.map(c => 514 `"${c.espresso_tx}",${c.espresso_block},${c.espresso_index},${c.espresso_timestamp},"${c.caff_tx}",${c.caff_block},${c.caff_index},${c.caff_timestamp},${c.time_diff},${c.confidence},${(c.confidence * 100).toFixed(2)}` 515 ) 516 ].join('\n'); 517 518 const correlationCsvFile = `correlations-${timestamp}.csv`; 519 fs.writeFileSync(correlationCsvFile, correlationCsv); 520 log(`š¾ Correlations CSV: ${correlationCsvFile}`, 'green'); 521 } 522 523 // 5. Human-readable text report 524 const textReport = [ 525 'ā'.repeat(80), 526 'TRANSACTION CORRELATION ANALYSIS REPORT', 527 'ā'.repeat(80), 528 '', 529 `Generated: ${new Date().toISOString()}`, 530 `Duration: ${Math.floor((Date.now() - startTime) / 60000)} minutes`, 531 '', 532 'CONFIGURATION:', 533 ` Delay Range: ${DELAY_MIN}s - ${DELAY_MAX}s`, 534 ` Namespace: ${RARI_NAMESPACE} (RARI)`, 535 '', 536 'DATA COLLECTION:', 537 ` Espresso Transactions: ${espressoTransactions.size}`, 538 ` Caff Node Transactions: ${caffNodeTransactions.size}`, 539 ` Correlations Found: ${correlations.length}`, 540 '' 541 ]; 542 543 if (correlations.length > 0) { 544 textReport.push( 545 'CONFIDENCE DISTRIBUTION:', 546 ` High (ā„80%): ${highConf} (${((highConf/correlations.length)*100).toFixed(1)}%)`, 547 ` Medium (60-79%): ${medConf} (${((medConf/correlations.length)*100).toFixed(1)}%)`, 548 ` Low (<60%): ${lowConf} (${((lowConf/correlations.length)*100).toFixed(1)}%)`, 549 '', 550 'TIME DELAY STATISTICS:', 551 ` Average: ${avgDelay.toFixed(1)}s`, 552 ` Min: ${minDelay}s`, 553 ` Max: ${maxDelay}s`, 554 '', 555 'TOP CORRELATIONS:', 556 '' 557 ); 558 559 correlations.slice(0, 10).forEach((c, i) => { 560 textReport.push( 561 `${i + 1}. Confidence: ${(c.confidence * 100).toFixed(1)}%`, 562 ` Espresso: ${c.espresso_tx}`, 563 ` Block: #${c.espresso_block}, Index: ${c.espresso_index}, Time: ${formatTime(c.espresso_timestamp)}`, 564 ` Caff: ${c.caff_tx}`, 565 ` Block: #${c.caff_block}, Index: ${c.caff_index}, Time: ${formatTime(c.caff_timestamp)}`, 566 ` Delay: ${c.time_diff}s`, 567 '' 568 ); 569 }); 570 } 571 572 textReport.push( 573 'ā'.repeat(80), 574 '', 575 'FILES GENERATED:', 576 ` - ${jsonFile} (complete data in JSON format)`, 577 ` - ${espressoCsvFile} (Espresso transactions)`, 578 ` - ${caffCsvFile} (Caff Node transactions)` 579 ); 580 581 if (correlations.length > 0) { 582 textReport.push(` - correlations-${timestamp}.csv (correlation matches)`); 583 } 584 585 const textFile = `correlation-report-${timestamp}.txt`; 586 fs.writeFileSync(textFile, textReport.join('\n')); 587 log(`š¾ Text report: ${textFile}`, 'cyan'); 588 589 log(`\n${'ā'.repeat(80)}`, 'bright'); 590 log(`ā All data saved! Generated ${correlations.length > 0 ? 5 : 4} files.`, 'green'); 591 log(`${'ā'.repeat(80)}\n`, 'bright'); 592 } 593 594 /** 595 * Main monitoring loop 596 */ 597 async function monitor(durationMinutes) { 598 log(`\n${'ā'.repeat(80)}`, 'bright'); 599 log(`š Transaction Correlation Monitor`, 'bright'); 600 log(`${'ā'.repeat(80)}`, 'bright'); 601 log(`\nāļø Configuration:`, 'cyan'); 602 log(` Duration: ${durationMinutes} minutes`, 'cyan'); 603 log(` Delay Range: ${DELAY_MIN}s - ${DELAY_MAX}s`, 'cyan'); 604 log(` Namespace: ${RARI_NAMESPACE} (RARI)`, 'cyan'); 605 log(` Poll Interval: ${POLL_INTERVAL}ms`, 'cyan'); 606 607 try { 608 // Get current block heights 609 log(`\nš Fetching current block heights...`, 'cyan'); 610 const latest = await getLatestBlocks(); 611 log(` Espresso: #${latest.espresso}`, 'blue'); 612 log(` Caff Node: #${latest.caff}`, 'yellow'); 613 614 // Calculate block ranges (assuming ~12s block time for both) 615 const espressoRange = calculateBlockRange(latest.espresso, durationMinutes, 12); 616 const caffRange = calculateBlockRange(latest.caff, durationMinutes, 12); 617 618 log(`\nš Block ranges to scan:`, 'cyan'); 619 log(` Espresso: #${espressoRange.start} - #${espressoRange.end}`, 'blue'); 620 log(` Caff Node: #${caffRange.start} - #${caffRange.end}`, 'yellow'); 621 622 // Monitor both networks 623 await Promise.all([ 624 monitorEspresso(espressoRange.start, espressoRange.end), 625 monitorCaffNode(caffRange.start, caffRange.end) 626 ]); 627 628 // Correlate 629 log(`\nš Analyzing correlations...`, 'cyan'); 630 correlateTransactions(); 631 632 // Generate report 633 generateReport(); 634 635 } catch (error) { 636 log(`\nā Error: ${error.message}`, 'red'); 637 console.error(error); 638 } 639 } 640 641 // CLI 642 const durationMinutes = parseInt(process.argv[2]) || 10; 643 monitor(durationMinutes).then(() => { 644 log(`\nā Monitoring complete!`, 'green'); 645 process.exit(0); 646 }).catch(error => { 647 log(`\nā Fatal error: ${error.message}`, 'red'); 648 process.exit(1); 649 });