safecoin_cli_output/
display.rs

1use {
2    crate::cli_output::CliSignatureVerificationStatus,
3    chrono::{DateTime, Local, NaiveDateTime, SecondsFormat, TimeZone, Utc},
4    console::style,
5    indicatif::{ProgressBar, ProgressStyle},
6    safecoin_cli_config::SettingType,
7    solana_sdk::{
8        clock::UnixTimestamp,
9        hash::Hash,
10        instruction::CompiledInstruction,
11        message::v0::MessageAddressTableLookup,
12        native_token::lamports_to_sol,
13        program_utils::limited_deserialize,
14        pubkey::Pubkey,
15        signature::Signature,
16        stake,
17        transaction::{TransactionError, TransactionVersion, VersionedTransaction},
18    },
19    safecoin_transaction_status::{
20        Rewards, UiReturnDataEncoding, UiTransactionReturnData, UiTransactionStatusMeta,
21    },
22    safe_memo::{id as safe_memo_id, v1::id as safe_memo_v1_id},
23    std::{collections::HashMap, fmt, io},
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 == safe_memo_v1_id().to_string()) || (k_str == safe_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!("{:.9}", sol);
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            " SAFE".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 mut fee_payer_index = None;
220    for (account_index, account) in account_keys.iter().enumerate() {
221        if fee_payer_index.is_none() && message.is_non_loader_key(account_index) {
222            fee_payer_index = Some(account_index)
223        }
224
225        let account_meta = CliAccountMeta {
226            is_signer: message.is_signer(account_index),
227            is_writable: message.is_maybe_writable(account_index),
228            is_invoked: message.is_invoked(account_index),
229        };
230
231        write_account(
232            w,
233            account_index,
234            *account,
235            format_account_mode(account_meta),
236            Some(account_index) == fee_payer_index,
237            prefix,
238        )?;
239    }
240
241    for (instruction_index, instruction) in message.instructions().iter().enumerate() {
242        let program_pubkey = account_keys[instruction.program_id_index as usize];
243        let instruction_accounts = instruction
244            .accounts
245            .iter()
246            .map(|account_index| (account_keys[*account_index as usize], *account_index));
247
248        write_instruction(
249            w,
250            instruction_index,
251            program_pubkey,
252            instruction,
253            instruction_accounts,
254            prefix,
255        )?;
256    }
257
258    if let Some(address_table_lookups) = message.address_table_lookups() {
259        write_address_table_lookups(w, address_table_lookups, prefix)?;
260    }
261
262    if let Some(transaction_status) = transaction_status {
263        write_status(w, &transaction_status.status, prefix)?;
264        write_fees(w, transaction_status.fee, prefix)?;
265        write_balances(w, transaction_status, prefix)?;
266        write_compute_units_consumed(
267            w,
268            transaction_status.compute_units_consumed.clone().into(),
269            prefix,
270        )?;
271        write_log_messages(w, transaction_status.log_messages.as_ref().into(), prefix)?;
272        write_return_data(w, transaction_status.return_data.as_ref().into(), prefix)?;
273        write_rewards(w, transaction_status.rewards.as_ref().into(), prefix)?;
274    } else {
275        writeln!(w, "{}Status: Unavailable", prefix)?;
276    }
277
278    Ok(())
279}
280
281fn transform_lookups_to_unknown_keys(lookups: &[MessageAddressTableLookup]) -> 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(block_time, 0)),
326            CliTimezone::Utc => format!("{:?}", Utc.timestamp(block_time, 0)),
327        };
328        writeln!(w, "{}Block Time: {}", prefix, 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, "{}Version: {}", prefix, 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, "{}Recent Blockhash: {:?}", prefix, 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
361            .iter()
362            .map(|s| format!(" ({})", s))
363            .collect()
364    } else {
365        vec!["".to_string(); signatures.len()]
366    };
367    for (signature_index, (signature, sigverify_status)) in
368        signatures.iter().zip(&sigverify_statuses).enumerate()
369    {
370        writeln!(
371            w,
372            "{}Signature {}: {:?}{}",
373            prefix, signature_index, signature, sigverify_status,
374        )?;
375    }
376    Ok(())
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
380enum AccountKeyType<'a> {
381    Known(&'a Pubkey),
382    Unknown {
383        lookup_index: usize,
384        table_index: u8,
385    },
386}
387
388impl fmt::Display for AccountKeyType<'_> {
389    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
390        match self {
391            Self::Known(address) => write!(f, "{}", address),
392            Self::Unknown {
393                lookup_index,
394                table_index,
395            } => {
396                write!(
397                    f,
398                    "Unknown Address (uses lookup {} and index {})",
399                    lookup_index, table_index
400                )
401            }
402        }
403    }
404}
405
406fn write_account<W: io::Write>(
407    w: &mut W,
408    account_index: usize,
409    account_address: AccountKeyType,
410    account_mode: String,
411    is_fee_payer: bool,
412    prefix: &str,
413) -> io::Result<()> {
414    writeln!(
415        w,
416        "{}Account {}: {} {}{}",
417        prefix,
418        account_index,
419        account_mode,
420        account_address,
421        if is_fee_payer { " (fee payer)" } else { "" },
422    )
423}
424
425fn write_instruction<'a, W: io::Write>(
426    w: &mut W,
427    instruction_index: usize,
428    program_pubkey: AccountKeyType,
429    instruction: &CompiledInstruction,
430    instruction_accounts: impl Iterator<Item = (AccountKeyType<'a>, u8)>,
431    prefix: &str,
432) -> io::Result<()> {
433    writeln!(w, "{}Instruction {}", prefix, instruction_index)?;
434    writeln!(
435        w,
436        "{}  Program:   {} ({})",
437        prefix, program_pubkey, instruction.program_id_index
438    )?;
439    for (index, (account_address, account_index)) in instruction_accounts.enumerate() {
440        writeln!(
441            w,
442            "{}  Account {}: {} ({})",
443            prefix, index, account_address, account_index
444        )?;
445    }
446
447    let mut raw = true;
448    if let AccountKeyType::Known(program_pubkey) = program_pubkey {
449        if program_pubkey == &solana_vote_program::id() {
450            if let Ok(vote_instruction) = limited_deserialize::<
451                solana_vote_program::vote_instruction::VoteInstruction,
452            >(&instruction.data)
453            {
454                writeln!(w, "{}  {:?}", prefix, vote_instruction)?;
455                raw = false;
456            }
457        } else if program_pubkey == &stake::program::id() {
458            if let Ok(stake_instruction) =
459                limited_deserialize::<stake::instruction::StakeInstruction>(&instruction.data)
460            {
461                writeln!(w, "{}  {:?}", prefix, stake_instruction)?;
462                raw = false;
463            }
464        } else if program_pubkey == &solana_sdk::system_program::id() {
465            if let Ok(system_instruction) = limited_deserialize::<
466                solana_sdk::system_instruction::SystemInstruction,
467            >(&instruction.data)
468            {
469                writeln!(w, "{}  {:?}", prefix, system_instruction)?;
470                raw = false;
471            }
472        } else if is_memo_program(program_pubkey) {
473            if let Ok(s) = std::str::from_utf8(&instruction.data) {
474                writeln!(w, "{}  Data: \"{}\"", prefix, s)?;
475                raw = false;
476            }
477        }
478    }
479
480    if raw {
481        writeln!(w, "{}  Data: {:?}", prefix, instruction.data)?;
482    }
483
484    Ok(())
485}
486
487fn write_address_table_lookups<W: io::Write>(
488    w: &mut W,
489    address_table_lookups: &[MessageAddressTableLookup],
490    prefix: &str,
491) -> io::Result<()> {
492    for (lookup_index, lookup) in address_table_lookups.iter().enumerate() {
493        writeln!(w, "{}Address Table Lookup {}", prefix, lookup_index,)?;
494        writeln!(w, "{}  Table Account: {}", prefix, lookup.account_key,)?;
495        writeln!(
496            w,
497            "{}  Writable Indexes: {:?}",
498            prefix,
499            &lookup.writable_indexes[..],
500        )?;
501        writeln!(
502            w,
503            "{}  Readonly Indexes: {:?}",
504            prefix,
505            &lookup.readonly_indexes[..],
506        )?;
507    }
508    Ok(())
509}
510
511fn write_rewards<W: io::Write>(
512    w: &mut W,
513    rewards: Option<&Rewards>,
514    prefix: &str,
515) -> io::Result<()> {
516    if let Some(rewards) = rewards {
517        if !rewards.is_empty() {
518            writeln!(w, "{}Rewards:", prefix,)?;
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                    lamports_to_sol(reward.lamports.unsigned_abs()),
538                    lamports_to_sol(reward.post_balance)
539                )?;
540            }
541        }
542    }
543    Ok(())
544}
545
546fn write_status<W: io::Write>(
547    w: &mut W,
548    transaction_status: &Result<(), TransactionError>,
549    prefix: &str,
550) -> io::Result<()> {
551    writeln!(
552        w,
553        "{}Status: {}",
554        prefix,
555        match transaction_status {
556            Ok(_) => "Ok".into(),
557            Err(err) => err.to_string(),
558        }
559    )
560}
561
562fn write_fees<W: io::Write>(w: &mut W, transaction_fee: u64, prefix: &str) -> io::Result<()> {
563    writeln!(w, "{}  Fee: ◎{}", prefix, lamports_to_sol(transaction_fee))
564}
565
566fn write_balances<W: io::Write>(
567    w: &mut W,
568    transaction_status: &UiTransactionStatusMeta,
569    prefix: &str,
570) -> io::Result<()> {
571    assert_eq!(
572        transaction_status.pre_balances.len(),
573        transaction_status.post_balances.len()
574    );
575    for (i, (pre, post)) in transaction_status
576        .pre_balances
577        .iter()
578        .zip(transaction_status.post_balances.iter())
579        .enumerate()
580    {
581        if pre == post {
582            writeln!(
583                w,
584                "{}  Account {} balance: ◎{}",
585                prefix,
586                i,
587                lamports_to_sol(*pre)
588            )?;
589        } else {
590            writeln!(
591                w,
592                "{}  Account {} balance: ◎{} -> ◎{}",
593                prefix,
594                i,
595                lamports_to_sol(*pre),
596                lamports_to_sol(*post)
597            )?;
598        }
599    }
600    Ok(())
601}
602
603fn write_return_data<W: io::Write>(
604    w: &mut W,
605    return_data: Option<&UiTransactionReturnData>,
606    prefix: &str,
607) -> io::Result<()> {
608    if let Some(return_data) = return_data {
609        let (data, encoding) = &return_data.data;
610        let raw_return_data = match encoding {
611            UiReturnDataEncoding::Base64 => base64::decode(data).map_err(|err| {
612                io::Error::new(
613                    io::ErrorKind::Other,
614                    format!("could not parse data as {:?}: {:?}", encoding, err),
615                )
616            })?,
617        };
618        if !raw_return_data.is_empty() {
619            use pretty_hex::*;
620            writeln!(
621                w,
622                "{}Return Data from Program {}:",
623                prefix, return_data.program_id
624            )?;
625            writeln!(w, "{}  {:?}", prefix, raw_return_data.hex_dump())?;
626        }
627    }
628    Ok(())
629}
630
631fn write_compute_units_consumed<W: io::Write>(
632    w: &mut W,
633    compute_units_consumed: Option<u64>,
634    prefix: &str,
635) -> io::Result<()> {
636    if let Some(cus) = compute_units_consumed {
637        writeln!(w, "{}Compute Units Consumed: {}", prefix, cus)?;
638    }
639    Ok(())
640}
641
642fn write_log_messages<W: io::Write>(
643    w: &mut W,
644    log_messages: Option<&Vec<String>>,
645    prefix: &str,
646) -> io::Result<()> {
647    if let Some(log_messages) = log_messages {
648        if !log_messages.is_empty() {
649            writeln!(w, "{}Log Messages:", prefix,)?;
650            for log_message in log_messages {
651                writeln!(w, "{}  {}", prefix, log_message)?;
652            }
653        }
654    }
655    Ok(())
656}
657
658pub fn println_transaction(
659    transaction: &VersionedTransaction,
660    transaction_status: Option<&UiTransactionStatusMeta>,
661    prefix: &str,
662    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
663    block_time: Option<UnixTimestamp>,
664) {
665    let mut w = Vec::new();
666    if write_transaction(
667        &mut w,
668        transaction,
669        transaction_status,
670        prefix,
671        sigverify_status,
672        block_time,
673        CliTimezone::Local,
674    )
675    .is_ok()
676    {
677        if let Ok(s) = String::from_utf8(w) {
678            print!("{}", s);
679        }
680    }
681}
682
683pub fn writeln_transaction(
684    f: &mut dyn fmt::Write,
685    transaction: &VersionedTransaction,
686    transaction_status: Option<&UiTransactionStatusMeta>,
687    prefix: &str,
688    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
689    block_time: Option<UnixTimestamp>,
690) -> fmt::Result {
691    let mut w = Vec::new();
692    let write_result = write_transaction(
693        &mut w,
694        transaction,
695        transaction_status,
696        prefix,
697        sigverify_status,
698        block_time,
699        CliTimezone::Local,
700    );
701
702    if write_result.is_ok() {
703        if let Ok(s) = String::from_utf8(w) {
704            write!(f, "{}", s)?;
705        }
706    }
707    Ok(())
708}
709
710/// Creates a new process bar for processing that will take an unknown amount of time
711pub fn new_spinner_progress_bar() -> ProgressBar {
712    let progress_bar = ProgressBar::new(42);
713    progress_bar
714        .set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}"));
715    progress_bar.enable_steady_tick(100);
716    progress_bar
717}
718
719pub fn unix_timestamp_to_string(unix_timestamp: UnixTimestamp) -> String {
720    match NaiveDateTime::from_timestamp_opt(unix_timestamp, 0) {
721        Some(ndt) => DateTime::<Utc>::from_utc(ndt, Utc).to_rfc3339_opts(SecondsFormat::Secs, true),
722        None => format!("UnixTimestamp {}", unix_timestamp),
723    }
724}
725
726#[cfg(test)]
727mod test {
728    use {
729        super::*,
730        solana_sdk::{
731            message::{
732                v0::{self, LoadedAddresses},
733                Message as LegacyMessage, MessageHeader, VersionedMessage,
734            },
735            pubkey::Pubkey,
736            signature::{Keypair, Signer},
737            transaction::Transaction,
738            transaction_context::TransactionReturnData,
739        },
740        safecoin_transaction_status::{Reward, RewardType, TransactionStatusMeta},
741        std::io::BufWriter,
742    };
743
744    fn new_test_keypair() -> Keypair {
745        let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap();
746        let public = ed25519_dalek::PublicKey::from(&secret);
747        let keypair = ed25519_dalek::Keypair { secret, public };
748        Keypair::from_bytes(&keypair.to_bytes()).unwrap()
749    }
750
751    fn new_test_v0_transaction() -> VersionedTransaction {
752        let keypair = new_test_keypair();
753        let account_key = Pubkey::new_from_array([1u8; 32]);
754        let address_table_key = Pubkey::new_from_array([2u8; 32]);
755        VersionedTransaction::try_new(
756            VersionedMessage::V0(v0::Message {
757                header: MessageHeader {
758                    num_required_signatures: 1,
759                    num_readonly_signed_accounts: 0,
760                    num_readonly_unsigned_accounts: 1,
761                },
762                recent_blockhash: Hash::default(),
763                account_keys: vec![keypair.pubkey(), account_key],
764                address_table_lookups: vec![MessageAddressTableLookup {
765                    account_key: address_table_key,
766                    writable_indexes: vec![0],
767                    readonly_indexes: vec![1],
768                }],
769                instructions: vec![CompiledInstruction::new_from_raw_parts(
770                    3,
771                    vec![],
772                    vec![1, 2],
773                )],
774            }),
775            &[&keypair],
776        )
777        .unwrap()
778    }
779
780    #[test]
781    fn test_write_legacy_transaction() {
782        let keypair = new_test_keypair();
783        let account_key = Pubkey::new_from_array([1u8; 32]);
784        let transaction = VersionedTransaction::from(Transaction::new(
785            &[&keypair],
786            LegacyMessage {
787                header: MessageHeader {
788                    num_required_signatures: 1,
789                    num_readonly_signed_accounts: 0,
790                    num_readonly_unsigned_accounts: 1,
791                },
792                recent_blockhash: Hash::default(),
793                account_keys: vec![keypair.pubkey(), account_key],
794                instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])],
795            },
796            Hash::default(),
797        ));
798
799        let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction);
800        let meta = TransactionStatusMeta {
801            status: Ok(()),
802            fee: 5000,
803            pre_balances: vec![5000, 10_000],
804            post_balances: vec![0, 9_900],
805            inner_instructions: None,
806            log_messages: Some(vec!["Test message".to_string()]),
807            pre_token_balances: None,
808            post_token_balances: None,
809            rewards: Some(vec![Reward {
810                pubkey: account_key.to_string(),
811                lamports: -100,
812                post_balance: 9_900,
813                reward_type: Some(RewardType::Rent),
814                commission: None,
815            }]),
816            loaded_addresses: LoadedAddresses::default(),
817            return_data: Some(TransactionReturnData {
818                program_id: Pubkey::new_from_array([2u8; 32]),
819                data: vec![1, 2, 3],
820            }),
821            compute_units_consumed: Some(1234u64),
822        };
823
824        let output = {
825            let mut write_buffer = BufWriter::new(Vec::new());
826            write_transaction(
827                &mut write_buffer,
828                &transaction,
829                Some(&meta.into()),
830                "",
831                Some(&sigverify_status),
832                Some(1628633791),
833                CliTimezone::Utc,
834            )
835            .unwrap();
836            let bytes = write_buffer.into_inner().unwrap();
837            String::from_utf8(bytes).unwrap()
838        };
839
840        assert_eq!(
841            output,
842            r#"Block Time: 2021-08-10T22:16:31Z
843Version: legacy
844Recent Blockhash: 11111111111111111111111111111111
845Signature 0: 5pkjrE4VBa3Bu9CMKXgh1U345cT1gGo8QBVRTzHAo6gHeiPae5BTbShP15g6NgqRMNqu8Qrhph1ATmrfC1Ley3rx (pass)
846Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
847Account 1: -r-x 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
848Instruction 0
849  Program:   4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
850  Account 0: 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (0)
851  Data: []
852Status: Ok
853  Fee: ◎0.000005
854  Account 0 balance: ◎0.000005 -> ◎0
855  Account 1 balance: ◎0.00001 -> ◎0.0000099
856Compute Units Consumed: 1234
857Log Messages:
858  Test message
859Return Data from Program 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR:
860  Length: 3 (0x3) bytes
8610000:   01 02 03                                             ...
862Rewards:
863  Address                                            Type        Amount            New Balance         \0
864  4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi        rent        -◎0.000000100     ◎0.000009900       \0
865"#.replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI
866        );
867    }
868
869    #[test]
870    fn test_write_v0_transaction() {
871        let versioned_tx = new_test_v0_transaction();
872        let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&versioned_tx);
873        let address_table_entry1 = Pubkey::new_from_array([3u8; 32]);
874        let address_table_entry2 = Pubkey::new_from_array([4u8; 32]);
875        let loaded_addresses = LoadedAddresses {
876            writable: vec![address_table_entry1],
877            readonly: vec![address_table_entry2],
878        };
879        let meta = TransactionStatusMeta {
880            status: Ok(()),
881            fee: 5000,
882            pre_balances: vec![5000, 10_000, 15_000, 20_000],
883            post_balances: vec![0, 10_000, 14_900, 20_000],
884            inner_instructions: None,
885            log_messages: Some(vec!["Test message".to_string()]),
886            pre_token_balances: None,
887            post_token_balances: None,
888            rewards: Some(vec![Reward {
889                pubkey: address_table_entry1.to_string(),
890                lamports: -100,
891                post_balance: 14_900,
892                reward_type: Some(RewardType::Rent),
893                commission: None,
894            }]),
895            loaded_addresses,
896            return_data: Some(TransactionReturnData {
897                program_id: Pubkey::new_from_array([2u8; 32]),
898                data: vec![1, 2, 3],
899            }),
900            compute_units_consumed: Some(2345u64),
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.000000100     ◎0.000014900       \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}