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