solana_cli_output/
display.rs

1use {
2    crate::cli_output::CliSignatureVerificationStatus,
3    agave_reserved_account_keys::ReservedAccountKeys,
4    base64::{prelude::BASE64_STANDARD, Engine},
5    chrono::{DateTime, Local, SecondsFormat, TimeZone, Utc},
6    console::style,
7    indicatif::{ProgressBar, ProgressStyle},
8    solana_bincode::limited_deserialize,
9    solana_cli_config::SettingType,
10    solana_clock::UnixTimestamp,
11    solana_hash::Hash,
12    solana_message::{compiled_instruction::CompiledInstruction, v0::MessageAddressTableLookup},
13    solana_pubkey::Pubkey,
14    solana_signature::Signature,
15    solana_stake_interface as stake,
16    solana_transaction::versioned::{TransactionVersion, VersionedTransaction},
17    solana_transaction_status::{
18        Rewards, UiReturnDataEncoding, UiTransactionReturnData, UiTransactionStatusMeta,
19    },
20    solana_transaction_status_client_types::UiTransactionError,
21    spl_memo_interface::{v1::id as spl_memo_v1_id, v3::id as spl_memo_v3_id},
22    std::{collections::HashMap, fmt, io, time::Duration},
23};
24
25#[derive(Clone, Debug)]
26pub struct BuildBalanceMessageConfig {
27    pub use_lamports_unit: bool,
28    pub show_unit: bool,
29    pub trim_trailing_zeros: bool,
30}
31
32impl Default for BuildBalanceMessageConfig {
33    fn default() -> Self {
34        Self {
35            use_lamports_unit: false,
36            show_unit: true,
37            trim_trailing_zeros: true,
38        }
39    }
40}
41
42fn is_memo_program(k: &Pubkey) -> bool {
43    let k_str = k.to_string();
44    (k_str == spl_memo_v1_id().to_string()) || (k_str == spl_memo_v3_id().to_string())
45}
46
47pub fn build_balance_message_with_config(
48    lamports: u64,
49    config: &BuildBalanceMessageConfig,
50) -> String {
51    let value = if config.use_lamports_unit {
52        lamports.to_string()
53    } else {
54        const LAMPORTS_PER_SOL_F64: f64 = 1_000_000_000.;
55        let sol = lamports as f64 / LAMPORTS_PER_SOL_F64;
56        let sol_str = format!("{sol:.9}");
57        if config.trim_trailing_zeros {
58            sol_str
59                .trim_end_matches('0')
60                .trim_end_matches('.')
61                .to_string()
62        } else {
63            sol_str
64        }
65    };
66    let unit = if config.show_unit {
67        if config.use_lamports_unit {
68            let ess = if lamports == 1 { "" } else { "s" };
69            format!(" lamport{ess}")
70        } else {
71            " SOL".to_string()
72        }
73    } else {
74        "".to_string()
75    };
76    format!("{value}{unit}")
77}
78
79pub fn build_balance_message(lamports: u64, use_lamports_unit: bool, show_unit: bool) -> String {
80    build_balance_message_with_config(
81        lamports,
82        &BuildBalanceMessageConfig {
83            use_lamports_unit,
84            show_unit,
85            ..BuildBalanceMessageConfig::default()
86        },
87    )
88}
89
90// Pretty print a "name value"
91pub fn println_name_value(name: &str, value: &str) {
92    let styled_value = if value.is_empty() {
93        style("(not set)").italic()
94    } else {
95        style(value)
96    };
97    println!("{} {}", style(name).bold(), styled_value);
98}
99
100pub fn writeln_name_value(f: &mut dyn fmt::Write, name: &str, value: &str) -> fmt::Result {
101    let styled_value = if value.is_empty() {
102        style("(not set)").italic()
103    } else {
104        style(value)
105    };
106    writeln!(f, "{} {}", style(name).bold(), styled_value)
107}
108
109pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) {
110    let description = match setting_type {
111        SettingType::Explicit => "",
112        SettingType::Computed => "(computed)",
113        SettingType::SystemDefault => "(default)",
114    };
115
116    println!(
117        "{} {} {}",
118        style(name).bold(),
119        style(value),
120        style(description).italic(),
121    );
122}
123
124pub fn format_labeled_address(pubkey: &str, address_labels: &HashMap<String, String>) -> String {
125    let label = address_labels.get(pubkey);
126    match label {
127        Some(label) => format!(
128            "{:.31} ({:.4}..{})",
129            label,
130            pubkey,
131            pubkey.split_at(pubkey.len() - 4).1
132        ),
133        None => pubkey.to_string(),
134    }
135}
136
137pub fn println_signers(
138    blockhash: &Hash,
139    signers: &[String],
140    absent: &[String],
141    bad_sig: &[String],
142) {
143    println!();
144    println!("Blockhash: {blockhash}");
145    if !signers.is_empty() {
146        println!("Signers (Pubkey=Signature):");
147        signers.iter().for_each(|signer| println!("  {signer}"))
148    }
149    if !absent.is_empty() {
150        println!("Absent Signers (Pubkey):");
151        absent.iter().for_each(|pubkey| println!("  {pubkey}"))
152    }
153    if !bad_sig.is_empty() {
154        println!("Bad Signatures (Pubkey):");
155        bad_sig.iter().for_each(|pubkey| println!("  {pubkey}"))
156    }
157    println!();
158}
159
160struct CliAccountMeta {
161    is_signer: bool,
162    is_writable: bool,
163    is_invoked: bool,
164}
165
166fn format_account_mode(meta: CliAccountMeta) -> String {
167    format!(
168        "{}r{}{}", // accounts are always readable...
169        if meta.is_signer {
170            "s" // stands for signer
171        } else {
172            "-"
173        },
174        if meta.is_writable {
175            "w" // comment for consistent rust fmt (no joking; lol)
176        } else {
177            "-"
178        },
179        // account may be executable on-chain while not being
180        // designated as a program-id in the message
181        if meta.is_invoked {
182            "x"
183        } else {
184            // programs to be executed via CPI cannot be identified as
185            // executable from the message
186            "-"
187        },
188    )
189}
190
191fn write_transaction<W: io::Write>(
192    w: &mut W,
193    transaction: &VersionedTransaction,
194    transaction_status: Option<&UiTransactionStatusMeta>,
195    prefix: &str,
196    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
197    block_time: Option<UnixTimestamp>,
198    timezone: CliTimezone,
199) -> io::Result<()> {
200    write_block_time(w, block_time, timezone, prefix)?;
201
202    let message = &transaction.message;
203    let account_keys: Vec<AccountKeyType> = {
204        let static_keys_iter = message
205            .static_account_keys()
206            .iter()
207            .map(AccountKeyType::Known);
208        let dynamic_keys: Vec<AccountKeyType> = message
209            .address_table_lookups()
210            .map(transform_lookups_to_unknown_keys)
211            .unwrap_or_default();
212        static_keys_iter.chain(dynamic_keys).collect()
213    };
214
215    write_version(w, transaction.version(), prefix)?;
216    write_recent_blockhash(w, message.recent_blockhash(), prefix)?;
217    write_signatures(w, &transaction.signatures, sigverify_status, prefix)?;
218
219    let reserved_account_keys = ReservedAccountKeys::new_all_activated().active;
220    for (account_index, account) in account_keys.iter().enumerate() {
221        let account_meta = CliAccountMeta {
222            is_signer: message.is_signer(account_index),
223            is_writable: message.is_maybe_writable(account_index, Some(&reserved_account_keys)),
224            is_invoked: message.is_invoked(account_index),
225        };
226
227        let is_fee_payer = account_index == 0;
228        write_account(
229            w,
230            account_index,
231            *account,
232            format_account_mode(account_meta),
233            is_fee_payer,
234            prefix,
235        )?;
236    }
237
238    for (instruction_index, instruction) in message.instructions().iter().enumerate() {
239        let program_pubkey = account_keys[instruction.program_id_index as usize];
240        let instruction_accounts = instruction
241            .accounts
242            .iter()
243            .map(|account_index| (account_keys[*account_index as usize], *account_index));
244
245        write_instruction(
246            w,
247            instruction_index,
248            program_pubkey,
249            instruction,
250            instruction_accounts,
251            prefix,
252        )?;
253    }
254
255    if let Some(address_table_lookups) = message.address_table_lookups() {
256        write_address_table_lookups(w, address_table_lookups, prefix)?;
257    }
258
259    if let Some(transaction_status) = transaction_status {
260        write_status(w, &transaction_status.status, prefix)?;
261        write_fees(w, transaction_status.fee, prefix)?;
262        write_balances(w, transaction_status, prefix)?;
263        write_compute_units_consumed(
264            w,
265            transaction_status.compute_units_consumed.clone().into(),
266            prefix,
267        )?;
268        write_log_messages(w, transaction_status.log_messages.as_ref().into(), prefix)?;
269        write_return_data(w, transaction_status.return_data.as_ref().into(), prefix)?;
270        write_rewards(w, transaction_status.rewards.as_ref().into(), prefix)?;
271    } else {
272        writeln!(w, "{prefix}Status: Unavailable")?;
273    }
274
275    Ok(())
276}
277
278fn transform_lookups_to_unknown_keys(
279    lookups: &[MessageAddressTableLookup],
280) -> Vec<AccountKeyType<'_>> {
281    let unknown_writable_keys = lookups
282        .iter()
283        .enumerate()
284        .flat_map(|(lookup_index, lookup)| {
285            lookup
286                .writable_indexes
287                .iter()
288                .map(move |table_index| AccountKeyType::Unknown {
289                    lookup_index,
290                    table_index: *table_index,
291                })
292        });
293
294    let unknown_readonly_keys = lookups
295        .iter()
296        .enumerate()
297        .flat_map(|(lookup_index, lookup)| {
298            lookup
299                .readonly_indexes
300                .iter()
301                .map(move |table_index| AccountKeyType::Unknown {
302                    lookup_index,
303                    table_index: *table_index,
304                })
305        });
306
307    unknown_writable_keys.chain(unknown_readonly_keys).collect()
308}
309
310enum CliTimezone {
311    Local,
312    #[allow(dead_code)]
313    Utc,
314}
315
316fn write_block_time<W: io::Write>(
317    w: &mut W,
318    block_time: Option<UnixTimestamp>,
319    timezone: CliTimezone,
320    prefix: &str,
321) -> io::Result<()> {
322    if let Some(block_time) = block_time {
323        let block_time_output = match timezone {
324            CliTimezone::Local => format!("{:?}", Local.timestamp_opt(block_time, 0).unwrap()),
325            CliTimezone::Utc => format!("{:?}", Utc.timestamp_opt(block_time, 0).unwrap()),
326        };
327        writeln!(w, "{prefix}Block Time: {block_time_output}",)?;
328    }
329    Ok(())
330}
331
332fn write_version<W: io::Write>(
333    w: &mut W,
334    version: TransactionVersion,
335    prefix: &str,
336) -> io::Result<()> {
337    let version = match version {
338        TransactionVersion::Legacy(_) => "legacy".to_string(),
339        TransactionVersion::Number(number) => number.to_string(),
340    };
341    writeln!(w, "{prefix}Version: {version}")
342}
343
344fn write_recent_blockhash<W: io::Write>(
345    w: &mut W,
346    recent_blockhash: &Hash,
347    prefix: &str,
348) -> io::Result<()> {
349    writeln!(w, "{prefix}Recent Blockhash: {recent_blockhash:?}")
350}
351
352fn write_signatures<W: io::Write>(
353    w: &mut W,
354    signatures: &[Signature],
355    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
356    prefix: &str,
357) -> io::Result<()> {
358    let sigverify_statuses = if let Some(sigverify_status) = sigverify_status {
359        sigverify_status.iter().map(|s| format!(" ({s})")).collect()
360    } else {
361        vec!["".to_string(); signatures.len()]
362    };
363    for (signature_index, (signature, sigverify_status)) in
364        signatures.iter().zip(&sigverify_statuses).enumerate()
365    {
366        writeln!(
367            w,
368            "{prefix}Signature {signature_index}: {signature:?}{sigverify_status}",
369        )?;
370    }
371    Ok(())
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375enum AccountKeyType<'a> {
376    Known(&'a Pubkey),
377    Unknown {
378        lookup_index: usize,
379        table_index: u8,
380    },
381}
382
383impl fmt::Display for AccountKeyType<'_> {
384    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
385        match self {
386            Self::Known(address) => write!(f, "{address}"),
387            Self::Unknown {
388                lookup_index,
389                table_index,
390            } => {
391                write!(
392                    f,
393                    "Unknown Address (uses lookup {lookup_index} and index {table_index})"
394                )
395            }
396        }
397    }
398}
399
400fn write_account<W: io::Write>(
401    w: &mut W,
402    account_index: usize,
403    account_address: AccountKeyType,
404    account_mode: String,
405    is_fee_payer: bool,
406    prefix: &str,
407) -> io::Result<()> {
408    writeln!(
409        w,
410        "{}Account {}: {} {}{}",
411        prefix,
412        account_index,
413        account_mode,
414        account_address,
415        if is_fee_payer { " (fee payer)" } else { "" },
416    )
417}
418
419fn write_instruction<'a, W: io::Write>(
420    w: &mut W,
421    instruction_index: usize,
422    program_pubkey: AccountKeyType,
423    instruction: &CompiledInstruction,
424    instruction_accounts: impl Iterator<Item = (AccountKeyType<'a>, u8)>,
425    prefix: &str,
426) -> io::Result<()> {
427    writeln!(w, "{prefix}Instruction {instruction_index}")?;
428    writeln!(
429        w,
430        "{}  Program:   {} ({})",
431        prefix, program_pubkey, instruction.program_id_index
432    )?;
433    for (index, (account_address, account_index)) in instruction_accounts.enumerate() {
434        writeln!(
435            w,
436            "{prefix}  Account {index}: {account_address} ({account_index})"
437        )?;
438    }
439
440    let mut raw = true;
441    if let AccountKeyType::Known(program_pubkey) = program_pubkey {
442        if program_pubkey == &solana_vote_program::id() {
443            if let Ok(vote_instruction) =
444                limited_deserialize::<solana_vote_program::vote_instruction::VoteInstruction>(
445                    &instruction.data,
446                    solana_packet::PACKET_DATA_SIZE as u64,
447                )
448            {
449                writeln!(w, "{prefix}  {vote_instruction:?}")?;
450                raw = false;
451            }
452        } else if program_pubkey == &stake::program::id() {
453            if let Ok(stake_instruction) = limited_deserialize::<stake::instruction::StakeInstruction>(
454                &instruction.data,
455                solana_packet::PACKET_DATA_SIZE as u64,
456            ) {
457                writeln!(w, "{prefix}  {stake_instruction:?}")?;
458                raw = false;
459            }
460        } else if program_pubkey == &solana_sdk_ids::system_program::id() {
461            if let Ok(system_instruction) =
462                limited_deserialize::<solana_system_interface::instruction::SystemInstruction>(
463                    &instruction.data,
464                    solana_packet::PACKET_DATA_SIZE as u64,
465                )
466            {
467                writeln!(w, "{prefix}  {system_instruction:?}")?;
468                raw = false;
469            }
470        } else if is_memo_program(program_pubkey) {
471            if let Ok(s) = std::str::from_utf8(&instruction.data) {
472                writeln!(w, "{prefix}  Data: \"{s}\"")?;
473                raw = false;
474            }
475        }
476    }
477
478    if raw {
479        writeln!(w, "{}  Data: {:?}", prefix, instruction.data)?;
480    }
481
482    Ok(())
483}
484
485fn write_address_table_lookups<W: io::Write>(
486    w: &mut W,
487    address_table_lookups: &[MessageAddressTableLookup],
488    prefix: &str,
489) -> io::Result<()> {
490    for (lookup_index, lookup) in address_table_lookups.iter().enumerate() {
491        writeln!(w, "{prefix}Address Table Lookup {lookup_index}",)?;
492        writeln!(w, "{}  Table Account: {}", prefix, lookup.account_key,)?;
493        writeln!(
494            w,
495            "{}  Writable Indexes: {:?}",
496            prefix,
497            &lookup.writable_indexes[..],
498        )?;
499        writeln!(
500            w,
501            "{}  Readonly Indexes: {:?}",
502            prefix,
503            &lookup.readonly_indexes[..],
504        )?;
505    }
506    Ok(())
507}
508
509fn write_rewards<W: io::Write>(
510    w: &mut W,
511    rewards: Option<&Rewards>,
512    prefix: &str,
513) -> io::Result<()> {
514    if let Some(rewards) = rewards {
515        if !rewards.is_empty() {
516            writeln!(w, "{prefix}Rewards:",)?;
517            writeln!(
518                w,
519                "{}  {:<44}  {:^15}  {:<16}  {:<20}",
520                prefix, "Address", "Type", "Amount", "New Balance"
521            )?;
522            for reward in rewards {
523                let sign = if reward.lamports < 0 { "-" } else { "" };
524                writeln!(
525                    w,
526                    "{}  {:<44}  {:^15}  {}◎{:<14.9}  ◎{:<18.9}",
527                    prefix,
528                    reward.pubkey,
529                    if let Some(reward_type) = reward.reward_type {
530                        format!("{reward_type}")
531                    } else {
532                        "-".to_string()
533                    },
534                    sign,
535                    build_balance_message(reward.lamports.unsigned_abs(), false, false),
536                    build_balance_message(reward.post_balance, false, false)
537                )?;
538            }
539        }
540    }
541    Ok(())
542}
543
544fn write_status<W: io::Write>(
545    w: &mut W,
546    transaction_status: &Result<(), UiTransactionError>,
547    prefix: &str,
548) -> io::Result<()> {
549    writeln!(
550        w,
551        "{}Status: {}",
552        prefix,
553        match transaction_status {
554            Ok(_) => "Ok".into(),
555            Err(err) => err.to_string(),
556        }
557    )
558}
559
560fn write_fees<W: io::Write>(w: &mut W, transaction_fee: u64, prefix: &str) -> io::Result<()> {
561    writeln!(
562        w,
563        "{}  Fee: ◎{}",
564        prefix,
565        build_balance_message(transaction_fee, false, false)
566    )
567}
568
569fn write_balances<W: io::Write>(
570    w: &mut W,
571    transaction_status: &UiTransactionStatusMeta,
572    prefix: &str,
573) -> io::Result<()> {
574    assert_eq!(
575        transaction_status.pre_balances.len(),
576        transaction_status.post_balances.len()
577    );
578    for (i, (pre, post)) in transaction_status
579        .pre_balances
580        .iter()
581        .zip(transaction_status.post_balances.iter())
582        .enumerate()
583    {
584        if pre == post {
585            writeln!(
586                w,
587                "{}  Account {} balance: ◎{}",
588                prefix,
589                i,
590                build_balance_message(*pre, false, false)
591            )?;
592        } else {
593            writeln!(
594                w,
595                "{}  Account {} balance: ◎{} -> ◎{}",
596                prefix,
597                i,
598                build_balance_message(*pre, false, false),
599                build_balance_message(*post, false, false)
600            )?;
601        }
602    }
603    Ok(())
604}
605
606fn write_return_data<W: io::Write>(
607    w: &mut W,
608    return_data: Option<&UiTransactionReturnData>,
609    prefix: &str,
610) -> io::Result<()> {
611    if let Some(return_data) = return_data {
612        let (data, encoding) = &return_data.data;
613        let raw_return_data = match encoding {
614            UiReturnDataEncoding::Base64 => BASE64_STANDARD.decode(data).map_err(|err| {
615                io::Error::other(format!("could not parse data as {encoding:?}: {err:?}"))
616            })?,
617        };
618        if !raw_return_data.is_empty() {
619            use pretty_hex::*;
620            writeln!(
621                w,
622                "{}Return Data from Program {}:",
623                prefix, return_data.program_id
624            )?;
625            writeln!(w, "{}  {:?}", prefix, raw_return_data.hex_dump())?;
626        }
627    }
628    Ok(())
629}
630
631fn write_compute_units_consumed<W: io::Write>(
632    w: &mut W,
633    compute_units_consumed: Option<u64>,
634    prefix: &str,
635) -> io::Result<()> {
636    if let Some(cus) = compute_units_consumed {
637        writeln!(w, "{prefix}Compute Units Consumed: {cus}")?;
638    }
639    Ok(())
640}
641
642fn write_log_messages<W: io::Write>(
643    w: &mut W,
644    log_messages: Option<&Vec<String>>,
645    prefix: &str,
646) -> io::Result<()> {
647    if let Some(log_messages) = log_messages {
648        if !log_messages.is_empty() {
649            writeln!(w, "{prefix}Log Messages:",)?;
650            for log_message in log_messages {
651                writeln!(w, "{prefix}  {log_message}")?;
652            }
653        }
654    }
655    Ok(())
656}
657
658pub fn println_transaction(
659    transaction: &VersionedTransaction,
660    transaction_status: Option<&UiTransactionStatusMeta>,
661    prefix: &str,
662    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
663    block_time: Option<UnixTimestamp>,
664) {
665    let mut w = Vec::new();
666    if write_transaction(
667        &mut w,
668        transaction,
669        transaction_status,
670        prefix,
671        sigverify_status,
672        block_time,
673        CliTimezone::Local,
674    )
675    .is_ok()
676    {
677        if let Ok(s) = String::from_utf8(w) {
678            print!("{s}");
679        }
680    }
681}
682
683pub fn writeln_transaction(
684    f: &mut dyn fmt::Write,
685    transaction: &VersionedTransaction,
686    transaction_status: Option<&UiTransactionStatusMeta>,
687    prefix: &str,
688    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
689    block_time: Option<UnixTimestamp>,
690) -> fmt::Result {
691    let mut w = Vec::new();
692    let write_result = write_transaction(
693        &mut w,
694        transaction,
695        transaction_status,
696        prefix,
697        sigverify_status,
698        block_time,
699        CliTimezone::Local,
700    );
701
702    if write_result.is_ok() {
703        if let Ok(s) = String::from_utf8(w) {
704            write!(f, "{s}")?;
705        }
706    }
707    Ok(())
708}
709
710/// Creates a new process bar for processing that will take an unknown amount of time
711pub fn new_spinner_progress_bar() -> ProgressBar {
712    let progress_bar = ProgressBar::new(42);
713    progress_bar.set_style(
714        ProgressStyle::default_spinner()
715            .template("{spinner:.green} {wide_msg}")
716            .expect("ProgressStyle::template direct input to be correct"),
717    );
718    progress_bar.enable_steady_tick(Duration::from_millis(100));
719    progress_bar
720}
721
722pub fn unix_timestamp_to_string(unix_timestamp: UnixTimestamp) -> String {
723    match DateTime::from_timestamp(unix_timestamp, 0) {
724        Some(ndt) => ndt.to_rfc3339_opts(SecondsFormat::Secs, true),
725        None => format!("UnixTimestamp {unix_timestamp}"),
726    }
727}
728
729#[cfg(test)]
730mod test {
731    use {
732        super::*,
733        solana_keypair::Keypair,
734        solana_message::{
735            v0::{self, LoadedAddresses},
736            Message as LegacyMessage, MessageHeader, VersionedMessage,
737        },
738        solana_pubkey::Pubkey,
739        solana_signer::Signer,
740        solana_transaction::Transaction,
741        solana_transaction_context::TransactionReturnData,
742        solana_transaction_status::{Reward, RewardType, TransactionStatusMeta},
743        std::io::BufWriter,
744    };
745
746    fn new_test_keypair() -> Keypair {
747        let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap();
748        let public = ed25519_dalek::PublicKey::from(&secret);
749        let keypair = ed25519_dalek::Keypair { secret, public };
750        Keypair::try_from(keypair.to_bytes().as_ref()).unwrap()
751    }
752
753    fn new_test_v0_transaction() -> VersionedTransaction {
754        let keypair = new_test_keypair();
755        let account_key = Pubkey::new_from_array([1u8; 32]);
756        let address_table_key = Pubkey::new_from_array([2u8; 32]);
757        VersionedTransaction::try_new(
758            VersionedMessage::V0(v0::Message {
759                header: MessageHeader {
760                    num_required_signatures: 1,
761                    num_readonly_signed_accounts: 0,
762                    num_readonly_unsigned_accounts: 1,
763                },
764                recent_blockhash: Hash::default(),
765                account_keys: vec![keypair.pubkey(), account_key],
766                address_table_lookups: vec![MessageAddressTableLookup {
767                    account_key: address_table_key,
768                    writable_indexes: vec![0],
769                    readonly_indexes: vec![1],
770                }],
771                instructions: vec![CompiledInstruction::new_from_raw_parts(
772                    3,
773                    vec![],
774                    vec![1, 2],
775                )],
776            }),
777            &[&keypair],
778        )
779        .unwrap()
780    }
781
782    #[test]
783    fn test_write_legacy_transaction() {
784        let keypair = new_test_keypair();
785        let account_key = Pubkey::new_from_array([1u8; 32]);
786        let transaction = VersionedTransaction::from(Transaction::new(
787            &[&keypair],
788            LegacyMessage {
789                header: MessageHeader {
790                    num_required_signatures: 1,
791                    num_readonly_signed_accounts: 0,
792                    num_readonly_unsigned_accounts: 1,
793                },
794                recent_blockhash: Hash::default(),
795                account_keys: vec![keypair.pubkey(), account_key],
796                instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])],
797            },
798            Hash::default(),
799        ));
800
801        let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction);
802        let meta = TransactionStatusMeta {
803            status: Ok(()),
804            fee: 5000,
805            pre_balances: vec![5000, 10_000],
806            post_balances: vec![0, 9_900],
807            inner_instructions: None,
808            log_messages: Some(vec!["Test message".to_string()]),
809            pre_token_balances: None,
810            post_token_balances: None,
811            rewards: Some(vec![Reward {
812                pubkey: account_key.to_string(),
813                lamports: -100,
814                post_balance: 9_900,
815                reward_type: Some(RewardType::Rent),
816                commission: None,
817            }]),
818            loaded_addresses: LoadedAddresses::default(),
819            return_data: Some(TransactionReturnData {
820                program_id: Pubkey::new_from_array([2u8; 32]),
821                data: vec![1, 2, 3],
822            }),
823            compute_units_consumed: Some(1234u64),
824            cost_units: Some(5678),
825        };
826
827        let output = {
828            let mut write_buffer = BufWriter::new(Vec::new());
829            write_transaction(
830                &mut write_buffer,
831                &transaction,
832                Some(&meta.into()),
833                "",
834                Some(&sigverify_status),
835                Some(1628633791),
836                CliTimezone::Utc,
837            )
838            .unwrap();
839            let bytes = write_buffer.into_inner().unwrap();
840            String::from_utf8(bytes).unwrap()
841        };
842
843        assert_eq!(
844            output,
845            r"Block Time: 2021-08-10T22:16:31Z
846Version: legacy
847Recent Blockhash: 11111111111111111111111111111111
848Signature 0: 5pkjrE4VBa3Bu9CMKXgh1U345cT1gGo8QBVRTzHAo6gHeiPae5BTbShP15g6NgqRMNqu8Qrhph1ATmrfC1Ley3rx (pass)
849Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
850Account 1: -r-x 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
851Instruction 0
852  Program:   4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
853  Account 0: 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (0)
854  Data: []
855Status: Ok
856  Fee: ◎0.000005
857  Account 0 balance: ◎0.000005 -> ◎0
858  Account 1 balance: ◎0.00001 -> ◎0.0000099
859Compute Units Consumed: 1234
860Log Messages:
861  Test message
862Return Data from Program 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR:
863  Length: 3 (0x3) bytes
8640000:   01 02 03                                             ...
865Rewards:
866  Address                                            Type        Amount            New Balance         \0
867  4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi        rent        -◎0.0000001       ◎0.0000099         \0
868".replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI
869        );
870    }
871
872    #[test]
873    fn test_write_v0_transaction() {
874        let versioned_tx = new_test_v0_transaction();
875        let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&versioned_tx);
876        let address_table_entry1 = Pubkey::new_from_array([3u8; 32]);
877        let address_table_entry2 = Pubkey::new_from_array([4u8; 32]);
878        let loaded_addresses = LoadedAddresses {
879            writable: vec![address_table_entry1],
880            readonly: vec![address_table_entry2],
881        };
882        let meta = TransactionStatusMeta {
883            status: Ok(()),
884            fee: 5000,
885            pre_balances: vec![5000, 10_000, 15_000, 20_000],
886            post_balances: vec![0, 10_000, 14_900, 20_000],
887            inner_instructions: None,
888            log_messages: Some(vec!["Test message".to_string()]),
889            pre_token_balances: None,
890            post_token_balances: None,
891            rewards: Some(vec![Reward {
892                pubkey: address_table_entry1.to_string(),
893                lamports: -100,
894                post_balance: 14_900,
895                reward_type: Some(RewardType::Rent),
896                commission: None,
897            }]),
898            loaded_addresses,
899            return_data: Some(TransactionReturnData {
900                program_id: Pubkey::new_from_array([2u8; 32]),
901                data: vec![1, 2, 3],
902            }),
903            compute_units_consumed: Some(2345u64),
904            cost_units: Some(5678),
905        };
906
907        let output = {
908            let mut write_buffer = BufWriter::new(Vec::new());
909            write_transaction(
910                &mut write_buffer,
911                &versioned_tx,
912                Some(&meta.into()),
913                "",
914                Some(&sigverify_status),
915                Some(1628633791),
916                CliTimezone::Utc,
917            )
918            .unwrap();
919            let bytes = write_buffer.into_inner().unwrap();
920            String::from_utf8(bytes).unwrap()
921        };
922
923        assert_eq!(
924            output,
925            r"Block Time: 2021-08-10T22:16:31Z
926Version: 0
927Recent Blockhash: 11111111111111111111111111111111
928Signature 0: 5iEy3TT3ZhTA1NkuCY8GrQGNVY8d5m1bpjdh5FT3Ca4Py81fMipAZjafDuKJKrkw5q5UAAd8oPcgZ4nyXpHt4Fp7 (pass)
929Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
930Account 1: -r-- 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
931Account 2: -rw- Unknown Address (uses lookup 0 and index 0)
932Account 3: -r-x Unknown Address (uses lookup 0 and index 1)
933Instruction 0
934  Program:   Unknown Address (uses lookup 0 and index 1) (3)
935  Account 0: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
936  Account 1: Unknown Address (uses lookup 0 and index 0) (2)
937  Data: []
938Address Table Lookup 0
939  Table Account: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
940  Writable Indexes: [0]
941  Readonly Indexes: [1]
942Status: Ok
943  Fee: ◎0.000005
944  Account 0 balance: ◎0.000005 -> ◎0
945  Account 1 balance: ◎0.00001
946  Account 2 balance: ◎0.000015 -> ◎0.0000149
947  Account 3 balance: ◎0.00002
948Compute Units Consumed: 2345
949Log Messages:
950  Test message
951Return Data from Program 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR:
952  Length: 3 (0x3) bytes
9530000:   01 02 03                                             ...
954Rewards:
955  Address                                            Type        Amount            New Balance         \0
956  CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8        rent        -◎0.0000001       ◎0.0000149         \0
957".replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI
958        );
959    }
960
961    #[test]
962    fn test_format_labeled_address() {
963        let pubkey = Pubkey::default().to_string();
964        let mut address_labels = HashMap::new();
965
966        assert_eq!(format_labeled_address(&pubkey, &address_labels), pubkey);
967
968        address_labels.insert(pubkey.to_string(), "Default Address".to_string());
969        assert_eq!(
970            &format_labeled_address(&pubkey, &address_labels),
971            "Default Address (1111..1111)"
972        );
973
974        address_labels.insert(
975            pubkey.to_string(),
976            "abcdefghijklmnopqrstuvwxyz1234567890".to_string(),
977        );
978        assert_eq!(
979            &format_labeled_address(&pubkey, &address_labels),
980            "abcdefghijklmnopqrstuvwxyz12345 (1111..1111)"
981        );
982    }
983
984    #[test]
985    fn test_unix_timestamp_to_string() {
986        assert_eq!(unix_timestamp_to_string(1628633791), "2021-08-10T22:16:31Z");
987    }
988}