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