diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 7bd5ac17b0..a3a55cf758 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -26,10 +26,13 @@ use { pubkey::Pubkey, signature::Signature, stake_history::StakeHistoryEntry, - transaction::Transaction, + transaction::{Transaction, TransactionError}, }, solana_stake_program::stake_state::{Authorized, Lockup}, - solana_transaction_status::EncodedConfirmedBlock, + solana_transaction_status::{ + EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus, + UiTransactionStatusMeta, + }, solana_vote_program::{ authorized_voters::AuthorizedVoters, vote_state::{BlockTimestamp, Lockout, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY}, @@ -1711,7 +1714,8 @@ pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly { } } -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub enum CliSignatureVerificationStatus { None, Pass, @@ -1743,6 +1747,7 @@ impl fmt::Display for CliSignatureVerificationStatus { } #[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct CliBlock { #[serde(flatten)] pub encoded_confirmed_block: EncodedConfirmedBlock, @@ -1830,12 +1835,103 @@ impl fmt::Display for CliBlock { &transaction_with_meta.meta, " ", None, + None, )?; } Ok(()) } } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliTransaction { + pub transaction: EncodedTransaction, + pub meta: Option, + pub block_time: Option, + #[serde(skip_serializing)] + pub slot: Option, + #[serde(skip_serializing)] + pub decoded_transaction: Transaction, + #[serde(skip_serializing)] + pub prefix: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub sigverify_status: Vec, +} + +impl QuietDisplay for CliTransaction {} +impl VerboseDisplay for CliTransaction {} + +impl fmt::Display for CliTransaction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln_transaction( + f, + &self.decoded_transaction, + &self.meta, + &self.prefix, + if !self.sigverify_status.is_empty() { + Some(&self.sigverify_status) + } else { + None + }, + self.block_time, + ) + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CliTransactionConfirmation { + pub confirmation_status: Option, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub transaction: Option, + #[serde(skip_serializing)] + pub get_transaction_error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub err: Option, +} + +impl QuietDisplay for CliTransactionConfirmation {} +impl VerboseDisplay for CliTransactionConfirmation { + fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result { + if let Some(transaction) = &self.transaction { + writeln!( + w, + "\nTransaction executed in slot {}:", + transaction.slot.expect("slot should exist") + )?; + write!(w, "{}", transaction)?; + } else if let Some(confirmation_status) = &self.confirmation_status { + if confirmation_status != &TransactionConfirmationStatus::Finalized { + writeln!(w)?; + writeln!( + w, + "Unable to get finalized transaction details: not yet finalized" + )?; + } else if let Some(err) = &self.get_transaction_error { + writeln!(w)?; + writeln!(w, "Unable to get finalized transaction details: {}", err)?; + } + } + writeln!(w)?; + write!(w, "{}", self) + } +} + +impl fmt::Display for CliTransactionConfirmation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self.confirmation_status { + None => write!(f, "Not found"), + Some(confirmation_status) => { + if let Some(err) = &self.err { + write!(f, "Transaction failed: {}", err) + } else { + write!(f, "{:?}", confirmation_status) + } + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index 94874cc107..0eb2f34301 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -1,6 +1,6 @@ use { crate::cli_output::CliSignatureVerificationStatus, - chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc}, + chrono::{DateTime, Local, NaiveDateTime, SecondsFormat, TimeZone, Utc}, console::style, indicatif::{ProgressBar, ProgressStyle}, solana_sdk::{ @@ -131,8 +131,17 @@ pub fn write_transaction( transaction_status: &Option, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, + block_time: Option, ) -> io::Result<()> { 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: {:?}", @@ -277,6 +286,7 @@ pub fn println_transaction( transaction_status: &Option, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, + block_time: Option, ) { let mut w = Vec::new(); if write_transaction( @@ -285,6 +295,7 @@ pub fn println_transaction( transaction_status, prefix, sigverify_status, + block_time, ) .is_ok() { @@ -300,6 +311,7 @@ pub fn writeln_transaction( transaction_status: &Option, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, + block_time: Option, ) -> fmt::Result { let mut w = Vec::new(); if write_transaction( @@ -308,6 +320,7 @@ pub fn writeln_transaction( transaction_status, prefix, sigverify_status, + block_time, ) .is_ok() { diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 73808643ea..f364f97cf4 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -17,8 +17,9 @@ use solana_clap_utils::{ offline::*, }; use solana_cli_output::{ - display::{build_balance_message, println_name_value, println_transaction}, - return_signers, CliAccount, CliSignature, CliSignatureVerificationStatus, OutputFormat, + display::{build_balance_message, println_name_value}, + return_signers, CliAccount, CliSignature, CliSignatureVerificationStatus, CliTransaction, + CliTransactionConfirmation, OutputFormat, }; use solana_client::{ blockhash_query::BlockhashQuery, @@ -50,9 +51,7 @@ use solana_stake_program::{ stake_instruction::LockupArgs, stake_state::{Lockup, StakeAuthorize}, }; -use solana_transaction_status::{ - EncodedTransaction, TransactionConfirmationStatus, UiTransactionEncoding, -}; +use solana_transaction_status::{EncodedTransaction, UiTransactionEncoding}; use solana_vote_program::vote_state::VoteAuthorize; use std::{ collections::HashMap, @@ -1005,60 +1004,72 @@ fn process_confirm( ) -> ProcessResult { match rpc_client.get_signature_statuses_with_history(&[*signature]) { Ok(status) => { - if let Some(transaction_status) = &status.value[0] { + let cli_transaction = if let Some(transaction_status) = &status.value[0] { + let mut transaction = None; + let mut get_transaction_error = None; if config.verbose { match rpc_client .get_confirmed_transaction(signature, UiTransactionEncoding::Base64) { Ok(confirmed_transaction) => { - println!( - "\nTransaction executed in slot {}:", - confirmed_transaction.slot - ); - println_transaction( - &confirmed_transaction - .transaction - .transaction - .decode() - .expect("Successful decode"), - &confirmed_transaction.transaction.meta, - " ", - None, + let decoded_transaction = confirmed_transaction + .transaction + .transaction + .decode() + .expect("Successful decode"); + let json_transaction = EncodedTransaction::encode( + decoded_transaction.clone(), + UiTransactionEncoding::Json, ); + + transaction = Some(CliTransaction { + transaction: json_transaction, + meta: confirmed_transaction.transaction.meta, + block_time: confirmed_transaction.block_time, + slot: Some(confirmed_transaction.slot), + decoded_transaction, + prefix: " ".to_string(), + sigverify_status: vec![], + }); } Err(err) => { - if transaction_status.confirmation_status() - != TransactionConfirmationStatus::Finalized - { - println!(); - println!("Unable to get finalized transaction details: not yet finalized") - } else { - println!(); - println!("Unable to get finalized transaction details: {}", err) - } + get_transaction_error = Some(format!("{:?}", err)); } } - println!(); } - - if let Some(err) = &transaction_status.err { - Ok(format!("Transaction failed: {}", err)) - } else { - Ok(format!("{:?}", transaction_status.confirmation_status())) + CliTransactionConfirmation { + confirmation_status: Some(transaction_status.confirmation_status()), + transaction, + get_transaction_error, + err: transaction_status.err.clone(), } } else { - Ok("Not found".to_string()) - } + CliTransactionConfirmation { + confirmation_status: None, + transaction: None, + get_transaction_error: None, + err: None, + } + }; + Ok(config.output_format.formatted_string(&cli_transaction)) } Err(err) => Err(CliError::RpcRequestError(format!("Unable to confirm: {}", err)).into()), } } #[allow(clippy::unnecessary_wraps)] -fn process_decode_transaction(transaction: &Transaction) -> ProcessResult { - let sig_stats = CliSignatureVerificationStatus::verify_transaction(&transaction); - println_transaction(transaction, &None, "", Some(&sig_stats)); - Ok("".to_string()) +fn process_decode_transaction(config: &CliConfig, transaction: &Transaction) -> ProcessResult { + let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction); + let decode_transaction = CliTransaction { + decoded_transaction: transaction.clone(), + transaction: EncodedTransaction::encode(transaction.clone(), UiTransactionEncoding::Json), + meta: None, + block_time: None, + slot: None, + prefix: "".to_string(), + sigverify_status, + }; + Ok(config.output_format.formatted_string(&decode_transaction)) } fn process_show_account( @@ -1745,7 +1756,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { } => process_balance(&rpc_client, config, &pubkey, *use_lamports_unit), // Confirm the last client transaction by signature CliCommand::Confirm(signature) => process_confirm(&rpc_client, config, signature), - CliCommand::DecodeTransaction(transaction) => process_decode_transaction(transaction), + CliCommand::DecodeTransaction(transaction) => { + process_decode_transaction(config, transaction) + } CliCommand::ResolveSigner(path) => { if let Some(path) = path { Ok(path.to_string()) @@ -2189,6 +2202,7 @@ mod tests { signature::{keypair_from_seed, read_keypair_file, write_keypair_file, Keypair, Presigner}, transaction::TransactionError, }; + use solana_transaction_status::TransactionConfirmationStatus; use std::path::PathBuf; fn make_tmp_path(name: &str) -> String { diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index df29eb5cc2..6d53e4f8db 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1785,6 +1785,7 @@ pub fn process_transaction_history( &confirmed_transaction.transaction.meta, " ", None, + None, ); } Err(err) => println!(" Unable to get confirmed transaction details: {}", err), diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs index 8e73cf20a9..7252e4c29c 100644 --- a/ledger-tool/src/bigtable.rs +++ b/ledger-tool/src/bigtable.rs @@ -4,10 +4,13 @@ use solana_clap_utils::{ input_parsers::pubkey_of, input_validators::{is_slot, is_valid_pubkey}, }; -use solana_cli_output::{display::println_transaction, CliBlock, OutputFormat}; +use solana_cli_output::{ + display::println_transaction, CliBlock, CliTransaction, CliTransactionConfirmation, + OutputFormat, +}; use solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType}; use solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature}; -use solana_transaction_status::{ConfirmedBlock, UiTransactionEncoding}; +use solana_transaction_status::{ConfirmedBlock, EncodedTransaction, UiTransactionEncoding}; use std::{ path::Path, process::exit, @@ -75,37 +78,48 @@ async fn blocks(starting_slot: Slot, limit: usize) -> Result<(), Box Result<(), Box> { +async fn confirm( + signature: &Signature, + verbose: bool, + output_format: OutputFormat, +) -> Result<(), Box> { let bigtable = solana_storage_bigtable::LedgerStorage::new(false, None) .await .map_err(|err| format!("Failed to connect to storage: {:?}", err))?; let transaction_status = bigtable.get_signature_status(signature).await?; + let mut transaction = None; + let mut get_transaction_error = None; if verbose { match bigtable.get_confirmed_transaction(signature).await { Ok(Some(confirmed_transaction)) => { - println!( - "\nTransaction executed in slot {}:", - confirmed_transaction.slot - ); - println_transaction( - &confirmed_transaction.transaction.transaction, - &confirmed_transaction.transaction.meta.map(|m| m.into()), - " ", - None, - ); + transaction = Some(CliTransaction { + transaction: EncodedTransaction::encode( + confirmed_transaction.transaction.transaction.clone(), + UiTransactionEncoding::Json, + ), + meta: confirmed_transaction.transaction.meta.map(|m| m.into()), + block_time: confirmed_transaction.block_time, + slot: Some(confirmed_transaction.slot), + decoded_transaction: confirmed_transaction.transaction.transaction, + prefix: " ".to_string(), + sigverify_status: vec![], + }); + } + Ok(None) => {} + Err(err) => { + get_transaction_error = Some(format!("{:?}", err)); } - Ok(None) => println!("Finalized transaction details not available"), - Err(err) => println!("Unable to get finalized transaction details: {}", err), } - println!(); - } - if let Some(err) = &transaction_status.err { - println!("Transaction failed: {}", err); - } else { - println!("{:?}", transaction_status.confirmation_status()); } + let cli_transaction = CliTransactionConfirmation { + confirmation_status: Some(transaction_status.confirmation_status()), + transaction, + get_transaction_error, + err: transaction_status.err.clone(), + }; + println!("{}", output_format.formatted_string(&cli_transaction)); Ok(()) } @@ -174,6 +188,7 @@ pub async fn transaction_history( &transaction_with_meta.meta.clone().map(|m| m.into()), " ", None, + None, ); } } @@ -301,13 +316,6 @@ impl BigTableSubCommand for App<'_, '_> { .required(true) .index(1) .help("The transaction signature to confirm"), - ) - .arg( - Arg::with_name("verbose") - .short("v") - .long("verbose") - .takes_value(false) - .help("Show additional information"), ), ) .subcommand( @@ -366,13 +374,6 @@ impl BigTableSubCommand for App<'_, '_> { .long("show-transactions") .takes_value(false) .help("Display the full transactions"), - ) - .arg( - Arg::with_name("verbose") - .short("v") - .long("verbose") - .takes_value(false) - .help("Show additional information"), ), ), ) @@ -382,6 +383,7 @@ impl BigTableSubCommand for App<'_, '_> { pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) { let runtime = tokio::runtime::Runtime::new().unwrap(); + let verbose = matches.is_present("verbose"); let output_format = matches .value_of("output_format") .map(|value| match value { @@ -389,7 +391,11 @@ pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) { "json-compact" => OutputFormat::JsonCompact, _ => unreachable!(), }) - .unwrap_or(OutputFormat::Display); + .unwrap_or(if verbose { + OutputFormat::DisplayVerbose + } else { + OutputFormat::Display + }); let future = match matches.subcommand() { ("upload", Some(arg_matches)) => { @@ -425,9 +431,8 @@ pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) { .unwrap() .parse() .expect("Invalid signature"); - let verbose = arg_matches.is_present("verbose"); - runtime.block_on(confirm(&signature, verbose)) + runtime.block_on(confirm(&signature, verbose, output_format)) } ("transaction-history", Some(arg_matches)) => { let address = pubkey_of(arg_matches, "address").unwrap(); @@ -439,7 +444,6 @@ pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) { let until = arg_matches .value_of("until") .map(|signature| signature.parse().expect("Invalid signature")); - let verbose = arg_matches.is_present("verbose"); let show_transactions = arg_matches.is_present("show_transactions"); runtime.block_on(transaction_history( diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index a3be30a0b6..ad2aa5b3e0 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -123,6 +123,7 @@ fn output_entry( &transaction_status, " ", None, + None, ); } } @@ -852,6 +853,15 @@ fn main() { .possible_values(&["json", "json-compact"]) .help("Return information in specified output format, currently only available for bigtable subcommands"), ) + .arg( + Arg::with_name("verbose") + .short("v") + .long("verbose") + .global(true) + .multiple(true) + .takes_value(false) + .help("Show additional information where supported"), + ) .bigtable_subcommand() .subcommand( SubCommand::with_name("print") @@ -873,14 +883,6 @@ fn main() { .takes_value(false) .help("Only print root slots"), ) - .arg( - Arg::with_name("verbose") - .long("verbose") - .short("v") - .multiple(true) - .takes_value(false) - .help("How verbose to print the ledger contents."), - ) ) .subcommand( SubCommand::with_name("copy") @@ -1349,7 +1351,7 @@ fn main() { let num_slots = value_t!(arg_matches, "num_slots", Slot).ok(); let allow_dead_slots = arg_matches.is_present("allow_dead_slots"); let only_rooted = arg_matches.is_present("only_rooted"); - let verbose = arg_matches.occurrences_of("verbose"); + let verbose = matches.occurrences_of("verbose"); output_ledger( open_blockstore( &ledger_path, diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index cac2d66dc6..09efa69e32 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -384,10 +384,11 @@ fn execute_transactions(bank: &Bank, txs: &[Transaction]) -> Vec