From 021135978d4ad49f6111dc41a171902c0dfed1fc Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Fri, 11 Mar 2022 10:49:53 +0800 Subject: [PATCH] Refactor: Split up cli transaction display methods (#23547) --- Cargo.lock | 1 + cli-output/Cargo.toml | 3 + cli-output/src/cli_output.rs | 4 +- cli-output/src/display.rs | 589 ++++++++++++++++++++++++----------- cli/src/cluster_query.rs | 2 +- ledger-tool/src/bigtable.rs | 6 +- ledger-tool/src/main.rs | 6 +- 7 files changed, 420 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index acc3aa738..e18f9f130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4540,6 +4540,7 @@ dependencies = [ "chrono", "clap 2.33.3", "console", + "ed25519-dalek", "humantime", "indicatif", "serde", diff --git a/cli-output/Cargo.toml b/cli-output/Cargo.toml index 83befdc3e..a71509646 100644 --- a/cli-output/Cargo.toml +++ b/cli-output/Cargo.toml @@ -27,5 +27,8 @@ solana-transaction-status = { path = "../transaction-status", version = "=1.10.2 solana-vote-program = { path = "../programs/vote", version = "=1.10.2" } spl-memo = { version = "=3.0.1", features = ["no-entrypoint"] } +[dev-dependencies] +ed25519-dalek = "=1.0.1" + [package.metadata.docs.rs] targets = ["x86_64-unknown-linux-gnu"] diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 6ef3d893f..c9e0310ca 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -2335,7 +2335,7 @@ impl fmt::Display for CliBlock { writeln_transaction( f, &transaction_with_meta.transaction.decode().unwrap(), - &transaction_with_meta.meta, + transaction_with_meta.meta.as_ref(), " ", None, None, @@ -2369,7 +2369,7 @@ impl fmt::Display for CliTransaction { writeln_transaction( f, &self.decoded_transaction, - &self.meta, + self.meta.as_ref(), &self.prefix, if !self.sigverify_status.is_empty() { Some(&self.sigverify_status) diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index 6e2e5b13d..c7c222b68 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -4,10 +4,17 @@ use { console::style, indicatif::{ProgressBar, ProgressStyle}, solana_sdk::{ - clock::UnixTimestamp, hash::Hash, message::Message, native_token::lamports_to_sol, - program_utils::limited_deserialize, pubkey::Pubkey, stake, transaction::Transaction, + clock::UnixTimestamp, + hash::Hash, + instruction::CompiledInstruction, + native_token::lamports_to_sol, + program_utils::limited_deserialize, + pubkey::Pubkey, + signature::Signature, + stake, + transaction::{Transaction, TransactionError}, }, - solana_transaction_status::UiTransactionStatusMeta, + solana_transaction_status::{Rewards, UiTransactionStatusMeta}, spl_memo::{id as spl_memo_id, v1::id as spl_memo_v1_id}, std::{collections::HashMap, fmt, io}, }; @@ -131,22 +138,28 @@ pub fn println_signers( println!(); } -fn format_account_mode(message: &Message, index: usize) -> String { +struct CliAccountMeta { + is_signer: bool, + is_writable: bool, + is_invoked: bool, +} + +fn format_account_mode(meta: CliAccountMeta) -> String { format!( "{}r{}{}", // accounts are always readable... - if message.is_signer(index) { + if meta.is_signer { "s" // stands for signer } else { "-" }, - if message.is_writable(index) { + if meta.is_writable { "w" // comment for consistent rust fmt (no joking; lol) } else { "-" }, // account may be executable on-chain while not being // designated as a program-id in the message - if message.maybe_executable(index) { + if meta.is_invoked { "x" } else { // programs to be executed via CPI cannot be identified as @@ -156,202 +169,66 @@ fn format_account_mode(message: &Message, index: usize) -> String { ) } -pub fn write_transaction( +fn write_transaction( w: &mut W, transaction: &Transaction, - transaction_status: &Option, + transaction_status: Option<&UiTransactionStatusMeta>, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, block_time: Option, + timezone: CliTimezone, ) -> io::Result<()> { + write_block_time(w, block_time, timezone, prefix)?; + let message = &transaction.message; - if let Some(block_time) = block_time { - writeln!( - w, - "{}Block Time: {:?}", - prefix, - Local.timestamp(block_time, 0) - )?; - } - writeln!( - w, - "{}Recent Blockhash: {:?}", - prefix, message.recent_blockhash - )?; - let sigverify_statuses = if let Some(sigverify_status) = sigverify_status { - sigverify_status - .iter() - .map(|s| format!(" ({})", s)) - .collect() - } else { - vec!["".to_string(); transaction.signatures.len()] - }; - for (signature_index, (signature, sigverify_status)) in transaction - .signatures - .iter() - .zip(&sigverify_statuses) - .enumerate() - { - writeln!( - w, - "{}Signature {}: {:?}{}", - prefix, signature_index, signature, sigverify_status, - )?; - } + write_recent_blockhash(w, &message.recent_blockhash, prefix)?; + write_signatures(w, &transaction.signatures, sigverify_status, prefix)?; + let mut fee_payer_index = None; for (account_index, account) in message.account_keys.iter().enumerate() { if fee_payer_index.is_none() && message.is_non_loader_key(account_index) { fee_payer_index = Some(account_index) } - writeln!( + + let account_meta = CliAccountMeta { + is_signer: message.is_signer(account_index), + is_writable: message.is_writable(account_index), + is_invoked: message.maybe_executable(account_index), + }; + + write_account( w, - "{}Account {}: {} {}{}", - prefix, account_index, - format_account_mode(message, account_index), account, - if Some(account_index) == fee_payer_index { - " (fee payer)" - } else { - "" - }, + format_account_mode(account_meta), + Some(account_index) == fee_payer_index, + prefix, )?; } + for (instruction_index, instruction) in message.instructions.iter().enumerate() { let program_pubkey = message.account_keys[instruction.program_id_index as usize]; - writeln!(w, "{}Instruction {}", prefix, instruction_index)?; - writeln!( + let instruction_accounts = instruction.accounts.iter().map(|account_index| { + let account_pubkey = &message.account_keys[*account_index as usize]; + (account_pubkey, *account_index) + }); + + write_instruction( w, - "{} Program: {} ({})", - prefix, program_pubkey, instruction.program_id_index + instruction_index, + &program_pubkey, + instruction, + instruction_accounts, + prefix, )?; - for (account_index, account) in instruction.accounts.iter().enumerate() { - let account_pubkey = message.account_keys[*account as usize]; - writeln!( - w, - "{} Account {}: {} ({})", - prefix, account_index, account_pubkey, account - )?; - } - - let mut raw = true; - if program_pubkey == solana_vote_program::id() { - if let Ok(vote_instruction) = limited_deserialize::< - solana_vote_program::vote_instruction::VoteInstruction, - >(&instruction.data) - { - writeln!(w, "{} {:?}", prefix, vote_instruction)?; - raw = false; - } - } else if program_pubkey == stake::program::id() { - if let Ok(stake_instruction) = - limited_deserialize::(&instruction.data) - { - writeln!(w, "{} {:?}", prefix, stake_instruction)?; - raw = false; - } - } else if program_pubkey == solana_sdk::system_program::id() { - if let Ok(system_instruction) = limited_deserialize::< - solana_sdk::system_instruction::SystemInstruction, - >(&instruction.data) - { - writeln!(w, "{} {:?}", prefix, system_instruction)?; - raw = false; - } - } else if is_memo_program(&program_pubkey) { - if let Ok(s) = std::str::from_utf8(&instruction.data) { - writeln!(w, "{} Data: \"{}\"", prefix, s)?; - raw = false; - } - } - - if raw { - writeln!(w, "{} Data: {:?}", prefix, instruction.data)?; - } } if let Some(transaction_status) = transaction_status { - writeln!( - w, - "{}Status: {}", - prefix, - match &transaction_status.status { - Ok(_) => "Ok".into(), - Err(err) => err.to_string(), - } - )?; - writeln!( - w, - "{} Fee: ◎{}", - prefix, - lamports_to_sol(transaction_status.fee) - )?; - assert_eq!( - transaction_status.pre_balances.len(), - transaction_status.post_balances.len() - ); - for (i, (pre, post)) in transaction_status - .pre_balances - .iter() - .zip(transaction_status.post_balances.iter()) - .enumerate() - { - if pre == post { - writeln!( - w, - "{} Account {} balance: ◎{}", - prefix, - i, - lamports_to_sol(*pre) - )?; - } else { - writeln!( - w, - "{} Account {} balance: ◎{} -> ◎{}", - prefix, - i, - lamports_to_sol(*pre), - lamports_to_sol(*post) - )?; - } - } - - if let Some(log_messages) = &transaction_status.log_messages { - if !log_messages.is_empty() { - writeln!(w, "{}Log Messages:", prefix,)?; - for log_message in log_messages { - writeln!(w, "{} {}", prefix, log_message)?; - } - } - } - - if let Some(rewards) = &transaction_status.rewards { - if !rewards.is_empty() { - writeln!(w, "{}Rewards:", prefix,)?; - writeln!( - w, - "{} {:<44} {:^15} {:<15} {:<20}", - prefix, "Address", "Type", "Amount", "New Balance" - )?; - for reward in rewards { - let sign = if reward.lamports < 0 { "-" } else { "" }; - writeln!( - w, - "{} {:<44} {:^15} {}◎{:<14.9} ◎{:<18.9}", - prefix, - reward.pubkey, - if let Some(reward_type) = reward.reward_type { - format!("{}", reward_type) - } else { - "-".to_string() - }, - sign, - lamports_to_sol(reward.lamports.abs() as u64), - lamports_to_sol(reward.post_balance) - )?; - } - } - } + write_status(w, &transaction_status.status, prefix)?; + write_fees(w, transaction_status.fee, prefix)?; + write_balances(w, transaction_status, prefix)?; + write_log_messages(w, transaction_status.log_messages.as_ref(), prefix)?; + write_rewards(w, transaction_status.rewards.as_ref(), prefix)?; } else { writeln!(w, "{}Status: Unavailable", prefix)?; } @@ -359,9 +236,252 @@ pub fn write_transaction( Ok(()) } +enum CliTimezone { + Local, + #[allow(dead_code)] + Utc, +} + +fn write_block_time( + w: &mut W, + block_time: Option, + timezone: CliTimezone, + prefix: &str, +) -> io::Result<()> { + if let Some(block_time) = block_time { + let block_time_output = match timezone { + CliTimezone::Local => format!("{:?}", Local.timestamp(block_time, 0)), + CliTimezone::Utc => format!("{:?}", Utc.timestamp(block_time, 0)), + }; + writeln!(w, "{}Block Time: {}", prefix, block_time_output,)?; + } + Ok(()) +} + +fn write_recent_blockhash( + w: &mut W, + recent_blockhash: &Hash, + prefix: &str, +) -> io::Result<()> { + writeln!(w, "{}Recent Blockhash: {:?}", prefix, recent_blockhash) +} + +fn write_signatures( + w: &mut W, + signatures: &[Signature], + sigverify_status: Option<&[CliSignatureVerificationStatus]>, + prefix: &str, +) -> io::Result<()> { + let sigverify_statuses = if let Some(sigverify_status) = sigverify_status { + sigverify_status + .iter() + .map(|s| format!(" ({})", s)) + .collect() + } else { + vec!["".to_string(); signatures.len()] + }; + for (signature_index, (signature, sigverify_status)) in + signatures.iter().zip(&sigverify_statuses).enumerate() + { + writeln!( + w, + "{}Signature {}: {:?}{}", + prefix, signature_index, signature, sigverify_status, + )?; + } + Ok(()) +} + +fn write_account( + w: &mut W, + account_index: usize, + account_address: &Pubkey, + account_mode: String, + is_fee_payer: bool, + prefix: &str, +) -> io::Result<()> { + writeln!( + w, + "{}Account {}: {} {}{}", + prefix, + account_index, + account_mode, + account_address, + if is_fee_payer { " (fee payer)" } else { "" }, + ) +} + +fn write_instruction<'a, W: io::Write>( + w: &mut W, + instruction_index: usize, + program_pubkey: &Pubkey, + instruction: &CompiledInstruction, + instruction_accounts: impl Iterator, + prefix: &str, +) -> io::Result<()> { + writeln!(w, "{}Instruction {}", prefix, instruction_index)?; + writeln!( + w, + "{} Program: {} ({})", + prefix, program_pubkey, instruction.program_id_index + )?; + for (index, (account_address, account_index)) in instruction_accounts.enumerate() { + writeln!( + w, + "{} Account {}: {} ({})", + prefix, index, account_address, account_index + )?; + } + + let mut raw = true; + if program_pubkey == &solana_vote_program::id() { + if let Ok(vote_instruction) = limited_deserialize::< + solana_vote_program::vote_instruction::VoteInstruction, + >(&instruction.data) + { + writeln!(w, "{} {:?}", prefix, vote_instruction)?; + raw = false; + } + } else if program_pubkey == &stake::program::id() { + if let Ok(stake_instruction) = + limited_deserialize::(&instruction.data) + { + writeln!(w, "{} {:?}", prefix, stake_instruction)?; + raw = false; + } + } else if program_pubkey == &solana_sdk::system_program::id() { + if let Ok(system_instruction) = limited_deserialize::< + solana_sdk::system_instruction::SystemInstruction, + >(&instruction.data) + { + writeln!(w, "{} {:?}", prefix, system_instruction)?; + raw = false; + } + } else if is_memo_program(program_pubkey) { + if let Ok(s) = std::str::from_utf8(&instruction.data) { + writeln!(w, "{} Data: \"{}\"", prefix, s)?; + raw = false; + } + } + + if raw { + writeln!(w, "{} Data: {:?}", prefix, instruction.data)?; + } + + Ok(()) +} + +fn write_rewards( + w: &mut W, + rewards: Option<&Rewards>, + prefix: &str, +) -> io::Result<()> { + if let Some(rewards) = rewards { + if !rewards.is_empty() { + writeln!(w, "{}Rewards:", prefix,)?; + writeln!( + w, + "{} {:<44} {:^15} {:<16} {:<20}", + prefix, "Address", "Type", "Amount", "New Balance" + )?; + for reward in rewards { + let sign = if reward.lamports < 0 { "-" } else { "" }; + writeln!( + w, + "{} {:<44} {:^15} {}◎{:<14.9} ◎{:<18.9}", + prefix, + reward.pubkey, + if let Some(reward_type) = reward.reward_type { + format!("{}", reward_type) + } else { + "-".to_string() + }, + sign, + lamports_to_sol(reward.lamports.abs() as u64), + lamports_to_sol(reward.post_balance) + )?; + } + } + } + Ok(()) +} + +fn write_status( + w: &mut W, + transaction_status: &Result<(), TransactionError>, + prefix: &str, +) -> io::Result<()> { + writeln!( + w, + "{}Status: {}", + prefix, + match transaction_status { + Ok(_) => "Ok".into(), + Err(err) => err.to_string(), + } + ) +} + +fn write_fees(w: &mut W, transaction_fee: u64, prefix: &str) -> io::Result<()> { + writeln!(w, "{} Fee: ◎{}", prefix, lamports_to_sol(transaction_fee)) +} + +fn write_balances( + w: &mut W, + transaction_status: &UiTransactionStatusMeta, + prefix: &str, +) -> io::Result<()> { + assert_eq!( + transaction_status.pre_balances.len(), + transaction_status.post_balances.len() + ); + for (i, (pre, post)) in transaction_status + .pre_balances + .iter() + .zip(transaction_status.post_balances.iter()) + .enumerate() + { + if pre == post { + writeln!( + w, + "{} Account {} balance: ◎{}", + prefix, + i, + lamports_to_sol(*pre) + )?; + } else { + writeln!( + w, + "{} Account {} balance: ◎{} -> ◎{}", + prefix, + i, + lamports_to_sol(*pre), + lamports_to_sol(*post) + )?; + } + } + Ok(()) +} + +fn write_log_messages( + w: &mut W, + log_messages: Option<&Vec>, + prefix: &str, +) -> io::Result<()> { + if let Some(log_messages) = log_messages { + if !log_messages.is_empty() { + writeln!(w, "{}Log Messages:", prefix,)?; + for log_message in log_messages { + writeln!(w, "{} {}", prefix, log_message)?; + } + } + } + Ok(()) +} + pub fn println_transaction( transaction: &Transaction, - transaction_status: &Option, + transaction_status: Option<&UiTransactionStatusMeta>, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, block_time: Option, @@ -374,6 +494,7 @@ pub fn println_transaction( prefix, sigverify_status, block_time, + CliTimezone::Local, ) .is_ok() { @@ -386,22 +507,23 @@ pub fn println_transaction( pub fn writeln_transaction( f: &mut dyn fmt::Write, transaction: &Transaction, - transaction_status: &Option, + transaction_status: Option<&UiTransactionStatusMeta>, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, block_time: Option, ) -> fmt::Result { let mut w = Vec::new(); - if write_transaction( + let write_result = write_transaction( &mut w, transaction, transaction_status, prefix, sigverify_status, block_time, - ) - .is_ok() - { + CliTimezone::Local, + ); + + if write_result.is_ok() { if let Ok(s) = String::from_utf8(w) { write!(f, "{}", s)?; } @@ -427,7 +549,102 @@ pub fn unix_timestamp_to_string(unix_timestamp: UnixTimestamp) -> String { #[cfg(test)] mod test { - use {super::*, solana_sdk::pubkey::Pubkey}; + use { + super::*, + solana_sdk::{ + message::{v0::LoadedAddresses, Message as LegacyMessage, MessageHeader}, + pubkey::Pubkey, + signature::{Keypair, Signer}, + }, + solana_transaction_status::{Reward, RewardType, TransactionStatusMeta}, + std::io::BufWriter, + }; + + fn test_keypair() -> Keypair { + let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap(); + let public = ed25519_dalek::PublicKey::from(&secret); + let keypair = ed25519_dalek::Keypair { secret, public }; + Keypair::from_bytes(&keypair.to_bytes()).unwrap() + } + + #[test] + fn test_write_transaction() { + let keypair = test_keypair(); + let account_key = Pubkey::new_from_array([1u8; 32]); + let transaction = Transaction::new( + &[&keypair], + LegacyMessage { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + recent_blockhash: Hash::default(), + account_keys: vec![keypair.pubkey(), account_key], + instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])], + }, + Hash::default(), + ); + + let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction); + let meta = TransactionStatusMeta { + status: Ok(()), + fee: 5000, + pre_balances: vec![5000, 10_000], + post_balances: vec![0, 9_900], + inner_instructions: None, + log_messages: Some(vec!["Test message".to_string()]), + pre_token_balances: None, + post_token_balances: None, + rewards: Some(vec![Reward { + pubkey: account_key.to_string(), + lamports: -100, + post_balance: 9_900, + reward_type: Some(RewardType::Rent), + commission: None, + }]), + loaded_addresses: LoadedAddresses::default(), + }; + + let output = { + let mut write_buffer = BufWriter::new(Vec::new()); + write_transaction( + &mut write_buffer, + &transaction, + Some(&meta.into()), + "", + Some(&sigverify_status), + Some(1628633791), + CliTimezone::Utc, + ) + .unwrap(); + let bytes = write_buffer.into_inner().unwrap(); + String::from_utf8(bytes).unwrap() + }; + + assert_eq!( + output, + r#"Block Time: 2021-08-10T22:16:31Z +Recent Blockhash: 11111111111111111111111111111111 +Signature 0: 5pkjrE4VBa3Bu9CMKXgh1U345cT1gGo8QBVRTzHAo6gHeiPae5BTbShP15g6NgqRMNqu8Qrhph1ATmrfC1Ley3rx (pass) +Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer) +Account 1: -r-x 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi +Instruction 0 + Program: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1) + Account 0: 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (0) + Data: [] +Status: Ok + Fee: ◎0.000005 + Account 0 balance: ◎0.000005 -> ◎0 + Account 1 balance: ◎0.00001 -> ◎0.0000099 +Log Messages: + Test message +Rewards: + Address Type Amount New Balance \0 + 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi rent -◎0.000000100 ◎0.000009900 \0 +"#.replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI + ); + } #[test] fn test_format_labeled_address() { diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index ea2872c68..24aa2442e 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -2052,7 +2052,7 @@ pub fn process_transaction_history( .transaction .decode() .expect("Successful decode"), - &confirmed_transaction.transaction.meta, + confirmed_transaction.transaction.meta.as_ref(), " ", None, None, diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs index e1c15c570..53a204548 100644 --- a/ledger-tool/src/bigtable.rs +++ b/ledger-tool/src/bigtable.rs @@ -267,7 +267,11 @@ pub async fn transaction_history( Some(transaction_with_meta) => { println_transaction( &transaction_with_meta.transaction, - &transaction_with_meta.meta.clone().map(|m| m.into()), + transaction_with_meta + .meta + .clone() + .map(|m| m.into()) + .as_ref(), " ", None, None, diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 417602e07..5c3e98e8b 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -158,7 +158,11 @@ fn output_entry( if let Some(legacy_tx) = transaction.into_legacy_transaction() { solana_cli_output::display::println_transaction( - &legacy_tx, &tx_status, " ", None, None, + &legacy_tx, + tx_status.as_ref(), + " ", + None, + None, ); } else { eprintln!(