trident_explorer/
transaction.rs

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; // always first account
561        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}