Skip to main content

solana_cli_output/
display.rs

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