diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 6fc394f6709530..daf522c60055f4 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -10,7 +10,7 @@ use { QuietDisplay, VerboseDisplay, }, base64::{prelude::BASE64_STANDARD, Engine}, - chrono::{Local, TimeZone}, + chrono::{Local, TimeZone, Utc}, clap::ArgMatches, console::{style, Emoji}, inflector::cases::titlecase::to_title_case, @@ -1140,9 +1140,36 @@ fn show_votes_and_credits( Ok(()) } +enum Format { + Csv, + Human, +} + +macro_rules! format_as { + ($target:expr, $fmt1:expr, $fmt2:expr, $which_fmt:expr, $($arg:tt)*) => { + match $which_fmt { + Format::Csv => { + writeln!( + $target, + $fmt1, + $($arg)* + ) + }, + Format::Human => { + writeln!( + $target, + $fmt2, + $($arg)* + ) + } + } + }; +} + fn show_epoch_rewards( f: &mut fmt::Formatter, epoch_rewards: &Option>, + use_csv: bool, ) -> fmt::Result { if let Some(epoch_rewards) = epoch_rewards { if epoch_rewards.is_empty() { @@ -1150,9 +1177,12 @@ fn show_epoch_rewards( } writeln!(f, "Epoch Rewards:")?; - writeln!( + let fmt = if use_csv { Format::Csv } else { Format::Human }; + format_as!( f, + "{},{},{},{},{},{},{},{}", " {:<6} {:<11} {:<26} {:<18} {:<18} {:>14} {:>14} {:>10}", + fmt, "Epoch", "Reward Slot", "Time", @@ -1160,15 +1190,17 @@ fn show_epoch_rewards( "New Balance", "Percent Change", "APR", - "Commission" + "Commission", )?; for reward in epoch_rewards { - writeln!( + format_as!( f, - " {:<6} {:<11} {:<26} ◎{:<17.9} ◎{:<17.9} {:>13.6}% {:>14} {:>10}", + "{},{},{},{},{},{}%,{},{}", + " {:<6} {:<11} {:<26} ◎{:<17.9} ◎{:<17.9} {:>13.3}% {:>14} {:>10}", + fmt, reward.epoch, reward.effective_slot, - Local.timestamp_opt(reward.block_time, 0).unwrap(), + Utc.timestamp_opt(reward.block_time, 0).unwrap(), lamports_to_sol(reward.amount), lamports_to_sol(reward.post_balance), reward.percent_change, @@ -1219,6 +1251,8 @@ pub struct CliStakeState { pub deactivating_stake: Option, #[serde(skip_serializing_if = "Option::is_none")] pub epoch_rewards: Option>, + #[serde(skip_serializing)] + pub use_csv: bool, } impl QuietDisplay for CliStakeState {} @@ -1373,7 +1407,7 @@ impl fmt::Display for CliStakeState { } show_authorized(f, self.authorized.as_ref().unwrap())?; show_lockup(f, self.lockup.as_ref())?; - show_epoch_rewards(f, &self.epoch_rewards)? + show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)? } } Ok(()) @@ -1562,6 +1596,8 @@ pub struct CliVoteAccount { pub epoch_voting_history: Vec, #[serde(skip_serializing)] pub use_lamports_unit: bool, + #[serde(skip_serializing)] + pub use_csv: bool, #[serde(skip_serializing_if = "Option::is_none")] pub epoch_rewards: Option>, } @@ -1596,7 +1632,7 @@ impl fmt::Display for CliVoteAccount { self.recent_timestamp.slot )?; show_votes_and_credits(f, &self.votes, &self.epoch_voting_history)?; - show_epoch_rewards(f, &self.epoch_rewards)?; + show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?; Ok(()) } } @@ -3228,7 +3264,7 @@ mod tests { effective_slot: 100, epoch: 1, amount: 10, - block_time: UnixTimestamp::default(), + block_time: 0, apr: Some(10.0), }, CliEpochReward { @@ -3238,19 +3274,25 @@ mod tests { effective_slot: 200, epoch: 2, amount: 12, - block_time: UnixTimestamp::default(), + block_time: 1_000_000, apr: Some(13.0), }, ]; - let c = CliVoteAccount { + let mut c = CliVoteAccount { account_balance: 10000, validator_identity: Pubkey::default().to_string(), epoch_rewards: Some(epoch_rewards), + recent_timestamp: BlockTimestamp::default(), ..CliVoteAccount::default() }; let s = format!("{c}"); - assert!(!s.is_empty()); + assert_eq!(s, "Account Balance: 0.00001 SOL\nValidator Identity: 11111111111111111111111111111111\nVote Authority: {}\nWithdraw Authority: \nCredits: 0\nCommission: 0%\nRoot Slot: ~\nRecent Timestamp: 1970-01-01T00:00:00Z from slot 0\nEpoch Rewards:\n Epoch Reward Slot Time Amount New Balance Percent Change APR Commission\n 1 100 1970-01-01 00:00:00 UTC ◎0.000000010 ◎0.000000100 11.000% 10.00% 1%\n 2 200 1970-01-12 13:46:40 UTC ◎0.000000012 ◎0.000000100 11.000% 13.00% 1%\n"); + println!("{s}"); + + c.use_csv = true; + let s = format!("{c}"); + assert_eq!(s, "Account Balance: 0.00001 SOL\nValidator Identity: 11111111111111111111111111111111\nVote Authority: {}\nWithdraw Authority: \nCredits: 0\nCommission: 0%\nRoot Slot: ~\nRecent Timestamp: 1970-01-01T00:00:00Z from slot 0\nEpoch Rewards:\nEpoch,Reward Slot,Time,Amount,New Balance,Percent Change,APR,Commission\n1,100,1970-01-01 00:00:00 UTC,0.00000001,0.0000001,11%,10.00%,1%\n2,200,1970-01-12 13:46:40 UTC,0.000000012,0.0000001,11%,13.00%,1%\n"); println!("{s}"); } } diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 17a35f7da0a2ab..8252e13bbbd6a2 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -261,6 +261,7 @@ pub enum CliCommand { pubkey: Pubkey, use_lamports_unit: bool, with_rewards: Option, + use_csv: bool, }, StakeAuthorize { stake_account_pubkey: Pubkey, @@ -333,6 +334,7 @@ pub enum CliCommand { ShowVoteAccount { pubkey: Pubkey, use_lamports_unit: bool, + use_csv: bool, with_rewards: Option, }, WithdrawFromVoteAccount { @@ -1277,12 +1279,14 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { pubkey: stake_account_pubkey, use_lamports_unit, with_rewards, + use_csv, } => process_show_stake_account( &rpc_client, config, stake_account_pubkey, *use_lamports_unit, *with_rewards, + *use_csv, ), CliCommand::ShowStakeHistory { use_lamports_unit, @@ -1441,12 +1445,14 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { CliCommand::ShowVoteAccount { pubkey: vote_account_pubkey, use_lamports_unit, + use_csv, with_rewards, } => process_show_vote_account( &rpc_client, config, vote_account_pubkey, *use_lamports_unit, + *use_csv, *with_rewards, ), CliCommand::WithdrawFromVoteAccount { diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 03949f7a7bab23..0470cf761ad95d 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1821,6 +1821,7 @@ pub fn process_show_stakes( &stake_history, &clock, new_rate_activation_epoch, + false, ), }); } @@ -1840,6 +1841,7 @@ pub fn process_show_stakes( &stake_history, &clock, new_rate_activation_epoch, + false, ), }); } diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 04101397120142..96c1b50b3576e6 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -706,12 +706,18 @@ impl StakeSubCommands for App<'_, '_> { .takes_value(false) .help("Display inflation rewards"), ) + .arg( + Arg::with_name("csv") + .long("csv") + .takes_value(false) + .help("Format stake account data in csv") + ) .arg( Arg::with_name("num_rewards_epochs") .long("num-rewards-epochs") .takes_value(true) .value_name("NUM") - .validator(|s| is_within_range(s, 1..=10)) + .validator(|s| is_within_range(s, 1..=50)) .default_value_if("with_rewards", None, "1") .requires("with_rewards") .help("Display rewards for NUM recent epochs, max 10 [default: latest epoch only]"), @@ -1293,6 +1299,7 @@ pub fn parse_show_stake_account( let stake_account_pubkey = pubkey_of_signer(matches, "stake_account_pubkey", wallet_manager)?.unwrap(); let use_lamports_unit = matches.is_present("lamports"); + let use_csv = matches.is_present("csv"); let with_rewards = if matches.is_present("with_rewards") { Some(value_of(matches, "num_rewards_epochs").unwrap()) } else { @@ -1303,6 +1310,7 @@ pub fn parse_show_stake_account( pubkey: stake_account_pubkey, use_lamports_unit, with_rewards, + use_csv, }, signers: vec![], }) @@ -2226,6 +2234,7 @@ pub fn build_stake_state( stake_history: &StakeHistory, clock: &Clock, new_rate_activation_epoch: Option, + use_csv: bool, ) -> CliStakeState { match stake_state { StakeStateV2::Stake( @@ -2282,6 +2291,7 @@ pub fn build_stake_state( active_stake: u64_some_if_not_zero(effective), activating_stake: u64_some_if_not_zero(activating), deactivating_stake: u64_some_if_not_zero(deactivating), + use_csv, ..CliStakeState::default() } } @@ -2448,6 +2458,7 @@ pub fn process_show_stake_account( stake_account_address: &Pubkey, use_lamports_unit: bool, with_rewards: Option, + use_csv: bool, ) -> ProcessResult { let stake_account = rpc_client.get_account(stake_account_address)?; if stake_account.owner != stake::program::id() { @@ -2478,6 +2489,7 @@ pub fn process_show_stake_account( &stake_history, &clock, new_rate_activation_epoch, + use_csv, ); if state.stake_type == CliStakeType::Stake && state.activation_epoch.is_some() { diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 6c98e49c3bff42..e4456fe1d2355c 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -333,12 +333,18 @@ impl VoteSubCommands for App<'_, '_> { .takes_value(false) .help("Display inflation rewards"), ) + .arg( + Arg::with_name("csv") + .long("csv") + .takes_value(false) + .help("Format rewards in a CSV table"), + ) .arg( Arg::with_name("num_rewards_epochs") .long("num-rewards-epochs") .takes_value(true) .value_name("NUM") - .validator(|s| is_within_range(s, 1..=10)) + .validator(|s| is_within_range(s, 1..=50)) .default_value_if("with_rewards", None, "1") .requires("with_rewards") .help("Display rewards for NUM recent epochs, max 10 [default: latest epoch only]"), @@ -648,6 +654,7 @@ pub fn parse_vote_get_account_command( let vote_account_pubkey = pubkey_of_signer(matches, "vote_account_pubkey", wallet_manager)?.unwrap(); let use_lamports_unit = matches.is_present("lamports"); + let use_csv = matches.is_present("csv"); let with_rewards = if matches.is_present("with_rewards") { Some(value_of(matches, "num_rewards_epochs").unwrap()) } else { @@ -657,6 +664,7 @@ pub fn parse_vote_get_account_command( command: CliCommand::ShowVoteAccount { pubkey: vote_account_pubkey, use_lamports_unit, + use_csv, with_rewards, }, signers: vec![], @@ -1208,6 +1216,7 @@ pub fn process_show_vote_account( config: &CliConfig, vote_account_address: &Pubkey, use_lamports_unit: bool, + use_csv: bool, with_rewards: Option, ) -> ProcessResult { let (vote_account, vote_state) = @@ -1257,6 +1266,7 @@ pub fn process_show_vote_account( votes, epoch_voting_history, use_lamports_unit, + use_csv, epoch_rewards, };