Skip to main content

solana_cli_output/
display.rs

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