1use 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
19fn 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
32fn 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
41static KNOWN_ACCOUNTS: &[(&str, &str)] = &[
43 (
45 "FNt7byTHev1k5x2cXZLBr8TdWiC3zoP5vcnZR4P682Uy",
46 "test program",
47 ),
48 (
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 (
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 (
132 "amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx",
133 "v2 address merkle tree",
134 ),
135 (
137 "HZH7qSLcpAeDqCopVU4e5XkhT9j3JFsQiq8CmruY3aru",
138 "light system cpi authority",
139 ),
140 (
141 "GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy",
142 "light token cpi authority",
143 ),
144 (
146 "r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti",
147 "rent sponsor",
148 ),
149 (
151 "ACXg8a7VaqecBWrSbdu73W4Pg9gsqXJ3EXAqkHyhvVXg",
152 "compressible config",
153 ),
154 (
156 "35hkDgaAKwMCaxRz2ocSZ6NaUrtKkyNqU6c4RV3tYJRh",
157 "registered program pda",
158 ),
159 (
161 "8gH9tmziWsS8Wc4fnoN5ax3jsSumNYoRDuSBvmH2GMH8",
162 "config counter pda",
163 ),
164 (
166 "DumMsyvkaGJG4QnQ1BhTgvoRMXsgGxfpKDUCr22Xqu4w",
167 "registered registry program pda",
168 ),
169 (
171 "HwXnGK3tPkkVY6P439H2p68AxpeuWXd5PcrAxFpbmfbA",
172 "account compression authority pda",
173 ),
174 (
176 "CHK57ywWSDncAoRu1F8QgwYJeXuAJyyBYT4LixLXvMZ1",
177 "sol pool pda",
178 ),
179 (
181 "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV",
182 "noop program",
183 ),
184 ("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#[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#[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#[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
266pub 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 fn apply_line_breaks(&self, text: &str) -> String {
282 let mut result = String::new();
283
284 for line in text.lines() {
285 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 fn format_line_if_needed(&self, line: &str) -> Option<String> {
299 let leading_chars = line
301 .chars()
302 .take_while(|&c| c.is_whitespace() || "│├└┌┬┴┐┤─".contains(c))
303 .collect::<String>();
304
305 if line.contains(": [") && line.contains("]") {
307 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]; let array_part = &line[start + 2..end + 1]; let suffix = &line[end + 1..];
314
315 let max_width = if line.contains("Raw instruction data") {
317 80 } else {
319 50 };
321
322 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 if line.contains('|') && !line.trim_start().starts_with('|') {
337 let mut new_line = String::new();
339 let mut any_modified = false;
340
341 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 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); 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 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 if value.starts_with('[') && value.ends_with(']') {
384 let inner = &value[1..value.len() - 1]; 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 if current_line.len() + addition.len() > max_width && !current_line.is_empty() {
401 if first_line {
403 result.push_str(¤t_line);
404 first_line = false;
405 } else {
406 result.push_str(&format!("\n{}{}", indent, current_line));
407 }
408 current_line = addition;
410 } else {
411 current_line.push_str(&addition);
412 }
413 }
414
415 if !current_line.is_empty() {
417 if first_line {
418 result.push_str(¤t_line);
419 } else {
420 result.push_str(&format!("\n{}{}", indent, current_line));
421 }
422 }
423
424 result.push(']');
425 } else {
426 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 pub fn format(&self, log: &EnhancedTransactionLog, tx_number: usize) -> String {
449 let mut output = String::new();
450
451 writeln!(output, "{}┌──────────────────────────────────────────────────────────── Transaction #{} ─────────────────────────────────────────────────────────────┐{}", self.colors.gray, tx_number, self.colors.reset).expect("Failed to write box header");
453
454 self.write_transaction_header(&mut output, log)
456 .expect("Failed to write header");
457
458 if !log.instructions.is_empty() {
460 self.write_instructions_section(&mut output, log)
461 .expect("Failed to write instructions");
462 }
463
464 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 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 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 writeln!(output, "{}└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘{}", self.colors.gray, self.colors.reset).expect("Failed to write box footer");
484
485 self.apply_line_breaks(&output)
487 }
488
489 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 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 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 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 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 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 match self.config.verbosity {
611 LogVerbosity::Detailed | LogVerbosity::Full => {
612 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 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 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 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 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 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 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 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 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 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 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 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 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 result.push('(');
848 result.push_str(trimmed);
849 result.push(')');
850 } else {
851 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 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 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; 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 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 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 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 result.push('[');
928 result.push_str(&array_content);
929 result.push(']');
930 }
931 } else {
932 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 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 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 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 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 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 if !field.name.is_empty() {
1023 writeln!(
1024 output,
1025 "{}{}{}:{}",
1026 field_indent, self.colors.gray, field.name, self.colors.reset
1027 )?;
1028 }
1029 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 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 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 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 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 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 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 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 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 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 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 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}