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