light_program_test/logging/
formatter.rs

1//! Transaction formatting utilities for explorer-style output
2
3use std::fmt::{self, Write};
4
5use solana_sdk::system_program;
6use tabled::{Table, Tabled};
7
8use super::{
9    config::{EnhancedLoggingConfig, LogVerbosity},
10    types::{
11        AccountAccess, AccountChange, EnhancedInstructionLog, EnhancedTransactionLog,
12        TransactionStatus,
13    },
14};
15
16/// Row for account table display
17#[derive(Tabled)]
18struct AccountRow {
19    #[tabled(rename = "#")]
20    symbol: String,
21    #[tabled(rename = "Account")]
22    pubkey: String,
23    #[tabled(rename = "Type")]
24    access: String,
25    #[tabled(rename = "Name")]
26    name: String,
27}
28
29/// Colors for terminal output
30#[derive(Debug, Clone)]
31pub struct Colors {
32    pub bold: String,
33    pub reset: String,
34    pub green: String,
35    pub red: String,
36    pub yellow: String,
37    pub blue: String,
38    pub cyan: String,
39    pub gray: String,
40}
41
42impl Colors {
43    pub fn new(use_colors: bool) -> Self {
44        if use_colors {
45            Self {
46                bold: "\x1b[1m".to_string(),
47                reset: "\x1b[0m".to_string(),
48                green: "\x1b[32m".to_string(),
49                red: "\x1b[31m".to_string(),
50                yellow: "\x1b[33m".to_string(),
51                blue: "\x1b[34m".to_string(),
52                cyan: "\x1b[36m".to_string(),
53                gray: "\x1b[90m".to_string(),
54            }
55        } else {
56            Self {
57                bold: String::new(),
58                reset: String::new(),
59                green: String::new(),
60                red: String::new(),
61                yellow: String::new(),
62                blue: String::new(),
63                cyan: String::new(),
64                gray: String::new(),
65            }
66        }
67    }
68}
69
70/// Transaction formatter with configurable output
71pub struct TransactionFormatter {
72    config: EnhancedLoggingConfig,
73    colors: Colors,
74}
75
76impl TransactionFormatter {
77    pub fn new(config: &EnhancedLoggingConfig) -> Self {
78        Self {
79            config: config.clone(),
80            colors: Colors::new(config.use_colors),
81        }
82    }
83
84    /// Apply line breaks to long values in the complete output
85    fn apply_line_breaks(&self, text: &str) -> String {
86        let mut result = String::new();
87
88        for line in text.lines() {
89            // Look for patterns that need line breaking
90            if let Some(formatted_line) = self.format_line_if_needed(line) {
91                result.push_str(&formatted_line);
92            } else {
93                result.push_str(line);
94            }
95            result.push('\n');
96        }
97
98        result
99    }
100
101    /// Format a line if it contains long values that need breaking
102    fn format_line_if_needed(&self, line: &str) -> Option<String> {
103        // Extract leading whitespace/indentation and table characters
104        let leading_chars = line
105            .chars()
106            .take_while(|&c| c.is_whitespace() || "│├└┌┬┴┐┤─".contains(c))
107            .collect::<String>();
108
109        // Match patterns like "address: [0, 1, 2, 3, ...]" or "Raw instruction data (N bytes): [...]"
110        if line.contains(": [") && line.contains("]") {
111            // Handle byte arrays
112            if let Some(start) = line.find(": [") {
113                if let Some(end_pos) = line[start..].find(']') {
114                    let end = start + end_pos;
115                    let prefix = &line[..start + 2]; // Include ": "
116                    let array_part = &line[start + 2..end + 1]; // The "[...]" part
117                    let suffix = &line[end + 1..];
118
119                    // For raw instruction data, use a shorter line length to better fit in terminal
120                    let max_width = if line.contains("Raw instruction data") {
121                        80 // Wider for raw instruction data to fit more numbers per line
122                    } else {
123                        50 // Keep existing width for other arrays
124                    };
125
126                    // Always format if it's raw instruction data or if it exceeds max_width
127                    if line.contains("Raw instruction data") || array_part.len() > max_width {
128                        let formatted_array = self.format_long_value_with_indent(
129                            array_part,
130                            max_width,
131                            &leading_chars,
132                        );
133                        return Some(format!("{}{}{}", prefix, formatted_array, suffix));
134                    }
135                }
136            }
137        }
138
139        // Handle long base58 strings (44+ characters) in table cells
140        if line.contains('|') && !line.trim_start().starts_with('|') {
141            // This is a table content line, not a border
142            let mut new_line = String::new();
143            let mut modified = false;
144
145            // Split by table separators while preserving them
146            let parts: Vec<&str> = line.split('|').collect();
147            for (i, part) in parts.iter().enumerate() {
148                if i > 0 {
149                    new_line.push('|');
150                }
151
152                // Check if this cell contains a long value
153                for word in part.split_whitespace() {
154                    if word.len() > 44 && word.chars().all(|c| c.is_alphanumeric()) {
155                        let indent = " ".repeat(leading_chars.len() + 2); // Extra space for table formatting
156                        let formatted_word = self.format_long_value_with_indent(word, 44, &indent);
157                        new_line.push_str(&part.replace(word, &formatted_word));
158                        modified = true;
159                        break;
160                    }
161                }
162
163                if !modified {
164                    new_line.push_str(part);
165                }
166            }
167
168            if modified {
169                return Some(new_line);
170            }
171        }
172
173        None
174    }
175
176    /// Format long value with proper indentation for continuation lines
177    fn format_long_value_with_indent(&self, value: &str, max_width: usize, indent: &str) -> String {
178        if value.len() <= max_width {
179            return value.to_string();
180        }
181
182        let mut result = String::new();
183
184        // Handle byte arrays specially by breaking at natural comma boundaries when possible
185        if value.starts_with('[') && value.ends_with(']') {
186            // This is a byte array - try to break at comma boundaries for better readability
187            let inner = &value[1..value.len() - 1]; // Remove [ and ]
188            let parts: Vec<&str> = inner.split(", ").collect();
189
190            result.push('[');
191            let mut current_line = String::new();
192            let mut first_line = true;
193
194            for (i, part) in parts.iter().enumerate() {
195                let addition = if i == 0 {
196                    part.to_string()
197                } else {
198                    format!(", {}", part)
199                };
200
201                // Check if adding this part would exceed the line width
202                if current_line.len() + addition.len() > max_width && !current_line.is_empty() {
203                    // Add current line to result and start new line
204                    if first_line {
205                        result.push_str(&current_line);
206                        first_line = false;
207                    } else {
208                        result.push_str(&format!("\n{}{}", indent, current_line));
209                    }
210                    current_line = part.to_string();
211                } else {
212                    current_line.push_str(&addition);
213                }
214            }
215
216            // Add the last line
217            if !current_line.is_empty() {
218                if first_line {
219                    result.push_str(&current_line);
220                } else {
221                    result.push_str(&format!("\n{}{}", indent, current_line));
222                }
223            }
224
225            result.push(']');
226        } else {
227            // Fall back to character-based breaking for non-array values
228            let chars = value.chars().collect::<Vec<char>>();
229            let mut pos = 0;
230
231            while pos < chars.len() {
232                let end = (pos + max_width).min(chars.len());
233                let chunk: String = chars[pos..end].iter().collect();
234
235                if pos == 0 {
236                    result.push_str(&chunk);
237                } else {
238                    result.push_str(&format!("\n{}{}", indent, chunk));
239                }
240
241                pos = end;
242            }
243        }
244
245        result
246    }
247
248    /// Format complete transaction log
249    pub fn format(&self, log: &EnhancedTransactionLog, tx_number: usize) -> String {
250        let mut output = String::new();
251
252        // Transaction box header with number
253        writeln!(output, "{}┌───────────────────────────────────────── Transaction #{} ─────────────────────────────────────────────┐{}", self.colors.gray, tx_number, self.colors.reset).expect("Failed to write box header");
254
255        // Transaction header
256        self.write_transaction_header(&mut output, log)
257            .expect("Failed to write header");
258
259        // Instructions section
260        if !log.instructions.is_empty() {
261            self.write_instructions_section(&mut output, log)
262                .expect("Failed to write instructions");
263        }
264
265        // Account changes section
266        if self.config.show_account_changes && !log.account_changes.is_empty() {
267            self.write_account_changes_section(&mut output, log)
268                .expect("Failed to write account changes");
269        }
270
271        // Light Protocol events section
272        if !log.light_events.is_empty() {
273            self.write_light_events_section(&mut output, log)
274                .expect("Failed to write Light Protocol events");
275        }
276
277        // Program logs section (LiteSVM pretty logs)
278        if !log.program_logs_pretty.trim().is_empty() {
279            self.write_program_logs_section(&mut output, log)
280                .expect("Failed to write program logs");
281        }
282
283        // Transaction box footer
284        writeln!(output, "{}└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘{}", self.colors.gray, self.colors.reset).expect("Failed to write box footer");
285
286        // Apply line breaks for long values in the complete output
287        self.apply_line_breaks(&output)
288    }
289
290    /// Write transaction header with status, fee, and compute units
291    fn write_transaction_header(
292        &self,
293        output: &mut String,
294        log: &EnhancedTransactionLog,
295    ) -> fmt::Result {
296        writeln!(
297            output,
298            "{}│{} {}Transaction: {}{} | Slot: {} | Status: {}{}",
299            self.colors.gray,
300            self.colors.reset,
301            self.colors.bold,
302            self.colors.cyan,
303            log.signature,
304            log.slot,
305            self.status_color(&log.status),
306            log.status.text(),
307        )?;
308
309        writeln!(
310            output,
311            "{}│{} Fee: {}{:.6} SOL | Compute Used: {}{}/{} CU{}",
312            self.colors.gray,
313            self.colors.reset,
314            self.colors.yellow,
315            log.fee as f64 / 1_000_000_000.0,
316            self.colors.blue,
317            log.compute_used,
318            log.compute_total,
319            self.colors.reset
320        )?;
321
322        writeln!(output, "{}│{}", self.colors.gray, self.colors.reset)?;
323        Ok(())
324    }
325
326    /// Write instructions hierarchy
327    fn write_instructions_section(
328        &self,
329        output: &mut String,
330        log: &EnhancedTransactionLog,
331    ) -> fmt::Result {
332        writeln!(
333            output,
334            "{}│{} {}Instructions ({}):{}",
335            self.colors.gray,
336            self.colors.reset,
337            self.colors.bold,
338            log.instructions.len(),
339            self.colors.reset
340        )?;
341        writeln!(output, "{}│{}", self.colors.gray, self.colors.reset)?;
342
343        for (i, instruction) in log.instructions.iter().enumerate() {
344            self.write_instruction(output, instruction, 0, i + 1)?;
345        }
346
347        Ok(())
348    }
349
350    /// Write single instruction with proper indentation and hierarchy
351    fn write_instruction(
352        &self,
353        output: &mut String,
354        instruction: &EnhancedInstructionLog,
355        depth: usize,
356        number: usize,
357    ) -> fmt::Result {
358        let indent = self.get_tree_indent(depth);
359        let prefix = if depth == 0 { "├─" } else { "└─" };
360
361        // Instruction header
362        let inner_count = if instruction.inner_instructions.is_empty() {
363            String::new()
364        } else {
365            format!(".{}", instruction.inner_instructions.len())
366        };
367
368        write!(
369            output,
370            "{}{} {}#{}{} {}{} ({}{}{})",
371            indent,
372            prefix,
373            self.colors.bold,
374            number,
375            inner_count,
376            self.colors.blue,
377            instruction.program_id,
378            self.colors.cyan,
379            instruction.program_name,
380            self.colors.reset
381        )?;
382
383        // Add instruction name if parsed
384        if let Some(ref name) = instruction.instruction_name {
385            write!(
386                output,
387                " - {}{}{}",
388                self.colors.yellow, name, self.colors.reset
389            )?;
390        }
391
392        // Add compute units if available and requested
393        if self.config.show_compute_units {
394            if let Some(compute) = instruction.compute_consumed {
395                write!(
396                    output,
397                    " {}({}{}CU{})",
398                    self.colors.gray, self.colors.blue, compute, self.colors.gray
399                )?;
400            }
401        }
402
403        writeln!(output, "{}", self.colors.reset)?;
404
405        // Show instruction details based on verbosity
406        match self.config.verbosity {
407            LogVerbosity::Detailed | LogVerbosity::Full => {
408                if let Some(ref parsed) = instruction.parsed_data {
409                    self.write_parsed_instruction_data(
410                        output,
411                        parsed,
412                        &instruction.data,
413                        depth + 1,
414                    )?;
415                } else if !instruction.data.is_empty() {
416                    // Show raw instruction data for unparseable instructions with chunking
417                    // Skip instruction data for account compression program unless explicitly configured
418                    let should_show_data = if instruction.program_name == "Account Compression" {
419                        self.config.show_compression_instruction_data
420                    } else {
421                        true
422                    };
423
424                    if should_show_data {
425                        let indent = self.get_tree_indent(depth + 1);
426                        writeln!(
427                            output,
428                            "{}{}Raw instruction data ({} bytes): {}[",
429                            indent,
430                            self.colors.gray,
431                            instruction.data.len(),
432                            self.colors.cyan
433                        )?;
434
435                        // Chunk the data into 32-byte groups for better readability
436                        for (i, chunk) in instruction.data.chunks(32).enumerate() {
437                            write!(output, "{}  ", indent)?;
438                            for (j, byte) in chunk.iter().enumerate() {
439                                if j > 0 {
440                                    write!(output, ", ")?;
441                                }
442                                write!(output, "{}", byte)?;
443                            }
444                            if i < instruction.data.chunks(32).len() - 1 {
445                                writeln!(output, ",")?;
446                            } else {
447                                writeln!(output, "]{}", self.colors.reset)?;
448                            }
449                        }
450                    }
451                }
452            }
453            _ => {}
454        }
455
456        // Show accounts if verbose
457        if self.config.verbosity == LogVerbosity::Full && !instruction.accounts.is_empty() {
458            let accounts_indent = self.get_tree_indent(depth + 1);
459            writeln!(
460                output,
461                "{}{}Accounts ({}):{}",
462                accounts_indent,
463                self.colors.gray,
464                instruction.accounts.len(),
465                self.colors.reset
466            )?;
467
468            // Create a table for better account formatting
469            let mut account_rows: Vec<AccountRow> = Vec::new();
470
471            for (idx, account) in instruction.accounts.iter().enumerate() {
472                let access = if account.is_signer && account.is_writable {
473                    AccountAccess::SignerWritable
474                } else if account.is_signer {
475                    AccountAccess::Signer
476                } else if account.is_writable {
477                    AccountAccess::Writable
478                } else {
479                    AccountAccess::Readonly
480                };
481
482                let account_name = self.get_account_name(&account.pubkey);
483                account_rows.push(AccountRow {
484                    symbol: access.symbol(idx + 1),
485                    pubkey: account.pubkey.to_string(),
486                    access: access.text().to_string(),
487                    name: account_name,
488                });
489            }
490
491            if !account_rows.is_empty() {
492                let table = Table::new(account_rows)
493                    .to_string()
494                    .lines()
495                    .map(|line| format!("{}{}", accounts_indent, line))
496                    .collect::<Vec<_>>()
497                    .join("\n");
498                writeln!(output, "{}", table)?;
499            }
500        }
501
502        // Write inner instructions recursively
503        for (i, inner) in instruction.inner_instructions.iter().enumerate() {
504            if depth < self.config.max_inner_instruction_depth {
505                self.write_instruction(output, inner, depth + 1, i + 1)?;
506            }
507        }
508
509        Ok(())
510    }
511
512    /// Write parsed instruction data
513    fn write_parsed_instruction_data(
514        &self,
515        output: &mut String,
516        parsed: &super::types::ParsedInstructionData,
517        instruction_data: &[u8],
518        depth: usize,
519    ) -> fmt::Result {
520        let indent = self.get_tree_indent(depth);
521
522        match parsed {
523            super::types::ParsedInstructionData::LightSystemProgram {
524                instruction_type,
525                compressed_accounts,
526                proof_info,
527                address_params,
528                fee_info,
529                input_account_data,
530                output_account_data,
531            } => {
532                writeln!(
533                    output,
534                    "{}{}Light System: {}{}{}",
535                    indent,
536                    self.colors.gray,
537                    self.colors.yellow,
538                    instruction_type,
539                    self.colors.reset
540                )?;
541
542                if let Some(compressed_accounts) = compressed_accounts {
543                    writeln!(
544                        output,
545                        "{}{}Accounts: {}in: {}, out: {}{}",
546                        indent,
547                        self.colors.gray,
548                        self.colors.cyan,
549                        compressed_accounts.input_accounts,
550                        compressed_accounts.output_accounts,
551                        self.colors.reset
552                    )?;
553                }
554
555                if let Some(proof_info) = proof_info {
556                    if proof_info.has_validity_proof {
557                        writeln!(
558                            output,
559                            "{}{}Proof: {}{} proof{}",
560                            indent,
561                            self.colors.gray,
562                            self.colors.cyan,
563                            proof_info.proof_type,
564                            self.colors.reset
565                        )?;
566                    }
567                }
568
569                // Display input account data
570                if let Some(ref input_accounts) = input_account_data {
571                    writeln!(
572                        output,
573                        "{}{}Input Accounts ({}):{}",
574                        indent,
575                        self.colors.gray,
576                        input_accounts.len(),
577                        self.colors.reset
578                    )?;
579                    for (i, acc_data) in input_accounts.iter().enumerate() {
580                        writeln!(
581                            output,
582                            "{}  {}[{}]{}",
583                            indent, self.colors.gray, i, self.colors.reset
584                        )?;
585                        writeln!(
586                            output,
587                            "{}      {}owner: {}{}{}",
588                            indent,
589                            self.colors.gray,
590                            self.colors.yellow,
591                            acc_data
592                                .owner
593                                .map(|o| o.to_string())
594                                .unwrap_or("None".to_string()),
595                            self.colors.reset
596                        )?;
597                        if let Some(ref address) = acc_data.address {
598                            writeln!(
599                                output,
600                                "{}      {}address: {}{:?}{}",
601                                indent,
602                                self.colors.gray,
603                                self.colors.cyan,
604                                address,
605                                self.colors.reset
606                            )?;
607                        }
608                        writeln!(
609                            output,
610                            "{}      {}lamports: {}{}{}",
611                            indent,
612                            self.colors.gray,
613                            self.colors.cyan,
614                            acc_data.lamports,
615                            self.colors.reset
616                        )?;
617                        if !acc_data.data_hash.is_empty() {
618                            writeln!(
619                                output,
620                                "{}      {}data_hash: {}{:?}{}",
621                                indent,
622                                self.colors.gray,
623                                self.colors.cyan,
624                                acc_data.data_hash,
625                                self.colors.reset
626                            )?;
627                        }
628                        if !acc_data.discriminator.is_empty() {
629                            writeln!(
630                                output,
631                                "{}      {}discriminator: {}{:?}{}",
632                                indent,
633                                self.colors.gray,
634                                self.colors.cyan,
635                                acc_data.discriminator,
636                                self.colors.reset
637                            )?;
638                        }
639                        if let Some(tree_idx) = acc_data.merkle_tree_index {
640                            if let Some(tree_pubkey) = acc_data.merkle_tree_pubkey {
641                                writeln!(
642                                    output,
643                                    "{}      {}merkle_tree_pubkey (index {}{}{}): {}{}{}",
644                                    indent,
645                                    self.colors.gray,
646                                    self.colors.cyan,
647                                    tree_idx,
648                                    self.colors.gray,
649                                    self.colors.yellow,
650                                    tree_pubkey,
651                                    self.colors.reset
652                                )?;
653                            } else {
654                                writeln!(
655                                    output,
656                                    "{}      {}merkle_tree_index: {}{}{}",
657                                    indent,
658                                    self.colors.gray,
659                                    self.colors.cyan,
660                                    tree_idx,
661                                    self.colors.reset
662                                )?;
663                            }
664                        } else if let Some(tree_pubkey) = acc_data.merkle_tree_pubkey {
665                            writeln!(
666                                output,
667                                "{}      {}merkle_tree_pubkey: {}{}{}",
668                                indent,
669                                self.colors.gray,
670                                self.colors.yellow,
671                                tree_pubkey,
672                                self.colors.reset
673                            )?;
674                        }
675                        if let Some(queue_idx) = acc_data.queue_index {
676                            if let Some(queue_pubkey) = acc_data.queue_pubkey {
677                                writeln!(
678                                    output,
679                                    "{}      {}queue_pubkey (index {}{}{}): {}{}{}",
680                                    indent,
681                                    self.colors.gray,
682                                    self.colors.cyan,
683                                    queue_idx,
684                                    self.colors.gray,
685                                    self.colors.yellow,
686                                    queue_pubkey,
687                                    self.colors.reset
688                                )?;
689                            } else {
690                                writeln!(
691                                    output,
692                                    "{}      {}queue_index: {}{}{}",
693                                    indent,
694                                    self.colors.gray,
695                                    self.colors.cyan,
696                                    queue_idx,
697                                    self.colors.reset
698                                )?;
699                            }
700                        } else if let Some(queue_pubkey) = acc_data.queue_pubkey {
701                            writeln!(
702                                output,
703                                "{}      {}queue_pubkey: {}{}{}",
704                                indent,
705                                self.colors.gray,
706                                self.colors.yellow,
707                                queue_pubkey,
708                                self.colors.reset
709                            )?;
710                        }
711                        // Display leaf index after queue_pubkey
712                        if let Some(leaf_idx) = acc_data.leaf_index {
713                            writeln!(
714                                output,
715                                "{}      {}leaf_index: {}{}{}",
716                                indent,
717                                self.colors.gray,
718                                self.colors.cyan,
719                                leaf_idx,
720                                self.colors.reset
721                            )?;
722                        }
723                        // Display root index after leaf index
724                        if let Some(root_idx) = acc_data.root_index {
725                            writeln!(
726                                output,
727                                "{}      {}root_index: {}{}{}",
728                                indent,
729                                self.colors.gray,
730                                self.colors.cyan,
731                                root_idx,
732                                self.colors.reset
733                            )?;
734                        }
735                    }
736                }
737
738                // Display output account data
739                if let Some(ref output_data) = output_account_data {
740                    writeln!(
741                        output,
742                        "{}{}Output Accounts ({}):{}",
743                        indent,
744                        self.colors.gray,
745                        output_data.len(),
746                        self.colors.reset
747                    )?;
748                    for (i, acc_data) in output_data.iter().enumerate() {
749                        writeln!(
750                            output,
751                            "{}  {}[{}]{}",
752                            indent, self.colors.gray, i, self.colors.reset
753                        )?;
754                        writeln!(
755                            output,
756                            "{}      {}owner: {}{}{}",
757                            indent,
758                            self.colors.gray,
759                            self.colors.yellow,
760                            acc_data
761                                .owner
762                                .map(|o| o.to_string())
763                                .unwrap_or("None".to_string()),
764                            self.colors.reset
765                        )?;
766                        if let Some(ref address) = acc_data.address {
767                            writeln!(
768                                output,
769                                "{}      {}address: {}{:?}{}",
770                                indent,
771                                self.colors.gray,
772                                self.colors.cyan,
773                                address,
774                                self.colors.reset
775                            )?;
776                        }
777                        writeln!(
778                            output,
779                            "{}      {}lamports: {}{}{}",
780                            indent,
781                            self.colors.gray,
782                            self.colors.cyan,
783                            acc_data.lamports,
784                            self.colors.reset
785                        )?;
786                        if !acc_data.data_hash.is_empty() {
787                            writeln!(
788                                output,
789                                "{}      {}data_hash: {}{:?}{}",
790                                indent,
791                                self.colors.gray,
792                                self.colors.cyan,
793                                acc_data.data_hash,
794                                self.colors.reset
795                            )?;
796                        }
797                        if !acc_data.discriminator.is_empty() {
798                            writeln!(
799                                output,
800                                "{}      {}discriminator: {}{:?}{}",
801                                indent,
802                                self.colors.gray,
803                                self.colors.cyan,
804                                acc_data.discriminator,
805                                self.colors.reset
806                            )?;
807                        }
808                        if let Some(ref data) = acc_data.data {
809                            writeln!(
810                                output,
811                                "{}      {}data ({} bytes): {}{:?}{}",
812                                indent,
813                                self.colors.gray,
814                                data.len(),
815                                self.colors.cyan,
816                                data,
817                                self.colors.reset
818                            )?;
819                        }
820                        if let Some(tree_idx) = acc_data.merkle_tree_index {
821                            if let Some(tree_pubkey) = acc_data.merkle_tree_pubkey {
822                                writeln!(
823                                    output,
824                                    "{}      {}merkle_tree_pubkey (index {}{}{}): {}{}{}",
825                                    indent,
826                                    self.colors.gray,
827                                    self.colors.cyan,
828                                    tree_idx,
829                                    self.colors.gray,
830                                    self.colors.yellow,
831                                    tree_pubkey,
832                                    self.colors.reset
833                                )?;
834                            } else {
835                                writeln!(
836                                    output,
837                                    "{}      {}merkle_tree_index: {}{}{}",
838                                    indent,
839                                    self.colors.gray,
840                                    self.colors.cyan,
841                                    tree_idx,
842                                    self.colors.reset
843                                )?;
844                            }
845                        } else if let Some(tree_pubkey) = acc_data.merkle_tree_pubkey {
846                            writeln!(
847                                output,
848                                "{}      {}merkle_tree_pubkey: {}{}{}",
849                                indent,
850                                self.colors.gray,
851                                self.colors.yellow,
852                                tree_pubkey,
853                                self.colors.reset
854                            )?;
855                        }
856                    }
857                }
858
859                // Display address parameters with actual values
860                if let Some(address_params) = address_params {
861                    writeln!(
862                        output,
863                        "{}{}New Addresses ({}):{}",
864                        indent,
865                        self.colors.gray,
866                        address_params.len(),
867                        self.colors.reset
868                    )?;
869                    for (i, addr_param) in address_params.iter().enumerate() {
870                        writeln!(
871                            output,
872                            "{}  {}[{}] {}seed: {}{:?}{}",
873                            indent,
874                            self.colors.gray,
875                            i,
876                            self.colors.gray,
877                            self.colors.cyan,
878                            addr_param.seed,
879                            self.colors.reset
880                        )?;
881
882                        // Check if v2 by comparing tree and queue pubkeys
883                        let is_v2 = addr_param.address_merkle_tree_pubkey
884                            == addr_param.address_queue_pubkey;
885
886                        // Display address tree
887                        if let Some(tree_pubkey) = addr_param.address_merkle_tree_pubkey {
888                            writeln!(
889                                output,
890                                "{}      {}tree[{}]: {}{}{}",
891                                indent,
892                                self.colors.gray,
893                                addr_param.merkle_tree_index.unwrap_or(0),
894                                self.colors.yellow,
895                                tree_pubkey,
896                                self.colors.reset
897                            )?;
898                        }
899
900                        // Only display queue for v1 trees (when different from tree)
901                        if !is_v2 {
902                            if let Some(queue_pubkey) = addr_param.address_queue_pubkey {
903                                writeln!(
904                                    output,
905                                    "{}      {}queue[{}]: {}{}{}",
906                                    indent,
907                                    self.colors.gray,
908                                    addr_param.address_queue_index.unwrap_or(0),
909                                    self.colors.yellow,
910                                    queue_pubkey,
911                                    self.colors.reset
912                                )?;
913                            }
914                        }
915
916                        if let Some(ref derived_addr) = addr_param.derived_address {
917                            writeln!(
918                                output,
919                                "{}      {}address: {}{:?}{}",
920                                indent,
921                                self.colors.gray,
922                                self.colors.cyan,
923                                derived_addr,
924                                self.colors.reset
925                            )?;
926                        }
927                        let assignment_str = match addr_param.assigned_account_index {
928                            super::types::AddressAssignment::AssignedIndex(idx) => {
929                                format!("{}", idx)
930                            }
931                            super::types::AddressAssignment::None => "none".to_string(),
932                            super::types::AddressAssignment::V1 => "n/a (v1)".to_string(),
933                        };
934                        writeln!(
935                            output,
936                            "{}      {}assigned: {}{}{}",
937                            indent,
938                            self.colors.gray,
939                            self.colors.yellow,
940                            assignment_str,
941                            self.colors.reset
942                        )?;
943                    }
944                }
945
946                if let Some(fee_info) = fee_info {
947                    if let Some(relay_fee) = fee_info.relay_fee {
948                        writeln!(
949                            output,
950                            "{}{}Relay Fee: {}{} lamports{}",
951                            indent,
952                            self.colors.gray,
953                            self.colors.yellow,
954                            relay_fee,
955                            self.colors.reset
956                        )?;
957                    }
958                    if let Some(compression_fee) = fee_info.compression_fee {
959                        writeln!(
960                            output,
961                            "{}{}Compression Fee: {}{} lamports{}",
962                            indent,
963                            self.colors.gray,
964                            self.colors.yellow,
965                            compression_fee,
966                            self.colors.reset
967                        )?;
968                    }
969                }
970            }
971            super::types::ParsedInstructionData::ComputeBudget {
972                instruction_type,
973                value,
974            } => {
975                write!(
976                    output,
977                    "{}{}Compute Budget: {}{}{}",
978                    indent,
979                    self.colors.gray,
980                    self.colors.yellow,
981                    instruction_type,
982                    self.colors.reset
983                )?;
984
985                if let Some(val) = value {
986                    writeln!(output, " ({})", val)?;
987                } else {
988                    writeln!(output)?;
989                }
990            }
991            super::types::ParsedInstructionData::System {
992                instruction_type,
993                lamports,
994                space: _,
995                new_account: _,
996            } => {
997                write!(
998                    output,
999                    "{}{}System: {}{}{}",
1000                    indent,
1001                    self.colors.gray,
1002                    self.colors.yellow,
1003                    instruction_type,
1004                    self.colors.reset
1005                )?;
1006
1007                if let Some(amount) = lamports {
1008                    writeln!(output, " ({} lamports)", amount)?;
1009                } else {
1010                    writeln!(output)?;
1011                }
1012            }
1013            super::types::ParsedInstructionData::Unknown {
1014                program_name,
1015                data_preview: _,
1016            } => {
1017                writeln!(
1018                    output,
1019                    "{}{}Program: {}{}{}",
1020                    indent, self.colors.gray, self.colors.yellow, program_name, self.colors.reset
1021                )?;
1022
1023                // Show raw instruction data for unknown programs with chunking
1024                // Skip instruction data for account compression program unless explicitly configured
1025                let should_show_data = if program_name == "Account Compression" {
1026                    self.config.show_compression_instruction_data
1027                } else {
1028                    true
1029                };
1030
1031                if !instruction_data.is_empty() && should_show_data {
1032                    writeln!(
1033                        output,
1034                        "{}{}Raw instruction data ({} bytes): {}[",
1035                        indent,
1036                        self.colors.gray,
1037                        instruction_data.len(),
1038                        self.colors.cyan
1039                    )?;
1040
1041                    // Chunk the data into 32-byte groups for better readability
1042                    for (i, chunk) in instruction_data.chunks(32).enumerate() {
1043                        write!(output, "{}  ", indent)?;
1044                        for (j, byte) in chunk.iter().enumerate() {
1045                            if j > 0 {
1046                                write!(output, ", ")?;
1047                            }
1048                            write!(output, "{}", byte)?;
1049                        }
1050                        if i < instruction_data.chunks(32).len() - 1 {
1051                            writeln!(output, ",")?;
1052                        } else {
1053                            writeln!(output, "]{}", self.colors.reset)?;
1054                        }
1055                    }
1056                }
1057            }
1058        }
1059
1060        Ok(())
1061    }
1062
1063    /// Write account changes section
1064    fn write_account_changes_section(
1065        &self,
1066        output: &mut String,
1067        log: &EnhancedTransactionLog,
1068    ) -> fmt::Result {
1069        writeln!(output)?;
1070        writeln!(
1071            output,
1072            "{}Account Changes ({}):{}\n",
1073            self.colors.bold,
1074            log.account_changes.len(),
1075            self.colors.reset
1076        )?;
1077
1078        for change in &log.account_changes {
1079            self.write_account_change(output, change)?;
1080        }
1081
1082        Ok(())
1083    }
1084
1085    /// Write single account change
1086    fn write_account_change(&self, output: &mut String, change: &AccountChange) -> fmt::Result {
1087        writeln!(
1088            output,
1089            "│ {}{} {} ({}) - {}{}{}",
1090            change.access.symbol(change.account_index),
1091            self.colors.cyan,
1092            change.pubkey,
1093            change.access.text(),
1094            self.colors.yellow,
1095            change.account_type,
1096            self.colors.reset
1097        )?;
1098
1099        if change.lamports_before != change.lamports_after {
1100            writeln!(
1101                output,
1102                "│   {}Lamports: {} → {}{}",
1103                self.colors.gray, change.lamports_before, change.lamports_after, self.colors.reset
1104            )?;
1105        }
1106
1107        Ok(())
1108    }
1109
1110    /// Write Light Protocol events section
1111    fn write_light_events_section(
1112        &self,
1113        output: &mut String,
1114        log: &EnhancedTransactionLog,
1115    ) -> fmt::Result {
1116        writeln!(output)?;
1117        writeln!(
1118            output,
1119            "{}Light Protocol Events ({}):{}\n",
1120            self.colors.bold,
1121            log.light_events.len(),
1122            self.colors.reset
1123        )?;
1124
1125        for event in &log.light_events {
1126            writeln!(
1127                output,
1128                "│ {}Event: {}{}{}",
1129                self.colors.blue, self.colors.yellow, event.event_type, self.colors.reset
1130            )?;
1131
1132            if !event.compressed_accounts.is_empty() {
1133                writeln!(
1134                    output,
1135                    "│   {}Compressed Accounts: {}{}",
1136                    self.colors.gray,
1137                    event.compressed_accounts.len(),
1138                    self.colors.reset
1139                )?;
1140            }
1141
1142            if !event.merkle_tree_changes.is_empty() {
1143                writeln!(
1144                    output,
1145                    "│   {}Merkle Tree Changes: {}{}",
1146                    self.colors.gray,
1147                    event.merkle_tree_changes.len(),
1148                    self.colors.reset
1149                )?;
1150            }
1151        }
1152
1153        Ok(())
1154    }
1155
1156    /// Write program logs section using LiteSVM's pretty logs
1157    fn write_program_logs_section(
1158        &self,
1159        output: &mut String,
1160        log: &EnhancedTransactionLog,
1161    ) -> fmt::Result {
1162        writeln!(output)?;
1163        writeln!(
1164            output,
1165            "{}│{} {}Program Logs:{}",
1166            self.colors.gray, self.colors.reset, self.colors.bold, self.colors.reset
1167        )?;
1168        writeln!(output, "{}│{}", self.colors.gray, self.colors.reset)?;
1169
1170        // Display LiteSVM's pretty formatted logs with proper indentation
1171        for line in log.program_logs_pretty.lines() {
1172            if !line.trim().is_empty() {
1173                writeln!(
1174                    output,
1175                    "{}│{} {}",
1176                    self.colors.gray, self.colors.reset, line
1177                )?;
1178            }
1179        }
1180
1181        Ok(())
1182    }
1183
1184    /// Get tree-style indentation for given depth
1185    fn get_tree_indent(&self, depth: usize) -> String {
1186        let border = format!("{}│{} ", self.colors.gray, self.colors.reset);
1187        if depth == 0 {
1188            border
1189        } else {
1190            format!("{}{}", border, "│  ".repeat(depth))
1191        }
1192    }
1193
1194    /// Get color for transaction status
1195    fn status_color(&self, status: &TransactionStatus) -> &str {
1196        match status {
1197            TransactionStatus::Success => &self.colors.green,
1198            TransactionStatus::Failed(_) => &self.colors.red,
1199            TransactionStatus::Unknown => &self.colors.yellow,
1200        }
1201    }
1202
1203    /// Get human-readable name for known accounts using constants and test accounts
1204    fn get_account_name(&self, pubkey: &solana_sdk::pubkey::Pubkey) -> String {
1205        let pubkey_bytes = pubkey.to_bytes();
1206
1207        // Light Protocol Programs and Accounts from constants
1208        if pubkey_bytes == light_sdk_types::constants::LIGHT_SYSTEM_PROGRAM_ID {
1209            return "light system program".to_string();
1210        }
1211        if pubkey_bytes == light_sdk_types::constants::ACCOUNT_COMPRESSION_PROGRAM_ID {
1212            return "account compression program".to_string();
1213        }
1214        if pubkey_bytes == light_sdk_types::constants::REGISTERED_PROGRAM_PDA {
1215            return "registered program pda".to_string();
1216        }
1217        if pubkey_bytes == light_sdk_types::constants::ACCOUNT_COMPRESSION_AUTHORITY_PDA {
1218            return "account compression authority".to_string();
1219        }
1220        if pubkey_bytes == light_sdk_types::constants::NOOP_PROGRAM_ID {
1221            return "noop program".to_string();
1222        }
1223        if pubkey_bytes == light_sdk_types::constants::C_TOKEN_PROGRAM_ID {
1224            return "compressed token program".to_string();
1225        }
1226        if pubkey_bytes == light_sdk_types::constants::ADDRESS_TREE_V1 {
1227            return "address tree v1".to_string();
1228        }
1229        if pubkey_bytes == light_sdk_types::constants::ADDRESS_QUEUE_V1 {
1230            return "address queue v1".to_string();
1231        }
1232        if pubkey_bytes == light_sdk_types::constants::SOL_POOL_PDA {
1233            return "sol pool pda".to_string();
1234        }
1235
1236        // String-based matches for test accounts and other addresses
1237        match pubkey.to_string().as_str() {
1238            "FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy" => "test program".to_string(),
1239
1240            // Test accounts from test_accounts.rs - Local Test Validator
1241            "smt1NamzXdq4AMqS2fS2F1i5KTYPZRhoHgWx38d8WsT" => "v1 state merkle tree".to_string(),
1242            "nfq1NvQDJ2GEgnS8zt9prAe8rjjpAW1zFkrvZoBR148" => "v1 nullifier queue".to_string(),
1243            "cpi1uHzrEhBG733DoEJNgHCyRS3XmmyVNZx5fonubE4" => "v1 cpi context".to_string(),
1244            "amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2" => "v1 address merkle tree".to_string(),
1245            "aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F" => "v1 address queue".to_string(),
1246
1247            // V2 State Trees and Queues (5 tree triples)
1248            "bmt1LryLZUMmF7ZtqESaw7wifBXLfXHQYoE4GAmrahU" => "v2 state merkle tree 1".to_string(),
1249            "oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto" => "v2 state output queue 1".to_string(),
1250            "cpi15BoVPKgEPw5o8wc2T816GE7b378nMXnhH3Xbq4y" => "v2 cpi context 1".to_string(),
1251            "bmt2UxoBxB9xWev4BkLvkGdapsz6sZGkzViPNph7VFi" => "v2 state merkle tree 2".to_string(),
1252            "oq2UkeMsJLfXt2QHzim242SUi3nvjJs8Pn7Eac9H9vg" => "v2 state output queue 2".to_string(),
1253            "cpi2yGapXUR3As5SjnHBAVvmApNiLsbeZpF3euWnW6B" => "v2 cpi context 2".to_string(),
1254            "bmt3ccLd4bqSVZVeCJnH1F6C8jNygAhaDfxDwePyyGb" => "v2 state merkle tree 3".to_string(),
1255            "oq3AxjekBWgo64gpauB6QtuZNesuv19xrhaC1ZM1THQ" => "v2 state output queue 3".to_string(),
1256            "cpi3mbwMpSX8FAGMZVP85AwxqCaQMfEk9Em1v8QK9Rf" => "v2 cpi context 3".to_string(),
1257            "bmt4d3p1a4YQgk9PeZv5s4DBUmbF5NxqYpk9HGjQsd8" => "v2 state merkle tree 4".to_string(),
1258            "oq4ypwvVGzCUMoiKKHWh4S1SgZJ9vCvKpcz6RT6A8dq" => "v2 state output queue 4".to_string(),
1259            "cpi4yyPDc4bCgHAnsenunGA8Y77j3XEDyjgfyCKgcoc" => "v2 cpi context 4".to_string(),
1260            "bmt5yU97jC88YXTuSukYHa8Z5Bi2ZDUtmzfkDTA2mG2" => "v2 state merkle tree 5".to_string(),
1261            "oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P" => "v2 state output queue 5".to_string(),
1262            "cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6" => "v2 cpi context 5".to_string(),
1263
1264            // V2 Address Trees (test accounts)
1265            "amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx" => "v2 address merkle tree".to_string(),
1266
1267            // CPI Authority (commonly used in tests)
1268            "HZH7qSLcpAeDqCopVU4e5XkhT9j3JFsQiq8CmruY3aru" => "cpi authority pda".to_string(),
1269
1270            // Solana Native Programs
1271            id if id == system_program::ID.to_string() => "system program".to_string(),
1272            "ComputeBudget111111111111111111111111111111" => "compute budget program".to_string(),
1273            "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" => "token program".to_string(),
1274            "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" => {
1275                "associated token program".to_string()
1276            }
1277
1278            _ => {
1279                // Check if it's a PDA or regular account
1280                if pubkey.is_on_curve() {
1281                    "user account".to_string()
1282                } else {
1283                    "pda account".to_string()
1284                }
1285            }
1286        }
1287    }
1288}