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