Skip to main content

solana_transaction_status/
lib.rs

1#![cfg(feature = "agave-unstable-api")]
2#![allow(clippy::arithmetic_side_effects)]
3
4pub use {
5    crate::extract_memos::extract_and_fmt_memos,
6    solana_reward_info::RewardType,
7    solana_transaction_status_client_types::{
8        ConfirmedTransactionStatusWithSignature, EncodeError, EncodedConfirmedBlock,
9        EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction,
10        EncodedTransactionWithStatusMeta, InnerInstruction, InnerInstructions, Reward, Rewards,
11        TransactionBinaryEncoding, TransactionConfirmationStatus, TransactionDetails,
12        TransactionStatus, TransactionStatusMeta, TransactionTokenBalance, UiAccountsList,
13        UiAddressTableLookup, UiCompiledInstruction, UiConfirmedBlock, UiInnerInstructions,
14        UiInstruction, UiLoadedAddresses, UiMessage, UiParsedInstruction, UiParsedMessage,
15        UiPartiallyDecodedInstruction, UiRawMessage, UiReturnDataEncoding, UiTransaction,
16        UiTransactionEncoding, UiTransactionReturnData, UiTransactionStatusMeta,
17        UiTransactionTokenBalance, option_serializer,
18    },
19};
20use {
21    crate::{
22        option_serializer::OptionSerializer,
23        parse_accounts::{
24            parse_legacy_message_accounts, parse_v0_message_accounts, parse_v1_message_accounts,
25        },
26        parse_instruction::parse,
27    },
28    agave_reserved_account_keys::ReservedAccountKeys,
29    base64::{Engine, prelude::BASE64_STANDARD},
30    serde::{Deserialize, Serialize},
31    solana_clock::{Slot, UnixTimestamp},
32    solana_hash::Hash,
33    solana_instruction::TRANSACTION_LEVEL_STACK_HEIGHT,
34    solana_message::{
35        AccountKeys, Message, VersionedMessage,
36        compiled_instruction::CompiledInstruction,
37        v0::{self, LoadedAddresses, LoadedMessage},
38    },
39    solana_pubkey::Pubkey,
40    solana_signature::Signature,
41    solana_transaction::{
42        Transaction,
43        versioned::{TransactionVersion, VersionedTransaction},
44    },
45    solana_transaction_error::TransactionError,
46    solana_transaction_status_client_types::UiTransactionConfig,
47    std::collections::HashSet,
48    thiserror::Error,
49};
50
51pub mod extract_memos;
52pub mod parse_accounts;
53pub mod parse_address_lookup_table;
54pub mod parse_associated_token;
55pub mod parse_bpf_loader;
56pub mod parse_instruction;
57pub mod parse_stake;
58pub mod parse_system;
59pub mod parse_token;
60pub mod parse_vote;
61pub mod token_balances;
62
63fn serialize_versioned_transaction(transaction: &VersionedTransaction) -> Vec<u8> {
64    wincode::serialize(transaction).expect("serialize versioned transaction")
65}
66
67pub struct BlockEncodingOptions {
68    pub transaction_details: TransactionDetails,
69    pub show_rewards: bool,
70    pub max_supported_transaction_version: Option<u8>,
71}
72
73/// Represents types that can be encoded into one of several encoding formats
74pub trait Encodable {
75    type Encoded;
76    fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded;
77}
78
79/// Represents types that can be encoded into one of several encoding formats
80pub trait EncodableWithMeta {
81    type Encoded;
82    fn encode_with_meta(
83        &self,
84        encoding: UiTransactionEncoding,
85        meta: &TransactionStatusMeta,
86    ) -> Self::Encoded;
87    fn json_encode(&self) -> Self::Encoded;
88}
89
90trait JsonAccounts {
91    type Encoded;
92    fn build_json_accounts(&self) -> Self::Encoded;
93}
94
95fn make_ui_partially_decoded_instruction(
96    instruction: &CompiledInstruction,
97    account_keys: &AccountKeys,
98    stack_height: Option<u32>,
99) -> UiPartiallyDecodedInstruction {
100    UiPartiallyDecodedInstruction {
101        program_id: account_keys[instruction.program_id_index as usize].to_string(),
102        accounts: instruction
103            .accounts
104            .iter()
105            .map(|&i| account_keys[i as usize].to_string())
106            .collect(),
107        data: bs58::encode(instruction.data.clone()).into_string(),
108        stack_height,
109    }
110}
111
112pub fn parse_ui_instruction(
113    instruction: &CompiledInstruction,
114    account_keys: &AccountKeys,
115    stack_height: Option<u32>,
116) -> UiInstruction {
117    let program_id = &account_keys[instruction.program_id_index as usize];
118    if let Ok(parsed_instruction) = parse(program_id, instruction, account_keys, stack_height) {
119        UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed_instruction))
120    } else {
121        UiInstruction::Parsed(UiParsedInstruction::PartiallyDecoded(
122            make_ui_partially_decoded_instruction(instruction, account_keys, stack_height),
123        ))
124    }
125}
126
127/// Maps a list of inner instructions from `solana_sdk` into a list of this
128/// crate's representation of inner instructions (with instruction indices).
129pub fn map_inner_instructions(
130    inner_instructions: solana_message::inner_instruction::InnerInstructionsList,
131) -> impl Iterator<Item = InnerInstructions> {
132    inner_instructions
133        .into_iter()
134        .enumerate()
135        .map(|(index, instructions)| InnerInstructions {
136            index: index as u8,
137            instructions: instructions
138                .into_iter()
139                .map(|info| InnerInstruction {
140                    stack_height: Some(u32::from(info.stack_height)),
141                    instruction: info.instruction,
142                })
143                .collect(),
144        })
145        .filter(|i| !i.instructions.is_empty())
146}
147
148pub fn parse_ui_inner_instructions(
149    inner_instructions: InnerInstructions,
150    account_keys: &AccountKeys,
151) -> UiInnerInstructions {
152    UiInnerInstructions {
153        index: inner_instructions.index,
154        instructions: inner_instructions
155            .instructions
156            .iter()
157            .map(
158                |InnerInstruction {
159                     instruction: ix,
160                     stack_height,
161                 }| { parse_ui_instruction(ix, account_keys, *stack_height) },
162            )
163            .collect(),
164    }
165}
166
167fn build_simple_ui_transaction_status_meta(
168    meta: TransactionStatusMeta,
169    show_rewards: bool,
170) -> UiTransactionStatusMeta {
171    UiTransactionStatusMeta {
172        err: meta.status.clone().map_err(Into::into).err(),
173        status: meta.status.map_err(Into::into),
174        fee: meta.fee,
175        pre_balances: meta.pre_balances,
176        post_balances: meta.post_balances,
177        inner_instructions: OptionSerializer::Skip,
178        log_messages: OptionSerializer::Skip,
179        pre_token_balances: meta
180            .pre_token_balances
181            .map(|balance| balance.into_iter().map(Into::into).collect())
182            .into(),
183        post_token_balances: meta
184            .post_token_balances
185            .map(|balance| balance.into_iter().map(Into::into).collect())
186            .into(),
187        rewards: if show_rewards {
188            meta.rewards.into()
189        } else {
190            OptionSerializer::Skip
191        },
192        loaded_addresses: OptionSerializer::Skip,
193        return_data: OptionSerializer::Skip,
194        compute_units_consumed: OptionSerializer::Skip,
195        cost_units: OptionSerializer::Skip,
196    }
197}
198
199fn parse_ui_transaction_status_meta(
200    meta: TransactionStatusMeta,
201    static_keys: &[Pubkey],
202    show_rewards: bool,
203) -> UiTransactionStatusMeta {
204    let account_keys = AccountKeys::new(static_keys, Some(&meta.loaded_addresses));
205    UiTransactionStatusMeta {
206        err: meta.status.clone().map_err(Into::into).err(),
207        status: meta.status.map_err(Into::into),
208        fee: meta.fee,
209        pre_balances: meta.pre_balances,
210        post_balances: meta.post_balances,
211        inner_instructions: meta
212            .inner_instructions
213            .map(|ixs| {
214                ixs.into_iter()
215                    .map(|ix| parse_ui_inner_instructions(ix, &account_keys))
216                    .collect()
217            })
218            .into(),
219        log_messages: meta.log_messages.into(),
220        pre_token_balances: meta
221            .pre_token_balances
222            .map(|balance| balance.into_iter().map(Into::into).collect())
223            .into(),
224        post_token_balances: meta
225            .post_token_balances
226            .map(|balance| balance.into_iter().map(Into::into).collect())
227            .into(),
228        rewards: if show_rewards { meta.rewards } else { None }.into(),
229        loaded_addresses: OptionSerializer::Skip,
230        return_data: OptionSerializer::or_skip(
231            meta.return_data.map(|return_data| return_data.into()),
232        ),
233        compute_units_consumed: OptionSerializer::or_skip(meta.compute_units_consumed),
234        cost_units: OptionSerializer::or_skip(meta.cost_units),
235    }
236}
237
238#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
239pub struct RewardsAndNumPartitions {
240    pub rewards: Rewards,
241    pub num_partitions: Option<u64>,
242}
243
244#[derive(Debug, Error)]
245pub enum ConvertBlockError {
246    #[error("transactions missing after converted, before: {0}, after: {1}")]
247    TransactionsMissing(usize, usize),
248}
249
250#[derive(Clone, Debug, PartialEq)]
251pub struct ConfirmedBlock {
252    pub previous_blockhash: String,
253    pub blockhash: String,
254    pub parent_slot: Slot,
255    pub transactions: Vec<TransactionWithStatusMeta>,
256    pub rewards: Rewards,
257    pub num_partitions: Option<u64>,
258    pub block_time: Option<UnixTimestamp>,
259    pub block_height: Option<u64>,
260}
261
262// Confirmed block with type guarantees that transaction metadata
263// is always present. Used for uploading to BigTable.
264#[derive(Clone, Debug, PartialEq)]
265pub struct VersionedConfirmedBlock {
266    pub previous_blockhash: String,
267    pub blockhash: String,
268    pub parent_slot: Slot,
269    pub transactions: Vec<VersionedTransactionWithStatusMeta>,
270    pub rewards: Rewards,
271    pub num_partitions: Option<u64>,
272    pub block_time: Option<UnixTimestamp>,
273    pub block_height: Option<u64>,
274}
275
276impl From<VersionedConfirmedBlock> for ConfirmedBlock {
277    fn from(block: VersionedConfirmedBlock) -> Self {
278        Self {
279            previous_blockhash: block.previous_blockhash,
280            blockhash: block.blockhash,
281            parent_slot: block.parent_slot,
282            transactions: block
283                .transactions
284                .into_iter()
285                .map(TransactionWithStatusMeta::Complete)
286                .collect(),
287            rewards: block.rewards,
288            num_partitions: block.num_partitions,
289            block_time: block.block_time,
290            block_height: block.block_height,
291        }
292    }
293}
294
295impl TryFrom<ConfirmedBlock> for VersionedConfirmedBlock {
296    type Error = ConvertBlockError;
297
298    fn try_from(block: ConfirmedBlock) -> Result<Self, Self::Error> {
299        let expected_transaction_count = block.transactions.len();
300
301        let txs: Vec<_> = block
302            .transactions
303            .into_iter()
304            .filter_map(|tx| match tx {
305                TransactionWithStatusMeta::MissingMetadata(_) => None,
306                TransactionWithStatusMeta::Complete(tx) => Some(tx),
307            })
308            .collect();
309
310        if txs.len() != expected_transaction_count {
311            return Err(ConvertBlockError::TransactionsMissing(
312                expected_transaction_count,
313                txs.len(),
314            ));
315        }
316
317        Ok(Self {
318            previous_blockhash: block.previous_blockhash,
319            blockhash: block.blockhash,
320            parent_slot: block.parent_slot,
321            transactions: txs,
322            rewards: block.rewards,
323            num_partitions: block.num_partitions,
324            block_time: block.block_time,
325            block_height: block.block_height,
326        })
327    }
328}
329
330impl ConfirmedBlock {
331    pub fn encode_with_options(
332        self,
333        encoding: UiTransactionEncoding,
334        options: BlockEncodingOptions,
335    ) -> Result<UiConfirmedBlock, EncodeError> {
336        let (transactions, signatures) = match options.transaction_details {
337            TransactionDetails::Full => (
338                Some(
339                    self.transactions
340                        .into_iter()
341                        .map(|tx_with_meta| {
342                            tx_with_meta.encode(
343                                encoding,
344                                options.max_supported_transaction_version,
345                                options.show_rewards,
346                            )
347                        })
348                        .collect::<Result<Vec<_>, _>>()?,
349                ),
350                None,
351            ),
352            TransactionDetails::Signatures => (
353                None,
354                Some(
355                    self.transactions
356                        .into_iter()
357                        .map(|tx_with_meta| tx_with_meta.transaction_signature().to_string())
358                        .collect(),
359                ),
360            ),
361            TransactionDetails::None => (None, None),
362            TransactionDetails::Accounts => (
363                Some(
364                    self.transactions
365                        .into_iter()
366                        .map(|tx_with_meta| {
367                            tx_with_meta.build_json_accounts(
368                                options.max_supported_transaction_version,
369                                options.show_rewards,
370                            )
371                        })
372                        .collect::<Result<Vec<_>, _>>()?,
373                ),
374                None,
375            ),
376        };
377        Ok(UiConfirmedBlock {
378            previous_blockhash: self.previous_blockhash,
379            blockhash: self.blockhash,
380            parent_slot: self.parent_slot,
381            transactions,
382            signatures,
383            rewards: if options.show_rewards {
384                Some(self.rewards)
385            } else {
386                None
387            },
388            num_reward_partitions: self.num_partitions,
389            block_time: self.block_time,
390            block_height: self.block_height,
391        })
392    }
393}
394
395// Confirmed block with type guarantees that transaction metadata is always
396// present, as well as a list of the entry data needed to cryptographically
397// verify the block. Used for uploading to BigTable.
398pub struct VersionedConfirmedBlockWithEntries {
399    pub block: VersionedConfirmedBlock,
400    pub entries: Vec<EntrySummary>,
401}
402
403// Data needed to reconstruct an Entry, given an ordered list of transactions in
404// a block. Used for uploading to BigTable.
405#[derive(Clone, Copy)]
406pub struct EntrySummary {
407    pub num_hashes: u64,
408    pub hash: Hash,
409    pub num_transactions: u64,
410    pub starting_transaction_index: usize,
411}
412
413#[derive(Clone, Debug, PartialEq)]
414#[allow(clippy::large_enum_variant)]
415pub enum TransactionWithStatusMeta {
416    // Very old transactions may be missing metadata
417    MissingMetadata(Transaction),
418    // Versioned stored transaction always have metadata
419    Complete(VersionedTransactionWithStatusMeta),
420}
421
422#[derive(Clone, Debug, PartialEq)]
423pub struct VersionedTransactionWithStatusMeta {
424    pub transaction: VersionedTransaction,
425    pub meta: TransactionStatusMeta,
426}
427
428impl TransactionWithStatusMeta {
429    pub fn get_status_meta(&self) -> Option<TransactionStatusMeta> {
430        match self {
431            Self::MissingMetadata(_) => None,
432            Self::Complete(tx_with_meta) => Some(tx_with_meta.meta.clone()),
433        }
434    }
435
436    pub fn get_transaction(&self) -> VersionedTransaction {
437        match self {
438            Self::MissingMetadata(transaction) => VersionedTransaction::from(transaction.clone()),
439            Self::Complete(tx_with_meta) => tx_with_meta.transaction.clone(),
440        }
441    }
442
443    pub fn transaction_signature(&self) -> &Signature {
444        match self {
445            Self::MissingMetadata(transaction) => &transaction.signatures[0],
446            Self::Complete(VersionedTransactionWithStatusMeta { transaction, .. }) => {
447                &transaction.signatures[0]
448            }
449        }
450    }
451
452    pub fn encode(
453        self,
454        encoding: UiTransactionEncoding,
455        max_supported_transaction_version: Option<u8>,
456        show_rewards: bool,
457    ) -> Result<EncodedTransactionWithStatusMeta, EncodeError> {
458        match self {
459            Self::MissingMetadata(ref transaction) => Ok(EncodedTransactionWithStatusMeta {
460                version: None,
461                transaction: transaction.encode(encoding),
462                meta: None,
463            }),
464            Self::Complete(tx_with_meta) => {
465                tx_with_meta.encode(encoding, max_supported_transaction_version, show_rewards)
466            }
467        }
468    }
469
470    pub fn account_keys(&self) -> AccountKeys<'_> {
471        match self {
472            Self::MissingMetadata(tx) => AccountKeys::new(&tx.message.account_keys, None),
473            Self::Complete(tx_with_meta) => tx_with_meta.account_keys(),
474        }
475    }
476
477    fn build_json_accounts(
478        self,
479        max_supported_transaction_version: Option<u8>,
480        show_rewards: bool,
481    ) -> Result<EncodedTransactionWithStatusMeta, EncodeError> {
482        match self {
483            Self::MissingMetadata(ref transaction) => Ok(EncodedTransactionWithStatusMeta {
484                version: None,
485                transaction: transaction.build_json_accounts(),
486                meta: None,
487            }),
488            Self::Complete(tx_with_meta) => {
489                tx_with_meta.build_json_accounts(max_supported_transaction_version, show_rewards)
490            }
491        }
492    }
493}
494
495impl VersionedTransactionWithStatusMeta {
496    fn validate_version(
497        &self,
498        max_supported_transaction_version: Option<u8>,
499    ) -> Result<Option<TransactionVersion>, EncodeError> {
500        match (
501            max_supported_transaction_version,
502            self.transaction.version(),
503        ) {
504            // Set to none because old clients can't handle this field
505            (None, TransactionVersion::LEGACY) => Ok(None),
506            (None, TransactionVersion::Number(version)) => {
507                Err(EncodeError::UnsupportedTransactionVersion(version))
508            }
509            (Some(_), TransactionVersion::LEGACY) => Ok(Some(TransactionVersion::LEGACY)),
510            (Some(max_version), TransactionVersion::Number(version)) => {
511                if version <= max_version {
512                    Ok(Some(TransactionVersion::Number(version)))
513                } else {
514                    Err(EncodeError::UnsupportedTransactionVersion(version))
515                }
516            }
517        }
518    }
519
520    pub fn encode(
521        self,
522        encoding: UiTransactionEncoding,
523        max_supported_transaction_version: Option<u8>,
524        show_rewards: bool,
525    ) -> Result<EncodedTransactionWithStatusMeta, EncodeError> {
526        let version = self.validate_version(max_supported_transaction_version)?;
527
528        Ok(EncodedTransactionWithStatusMeta {
529            transaction: self.transaction.encode_with_meta(encoding, &self.meta),
530            meta: Some(match encoding {
531                UiTransactionEncoding::JsonParsed => parse_ui_transaction_status_meta(
532                    self.meta,
533                    self.transaction.message.static_account_keys(),
534                    show_rewards,
535                ),
536                _ => {
537                    let mut meta = UiTransactionStatusMeta::from(self.meta);
538                    if !show_rewards {
539                        meta.rewards = OptionSerializer::None;
540                    }
541                    meta
542                }
543            }),
544            version,
545        })
546    }
547
548    pub fn account_keys(&self) -> AccountKeys<'_> {
549        AccountKeys::new(
550            self.transaction.message.static_account_keys(),
551            Some(&self.meta.loaded_addresses),
552        )
553    }
554
555    fn build_json_accounts(
556        self,
557        max_supported_transaction_version: Option<u8>,
558        show_rewards: bool,
559    ) -> Result<EncodedTransactionWithStatusMeta, EncodeError> {
560        let version = self.validate_version(max_supported_transaction_version)?;
561        let reserved_account_keys = ReservedAccountKeys::new_all_activated();
562
563        let account_keys = match &self.transaction.message {
564            VersionedMessage::Legacy(message) => parse_legacy_message_accounts(message),
565            VersionedMessage::V0(message) => {
566                let loaded_message = LoadedMessage::new_borrowed(
567                    message,
568                    &self.meta.loaded_addresses,
569                    &reserved_account_keys.active,
570                );
571                parse_v0_message_accounts(&loaded_message)
572            }
573            VersionedMessage::V1(message) => parse_v1_message_accounts(message),
574        };
575
576        Ok(EncodedTransactionWithStatusMeta {
577            transaction: EncodedTransaction::Accounts(UiAccountsList {
578                signatures: self
579                    .transaction
580                    .signatures
581                    .iter()
582                    .map(ToString::to_string)
583                    .collect(),
584                account_keys,
585            }),
586            meta: Some(build_simple_ui_transaction_status_meta(
587                self.meta,
588                show_rewards,
589            )),
590            version,
591        })
592    }
593}
594
595#[derive(Debug, Clone, PartialEq)]
596pub struct ConfirmedTransactionWithStatusMeta {
597    pub slot: Slot,
598    pub tx_with_meta: TransactionWithStatusMeta,
599    pub block_time: Option<UnixTimestamp>,
600    pub index: u32,
601}
602
603#[derive(Debug, Clone, PartialEq)]
604pub struct VersionedConfirmedTransactionWithStatusMeta {
605    pub slot: Slot,
606    pub tx_with_meta: VersionedTransactionWithStatusMeta,
607    pub block_time: Option<UnixTimestamp>,
608}
609
610impl ConfirmedTransactionWithStatusMeta {
611    pub fn encode(
612        self,
613        encoding: UiTransactionEncoding,
614        max_supported_transaction_version: Option<u8>,
615    ) -> Result<EncodedConfirmedTransactionWithStatusMeta, EncodeError> {
616        Ok(EncodedConfirmedTransactionWithStatusMeta {
617            slot: self.slot,
618            transaction: self.tx_with_meta.encode(
619                encoding,
620                max_supported_transaction_version,
621                true,
622            )?,
623            block_time: self.block_time,
624            transaction_index: Some(self.index),
625        })
626    }
627
628    pub fn get_transaction(&self) -> VersionedTransaction {
629        self.tx_with_meta.get_transaction()
630    }
631}
632
633impl EncodableWithMeta for VersionedTransaction {
634    type Encoded = EncodedTransaction;
635    fn encode_with_meta(
636        &self,
637        encoding: UiTransactionEncoding,
638        meta: &TransactionStatusMeta,
639    ) -> Self::Encoded {
640        match encoding {
641            UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary(
642                bs58::encode(serialize_versioned_transaction(self)).into_string(),
643            ),
644            UiTransactionEncoding::Base58 => EncodedTransaction::Binary(
645                bs58::encode(serialize_versioned_transaction(self)).into_string(),
646                TransactionBinaryEncoding::Base58,
647            ),
648            UiTransactionEncoding::Base64 => EncodedTransaction::Binary(
649                BASE64_STANDARD.encode(serialize_versioned_transaction(self)),
650                TransactionBinaryEncoding::Base64,
651            ),
652            UiTransactionEncoding::Json => self.json_encode(),
653            UiTransactionEncoding::JsonParsed => EncodedTransaction::Json(UiTransaction {
654                signatures: self.signatures.iter().map(ToString::to_string).collect(),
655                message: match &self.message {
656                    VersionedMessage::Legacy(message) => {
657                        message.encode(UiTransactionEncoding::JsonParsed)
658                    }
659                    VersionedMessage::V0(message) => {
660                        message.encode_with_meta(UiTransactionEncoding::JsonParsed, meta)
661                    }
662                    VersionedMessage::V1(message) => {
663                        message.encode(UiTransactionEncoding::JsonParsed)
664                    }
665                },
666            }),
667        }
668    }
669    fn json_encode(&self) -> Self::Encoded {
670        EncodedTransaction::Json(UiTransaction {
671            signatures: self.signatures.iter().map(ToString::to_string).collect(),
672            message: match &self.message {
673                VersionedMessage::Legacy(message) => message.encode(UiTransactionEncoding::Json),
674                VersionedMessage::V0(message) => message.json_encode(),
675                VersionedMessage::V1(message) => message.encode(UiTransactionEncoding::Json),
676            },
677        })
678    }
679}
680
681impl Encodable for VersionedTransaction {
682    type Encoded = EncodedTransaction;
683    fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
684        match encoding {
685            UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary(
686                bs58::encode(serialize_versioned_transaction(self)).into_string(),
687            ),
688            UiTransactionEncoding::Base58 => EncodedTransaction::Binary(
689                bs58::encode(serialize_versioned_transaction(self)).into_string(),
690                TransactionBinaryEncoding::Base58,
691            ),
692            UiTransactionEncoding::Base64 => EncodedTransaction::Binary(
693                BASE64_STANDARD.encode(serialize_versioned_transaction(self)),
694                TransactionBinaryEncoding::Base64,
695            ),
696            UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => {
697                EncodedTransaction::Json(UiTransaction {
698                    signatures: self.signatures.iter().map(ToString::to_string).collect(),
699                    message: match &self.message {
700                        VersionedMessage::Legacy(message) => {
701                            message.encode(UiTransactionEncoding::JsonParsed)
702                        }
703                        VersionedMessage::V0(message) => {
704                            message.encode(UiTransactionEncoding::JsonParsed)
705                        }
706                        VersionedMessage::V1(message) => {
707                            message.encode(UiTransactionEncoding::JsonParsed)
708                        }
709                    },
710                })
711            }
712        }
713    }
714}
715
716impl Encodable for Transaction {
717    type Encoded = EncodedTransaction;
718    fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
719        match encoding {
720            UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary(
721                bs58::encode(bincode::serialize(self).unwrap()).into_string(),
722            ),
723            UiTransactionEncoding::Base58 => EncodedTransaction::Binary(
724                bs58::encode(bincode::serialize(self).unwrap()).into_string(),
725                TransactionBinaryEncoding::Base58,
726            ),
727            UiTransactionEncoding::Base64 => EncodedTransaction::Binary(
728                BASE64_STANDARD.encode(bincode::serialize(self).unwrap()),
729                TransactionBinaryEncoding::Base64,
730            ),
731            UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => {
732                EncodedTransaction::Json(UiTransaction {
733                    signatures: self.signatures.iter().map(ToString::to_string).collect(),
734                    message: self.message.encode(encoding),
735                })
736            }
737        }
738    }
739}
740
741impl JsonAccounts for Transaction {
742    type Encoded = EncodedTransaction;
743    fn build_json_accounts(&self) -> Self::Encoded {
744        EncodedTransaction::Accounts(UiAccountsList {
745            signatures: self.signatures.iter().map(ToString::to_string).collect(),
746            account_keys: parse_legacy_message_accounts(&self.message),
747        })
748    }
749}
750
751impl Encodable for Message {
752    type Encoded = UiMessage;
753    fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
754        if encoding == UiTransactionEncoding::JsonParsed {
755            let account_keys = AccountKeys::new(&self.account_keys, None);
756            UiMessage::Parsed(UiParsedMessage {
757                account_keys: parse_legacy_message_accounts(self),
758                recent_blockhash: self.recent_blockhash.to_string(),
759                instructions: self
760                    .instructions
761                    .iter()
762                    .map(|instruction| {
763                        parse_ui_instruction(
764                            instruction,
765                            &account_keys,
766                            Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32),
767                        )
768                    })
769                    .collect(),
770                address_table_lookups: None,
771                transaction_config: None,
772            })
773        } else {
774            UiMessage::Raw(UiRawMessage {
775                header: self.header,
776                account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
777                recent_blockhash: self.recent_blockhash.to_string(),
778                instructions: self
779                    .instructions
780                    .iter()
781                    .map(|ix| {
782                        UiCompiledInstruction::from(ix, Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32))
783                    })
784                    .collect(),
785                address_table_lookups: None,
786                transaction_config: None,
787            })
788        }
789    }
790}
791
792impl Encodable for v0::Message {
793    type Encoded = UiMessage;
794    fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
795        if encoding == UiTransactionEncoding::JsonParsed {
796            let account_keys = AccountKeys::new(&self.account_keys, None);
797            let loaded_addresses = LoadedAddresses::default();
798            let loaded_message =
799                LoadedMessage::new_borrowed(self, &loaded_addresses, &HashSet::new());
800            UiMessage::Parsed(UiParsedMessage {
801                account_keys: parse_v0_message_accounts(&loaded_message),
802                recent_blockhash: self.recent_blockhash.to_string(),
803                instructions: self
804                    .instructions
805                    .iter()
806                    .map(|instruction| {
807                        parse_ui_instruction(
808                            instruction,
809                            &account_keys,
810                            Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32),
811                        )
812                    })
813                    .collect(),
814                address_table_lookups: None,
815                transaction_config: None,
816            })
817        } else {
818            UiMessage::Raw(UiRawMessage {
819                header: self.header,
820                account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
821                recent_blockhash: self.recent_blockhash.to_string(),
822                instructions: self
823                    .instructions
824                    .iter()
825                    .map(|ix| {
826                        UiCompiledInstruction::from(ix, Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32))
827                    })
828                    .collect(),
829                address_table_lookups: None,
830                transaction_config: None,
831            })
832        }
833    }
834}
835
836impl EncodableWithMeta for v0::Message {
837    type Encoded = UiMessage;
838    fn encode_with_meta(
839        &self,
840        encoding: UiTransactionEncoding,
841        meta: &TransactionStatusMeta,
842    ) -> Self::Encoded {
843        if encoding == UiTransactionEncoding::JsonParsed {
844            let reserved_account_keys = ReservedAccountKeys::new_all_activated();
845            let account_keys = AccountKeys::new(&self.account_keys, Some(&meta.loaded_addresses));
846            let loaded_message = LoadedMessage::new_borrowed(
847                self,
848                &meta.loaded_addresses,
849                &reserved_account_keys.active,
850            );
851            UiMessage::Parsed(UiParsedMessage {
852                account_keys: parse_v0_message_accounts(&loaded_message),
853                recent_blockhash: self.recent_blockhash.to_string(),
854                instructions: self
855                    .instructions
856                    .iter()
857                    .map(|instruction| {
858                        parse_ui_instruction(
859                            instruction,
860                            &account_keys,
861                            Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32),
862                        )
863                    })
864                    .collect(),
865                address_table_lookups: Some(
866                    self.address_table_lookups.iter().map(Into::into).collect(),
867                ),
868                transaction_config: None,
869            })
870        } else {
871            self.json_encode()
872        }
873    }
874    fn json_encode(&self) -> Self::Encoded {
875        UiMessage::Raw(UiRawMessage {
876            header: self.header,
877            account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
878            recent_blockhash: self.recent_blockhash.to_string(),
879            instructions: self
880                .instructions
881                .iter()
882                .map(|ix| {
883                    UiCompiledInstruction::from(ix, Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32))
884                })
885                .collect(),
886            address_table_lookups: Some(
887                self.address_table_lookups.iter().map(Into::into).collect(),
888            ),
889            transaction_config: None,
890        })
891    }
892}
893
894impl Encodable for solana_message::v1::Message {
895    type Encoded = UiMessage;
896    fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded {
897        if encoding == UiTransactionEncoding::JsonParsed {
898            let account_keys = AccountKeys::new(&self.account_keys, None);
899            UiMessage::Parsed(UiParsedMessage {
900                account_keys: parse_v1_message_accounts(self),
901                recent_blockhash: self.lifetime_specifier.to_string(),
902                instructions: self
903                    .instructions
904                    .iter()
905                    .map(|instruction| {
906                        parse_ui_instruction(
907                            instruction,
908                            &account_keys,
909                            Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32),
910                        )
911                    })
912                    .collect(),
913                address_table_lookups: None,
914                transaction_config: Some(UiTransactionConfig::from(&self.config)),
915            })
916        } else {
917            UiMessage::Raw(UiRawMessage {
918                header: self.header,
919                account_keys: self.account_keys.iter().map(ToString::to_string).collect(),
920                recent_blockhash: self.lifetime_specifier.to_string(),
921                instructions: self
922                    .instructions
923                    .iter()
924                    .map(|ix| {
925                        UiCompiledInstruction::from(ix, Some(TRANSACTION_LEVEL_STACK_HEIGHT as u32))
926                    })
927                    .collect(),
928                address_table_lookups: None,
929                transaction_config: Some(UiTransactionConfig::from(&self.config)),
930            })
931        }
932    }
933}
934
935// A serialized `Vec<TransactionByAddrInfo>` is stored in the `tx-by-addr` table.  The row keys are
936// the one's compliment of the slot so that rows may be listed in reverse order
937#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
938pub struct TransactionByAddrInfo {
939    pub signature: Signature,          // The transaction signature
940    pub err: Option<TransactionError>, // None if the transaction executed successfully
941    pub index: u32,                    // Where the transaction is located in the block
942    pub memo: Option<String>,          // Transaction memo
943    pub block_time: Option<UnixTimestamp>,
944}
945
946#[cfg(test)]
947mod test {
948    use super::*;
949
950    #[test]
951    fn test_ui_transaction_status_meta_ctors_serialization() {
952        let meta = TransactionStatusMeta {
953            status: Ok(()),
954            fee: 1234,
955            pre_balances: vec![1, 2, 3],
956            post_balances: vec![4, 5, 6],
957            inner_instructions: None,
958            log_messages: None,
959            pre_token_balances: None,
960            post_token_balances: None,
961            rewards: None,
962            loaded_addresses: LoadedAddresses {
963                writable: vec![],
964                readonly: vec![],
965            },
966            return_data: None,
967            compute_units_consumed: None,
968            cost_units: None,
969        };
970        #[rustfmt::skip]
971        let expected_json_output_value: serde_json::Value = serde_json::from_str(
972            "{\
973             \"err\":null,\
974             \"status\":{\"Ok\":null},\
975             \"fee\":1234,\
976             \"preBalances\":[1,2,3],\
977             \"postBalances\":[4,5,6],\
978             \"innerInstructions\":null,\
979             \"logMessages\":null,\
980             \"preTokenBalances\":null,\
981             \"postTokenBalances\":null,\
982             \"rewards\":null,\
983             \"loadedAddresses\":{\
984                 \"readonly\": [],\
985                 \"writable\": []\
986             }\
987             }",
988        )
989        .unwrap();
990        let ui_meta_from: UiTransactionStatusMeta = meta.clone().into();
991        assert_eq!(
992            serde_json::to_value(ui_meta_from).unwrap(),
993            expected_json_output_value
994        );
995
996        #[rustfmt::skip]
997        let expected_json_output_value: serde_json::Value = serde_json::from_str(
998            "{\
999             \"err\":null,\
1000             \"status\":{\"Ok\":null},\
1001             \"fee\":1234,\
1002             \"preBalances\":[1,2,3],\
1003             \"postBalances\":[4,5,6],\
1004             \"innerInstructions\":null,\
1005             \"logMessages\":null,\
1006             \"preTokenBalances\":null,\
1007             \"postTokenBalances\":null,\
1008             \"rewards\":null\
1009             }",
1010        )
1011        .unwrap();
1012        let ui_meta_parse_with_rewards = parse_ui_transaction_status_meta(meta.clone(), &[], true);
1013        assert_eq!(
1014            serde_json::to_value(ui_meta_parse_with_rewards).unwrap(),
1015            expected_json_output_value
1016        );
1017
1018        let ui_meta_parse_no_rewards = parse_ui_transaction_status_meta(meta, &[], false);
1019        assert_eq!(
1020            serde_json::to_value(ui_meta_parse_no_rewards).unwrap(),
1021            expected_json_output_value
1022        );
1023    }
1024
1025    #[test]
1026    fn test_confirmed_transaction_with_status_meta_encode() {
1027        use solana_transaction::versioned::VersionedTransaction;
1028
1029        let pubkey1 = solana_pubkey::new_rand();
1030        let pubkey2 = solana_pubkey::new_rand();
1031
1032        let message = solana_message::v0::Message {
1033            header: solana_message::MessageHeader {
1034                num_required_signatures: 1,
1035                num_readonly_signed_accounts: 0,
1036                num_readonly_unsigned_accounts: 1,
1037            },
1038            account_keys: vec![pubkey1, pubkey2],
1039            recent_blockhash: solana_hash::Hash::default(),
1040            instructions: vec![],
1041            address_table_lookups: vec![],
1042        };
1043
1044        let tx = VersionedTransaction {
1045            signatures: vec![solana_signature::Signature::from([1u8; 64])],
1046            message: solana_message::VersionedMessage::V0(message),
1047        };
1048
1049        let meta = TransactionStatusMeta {
1050            status: Ok(()),
1051            fee: 5000,
1052            pre_balances: vec![1_000_000, 0],
1053            post_balances: vec![994_900, 100],
1054            inner_instructions: None,
1055            log_messages: None,
1056            pre_token_balances: None,
1057            post_token_balances: None,
1058            rewards: None,
1059            loaded_addresses: LoadedAddresses::default(),
1060            return_data: None,
1061            compute_units_consumed: None,
1062            cost_units: None,
1063        };
1064
1065        let confirmed_tx = ConfirmedTransactionWithStatusMeta {
1066            slot: 42,
1067            tx_with_meta: TransactionWithStatusMeta::Complete(VersionedTransactionWithStatusMeta {
1068                transaction: tx,
1069                meta,
1070            }),
1071            block_time: Some(1234567890),
1072            index: 7,
1073        };
1074
1075        let encoded = confirmed_tx
1076            .encode(UiTransactionEncoding::Base64, Some(0))
1077            .unwrap();
1078
1079        assert_eq!(encoded.slot, 42);
1080        assert_eq!(encoded.block_time, Some(1234567890));
1081        assert_eq!(encoded.transaction_index, Some(7));
1082    }
1083
1084    #[test]
1085    fn test_v1_binary_encoding_uses_wire_format() {
1086        let message = solana_message::v1::Message::new(
1087            solana_message::MessageHeader {
1088                num_required_signatures: 1,
1089                num_readonly_signed_accounts: 0,
1090                num_readonly_unsigned_accounts: 1,
1091            },
1092            solana_message::v1::TransactionConfig::empty(),
1093            solana_hash::Hash::new_from_array([7; solana_hash::HASH_BYTES]),
1094            vec![
1095                solana_pubkey::Pubkey::new_unique(),
1096                solana_pubkey::Pubkey::new_unique(),
1097            ],
1098            vec![],
1099        );
1100        let transaction = VersionedTransaction {
1101            signatures: vec![solana_signature::Signature::default()],
1102            message: solana_message::VersionedMessage::V1(message),
1103        };
1104
1105        let EncodedTransaction::Binary(encoded, TransactionBinaryEncoding::Base64) =
1106            transaction.encode(UiTransactionEncoding::Base64)
1107        else {
1108            panic!("expected binary base64 output");
1109        };
1110        let bytes = BASE64_STANDARD.decode(&encoded).unwrap();
1111
1112        assert_eq!(bytes[0], solana_message::v1::V1_PREFIX);
1113        assert_eq!(bytes, wincode::serialize(&transaction).unwrap());
1114        assert_eq!(
1115            EncodedTransaction::Binary(encoded, TransactionBinaryEncoding::Base64).decode(),
1116            Some(transaction)
1117        );
1118    }
1119}