1use {
2 crate::cli_output::CliSignatureVerificationStatus,
3 base64::{prelude::BASE64_STANDARD, Engine},
4 chrono::{DateTime, Local, SecondsFormat, TimeZone, Utc},
5 console::style,
6 indicatif::{ProgressBar, ProgressStyle},
7 solana_cli_config::SettingType,
8 solana_sdk::{
9 clock::UnixTimestamp,
10 hash::Hash,
11 instruction::CompiledInstruction,
12 message::v0::MessageAddressTableLookup,
13 native_token::lamports_to_sol,
14 program_utils::limited_deserialize,
15 pubkey::Pubkey,
16 reserved_account_keys::ReservedAccountKeys,
17 signature::Signature,
18 stake,
19 transaction::{TransactionError, TransactionVersion, VersionedTransaction},
20 },
21 solana_transaction_status::{
22 Rewards, UiReturnDataEncoding, UiTransactionReturnData, UiTransactionStatusMeta,
23 },
24 spl_memo::{id as spl_memo_id, v1::id as spl_memo_v1_id},
25 std::{collections::HashMap, fmt, io, time::Duration},
26};
27
28#[derive(Clone, Debug)]
29pub struct BuildBalanceMessageConfig {
30 pub use_lamports_unit: bool,
31 pub show_unit: bool,
32 pub trim_trailing_zeros: bool,
33}
34
35impl Default for BuildBalanceMessageConfig {
36 fn default() -> Self {
37 Self {
38 use_lamports_unit: false,
39 show_unit: true,
40 trim_trailing_zeros: true,
41 }
42 }
43}
44
45fn is_memo_program(k: &Pubkey) -> bool {
46 let k_str = k.to_string();
47 (k_str == spl_memo_v1_id().to_string()) || (k_str == spl_memo_id().to_string())
48}
49
50pub fn build_balance_message_with_config(
51 lamports: u64,
52 config: &BuildBalanceMessageConfig,
53) -> String {
54 let value = if config.use_lamports_unit {
55 lamports.to_string()
56 } else {
57 let sol = lamports_to_sol(lamports);
58 let sol_str = format!("{sol:.9}");
59 if config.trim_trailing_zeros {
60 sol_str
61 .trim_end_matches('0')
62 .trim_end_matches('.')
63 .to_string()
64 } else {
65 sol_str
66 }
67 };
68 let unit = if config.show_unit {
69 if config.use_lamports_unit {
70 let ess = if lamports == 1 { "" } else { "s" };
71 format!(" lamport{ess}")
72 } else {
73 " SOL".to_string()
74 }
75 } else {
76 "".to_string()
77 };
78 format!("{value}{unit}")
79}
80
81pub fn build_balance_message(lamports: u64, use_lamports_unit: bool, show_unit: bool) -> String {
82 build_balance_message_with_config(
83 lamports,
84 &BuildBalanceMessageConfig {
85 use_lamports_unit,
86 show_unit,
87 ..BuildBalanceMessageConfig::default()
88 },
89 )
90}
91
92pub fn println_name_value(name: &str, value: &str) {
94 let styled_value = if value.is_empty() {
95 style("(not set)").italic()
96 } else {
97 style(value)
98 };
99 println!("{} {}", style(name).bold(), styled_value);
100}
101
102pub fn writeln_name_value(f: &mut dyn fmt::Write, name: &str, value: &str) -> fmt::Result {
103 let styled_value = if value.is_empty() {
104 style("(not set)").italic()
105 } else {
106 style(value)
107 };
108 writeln!(f, "{} {}", style(name).bold(), styled_value)
109}
110
111pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) {
112 let description = match setting_type {
113 SettingType::Explicit => "",
114 SettingType::Computed => "(computed)",
115 SettingType::SystemDefault => "(default)",
116 };
117
118 println!(
119 "{} {} {}",
120 style(name).bold(),
121 style(value),
122 style(description).italic(),
123 );
124}
125
126pub fn format_labeled_address(pubkey: &str, address_labels: &HashMap<String, String>) -> String {
127 let label = address_labels.get(pubkey);
128 match label {
129 Some(label) => format!(
130 "{:.31} ({:.4}..{})",
131 label,
132 pubkey,
133 pubkey.split_at(pubkey.len() - 4).1
134 ),
135 None => pubkey.to_string(),
136 }
137}
138
139pub fn println_signers(
140 blockhash: &Hash,
141 signers: &[String],
142 absent: &[String],
143 bad_sig: &[String],
144) {
145 println!();
146 println!("Blockhash: {blockhash}");
147 if !signers.is_empty() {
148 println!("Signers (Pubkey=Signature):");
149 signers.iter().for_each(|signer| println!(" {signer}"))
150 }
151 if !absent.is_empty() {
152 println!("Absent Signers (Pubkey):");
153 absent.iter().for_each(|pubkey| println!(" {pubkey}"))
154 }
155 if !bad_sig.is_empty() {
156 println!("Bad Signatures (Pubkey):");
157 bad_sig.iter().for_each(|pubkey| println!(" {pubkey}"))
158 }
159 println!();
160}
161
162struct CliAccountMeta {
163 is_signer: bool,
164 is_writable: bool,
165 is_invoked: bool,
166}
167
168fn format_account_mode(meta: CliAccountMeta) -> String {
169 format!(
170 "{}r{}{}", if meta.is_signer {
172 "s" } else {
174 "-"
175 },
176 if meta.is_writable {
177 "w" } else {
179 "-"
180 },
181 if meta.is_invoked {
184 "x"
185 } else {
186 "-"
189 },
190 )
191}
192
193fn write_transaction<W: io::Write>(
194 w: &mut W,
195 transaction: &VersionedTransaction,
196 transaction_status: Option<&UiTransactionStatusMeta>,
197 prefix: &str,
198 sigverify_status: Option<&[CliSignatureVerificationStatus]>,
199 block_time: Option<UnixTimestamp>,
200 timezone: CliTimezone,
201) -> io::Result<()> {
202 write_block_time(w, block_time, timezone, prefix)?;
203
204 let message = &transaction.message;
205 let account_keys: Vec<AccountKeyType> = {
206 let static_keys_iter = message
207 .static_account_keys()
208 .iter()
209 .map(AccountKeyType::Known);
210 let dynamic_keys: Vec<AccountKeyType> = message
211 .address_table_lookups()
212 .map(transform_lookups_to_unknown_keys)
213 .unwrap_or_default();
214 static_keys_iter.chain(dynamic_keys).collect()
215 };
216
217 write_version(w, transaction.version(), prefix)?;
218 write_recent_blockhash(w, message.recent_blockhash(), prefix)?;
219 write_signatures(w, &transaction.signatures, sigverify_status, prefix)?;
220
221 let reserved_account_keys = ReservedAccountKeys::new_all_activated().active;
222 for (account_index, account) in account_keys.iter().enumerate() {
223 let account_meta = CliAccountMeta {
224 is_signer: message.is_signer(account_index),
225 is_writable: message.is_maybe_writable(account_index, Some(&reserved_account_keys)),
226 is_invoked: message.is_invoked(account_index),
227 };
228
229 let is_fee_payer = account_index == 0;
230 write_account(
231 w,
232 account_index,
233 *account,
234 format_account_mode(account_meta),
235 is_fee_payer,
236 prefix,
237 )?;
238 }
239
240 for (instruction_index, instruction) in message.instructions().iter().enumerate() {
241 let program_pubkey = account_keys[instruction.program_id_index as usize];
242 let instruction_accounts = instruction
243 .accounts
244 .iter()
245 .map(|account_index| (account_keys[*account_index as usize], *account_index));
246
247 write_instruction(
248 w,
249 instruction_index,
250 program_pubkey,
251 instruction,
252 instruction_accounts,
253 prefix,
254 )?;
255 }
256
257 if let Some(address_table_lookups) = message.address_table_lookups() {
258 write_address_table_lookups(w, address_table_lookups, prefix)?;
259 }
260
261 if let Some(transaction_status) = transaction_status {
262 write_status(w, &transaction_status.status, prefix)?;
263 write_fees(w, transaction_status.fee, prefix)?;
264 write_balances(w, transaction_status, prefix)?;
265 write_compute_units_consumed(
266 w,
267 transaction_status.compute_units_consumed.clone().into(),
268 prefix,
269 )?;
270 write_log_messages(w, transaction_status.log_messages.as_ref().into(), prefix)?;
271 write_return_data(w, transaction_status.return_data.as_ref().into(), prefix)?;
272 write_rewards(w, transaction_status.rewards.as_ref().into(), prefix)?;
273 } else {
274 writeln!(w, "{prefix}Status: Unavailable")?;
275 }
276
277 Ok(())
278}
279
280fn transform_lookups_to_unknown_keys(lookups: &[MessageAddressTableLookup]) -> 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) = limited_deserialize::<
444 solana_vote_program::vote_instruction::VoteInstruction,
445 >(&instruction.data)
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) =
452 limited_deserialize::<stake::instruction::StakeInstruction>(&instruction.data)
453 {
454 writeln!(w, "{prefix} {stake_instruction:?}")?;
455 raw = false;
456 }
457 } else if program_pubkey == &solana_sdk::system_program::id() {
458 if let Ok(system_instruction) = limited_deserialize::<
459 solana_sdk::system_instruction::SystemInstruction,
460 >(&instruction.data)
461 {
462 writeln!(w, "{prefix} {system_instruction:?}")?;
463 raw = false;
464 }
465 } else if is_memo_program(program_pubkey) {
466 if let Ok(s) = std::str::from_utf8(&instruction.data) {
467 writeln!(w, "{prefix} Data: \"{s}\"")?;
468 raw = false;
469 }
470 }
471 }
472
473 if raw {
474 writeln!(w, "{} Data: {:?}", prefix, instruction.data)?;
475 }
476
477 Ok(())
478}
479
480fn write_address_table_lookups<W: io::Write>(
481 w: &mut W,
482 address_table_lookups: &[MessageAddressTableLookup],
483 prefix: &str,
484) -> io::Result<()> {
485 for (lookup_index, lookup) in address_table_lookups.iter().enumerate() {
486 writeln!(w, "{prefix}Address Table Lookup {lookup_index}",)?;
487 writeln!(w, "{} Table Account: {}", prefix, lookup.account_key,)?;
488 writeln!(
489 w,
490 "{} Writable Indexes: {:?}",
491 prefix,
492 &lookup.writable_indexes[..],
493 )?;
494 writeln!(
495 w,
496 "{} Readonly Indexes: {:?}",
497 prefix,
498 &lookup.readonly_indexes[..],
499 )?;
500 }
501 Ok(())
502}
503
504fn write_rewards<W: io::Write>(
505 w: &mut W,
506 rewards: Option<&Rewards>,
507 prefix: &str,
508) -> io::Result<()> {
509 if let Some(rewards) = rewards {
510 if !rewards.is_empty() {
511 writeln!(w, "{prefix}Rewards:",)?;
512 writeln!(
513 w,
514 "{} {:<44} {:^15} {:<16} {:<20}",
515 prefix, "Address", "Type", "Amount", "New Balance"
516 )?;
517 for reward in rewards {
518 let sign = if reward.lamports < 0 { "-" } else { "" };
519 writeln!(
520 w,
521 "{} {:<44} {:^15} {}◎{:<14.9} ◎{:<18.9}",
522 prefix,
523 reward.pubkey,
524 if let Some(reward_type) = reward.reward_type {
525 format!("{reward_type}")
526 } else {
527 "-".to_string()
528 },
529 sign,
530 lamports_to_sol(reward.lamports.unsigned_abs()),
531 lamports_to_sol(reward.post_balance)
532 )?;
533 }
534 }
535 }
536 Ok(())
537}
538
539fn write_status<W: io::Write>(
540 w: &mut W,
541 transaction_status: &Result<(), TransactionError>,
542 prefix: &str,
543) -> io::Result<()> {
544 writeln!(
545 w,
546 "{}Status: {}",
547 prefix,
548 match transaction_status {
549 Ok(_) => "Ok".into(),
550 Err(err) => err.to_string(),
551 }
552 )
553}
554
555fn write_fees<W: io::Write>(w: &mut W, transaction_fee: u64, prefix: &str) -> io::Result<()> {
556 writeln!(w, "{} Fee: ◎{}", prefix, lamports_to_sol(transaction_fee))
557}
558
559fn write_balances<W: io::Write>(
560 w: &mut W,
561 transaction_status: &UiTransactionStatusMeta,
562 prefix: &str,
563) -> io::Result<()> {
564 assert_eq!(
565 transaction_status.pre_balances.len(),
566 transaction_status.post_balances.len()
567 );
568 for (i, (pre, post)) in transaction_status
569 .pre_balances
570 .iter()
571 .zip(transaction_status.post_balances.iter())
572 .enumerate()
573 {
574 if pre == post {
575 writeln!(
576 w,
577 "{} Account {} balance: ◎{}",
578 prefix,
579 i,
580 lamports_to_sol(*pre)
581 )?;
582 } else {
583 writeln!(
584 w,
585 "{} Account {} balance: ◎{} -> ◎{}",
586 prefix,
587 i,
588 lamports_to_sol(*pre),
589 lamports_to_sol(*post)
590 )?;
591 }
592 }
593 Ok(())
594}
595
596fn write_return_data<W: io::Write>(
597 w: &mut W,
598 return_data: Option<&UiTransactionReturnData>,
599 prefix: &str,
600) -> io::Result<()> {
601 if let Some(return_data) = return_data {
602 let (data, encoding) = &return_data.data;
603 let raw_return_data = match encoding {
604 UiReturnDataEncoding::Base64 => BASE64_STANDARD.decode(data).map_err(|err| {
605 io::Error::new(
606 io::ErrorKind::Other,
607 format!("could not parse data as {encoding:?}: {err:?}"),
608 )
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_sdk::{
727 message::{
728 v0::{self, LoadedAddresses},
729 Message as LegacyMessage, MessageHeader, VersionedMessage,
730 },
731 pubkey::Pubkey,
732 signature::{Keypair, Signer},
733 transaction::Transaction,
734 transaction_context::TransactionReturnData,
735 },
736 solana_transaction_status::{Reward, RewardType, TransactionStatusMeta},
737 std::io::BufWriter,
738 };
739
740 fn new_test_keypair() -> Keypair {
741 let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap();
742 let public = ed25519_dalek::PublicKey::from(&secret);
743 let keypair = ed25519_dalek::Keypair { secret, public };
744 Keypair::from_bytes(&keypair.to_bytes()).unwrap()
745 }
746
747 fn new_test_v0_transaction() -> VersionedTransaction {
748 let keypair = new_test_keypair();
749 let account_key = Pubkey::new_from_array([1u8; 32]);
750 let address_table_key = Pubkey::new_from_array([2u8; 32]);
751 VersionedTransaction::try_new(
752 VersionedMessage::V0(v0::Message {
753 header: MessageHeader {
754 num_required_signatures: 1,
755 num_readonly_signed_accounts: 0,
756 num_readonly_unsigned_accounts: 1,
757 },
758 recent_blockhash: Hash::default(),
759 account_keys: vec![keypair.pubkey(), account_key],
760 address_table_lookups: vec![MessageAddressTableLookup {
761 account_key: address_table_key,
762 writable_indexes: vec![0],
763 readonly_indexes: vec![1],
764 }],
765 instructions: vec![CompiledInstruction::new_from_raw_parts(
766 3,
767 vec![],
768 vec![1, 2],
769 )],
770 }),
771 &[&keypair],
772 )
773 .unwrap()
774 }
775
776 #[test]
777 fn test_write_legacy_transaction() {
778 let keypair = new_test_keypair();
779 let account_key = Pubkey::new_from_array([1u8; 32]);
780 let transaction = VersionedTransaction::from(Transaction::new(
781 &[&keypair],
782 LegacyMessage {
783 header: MessageHeader {
784 num_required_signatures: 1,
785 num_readonly_signed_accounts: 0,
786 num_readonly_unsigned_accounts: 1,
787 },
788 recent_blockhash: Hash::default(),
789 account_keys: vec![keypair.pubkey(), account_key],
790 instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])],
791 },
792 Hash::default(),
793 ));
794
795 let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction);
796 let meta = TransactionStatusMeta {
797 status: Ok(()),
798 fee: 5000,
799 pre_balances: vec![5000, 10_000],
800 post_balances: vec![0, 9_900],
801 inner_instructions: None,
802 log_messages: Some(vec!["Test message".to_string()]),
803 pre_token_balances: None,
804 post_token_balances: None,
805 rewards: Some(vec![Reward {
806 pubkey: account_key.to_string(),
807 lamports: -100,
808 post_balance: 9_900,
809 reward_type: Some(RewardType::Rent),
810 commission: None,
811 }]),
812 loaded_addresses: LoadedAddresses::default(),
813 return_data: Some(TransactionReturnData {
814 program_id: Pubkey::new_from_array([2u8; 32]),
815 data: vec![1, 2, 3],
816 }),
817 compute_units_consumed: Some(1234u64),
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 };
898
899 let output = {
900 let mut write_buffer = BufWriter::new(Vec::new());
901 write_transaction(
902 &mut write_buffer,
903 &versioned_tx,
904 Some(&meta.into()),
905 "",
906 Some(&sigverify_status),
907 Some(1628633791),
908 CliTimezone::Utc,
909 )
910 .unwrap();
911 let bytes = write_buffer.into_inner().unwrap();
912 String::from_utf8(bytes).unwrap()
913 };
914
915 assert_eq!(
916 output,
917 r"Block Time: 2021-08-10T22:16:31Z
918Version: 0
919Recent Blockhash: 11111111111111111111111111111111
920Signature 0: 5iEy3TT3ZhTA1NkuCY8GrQGNVY8d5m1bpjdh5FT3Ca4Py81fMipAZjafDuKJKrkw5q5UAAd8oPcgZ4nyXpHt4Fp7 (pass)
921Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
922Account 1: -r-- 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
923Account 2: -rw- Unknown Address (uses lookup 0 and index 0)
924Account 3: -r-x Unknown Address (uses lookup 0 and index 1)
925Instruction 0
926 Program: Unknown Address (uses lookup 0 and index 1) (3)
927 Account 0: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
928 Account 1: Unknown Address (uses lookup 0 and index 0) (2)
929 Data: []
930Address Table Lookup 0
931 Table Account: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
932 Writable Indexes: [0]
933 Readonly Indexes: [1]
934Status: Ok
935 Fee: ◎0.000005
936 Account 0 balance: ◎0.000005 -> ◎0
937 Account 1 balance: ◎0.00001
938 Account 2 balance: ◎0.000015 -> ◎0.0000149
939 Account 3 balance: ◎0.00002
940Compute Units Consumed: 2345
941Log Messages:
942 Test message
943Return Data from Program 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR:
944 Length: 3 (0x3) bytes
9450000: 01 02 03 ...
946Rewards:
947 Address Type Amount New Balance \0
948 CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8 rent -◎0.000000100 ◎0.000014900 \0
949".replace("\\0", "") );
951 }
952
953 #[test]
954 fn test_format_labeled_address() {
955 let pubkey = Pubkey::default().to_string();
956 let mut address_labels = HashMap::new();
957
958 assert_eq!(format_labeled_address(&pubkey, &address_labels), pubkey);
959
960 address_labels.insert(pubkey.to_string(), "Default Address".to_string());
961 assert_eq!(
962 &format_labeled_address(&pubkey, &address_labels),
963 "Default Address (1111..1111)"
964 );
965
966 address_labels.insert(
967 pubkey.to_string(),
968 "abcdefghijklmnopqrstuvwxyz1234567890".to_string(),
969 );
970 assert_eq!(
971 &format_labeled_address(&pubkey, &address_labels),
972 "abcdefghijklmnopqrstuvwxyz12345 (1111..1111)"
973 );
974 }
975
976 #[test]
977 fn test_unix_timestamp_to_string() {
978 assert_eq!(unix_timestamp_to_string(1628633791), "2021-08-10T22:16:31Z");
979 }
980}