#![allow(clippy::to_string_in_format_args)]
use {
    crate::{
        cli_version::CliVersion,
        display::{
            build_balance_message, build_balance_message_with_config, format_labeled_address,
            unix_timestamp_to_string, writeln_name_value, writeln_transaction,
            BuildBalanceMessageConfig,
        },
        QuietDisplay, VerboseDisplay,
    },
    base64::{prelude::BASE64_STANDARD, Engine},
    chrono::{Local, TimeZone, Utc},
    clap::ArgMatches,
    console::{style, Emoji},
    inflector::cases::titlecase::to_title_case,
    serde::{Deserialize, Serialize},
    serde_json::{Map, Value},
    solana_account::ReadableAccount,
    solana_account_decoder::{
        encode_ui_account, parse_account_data::AccountAdditionalDataV3,
        parse_token::UiTokenAccount, UiAccountEncoding, UiDataSliceConfig,
    },
    solana_clap_utils::keypair::SignOnly,
    solana_clock::{Epoch, Slot, UnixTimestamp},
    solana_epoch_info::EpochInfo,
    solana_hash::Hash,
    solana_pubkey::Pubkey,
    solana_rpc_client_api::response::{
        RpcAccountBalance, RpcContactInfo, RpcInflationGovernor, RpcInflationRate, RpcKeyedAccount,
        RpcSupply, RpcVoteAccountInfo,
    },
    solana_signature::Signature,
    solana_stake_interface::{
        stake_history::StakeHistoryEntry,
        state::{Authorized, Lockup},
    },
    solana_transaction::{versioned::VersionedTransaction, Transaction},
    solana_transaction_status::{
        EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus,
        UiTransactionStatusMeta,
    },
    solana_transaction_status_client_types::UiTransactionError,
    solana_vote_program::{
        authorized_voters::AuthorizedVoters,
        vote_state::{BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY},
    },
    std::{
        collections::{BTreeMap, HashMap},
        fmt,
        str::FromStr,
        time::Duration,
    },
};

static CHECK_MARK: Emoji = Emoji("✅ ", "");
static CROSS_MARK: Emoji = Emoji("❌ ", "");
static WARNING: Emoji = Emoji("⚠️", "!");

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OutputFormat {
    Display,
    Json,
    JsonCompact,
    DisplayQuiet,
    DisplayVerbose,
}

impl OutputFormat {
    pub fn formatted_string<T>(&self, item: &T) -> String
    where
        T: Serialize + fmt::Display + QuietDisplay + VerboseDisplay,
    {
        match self {
            OutputFormat::Display => format!("{item}"),
            OutputFormat::DisplayQuiet => {
                let mut s = String::new();
                QuietDisplay::write_str(item, &mut s).unwrap();
                s
            }
            OutputFormat::DisplayVerbose => {
                let mut s = String::new();
                VerboseDisplay::write_str(item, &mut s).unwrap();
                s
            }
            OutputFormat::Json => serde_json::to_string_pretty(item).unwrap(),
            OutputFormat::JsonCompact => serde_json::to_value(item).unwrap().to_string(),
        }
    }

    pub fn from_matches(matches: &ArgMatches<'_>, output_name: &str, verbose: bool) -> Self {
        matches
            .value_of(output_name)
            .map(|value| match value {
                "json" => OutputFormat::Json,
                "json-compact" => OutputFormat::JsonCompact,
                _ => unreachable!(),
            })
            .unwrap_or(if verbose {
                OutputFormat::DisplayVerbose
            } else {
                OutputFormat::Display
            })
    }
}

#[derive(Serialize)]
pub struct CliPrioritizationFeeStats {
    pub fees: Vec<CliPrioritizationFee>,
    pub min: u64,
    pub max: u64,
    pub average: u64,
    pub num_slots: u64,
}

impl QuietDisplay for CliPrioritizationFeeStats {}
impl VerboseDisplay for CliPrioritizationFeeStats {
    fn write_str(&self, f: &mut dyn std::fmt::Write) -> fmt::Result {
        writeln!(f, "{:<11} prioritization_fee", "slot")?;
        for fee in &self.fees {
            write!(f, "{fee}")?;
        }
        write!(f, "{self}")
    }
}

impl fmt::Display for CliPrioritizationFeeStats {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "Fees in recent {} slots: Min: {} Max: {} Average: {}",
            self.num_slots, self.min, self.max, self.average
        )
    }
}

#[derive(Serialize)]
pub struct CliPrioritizationFee {
    pub slot: Slot,
    pub prioritization_fee: u64,
}

impl QuietDisplay for CliPrioritizationFee {}
impl VerboseDisplay for CliPrioritizationFee {}

impl fmt::Display for CliPrioritizationFee {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "{:<11} {}", self.slot, self.prioritization_fee)
    }
}

#[derive(Serialize, Deserialize)]
pub struct CliAccount {
    #[serde(flatten)]
    pub keyed_account: RpcKeyedAccount,
    #[serde(skip_serializing, skip_deserializing)]
    pub use_lamports_unit: bool,
}

pub struct CliAccountNewConfig {
    pub data_encoding: UiAccountEncoding,
    pub additional_data: Option<AccountAdditionalDataV3>,
    pub data_slice_config: Option<UiDataSliceConfig>,
    pub use_lamports_unit: bool,
}

impl Default for CliAccountNewConfig {
    fn default() -> Self {
        Self {
            data_encoding: UiAccountEncoding::Base64,
            additional_data: None,
            data_slice_config: None,
            use_lamports_unit: false,
        }
    }
}

impl CliAccount {
    pub fn new<T: ReadableAccount>(address: &Pubkey, account: &T, use_lamports_unit: bool) -> Self {
        Self::new_with_config(
            address,
            account,
            &CliAccountNewConfig {
                use_lamports_unit,
                ..CliAccountNewConfig::default()
            },
        )
    }

    pub fn new_with_config<T: ReadableAccount>(
        address: &Pubkey,
        account: &T,
        config: &CliAccountNewConfig,
    ) -> Self {
        let CliAccountNewConfig {
            data_encoding,
            additional_data,
            data_slice_config,
            use_lamports_unit,
        } = *config;
        Self {
            keyed_account: RpcKeyedAccount {
                pubkey: address.to_string(),
                account: encode_ui_account(
                    address,
                    account,
                    data_encoding,
                    additional_data,
                    data_slice_config,
                ),
            },
            use_lamports_unit,
        }
    }
}

impl QuietDisplay for CliAccount {}
impl VerboseDisplay for CliAccount {}

impl fmt::Display for CliAccount {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Public Key:", &self.keyed_account.pubkey)?;
        writeln_name_value(
            f,
            "Balance:",
            &build_balance_message(
                self.keyed_account.account.lamports,
                self.use_lamports_unit,
                true,
            ),
        )?;
        writeln_name_value(f, "Owner:", &self.keyed_account.account.owner)?;
        writeln_name_value(
            f,
            "Executable:",
            &self.keyed_account.account.executable.to_string(),
        )?;
        writeln_name_value(
            f,
            "Rent Epoch:",
            &self.keyed_account.account.rent_epoch.to_string(),
        )?;
        Ok(())
    }
}

#[derive(Default, Serialize, Deserialize)]
pub struct CliBlockProduction {
    pub epoch: Epoch,
    pub start_slot: Slot,
    pub end_slot: Slot,
    pub total_slots: usize,
    pub total_blocks_produced: usize,
    pub total_slots_skipped: usize,
    pub leaders: Vec<CliBlockProductionEntry>,
    pub individual_slot_status: Vec<CliSlotStatus>,
    #[serde(skip_serializing)]
    pub verbose: bool,
}

impl QuietDisplay for CliBlockProduction {}
impl VerboseDisplay for CliBlockProduction {}

impl fmt::Display for CliBlockProduction {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "{}",
            style(format!(
                "  {:<44}  {:>15}  {:>15}  {:>15}  {:>15}",
                "Identity", "Leader Slots", "Blocks Produced", "Skipped Slots", "Skip Rate",
            ))
            .bold()
        )?;
        for leader in &self.leaders {
            writeln!(
                f,
                "  {:<44}  {:>15}  {:>15}  {:>15}  {:>22.2}%",
                leader.identity_pubkey,
                leader.leader_slots,
                leader.blocks_produced,
                leader.skipped_slots,
                leader.skipped_slots as f64 / leader.leader_slots as f64 * 100.
            )?;
        }
        writeln!(f)?;
        writeln!(
            f,
            "  {:<44}  {:>15}  {:>15}  {:>15}  {:>22.2}%",
            format!("Epoch {} total:", self.epoch),
            self.total_slots,
            self.total_blocks_produced,
            self.total_slots_skipped,
            self.total_slots_skipped as f64 / self.total_slots as f64 * 100.
        )?;
        writeln!(
            f,
            "  (using data from {} slots: {} to {})",
            self.total_slots, self.start_slot, self.end_slot
        )?;
        if self.verbose {
            writeln!(f)?;
            writeln!(f)?;
            writeln!(
                f,
                "{}",
                style(format!("  {:<15} {:<44}", "Slot", "Identity Pubkey")).bold(),
            )?;
            for status in &self.individual_slot_status {
                if status.skipped {
                    writeln!(
                        f,
                        "{}",
                        style(format!(
                            "  {:<15} {:<44} SKIPPED",
                            status.slot, status.leader
                        ))
                        .red()
                    )?;
                } else {
                    writeln!(
                        f,
                        "{}",
                        style(format!("  {:<15} {:<44}", status.slot, status.leader))
                    )?;
                }
            }
        }
        Ok(())
    }
}

#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliBlockProductionEntry {
    pub identity_pubkey: String,
    pub leader_slots: u64,
    pub blocks_produced: u64,
    pub skipped_slots: u64,
}

#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliSlotStatus {
    pub slot: Slot,
    pub leader: String,
    pub skipped: bool,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliEpochInfo {
    #[serde(flatten)]
    pub epoch_info: EpochInfo,
    pub epoch_completed_percent: f64,
    #[serde(skip)]
    pub average_slot_time_ms: u64,
    #[serde(skip)]
    pub start_block_time: Option<UnixTimestamp>,
    #[serde(skip)]
    pub current_block_time: Option<UnixTimestamp>,
}

impl QuietDisplay for CliEpochInfo {}
impl VerboseDisplay for CliEpochInfo {}

impl fmt::Display for CliEpochInfo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(
            f,
            "Block height:",
            &self.epoch_info.block_height.to_string(),
        )?;
        writeln_name_value(f, "Slot:", &self.epoch_info.absolute_slot.to_string())?;
        writeln_name_value(f, "Epoch:", &self.epoch_info.epoch.to_string())?;
        if let Some(transaction_count) = &self.epoch_info.transaction_count {
            writeln_name_value(f, "Transaction Count:", &transaction_count.to_string())?;
        }
        let start_slot = self.epoch_info.absolute_slot - self.epoch_info.slot_index;
        let end_slot = start_slot + self.epoch_info.slots_in_epoch;
        writeln_name_value(
            f,
            "Epoch Slot Range:",
            &format!("[{start_slot}..{end_slot})"),
        )?;
        writeln_name_value(
            f,
            "Epoch Completed Percent:",
            &format!("{:>3.3}%", self.epoch_completed_percent),
        )?;
        let remaining_slots_in_epoch = self.epoch_info.slots_in_epoch - self.epoch_info.slot_index;
        writeln_name_value(
            f,
            "Epoch Completed Slots:",
            &format!(
                "{}/{} ({} remaining)",
                self.epoch_info.slot_index,
                self.epoch_info.slots_in_epoch,
                remaining_slots_in_epoch
            ),
        )?;
        let (time_elapsed, annotation) = if let (Some(start_block_time), Some(current_block_time)) =
            (self.start_block_time, self.current_block_time)
        {
            (
                Duration::from_secs((current_block_time - start_block_time) as u64),
                None,
            )
        } else {
            (
                slot_to_duration(self.epoch_info.slot_index, self.average_slot_time_ms),
                Some("* estimated based on current slot durations"),
            )
        };
        let time_remaining = slot_to_duration(remaining_slots_in_epoch, self.average_slot_time_ms);
        writeln_name_value(
            f,
            "Epoch Completed Time:",
            &format!(
                "{}{}/{} ({} remaining)",
                humantime::format_duration(time_elapsed),
                if annotation.is_some() { "*" } else { "" },
                humantime::format_duration(time_elapsed + time_remaining),
                humantime::format_duration(time_remaining),
            ),
        )?;
        if let Some(annotation) = annotation {
            writeln!(f)?;
            writeln!(f, "{annotation}")?;
        }
        Ok(())
    }
}

fn slot_to_duration(slot: Slot, slot_time_ms: u64) -> Duration {
    Duration::from_secs((slot * slot_time_ms) / 1000)
}

#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CliValidatorsStakeByVersion {
    pub current_validators: usize,
    pub delinquent_validators: usize,
    pub current_active_stake: u64,
    pub delinquent_active_stake: u64,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
pub enum CliValidatorsSortOrder {
    Delinquent,
    Commission,
    EpochCredits,
    Identity,
    LastVote,
    Root,
    SkipRate,
    Stake,
    VoteAccount,
    Version,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliValidators {
    pub total_active_stake: u64,
    pub total_current_stake: u64,
    pub total_delinquent_stake: u64,
    pub validators: Vec<CliValidator>,
    pub average_skip_rate: f64,
    pub average_stake_weighted_skip_rate: f64,
    #[serde(skip_serializing)]
    pub validators_sort_order: CliValidatorsSortOrder,
    #[serde(skip_serializing)]
    pub validators_reverse_sort: bool,
    #[serde(skip_serializing)]
    pub number_validators: bool,
    pub stake_by_version: BTreeMap<CliVersion, CliValidatorsStakeByVersion>,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}

impl QuietDisplay for CliValidators {}
impl VerboseDisplay for CliValidators {}

impl fmt::Display for CliValidators {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fn write_vote_account(
            f: &mut fmt::Formatter,
            validator: &CliValidator,
            total_active_stake: u64,
            use_lamports_unit: bool,
            highest_last_vote: u64,
            highest_root: u64,
        ) -> fmt::Result {
            fn non_zero_or_dash(v: u64, max_v: u64) -> String {
                if v == 0 {
                    "        -      ".into()
                } else if v == max_v {
                    format!("{v:>9} (  0)")
                } else if v > max_v.saturating_sub(100) {
                    format!("{:>9} ({:>3})", v, -(max_v.saturating_sub(v) as isize))
                } else {
                    format!("{v:>9}      ")
                }
            }

            writeln!(
                f,
                "{} {:<44}  {:<44}  {:>3}%  {:>14}  {:>14} {:>7} {:>8}  {:>7}  {:>22} ({:.2}%)",
                if validator.delinquent {
                    WARNING.to_string()
                } else {
                    "\u{a0}".to_string()
                },
                validator.identity_pubkey,
                validator.vote_account_pubkey,
                validator.commission,
                non_zero_or_dash(validator.last_vote, highest_last_vote),
                non_zero_or_dash(validator.root_slot, highest_root),
                if let Some(skip_rate) = validator.skip_rate {
                    format!("{skip_rate:.2}%")
                } else {
                    "-   ".to_string()
                },
                validator.epoch_credits,
                // convert to a string so that fill/alignment works correctly
                validator.version.to_string(),
                build_balance_message_with_config(
                    validator.activated_stake,
                    &BuildBalanceMessageConfig {
                        use_lamports_unit,
                        trim_trailing_zeros: false,
                        ..BuildBalanceMessageConfig::default()
                    }
                ),
                100. * validator.activated_stake as f64 / total_active_stake as f64,
            )
        }

        let padding = if self.number_validators {
            ((self.validators.len() + 1) as f64).log10().floor() as usize + 1
        } else {
            0
        };
        let header = style(format!(
            "{:padding$} {:<44}  {:<38}  {}  {}  {} {}  {}  {}  {:>22}",
            " ",
            "Identity",
            "Vote Account",
            "Commission",
            "Last Vote      ",
            "Root Slot    ",
            "Skip Rate",
            "Credits",
            "Version",
            "Active Stake",
            padding = padding + 2
        ))
        .bold();
        writeln!(f, "{header}")?;

        let mut sorted_validators = self.validators.clone();
        match self.validators_sort_order {
            CliValidatorsSortOrder::Delinquent => {
                sorted_validators.sort_by_key(|a| a.delinquent);
            }
            CliValidatorsSortOrder::Commission => {
                sorted_validators.sort_by_key(|a| a.commission);
            }
            CliValidatorsSortOrder::EpochCredits => {
                sorted_validators.sort_by_key(|a| a.epoch_credits);
            }
            CliValidatorsSortOrder::Identity => {
                sorted_validators.sort_by(|a, b| a.identity_pubkey.cmp(&b.identity_pubkey));
            }
            CliValidatorsSortOrder::LastVote => {
                sorted_validators.sort_by_key(|a| a.last_vote);
            }
            CliValidatorsSortOrder::Root => {
                sorted_validators.sort_by_key(|a| a.root_slot);
            }
            CliValidatorsSortOrder::VoteAccount => {
                sorted_validators.sort_by(|a, b| a.vote_account_pubkey.cmp(&b.vote_account_pubkey));
            }
            CliValidatorsSortOrder::SkipRate => {
                sorted_validators.sort_by(|a, b| {
                    use std::cmp::Ordering;
                    match (a.skip_rate, b.skip_rate) {
                        (None, None) => Ordering::Equal,
                        (None, Some(_)) => Ordering::Greater,
                        (Some(_), None) => Ordering::Less,
                        (Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
                    }
                });
            }
            CliValidatorsSortOrder::Stake => {
                sorted_validators.sort_by_key(|a| a.activated_stake);
            }
            CliValidatorsSortOrder::Version => {
                sorted_validators.sort_by(|a, b| {
                    (&a.version, a.activated_stake).cmp(&(&b.version, b.activated_stake))
                });
            }
        }

        if self.validators_reverse_sort {
            sorted_validators.reverse();
        }

        let highest_root = sorted_validators
            .iter()
            .map(|v| v.root_slot)
            .max()
            .unwrap_or_default();
        let highest_last_vote = sorted_validators
            .iter()
            .map(|v| v.last_vote)
            .max()
            .unwrap_or_default();

        for (i, validator) in sorted_validators.iter().enumerate() {
            if padding > 0 {
                let num = if self.validators_reverse_sort {
                    i + 1
                } else {
                    sorted_validators.len() - i
                };
                write!(f, "{num:padding$} ")?;
            }
            write_vote_account(
                f,
                validator,
                self.total_active_stake,
                self.use_lamports_unit,
                highest_last_vote,
                highest_root,
            )?;
        }

        // The actual header has long scrolled away.  Print the header once more as a footer
        if self.validators.len() > 100 {
            writeln!(f, "{header}")?;
        }

        writeln!(f)?;
        writeln_name_value(
            f,
            "Average Stake-Weighted Skip Rate:",
            &format!("{:.2}%", self.average_stake_weighted_skip_rate,),
        )?;
        writeln_name_value(
            f,
            "Average Unweighted Skip Rate:    ",
            &format!("{:.2}%", self.average_skip_rate),
        )?;

        writeln!(f)?;
        writeln_name_value(
            f,
            "Active Stake:",
            &build_balance_message(self.total_active_stake, self.use_lamports_unit, true),
        )?;
        if self.total_delinquent_stake > 0 {
            writeln_name_value(
                f,
                "Current Stake:",
                &format!(
                    "{} ({:0.2}%)",
                    &build_balance_message(self.total_current_stake, self.use_lamports_unit, true),
                    100. * self.total_current_stake as f64 / self.total_active_stake as f64
                ),
            )?;
            writeln_name_value(
                f,
                "Delinquent Stake:",
                &format!(
                    "{} ({:0.2}%)",
                    &build_balance_message(
                        self.total_delinquent_stake,
                        self.use_lamports_unit,
                        true
                    ),
                    100. * self.total_delinquent_stake as f64 / self.total_active_stake as f64
                ),
            )?;
        }

        writeln!(f)?;
        writeln!(f, "{}", style("Stake By Version:").bold())?;
        for (version, info) in self.stake_by_version.iter().rev() {
            writeln!(
                f,
                "{:<7} - {:4} current validators ({:>5.2}%){}",
                // convert to a string so that fill/alignment works correctly
                version.to_string(),
                info.current_validators,
                100. * info.current_active_stake as f64 / self.total_active_stake as f64,
                if info.delinquent_validators > 0 {
                    format!(
                        " {:3} delinquent validators ({:>5.2}%)",
                        info.delinquent_validators,
                        100. * info.delinquent_active_stake as f64 / self.total_active_stake as f64
                    )
                } else {
                    "".to_string()
                },
            )?;
        }

        Ok(())
    }
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CliValidator {
    pub identity_pubkey: String,
    pub vote_account_pubkey: String,
    pub commission: u8,
    pub last_vote: u64,
    pub root_slot: u64,
    pub credits: u64,       // lifetime credits
    pub epoch_credits: u64, // credits earned in the current epoch
    pub activated_stake: u64,
    pub version: CliVersion,
    pub delinquent: bool,
    pub skip_rate: Option<f64>,
}

impl CliValidator {
    pub fn new(
        vote_account: &RpcVoteAccountInfo,
        current_epoch: Epoch,
        version: CliVersion,
        skip_rate: Option<f64>,
        address_labels: &HashMap<String, String>,
    ) -> Self {
        Self::_new(
            vote_account,
            current_epoch,
            version,
            skip_rate,
            address_labels,
            false,
        )
    }

    pub fn new_delinquent(
        vote_account: &RpcVoteAccountInfo,
        current_epoch: Epoch,
        version: CliVersion,
        skip_rate: Option<f64>,
        address_labels: &HashMap<String, String>,
    ) -> Self {
        Self::_new(
            vote_account,
            current_epoch,
            version,
            skip_rate,
            address_labels,
            true,
        )
    }

    fn _new(
        vote_account: &RpcVoteAccountInfo,
        current_epoch: Epoch,
        version: CliVersion,
        skip_rate: Option<f64>,
        address_labels: &HashMap<String, String>,
        delinquent: bool,
    ) -> Self {
        let (credits, epoch_credits) = vote_account
            .epoch_credits
            .iter()
            .find_map(|(epoch, credits, pre_credits)| {
                if *epoch == current_epoch {
                    Some((*credits, credits.saturating_sub(*pre_credits)))
                } else {
                    None
                }
            })
            .unwrap_or((0, 0));
        Self {
            identity_pubkey: format_labeled_address(&vote_account.node_pubkey, address_labels),
            vote_account_pubkey: format_labeled_address(&vote_account.vote_pubkey, address_labels),
            commission: vote_account.commission,
            last_vote: vote_account.last_vote,
            root_slot: vote_account.root_slot,
            credits,
            epoch_credits,
            activated_stake: vote_account.activated_stake,
            version,
            delinquent,
            skip_rate,
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliHistorySignatureVec(Vec<CliHistorySignature>);

impl CliHistorySignatureVec {
    pub fn new(list: Vec<CliHistorySignature>) -> Self {
        Self(list)
    }
}

impl QuietDisplay for CliHistorySignatureVec {}
impl VerboseDisplay for CliHistorySignatureVec {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        for signature in &self.0 {
            VerboseDisplay::write_str(signature, w)?;
        }
        writeln!(w, "{} transactions found", self.0.len())
    }
}

impl fmt::Display for CliHistorySignatureVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for signature in &self.0 {
            write!(f, "{signature}")?;
        }
        writeln!(f, "{} transactions found", self.0.len())
    }
}

#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CliHistorySignature {
    pub signature: String,
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub verbose: Option<CliHistoryVerbose>,
}

impl QuietDisplay for CliHistorySignature {}
impl VerboseDisplay for CliHistorySignature {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        let verbose = self
            .verbose
            .as_ref()
            .expect("should have verbose.is_some()");
        writeln!(
            w,
            "{} [slot={} {}status={}] {}",
            self.signature,
            verbose.slot,
            match verbose.block_time {
                None => "".to_string(),
                Some(block_time) => format!("timestamp={} ", unix_timestamp_to_string(block_time)),
            },
            if let Some(err) = &verbose.err {
                format!("Failed: {err:?}")
            } else {
                match &verbose.confirmation_status {
                    None => "Finalized".to_string(),
                    Some(status) => format!("{status:?}"),
                }
            },
            verbose.memo.clone().unwrap_or_default(),
        )
    }
}

impl fmt::Display for CliHistorySignature {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "{}", self.signature)
    }
}

#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CliHistoryVerbose {
    pub slot: Slot,
    pub block_time: Option<UnixTimestamp>,
    pub err: Option<UiTransactionError>,
    pub confirmation_status: Option<TransactionConfirmationStatus>,
    pub memo: Option<String>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliHistoryTransactionVec(Vec<CliTransactionConfirmation>);

impl CliHistoryTransactionVec {
    pub fn new(list: Vec<CliTransactionConfirmation>) -> Self {
        Self(list)
    }
}

impl QuietDisplay for CliHistoryTransactionVec {}
impl VerboseDisplay for CliHistoryTransactionVec {}

impl fmt::Display for CliHistoryTransactionVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for transaction in &self.0 {
            VerboseDisplay::write_str(transaction, f)?;
            writeln!(f)?;
        }
        writeln!(f, "{} transactions found", self.0.len())
    }
}

#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliNonceAccount {
    pub balance: u64,
    pub minimum_balance_for_rent_exemption: u64,
    pub nonce: Option<String>,
    pub lamports_per_signature: Option<u64>,
    pub authority: Option<String>,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}

impl QuietDisplay for CliNonceAccount {}
impl VerboseDisplay for CliNonceAccount {}

impl fmt::Display for CliNonceAccount {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "Balance: {}",
            build_balance_message(self.balance, self.use_lamports_unit, true)
        )?;
        writeln!(
            f,
            "Minimum Balance Required: {}",
            build_balance_message(
                self.minimum_balance_for_rent_exemption,
                self.use_lamports_unit,
                true
            )
        )?;
        let nonce = self.nonce.as_deref().unwrap_or("uninitialized");
        writeln!(f, "Nonce blockhash: {nonce}")?;
        if let Some(fees) = self.lamports_per_signature {
            writeln!(f, "Fee: {fees} lamports per signature")?;
        } else {
            writeln!(f, "Fees: uninitialized")?;
        }
        let authority = self.authority.as_deref().unwrap_or("uninitialized");
        writeln!(f, "Authority: {authority}")
    }
}

#[derive(Serialize, Deserialize)]
pub struct CliStakeVec(Vec<CliKeyedStakeState>);

impl CliStakeVec {
    pub fn new(list: Vec<CliKeyedStakeState>) -> Self {
        Self(list)
    }
}

impl QuietDisplay for CliStakeVec {}
impl VerboseDisplay for CliStakeVec {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        for state in &self.0 {
            writeln!(w)?;
            VerboseDisplay::write_str(state, w)?;
        }
        Ok(())
    }
}

impl fmt::Display for CliStakeVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for state in &self.0 {
            writeln!(f)?;
            write!(f, "{state}")?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliKeyedStakeState {
    pub stake_pubkey: String,
    #[serde(flatten)]
    pub stake_state: CliStakeState,
}

impl QuietDisplay for CliKeyedStakeState {}
impl VerboseDisplay for CliKeyedStakeState {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        writeln!(w, "Stake Pubkey: {}", self.stake_pubkey)?;
        VerboseDisplay::write_str(&self.stake_state, w)
    }
}

impl fmt::Display for CliKeyedStakeState {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "Stake Pubkey: {}", self.stake_pubkey)?;
        write!(f, "{}", self.stake_state)
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliEpochReward {
    pub epoch: Epoch,
    pub effective_slot: Slot,
    pub amount: u64,       // lamports
    pub post_balance: u64, // lamports
    pub percent_change: f64,
    pub apr: Option<f64>,
    pub commission: Option<u8>,
    pub block_time: UnixTimestamp,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliKeyedEpochReward {
    pub address: String,
    pub reward: Option<CliEpochReward>,
}

#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CliEpochRewardsMetadata {
    pub epoch: Epoch,
    #[deprecated(
        since = "2.2.0",
        note = "Please use CliEpochReward::effective_slot per reward"
    )]
    pub effective_slot: Slot,
    #[deprecated(
        since = "2.2.0",
        note = "Please use CliEpochReward::block_time per reward"
    )]
    pub block_time: UnixTimestamp,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliKeyedEpochRewards {
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub epoch_metadata: Option<CliEpochRewardsMetadata>,
    pub rewards: Vec<CliKeyedEpochReward>,
}

impl QuietDisplay for CliKeyedEpochRewards {}
impl VerboseDisplay for CliKeyedEpochRewards {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        if self.rewards.is_empty() {
            writeln!(w, "No rewards found in epoch")?;
            return Ok(());
        }

        if let Some(metadata) = &self.epoch_metadata {
            writeln!(w, "Epoch: {}", metadata.epoch)?;
        }
        writeln!(w, "Epoch Rewards:")?;
        writeln!(
            w,
            "  {:<44}  {:<11}  {:<23}  {:<18}  {:<20}  {:>14}  {:>7}  {:>10}",
            "Address",
            "Reward Slot",
            "Time",
            "Amount",
            "New Balance",
            "Percent Change",
            "APR",
            "Commission"
        )?;
        for keyed_reward in &self.rewards {
            match &keyed_reward.reward {
                Some(reward) => {
                    writeln!(
                        w,
                        "  {:<44}  {:<11}  {:<23}  ◎{:<17.9}  ◎{:<19.9}  {:>13.9}%  {:>7}  {:>10}",
                        keyed_reward.address,
                        reward.effective_slot,
                        Utc.timestamp_opt(reward.block_time, 0).unwrap(),
                        build_balance_message(reward.amount, false, false),
                        build_balance_message(reward.post_balance, false, false),
                        reward.percent_change,
                        reward
                            .apr
                            .map(|apr| format!("{apr:.2}%"))
                            .unwrap_or_default(),
                        reward
                            .commission
                            .map(|commission| format!("{commission}%"))
                            .unwrap_or_else(|| "-".to_string()),
                    )?;
                }
                None => {
                    writeln!(w, "  {:<44}  No rewards in epoch", keyed_reward.address,)?;
                }
            }
        }
        Ok(())
    }
}

impl fmt::Display for CliKeyedEpochRewards {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.rewards.is_empty() {
            writeln!(f, "No rewards found in epoch")?;
            return Ok(());
        }

        if let Some(metadata) = &self.epoch_metadata {
            writeln!(f, "Epoch: {}", metadata.epoch)?;
        }
        writeln!(f, "Epoch Rewards:")?;
        writeln!(
            f,
            "  {:<44}  {:<18}  {:<18}  {:>14}  {:>7}  {:>10}",
            "Address", "Amount", "New Balance", "Percent Change", "APR", "Commission"
        )?;
        for keyed_reward in &self.rewards {
            match &keyed_reward.reward {
                Some(reward) => {
                    writeln!(
                        f,
                        "  {:<44}  ◎{:<17.9}  ◎{:<17.9}  {:>13.9}%  {:>7}  {:>10}",
                        keyed_reward.address,
                        build_balance_message(reward.amount, false, false),
                        build_balance_message(reward.post_balance, false, false),
                        reward.percent_change,
                        reward
                            .apr
                            .map(|apr| format!("{apr:.2}%"))
                            .unwrap_or_default(),
                        reward
                            .commission
                            .map(|commission| format!("{commission}%"))
                            .unwrap_or_else(|| "-".to_string())
                    )?;
                }
                None => {
                    writeln!(f, "  {:<44}  No rewards in epoch", keyed_reward.address,)?;
                }
            }
        }
        Ok(())
    }
}

fn show_votes_and_credits(
    f: &mut fmt::Formatter,
    votes: &[CliLandedVote],
    epoch_voting_history: &[CliEpochVotingHistory],
) -> fmt::Result {
    if votes.is_empty() {
        return Ok(());
    }

    // Existence of this should guarantee the occurrence of vote truncation
    let newest_history_entry = epoch_voting_history.iter().next_back();

    writeln!(
        f,
        "{} Votes (using {}/{} entries):",
        (if newest_history_entry.is_none() {
            "All"
        } else {
            "Recent"
        }),
        votes.len(),
        MAX_LOCKOUT_HISTORY
    )?;

    for vote in votes.iter().rev() {
        write!(
            f,
            "- slot: {} (confirmation count: {})",
            vote.slot, vote.confirmation_count
        )?;
        if vote.latency == 0 {
            writeln!(f)?;
        } else {
            writeln!(f, " (latency {})", vote.latency)?;
        }
    }
    if let Some(newest) = newest_history_entry {
        writeln!(
            f,
            "- ... (truncated {} rooted votes, which have been credited)",
            newest.credits
        )?;
    }

    if !epoch_voting_history.is_empty() {
        writeln!(
            f,
            "{} Epoch Voting History (using {}/{} entries):",
            (if epoch_voting_history.len() < MAX_EPOCH_CREDITS_HISTORY {
                "All"
            } else {
                "Recent"
            }),
            epoch_voting_history.len(),
            MAX_EPOCH_CREDITS_HISTORY
        )?;
        writeln!(
            f,
            "* missed credits include slots unavailable to vote on due to delinquent leaders",
        )?;
    }

    for entry in epoch_voting_history.iter().rev() {
        writeln!(
            f, // tame fmt so that this will be folded like following
            "- epoch: {}",
            entry.epoch
        )?;
        writeln!(
            f,
            "  credits range: ({}..{}]",
            entry.prev_credits, entry.credits
        )?;
        writeln!(
            f,
            "  credits/max credits: {}/{}",
            entry.credits_earned,
            entry.slots_in_epoch * u64::from(entry.max_credits_per_slot)
        )?;
    }
    if let Some(oldest) = epoch_voting_history.iter().next() {
        if oldest.prev_credits > 0 {
            // Oldest entry doesn't start with 0. so history must be truncated...

            // count of this combined pseudo credits range: (0..=oldest.prev_credits] like the above
            // (or this is just [1..=oldest.prev_credits] for human's simpler minds)
            let count = oldest.prev_credits;

            writeln!(
                f,
                "- ... (omitting {count} past rooted votes, which have already been credited)"
            )?;
        }
    }

    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<Vec<CliEpochReward>>,
    use_csv: bool,
) -> fmt::Result {
    if let Some(epoch_rewards) = epoch_rewards {
        if epoch_rewards.is_empty() {
            return Ok(());
        }

        writeln!(f, "Epoch Rewards:")?;
        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",
            "Amount",
            "New Balance",
            "Percent Change",
            "APR",
            "Commission",
        )?;
        for reward in epoch_rewards {
            format_as!(
                f,
                "{},{},{},{},{},{}%,{},{}",
                "  {:<6}  {:<11}  {:<26}  ◎{:<17.11}  ◎{:<17.11}  {:>13.3}%  {:>14}  {:>10}",
                fmt,
                reward.epoch,
                reward.effective_slot,
                Utc.timestamp_opt(reward.block_time, 0).unwrap(),
                build_balance_message(reward.amount, false, false),
                build_balance_message(reward.post_balance, false, false),
                reward.percent_change,
                reward
                    .apr
                    .map(|apr| format!("{apr:.2}%"))
                    .unwrap_or_default(),
                reward
                    .commission
                    .map(|commission| format!("{commission}%"))
                    .unwrap_or_else(|| "-".to_string())
            )?;
        }
    }
    Ok(())
}

#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliStakeState {
    pub stake_type: CliStakeType,
    pub account_balance: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub credits_observed: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub delegated_stake: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub delegated_vote_account_address: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub activation_epoch: Option<Epoch>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deactivation_epoch: Option<Epoch>,
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub authorized: Option<CliAuthorized>,
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub lockup: Option<CliLockup>,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
    #[serde(skip_serializing)]
    pub current_epoch: Epoch,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rent_exempt_reserve: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub active_stake: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub activating_stake: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub deactivating_stake: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub epoch_rewards: Option<Vec<CliEpochReward>>,
    #[serde(skip_serializing)]
    pub use_csv: bool,
}

impl QuietDisplay for CliStakeState {}
impl VerboseDisplay for CliStakeState {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        write!(w, "{self}")?;
        if let Some(credits) = self.credits_observed {
            writeln!(w, "Credits Observed: {credits}")?;
        }
        Ok(())
    }
}

fn show_inactive_stake(
    me: &CliStakeState,
    f: &mut fmt::Formatter,
    delegated_stake: u64,
) -> fmt::Result {
    if let Some(deactivation_epoch) = me.deactivation_epoch {
        if me.current_epoch > deactivation_epoch {
            let deactivating_stake = me.deactivating_stake.or(me.active_stake);
            if let Some(deactivating_stake) = deactivating_stake {
                writeln!(
                    f,
                    "Inactive Stake: {}",
                    build_balance_message(
                        delegated_stake - deactivating_stake,
                        me.use_lamports_unit,
                        true
                    ),
                )?;
                writeln!(
                    f,
                    "Deactivating Stake: {}",
                    build_balance_message(deactivating_stake, me.use_lamports_unit, true),
                )?;
            }
        }
        writeln!(
            f,
            "Stake deactivates starting from epoch: {deactivation_epoch}"
        )?;
    }
    if let Some(delegated_vote_account_address) = &me.delegated_vote_account_address {
        writeln!(
            f,
            "Delegated Vote Account Address: {delegated_vote_account_address}"
        )?;
    }
    Ok(())
}

fn show_active_stake(
    me: &CliStakeState,
    f: &mut fmt::Formatter,
    delegated_stake: u64,
) -> fmt::Result {
    if me
        .deactivation_epoch
        .map(|d| me.current_epoch <= d)
        .unwrap_or(true)
    {
        let active_stake = me.active_stake.unwrap_or(0);
        writeln!(
            f,
            "Active Stake: {}",
            build_balance_message(active_stake, me.use_lamports_unit, true),
        )?;
        let activating_stake = me.activating_stake.or_else(|| {
            if me.active_stake.is_none() {
                Some(delegated_stake)
            } else {
                None
            }
        });
        if let Some(activating_stake) = activating_stake {
            writeln!(
                f,
                "Activating Stake: {}",
                build_balance_message(activating_stake, me.use_lamports_unit, true),
            )?;
            writeln!(
                f,
                "Stake activates starting from epoch: {}",
                me.activation_epoch.unwrap()
            )?;
        }
    }
    Ok(())
}

impl fmt::Display for CliStakeState {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fn show_authorized(f: &mut fmt::Formatter, authorized: &CliAuthorized) -> fmt::Result {
            writeln!(f, "Stake Authority: {}", authorized.staker)?;
            writeln!(f, "Withdraw Authority: {}", authorized.withdrawer)?;
            Ok(())
        }
        fn show_lockup(f: &mut fmt::Formatter, lockup: Option<&CliLockup>) -> fmt::Result {
            if let Some(lockup) = lockup {
                if lockup.unix_timestamp != UnixTimestamp::default() {
                    writeln!(
                        f,
                        "Lockup Timestamp: {}",
                        unix_timestamp_to_string(lockup.unix_timestamp)
                    )?;
                }
                if lockup.epoch != Epoch::default() {
                    writeln!(f, "Lockup Epoch: {}", lockup.epoch)?;
                }
                writeln!(f, "Lockup Custodian: {}", lockup.custodian)?;
            }
            Ok(())
        }

        writeln!(
            f,
            "Balance: {}",
            build_balance_message(self.account_balance, self.use_lamports_unit, true)
        )?;

        if let Some(rent_exempt_reserve) = self.rent_exempt_reserve {
            writeln!(
                f,
                "Rent Exempt Reserve: {}",
                build_balance_message(rent_exempt_reserve, self.use_lamports_unit, true)
            )?;
        }

        match self.stake_type {
            CliStakeType::RewardsPool => writeln!(f, "Stake account is a rewards pool")?,
            CliStakeType::Uninitialized => writeln!(f, "Stake account is uninitialized")?,
            CliStakeType::Initialized => {
                writeln!(f, "Stake account is undelegated")?;
                show_authorized(f, self.authorized.as_ref().unwrap())?;
                show_lockup(f, self.lockup.as_ref())?;
            }
            CliStakeType::Stake => {
                let show_delegation = {
                    self.active_stake.is_some()
                        || self.activating_stake.is_some()
                        || self.deactivating_stake.is_some()
                        || self
                            .deactivation_epoch
                            .map(|de| de > self.current_epoch)
                            .unwrap_or(true)
                };
                if show_delegation {
                    let delegated_stake = self.delegated_stake.unwrap();
                    writeln!(
                        f,
                        "Delegated Stake: {}",
                        build_balance_message(delegated_stake, self.use_lamports_unit, true)
                    )?;
                    show_active_stake(self, f, delegated_stake)?;
                    show_inactive_stake(self, f, delegated_stake)?;
                } else {
                    writeln!(f, "Stake account is undelegated")?;
                }
                show_authorized(f, self.authorized.as_ref().unwrap())?;
                show_lockup(f, self.lockup.as_ref())?;
                show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?
            }
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum CliStakeType {
    Stake,
    RewardsPool,
    Uninitialized,
    Initialized,
}

impl Default for CliStakeType {
    fn default() -> Self {
        Self::Uninitialized
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliStakeHistory {
    pub entries: Vec<CliStakeHistoryEntry>,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}

impl QuietDisplay for CliStakeHistory {}
impl VerboseDisplay for CliStakeHistory {}

impl fmt::Display for CliStakeHistory {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "{}",
            style(format!(
                "  {:<5}  {:>20}  {:>20}  {:>20}",
                "Epoch", "Effective Stake", "Activating Stake", "Deactivating Stake",
            ))
            .bold()
        )?;
        let config = BuildBalanceMessageConfig {
            use_lamports_unit: self.use_lamports_unit,
            show_unit: false,
            trim_trailing_zeros: false,
        };
        for entry in &self.entries {
            writeln!(
                f,
                "  {:>5}  {:>20}  {:>20}  {:>20} {}",
                entry.epoch,
                build_balance_message_with_config(entry.effective_stake, &config),
                build_balance_message_with_config(entry.activating_stake, &config),
                build_balance_message_with_config(entry.deactivating_stake, &config),
                if self.use_lamports_unit {
                    "lamports"
                } else {
                    "SOL"
                }
            )?;
        }
        Ok(())
    }
}

impl From<&(Epoch, StakeHistoryEntry)> for CliStakeHistoryEntry {
    fn from((epoch, entry): &(Epoch, StakeHistoryEntry)) -> Self {
        Self {
            epoch: *epoch,
            effective_stake: entry.effective,
            activating_stake: entry.activating,
            deactivating_stake: entry.deactivating,
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliStakeHistoryEntry {
    pub epoch: Epoch,
    pub effective_stake: u64,
    pub activating_stake: u64,
    pub deactivating_stake: u64,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliAuthorized {
    pub staker: String,
    pub withdrawer: String,
}

impl From<&Authorized> for CliAuthorized {
    fn from(authorized: &Authorized) -> Self {
        Self {
            staker: authorized.staker.to_string(),
            withdrawer: authorized.withdrawer.to_string(),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliLockup {
    pub unix_timestamp: UnixTimestamp,
    pub epoch: Epoch,
    pub custodian: String,
}

impl From<&Lockup> for CliLockup {
    fn from(lockup: &Lockup) -> Self {
        Self {
            unix_timestamp: lockup.unix_timestamp,
            epoch: lockup.epoch,
            custodian: lockup.custodian.to_string(),
        }
    }
}

#[derive(Serialize, Deserialize)]
pub struct CliValidatorInfoVec(Vec<CliValidatorInfo>);

impl CliValidatorInfoVec {
    pub fn new(list: Vec<CliValidatorInfo>) -> Self {
        Self(list)
    }
}

impl QuietDisplay for CliValidatorInfoVec {}
impl VerboseDisplay for CliValidatorInfoVec {}

impl fmt::Display for CliValidatorInfoVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.0.is_empty() {
            writeln!(f, "No validator info accounts found")?;
        }
        for validator_info in &self.0 {
            writeln!(f)?;
            write!(f, "{validator_info}")?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliValidatorInfo {
    pub identity_pubkey: String,
    pub info_pubkey: String,
    pub info: Map<String, Value>,
}

impl QuietDisplay for CliValidatorInfo {}
impl VerboseDisplay for CliValidatorInfo {}

impl fmt::Display for CliValidatorInfo {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln_name_value(f, "Validator Identity:", &self.identity_pubkey)?;
        writeln_name_value(f, "  Info Address:", &self.info_pubkey)?;
        for (key, value) in self.info.iter() {
            writeln_name_value(
                f,
                &format!("  {}:", to_title_case(key)),
                value.as_str().unwrap_or("?"),
            )?;
        }
        Ok(())
    }
}

#[derive(Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliVoteAccount {
    pub account_balance: u64,
    pub validator_identity: String,
    #[serde(flatten)]
    pub authorized_voters: CliAuthorizedVoters,
    pub authorized_withdrawer: String,
    pub credits: u64,
    pub commission: u8,
    pub root_slot: Option<Slot>,
    pub recent_timestamp: BlockTimestamp,
    pub votes: Vec<CliLandedVote>,
    pub epoch_voting_history: Vec<CliEpochVotingHistory>,
    #[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<Vec<CliEpochReward>>,
}

impl QuietDisplay for CliVoteAccount {}
impl VerboseDisplay for CliVoteAccount {}

impl fmt::Display for CliVoteAccount {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "Account Balance: {}",
            build_balance_message(self.account_balance, self.use_lamports_unit, true)
        )?;
        writeln!(f, "Validator Identity: {}", self.validator_identity)?;
        writeln!(f, "Vote Authority: {}", self.authorized_voters)?;
        writeln!(f, "Withdraw Authority: {}", self.authorized_withdrawer)?;
        writeln!(f, "Credits: {}", self.credits)?;
        writeln!(f, "Commission: {}%", self.commission)?;
        writeln!(
            f,
            "Root Slot: {}",
            match self.root_slot {
                Some(slot) => slot.to_string(),
                None => "~".to_string(),
            }
        )?;
        writeln!(
            f,
            "Recent Timestamp: {} from slot {}",
            unix_timestamp_to_string(self.recent_timestamp.timestamp),
            self.recent_timestamp.slot
        )?;
        show_votes_and_credits(f, &self.votes, &self.epoch_voting_history)?;
        show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?;
        Ok(())
    }
}

#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliAuthorizedVoters {
    authorized_voters: BTreeMap<Epoch, String>,
}

impl QuietDisplay for CliAuthorizedVoters {}
impl VerboseDisplay for CliAuthorizedVoters {}

impl fmt::Display for CliAuthorizedVoters {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if let Some((_epoch, current_authorized_voter)) = self.authorized_voters.first_key_value() {
            write!(f, "{current_authorized_voter}")?;
        } else {
            write!(f, "None")?;
        }
        if self.authorized_voters.len() > 1 {
            let (epoch, upcoming_authorized_voter) = self
                .authorized_voters
                .last_key_value()
                .expect("CliAuthorizedVoters::authorized_voters.len() > 1");
            writeln!(f)?;
            write!(
                f,
                "  New Vote Authority as of Epoch {epoch}: {upcoming_authorized_voter}"
            )?;
        }
        Ok(())
    }
}

impl From<&AuthorizedVoters> for CliAuthorizedVoters {
    fn from(authorized_voters: &AuthorizedVoters) -> Self {
        let mut voter_map: BTreeMap<Epoch, String> = BTreeMap::new();
        for (epoch, voter) in authorized_voters.iter() {
            voter_map.insert(*epoch, voter.to_string());
        }
        Self {
            authorized_voters: voter_map,
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliEpochVotingHistory {
    pub epoch: Epoch,
    pub slots_in_epoch: u64,
    pub credits_earned: u64,
    pub credits: u64,
    pub prev_credits: u64,
    pub max_credits_per_slot: u8,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliLandedVote {
    pub latency: u8,
    pub slot: Slot,
    pub confirmation_count: u32,
}

impl From<&LandedVote> for CliLandedVote {
    fn from(landed_vote: &LandedVote) -> Self {
        Self {
            latency: landed_vote.latency,
            slot: landed_vote.slot(),
            confirmation_count: landed_vote.confirmation_count(),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliBlockTime {
    pub slot: Slot,
    pub timestamp: UnixTimestamp,
}

impl QuietDisplay for CliBlockTime {}
impl VerboseDisplay for CliBlockTime {}

impl fmt::Display for CliBlockTime {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln_name_value(f, "Block:", &self.slot.to_string())?;
        writeln_name_value(f, "Date:", &unix_timestamp_to_string(self.timestamp))
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliLeaderSchedule {
    pub epoch: Epoch,
    pub leader_schedule_entries: Vec<CliLeaderScheduleEntry>,
}

impl QuietDisplay for CliLeaderSchedule {}
impl VerboseDisplay for CliLeaderSchedule {}

impl fmt::Display for CliLeaderSchedule {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        for entry in &self.leader_schedule_entries {
            writeln!(f, "  {:<15} {:<44}", entry.slot, entry.leader)?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliLeaderScheduleEntry {
    pub slot: Slot,
    pub leader: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliInflation {
    pub governor: RpcInflationGovernor,
    pub current_rate: RpcInflationRate,
}

impl QuietDisplay for CliInflation {}
impl VerboseDisplay for CliInflation {}

impl fmt::Display for CliInflation {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f, "{}", style("Inflation Governor:").bold())?;
        if (self.governor.initial - self.governor.terminal).abs() < f64::EPSILON {
            writeln!(
                f,
                "Fixed rate:              {:>5.2}%",
                self.governor.terminal * 100.
            )?;
        } else {
            writeln!(
                f,
                "Initial rate:            {:>5.2}%",
                self.governor.initial * 100.
            )?;
            writeln!(
                f,
                "Terminal rate:           {:>5.2}%",
                self.governor.terminal * 100.
            )?;
            writeln!(
                f,
                "Rate reduction per year: {:>5.2}%",
                self.governor.taper * 100.
            )?;
            writeln!(
                f,
                "* Rate reduction is derived using the target slot time in genesis config"
            )?;
        }
        if self.governor.foundation_term > 0. {
            writeln!(
                f,
                "Foundation percentage:   {:>5.2}%",
                self.governor.foundation
            )?;
            writeln!(
                f,
                "Foundation term:         {:.1} years",
                self.governor.foundation_term
            )?;
        }

        writeln!(
            f,
            "\n{}",
            style(format!("Inflation for Epoch {}:", self.current_rate.epoch)).bold()
        )?;
        writeln!(
            f,
            "Total rate:              {:>5.2}%",
            self.current_rate.total * 100.
        )?;
        writeln!(
            f,
            "Staking rate:            {:>5.2}%",
            self.current_rate.validator * 100.
        )?;

        if self.current_rate.foundation > 0. {
            writeln!(
                f,
                "Foundation rate:         {:>5.2}%",
                self.current_rate.foundation * 100.
            )?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CliSignOnlyData {
    pub blockhash: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub signers: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub absent: Vec<String>,
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub bad_sig: Vec<String>,
}

impl QuietDisplay for CliSignOnlyData {}
impl VerboseDisplay for CliSignOnlyData {}

impl fmt::Display for CliSignOnlyData {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Blockhash:", &self.blockhash)?;
        if let Some(message) = self.message.as_ref() {
            writeln_name_value(f, "Transaction Message:", message)?;
        }
        if !self.signers.is_empty() {
            writeln!(f, "{}", style("Signers (Pubkey=Signature):").bold())?;
            for signer in self.signers.iter() {
                writeln!(f, " {signer}")?;
            }
        }
        if !self.absent.is_empty() {
            writeln!(f, "{}", style("Absent Signers (Pubkey):").bold())?;
            for pubkey in self.absent.iter() {
                writeln!(f, " {pubkey}")?;
            }
        }
        if !self.bad_sig.is_empty() {
            writeln!(f, "{}", style("Bad Signatures (Pubkey):").bold())?;
            for pubkey in self.bad_sig.iter() {
                writeln!(f, " {pubkey}")?;
            }
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliSignature {
    pub signature: String,
}

impl QuietDisplay for CliSignature {}
impl VerboseDisplay for CliSignature {}

impl fmt::Display for CliSignature {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Signature:", &self.signature)?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliAccountBalances {
    pub accounts: Vec<RpcAccountBalance>,
}

impl QuietDisplay for CliAccountBalances {}
impl VerboseDisplay for CliAccountBalances {}

impl fmt::Display for CliAccountBalances {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "{}",
            style(format!("{:<44}  {}", "Address", "Balance")).bold()
        )?;
        for account in &self.accounts {
            writeln!(
                f,
                "{:<44}  {}",
                account.address,
                &format!(
                    "{} SOL",
                    build_balance_message(account.lamports, false, false)
                ),
            )?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliSupply {
    pub total: u64,
    pub circulating: u64,
    pub non_circulating: u64,
    pub non_circulating_accounts: Vec<String>,
    #[serde(skip_serializing)]
    pub print_accounts: bool,
}

impl From<RpcSupply> for CliSupply {
    fn from(rpc_supply: RpcSupply) -> Self {
        Self {
            total: rpc_supply.total,
            circulating: rpc_supply.circulating,
            non_circulating: rpc_supply.non_circulating,
            non_circulating_accounts: rpc_supply.non_circulating_accounts,
            print_accounts: false,
        }
    }
}

impl QuietDisplay for CliSupply {}
impl VerboseDisplay for CliSupply {}

impl fmt::Display for CliSupply {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln_name_value(
            f,
            "Total:",
            &format!("{} SOL", build_balance_message(self.total, false, false)),
        )?;
        writeln_name_value(
            f,
            "Circulating:",
            &format!(
                "{} SOL",
                build_balance_message(self.circulating, false, false)
            ),
        )?;
        writeln_name_value(
            f,
            "Non-Circulating:",
            &format!(
                "{} SOL",
                build_balance_message(self.non_circulating, false, false)
            ),
        )?;
        if self.print_accounts {
            writeln!(f)?;
            writeln_name_value(f, "Non-Circulating Accounts:", " ")?;
            for account in &self.non_circulating_accounts {
                writeln!(f, "  {account}")?;
            }
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliFeesInner {
    pub slot: Slot,
    pub blockhash: String,
    pub lamports_per_signature: u64,
    pub last_valid_slot: Option<Slot>,
    pub last_valid_block_height: Option<Slot>,
}

impl QuietDisplay for CliFeesInner {}
impl VerboseDisplay for CliFeesInner {}

impl fmt::Display for CliFeesInner {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln_name_value(f, "Blockhash:", &self.blockhash)?;
        writeln_name_value(
            f,
            "Lamports per signature:",
            &self.lamports_per_signature.to_string(),
        )?;
        let last_valid_block_height = self
            .last_valid_block_height
            .map(|s| s.to_string())
            .unwrap_or_default();
        writeln_name_value(f, "Last valid block height:", &last_valid_block_height)
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliFees {
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub inner: Option<CliFeesInner>,
}

impl QuietDisplay for CliFees {}
impl VerboseDisplay for CliFees {}

impl fmt::Display for CliFees {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self.inner.as_ref() {
            Some(inner) => write!(f, "{inner}"),
            None => write!(f, "Fees unavailable"),
        }
    }
}

impl CliFees {
    pub fn some(
        slot: Slot,
        blockhash: Hash,
        lamports_per_signature: u64,
        last_valid_slot: Option<Slot>,
        last_valid_block_height: Option<Slot>,
    ) -> Self {
        Self {
            inner: Some(CliFeesInner {
                slot,
                blockhash: blockhash.to_string(),
                lamports_per_signature,
                last_valid_slot,
                last_valid_block_height,
            }),
        }
    }
    pub fn none() -> Self {
        Self { inner: None }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliTokenAccount {
    pub address: String,
    #[serde(flatten)]
    pub token_account: UiTokenAccount,
}

impl QuietDisplay for CliTokenAccount {}
impl VerboseDisplay for CliTokenAccount {}

impl fmt::Display for CliTokenAccount {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Address:", &self.address)?;
        let account = &self.token_account;
        writeln_name_value(
            f,
            "Balance:",
            &account.token_amount.real_number_string_trimmed(),
        )?;
        let mint = format!(
            "{}{}",
            account.mint,
            if account.is_native { " (native)" } else { "" }
        );
        writeln_name_value(f, "Mint:", &mint)?;
        writeln_name_value(f, "Owner:", &account.owner)?;
        writeln_name_value(f, "State:", &format!("{:?}", account.state))?;
        if let Some(delegate) = &account.delegate {
            writeln!(f, "Delegation:")?;
            writeln_name_value(f, "  Delegate:", delegate)?;
            let allowance = account.delegated_amount.as_ref().unwrap();
            writeln_name_value(f, "  Allowance:", &allowance.real_number_string_trimmed())?;
        }
        writeln_name_value(
            f,
            "Close authority:",
            account.close_authority.as_ref().unwrap_or(&String::new()),
        )?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliProgramId {
    pub program_id: String,
    pub signature: Option<String>,
}

impl QuietDisplay for CliProgramId {}
impl VerboseDisplay for CliProgramId {}

impl fmt::Display for CliProgramId {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln_name_value(f, "Program Id:", &self.program_id)?;
        if let Some(ref signature) = self.signature {
            writeln!(f)?;
            writeln_name_value(f, "Signature:", signature)?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliProgramBuffer {
    pub buffer: String,
}

impl QuietDisplay for CliProgramBuffer {}
impl VerboseDisplay for CliProgramBuffer {}

impl fmt::Display for CliProgramBuffer {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln_name_value(f, "Buffer:", &self.buffer)
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CliProgramAccountType {
    Buffer,
    Program,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliProgramAuthority {
    pub authority: String,
    pub account_type: CliProgramAccountType,
}

impl QuietDisplay for CliProgramAuthority {}
impl VerboseDisplay for CliProgramAuthority {}

impl fmt::Display for CliProgramAuthority {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln_name_value(f, "Account Type:", &format!("{:?}", self.account_type))?;
        writeln_name_value(f, "Authority:", &self.authority)
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliProgram {
    pub program_id: String,
    pub owner: String,
    pub data_len: usize,
}
impl QuietDisplay for CliProgram {}
impl VerboseDisplay for CliProgram {}
impl fmt::Display for CliProgram {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Program Id:", &self.program_id)?;
        writeln_name_value(f, "Owner:", &self.owner)?;
        writeln_name_value(
            f,
            "Data Length:",
            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
        )?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliProgramV4 {
    pub program_id: String,
    pub owner: String,
    pub authority: String,
    pub last_deploy_slot: u64,
    pub status: String,
    pub data_len: usize,
}
impl QuietDisplay for CliProgramV4 {}
impl VerboseDisplay for CliProgramV4 {}
impl fmt::Display for CliProgramV4 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Program Id:", &self.program_id)?;
        writeln_name_value(f, "Owner:", &self.owner)?;
        writeln_name_value(f, "Authority:", &self.authority)?;
        writeln_name_value(
            f,
            "Last Deployed In Slot:",
            &self.last_deploy_slot.to_string(),
        )?;
        writeln_name_value(f, "Status:", &self.status)?;
        writeln_name_value(
            f,
            "Data Length:",
            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
        )?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliProgramsV4 {
    pub programs: Vec<CliProgramV4>,
}
impl QuietDisplay for CliProgramsV4 {}
impl VerboseDisplay for CliProgramsV4 {}
impl fmt::Display for CliProgramsV4 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "{}",
            style(format!(
                "{:<44} | {:<9} | {:<44} | {:<10}",
                "Program Id", "Slot", "Authority", "Status"
            ))
            .bold()
        )?;
        for program in self.programs.iter() {
            writeln!(
                f,
                "{}",
                &format!(
                    "{:<44} | {:<9} | {:<44} | {:<10}",
                    program.program_id, program.last_deploy_slot, program.authority, program.status,
                )
            )?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableProgram {
    pub program_id: String,
    pub owner: String,
    pub programdata_address: String,
    pub authority: String,
    pub last_deploy_slot: u64,
    pub data_len: usize,
    pub lamports: u64,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}
impl QuietDisplay for CliUpgradeableProgram {}
impl VerboseDisplay for CliUpgradeableProgram {}
impl fmt::Display for CliUpgradeableProgram {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Program Id:", &self.program_id)?;
        writeln_name_value(f, "Owner:", &self.owner)?;
        writeln_name_value(f, "ProgramData Address:", &self.programdata_address)?;
        writeln_name_value(f, "Authority:", &self.authority)?;
        writeln_name_value(
            f,
            "Last Deployed In Slot:",
            &self.last_deploy_slot.to_string(),
        )?;
        writeln_name_value(
            f,
            "Data Length:",
            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
        )?;
        writeln_name_value(
            f,
            "Balance:",
            &build_balance_message(self.lamports, self.use_lamports_unit, true),
        )?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeablePrograms {
    pub programs: Vec<CliUpgradeableProgram>,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}
impl QuietDisplay for CliUpgradeablePrograms {}
impl VerboseDisplay for CliUpgradeablePrograms {}
impl fmt::Display for CliUpgradeablePrograms {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "{}",
            style(format!(
                "{:<44} | {:<9} | {:<44} | {}",
                "Program Id", "Slot", "Authority", "Balance"
            ))
            .bold()
        )?;
        for program in self.programs.iter() {
            writeln!(
                f,
                "{}",
                &format!(
                    "{:<44} | {:<9} | {:<44} | {}",
                    program.program_id,
                    program.last_deploy_slot,
                    program.authority,
                    build_balance_message(program.lamports, self.use_lamports_unit, true)
                )
            )?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableProgramClosed {
    pub program_id: String,
    pub lamports: u64,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}
impl QuietDisplay for CliUpgradeableProgramClosed {}
impl VerboseDisplay for CliUpgradeableProgramClosed {}
impl fmt::Display for CliUpgradeableProgramClosed {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "Closed Program Id {}, {} reclaimed",
            &self.program_id,
            &build_balance_message(self.lamports, self.use_lamports_unit, true)
        )?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableProgramExtended {
    pub program_id: String,
    pub additional_bytes: u32,
}
impl QuietDisplay for CliUpgradeableProgramExtended {}
impl VerboseDisplay for CliUpgradeableProgramExtended {}
impl fmt::Display for CliUpgradeableProgramExtended {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "Extended Program Id {} by {} bytes",
            &self.program_id, self.additional_bytes,
        )?;
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableProgramMigrated {
    pub program_id: String,
}
impl QuietDisplay for CliUpgradeableProgramMigrated {}
impl VerboseDisplay for CliUpgradeableProgramMigrated {}
impl fmt::Display for CliUpgradeableProgramMigrated {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "Migrated Program Id {} from loader-v3 to loader-v4",
            &self.program_id,
        )?;
        Ok(())
    }
}

#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableBuffer {
    pub address: String,
    pub authority: String,
    pub data_len: usize,
    pub lamports: u64,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}
impl QuietDisplay for CliUpgradeableBuffer {}
impl VerboseDisplay for CliUpgradeableBuffer {}
impl fmt::Display for CliUpgradeableBuffer {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Buffer Address:", &self.address)?;
        writeln_name_value(f, "Authority:", &self.authority)?;
        writeln_name_value(
            f,
            "Balance:",
            &build_balance_message(self.lamports, self.use_lamports_unit, true),
        )?;
        writeln_name_value(
            f,
            "Data Length:",
            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
        )?;

        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliUpgradeableBuffers {
    pub buffers: Vec<CliUpgradeableBuffer>,
    #[serde(skip_serializing)]
    pub use_lamports_unit: bool,
}
impl QuietDisplay for CliUpgradeableBuffers {}
impl VerboseDisplay for CliUpgradeableBuffers {}
impl fmt::Display for CliUpgradeableBuffers {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln!(
            f,
            "{}",
            style(format!(
                "{:<44} | {:<44} | {}",
                "Buffer Address", "Authority", "Balance"
            ))
            .bold()
        )?;
        for buffer in self.buffers.iter() {
            writeln!(
                f,
                "{}",
                &format!(
                    "{:<44} | {:<44} | {}",
                    buffer.address,
                    buffer.authority,
                    build_balance_message(buffer.lamports, self.use_lamports_unit, true)
                )
            )?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CliAddressLookupTable {
    pub lookup_table_address: String,
    pub authority: Option<String>,
    pub deactivation_slot: u64,
    pub last_extended_slot: u64,
    pub addresses: Vec<String>,
}
impl QuietDisplay for CliAddressLookupTable {}
impl VerboseDisplay for CliAddressLookupTable {}
impl fmt::Display for CliAddressLookupTable {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
        if let Some(authority) = &self.authority {
            writeln_name_value(f, "Authority:", authority)?;
        } else {
            writeln_name_value(f, "Authority:", "None (frozen)")?;
        }
        if self.deactivation_slot == u64::MAX {
            writeln_name_value(f, "Deactivation Slot:", "None (still active)")?;
        } else {
            writeln_name_value(f, "Deactivation Slot:", &self.deactivation_slot.to_string())?;
        }
        if self.last_extended_slot == 0 {
            writeln_name_value(f, "Last Extended Slot:", "None (empty)")?;
        } else {
            writeln_name_value(
                f,
                "Last Extended Slot:",
                &self.last_extended_slot.to_string(),
            )?;
        }
        if self.addresses.is_empty() {
            writeln_name_value(f, "Address Table Entries:", "None (empty)")?;
        } else {
            writeln!(f, "{}", style("Address Table Entries:".to_string()).bold())?;
            writeln!(f)?;
            writeln!(
                f,
                "{}",
                style(format!("  {:<5}  {}", "Index", "Address")).bold()
            )?;
            for (index, address) in self.addresses.iter().enumerate() {
                writeln!(f, "  {index:<5}  {address}")?;
            }
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliAddressLookupTableCreated {
    pub lookup_table_address: String,
    pub signature: String,
}
impl QuietDisplay for CliAddressLookupTableCreated {}
impl VerboseDisplay for CliAddressLookupTableCreated {}
impl fmt::Display for CliAddressLookupTableCreated {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Signature:", &self.signature)?;
        writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
        Ok(())
    }
}

#[derive(Debug, Default)]
pub struct ReturnSignersConfig {
    pub dump_transaction_message: bool,
}

pub fn return_signers(
    tx: &Transaction,
    output_format: &OutputFormat,
) -> Result<String, Box<dyn std::error::Error>> {
    return_signers_with_config(tx, output_format, &ReturnSignersConfig::default())
}

pub fn return_signers_with_config(
    tx: &Transaction,
    output_format: &OutputFormat,
    config: &ReturnSignersConfig,
) -> Result<String, Box<dyn std::error::Error>> {
    let cli_command = return_signers_data(tx, config);
    Ok(output_format.formatted_string(&cli_command))
}

pub fn return_signers_data(tx: &Transaction, config: &ReturnSignersConfig) -> CliSignOnlyData {
    let verify_results = tx.verify_with_results();
    let mut signers = Vec::new();
    let mut absent = Vec::new();
    let mut bad_sig = Vec::new();
    tx.signatures
        .iter()
        .zip(tx.message.account_keys.iter())
        .zip(verify_results)
        .for_each(|((sig, key), res)| {
            if res {
                signers.push(format!("{key}={sig}"))
            } else if *sig == Signature::default() {
                absent.push(key.to_string());
            } else {
                bad_sig.push(key.to_string());
            }
        });
    let message = if config.dump_transaction_message {
        let message_data = tx.message_data();
        Some(BASE64_STANDARD.encode(message_data))
    } else {
        None
    };

    CliSignOnlyData {
        blockhash: tx.message.recent_blockhash.to_string(),
        message,
        signers,
        absent,
        bad_sig,
    }
}

pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly {
    let object: Value = serde_json::from_str(reply).unwrap();
    let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
    let blockhash = blockhash_str.parse::<Hash>().unwrap();
    let mut present_signers: Vec<(Pubkey, Signature)> = Vec::new();
    let signer_strings = object.get("signers");
    if let Some(sig_strings) = signer_strings {
        present_signers = sig_strings
            .as_array()
            .unwrap()
            .iter()
            .map(|signer_string| {
                let mut signer = signer_string.as_str().unwrap().split('=');
                let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
                let sig = Signature::from_str(signer.next().unwrap()).unwrap();
                (key, sig)
            })
            .collect();
    }
    let mut absent_signers: Vec<Pubkey> = Vec::new();
    let signer_strings = object.get("absent");
    if let Some(sig_strings) = signer_strings {
        absent_signers = sig_strings
            .as_array()
            .unwrap()
            .iter()
            .map(|val| {
                let s = val.as_str().unwrap();
                Pubkey::from_str(s).unwrap()
            })
            .collect();
    }
    let mut bad_signers: Vec<Pubkey> = Vec::new();
    let signer_strings = object.get("badSig");
    if let Some(sig_strings) = signer_strings {
        bad_signers = sig_strings
            .as_array()
            .unwrap()
            .iter()
            .map(|val| {
                let s = val.as_str().unwrap();
                Pubkey::from_str(s).unwrap()
            })
            .collect();
    }

    let message = object
        .get("message")
        .and_then(|o| o.as_str())
        .map(|m| m.to_string());

    SignOnly {
        blockhash,
        message,
        present_signers,
        absent_signers,
        bad_signers,
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CliSignatureVerificationStatus {
    None,
    Pass,
    Fail,
}

impl CliSignatureVerificationStatus {
    pub fn verify_transaction(tx: &VersionedTransaction) -> Vec<Self> {
        tx.verify_with_results()
            .iter()
            .zip(&tx.signatures)
            .map(|(stat, sig)| match stat {
                true => CliSignatureVerificationStatus::Pass,
                false if sig == &Signature::default() => CliSignatureVerificationStatus::None,
                false => CliSignatureVerificationStatus::Fail,
            })
            .collect()
    }
}

impl fmt::Display for CliSignatureVerificationStatus {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::None => write!(f, "none"),
            Self::Pass => write!(f, "pass"),
            Self::Fail => write!(f, "fail"),
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
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_opt(block_time, 0).unwrap()
            )?;
        }
        if let Some(block_height) = self.encoded_confirmed_block.block_height {
            writeln!(f, "Block Height: {block_height:?}")?;
        }
        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}  {:>10}",
                "Address", "Type", "Amount", "New Balance", "Percent Change", "Commission"
            )?;
            for reward in rewards {
                let sign = if reward.lamports < 0 { "-" } else { "" };

                total_rewards += reward.lamports;
                #[allow(clippy::format_in_format_args)]
                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,
                        build_balance_message(reward.lamports.unsigned_abs(), false, false)
                    ),
                    if reward.post_balance == 0 {
                        "          -                 -".to_string()
                    } else {
                        format!(
                            "◎{:<19.9}  {:>13.9}%",
                            build_balance_message(reward.post_balance, false, false),
                            (reward.lamports.abs() as f64
                                / (reward.post_balance as f64 - reward.lamports as f64))
                                * 100.0
                        )
                    },
                    reward
                        .commission
                        .map(|commission| format!("{commission:>9}%"))
                        .unwrap_or_else(|| "    -".to_string())
                )?;
            }

            let sign = if total_rewards < 0 { "-" } else { "" };
            writeln!(
                f,
                "Total Rewards: {}◎{:<12.9}",
                sign,
                build_balance_message(total_rewards.unsigned_abs(), false, false)
            )?;
        }
        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.as_ref(),
                "  ",
                None,
                None,
            )?;
        }
        Ok(())
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliTransaction {
    pub transaction: EncodedTransaction,
    pub meta: Option<UiTransactionStatusMeta>,
    pub block_time: Option<UnixTimestamp>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub slot: Option<Slot>,
    #[serde(skip_serializing)]
    pub decoded_transaction: VersionedTransaction,
    #[serde(skip_serializing)]
    pub prefix: String,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub sigverify_status: Vec<CliSignatureVerificationStatus>,
}

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.as_ref(),
            &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<TransactionConfirmationStatus>,
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    pub transaction: Option<CliTransaction>,
    #[serde(skip_serializing)]
    pub get_transaction_error: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub err: Option<UiTransactionError>,
}

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:?}")
                }
            }
        }
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliGossipNode {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ip_address: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub identity_label: Option<String>,
    pub identity_pubkey: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub gossip_port: Option<u16>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tpu_port: Option<u16>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub rpc_host: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pubsub_host: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub feature_set: Option<u32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tpu_quic_port: Option<u16>,
}

impl CliGossipNode {
    pub fn new(info: RpcContactInfo, labels: &HashMap<String, String>) -> Self {
        Self {
            ip_address: info.gossip.map(|addr| addr.ip().to_string()),
            identity_label: labels.get(&info.pubkey).cloned(),
            identity_pubkey: info.pubkey,
            gossip_port: info.gossip.map(|addr| addr.port()),
            tpu_port: info.tpu.map(|addr| addr.port()),
            rpc_host: info.rpc.map(|addr| addr.to_string()),
            pubsub_host: info.pubsub.map(|addr| addr.to_string()),
            version: info.version,
            feature_set: info.feature_set,
            tpu_quic_port: info.tpu_quic.map(|addr| addr.port()),
        }
    }
}

fn unwrap_to_string_or_none<T>(option: Option<T>) -> String
where
    T: std::string::ToString,
{
    unwrap_to_string_or_default(option, "none")
}

fn unwrap_to_string_or_default<T>(option: Option<T>, default: &str) -> String
where
    T: std::string::ToString,
{
    option
        .as_ref()
        .map(|v| v.to_string())
        .unwrap_or_else(|| default.to_string())
}

impl fmt::Display for CliGossipNode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{:15} | {:44} | {:6} | {:5} | {:8} | {:21} | {:8}| {}",
            unwrap_to_string_or_none(self.ip_address.as_ref()),
            self.identity_label
                .as_ref()
                .unwrap_or(&self.identity_pubkey),
            unwrap_to_string_or_none(self.gossip_port.as_ref()),
            unwrap_to_string_or_none(self.tpu_port.as_ref()),
            unwrap_to_string_or_none(self.tpu_quic_port.as_ref()),
            unwrap_to_string_or_none(self.rpc_host.as_ref()),
            unwrap_to_string_or_default(self.version.as_ref(), "unknown"),
            unwrap_to_string_or_default(self.feature_set.as_ref(), "unknown"),
        )
    }
}

impl QuietDisplay for CliGossipNode {}
impl VerboseDisplay for CliGossipNode {}

#[derive(Serialize, Deserialize)]
pub struct CliGossipNodes(pub Vec<CliGossipNode>);

impl fmt::Display for CliGossipNodes {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "IP Address      | Identity                                     \
             | Gossip | TPU   | TPU-QUIC | RPC Address           | Version | Feature Set\n\
             ----------------+----------------------------------------------+\
             --------+-------+----------+-----------------------+---------+----------------",
        )?;
        for node in self.0.iter() {
            writeln!(f, "{node}")?;
        }
        writeln!(f, "Nodes: {}", self.0.len())
    }
}

impl QuietDisplay for CliGossipNodes {}
impl VerboseDisplay for CliGossipNodes {}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliPing {
    pub source_pubkey: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fixed_blockhash: Option<String>,
    #[serde(skip_serializing)]
    pub blockhash_from_cluster: bool,
    pub pings: Vec<CliPingData>,
    pub transaction_stats: CliPingTxStats,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub confirmation_stats: Option<CliPingConfirmationStats>,
}

impl fmt::Display for CliPing {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(f)?;
        writeln_name_value(f, "Source Account:", &self.source_pubkey)?;
        if let Some(fixed_blockhash) = &self.fixed_blockhash {
            let blockhash_origin = if self.blockhash_from_cluster {
                "fetched from cluster"
            } else {
                "supplied from cli arguments"
            };
            writeln!(
                f,
                "Fixed blockhash is used: {fixed_blockhash} ({blockhash_origin})"
            )?;
        }
        writeln!(f)?;
        for ping in &self.pings {
            write!(f, "{ping}")?;
        }
        writeln!(f)?;
        writeln!(f, "--- transaction statistics ---")?;
        write!(f, "{}", self.transaction_stats)?;
        if let Some(confirmation_stats) = &self.confirmation_stats {
            write!(f, "{confirmation_stats}")?;
        }
        Ok(())
    }
}

impl QuietDisplay for CliPing {}
impl VerboseDisplay for CliPing {}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliPingData {
    pub success: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub signature: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    #[serde(skip_serializing)]
    pub print_timestamp: bool,
    pub timestamp: String,
    pub sequence: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lamports: Option<u64>,
}
impl fmt::Display for CliPingData {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let (mark, msg) = if let Some(signature) = &self.signature {
            if self.success {
                (
                    CHECK_MARK,
                    format!(
                        "{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}",
                        self.lamports.unwrap(),
                        self.sequence,
                        self.ms.unwrap(),
                        signature
                    ),
                )
            } else if let Some(error) = &self.error {
                (
                    CROSS_MARK,
                    format!(
                        "Transaction failed:    seq={:<3} error={:?} signature={}",
                        self.sequence, error, signature
                    ),
                )
            } else {
                (
                    CROSS_MARK,
                    format!(
                        "Confirmation timeout:  seq={:<3}             signature={}",
                        self.sequence, signature
                    ),
                )
            }
        } else {
            (
                CROSS_MARK,
                format!(
                    "Submit failed:         seq={:<3} error={:?}",
                    self.sequence,
                    self.error.as_ref().unwrap(),
                ),
            )
        };

        writeln!(
            f,
            "{}{}{}",
            if self.print_timestamp {
                &self.timestamp
            } else {
                ""
            },
            mark,
            msg
        )
    }
}

impl QuietDisplay for CliPingData {}
impl VerboseDisplay for CliPingData {}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliPingTxStats {
    pub num_transactions: u32,
    pub num_transaction_confirmed: u32,
}
impl fmt::Display for CliPingTxStats {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
            self.num_transactions,
            self.num_transaction_confirmed,
            (100.
                - f64::from(self.num_transaction_confirmed) / f64::from(self.num_transactions)
                    * 100.)
        )
    }
}

impl QuietDisplay for CliPingTxStats {}
impl VerboseDisplay for CliPingTxStats {}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliPingConfirmationStats {
    pub min: f64,
    pub mean: f64,
    pub max: f64,
    pub std_dev: f64,
}
impl fmt::Display for CliPingConfirmationStats {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
            self.min, self.mean, self.max, self.std_dev,
        )
    }
}
impl QuietDisplay for CliPingConfirmationStats {}
impl VerboseDisplay for CliPingConfirmationStats {}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CliBalance {
    pub lamports: u64,
    #[serde(skip)]
    pub config: BuildBalanceMessageConfig,
}

impl QuietDisplay for CliBalance {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        let config = BuildBalanceMessageConfig {
            show_unit: false,
            trim_trailing_zeros: true,
            ..self.config
        };
        let balance_message = build_balance_message_with_config(self.lamports, &config);
        write!(w, "{balance_message}")
    }
}

impl VerboseDisplay for CliBalance {
    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
        let config = BuildBalanceMessageConfig {
            show_unit: true,
            trim_trailing_zeros: false,
            ..self.config
        };
        let balance_message = build_balance_message_with_config(self.lamports, &config);
        write!(w, "{balance_message}")
    }
}

impl fmt::Display for CliBalance {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let balance_message = build_balance_message_with_config(self.lamports, &self.config);
        write!(f, "{balance_message}")
    }
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CliFindProgramDerivedAddress {
    pub address: String,
    pub bump_seed: u8,
}

impl QuietDisplay for CliFindProgramDerivedAddress {}
impl VerboseDisplay for CliFindProgramDerivedAddress {}

impl fmt::Display for CliFindProgramDerivedAddress {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.address)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use {
        super::*,
        clap::{App, Arg},
        solana_keypair::keypair_from_seed,
        solana_message::Message,
        solana_pubkey::Pubkey,
        solana_signature::Signature,
        solana_signer::{null_signer::NullSigner, Signer, SignerError},
        solana_system_interface::instruction::transfer,
        solana_transaction::Transaction,
    };

    #[test]
    fn test_return_signers() {
        struct BadSigner {
            pubkey: Pubkey,
        }

        impl BadSigner {
            pub fn new(pubkey: Pubkey) -> Self {
                Self { pubkey }
            }
        }

        impl Signer for BadSigner {
            fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
                Ok(self.pubkey)
            }

            fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
                Ok(Signature::from([1u8; 64]))
            }

            fn is_interactive(&self) -> bool {
                false
            }
        }

        let present: Box<dyn Signer> = Box::new(keypair_from_seed(&[2u8; 32]).unwrap());
        let absent: Box<dyn Signer> = Box::new(NullSigner::new(&Pubkey::from([3u8; 32])));
        let bad: Box<dyn Signer> = Box::new(BadSigner::new(Pubkey::from([4u8; 32])));
        let to = Pubkey::from([5u8; 32]);
        let nonce = Pubkey::from([6u8; 32]);
        let from = present.pubkey();
        let fee_payer = absent.pubkey();
        let nonce_auth = bad.pubkey();
        let mut tx = Transaction::new_unsigned(Message::new_with_nonce(
            vec![transfer(&from, &to, 42)],
            Some(&fee_payer),
            &nonce,
            &nonce_auth,
        ));

        let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()];
        let blockhash = Hash::new_from_array([7u8; 32]);
        tx.try_partial_sign(&signers, blockhash).unwrap();
        let res = return_signers(&tx, &OutputFormat::JsonCompact).unwrap();
        let sign_only = parse_sign_only_reply_string(&res);
        assert_eq!(sign_only.blockhash, blockhash);
        assert_eq!(sign_only.message, None);
        assert_eq!(sign_only.present_signers[0].0, present.pubkey());
        assert_eq!(sign_only.absent_signers[0], absent.pubkey());
        assert_eq!(sign_only.bad_signers[0], bad.pubkey());

        let res_data = return_signers_data(&tx, &ReturnSignersConfig::default());
        assert_eq!(
            res_data,
            CliSignOnlyData {
                blockhash: blockhash.to_string(),
                message: None,
                signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
                absent: vec![absent.pubkey().to_string()],
                bad_sig: vec![bad.pubkey().to_string()],
            }
        );

        let expected_msg = "AwECBwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgTl3Dqh9\
            F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\
            BAQEBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBgYGBgYGBgYGBgYGBgYGBgYG\
            BgYGBgYGBgYGBgYGBgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAan1RcZLFaO\
            4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH\
            BwcCBQMEBgIEBAAAAAUCAQMMAgAAACoAAAAAAAAA"
            .to_string();
        let config = ReturnSignersConfig {
            dump_transaction_message: true,
        };
        let res = return_signers_with_config(&tx, &OutputFormat::JsonCompact, &config).unwrap();
        let sign_only = parse_sign_only_reply_string(&res);
        assert_eq!(sign_only.blockhash, blockhash);
        assert_eq!(sign_only.message, Some(expected_msg.clone()));
        assert_eq!(sign_only.present_signers[0].0, present.pubkey());
        assert_eq!(sign_only.absent_signers[0], absent.pubkey());
        assert_eq!(sign_only.bad_signers[0], bad.pubkey());

        let res_data = return_signers_data(&tx, &config);
        assert_eq!(
            res_data,
            CliSignOnlyData {
                blockhash: blockhash.to_string(),
                message: Some(expected_msg),
                signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
                absent: vec![absent.pubkey().to_string()],
                bad_sig: vec![bad.pubkey().to_string()],
            }
        );
    }

    #[test]
    fn test_verbose_quiet_output_formats() {
        #[derive(Deserialize, Serialize)]
        struct FallbackToDisplay {}
        impl std::fmt::Display for FallbackToDisplay {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(f, "display")
            }
        }
        impl QuietDisplay for FallbackToDisplay {}
        impl VerboseDisplay for FallbackToDisplay {}

        let f = FallbackToDisplay {};
        assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
        assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "display");
        assert_eq!(
            &OutputFormat::DisplayVerbose.formatted_string(&f),
            "display"
        );

        #[derive(Deserialize, Serialize)]
        struct DiscreteVerbosityDisplay {}
        impl std::fmt::Display for DiscreteVerbosityDisplay {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(f, "display")
            }
        }
        impl QuietDisplay for DiscreteVerbosityDisplay {
            fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
                write!(w, "quiet")
            }
        }
        impl VerboseDisplay for DiscreteVerbosityDisplay {
            fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
                write!(w, "verbose")
            }
        }

        let f = DiscreteVerbosityDisplay {};
        assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
        assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "quiet");
        assert_eq!(
            &OutputFormat::DisplayVerbose.formatted_string(&f),
            "verbose"
        );
    }

    #[test]
    fn test_output_format_from_matches() {
        let app = App::new("test").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"),
        );
        let matches = app
            .clone()
            .get_matches_from(vec!["test", "--output", "json"]);
        assert_eq!(
            OutputFormat::from_matches(&matches, "output_format", false),
            OutputFormat::Json
        );
        assert_eq!(
            OutputFormat::from_matches(&matches, "output_format", true),
            OutputFormat::Json
        );

        let matches = app
            .clone()
            .get_matches_from(vec!["test", "--output", "json-compact"]);
        assert_eq!(
            OutputFormat::from_matches(&matches, "output_format", false),
            OutputFormat::JsonCompact
        );
        assert_eq!(
            OutputFormat::from_matches(&matches, "output_format", true),
            OutputFormat::JsonCompact
        );

        let matches = app.clone().get_matches_from(vec!["test"]);
        assert_eq!(
            OutputFormat::from_matches(&matches, "output_format", false),
            OutputFormat::Display
        );
        assert_eq!(
            OutputFormat::from_matches(&matches, "output_format", true),
            OutputFormat::DisplayVerbose
        );
    }

    #[test]
    fn test_format_vote_account() {
        let epoch_rewards = vec![
            CliEpochReward {
                percent_change: 11.0,
                post_balance: 100,
                commission: Some(1),
                effective_slot: 100,
                epoch: 1,
                amount: 10,
                block_time: 0,
                apr: Some(10.0),
            },
            CliEpochReward {
                percent_change: 11.0,
                post_balance: 100,
                commission: Some(1),
                effective_slot: 200,
                epoch: 2,
                amount: 12,
                block_time: 1_000_000,
                apr: Some(13.0),
            },
        ];

        let mut c = CliVoteAccount {
            account_balance: 10000,
            validator_identity: Pubkey::default().to_string(),
            epoch_rewards: Some(epoch_rewards),
            recent_timestamp: BlockTimestamp::default(),
            ..CliVoteAccount::default()
        };
        #[rustfmt::skip]
        let expected_output_common =
            "Account Balance: 0.00001 SOL\n\
             Validator Identity: 11111111111111111111111111111111\n\
             Vote Authority: None\n\
             Withdraw Authority: \n\
             Credits: 0\n\
             Commission: 0%\n\
             Root Slot: ~\n\
             Recent Timestamp: 1970-01-01T00:00:00Z from slot 0\n";

        let s = format!("{c}");
        #[rustfmt::skip]
        let expected_epoch_rewards_output =
            "Epoch Rewards:\n  \
             Epoch   Reward Slot  Time                        Amount              New Balance         Percent Change             APR  Commission\n  \
             1       100          1970-01-01 00:00:00 UTC  ◎0.00000001         ◎0.0000001                 11.000%          10.00%          1%\n  \
             2       200          1970-01-12 13:46:40 UTC  ◎0.000000012        ◎0.0000001                 11.000%          13.00%          1%\n";
        assert_eq!(
            s,
            format!("{expected_output_common}{expected_epoch_rewards_output}")
        );
        println!("{s}");

        c.use_csv = true;
        let s = format!("{c}");
        #[rustfmt::skip]
        let expected_epoch_rewards_output =
            "Epoch Rewards:\n\
             Epoch,Reward Slot,Time,Amount,New Balance,Percent Change,APR,Commission\n\
             1,100,1970-01-01 00:00:00 UTC,0.00000001,0.0000001,11%,10.00%,1%\n\
             2,200,1970-01-12 13:46:40 UTC,0.000000012,0.0000001,11%,13.00%,1%\n";
        assert_eq!(
            s,
            format!("{expected_output_common}{expected_epoch_rewards_output}")
        );
        println!("{s}");
    }
}
