diff --git a/crates/cli/src/commands/batch.rs b/crates/cli/src/commands/batch.rs new file mode 100644 index 00000000..9085182f --- /dev/null +++ b/crates/cli/src/commands/batch.rs @@ -0,0 +1,31 @@ +use crate::ui::error_summary::ErrorSummaryList; +use clap::Args; +use grat_core::types::config::NetworkConfig; + +#[derive(Args)] +pub struct BatchArgs { + #[arg(required = true, num_args = 1..)] + pub tx_hashes: Vec, +} + +pub async fn run(args: BatchArgs, network: &NetworkConfig) -> anyhow::Result<()> { + let mut all_reports = Vec::new(); + + let spinner = indicatif::ProgressBar::new_spinner(); + spinner.set_message(format!("Decoding {} transactions...", args.tx_hashes.len())); + spinner.enable_steady_tick(std::time::Duration::from_millis(100)); + + for hash in &args.tx_hashes { + if let Ok(reports) = + grat_core::decode::decode_transaction_with_op_filter(hash, network, None).await + { + all_reports.extend(reports); + } + } + + spinner.finish_and_clear(); + + ErrorSummaryList::render(&all_reports); + + Ok(()) +} diff --git a/crates/cli/src/commands/mod.rs b/crates/cli/src/commands/mod.rs index 76cc352b..5ed825ad 100644 --- a/crates/cli/src/commands/mod.rs +++ b/crates/cli/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod batch; pub mod clean; pub mod db; pub mod decode; diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index f93ed586..78049d1d 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -51,6 +51,8 @@ enum Commands { #[command(next_help_heading = "Analysis Commands")] Decode(commands::decode::DecodeArgs), + Batch(commands::batch::BatchArgs), + Inspect(commands::inspect::InspectArgs), Trace(commands::trace::TraceArgs), @@ -128,6 +130,7 @@ async fn main() -> anyhow::Result<()> { match cli.command { Commands::Decode(args) => commands::decode::run(args, &network, &cli.output, save).await?, + Commands::Batch(args) => commands::batch::run(args, &network).await?, Commands::Inspect(args) => { commands::inspect::run(args, &network, &cli.output, save).await?; } diff --git a/crates/cli/src/ui/error_summary.rs b/crates/cli/src/ui/error_summary.rs new file mode 100644 index 00000000..0c126426 --- /dev/null +++ b/crates/cli/src/ui/error_summary.rs @@ -0,0 +1,96 @@ +use colored::Colorize; +use grat_core::types::report::{DiagnosticReport, Severity}; +use std::collections::HashMap; +use tabled::{settings::Style, Table, Tabled}; + +pub struct ErrorSummaryList; + +#[derive(Tabled)] +struct ErrorSummaryRow { + #[tabled(rename = "Severity")] + severity: String, + #[tabled(rename = "Category")] + category: String, + #[tabled(rename = "Error Name")] + name: String, + #[tabled(rename = "Count")] + count: usize, + #[tabled(rename = "Message")] + message: String, +} + +impl ErrorSummaryList { + pub fn render(reports: &[DiagnosticReport]) { + if reports.is_empty() { + println!("No errors found in the provided batch."); + return; + } + + // Group by category, name, and severity + let mut grouped: HashMap<(String, String, String), (usize, String)> = HashMap::new(); + + for report in reports { + let sev_str = match report.severity { + Severity::Fatal => "Fatal", + Severity::Error => "Error", + Severity::Warning => "Warning", + Severity::Info => "Info", + } + .to_string(); + + let key = ( + report.error_category.clone(), + report.error_name.clone(), + sev_str, + ); + let entry = grouped.entry(key).or_insert((0, report.summary.clone())); + entry.0 += 1; + } + + let mut rows: Vec = grouped + .into_iter() + .map(|((category, name, severity), (count, message))| { + let sev_str = match severity.as_str() { + "Fatal" => "FATAL".red().bold().to_string(), + "Error" => "ERROR".red().to_string(), + "Warning" => "WARN".yellow().to_string(), + "Info" => "INFO".blue().to_string(), + _ => severity, + }; + + ErrorSummaryRow { + severity: sev_str, + category, + name, + count, + message, + } + }) + .collect(); + + // Sort by severity (Fatal > Error > Warning > Info) then count descending + rows.sort_by(|a, b| { + let sev_a = severity_weight(&a.severity); + let sev_b = severity_weight(&b.severity); + sev_b.cmp(&sev_a).then(b.count.cmp(&a.count)) + }); + + let mut table = Table::new(rows); + table.with(Style::rounded()); + + println!("\n=== Batch Decoding Summary ==="); + println!("{}", table); + } +} + +fn severity_weight(sev_str: &str) -> u8 { + if sev_str.contains("FATAL") { + 4 + } else if sev_str.contains("ERROR") { + 3 + } else if sev_str.contains("WARN") { + 2 + } else { + 1 + } +} diff --git a/crates/cli/src/ui/mod.rs b/crates/cli/src/ui/mod.rs index 163a4fba..f2c4fa15 100644 --- a/crates/cli/src/ui/mod.rs +++ b/crates/cli/src/ui/mod.rs @@ -1 +1,2 @@ +pub mod error_summary; pub mod markdown;