/ 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  });