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_native_token::lamports_to_sol,
14    solana_pubkey::Pubkey,
15    solana_signature::Signature,
16    solana_stake_interface as stake,
17    solana_transaction::versioned::{TransactionVersion, VersionedTransaction},
18    solana_transaction_error::TransactionError,
19    solana_transaction_status::{
20        Rewards, UiReturnDataEncoding, UiTransactionReturnData, UiTransactionStatusMeta,
21    },
22    spl_memo::{id as spl_memo_id, v1::id as spl_memo_v1_id},
23    std::{collections::HashMap, fmt, io, time::Duration},
24};
25
26#[derive(Clone, Debug)]
27pub struct BuildBalanceMessageConfig {
28    pub use_lamports_unit: bool,
29    pub show_unit: bool,
30    pub trim_trailing_zeros: bool,
31}
32
33impl Default for BuildBalanceMessageConfig {
34    fn default() -> Self {
35        Self {
36            use_lamports_unit: false,
37            show_unit: true,
38            trim_trailing_zeros: true,
39        }
40    }
41}
42
43fn is_memo_program(k: &Pubkey) -> bool {
44    let k_str = k.to_string();
45    (k_str == spl_memo_v1_id().to_string()) || (k_str == spl_memo_id().to_string())
46}
47
48pub fn build_balance_message_with_config(
49    lamports: u64,
50    config: &BuildBalanceMessageConfig,
51) -> String {
52    let value = if config.use_lamports_unit {
53        lamports.to_string()
54    } else {
55        let sol = lamports_to_sol(lamports);
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(lookups: &[MessageAddressTableLookup]) -> Vec<AccountKeyType> {
279    let unknown_writable_keys = lookups
280        .iter()
281        .enumerate()
282        .flat_map(|(lookup_index, lookup)| {
283            lookup
284                .writable_indexes
285                .iter()
286                .map(move |table_index| AccountKeyType::Unknown {
287                    lookup_index,
288                    table_index: *table_index,
289                })
290        });
291
292    let unknown_readonly_keys = lookups
293        .iter()
294        .enumerate()
295        .flat_map(|(lookup_index, lookup)| {
296            lookup
297                .readonly_indexes
298                .iter()
299                .map(move |table_index| AccountKeyType::Unknown {
300                    lookup_index,
301                    table_index: *table_index,
302                })
303        });
304
305    unknown_writable_keys.chain(unknown_readonly_keys).collect()
306}
307
308enum CliTimezone {
309    Local,
310    #[allow(dead_code)]
311    Utc,
312}
313
314fn write_block_time<W: io::Write>(
315    w: &mut W,
316    block_time: Option<UnixTimestamp>,
317    timezone: CliTimezone,
318    prefix: &str,
319) -> io::Result<()> {
320    if let Some(block_time) = block_time {
321        let block_time_output = match timezone {
322            CliTimezone::Local => format!("{:?}", Local.timestamp_opt(block_time, 0).unwrap()),
323            CliTimezone::Utc => format!("{:?}", Utc.timestamp_opt(block_time, 0).unwrap()),
324        };
325        writeln!(w, "{prefix}Block Time: {block_time_output}",)?;
326    }
327    Ok(())
328}
329
330fn write_version<W: io::Write>(
331    w: &mut W,
332    version: TransactionVersion,
333    prefix: &str,
334) -> io::Result<()> {
335    let version = match version {
336        TransactionVersion::Legacy(_) => "legacy".to_string(),
337        TransactionVersion::Number(number) => number.to_string(),
338    };
339    writeln!(w, "{prefix}Version: {version}")
340}
341
342fn write_recent_blockhash<W: io::Write>(
343    w: &mut W,
344    recent_blockhash: &Hash,
345    prefix: &str,
346) -> io::Result<()> {
347    writeln!(w, "{prefix}Recent Blockhash: {recent_blockhash:?}")
348}
349
350fn write_signatures<W: io::Write>(
351    w: &mut W,
352    signatures: &[Signature],
353    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
354    prefix: &str,
355) -> io::Result<()> {
356    let sigverify_statuses = if let Some(sigverify_status) = sigverify_status {
357        sigverify_status.iter().map(|s| format!(" ({s})")).collect()
358    } else {
359        vec!["".to_string(); signatures.len()]
360    };
361    for (signature_index, (signature, sigverify_status)) in
362        signatures.iter().zip(&sigverify_statuses).enumerate()
363    {
364        writeln!(
365            w,
366            "{prefix}Signature {signature_index}: {signature:?}{sigverify_status}",
367        )?;
368    }
369    Ok(())
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq)]
373enum AccountKeyType<'a> {
374    Known(&'a Pubkey),
375    Unknown {
376        lookup_index: usize,
377        table_index: u8,
378    },
379}
380
381impl fmt::Display for AccountKeyType<'_> {
382    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
383        match self {
384            Self::Known(address) => write!(f, "{address}"),
385            Self::Unknown {
386                lookup_index,
387                table_index,
388            } => {
389                write!(
390                    f,
391                    "Unknown Address (uses lookup {lookup_index} and index {table_index})"
392                )
393            }
394        }
395    }
396}
397
398fn write_account<W: io::Write>(
399    w: &mut W,
400    account_index: usize,
401    account_address: AccountKeyType,
402    account_mode: String,
403    is_fee_payer: bool,
404    prefix: &str,
405) -> io::Result<()> {
406    writeln!(
407        w,
408        "{}Account {}: {} {}{}",
409        prefix,
410        account_index,
411        account_mode,
412        account_address,
413        if is_fee_payer { " (fee payer)" } else { "" },
414    )
415}
416
417fn write_instruction<'a, W: io::Write>(
418    w: &mut W,
419    instruction_index: usize,
420    program_pubkey: AccountKeyType,
421    instruction: &CompiledInstruction,
422    instruction_accounts: impl Iterator<Item = (AccountKeyType<'a>, u8)>,
423    prefix: &str,
424) -> io::Result<()> {
425    writeln!(w, "{prefix}Instruction {instruction_index}")?;
426    writeln!(
427        w,
428        "{}  Program:   {} ({})",
429        prefix, program_pubkey, instruction.program_id_index
430    )?;
431    for (index, (account_address, account_index)) in instruction_accounts.enumerate() {
432        writeln!(
433            w,
434            "{prefix}  Account {index}: {account_address} ({account_index})"
435        )?;
436    }
437
438    let mut raw = true;
439    if let AccountKeyType::Known(program_pubkey) = program_pubkey {
440        if program_pubkey == &solana_vote_program::id() {
441            if let Ok(vote_instruction) =
442                limited_deserialize::<solana_vote_program::vote_instruction::VoteInstruction>(
443                    &instruction.data,
444                    solana_packet::PACKET_DATA_SIZE as u64,
445                )
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) = limited_deserialize::<stake::instruction::StakeInstruction>(
452                &instruction.data,
453                solana_packet::PACKET_DATA_SIZE as u64,
454            ) {
455                writeln!(w, "{prefix}  {stake_instruction:?}")?;
456                raw = false;
457            }
458        } else if program_pubkey == &solana_sdk_ids::system_program::id() {
459            if let Ok(system_instruction) =
460                limited_deserialize::<solana_system_interface::instruction::SystemInstruction>(
461                    &instruction.data,
462                    solana_packet::PACKET_DATA_SIZE as u64,
463                )
464            {
465                writeln!(w, "{prefix}  {system_instruction:?}")?;
466                raw = false;
467            }
468        } else if is_memo_program(program_pubkey) {
469            if let Ok(s) = std::str::from_utf8(&instruction.data) {
470                writeln!(w, "{prefix}  Data: \"{s}\"")?;
471                raw = false;
472            }
473        }
474    }
475
476    if raw {
477        writeln!(w, "{}  Data: {:?}", prefix, instruction.data)?;
478    }
479
480    Ok(())
481}
482
483fn write_address_table_lookups<W: io::Write>(
484    w: &mut W,
485    address_table_lookups: &[MessageAddressTableLookup],
486    prefix: &str,
487) -> io::Result<()> {
488    for (lookup_index, lookup) in address_table_lookups.iter().enumerate() {
489        writeln!(w, "{prefix}Address Table Lookup {lookup_index}",)?;
490        writeln!(w, "{}  Table Account: {}", prefix, lookup.account_key,)?;
491        writeln!(
492            w,
493            "{}  Writable Indexes: {:?}",
494            prefix,
495            &lookup.writable_indexes[..],
496        )?;
497        writeln!(
498            w,
499            "{}  Readonly Indexes: {:?}",
500            prefix,
501            &lookup.readonly_indexes[..],
502        )?;
503    }
504    Ok(())
505}
506
507fn write_rewards<W: io::Write>(
508    w: &mut W,
509    rewards: Option<&Rewards>,
510    prefix: &str,
511) -> io::Result<()> {
512    if let Some(rewards) = rewards {
513        if !rewards.is_empty() {
514            writeln!(w, "{prefix}Rewards:",)?;
515            writeln!(
516                w,
517                "{}  {:<44}  {:^15}  {:<16}  {:<20}",
518                prefix, "Address", "Type", "Amount", "New Balance"
519            )?;
520            for reward in rewards {
521                let sign = if reward.lamports < 0 { "-" } else { "" };
522                writeln!(
523                    w,
524                    "{}  {:<44}  {:^15}  {}◎{:<14.9}  ◎{:<18.9}",
525                    prefix,
526                    reward.pubkey,
527                    if let Some(reward_type) = reward.reward_type {
528                        format!("{reward_type}")
529                    } else {
530                        "-".to_string()
531                    },
532                    sign,
533                    lamports_to_sol(reward.lamports.unsigned_abs()),
534                    lamports_to_sol(reward.post_balance)
535                )?;
536            }
537        }
538    }
539    Ok(())
540}
541
542fn write_status<W: io::Write>(
543    w: &mut W,
544    transaction_status: &Result<(), TransactionError>,
545    prefix: &str,
546) -> io::Result<()> {
547    writeln!(
548        w,
549        "{}Status: {}",
550        prefix,
551        match transaction_status {
552            Ok(_) => "Ok".into(),
553            Err(err) => err.to_string(),
554        }
555    )
556}
557
558fn write_fees<W: io::Write>(w: &mut W, transaction_fee: u64, prefix: &str) -> io::Result<()> {
559    writeln!(w, "{}  Fee: ◎{}", prefix, lamports_to_sol(transaction_fee))
560}
561
562fn write_balances<W: io::Write>(
563    w: &mut W,
564    transaction_status: &UiTransactionStatusMeta,
565    prefix: &str,
566) -> io::Result<()> {
567    assert_eq!(
568        transaction_status.pre_balances.len(),
569        transaction_status.post_balances.len()
570    );
571    for (i, (pre, post)) in transaction_status
572        .pre_balances
573        .iter()
574        .zip(transaction_status.post_balances.iter())
575        .enumerate()
576    {
577        if pre == post {
578            writeln!(
579                w,
580                "{}  Account {} balance: ◎{}",
581                prefix,
582                i,
583                lamports_to_sol(*pre)
584            )?;
585        } else {
586            writeln!(
587                w,
588                "{}  Account {} balance: ◎{} -> ◎{}",
589                prefix,
590                i,
591                lamports_to_sol(*pre),
592                lamports_to_sol(*post)
593            )?;
594        }
595    }
596    Ok(())
597}
598
599fn write_return_data<W: io::Write>(
600    w: &mut W,
601    return_data: Option<&UiTransactionReturnData>,
602    prefix: &str,
603) -> io::Result<()> {
604    if let Some(return_data) = return_data {
605        let (data, encoding) = &return_data.data;
606        let raw_return_data = match encoding {
607            UiReturnDataEncoding::Base64 => BASE64_STANDARD.decode(data).map_err(|err| {
608                io::Error::other(format!("could not parse data as {encoding:?}: {err:?}"))
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_keypair::Keypair,
727        solana_message::{
728            v0::{self, LoadedAddresses},
729            Message as LegacyMessage, MessageHeader, VersionedMessage,
730        },
731        solana_pubkey::Pubkey,
732        solana_signer::Signer,
733        solana_transaction::Transaction,
734        solana_transaction_context::TransactionReturnData,
735        solana_transaction_status::{Reward, RewardType, TransactionStatusMeta},
736        std::io::BufWriter,
737    };
738
739    fn new_test_keypair() -> Keypair {
740        let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap();
741        let public = ed25519_dalek::PublicKey::from(&secret);
742        let keypair = ed25519_dalek::Keypair { secret, public };
743        Keypair::from_bytes(&keypair.to_bytes()).unwrap()
744    }
745
746    fn new_test_v0_transaction() -> VersionedTransaction {
747        let keypair = new_test_keypair();
748        let account_key = Pubkey::new_from_array([1u8; 32]);
749        let address_table_key = Pubkey::new_from_array([2u8; 32]);
750        VersionedTransaction::try_new(
751            VersionedMessage::V0(v0::Message {
752                header: MessageHeader {
753                    num_required_signatures: 1,
754                    num_readonly_signed_accounts: 0,
755                    num_readonly_unsigned_accounts: 1,
756                },
757                recent_blockhash: Hash::default(),
758                account_keys: vec![keypair.pubkey(), account_key],
759                address_table_lookups: vec![MessageAddressTableLookup {
760                    account_key: address_table_key,
761                    writable_indexes: vec![0],
762                    readonly_indexes: vec![1],
763                }],
764                instructions: vec![CompiledInstruction::new_from_raw_parts(
765                    3,
766                    vec![],
767                    vec![1, 2],
768                )],
769            }),
770            &[&keypair],
771        )
772        .unwrap()
773    }
774
775    #[test]
776    fn test_write_legacy_transaction() {
777        let keypair = new_test_keypair();
778        let account_key = Pubkey::new_from_array([1u8; 32]);
779        let transaction = VersionedTransaction::from(Transaction::new(
780            &[&keypair],
781            LegacyMessage {
782                header: MessageHeader {
783                    num_required_signatures: 1,
784                    num_readonly_signed_accounts: 0,
785                    num_readonly_unsigned_accounts: 1,
786                },
787                recent_blockhash: Hash::default(),
788                account_keys: vec![keypair.pubkey(), account_key],
789                instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])],
790            },
791            Hash::default(),
792        ));
793
794        let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction);
795        let meta = TransactionStatusMeta {
796            status: Ok(()),
797            fee: 5000,
798            pre_balances: vec![5000, 10_000],
799            post_balances: vec![0, 9_900],
800            inner_instructions: None,
801            log_messages: Some(vec!["Test message".to_string()]),
802            pre_token_balances: None,
803            post_token_balances: None,
804            rewards: Some(vec![Reward {
805                pubkey: account_key.to_string(),
806                lamports: -100,
807                post_balance: 9_900,
808                reward_type: Some(RewardType::Rent),
809                commission: None,
810            }]),
811            loaded_addresses: LoadedAddresses::default(),
812            return_data: Some(TransactionReturnData {
813                program_id: Pubkey::new_from_array([2u8; 32]),
814                data: vec![1, 2, 3],
815            }),
816            compute_units_consumed: Some(1234u64),
817            cost_units: Some(5678),
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            cost_units: Some(5678),
898        };
899
900        let output = {
901            let mut write_buffer = BufWriter::new(Vec::new());
902            write_transaction(
903                &mut write_buffer,
904                &versioned_tx,
905                Some(&meta.into()),
906                "",
907                Some(&sigverify_status),
908                Some(1628633791),
909                CliTimezone::Utc,
910            )
911            .unwrap();
912            let bytes = write_buffer.into_inner().unwrap();
913            String::from_utf8(bytes).unwrap()
914        };
915
916        assert_eq!(
917            output,
918            r"Block Time: 2021-08-10T22:16:31Z
919Version: 0
920Recent Blockhash: 11111111111111111111111111111111
921Signature 0: 5iEy3TT3ZhTA1NkuCY8GrQGNVY8d5m1bpjdh5FT3Ca4Py81fMipAZjafDuKJKrkw5q5UAAd8oPcgZ4nyXpHt4Fp7 (pass)
922Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
923Account 1: -r-- 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
924Account 2: -rw- Unknown Address (uses lookup 0 and index 0)
925Account 3: -r-x Unknown Address (uses lookup 0 and index 1)
926Instruction 0
927  Program:   Unknown Address (uses lookup 0 and index 1) (3)
928  Account 0: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
929  Account 1: Unknown Address (uses lookup 0 and index 0) (2)
930  Data: []
931Address Table Lookup 0
932  Table Account: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
933  Writable Indexes: [0]
934  Readonly Indexes: [1]
935Status: Ok
936  Fee: ◎0.000005
937  Account 0 balance: ◎0.000005 -> ◎0
938  Account 1 balance: ◎0.00001
939  Account 2 balance: ◎0.000015 -> ◎0.0000149
940  Account 3 balance: ◎0.00002
941Compute Units Consumed: 2345
942Log Messages:
943  Test message
944Return Data from Program 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR:
945  Length: 3 (0x3) bytes
9460000:   01 02 03                                             ...
947Rewards:
948  Address                                            Type        Amount            New Balance         \0
949  CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8        rent        -◎0.000000100     ◎0.000014900       \0
950".replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI
951        );
952    }
953
954    #[test]
955    fn test_format_labeled_address() {
956        let pubkey = Pubkey::default().to_string();
957        let mut address_labels = HashMap::new();
958
959        assert_eq!(format_labeled_address(&pubkey, &address_labels), pubkey);
960
961        address_labels.insert(pubkey.to_string(), "Default Address".to_string());
962        assert_eq!(
963            &format_labeled_address(&pubkey, &address_labels),
964            "Default Address (1111..1111)"
965        );
966
967        address_labels.insert(
968            pubkey.to_string(),
969            "abcdefghijklmnopqrstuvwxyz1234567890".to_string(),
970        );
971        assert_eq!(
972            &format_labeled_address(&pubkey, &address_labels),
973            "abcdefghijklmnopqrstuvwxyz12345 (1111..1111)"
974        );
975    }
976
977    #[test]
978    fn test_unix_timestamp_to_string() {
979        assert_eq!(unix_timestamp_to_string(1628633791), "2021-08-10T22:16:31Z");
980    }
981}