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 = "Data Len")]
225 data_len: String,
226 #[tabled(rename = "Lamports")]
227 lamports: String,
228 #[tabled(rename = "Change")]
229 lamports_change: String,
230}
231
232#[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
264pub 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 fn apply_line_breaks(&self, text: &str) -> String {
280 let mut result = String::new();
281
282 for line in text.lines() {
283 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 fn format_line_if_needed(&self, line: &str) -> Option<String> {
297 let leading_chars = line
299 .chars()
300 .take_while(|&c| c.is_whitespace() || "│├└┌┬┴┐┤─".contains(c))
301 .collect::<String>();
302
303 if line.contains(": [") && line.contains("]") {
305 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]; let array_part = &line[start + 2..end + 1]; let suffix = &line[end + 1..];
312
313 let max_width = if line.contains("Raw instruction data") {
315 80 } else {
317 50 };
319
320 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 if line.contains('|') && !line.trim_start().starts_with('|') {
335 let mut new_line = String::new();
337 let mut any_modified = false;
338
339 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 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); 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 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 if value.starts_with('[') && value.ends_with(']') {
382 let inner = &value[1..value.len() - 1]; 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 if current_line.len() + addition.len() > max_width && !current_line.is_empty() {
399 if first_line {
401 result.push_str(¤t_line);
402 first_line = false;
403 } else {
404 result.push_str(&format!("\n{}{}", indent, current_line));
405 }
406 current_line = addition;
408 } else {
409 current_line.push_str(&addition);
410 }
411 }
412
413 if !current_line.is_empty() {
415 if first_line {
416 result.push_str(¤t_line);
417 } else {
418 result.push_str(&format!("\n{}{}", indent, current_line));
419 }
420 }
421
422 result.push(']');
423 } else {
424 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 pub fn format(&self, log: &EnhancedTransactionLog, tx_number: usize) -> String {
447 let mut output = String::new();
448
449 writeln!(output, "{}┌──────────────────────────────────────────────────────────── Transaction #{} ─────────────────────────────────────────────────────────────┐{}", self.colors.gray, tx_number, self.colors.reset).expect("Failed to write box header");
451
452 self.write_transaction_header(&mut output, log)
454 .expect("Failed to write header");
455
456 if !log.instructions.is_empty() {
458 self.write_instructions_section(&mut output, log)
459 .expect("Failed to write instructions");
460 }
461
462 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 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 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 writeln!(output, "{}└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘{}", self.colors.gray, self.colors.reset).expect("Failed to write box footer");
482
483 self.apply_line_breaks(&output)
485 }
486
487 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 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 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 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 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 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 match self.config.verbosity {
609 LogVerbosity::Detailed | LogVerbosity::Full => {
610 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 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 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 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 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 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 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 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 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 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 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 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 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 result.push('(');
833 result.push_str(trimmed);
834 result.push(')');
835 } else {
836 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 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 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; 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 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 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 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 result.push('[');
913 result.push_str(&array_content);
914 result.push(']');
915 }
916 } else {
917 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 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 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 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 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 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 if !field.name.is_empty() {
1008 writeln!(
1009 output,
1010 "{}{}{}:{}",
1011 field_indent, self.colors.gray, field.name, self.colors.reset
1012 )?;
1013 }
1014 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 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 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 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 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 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 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 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 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 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 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 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}