Skip to main content

light_instruction_decoder/
formatter.rs

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