solana_cli_output/
display.rs

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