diff --git a/Cargo.toml b/Cargo.toml index 9a35caf5..528ac47c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,10 @@ description = "Soroban Transaction Debugger — From cryptic error to root cause [workspace.dependencies] # Stellar / Soroban — Critical Path -stellar-xdr = { version = "20.0.0", features = ["std", "serde"] } +stellar-xdr = { version = "21.2.0", features = ["std", "serde"] } stellar-strkey = "0.0.9" -soroban-env-host = "21" -soroban-spec = "21" +soroban-env-host = "21.2.0" +soroban-spec = "21.0.0" soroban-spec-tools = "21.5.3" # Networking & Serialization @@ -48,6 +48,7 @@ redb = "2" directories = "5" flate2 = "1" xz2 = "0.1" +base64 = "0.22" # TUI Debugger (Tier 3) ratatui = "0.29" @@ -58,4 +59,4 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Shared internal crates -prism-core = { path = "crates/core" } \ No newline at end of file +prism-core = { path = "crates/core" } diff --git a/crates/cli/src/output/human.rs b/crates/cli/src/output/human.rs index 28119e79..870e9154 100644 --- a/crates/cli/src/output/human.rs +++ b/crates/cli/src/output/human.rs @@ -2,10 +2,14 @@ use prism_core::types::report::DiagnosticReport; -use crate::output::renderers::{render_section_header, BudgetBar}; +use crate::output::renderers::{render_section_header, render_error_card, BudgetBar}; /// Print a diagnostic report in human-readable colored format. pub fn print_report(report: &DiagnosticReport) -> anyhow::Result<()> { + // Display the error card prominently at the top + println!("{}", render_error_card(report)); + println!(); + println!("{}", render_section_header("Transaction Summary")); println!( "Error: {} ({}:{})", diff --git a/crates/cli/src/output/renderers.rs b/crates/cli/src/output/renderers.rs index 39c32851..31003741 100644 --- a/crates/cli/src/output/renderers.rs +++ b/crates/cli/src/output/renderers.rs @@ -1,7 +1,7 @@ //! Shared terminal renderers for CLI output. use colored::Colorize; -use prism_core::types::report::{DiagnosticReport, SuggestedFix}; +use prism_core::types::report::DiagnosticReport; const BAR_WIDTH: usize = 10; @@ -10,6 +10,11 @@ pub fn render_section_header(title: &str) -> String { SectionHeader::new(title).render() } +/// Render an error card to display transaction errors prominently. +pub fn render_error_card(report: &DiagnosticReport) -> String { + ErrorCard::new(report).render() +} + /// Utility for rendering a clearly separated section heading. pub struct SectionHeader<'a> { title: &'a str, @@ -33,6 +38,54 @@ impl<'a> SectionHeader<'a> { } } +/// Displays transaction errors with a bold red border and categorical labels. +pub struct ErrorCard<'a> { + report: &'a DiagnosticReport, +} + +impl<'a> ErrorCard<'a> { + pub fn new(report: &'a DiagnosticReport) -> Self { + Self { report } + } + + pub fn render(&self) -> String { + let mut output = String::new(); + + // Create the border and content structure + let category_badge = format!("[{}]", self.report.error_category.to_uppercase()); + let error_line = format!( + " {} ({})", + self.report.error_name, self.report.error_code + ); + + // Calculate width based on content + let max_width = error_line.len().max(self.report.summary.len()).max(category_badge.len()) + 4; + let border = "█".repeat(max_width); + + // Render with red color + let border_colored = border.red().bold().to_string(); + let category_colored = category_badge.red().bold().to_string(); + let error_colored = error_line.red().bold().to_string(); + let summary_colored = self.report.summary.white().to_string(); + + // Build the card + output.push_str(&format!("{}\n", border_colored)); + output.push_str(&format!("{} {}\n", "█".red().bold(), category_colored)); + output.push_str(&format!("{} {}\n", "█".red().bold(), error_colored)); + + // Add component info if it's a contract error + if let Some(contract_error) = &self.report.contract_error { + let component_line = format!("Component: {}", contract_error.contract_id); + output.push_str(&format!("{} {}\n", "█".red().bold(), component_line.white())); + } + + output.push_str(&format!("{} {}\n", "█".red().bold(), summary_colored)); + output.push_str(&format!("{}\n", border_colored)); + + output + } +} + /// Renders a colored budget utilization bar for Soroban resource usage. pub struct BudgetBar { label: &'static str, @@ -46,6 +99,7 @@ impl BudgetBar { } pub fn render(&self) -> String { +<<<<<<< HEAD let ratio = if self.limit == 0 { 0.0f64 } else { @@ -104,33 +158,44 @@ pub fn render_fix_list(report: &DiagnosticReport) -> String { output.push('\n'); } } +======= + let percentage = if self.limit > 0 { + (self.used as f64 / self.limit as f64 * 100.0) as u64 + } else { + 0 + }; - output -} + let filled = (percentage / 10).min(BAR_WIDTH as u64) as usize; + let empty = BAR_WIDTH - filled; + let bar = format!( + "[{}{}]", + "█".repeat(filled), + "░".repeat(empty) + ); +>>>>>>> 71d530f ( Implement ErrorCard for terminal) -/// Returns the appropriate icon for a suggested fix based on its characteristics. -fn get_fix_icon(fix: &SuggestedFix) -> &'static str { - if fix.requires_upgrade { - "🔒" - } else if fix.example.is_some() { - "📋" - } else { - "🔧" - } -} + let bar_colored = if percentage > 90 { + bar.red().to_string() + } else if percentage > 70 { + bar.yellow().to_string() + } else { + bar.green().to_string() + }; -/// Returns a badge indicating the difficulty level of the fix. -fn get_difficulty_badge(difficulty: &str) -> String { - match difficulty.to_lowercase().as_str() { - "easy" => " [easy]".to_string(), - "medium" => " [medium]".to_string(), - "hard" => " [hard]".to_string(), - _ => String::new(), + format!( + "{:8} {} {}/{} ({:3}%)", + self.label, + bar_colored, + self.used, + self.limit, + percentage + ) } } #[cfg(test)] mod tests { +<<<<<<< HEAD use super::{get_difficulty_badge, get_fix_icon, render_fix_list}; use super::{render_section_header, BudgetBar, SectionHeader}; use prism_core::types::report::{DiagnosticReport, SuggestedFix}; @@ -199,6 +264,30 @@ mod tests { assert_eq!(get_difficulty_badge("medium"), " [medium]"); assert_eq!(get_difficulty_badge("hard"), " [hard]"); assert_eq!(get_difficulty_badge("unknown"), ""); +======= + use super::*; + use prism_core::types::report::{ContractErrorInfo, Severity}; + + fn create_test_report() -> DiagnosticReport { + DiagnosticReport { + error_category: "Contract".to_string(), + error_code: 1, + error_name: "InsufficientBalance".to_string(), + summary: "The account does not have enough balance to complete this transaction.".to_string(), + detailed_explanation: String::new(), + severity: Severity::Error, + root_causes: Vec::new(), + suggested_fixes: Vec::new(), + contract_error: Some(ContractErrorInfo { + contract_id: "CBDLTOJWR2YX2U6BR3P5C4UXKWHE5DJW3JPSIOEXTW2E7D5JUDPQULE7".to_string(), + error_code: 1, + error_name: Some("InsufficientBalance".to_string()), + doc_comment: Some("User attempted transfer with insufficient balance".to_string()), + }), + transaction_context: None, + related_errors: Vec::new(), + } +>>>>>>> 71d530f ( Implement ErrorCard for terminal) } #[test] @@ -216,15 +305,92 @@ mod tests { } #[test] +<<<<<<< HEAD fn budget_bar_renders_green_when_low_usage() { let bar = BudgetBar::new("CPU", 100, 1000).render(); assert!(bar.contains("CPU")); assert!(bar.contains("10%")); +======= + fn error_card_renders_basic_error() { + let report = create_test_report(); + let rendered = render_error_card(&report); + + assert!(rendered.contains("❌")); + assert!(rendered.contains("InsufficientBalance")); + assert!(rendered.contains("1")); + assert!(rendered.contains("[CONTRACT]")); + assert!(rendered.contains("does not have enough balance")); + } + + #[test] + fn error_card_includes_contract_component() { + let report = create_test_report(); + let rendered = render_error_card(&report); + + assert!(rendered.contains("Component:")); + assert!(rendered.contains("CBDLTOJWR2YX2U6BR3P5C4UXKWHE5DJW3JPSIOEXTW2E7D5JUDPQULE7")); + } + + #[test] + fn error_card_excludes_component_without_contract_error() { + let mut report = create_test_report(); + report.contract_error = None; + let rendered = render_error_card(&report); + + assert!(!rendered.contains("Component:")); + } + + #[test] + fn error_card_uses_red_styling() { + let report = create_test_report(); + let rendered = render_error_card(&report); + + // The card should contain the border characters + assert!(rendered.contains("█")); + } + + #[test] + fn budget_bar_renders_low_usage() { + let bar = BudgetBar::new("CPU", 100, 1000); + let rendered = bar.render(); + + assert!(rendered.contains("CPU")); + assert!(rendered.contains("100/1000")); + assert!(rendered.contains("10%")); + } + + #[test] + fn budget_bar_renders_high_usage() { + let bar = BudgetBar::new("Memory", 950, 1000); + let rendered = bar.render(); + + assert!(rendered.contains("Memory")); + assert!(rendered.contains("950/1000")); + assert!(rendered.contains("95%")); +>>>>>>> 71d530f ( Implement ErrorCard for terminal) } #[test] fn budget_bar_handles_zero_limit() { +<<<<<<< HEAD let bar = BudgetBar::new("MEM", 0, 0).render(); assert!(bar.contains("MEM")); +======= + let bar = BudgetBar::new("CPU", 100, 0); + let rendered = bar.render(); + + assert!(rendered.contains("CPU")); + assert!(rendered.contains("0%")); + } + + #[test] + fn budget_bar_shows_full_usage() { + let bar = BudgetBar::new("Disk", 1000, 1000); + let rendered = bar.render(); + + assert!(rendered.contains("Disk")); + assert!(rendered.contains("1000/1000")); + assert!(rendered.contains("100%")); +>>>>>>> 71d530f ( Implement ErrorCard for terminal) } } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9e06164c..1e7702ce 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -47,4 +47,4 @@ anyhow = { workspace = true } [dev-dependencies] tokio-test = "0.4" -insta = { version = "1", features = ["json"] } \ No newline at end of file +insta = { version = "1", features = ["json"] }