diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 916c9a12f9..7bd5ac17b0 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -2,10 +2,12 @@ use { crate::{ display::{ build_balance_message, build_balance_message_with_config, format_labeled_address, - unix_timestamp_to_string, writeln_name_value, BuildBalanceMessageConfig, + unix_timestamp_to_string, writeln_name_value, writeln_transaction, + BuildBalanceMessageConfig, }, QuietDisplay, VerboseDisplay, }, + chrono::{Local, TimeZone}, console::{style, Emoji}, inflector::cases::titlecase::to_title_case, serde::{Deserialize, Serialize}, @@ -27,6 +29,7 @@ use { transaction::Transaction, }, solana_stake_program::stake_state::{Authorized, Lockup}, + solana_transaction_status::EncodedConfirmedBlock, solana_vote_program::{ authorized_voters::AuthorizedVoters, vote_state::{BlockTimestamp, Lockout, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY}, @@ -1739,6 +1742,100 @@ impl fmt::Display for CliSignatureVerificationStatus { } } +#[derive(Serialize, Deserialize)] +pub struct CliBlock { + #[serde(flatten)] + pub encoded_confirmed_block: EncodedConfirmedBlock, + #[serde(skip_serializing)] + pub slot: Slot, +} + +impl QuietDisplay for CliBlock {} +impl VerboseDisplay for CliBlock {} + +impl fmt::Display for CliBlock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Slot: {}", self.slot)?; + writeln!( + f, + "Parent Slot: {}", + self.encoded_confirmed_block.parent_slot + )?; + writeln!(f, "Blockhash: {}", self.encoded_confirmed_block.blockhash)?; + writeln!( + f, + "Previous Blockhash: {}", + self.encoded_confirmed_block.previous_blockhash + )?; + if let Some(block_time) = self.encoded_confirmed_block.block_time { + writeln!(f, "Block Time: {:?}", Local.timestamp(block_time, 0))?; + } + if !self.encoded_confirmed_block.rewards.is_empty() { + let mut rewards = self.encoded_confirmed_block.rewards.clone(); + rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey)); + let mut total_rewards = 0; + writeln!(f, "Rewards:")?; + writeln!( + f, + " {:<44} {:^15} {:<15} {:<20} {:>14}", + "Address", "Type", "Amount", "New Balance", "Percent Change" + )?; + for reward in rewards { + let sign = if reward.lamports < 0 { "-" } else { "" }; + + total_rewards += reward.lamports; + writeln!( + f, + " {:<44} {:^15} {:>15} {}", + reward.pubkey, + if let Some(reward_type) = reward.reward_type { + format!("{}", reward_type) + } else { + "-".to_string() + }, + format!( + "{}◎{:<14.9}", + sign, + lamports_to_sol(reward.lamports.abs() as u64) + ), + if reward.post_balance == 0 { + " - -".to_string() + } else { + format!( + "◎{:<19.9} {:>13.9}%", + lamports_to_sol(reward.post_balance), + (reward.lamports.abs() as f64 + / (reward.post_balance as f64 - reward.lamports as f64)) + * 100.0 + ) + } + )?; + } + + let sign = if total_rewards < 0 { "-" } else { "" }; + writeln!( + f, + "Total Rewards: {}◎{:<12.9}", + sign, + lamports_to_sol(total_rewards.abs() as u64) + )?; + } + for (index, transaction_with_meta) in + self.encoded_confirmed_block.transactions.iter().enumerate() + { + writeln!(f, "Transaction {}:", index)?; + writeln_transaction( + f, + &transaction_with_meta.transaction.decode().unwrap(), + &transaction_with_meta.meta, + " ", + None, + )?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index 6729ebe276..94874cc107 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -294,6 +294,30 @@ pub fn println_transaction( } } +pub fn writeln_transaction( + f: &mut dyn fmt::Write, + transaction: &Transaction, + transaction_status: &Option, + prefix: &str, + sigverify_status: Option<&[CliSignatureVerificationStatus]>, +) -> fmt::Result { + let mut w = Vec::new(); + if write_transaction( + &mut w, + transaction, + transaction_status, + prefix, + sigverify_status, + ) + .is_ok() + { + if let Ok(s) = String::from_utf8(w) { + write!(f, "{}", s)?; + } + } + Ok(()) +} + /// Creates a new process bar for processing that will take an unknown amount of time pub fn new_spinner_progress_bar() -> ProgressBar { let progress_bar = ProgressBar::new(42); diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index b87eba804b..df29eb5cc2 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -3,7 +3,6 @@ use crate::{ spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, stake::is_stake_program_v2_enabled, }; -use chrono::{Local, TimeZone}; use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; use console::{style, Emoji}; use serde::{Deserialize, Serialize}; @@ -896,7 +895,7 @@ pub fn process_leader_schedule( pub fn process_get_block( rpc_client: &RpcClient, - _config: &CliConfig, + config: &CliConfig, slot: Option, ) -> ProcessResult { let slot = if let Some(slot) = slot { @@ -905,72 +904,13 @@ pub fn process_get_block( rpc_client.get_slot_with_commitment(CommitmentConfig::finalized())? }; - let mut block = + let encoded_confirmed_block = rpc_client.get_confirmed_block_with_encoding(slot, UiTransactionEncoding::Base64)?; - - println!("Slot: {}", slot); - println!("Parent Slot: {}", block.parent_slot); - println!("Blockhash: {}", block.blockhash); - println!("Previous Blockhash: {}", block.previous_blockhash); - if let Some(block_time) = block.block_time { - println!("Block Time: {:?}", Local.timestamp(block_time, 0)); - } - if !block.rewards.is_empty() { - block.rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey)); - let mut total_rewards = 0; - println!("Rewards:",); - println!( - " {:<44} {:^15} {:<15} {:<20} {:>14}", - "Address", "Type", "Amount", "New Balance", "Percent Change" - ); - for reward in block.rewards { - let sign = if reward.lamports < 0 { "-" } else { "" }; - - total_rewards += reward.lamports; - println!( - " {:<44} {:^15} {:>15} {}", - reward.pubkey, - if let Some(reward_type) = reward.reward_type { - format!("{}", reward_type) - } else { - "-".to_string() - }, - format!( - "{}◎{:<14.9}", - sign, - lamports_to_sol(reward.lamports.abs() as u64) - ), - if reward.post_balance == 0 { - " - -".to_string() - } else { - format!( - "◎{:<19.9} {:>13.9}%", - lamports_to_sol(reward.post_balance), - (reward.lamports.abs() as f64 - / (reward.post_balance as f64 - reward.lamports as f64)) - * 100.0 - ) - } - ); - } - - let sign = if total_rewards < 0 { "-" } else { "" }; - println!( - "Total Rewards: {}◎{:<12.9}", - sign, - lamports_to_sol(total_rewards.abs() as u64) - ); - } - for (index, transaction_with_meta) in block.transactions.iter().enumerate() { - println!("Transaction {}:", index); - println_transaction( - &transaction_with_meta.transaction.decode().unwrap(), - &transaction_with_meta.meta, - " ", - None, - ); - } - Ok("".to_string()) + let cli_block = CliBlock { + encoded_confirmed_block, + slot, + }; + Ok(config.output_format.formatted_string(&cli_block)) } pub fn process_get_block_time( diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs index 9f2ae53573..8e73cf20a9 100644 --- a/ledger-tool/src/bigtable.rs +++ b/ledger-tool/src/bigtable.rs @@ -4,10 +4,10 @@ use solana_clap_utils::{ input_parsers::pubkey_of, input_validators::{is_slot, is_valid_pubkey}, }; -use solana_cli_output::display::println_transaction; +use solana_cli_output::{display::println_transaction, CliBlock, OutputFormat}; use solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType}; use solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature}; -use solana_transaction_status::ConfirmedBlock; +use solana_transaction_status::{ConfirmedBlock, UiTransactionEncoding}; use std::{ path::Path, process::exit, @@ -48,32 +48,18 @@ async fn first_available_block() -> Result<(), Box> { Ok(()) } -async fn block(slot: Slot) -> Result<(), Box> { +async fn block(slot: Slot, 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 block = bigtable.get_confirmed_block(slot).await?; - println!("Slot: {}", slot); - println!("Parent Slot: {}", block.parent_slot); - println!("Blockhash: {}", block.blockhash); - println!("Previous Blockhash: {}", block.previous_blockhash); - if block.block_time.is_some() { - println!("Block Time: {:?}", block.block_time); - } - if !block.rewards.is_empty() { - println!("Rewards: {:?}", block.rewards); - } - for (index, transaction_with_meta) in block.transactions.into_iter().enumerate() { - println!("Transaction {}:", index); - println_transaction( - &transaction_with_meta.transaction, - &transaction_with_meta.meta.map(|meta| meta.into()), - " ", - None, - ); - } + let cli_block = CliBlock { + encoded_confirmed_block: block.encode(UiTransactionEncoding::Base64), + slot, + }; + println!("{}", output_format.formatted_string(&cli_block)); Ok(()) } @@ -396,6 +382,15 @@ impl BigTableSubCommand for App<'_, '_> { pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) { let runtime = tokio::runtime::Runtime::new().unwrap(); + let output_format = matches + .value_of("output_format") + .map(|value| match value { + "json" => OutputFormat::Json, + "json-compact" => OutputFormat::JsonCompact, + _ => unreachable!(), + }) + .unwrap_or(OutputFormat::Display); + let future = match matches.subcommand() { ("upload", Some(arg_matches)) => { let starting_slot = value_t!(arg_matches, "starting_slot", Slot).unwrap_or(0); @@ -416,7 +411,7 @@ pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) { ("first-available-block", Some(_arg_matches)) => runtime.block_on(first_available_block()), ("block", Some(arg_matches)) => { let slot = value_t_or_exit!(arg_matches, "slot", Slot); - runtime.block_on(block(slot)) + runtime.block_on(block(slot, output_format)) } ("blocks", Some(arg_matches)) => { let starting_slot = value_t_or_exit!(arg_matches, "starting_slot", Slot); diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 5731c17291..a3be30a0b6 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -843,6 +843,15 @@ fn main() { .global(true) .help("Use DIR for ledger location"), ) + .arg( + Arg::with_name("output_format") + .long("output") + .value_name("FORMAT") + .global(true) + .takes_value(true) + .possible_values(&["json", "json-compact"]) + .help("Return information in specified output format, currently only available for bigtable subcommands"), + ) .bigtable_subcommand() .subcommand( SubCommand::with_name("print")