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