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