1use crate::{
2 error::Result,
3 output::{change_in_sol, classify_account, pretty_lamports_to_sol, status_to_string},
4 parse::{parse, partially_parse},
5};
6use chrono::{TimeZone, Utc};
7use console::style;
8use serde::Serialize;
9use serde_json::Value;
10use solana_program::message::VersionedMessage;
11use solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey};
12use solana_transaction_status::{
13 option_serializer::OptionSerializer, EncodedConfirmedTransactionWithStatusMeta,
14 EncodedTransactionWithStatusMeta, TransactionStatus,
15};
16use std::fmt;
17
18pub struct RawTransactionFieldVisibility {
19 overview: bool,
20 transaction: bool,
21}
22
23impl RawTransactionFieldVisibility {
24 pub fn new_all_enabled() -> Self {
25 Self {
26 overview: true,
27 transaction: true,
28 }
29 }
30
31 pub fn new_all_disabled() -> Self {
32 Self {
33 overview: false,
34 transaction: false,
35 }
36 }
37
38 pub fn overview(&self) -> bool {
39 self.overview
40 }
41
42 pub fn enable_overview(&mut self) -> &mut Self {
43 self.overview = true;
44 self
45 }
46
47 pub fn disable_overview(&mut self) -> &mut Self {
48 self.overview = false;
49 self
50 }
51
52 pub fn transaction(&self) -> bool {
53 self.transaction
54 }
55
56 pub fn enable_transaction(&mut self) -> &mut Self {
57 self.transaction = true;
58 self
59 }
60
61 pub fn disable_transaction(&mut self) -> &mut Self {
62 self.transaction = false;
63 self
64 }
65}
66
67#[derive(Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct DisplayRawMessageHeader {
70 pub num_required_signatures: u8,
71 pub num_readonly_signed_accounts: u8,
72 pub num_readonly_unsigned_accounts: u8,
73}
74
75#[derive(Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct DisplayRawInstruction {
78 pub program_id_index: u8,
79 pub accounts: Vec<u8>,
80 pub data: String,
81}
82
83#[derive(Serialize)]
84#[serde(rename_all = "camelCase")]
85pub struct DisplayRawMessage {
86 pub header: DisplayRawMessageHeader,
87 pub account_keys: Vec<String>,
88 pub recent_blockhash: String,
89 pub instructions: Vec<DisplayRawInstruction>,
90}
91
92#[derive(Serialize)]
93#[serde(rename_all = "camelCase")]
94pub struct DisplayRawTransactionContent {
95 pub signatures: Vec<String>,
96 pub message: DisplayRawMessage,
97}
98
99#[derive(Serialize)]
100#[serde(rename_all = "camelCase")]
101pub struct DisplayRawTransactionOverview {
102 pub signature: String,
103 pub result: String,
104 pub timestamp: String,
105 pub confirmation_status: String,
106 pub confirmations: String,
107 pub slot: u64,
108 pub recent_blockhash: String,
109 pub fee: String,
110}
111
112#[derive(Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct DisplayRawTransaction {
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub overview: Option<DisplayRawTransactionOverview>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub transaction: Option<DisplayRawTransactionContent>,
119}
120
121impl DisplayRawTransaction {
122 pub fn from(
123 transaction: &EncodedConfirmedTransactionWithStatusMeta,
124 transaction_status: &TransactionStatus,
125 visibility: &RawTransactionFieldVisibility,
126 ) -> Result<Self> {
127 let EncodedConfirmedTransactionWithStatusMeta {
128 slot,
129 transaction,
130 block_time,
131 } = transaction;
132
133 let EncodedTransactionWithStatusMeta {
134 transaction, meta, ..
135 } = transaction;
136
137 let decoded_transaction = transaction.decode().unwrap();
138
139 let message = decoded_transaction.message;
140
141 let overview = if visibility.overview {
142 Some(DisplayRawTransactionOverview {
143 signature: decoded_transaction.signatures[0].to_string(),
144 result: meta
145 .as_ref()
146 .unwrap()
147 .err
148 .as_ref()
149 .map(|err| err.to_string())
150 .unwrap_or_else(|| "Success".to_string()),
151 timestamp: Utc
152 .timestamp_opt(block_time.unwrap(), 0)
153 .unwrap()
154 .to_string(),
155 confirmation_status: status_to_string(
156 transaction_status.confirmation_status.as_ref().unwrap(),
157 ),
158 confirmations: transaction_status
159 .confirmations
160 .map_or_else(|| "MAX (32)".to_string(), |n| n.to_string()),
161 slot: *slot,
162 recent_blockhash: message.recent_blockhash().to_string(),
163 fee: format!("◎ {}", pretty_lamports_to_sol(meta.as_ref().unwrap().fee)),
164 })
165 } else {
166 None
167 };
168
169 let transaction = if visibility.transaction {
170 Some(DisplayRawTransactionContent {
171 signatures: decoded_transaction
172 .signatures
173 .into_iter()
174 .map(|sig| sig.to_string())
175 .collect(),
176 message: DisplayRawMessage {
177 header: DisplayRawMessageHeader {
178 num_required_signatures: message.header().num_required_signatures,
179 num_readonly_signed_accounts: message.header().num_readonly_signed_accounts,
180 num_readonly_unsigned_accounts: message
181 .header()
182 .num_readonly_unsigned_accounts,
183 },
184 account_keys: message
185 .static_account_keys()
186 .iter()
187 .map(|key| key.to_string())
188 .collect(),
189 recent_blockhash: message.recent_blockhash().to_string(),
190 instructions: message
191 .instructions()
192 .iter()
193 .map(|instruction| DisplayRawInstruction {
194 program_id_index: instruction.program_id_index,
195 accounts: instruction.accounts.clone(),
196 data: bs58::encode(instruction.data.clone()).into_string(),
197 })
198 .collect(),
199 },
200 })
201 } else {
202 None
203 };
204
205 Ok(DisplayRawTransaction {
206 overview,
207 transaction,
208 })
209 }
210}
211
212impl fmt::Display for DisplayRawTransaction {
213 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
214 if let Some(overview) = &self.overview {
215 writeln!(
216 f,
217 "================================================================================"
218 )?;
219 writeln!(f, "{:^80}", style("Overview").bold())?;
220 writeln!(
221 f,
222 "================================================================================"
223 )?;
224
225 writeln!(f)?;
226
227 writeln!(f, "{} {}", style("Signature:").bold(), overview.signature)?;
228 writeln!(f, "{} {}", style("Result:").bold(), overview.result)?;
229 writeln!(f, "{} {}", style("Timestamp:").bold(), overview.timestamp)?;
230 writeln!(
231 f,
232 "{} {}",
233 style("Confirmation Status:").bold(),
234 overview.confirmation_status
235 )?;
236 writeln!(
237 f,
238 "{} {}",
239 style("Confirmations:").bold(),
240 overview.confirmations
241 )?;
242 writeln!(f, "{} {}", style("Slot:").bold(), overview.slot)?;
243 writeln!(
244 f,
245 "{} {}",
246 style("Recent Blockhash:").bold(),
247 overview.recent_blockhash
248 )?;
249 write!(f, "{} {}", style("Fee:").bold(), overview.fee)?;
250 }
251
252 if self.overview.is_some() && self.transaction.is_some() {
253 writeln!(f)?;
254 writeln!(f)?;
255 }
256
257 if let Some(transaction) = &self.transaction {
258 writeln!(
259 f,
260 "================================================================================"
261 )?;
262 writeln!(f, "{:^80}", style("Raw Transaction").bold())?;
263 writeln!(
264 f,
265 "================================================================================"
266 )?;
267
268 writeln!(f)?;
269
270 writeln!(
271 f,
272 "{}",
273 style(format!("Signatures ({}):", transaction.signatures.len())).bold()
274 )?;
275
276 for (index, signature) in transaction.signatures.iter().enumerate() {
277 writeln!(f, " {:>2} {}", style(index).bold(), signature)?;
278 }
279
280 writeln!(f)?;
281
282 writeln!(f, "{}", style("Message:").bold())?;
283
284 writeln!(f, " {}", style("Header:").bold())?;
285
286 writeln!(
287 f,
288 " {} {}",
289 style("# of required signatures:").bold(),
290 transaction.message.header.num_required_signatures
291 )?;
292
293 writeln!(
294 f,
295 " {} {}",
296 style("# of read-only signed accounts:").bold(),
297 transaction.message.header.num_readonly_signed_accounts
298 )?;
299
300 writeln!(
301 f,
302 " {} {}",
303 style("# of read-only unsigned accounts:").bold(),
304 transaction.message.header.num_readonly_unsigned_accounts
305 )?;
306
307 writeln!(
308 f,
309 " {}",
310 style(format!(
311 "Account Keys ({}):",
312 transaction.message.account_keys.len()
313 ))
314 .bold()
315 )?;
316
317 for (index, account_key) in transaction.message.account_keys.iter().enumerate() {
318 writeln!(f, " {:>2} {}", style(index).bold(), account_key)?;
319 }
320
321 writeln!(f, " {}", style("Recent Blockhash:").bold())?;
322
323 writeln!(f, " {}", transaction.message.recent_blockhash)?;
324
325 write!(
326 f,
327 " {}",
328 style(format!(
329 "Instructions ({}):",
330 transaction.message.instructions.len()
331 ))
332 .bold()
333 )?;
334
335 for (
336 index,
337 DisplayRawInstruction {
338 program_id_index,
339 accounts,
340 data,
341 },
342 ) in transaction.message.instructions.iter().enumerate()
343 {
344 writeln!(f)?;
345 writeln!(
346 f,
347 " {:>2} {} {}",
348 style(index).bold(),
349 style("Program Id Index:").bold(),
350 program_id_index
351 )?;
352 writeln!(
353 f,
354 " {} {:?}",
355 style("Account Indices:").bold(),
356 accounts
357 )?;
358 write!(f, " {} {:?}", style("Data:").bold(), data)?;
359 }
360 }
361
362 Ok(())
363 }
364}
365
366pub struct TransactionFieldVisibility {
367 overview: bool,
368 transaction: bool,
369 log_messages: bool,
370}
371
372impl TransactionFieldVisibility {
373 pub fn new_all_enabled() -> Self {
374 Self {
375 overview: true,
376 transaction: true,
377 log_messages: true,
378 }
379 }
380
381 pub fn new_all_disabled() -> Self {
382 Self {
383 overview: false,
384 transaction: false,
385 log_messages: false,
386 }
387 }
388
389 pub fn overview(&self) -> bool {
390 self.overview
391 }
392
393 pub fn enable_overview(&mut self) -> &mut Self {
394 self.overview = true;
395 self
396 }
397
398 pub fn disable_overview(&mut self) -> &mut Self {
399 self.overview = false;
400 self
401 }
402
403 pub fn transaction(&self) -> bool {
404 self.transaction
405 }
406
407 pub fn enable_transaction(&mut self) -> &mut Self {
408 self.transaction = true;
409 self
410 }
411
412 pub fn disable_transaction(&mut self) -> &mut Self {
413 self.transaction = false;
414 self
415 }
416
417 pub fn log_messages(&self) -> bool {
418 self.log_messages
419 }
420
421 pub fn enable_log_messages(&mut self) -> &mut Self {
422 self.log_messages = true;
423 self
424 }
425
426 pub fn disable_log_messages(&mut self) -> &mut Self {
427 self.log_messages = false;
428 self
429 }
430}
431
432#[derive(Serialize)]
433#[serde(rename_all = "camelCase")]
434pub struct DisplayPartiallyParsedInstruction {
435 pub program_id: String,
436 pub accounts: Vec<String>,
437 pub data: String,
438}
439
440#[derive(Serialize)]
441#[serde(rename_all = "camelCase")]
442pub struct DisplayParsedInstruction {
443 pub program: String,
444 pub program_id: String,
445 pub parsed: Value,
446}
447
448#[derive(Serialize)]
449#[serde(rename_all = "camelCase")]
450pub enum DisplayInstruction {
451 Parsed(DisplayParsedInstruction),
452 PartiallyParsed(DisplayPartiallyParsedInstruction),
453}
454
455impl DisplayInstruction {
456 fn parse(instruction: &CompiledInstruction, account_keys: &[Pubkey]) -> Self {
457 let program_id = &account_keys[instruction.program_id_index as usize];
458 if let Ok(parsed_instruction) = parse(program_id, instruction, account_keys) {
459 DisplayInstruction::Parsed(parsed_instruction)
460 } else {
461 DisplayInstruction::PartiallyParsed(partially_parse(
462 program_id,
463 instruction,
464 account_keys,
465 ))
466 }
467 }
468}
469
470#[derive(Serialize)]
471#[serde(rename_all = "camelCase")]
472pub struct DisplayInputAccount {
473 pub pubkey: String,
474 pub fee_payer: bool,
475 pub writable: bool,
476 pub signer: bool,
477 pub program: bool,
478 pub post_balance_in_sol: String,
479 pub balance_change_in_sol: String,
480}
481
482#[derive(Serialize)]
483#[serde(rename_all = "camelCase")]
484pub struct DisplayTransactionContent {
485 pub accounts: Vec<DisplayInputAccount>,
486 pub instructions: Vec<DisplayInstruction>,
487}
488
489#[derive(Serialize)]
490#[serde(rename_all = "camelCase")]
491pub struct DisplayTransactionOverview {
492 pub signature: String,
493 pub result: String,
494 pub timestamp: String,
495 pub confirmation_status: String,
496 pub confirmations: String,
497 pub slot: u64,
498 pub recent_blockhash: String,
499 pub fee: String,
500}
501
502#[derive(Serialize)]
503#[serde(rename_all = "camelCase")]
504pub struct DisplayTransaction {
505 #[serde(skip_serializing_if = "Option::is_none")]
506 pub overview: Option<DisplayTransactionOverview>,
507 #[serde(skip_serializing_if = "Option::is_none")]
508 pub transaction: Option<DisplayTransactionContent>,
509 #[serde(skip_serializing_if = "Option::is_none")]
510 pub log_messages: Option<OptionSerializer<Vec<String>>>,
511}
512
513impl DisplayTransaction {
514 pub fn from(
515 transaction: &EncodedConfirmedTransactionWithStatusMeta,
516 transaction_status: &TransactionStatus,
517 visibility: &TransactionFieldVisibility,
518 ) -> Result<Self> {
519 let EncodedConfirmedTransactionWithStatusMeta {
520 slot,
521 transaction,
522 block_time,
523 } = transaction;
524
525 let EncodedTransactionWithStatusMeta {
526 transaction, meta, ..
527 } = transaction;
528
529 let decoded_transaction = transaction.decode().unwrap();
530
531 let message = decoded_transaction.message;
532 let overview = if visibility.overview {
533 Some(DisplayTransactionOverview {
534 signature: decoded_transaction.signatures[0].to_string(),
535 result: meta
536 .as_ref()
537 .unwrap()
538 .err
539 .as_ref()
540 .map(|err| err.to_string())
541 .unwrap_or_else(|| "Success".to_string()),
542 timestamp: Utc
543 .timestamp_opt(block_time.unwrap(), 0)
544 .unwrap()
545 .to_string(),
546 confirmation_status: status_to_string(
547 transaction_status.confirmation_status.as_ref().unwrap(),
548 ),
549 confirmations: transaction_status
550 .confirmations
551 .map_or_else(|| "MAX (32)".to_string(), |n| n.to_string()),
552 slot: *slot,
553 recent_blockhash: message.recent_blockhash().to_string(),
554 fee: format!("◎ {}", pretty_lamports_to_sol(meta.as_ref().unwrap().fee)),
555 })
556 } else {
557 None
558 };
559
560 let mut fee_payer_found = false; let transaction = if visibility.transaction {
562 Some(DisplayTransactionContent {
563 accounts: message
564 .static_account_keys()
565 .iter()
566 .enumerate()
567 .map(|(index, account_key)| DisplayInputAccount {
568 pubkey: account_key.to_string(),
569 fee_payer: if !fee_payer_found {
570 fee_payer_found = true;
571 true
572 } else {
573 false
574 },
575 writable: message.is_maybe_writable(index),
576 signer: message.is_signer(index),
577 program: match message.clone() {
578 VersionedMessage::Legacy(m) => m.maybe_executable(index),
579 VersionedMessage::V0(m) => m.is_key_called_as_program(index),
580 },
581 post_balance_in_sol: pretty_lamports_to_sol(
582 meta.as_ref().unwrap().post_balances[index],
583 ),
584 balance_change_in_sol: change_in_sol(
585 meta.as_ref().unwrap().post_balances[index],
586 meta.as_ref().unwrap().pre_balances[index],
587 ),
588 })
589 .collect(),
590 instructions: message
591 .instructions()
592 .iter()
593 .map(|instruction| {
594 DisplayInstruction::parse(instruction, message.static_account_keys())
595 })
596 .collect(),
597 })
598 } else {
599 None
600 };
601
602 let log_messages = if visibility.log_messages {
603 Some(meta.as_ref().unwrap().log_messages.clone())
604 } else {
605 None
606 };
607
608 Ok(DisplayTransaction {
609 overview,
610 transaction,
611 log_messages,
612 })
613 }
614}
615
616impl fmt::Display for DisplayTransaction {
617 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
618 if let Some(overview) = &self.overview {
619 writeln!(
620 f,
621 "================================================================================"
622 )?;
623 writeln!(f, "{:^80}", style("Overview").bold())?;
624 writeln!(
625 f,
626 "================================================================================"
627 )?;
628
629 writeln!(f)?;
630
631 writeln!(f, "{} {}", style("Signature:").bold(), overview.signature)?;
632 writeln!(f, "{} {}", style("Result:").bold(), overview.result)?;
633 writeln!(f, "{} {}", style("Timestamp:").bold(), overview.timestamp)?;
634 writeln!(
635 f,
636 "{} {}",
637 style("Confirmation Status:").bold(),
638 overview.confirmation_status
639 )?;
640 writeln!(
641 f,
642 "{} {}",
643 style("Confirmations:").bold(),
644 overview.confirmations
645 )?;
646 writeln!(f, "{} {}", style("Slot:").bold(), overview.slot)?;
647 writeln!(
648 f,
649 "{} {}",
650 style("Recent Blockhash:").bold(),
651 overview.recent_blockhash
652 )?;
653 write!(f, "{} {}", style("Fee:").bold(), overview.fee)?;
654 }
655
656 if self.overview.is_some() && self.transaction.is_some() {
657 writeln!(f)?;
658 writeln!(f)?;
659 }
660
661 if let Some(transaction) = &self.transaction {
662 writeln!(
663 f,
664 "================================================================================"
665 )?;
666 writeln!(f, "{:^80}", style("Transaction").bold())?;
667 writeln!(
668 f,
669 "================================================================================"
670 )?;
671
672 writeln!(f)?;
673
674 writeln!(
675 f,
676 "{}",
677 style(format!("Accounts ({}):", transaction.accounts.len())).bold()
678 )?;
679
680 for (index, account) in transaction.accounts.iter().enumerate() {
681 let account_type_string = classify_account(
682 account.fee_payer,
683 account.writable,
684 account.signer,
685 account.program,
686 );
687
688 let balance_information_string = if account.balance_change_in_sol != "0" {
689 format!(
690 "◎ {} (◎ {})",
691 account.post_balance_in_sol, account.balance_change_in_sol
692 )
693 } else {
694 format!("◎ {}", account.post_balance_in_sol)
695 };
696
697 writeln!(
698 f,
699 " {:>2} {:<44} {:31} {}",
700 style(index).bold(),
701 account.pubkey,
702 account_type_string,
703 balance_information_string
704 )?;
705 }
706
707 writeln!(f)?;
708
709 writeln!(
710 f,
711 "{}",
712 style(format!(
713 "Instructions ({}):",
714 transaction.instructions.len()
715 ))
716 .bold()
717 )?;
718
719 for (index, instruction) in transaction.instructions.iter().enumerate() {
720 if let DisplayInstruction::Parsed(instruction) = instruction {
721 writeln!(
722 f,
723 " {:>2} {} {} {}",
724 style(index).bold(),
725 style(&instruction.program).bold(),
726 style("Program:").bold(),
727 instruction.parsed["type"].to_string().trim_matches('"')
728 )?;
729 writeln!(f, " [{}]", instruction.program_id)?;
730 for (name, value) in instruction.parsed["info"].as_object().unwrap() {
731 writeln!(
732 f,
733 " {}{} {}",
734 style(name).bold(),
735 style(":").bold(),
736 value
737 )?;
738 }
739 } else if let DisplayInstruction::PartiallyParsed(instruction) = instruction {
740 writeln!(
741 f,
742 " {:>2} {} Unknown Instruction",
743 style(index).bold(),
744 style("Unknown Program:").bold(),
745 )?;
746 writeln!(f, " [{}]", instruction.program_id)?;
747 for (index, account) in instruction.accounts.iter().enumerate() {
748 writeln!(
749 f,
750 " {} {}{} {:<44}",
751 style("Account").bold(),
752 style(index).bold(),
753 style(":").bold(),
754 account,
755 )?;
756 }
757 writeln!(
758 f,
759 " {} {:?}",
760 style("Data:").bold(),
761 bs58::encode(instruction.data.clone()).into_string()
762 )?;
763 }
764 writeln!(f)?;
765 }
766 }
767
768 if self.overview.is_some() && self.transaction.is_none() && self.log_messages.is_some() {
769 writeln!(f)?;
770 writeln!(f)?;
771 }
772
773 if let Some(OptionSerializer::Some(log_messages)) = &self.log_messages {
774 write!(
775 f,
776 "{}",
777 style(format!("Log Messages ({}):", log_messages.len())).bold()
778 )?;
779
780 for (log_message_index, log_message) in log_messages.iter().enumerate() {
781 writeln!(f)?;
782 write!(f, " {:>2} {}", style(log_message_index).bold(), log_message)?;
783 }
784 }
785
786 Ok(())
787 }
788}