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