solana_cli_output/
cli_output.rs

1#![allow(clippy::to_string_in_format_args)]
2use {
3    crate::{
4        cli_version::CliVersion,
5        display::{
6            build_balance_message, build_balance_message_with_config, format_labeled_address,
7            unix_timestamp_to_string, writeln_name_value, writeln_transaction,
8            BuildBalanceMessageConfig,
9        },
10        QuietDisplay, VerboseDisplay,
11    },
12    base64::{prelude::BASE64_STANDARD, Engine},
13    chrono::{Local, TimeZone, Utc},
14    clap::ArgMatches,
15    console::{style, Emoji},
16    inflector::cases::titlecase::to_title_case,
17    serde::{Deserialize, Serialize},
18    serde_json::{Map, Value},
19    solana_account_decoder::{
20        encode_ui_account, parse_account_data::AccountAdditionalDataV2,
21        parse_token::UiTokenAccount, UiAccountEncoding, UiDataSliceConfig,
22    },
23    solana_clap_utils::keypair::SignOnly,
24    solana_rpc_client_api::response::{
25        RpcAccountBalance, RpcContactInfo, RpcInflationGovernor, RpcInflationRate, RpcKeyedAccount,
26        RpcSupply, RpcVoteAccountInfo,
27    },
28    solana_sdk::{
29        account::ReadableAccount,
30        clock::{Epoch, Slot, UnixTimestamp},
31        epoch_info::EpochInfo,
32        hash::Hash,
33        native_token::lamports_to_sol,
34        pubkey::Pubkey,
35        signature::Signature,
36        stake::state::{Authorized, Lockup},
37        stake_history::StakeHistoryEntry,
38        transaction::{Transaction, TransactionError, VersionedTransaction},
39    },
40    solana_transaction_status::{
41        EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus,
42        UiTransactionStatusMeta,
43    },
44    solana_vote_program::{
45        authorized_voters::AuthorizedVoters,
46        vote_state::{BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY},
47    },
48    std::{
49        collections::{BTreeMap, HashMap},
50        fmt,
51        str::FromStr,
52        time::Duration,
53    },
54};
55
56static CHECK_MARK: Emoji = Emoji("✅ ", "");
57static CROSS_MARK: Emoji = Emoji("❌ ", "");
58static WARNING: Emoji = Emoji("⚠️", "!");
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub enum OutputFormat {
62    Display,
63    Json,
64    JsonCompact,
65    DisplayQuiet,
66    DisplayVerbose,
67}
68
69impl OutputFormat {
70    pub fn formatted_string<T>(&self, item: &T) -> String
71    where
72        T: Serialize + fmt::Display + QuietDisplay + VerboseDisplay,
73    {
74        match self {
75            OutputFormat::Display => format!("{item}"),
76            OutputFormat::DisplayQuiet => {
77                let mut s = String::new();
78                QuietDisplay::write_str(item, &mut s).unwrap();
79                s
80            }
81            OutputFormat::DisplayVerbose => {
82                let mut s = String::new();
83                VerboseDisplay::write_str(item, &mut s).unwrap();
84                s
85            }
86            OutputFormat::Json => serde_json::to_string_pretty(item).unwrap(),
87            OutputFormat::JsonCompact => serde_json::to_value(item).unwrap().to_string(),
88        }
89    }
90
91    pub fn from_matches(matches: &ArgMatches<'_>, output_name: &str, verbose: bool) -> Self {
92        matches
93            .value_of(output_name)
94            .map(|value| match value {
95                "json" => OutputFormat::Json,
96                "json-compact" => OutputFormat::JsonCompact,
97                _ => unreachable!(),
98            })
99            .unwrap_or(if verbose {
100                OutputFormat::DisplayVerbose
101            } else {
102                OutputFormat::Display
103            })
104    }
105}
106
107#[derive(Serialize)]
108pub struct CliPrioritizationFeeStats {
109    pub fees: Vec<CliPrioritizationFee>,
110    pub min: u64,
111    pub max: u64,
112    pub average: u64,
113    pub num_slots: u64,
114}
115
116impl QuietDisplay for CliPrioritizationFeeStats {}
117impl VerboseDisplay for CliPrioritizationFeeStats {
118    fn write_str(&self, f: &mut dyn std::fmt::Write) -> fmt::Result {
119        writeln!(f, "{:<11} prioritization_fee", "slot")?;
120        for fee in &self.fees {
121            write!(f, "{}", fee)?;
122        }
123        write!(f, "{}", self)
124    }
125}
126
127impl fmt::Display for CliPrioritizationFeeStats {
128    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
129        writeln!(
130            f,
131            "Fees in recent {} slots: Min: {} Max: {} Average: {}",
132            self.num_slots, self.min, self.max, self.average
133        )
134    }
135}
136
137#[derive(Serialize)]
138pub struct CliPrioritizationFee {
139    pub slot: Slot,
140    pub prioritization_fee: u64,
141}
142
143impl QuietDisplay for CliPrioritizationFee {}
144impl VerboseDisplay for CliPrioritizationFee {}
145
146impl fmt::Display for CliPrioritizationFee {
147    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
148        writeln!(f, "{:<11} {}", self.slot, self.prioritization_fee)
149    }
150}
151
152#[derive(Serialize, Deserialize)]
153pub struct CliAccount {
154    #[serde(flatten)]
155    pub keyed_account: RpcKeyedAccount,
156    #[serde(skip_serializing, skip_deserializing)]
157    pub use_lamports_unit: bool,
158}
159
160pub struct CliAccountNewConfig {
161    pub data_encoding: UiAccountEncoding,
162    pub additional_data: Option<AccountAdditionalDataV2>,
163    pub data_slice_config: Option<UiDataSliceConfig>,
164    pub use_lamports_unit: bool,
165}
166
167impl Default for CliAccountNewConfig {
168    fn default() -> Self {
169        Self {
170            data_encoding: UiAccountEncoding::Base64,
171            additional_data: None,
172            data_slice_config: None,
173            use_lamports_unit: false,
174        }
175    }
176}
177
178impl CliAccount {
179    pub fn new<T: ReadableAccount>(address: &Pubkey, account: &T, use_lamports_unit: bool) -> Self {
180        Self::new_with_config(
181            address,
182            account,
183            &CliAccountNewConfig {
184                use_lamports_unit,
185                ..CliAccountNewConfig::default()
186            },
187        )
188    }
189
190    pub fn new_with_config<T: ReadableAccount>(
191        address: &Pubkey,
192        account: &T,
193        config: &CliAccountNewConfig,
194    ) -> Self {
195        let CliAccountNewConfig {
196            data_encoding,
197            additional_data,
198            data_slice_config,
199            use_lamports_unit,
200        } = *config;
201        Self {
202            keyed_account: RpcKeyedAccount {
203                pubkey: address.to_string(),
204                account: encode_ui_account(
205                    address,
206                    account,
207                    data_encoding,
208                    additional_data,
209                    data_slice_config,
210                ),
211            },
212            use_lamports_unit,
213        }
214    }
215}
216
217impl QuietDisplay for CliAccount {}
218impl VerboseDisplay for CliAccount {}
219
220impl fmt::Display for CliAccount {
221    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
222        writeln!(f)?;
223        writeln_name_value(f, "Public Key:", &self.keyed_account.pubkey)?;
224        writeln_name_value(
225            f,
226            "Balance:",
227            &build_balance_message(
228                self.keyed_account.account.lamports,
229                self.use_lamports_unit,
230                true,
231            ),
232        )?;
233        writeln_name_value(f, "Owner:", &self.keyed_account.account.owner)?;
234        writeln_name_value(
235            f,
236            "Executable:",
237            &self.keyed_account.account.executable.to_string(),
238        )?;
239        writeln_name_value(
240            f,
241            "Rent Epoch:",
242            &self.keyed_account.account.rent_epoch.to_string(),
243        )?;
244        Ok(())
245    }
246}
247
248#[derive(Default, Serialize, Deserialize)]
249pub struct CliBlockProduction {
250    pub epoch: Epoch,
251    pub start_slot: Slot,
252    pub end_slot: Slot,
253    pub total_slots: usize,
254    pub total_blocks_produced: usize,
255    pub total_slots_skipped: usize,
256    pub leaders: Vec<CliBlockProductionEntry>,
257    pub individual_slot_status: Vec<CliSlotStatus>,
258    #[serde(skip_serializing)]
259    pub verbose: bool,
260}
261
262impl QuietDisplay for CliBlockProduction {}
263impl VerboseDisplay for CliBlockProduction {}
264
265impl fmt::Display for CliBlockProduction {
266    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
267        writeln!(f)?;
268        writeln!(
269            f,
270            "{}",
271            style(format!(
272                "  {:<44}  {:>15}  {:>15}  {:>15}  {:>15}",
273                "Identity", "Leader Slots", "Blocks Produced", "Skipped Slots", "Skip Rate",
274            ))
275            .bold()
276        )?;
277        for leader in &self.leaders {
278            writeln!(
279                f,
280                "  {:<44}  {:>15}  {:>15}  {:>15}  {:>22.2}%",
281                leader.identity_pubkey,
282                leader.leader_slots,
283                leader.blocks_produced,
284                leader.skipped_slots,
285                leader.skipped_slots as f64 / leader.leader_slots as f64 * 100.
286            )?;
287        }
288        writeln!(f)?;
289        writeln!(
290            f,
291            "  {:<44}  {:>15}  {:>15}  {:>15}  {:>22.2}%",
292            format!("Epoch {} total:", self.epoch),
293            self.total_slots,
294            self.total_blocks_produced,
295            self.total_slots_skipped,
296            self.total_slots_skipped as f64 / self.total_slots as f64 * 100.
297        )?;
298        writeln!(
299            f,
300            "  (using data from {} slots: {} to {})",
301            self.total_slots, self.start_slot, self.end_slot
302        )?;
303        if self.verbose {
304            writeln!(f)?;
305            writeln!(f)?;
306            writeln!(
307                f,
308                "{}",
309                style(format!("  {:<15} {:<44}", "Slot", "Identity Pubkey")).bold(),
310            )?;
311            for status in &self.individual_slot_status {
312                if status.skipped {
313                    writeln!(
314                        f,
315                        "{}",
316                        style(format!(
317                            "  {:<15} {:<44} SKIPPED",
318                            status.slot, status.leader
319                        ))
320                        .red()
321                    )?;
322                } else {
323                    writeln!(
324                        f,
325                        "{}",
326                        style(format!("  {:<15} {:<44}", status.slot, status.leader))
327                    )?;
328                }
329            }
330        }
331        Ok(())
332    }
333}
334
335#[derive(Default, Serialize, Deserialize)]
336#[serde(rename_all = "camelCase")]
337pub struct CliBlockProductionEntry {
338    pub identity_pubkey: String,
339    pub leader_slots: u64,
340    pub blocks_produced: u64,
341    pub skipped_slots: u64,
342}
343
344#[derive(Default, Serialize, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct CliSlotStatus {
347    pub slot: Slot,
348    pub leader: String,
349    pub skipped: bool,
350}
351
352#[derive(Serialize, Deserialize)]
353#[serde(rename_all = "camelCase")]
354pub struct CliEpochInfo {
355    #[serde(flatten)]
356    pub epoch_info: EpochInfo,
357    pub epoch_completed_percent: f64,
358    #[serde(skip)]
359    pub average_slot_time_ms: u64,
360    #[serde(skip)]
361    pub start_block_time: Option<UnixTimestamp>,
362    #[serde(skip)]
363    pub current_block_time: Option<UnixTimestamp>,
364}
365
366impl QuietDisplay for CliEpochInfo {}
367impl VerboseDisplay for CliEpochInfo {}
368
369impl fmt::Display for CliEpochInfo {
370    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
371        writeln!(f)?;
372        writeln_name_value(
373            f,
374            "Block height:",
375            &self.epoch_info.block_height.to_string(),
376        )?;
377        writeln_name_value(f, "Slot:", &self.epoch_info.absolute_slot.to_string())?;
378        writeln_name_value(f, "Epoch:", &self.epoch_info.epoch.to_string())?;
379        if let Some(transaction_count) = &self.epoch_info.transaction_count {
380            writeln_name_value(f, "Transaction Count:", &transaction_count.to_string())?;
381        }
382        let start_slot = self.epoch_info.absolute_slot - self.epoch_info.slot_index;
383        let end_slot = start_slot + self.epoch_info.slots_in_epoch;
384        writeln_name_value(
385            f,
386            "Epoch Slot Range:",
387            &format!("[{start_slot}..{end_slot})"),
388        )?;
389        writeln_name_value(
390            f,
391            "Epoch Completed Percent:",
392            &format!("{:>3.3}%", self.epoch_completed_percent),
393        )?;
394        let remaining_slots_in_epoch = self.epoch_info.slots_in_epoch - self.epoch_info.slot_index;
395        writeln_name_value(
396            f,
397            "Epoch Completed Slots:",
398            &format!(
399                "{}/{} ({} remaining)",
400                self.epoch_info.slot_index,
401                self.epoch_info.slots_in_epoch,
402                remaining_slots_in_epoch
403            ),
404        )?;
405        let (time_elapsed, annotation) = if let (Some(start_block_time), Some(current_block_time)) =
406            (self.start_block_time, self.current_block_time)
407        {
408            (
409                Duration::from_secs((current_block_time - start_block_time) as u64),
410                None,
411            )
412        } else {
413            (
414                slot_to_duration(self.epoch_info.slot_index, self.average_slot_time_ms),
415                Some("* estimated based on current slot durations"),
416            )
417        };
418        let time_remaining = slot_to_duration(remaining_slots_in_epoch, self.average_slot_time_ms);
419        writeln_name_value(
420            f,
421            "Epoch Completed Time:",
422            &format!(
423                "{}{}/{} ({} remaining)",
424                humantime::format_duration(time_elapsed),
425                if annotation.is_some() { "*" } else { "" },
426                humantime::format_duration(time_elapsed + time_remaining),
427                humantime::format_duration(time_remaining),
428            ),
429        )?;
430        if let Some(annotation) = annotation {
431            writeln!(f)?;
432            writeln!(f, "{annotation}")?;
433        }
434        Ok(())
435    }
436}
437
438fn slot_to_duration(slot: Slot, slot_time_ms: u64) -> Duration {
439    Duration::from_secs((slot * slot_time_ms) / 1000)
440}
441
442#[derive(Serialize, Deserialize, Default)]
443#[serde(rename_all = "camelCase")]
444pub struct CliValidatorsStakeByVersion {
445    pub current_validators: usize,
446    pub delinquent_validators: usize,
447    pub current_active_stake: u64,
448    pub delinquent_active_stake: u64,
449}
450
451#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)]
452pub enum CliValidatorsSortOrder {
453    Delinquent,
454    Commission,
455    EpochCredits,
456    Identity,
457    LastVote,
458    Root,
459    SkipRate,
460    Stake,
461    VoteAccount,
462    Version,
463}
464
465#[derive(Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct CliValidators {
468    pub total_active_stake: u64,
469    pub total_current_stake: u64,
470    pub total_delinquent_stake: u64,
471    pub validators: Vec<CliValidator>,
472    pub average_skip_rate: f64,
473    pub average_stake_weighted_skip_rate: f64,
474    #[serde(skip_serializing)]
475    pub validators_sort_order: CliValidatorsSortOrder,
476    #[serde(skip_serializing)]
477    pub validators_reverse_sort: bool,
478    #[serde(skip_serializing)]
479    pub number_validators: bool,
480    pub stake_by_version: BTreeMap<CliVersion, CliValidatorsStakeByVersion>,
481    #[serde(skip_serializing)]
482    pub use_lamports_unit: bool,
483}
484
485impl QuietDisplay for CliValidators {}
486impl VerboseDisplay for CliValidators {}
487
488impl fmt::Display for CliValidators {
489    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
490        fn write_vote_account(
491            f: &mut fmt::Formatter,
492            validator: &CliValidator,
493            total_active_stake: u64,
494            use_lamports_unit: bool,
495            highest_last_vote: u64,
496            highest_root: u64,
497        ) -> fmt::Result {
498            fn non_zero_or_dash(v: u64, max_v: u64) -> String {
499                if v == 0 {
500                    "        -      ".into()
501                } else if v == max_v {
502                    format!("{v:>9} (  0)")
503                } else if v > max_v.saturating_sub(100) {
504                    format!("{:>9} ({:>3})", v, -(max_v.saturating_sub(v) as isize))
505                } else {
506                    format!("{v:>9}      ")
507                }
508            }
509
510            writeln!(
511                f,
512                "{} {:<44}  {:<44}  {:>3}%  {:>14}  {:>14} {:>7} {:>8}  {:>7}  {:>22} ({:.2}%)",
513                if validator.delinquent {
514                    WARNING.to_string()
515                } else {
516                    "\u{a0}".to_string()
517                },
518                validator.identity_pubkey,
519                validator.vote_account_pubkey,
520                validator.commission,
521                non_zero_or_dash(validator.last_vote, highest_last_vote),
522                non_zero_or_dash(validator.root_slot, highest_root),
523                if let Some(skip_rate) = validator.skip_rate {
524                    format!("{skip_rate:.2}%")
525                } else {
526                    "-   ".to_string()
527                },
528                validator.epoch_credits,
529                // convert to a string so that fill/alignment works correctly
530                validator.version.to_string(),
531                build_balance_message_with_config(
532                    validator.activated_stake,
533                    &BuildBalanceMessageConfig {
534                        use_lamports_unit,
535                        trim_trailing_zeros: false,
536                        ..BuildBalanceMessageConfig::default()
537                    }
538                ),
539                100. * validator.activated_stake as f64 / total_active_stake as f64,
540            )
541        }
542
543        let padding = if self.number_validators {
544            ((self.validators.len() + 1) as f64).log10().floor() as usize + 1
545        } else {
546            0
547        };
548        let header = style(format!(
549            "{:padding$} {:<44}  {:<38}  {}  {}  {} {}  {}  {}  {:>22}",
550            " ",
551            "Identity",
552            "Vote Account",
553            "Commission",
554            "Last Vote      ",
555            "Root Slot    ",
556            "Skip Rate",
557            "Credits",
558            "Version",
559            "Active Stake",
560            padding = padding + 2
561        ))
562        .bold();
563        writeln!(f, "{header}")?;
564
565        let mut sorted_validators = self.validators.clone();
566        match self.validators_sort_order {
567            CliValidatorsSortOrder::Delinquent => {
568                sorted_validators.sort_by_key(|a| a.delinquent);
569            }
570            CliValidatorsSortOrder::Commission => {
571                sorted_validators.sort_by_key(|a| a.commission);
572            }
573            CliValidatorsSortOrder::EpochCredits => {
574                sorted_validators.sort_by_key(|a| a.epoch_credits);
575            }
576            CliValidatorsSortOrder::Identity => {
577                sorted_validators.sort_by(|a, b| a.identity_pubkey.cmp(&b.identity_pubkey));
578            }
579            CliValidatorsSortOrder::LastVote => {
580                sorted_validators.sort_by_key(|a| a.last_vote);
581            }
582            CliValidatorsSortOrder::Root => {
583                sorted_validators.sort_by_key(|a| a.root_slot);
584            }
585            CliValidatorsSortOrder::VoteAccount => {
586                sorted_validators.sort_by(|a, b| a.vote_account_pubkey.cmp(&b.vote_account_pubkey));
587            }
588            CliValidatorsSortOrder::SkipRate => {
589                sorted_validators.sort_by(|a, b| {
590                    use std::cmp::Ordering;
591                    match (a.skip_rate, b.skip_rate) {
592                        (None, None) => Ordering::Equal,
593                        (None, Some(_)) => Ordering::Greater,
594                        (Some(_), None) => Ordering::Less,
595                        (Some(a), Some(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
596                    }
597                });
598            }
599            CliValidatorsSortOrder::Stake => {
600                sorted_validators.sort_by_key(|a| a.activated_stake);
601            }
602            CliValidatorsSortOrder::Version => {
603                sorted_validators.sort_by(|a, b| {
604                    (&a.version, a.activated_stake).cmp(&(&b.version, b.activated_stake))
605                });
606            }
607        }
608
609        if self.validators_reverse_sort {
610            sorted_validators.reverse();
611        }
612
613        let highest_root = sorted_validators
614            .iter()
615            .map(|v| v.root_slot)
616            .max()
617            .unwrap_or_default();
618        let highest_last_vote = sorted_validators
619            .iter()
620            .map(|v| v.last_vote)
621            .max()
622            .unwrap_or_default();
623
624        for (i, validator) in sorted_validators.iter().enumerate() {
625            if padding > 0 {
626                let num = if self.validators_reverse_sort {
627                    i + 1
628                } else {
629                    sorted_validators.len() - i
630                };
631                write!(f, "{num:padding$} ")?;
632            }
633            write_vote_account(
634                f,
635                validator,
636                self.total_active_stake,
637                self.use_lamports_unit,
638                highest_last_vote,
639                highest_root,
640            )?;
641        }
642
643        // The actual header has long scrolled away.  Print the header once more as a footer
644        if self.validators.len() > 100 {
645            writeln!(f, "{header}")?;
646        }
647
648        writeln!(f)?;
649        writeln_name_value(
650            f,
651            "Average Stake-Weighted Skip Rate:",
652            &format!("{:.2}%", self.average_stake_weighted_skip_rate,),
653        )?;
654        writeln_name_value(
655            f,
656            "Average Unweighted Skip Rate:    ",
657            &format!("{:.2}%", self.average_skip_rate),
658        )?;
659
660        writeln!(f)?;
661        writeln_name_value(
662            f,
663            "Active Stake:",
664            &build_balance_message(self.total_active_stake, self.use_lamports_unit, true),
665        )?;
666        if self.total_delinquent_stake > 0 {
667            writeln_name_value(
668                f,
669                "Current Stake:",
670                &format!(
671                    "{} ({:0.2}%)",
672                    &build_balance_message(self.total_current_stake, self.use_lamports_unit, true),
673                    100. * self.total_current_stake as f64 / self.total_active_stake as f64
674                ),
675            )?;
676            writeln_name_value(
677                f,
678                "Delinquent Stake:",
679                &format!(
680                    "{} ({:0.2}%)",
681                    &build_balance_message(
682                        self.total_delinquent_stake,
683                        self.use_lamports_unit,
684                        true
685                    ),
686                    100. * self.total_delinquent_stake as f64 / self.total_active_stake as f64
687                ),
688            )?;
689        }
690
691        writeln!(f)?;
692        writeln!(f, "{}", style("Stake By Version:").bold())?;
693        for (version, info) in self.stake_by_version.iter().rev() {
694            writeln!(
695                f,
696                "{:<7} - {:4} current validators ({:>5.2}%){}",
697                // convert to a string so that fill/alignment works correctly
698                version.to_string(),
699                info.current_validators,
700                100. * info.current_active_stake as f64 / self.total_active_stake as f64,
701                if info.delinquent_validators > 0 {
702                    format!(
703                        " {:3} delinquent validators ({:>5.2}%)",
704                        info.delinquent_validators,
705                        100. * info.delinquent_active_stake as f64 / self.total_active_stake as f64
706                    )
707                } else {
708                    "".to_string()
709                },
710            )?;
711        }
712
713        Ok(())
714    }
715}
716
717#[derive(Serialize, Deserialize, Clone)]
718#[serde(rename_all = "camelCase")]
719pub struct CliValidator {
720    pub identity_pubkey: String,
721    pub vote_account_pubkey: String,
722    pub commission: u8,
723    pub last_vote: u64,
724    pub root_slot: u64,
725    pub credits: u64,       // lifetime credits
726    pub epoch_credits: u64, // credits earned in the current epoch
727    pub activated_stake: u64,
728    pub version: CliVersion,
729    pub delinquent: bool,
730    pub skip_rate: Option<f64>,
731}
732
733impl CliValidator {
734    pub fn new(
735        vote_account: &RpcVoteAccountInfo,
736        current_epoch: Epoch,
737        version: CliVersion,
738        skip_rate: Option<f64>,
739        address_labels: &HashMap<String, String>,
740    ) -> Self {
741        Self::_new(
742            vote_account,
743            current_epoch,
744            version,
745            skip_rate,
746            address_labels,
747            false,
748        )
749    }
750
751    pub fn new_delinquent(
752        vote_account: &RpcVoteAccountInfo,
753        current_epoch: Epoch,
754        version: CliVersion,
755        skip_rate: Option<f64>,
756        address_labels: &HashMap<String, String>,
757    ) -> Self {
758        Self::_new(
759            vote_account,
760            current_epoch,
761            version,
762            skip_rate,
763            address_labels,
764            true,
765        )
766    }
767
768    fn _new(
769        vote_account: &RpcVoteAccountInfo,
770        current_epoch: Epoch,
771        version: CliVersion,
772        skip_rate: Option<f64>,
773        address_labels: &HashMap<String, String>,
774        delinquent: bool,
775    ) -> Self {
776        let (credits, epoch_credits) = vote_account
777            .epoch_credits
778            .iter()
779            .find_map(|(epoch, credits, pre_credits)| {
780                if *epoch == current_epoch {
781                    Some((*credits, credits.saturating_sub(*pre_credits)))
782                } else {
783                    None
784                }
785            })
786            .unwrap_or((0, 0));
787        Self {
788            identity_pubkey: format_labeled_address(&vote_account.node_pubkey, address_labels),
789            vote_account_pubkey: format_labeled_address(&vote_account.vote_pubkey, address_labels),
790            commission: vote_account.commission,
791            last_vote: vote_account.last_vote,
792            root_slot: vote_account.root_slot,
793            credits,
794            epoch_credits,
795            activated_stake: vote_account.activated_stake,
796            version,
797            delinquent,
798            skip_rate,
799        }
800    }
801}
802
803#[derive(Serialize, Deserialize)]
804#[serde(rename_all = "camelCase")]
805pub struct CliHistorySignatureVec(Vec<CliHistorySignature>);
806
807impl CliHistorySignatureVec {
808    pub fn new(list: Vec<CliHistorySignature>) -> Self {
809        Self(list)
810    }
811}
812
813impl QuietDisplay for CliHistorySignatureVec {}
814impl VerboseDisplay for CliHistorySignatureVec {
815    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
816        for signature in &self.0 {
817            VerboseDisplay::write_str(signature, w)?;
818        }
819        writeln!(w, "{} transactions found", self.0.len())
820    }
821}
822
823impl fmt::Display for CliHistorySignatureVec {
824    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
825        for signature in &self.0 {
826            write!(f, "{signature}")?;
827        }
828        writeln!(f, "{} transactions found", self.0.len())
829    }
830}
831
832#[derive(Serialize, Deserialize, Default)]
833#[serde(rename_all = "camelCase")]
834pub struct CliHistorySignature {
835    pub signature: String,
836    #[serde(flatten, skip_serializing_if = "Option::is_none")]
837    pub verbose: Option<CliHistoryVerbose>,
838}
839
840impl QuietDisplay for CliHistorySignature {}
841impl VerboseDisplay for CliHistorySignature {
842    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
843        let verbose = self
844            .verbose
845            .as_ref()
846            .expect("should have verbose.is_some()");
847        writeln!(
848            w,
849            "{} [slot={} {}status={}] {}",
850            self.signature,
851            verbose.slot,
852            match verbose.block_time {
853                None => "".to_string(),
854                Some(block_time) => format!("timestamp={} ", unix_timestamp_to_string(block_time)),
855            },
856            if let Some(err) = &verbose.err {
857                format!("Failed: {err:?}")
858            } else {
859                match &verbose.confirmation_status {
860                    None => "Finalized".to_string(),
861                    Some(status) => format!("{status:?}"),
862                }
863            },
864            verbose.memo.clone().unwrap_or_default(),
865        )
866    }
867}
868
869impl fmt::Display for CliHistorySignature {
870    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
871        writeln!(f, "{}", self.signature)
872    }
873}
874
875#[derive(Serialize, Deserialize, Default)]
876#[serde(rename_all = "camelCase")]
877pub struct CliHistoryVerbose {
878    pub slot: Slot,
879    pub block_time: Option<UnixTimestamp>,
880    pub err: Option<TransactionError>,
881    pub confirmation_status: Option<TransactionConfirmationStatus>,
882    pub memo: Option<String>,
883}
884
885#[derive(Serialize, Deserialize)]
886#[serde(rename_all = "camelCase")]
887pub struct CliHistoryTransactionVec(Vec<CliTransactionConfirmation>);
888
889impl CliHistoryTransactionVec {
890    pub fn new(list: Vec<CliTransactionConfirmation>) -> Self {
891        Self(list)
892    }
893}
894
895impl QuietDisplay for CliHistoryTransactionVec {}
896impl VerboseDisplay for CliHistoryTransactionVec {}
897
898impl fmt::Display for CliHistoryTransactionVec {
899    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
900        for transaction in &self.0 {
901            VerboseDisplay::write_str(transaction, f)?;
902            writeln!(f)?;
903        }
904        writeln!(f, "{} transactions found", self.0.len())
905    }
906}
907
908#[derive(Default, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910pub struct CliNonceAccount {
911    pub balance: u64,
912    pub minimum_balance_for_rent_exemption: u64,
913    pub nonce: Option<String>,
914    pub lamports_per_signature: Option<u64>,
915    pub authority: Option<String>,
916    #[serde(skip_serializing)]
917    pub use_lamports_unit: bool,
918}
919
920impl QuietDisplay for CliNonceAccount {}
921impl VerboseDisplay for CliNonceAccount {}
922
923impl fmt::Display for CliNonceAccount {
924    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
925        writeln!(
926            f,
927            "Balance: {}",
928            build_balance_message(self.balance, self.use_lamports_unit, true)
929        )?;
930        writeln!(
931            f,
932            "Minimum Balance Required: {}",
933            build_balance_message(
934                self.minimum_balance_for_rent_exemption,
935                self.use_lamports_unit,
936                true
937            )
938        )?;
939        let nonce = self.nonce.as_deref().unwrap_or("uninitialized");
940        writeln!(f, "Nonce blockhash: {nonce}")?;
941        if let Some(fees) = self.lamports_per_signature {
942            writeln!(f, "Fee: {fees} lamports per signature")?;
943        } else {
944            writeln!(f, "Fees: uninitialized")?;
945        }
946        let authority = self.authority.as_deref().unwrap_or("uninitialized");
947        writeln!(f, "Authority: {authority}")
948    }
949}
950
951#[derive(Serialize, Deserialize)]
952pub struct CliStakeVec(Vec<CliKeyedStakeState>);
953
954impl CliStakeVec {
955    pub fn new(list: Vec<CliKeyedStakeState>) -> Self {
956        Self(list)
957    }
958}
959
960impl QuietDisplay for CliStakeVec {}
961impl VerboseDisplay for CliStakeVec {
962    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
963        for state in &self.0 {
964            writeln!(w)?;
965            VerboseDisplay::write_str(state, w)?;
966        }
967        Ok(())
968    }
969}
970
971impl fmt::Display for CliStakeVec {
972    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
973        for state in &self.0 {
974            writeln!(f)?;
975            write!(f, "{state}")?;
976        }
977        Ok(())
978    }
979}
980
981#[derive(Serialize, Deserialize)]
982#[serde(rename_all = "camelCase")]
983pub struct CliKeyedStakeState {
984    pub stake_pubkey: String,
985    #[serde(flatten)]
986    pub stake_state: CliStakeState,
987}
988
989impl QuietDisplay for CliKeyedStakeState {}
990impl VerboseDisplay for CliKeyedStakeState {
991    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
992        writeln!(w, "Stake Pubkey: {}", self.stake_pubkey)?;
993        VerboseDisplay::write_str(&self.stake_state, w)
994    }
995}
996
997impl fmt::Display for CliKeyedStakeState {
998    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
999        writeln!(f, "Stake Pubkey: {}", self.stake_pubkey)?;
1000        write!(f, "{}", self.stake_state)
1001    }
1002}
1003
1004#[derive(Serialize, Deserialize)]
1005#[serde(rename_all = "camelCase")]
1006pub struct CliEpochReward {
1007    pub epoch: Epoch,
1008    pub effective_slot: Slot,
1009    pub amount: u64,       // lamports
1010    pub post_balance: u64, // lamports
1011    pub percent_change: f64,
1012    pub apr: Option<f64>,
1013    pub commission: Option<u8>,
1014    pub block_time: UnixTimestamp,
1015}
1016
1017#[derive(Serialize, Deserialize)]
1018#[serde(rename_all = "camelCase")]
1019pub struct CliKeyedEpochReward {
1020    pub address: String,
1021    pub reward: Option<CliEpochReward>,
1022}
1023
1024#[derive(Serialize, Deserialize)]
1025#[serde(rename_all = "camelCase")]
1026pub struct CliEpochRewardsMetadata {
1027    pub epoch: Epoch,
1028    pub effective_slot: Slot,
1029    pub block_time: UnixTimestamp,
1030}
1031
1032#[derive(Serialize, Deserialize)]
1033#[serde(rename_all = "camelCase")]
1034pub struct CliKeyedEpochRewards {
1035    #[serde(flatten, skip_serializing_if = "Option::is_none")]
1036    pub epoch_metadata: Option<CliEpochRewardsMetadata>,
1037    pub rewards: Vec<CliKeyedEpochReward>,
1038}
1039
1040impl QuietDisplay for CliKeyedEpochRewards {}
1041impl VerboseDisplay for CliKeyedEpochRewards {}
1042
1043impl fmt::Display for CliKeyedEpochRewards {
1044    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1045        if self.rewards.is_empty() {
1046            writeln!(f, "No rewards found in epoch")?;
1047            return Ok(());
1048        }
1049
1050        if let Some(metadata) = &self.epoch_metadata {
1051            writeln!(f, "Epoch: {}", metadata.epoch)?;
1052            writeln!(f, "Reward Slot: {}", metadata.effective_slot)?;
1053            let timestamp = metadata.block_time;
1054            writeln!(f, "Block Time: {}", unix_timestamp_to_string(timestamp))?;
1055        }
1056        writeln!(f, "Epoch Rewards:")?;
1057        writeln!(
1058            f,
1059            "  {:<44}  {:<18}  {:<18}  {:>14}  {:>14}  {:>10}",
1060            "Address", "Amount", "New Balance", "Percent Change", "APR", "Commission"
1061        )?;
1062        for keyed_reward in &self.rewards {
1063            match &keyed_reward.reward {
1064                Some(reward) => {
1065                    writeln!(
1066                        f,
1067                        "  {:<44}  ◎{:<17.9}  ◎{:<17.9}  {:>13.9}%  {:>14}  {:>10}",
1068                        keyed_reward.address,
1069                        lamports_to_sol(reward.amount),
1070                        lamports_to_sol(reward.post_balance),
1071                        reward.percent_change,
1072                        reward
1073                            .apr
1074                            .map(|apr| format!("{apr:.2}%"))
1075                            .unwrap_or_default(),
1076                        reward
1077                            .commission
1078                            .map(|commission| format!("{commission}%"))
1079                            .unwrap_or_else(|| "-".to_string())
1080                    )?;
1081                }
1082                None => {
1083                    writeln!(f, "  {:<44}  No rewards in epoch", keyed_reward.address,)?;
1084                }
1085            }
1086        }
1087        Ok(())
1088    }
1089}
1090
1091fn show_votes_and_credits(
1092    f: &mut fmt::Formatter,
1093    votes: &[CliLandedVote],
1094    epoch_voting_history: &[CliEpochVotingHistory],
1095) -> fmt::Result {
1096    if votes.is_empty() {
1097        return Ok(());
1098    }
1099
1100    // Existence of this should guarantee the occurrence of vote truncation
1101    let newest_history_entry = epoch_voting_history.iter().next_back();
1102
1103    writeln!(
1104        f,
1105        "{} Votes (using {}/{} entries):",
1106        (if newest_history_entry.is_none() {
1107            "All"
1108        } else {
1109            "Recent"
1110        }),
1111        votes.len(),
1112        MAX_LOCKOUT_HISTORY
1113    )?;
1114
1115    for vote in votes.iter().rev() {
1116        write!(
1117            f,
1118            "- slot: {} (confirmation count: {})",
1119            vote.slot, vote.confirmation_count
1120        )?;
1121        if vote.latency == 0 {
1122            writeln!(f)?;
1123        } else {
1124            writeln!(f, " (latency {})", vote.latency)?;
1125        }
1126    }
1127    if let Some(newest) = newest_history_entry {
1128        writeln!(
1129            f,
1130            "- ... (truncated {} rooted votes, which have been credited)",
1131            newest.credits
1132        )?;
1133    }
1134
1135    if !epoch_voting_history.is_empty() {
1136        writeln!(
1137            f,
1138            "{} Epoch Voting History (using {}/{} entries):",
1139            (if epoch_voting_history.len() < MAX_EPOCH_CREDITS_HISTORY {
1140                "All"
1141            } else {
1142                "Recent"
1143            }),
1144            epoch_voting_history.len(),
1145            MAX_EPOCH_CREDITS_HISTORY
1146        )?;
1147        writeln!(
1148            f,
1149            "* missed credits include slots unavailable to vote on due to delinquent leaders",
1150        )?;
1151    }
1152
1153    for entry in epoch_voting_history.iter().rev() {
1154        writeln!(
1155            f, // tame fmt so that this will be folded like following
1156            "- epoch: {}",
1157            entry.epoch
1158        )?;
1159        writeln!(
1160            f,
1161            "  credits range: ({}..{}]",
1162            entry.prev_credits, entry.credits
1163        )?;
1164        writeln!(
1165            f,
1166            "  credits/max credits: {}/{}",
1167            entry.credits_earned,
1168            entry.slots_in_epoch * u64::from(entry.max_credits_per_slot)
1169        )?;
1170    }
1171    if let Some(oldest) = epoch_voting_history.iter().next() {
1172        if oldest.prev_credits > 0 {
1173            // Oldest entry doesn't start with 0. so history must be truncated...
1174
1175            // count of this combined pseudo credits range: (0..=oldest.prev_credits] like the above
1176            // (or this is just [1..=oldest.prev_credits] for human's simpler minds)
1177            let count = oldest.prev_credits;
1178
1179            writeln!(
1180                f,
1181                "- ... (omitting {count} past rooted votes, which have already been credited)"
1182            )?;
1183        }
1184    }
1185
1186    Ok(())
1187}
1188
1189enum Format {
1190    Csv,
1191    Human,
1192}
1193
1194macro_rules! format_as {
1195    ($target:expr, $fmt1:expr, $fmt2:expr, $which_fmt:expr, $($arg:tt)*) => {
1196        match $which_fmt {
1197            Format::Csv => {
1198                writeln!(
1199                    $target,
1200                    $fmt1,
1201                    $($arg)*
1202                )
1203            },
1204            Format::Human => {
1205                writeln!(
1206                    $target,
1207                    $fmt2,
1208                    $($arg)*
1209                )
1210            }
1211        }
1212    };
1213}
1214
1215fn show_epoch_rewards(
1216    f: &mut fmt::Formatter,
1217    epoch_rewards: &Option<Vec<CliEpochReward>>,
1218    use_csv: bool,
1219) -> fmt::Result {
1220    if let Some(epoch_rewards) = epoch_rewards {
1221        if epoch_rewards.is_empty() {
1222            return Ok(());
1223        }
1224
1225        writeln!(f, "Epoch Rewards:")?;
1226        let fmt = if use_csv { Format::Csv } else { Format::Human };
1227        format_as!(
1228            f,
1229            "{},{},{},{},{},{},{},{}",
1230            "  {:<6}  {:<11}  {:<26}  {:<18}  {:<18}  {:>14}  {:>14}  {:>10}",
1231            fmt,
1232            "Epoch",
1233            "Reward Slot",
1234            "Time",
1235            "Amount",
1236            "New Balance",
1237            "Percent Change",
1238            "APR",
1239            "Commission",
1240        )?;
1241        for reward in epoch_rewards {
1242            format_as!(
1243                f,
1244                "{},{},{},{},{},{}%,{},{}",
1245                "  {:<6}  {:<11}  {:<26}  ◎{:<17.9}  ◎{:<17.9}  {:>13.3}%  {:>14}  {:>10}",
1246                fmt,
1247                reward.epoch,
1248                reward.effective_slot,
1249                Utc.timestamp_opt(reward.block_time, 0).unwrap(),
1250                lamports_to_sol(reward.amount),
1251                lamports_to_sol(reward.post_balance),
1252                reward.percent_change,
1253                reward
1254                    .apr
1255                    .map(|apr| format!("{apr:.2}%"))
1256                    .unwrap_or_default(),
1257                reward
1258                    .commission
1259                    .map(|commission| format!("{commission}%"))
1260                    .unwrap_or_else(|| "-".to_string())
1261            )?;
1262        }
1263    }
1264    Ok(())
1265}
1266
1267#[derive(Default, Serialize, Deserialize)]
1268#[serde(rename_all = "camelCase")]
1269pub struct CliStakeState {
1270    pub stake_type: CliStakeType,
1271    pub account_balance: u64,
1272    #[serde(skip_serializing_if = "Option::is_none")]
1273    pub credits_observed: Option<u64>,
1274    #[serde(skip_serializing_if = "Option::is_none")]
1275    pub delegated_stake: Option<u64>,
1276    #[serde(skip_serializing_if = "Option::is_none")]
1277    pub delegated_vote_account_address: Option<String>,
1278    #[serde(skip_serializing_if = "Option::is_none")]
1279    pub activation_epoch: Option<Epoch>,
1280    #[serde(skip_serializing_if = "Option::is_none")]
1281    pub deactivation_epoch: Option<Epoch>,
1282    #[serde(flatten, skip_serializing_if = "Option::is_none")]
1283    pub authorized: Option<CliAuthorized>,
1284    #[serde(flatten, skip_serializing_if = "Option::is_none")]
1285    pub lockup: Option<CliLockup>,
1286    #[serde(skip_serializing)]
1287    pub use_lamports_unit: bool,
1288    #[serde(skip_serializing)]
1289    pub current_epoch: Epoch,
1290    #[serde(skip_serializing_if = "Option::is_none")]
1291    pub rent_exempt_reserve: Option<u64>,
1292    #[serde(skip_serializing_if = "Option::is_none")]
1293    pub active_stake: Option<u64>,
1294    #[serde(skip_serializing_if = "Option::is_none")]
1295    pub activating_stake: Option<u64>,
1296    #[serde(skip_serializing_if = "Option::is_none")]
1297    pub deactivating_stake: Option<u64>,
1298    #[serde(skip_serializing_if = "Option::is_none")]
1299    pub epoch_rewards: Option<Vec<CliEpochReward>>,
1300    #[serde(skip_serializing)]
1301    pub use_csv: bool,
1302}
1303
1304impl QuietDisplay for CliStakeState {}
1305impl VerboseDisplay for CliStakeState {
1306    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
1307        write!(w, "{self}")?;
1308        if let Some(credits) = self.credits_observed {
1309            writeln!(w, "Credits Observed: {credits}")?;
1310        }
1311        Ok(())
1312    }
1313}
1314
1315fn show_inactive_stake(
1316    me: &CliStakeState,
1317    f: &mut fmt::Formatter,
1318    delegated_stake: u64,
1319) -> fmt::Result {
1320    if let Some(deactivation_epoch) = me.deactivation_epoch {
1321        if me.current_epoch > deactivation_epoch {
1322            let deactivating_stake = me.deactivating_stake.or(me.active_stake);
1323            if let Some(deactivating_stake) = deactivating_stake {
1324                writeln!(
1325                    f,
1326                    "Inactive Stake: {}",
1327                    build_balance_message(
1328                        delegated_stake - deactivating_stake,
1329                        me.use_lamports_unit,
1330                        true
1331                    ),
1332                )?;
1333                writeln!(
1334                    f,
1335                    "Deactivating Stake: {}",
1336                    build_balance_message(deactivating_stake, me.use_lamports_unit, true),
1337                )?;
1338            }
1339        }
1340        writeln!(
1341            f,
1342            "Stake deactivates starting from epoch: {deactivation_epoch}"
1343        )?;
1344    }
1345    if let Some(delegated_vote_account_address) = &me.delegated_vote_account_address {
1346        writeln!(
1347            f,
1348            "Delegated Vote Account Address: {delegated_vote_account_address}"
1349        )?;
1350    }
1351    Ok(())
1352}
1353
1354fn show_active_stake(
1355    me: &CliStakeState,
1356    f: &mut fmt::Formatter,
1357    delegated_stake: u64,
1358) -> fmt::Result {
1359    if me
1360        .deactivation_epoch
1361        .map(|d| me.current_epoch <= d)
1362        .unwrap_or(true)
1363    {
1364        let active_stake = me.active_stake.unwrap_or(0);
1365        writeln!(
1366            f,
1367            "Active Stake: {}",
1368            build_balance_message(active_stake, me.use_lamports_unit, true),
1369        )?;
1370        let activating_stake = me.activating_stake.or_else(|| {
1371            if me.active_stake.is_none() {
1372                Some(delegated_stake)
1373            } else {
1374                None
1375            }
1376        });
1377        if let Some(activating_stake) = activating_stake {
1378            writeln!(
1379                f,
1380                "Activating Stake: {}",
1381                build_balance_message(activating_stake, me.use_lamports_unit, true),
1382            )?;
1383            writeln!(
1384                f,
1385                "Stake activates starting from epoch: {}",
1386                me.activation_epoch.unwrap()
1387            )?;
1388        }
1389    }
1390    Ok(())
1391}
1392
1393impl fmt::Display for CliStakeState {
1394    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1395        fn show_authorized(f: &mut fmt::Formatter, authorized: &CliAuthorized) -> fmt::Result {
1396            writeln!(f, "Stake Authority: {}", authorized.staker)?;
1397            writeln!(f, "Withdraw Authority: {}", authorized.withdrawer)?;
1398            Ok(())
1399        }
1400        fn show_lockup(f: &mut fmt::Formatter, lockup: Option<&CliLockup>) -> fmt::Result {
1401            if let Some(lockup) = lockup {
1402                if lockup.unix_timestamp != UnixTimestamp::default() {
1403                    writeln!(
1404                        f,
1405                        "Lockup Timestamp: {}",
1406                        unix_timestamp_to_string(lockup.unix_timestamp)
1407                    )?;
1408                }
1409                if lockup.epoch != Epoch::default() {
1410                    writeln!(f, "Lockup Epoch: {}", lockup.epoch)?;
1411                }
1412                writeln!(f, "Lockup Custodian: {}", lockup.custodian)?;
1413            }
1414            Ok(())
1415        }
1416
1417        writeln!(
1418            f,
1419            "Balance: {}",
1420            build_balance_message(self.account_balance, self.use_lamports_unit, true)
1421        )?;
1422
1423        if let Some(rent_exempt_reserve) = self.rent_exempt_reserve {
1424            writeln!(
1425                f,
1426                "Rent Exempt Reserve: {}",
1427                build_balance_message(rent_exempt_reserve, self.use_lamports_unit, true)
1428            )?;
1429        }
1430
1431        match self.stake_type {
1432            CliStakeType::RewardsPool => writeln!(f, "Stake account is a rewards pool")?,
1433            CliStakeType::Uninitialized => writeln!(f, "Stake account is uninitialized")?,
1434            CliStakeType::Initialized => {
1435                writeln!(f, "Stake account is undelegated")?;
1436                show_authorized(f, self.authorized.as_ref().unwrap())?;
1437                show_lockup(f, self.lockup.as_ref())?;
1438            }
1439            CliStakeType::Stake => {
1440                let show_delegation = {
1441                    self.active_stake.is_some()
1442                        || self.activating_stake.is_some()
1443                        || self.deactivating_stake.is_some()
1444                        || self
1445                            .deactivation_epoch
1446                            .map(|de| de > self.current_epoch)
1447                            .unwrap_or(true)
1448                };
1449                if show_delegation {
1450                    let delegated_stake = self.delegated_stake.unwrap();
1451                    writeln!(
1452                        f,
1453                        "Delegated Stake: {}",
1454                        build_balance_message(delegated_stake, self.use_lamports_unit, true)
1455                    )?;
1456                    show_active_stake(self, f, delegated_stake)?;
1457                    show_inactive_stake(self, f, delegated_stake)?;
1458                } else {
1459                    writeln!(f, "Stake account is undelegated")?;
1460                }
1461                show_authorized(f, self.authorized.as_ref().unwrap())?;
1462                show_lockup(f, self.lockup.as_ref())?;
1463                show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?
1464            }
1465        }
1466        Ok(())
1467    }
1468}
1469
1470#[derive(Serialize, Deserialize, PartialEq, Eq)]
1471pub enum CliStakeType {
1472    Stake,
1473    RewardsPool,
1474    Uninitialized,
1475    Initialized,
1476}
1477
1478impl Default for CliStakeType {
1479    fn default() -> Self {
1480        Self::Uninitialized
1481    }
1482}
1483
1484#[derive(Serialize, Deserialize)]
1485#[serde(rename_all = "camelCase")]
1486pub struct CliStakeHistory {
1487    pub entries: Vec<CliStakeHistoryEntry>,
1488    #[serde(skip_serializing)]
1489    pub use_lamports_unit: bool,
1490}
1491
1492impl QuietDisplay for CliStakeHistory {}
1493impl VerboseDisplay for CliStakeHistory {}
1494
1495impl fmt::Display for CliStakeHistory {
1496    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1497        writeln!(f)?;
1498        writeln!(
1499            f,
1500            "{}",
1501            style(format!(
1502                "  {:<5}  {:>20}  {:>20}  {:>20}",
1503                "Epoch", "Effective Stake", "Activating Stake", "Deactivating Stake",
1504            ))
1505            .bold()
1506        )?;
1507        let config = BuildBalanceMessageConfig {
1508            use_lamports_unit: self.use_lamports_unit,
1509            show_unit: false,
1510            trim_trailing_zeros: false,
1511        };
1512        for entry in &self.entries {
1513            writeln!(
1514                f,
1515                "  {:>5}  {:>20}  {:>20}  {:>20} {}",
1516                entry.epoch,
1517                build_balance_message_with_config(entry.effective_stake, &config),
1518                build_balance_message_with_config(entry.activating_stake, &config),
1519                build_balance_message_with_config(entry.deactivating_stake, &config),
1520                if self.use_lamports_unit {
1521                    "lamports"
1522                } else {
1523                    "SOL"
1524                }
1525            )?;
1526        }
1527        Ok(())
1528    }
1529}
1530
1531impl From<&(Epoch, StakeHistoryEntry)> for CliStakeHistoryEntry {
1532    fn from((epoch, entry): &(Epoch, StakeHistoryEntry)) -> Self {
1533        Self {
1534            epoch: *epoch,
1535            effective_stake: entry.effective,
1536            activating_stake: entry.activating,
1537            deactivating_stake: entry.deactivating,
1538        }
1539    }
1540}
1541
1542#[derive(Serialize, Deserialize)]
1543#[serde(rename_all = "camelCase")]
1544pub struct CliStakeHistoryEntry {
1545    pub epoch: Epoch,
1546    pub effective_stake: u64,
1547    pub activating_stake: u64,
1548    pub deactivating_stake: u64,
1549}
1550
1551#[derive(Serialize, Deserialize)]
1552#[serde(rename_all = "camelCase")]
1553pub struct CliAuthorized {
1554    pub staker: String,
1555    pub withdrawer: String,
1556}
1557
1558impl From<&Authorized> for CliAuthorized {
1559    fn from(authorized: &Authorized) -> Self {
1560        Self {
1561            staker: authorized.staker.to_string(),
1562            withdrawer: authorized.withdrawer.to_string(),
1563        }
1564    }
1565}
1566
1567#[derive(Serialize, Deserialize)]
1568#[serde(rename_all = "camelCase")]
1569pub struct CliLockup {
1570    pub unix_timestamp: UnixTimestamp,
1571    pub epoch: Epoch,
1572    pub custodian: String,
1573}
1574
1575impl From<&Lockup> for CliLockup {
1576    fn from(lockup: &Lockup) -> Self {
1577        Self {
1578            unix_timestamp: lockup.unix_timestamp,
1579            epoch: lockup.epoch,
1580            custodian: lockup.custodian.to_string(),
1581        }
1582    }
1583}
1584
1585#[derive(Serialize, Deserialize)]
1586pub struct CliValidatorInfoVec(Vec<CliValidatorInfo>);
1587
1588impl CliValidatorInfoVec {
1589    pub fn new(list: Vec<CliValidatorInfo>) -> Self {
1590        Self(list)
1591    }
1592}
1593
1594impl QuietDisplay for CliValidatorInfoVec {}
1595impl VerboseDisplay for CliValidatorInfoVec {}
1596
1597impl fmt::Display for CliValidatorInfoVec {
1598    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1599        if self.0.is_empty() {
1600            writeln!(f, "No validator info accounts found")?;
1601        }
1602        for validator_info in &self.0 {
1603            writeln!(f)?;
1604            write!(f, "{validator_info}")?;
1605        }
1606        Ok(())
1607    }
1608}
1609
1610#[derive(Serialize, Deserialize)]
1611#[serde(rename_all = "camelCase")]
1612pub struct CliValidatorInfo {
1613    pub identity_pubkey: String,
1614    pub info_pubkey: String,
1615    pub info: Map<String, Value>,
1616}
1617
1618impl QuietDisplay for CliValidatorInfo {}
1619impl VerboseDisplay for CliValidatorInfo {}
1620
1621impl fmt::Display for CliValidatorInfo {
1622    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1623        writeln_name_value(f, "Validator Identity:", &self.identity_pubkey)?;
1624        writeln_name_value(f, "  Info Address:", &self.info_pubkey)?;
1625        for (key, value) in self.info.iter() {
1626            writeln_name_value(
1627                f,
1628                &format!("  {}:", to_title_case(key)),
1629                value.as_str().unwrap_or("?"),
1630            )?;
1631        }
1632        Ok(())
1633    }
1634}
1635
1636#[derive(Default, Serialize, Deserialize)]
1637#[serde(rename_all = "camelCase")]
1638pub struct CliVoteAccount {
1639    pub account_balance: u64,
1640    pub validator_identity: String,
1641    #[serde(flatten)]
1642    pub authorized_voters: CliAuthorizedVoters,
1643    pub authorized_withdrawer: String,
1644    pub credits: u64,
1645    pub commission: u8,
1646    pub root_slot: Option<Slot>,
1647    pub recent_timestamp: BlockTimestamp,
1648    pub votes: Vec<CliLandedVote>,
1649    pub epoch_voting_history: Vec<CliEpochVotingHistory>,
1650    #[serde(skip_serializing)]
1651    pub use_lamports_unit: bool,
1652    #[serde(skip_serializing)]
1653    pub use_csv: bool,
1654    #[serde(skip_serializing_if = "Option::is_none")]
1655    pub epoch_rewards: Option<Vec<CliEpochReward>>,
1656}
1657
1658impl QuietDisplay for CliVoteAccount {}
1659impl VerboseDisplay for CliVoteAccount {}
1660
1661impl fmt::Display for CliVoteAccount {
1662    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1663        writeln!(
1664            f,
1665            "Account Balance: {}",
1666            build_balance_message(self.account_balance, self.use_lamports_unit, true)
1667        )?;
1668        writeln!(f, "Validator Identity: {}", self.validator_identity)?;
1669        writeln!(f, "Vote Authority: {}", self.authorized_voters)?;
1670        writeln!(f, "Withdraw Authority: {}", self.authorized_withdrawer)?;
1671        writeln!(f, "Credits: {}", self.credits)?;
1672        writeln!(f, "Commission: {}%", self.commission)?;
1673        writeln!(
1674            f,
1675            "Root Slot: {}",
1676            match self.root_slot {
1677                Some(slot) => slot.to_string(),
1678                None => "~".to_string(),
1679            }
1680        )?;
1681        writeln!(
1682            f,
1683            "Recent Timestamp: {} from slot {}",
1684            unix_timestamp_to_string(self.recent_timestamp.timestamp),
1685            self.recent_timestamp.slot
1686        )?;
1687        show_votes_and_credits(f, &self.votes, &self.epoch_voting_history)?;
1688        show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?;
1689        Ok(())
1690    }
1691}
1692
1693#[derive(Default, Debug, Serialize, Deserialize)]
1694#[serde(rename_all = "camelCase")]
1695pub struct CliAuthorizedVoters {
1696    authorized_voters: BTreeMap<Epoch, String>,
1697}
1698
1699impl QuietDisplay for CliAuthorizedVoters {}
1700impl VerboseDisplay for CliAuthorizedVoters {}
1701
1702impl fmt::Display for CliAuthorizedVoters {
1703    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1704        if let Some((_epoch, current_authorized_voter)) = self.authorized_voters.first_key_value() {
1705            write!(f, "{current_authorized_voter}")?;
1706        } else {
1707            write!(f, "None")?;
1708        }
1709        if self.authorized_voters.len() > 1 {
1710            let (epoch, upcoming_authorized_voter) = self
1711                .authorized_voters
1712                .last_key_value()
1713                .expect("CliAuthorizedVoters::authorized_voters.len() > 1");
1714            writeln!(f)?;
1715            write!(
1716                f,
1717                "  New Vote Authority as of Epoch {epoch}: {upcoming_authorized_voter}"
1718            )?;
1719        }
1720        Ok(())
1721    }
1722}
1723
1724impl From<&AuthorizedVoters> for CliAuthorizedVoters {
1725    fn from(authorized_voters: &AuthorizedVoters) -> Self {
1726        let mut voter_map: BTreeMap<Epoch, String> = BTreeMap::new();
1727        for (epoch, voter) in authorized_voters.iter() {
1728            voter_map.insert(*epoch, voter.to_string());
1729        }
1730        Self {
1731            authorized_voters: voter_map,
1732        }
1733    }
1734}
1735
1736#[derive(Serialize, Deserialize)]
1737#[serde(rename_all = "camelCase")]
1738pub struct CliEpochVotingHistory {
1739    pub epoch: Epoch,
1740    pub slots_in_epoch: u64,
1741    pub credits_earned: u64,
1742    pub credits: u64,
1743    pub prev_credits: u64,
1744    pub max_credits_per_slot: u8,
1745}
1746
1747#[derive(Serialize, Deserialize)]
1748#[serde(rename_all = "camelCase")]
1749pub struct CliLandedVote {
1750    pub latency: u8,
1751    pub slot: Slot,
1752    pub confirmation_count: u32,
1753}
1754
1755impl From<&LandedVote> for CliLandedVote {
1756    fn from(landed_vote: &LandedVote) -> Self {
1757        Self {
1758            latency: landed_vote.latency,
1759            slot: landed_vote.slot(),
1760            confirmation_count: landed_vote.confirmation_count(),
1761        }
1762    }
1763}
1764
1765#[derive(Serialize, Deserialize)]
1766#[serde(rename_all = "camelCase")]
1767pub struct CliBlockTime {
1768    pub slot: Slot,
1769    pub timestamp: UnixTimestamp,
1770}
1771
1772impl QuietDisplay for CliBlockTime {}
1773impl VerboseDisplay for CliBlockTime {}
1774
1775impl fmt::Display for CliBlockTime {
1776    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1777        writeln_name_value(f, "Block:", &self.slot.to_string())?;
1778        writeln_name_value(f, "Date:", &unix_timestamp_to_string(self.timestamp))
1779    }
1780}
1781
1782#[derive(Serialize, Deserialize)]
1783#[serde(rename_all = "camelCase")]
1784pub struct CliLeaderSchedule {
1785    pub epoch: Epoch,
1786    pub leader_schedule_entries: Vec<CliLeaderScheduleEntry>,
1787}
1788
1789impl QuietDisplay for CliLeaderSchedule {}
1790impl VerboseDisplay for CliLeaderSchedule {}
1791
1792impl fmt::Display for CliLeaderSchedule {
1793    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1794        for entry in &self.leader_schedule_entries {
1795            writeln!(f, "  {:<15} {:<44}", entry.slot, entry.leader)?;
1796        }
1797        Ok(())
1798    }
1799}
1800
1801#[derive(Serialize, Deserialize)]
1802#[serde(rename_all = "camelCase")]
1803pub struct CliLeaderScheduleEntry {
1804    pub slot: Slot,
1805    pub leader: String,
1806}
1807
1808#[derive(Serialize, Deserialize)]
1809#[serde(rename_all = "camelCase")]
1810pub struct CliInflation {
1811    pub governor: RpcInflationGovernor,
1812    pub current_rate: RpcInflationRate,
1813}
1814
1815impl QuietDisplay for CliInflation {}
1816impl VerboseDisplay for CliInflation {}
1817
1818impl fmt::Display for CliInflation {
1819    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1820        writeln!(f, "{}", style("Inflation Governor:").bold())?;
1821        if (self.governor.initial - self.governor.terminal).abs() < f64::EPSILON {
1822            writeln!(
1823                f,
1824                "Fixed rate:              {:>5.2}%",
1825                self.governor.terminal * 100.
1826            )?;
1827        } else {
1828            writeln!(
1829                f,
1830                "Initial rate:            {:>5.2}%",
1831                self.governor.initial * 100.
1832            )?;
1833            writeln!(
1834                f,
1835                "Terminal rate:           {:>5.2}%",
1836                self.governor.terminal * 100.
1837            )?;
1838            writeln!(
1839                f,
1840                "Rate reduction per year: {:>5.2}%",
1841                self.governor.taper * 100.
1842            )?;
1843            writeln!(
1844                f,
1845                "* Rate reduction is derived using the target slot time in genesis config"
1846            )?;
1847        }
1848        if self.governor.foundation_term > 0. {
1849            writeln!(
1850                f,
1851                "Foundation percentage:   {:>5.2}%",
1852                self.governor.foundation
1853            )?;
1854            writeln!(
1855                f,
1856                "Foundation term:         {:.1} years",
1857                self.governor.foundation_term
1858            )?;
1859        }
1860
1861        writeln!(
1862            f,
1863            "\n{}",
1864            style(format!("Inflation for Epoch {}:", self.current_rate.epoch)).bold()
1865        )?;
1866        writeln!(
1867            f,
1868            "Total rate:              {:>5.2}%",
1869            self.current_rate.total * 100.
1870        )?;
1871        writeln!(
1872            f,
1873            "Staking rate:            {:>5.2}%",
1874            self.current_rate.validator * 100.
1875        )?;
1876
1877        if self.current_rate.foundation > 0. {
1878            writeln!(
1879                f,
1880                "Foundation rate:         {:>5.2}%",
1881                self.current_rate.foundation * 100.
1882            )?;
1883        }
1884        Ok(())
1885    }
1886}
1887
1888#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
1889#[serde(rename_all = "camelCase")]
1890pub struct CliSignOnlyData {
1891    pub blockhash: String,
1892    #[serde(skip_serializing_if = "Option::is_none")]
1893    pub message: Option<String>,
1894    #[serde(skip_serializing_if = "Vec::is_empty", default)]
1895    pub signers: Vec<String>,
1896    #[serde(skip_serializing_if = "Vec::is_empty", default)]
1897    pub absent: Vec<String>,
1898    #[serde(skip_serializing_if = "Vec::is_empty", default)]
1899    pub bad_sig: Vec<String>,
1900}
1901
1902impl QuietDisplay for CliSignOnlyData {}
1903impl VerboseDisplay for CliSignOnlyData {}
1904
1905impl fmt::Display for CliSignOnlyData {
1906    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1907        writeln!(f)?;
1908        writeln_name_value(f, "Blockhash:", &self.blockhash)?;
1909        if let Some(message) = self.message.as_ref() {
1910            writeln_name_value(f, "Transaction Message:", message)?;
1911        }
1912        if !self.signers.is_empty() {
1913            writeln!(f, "{}", style("Signers (Pubkey=Signature):").bold())?;
1914            for signer in self.signers.iter() {
1915                writeln!(f, " {signer}")?;
1916            }
1917        }
1918        if !self.absent.is_empty() {
1919            writeln!(f, "{}", style("Absent Signers (Pubkey):").bold())?;
1920            for pubkey in self.absent.iter() {
1921                writeln!(f, " {pubkey}")?;
1922            }
1923        }
1924        if !self.bad_sig.is_empty() {
1925            writeln!(f, "{}", style("Bad Signatures (Pubkey):").bold())?;
1926            for pubkey in self.bad_sig.iter() {
1927                writeln!(f, " {pubkey}")?;
1928            }
1929        }
1930        Ok(())
1931    }
1932}
1933
1934#[derive(Serialize, Deserialize)]
1935#[serde(rename_all = "camelCase")]
1936pub struct CliSignature {
1937    pub signature: String,
1938}
1939
1940impl QuietDisplay for CliSignature {}
1941impl VerboseDisplay for CliSignature {}
1942
1943impl fmt::Display for CliSignature {
1944    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1945        writeln!(f)?;
1946        writeln_name_value(f, "Signature:", &self.signature)?;
1947        Ok(())
1948    }
1949}
1950
1951#[derive(Serialize, Deserialize)]
1952#[serde(rename_all = "camelCase")]
1953pub struct CliAccountBalances {
1954    pub accounts: Vec<RpcAccountBalance>,
1955}
1956
1957impl QuietDisplay for CliAccountBalances {}
1958impl VerboseDisplay for CliAccountBalances {}
1959
1960impl fmt::Display for CliAccountBalances {
1961    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1962        writeln!(
1963            f,
1964            "{}",
1965            style(format!("{:<44}  {}", "Address", "Balance")).bold()
1966        )?;
1967        for account in &self.accounts {
1968            writeln!(
1969                f,
1970                "{:<44}  {}",
1971                account.address,
1972                &format!("{} SOL", lamports_to_sol(account.lamports))
1973            )?;
1974        }
1975        Ok(())
1976    }
1977}
1978
1979#[derive(Serialize, Deserialize)]
1980#[serde(rename_all = "camelCase")]
1981pub struct CliSupply {
1982    pub total: u64,
1983    pub circulating: u64,
1984    pub non_circulating: u64,
1985    pub non_circulating_accounts: Vec<String>,
1986    #[serde(skip_serializing)]
1987    pub print_accounts: bool,
1988}
1989
1990impl From<RpcSupply> for CliSupply {
1991    fn from(rpc_supply: RpcSupply) -> Self {
1992        Self {
1993            total: rpc_supply.total,
1994            circulating: rpc_supply.circulating,
1995            non_circulating: rpc_supply.non_circulating,
1996            non_circulating_accounts: rpc_supply.non_circulating_accounts,
1997            print_accounts: false,
1998        }
1999    }
2000}
2001
2002impl QuietDisplay for CliSupply {}
2003impl VerboseDisplay for CliSupply {}
2004
2005impl fmt::Display for CliSupply {
2006    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2007        writeln_name_value(f, "Total:", &format!("{} SOL", lamports_to_sol(self.total)))?;
2008        writeln_name_value(
2009            f,
2010            "Circulating:",
2011            &format!("{} SOL", lamports_to_sol(self.circulating)),
2012        )?;
2013        writeln_name_value(
2014            f,
2015            "Non-Circulating:",
2016            &format!("{} SOL", lamports_to_sol(self.non_circulating)),
2017        )?;
2018        if self.print_accounts {
2019            writeln!(f)?;
2020            writeln_name_value(f, "Non-Circulating Accounts:", " ")?;
2021            for account in &self.non_circulating_accounts {
2022                writeln!(f, "  {account}")?;
2023            }
2024        }
2025        Ok(())
2026    }
2027}
2028
2029#[derive(Serialize, Deserialize)]
2030#[serde(rename_all = "camelCase")]
2031pub struct CliFeesInner {
2032    pub slot: Slot,
2033    pub blockhash: String,
2034    pub lamports_per_signature: u64,
2035    pub last_valid_slot: Option<Slot>,
2036    pub last_valid_block_height: Option<Slot>,
2037}
2038
2039impl QuietDisplay for CliFeesInner {}
2040impl VerboseDisplay for CliFeesInner {}
2041
2042impl fmt::Display for CliFeesInner {
2043    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2044        writeln_name_value(f, "Blockhash:", &self.blockhash)?;
2045        writeln_name_value(
2046            f,
2047            "Lamports per signature:",
2048            &self.lamports_per_signature.to_string(),
2049        )?;
2050        let last_valid_block_height = self
2051            .last_valid_block_height
2052            .map(|s| s.to_string())
2053            .unwrap_or_default();
2054        writeln_name_value(f, "Last valid block height:", &last_valid_block_height)
2055    }
2056}
2057
2058#[derive(Serialize, Deserialize)]
2059#[serde(rename_all = "camelCase")]
2060pub struct CliFees {
2061    #[serde(flatten, skip_serializing_if = "Option::is_none")]
2062    pub inner: Option<CliFeesInner>,
2063}
2064
2065impl QuietDisplay for CliFees {}
2066impl VerboseDisplay for CliFees {}
2067
2068impl fmt::Display for CliFees {
2069    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2070        match self.inner.as_ref() {
2071            Some(inner) => write!(f, "{inner}"),
2072            None => write!(f, "Fees unavailable"),
2073        }
2074    }
2075}
2076
2077impl CliFees {
2078    pub fn some(
2079        slot: Slot,
2080        blockhash: Hash,
2081        lamports_per_signature: u64,
2082        last_valid_slot: Option<Slot>,
2083        last_valid_block_height: Option<Slot>,
2084    ) -> Self {
2085        Self {
2086            inner: Some(CliFeesInner {
2087                slot,
2088                blockhash: blockhash.to_string(),
2089                lamports_per_signature,
2090                last_valid_slot,
2091                last_valid_block_height,
2092            }),
2093        }
2094    }
2095    pub fn none() -> Self {
2096        Self { inner: None }
2097    }
2098}
2099
2100#[derive(Serialize, Deserialize)]
2101#[serde(rename_all = "camelCase")]
2102pub struct CliTokenAccount {
2103    pub address: String,
2104    #[serde(flatten)]
2105    pub token_account: UiTokenAccount,
2106}
2107
2108impl QuietDisplay for CliTokenAccount {}
2109impl VerboseDisplay for CliTokenAccount {}
2110
2111impl fmt::Display for CliTokenAccount {
2112    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2113        writeln!(f)?;
2114        writeln_name_value(f, "Address:", &self.address)?;
2115        let account = &self.token_account;
2116        writeln_name_value(
2117            f,
2118            "Balance:",
2119            &account.token_amount.real_number_string_trimmed(),
2120        )?;
2121        let mint = format!(
2122            "{}{}",
2123            account.mint,
2124            if account.is_native { " (native)" } else { "" }
2125        );
2126        writeln_name_value(f, "Mint:", &mint)?;
2127        writeln_name_value(f, "Owner:", &account.owner)?;
2128        writeln_name_value(f, "State:", &format!("{:?}", account.state))?;
2129        if let Some(delegate) = &account.delegate {
2130            writeln!(f, "Delegation:")?;
2131            writeln_name_value(f, "  Delegate:", delegate)?;
2132            let allowance = account.delegated_amount.as_ref().unwrap();
2133            writeln_name_value(f, "  Allowance:", &allowance.real_number_string_trimmed())?;
2134        }
2135        writeln_name_value(
2136            f,
2137            "Close authority:",
2138            account.close_authority.as_ref().unwrap_or(&String::new()),
2139        )?;
2140        Ok(())
2141    }
2142}
2143
2144#[derive(Serialize, Deserialize)]
2145#[serde(rename_all = "camelCase")]
2146pub struct CliProgramId {
2147    pub program_id: String,
2148    pub signature: Option<String>,
2149}
2150
2151impl QuietDisplay for CliProgramId {}
2152impl VerboseDisplay for CliProgramId {}
2153
2154impl fmt::Display for CliProgramId {
2155    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2156        writeln_name_value(f, "Program Id:", &self.program_id)?;
2157        if let Some(ref signature) = self.signature {
2158            writeln!(f)?;
2159            writeln_name_value(f, "Signature:", signature)?;
2160        }
2161        Ok(())
2162    }
2163}
2164
2165#[derive(Serialize, Deserialize)]
2166#[serde(rename_all = "camelCase")]
2167pub struct CliProgramBuffer {
2168    pub buffer: String,
2169}
2170
2171impl QuietDisplay for CliProgramBuffer {}
2172impl VerboseDisplay for CliProgramBuffer {}
2173
2174impl fmt::Display for CliProgramBuffer {
2175    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2176        writeln_name_value(f, "Buffer:", &self.buffer)
2177    }
2178}
2179
2180#[derive(Debug, Serialize, Deserialize)]
2181#[serde(rename_all = "camelCase")]
2182pub enum CliProgramAccountType {
2183    Buffer,
2184    Program,
2185}
2186
2187#[derive(Serialize, Deserialize)]
2188#[serde(rename_all = "camelCase")]
2189pub struct CliProgramAuthority {
2190    pub authority: String,
2191    pub account_type: CliProgramAccountType,
2192}
2193
2194impl QuietDisplay for CliProgramAuthority {}
2195impl VerboseDisplay for CliProgramAuthority {}
2196
2197impl fmt::Display for CliProgramAuthority {
2198    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2199        writeln_name_value(f, "Account Type:", &format!("{:?}", self.account_type))?;
2200        writeln_name_value(f, "Authority:", &self.authority)
2201    }
2202}
2203
2204#[derive(Serialize, Deserialize)]
2205#[serde(rename_all = "camelCase")]
2206pub struct CliProgram {
2207    pub program_id: String,
2208    pub owner: String,
2209    pub data_len: usize,
2210}
2211impl QuietDisplay for CliProgram {}
2212impl VerboseDisplay for CliProgram {}
2213impl fmt::Display for CliProgram {
2214    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2215        writeln!(f)?;
2216        writeln_name_value(f, "Program Id:", &self.program_id)?;
2217        writeln_name_value(f, "Owner:", &self.owner)?;
2218        writeln_name_value(
2219            f,
2220            "Data Length:",
2221            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2222        )?;
2223        Ok(())
2224    }
2225}
2226
2227#[derive(Serialize, Deserialize)]
2228#[serde(rename_all = "camelCase")]
2229pub struct CliProgramV4 {
2230    pub program_id: String,
2231    pub owner: String,
2232    pub authority: String,
2233    pub last_deploy_slot: u64,
2234    pub status: String,
2235    pub data_len: usize,
2236}
2237impl QuietDisplay for CliProgramV4 {}
2238impl VerboseDisplay for CliProgramV4 {}
2239impl fmt::Display for CliProgramV4 {
2240    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2241        writeln!(f)?;
2242        writeln_name_value(f, "Program Id:", &self.program_id)?;
2243        writeln_name_value(f, "Owner:", &self.owner)?;
2244        writeln_name_value(f, "Authority:", &self.authority)?;
2245        writeln_name_value(
2246            f,
2247            "Last Deployed In Slot:",
2248            &self.last_deploy_slot.to_string(),
2249        )?;
2250        writeln_name_value(f, "Status:", &self.status)?;
2251        writeln_name_value(
2252            f,
2253            "Data Length:",
2254            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2255        )?;
2256        Ok(())
2257    }
2258}
2259
2260#[derive(Serialize, Deserialize)]
2261#[serde(rename_all = "camelCase")]
2262pub struct CliProgramsV4 {
2263    pub programs: Vec<CliProgramV4>,
2264}
2265impl QuietDisplay for CliProgramsV4 {}
2266impl VerboseDisplay for CliProgramsV4 {}
2267impl fmt::Display for CliProgramsV4 {
2268    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2269        writeln!(f)?;
2270        writeln!(
2271            f,
2272            "{}",
2273            style(format!(
2274                "{:<44} | {:<9} | {:<44} | {:<10}",
2275                "Program Id", "Slot", "Authority", "Status"
2276            ))
2277            .bold()
2278        )?;
2279        for program in self.programs.iter() {
2280            writeln!(
2281                f,
2282                "{}",
2283                &format!(
2284                    "{:<44} | {:<9} | {:<44} | {:<10}",
2285                    program.program_id, program.last_deploy_slot, program.authority, program.status,
2286                )
2287            )?;
2288        }
2289        Ok(())
2290    }
2291}
2292
2293#[derive(Serialize, Deserialize)]
2294#[serde(rename_all = "camelCase")]
2295pub struct CliUpgradeableProgram {
2296    pub program_id: String,
2297    pub owner: String,
2298    pub programdata_address: String,
2299    pub authority: String,
2300    pub last_deploy_slot: u64,
2301    pub data_len: usize,
2302    pub lamports: u64,
2303    #[serde(skip_serializing)]
2304    pub use_lamports_unit: bool,
2305}
2306impl QuietDisplay for CliUpgradeableProgram {}
2307impl VerboseDisplay for CliUpgradeableProgram {}
2308impl fmt::Display for CliUpgradeableProgram {
2309    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2310        writeln!(f)?;
2311        writeln_name_value(f, "Program Id:", &self.program_id)?;
2312        writeln_name_value(f, "Owner:", &self.owner)?;
2313        writeln_name_value(f, "ProgramData Address:", &self.programdata_address)?;
2314        writeln_name_value(f, "Authority:", &self.authority)?;
2315        writeln_name_value(
2316            f,
2317            "Last Deployed In Slot:",
2318            &self.last_deploy_slot.to_string(),
2319        )?;
2320        writeln_name_value(
2321            f,
2322            "Data Length:",
2323            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2324        )?;
2325        writeln_name_value(
2326            f,
2327            "Balance:",
2328            &build_balance_message(self.lamports, self.use_lamports_unit, true),
2329        )?;
2330        Ok(())
2331    }
2332}
2333
2334#[derive(Serialize, Deserialize)]
2335#[serde(rename_all = "camelCase")]
2336pub struct CliUpgradeablePrograms {
2337    pub programs: Vec<CliUpgradeableProgram>,
2338    #[serde(skip_serializing)]
2339    pub use_lamports_unit: bool,
2340}
2341impl QuietDisplay for CliUpgradeablePrograms {}
2342impl VerboseDisplay for CliUpgradeablePrograms {}
2343impl fmt::Display for CliUpgradeablePrograms {
2344    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2345        writeln!(f)?;
2346        writeln!(
2347            f,
2348            "{}",
2349            style(format!(
2350                "{:<44} | {:<9} | {:<44} | {}",
2351                "Program Id", "Slot", "Authority", "Balance"
2352            ))
2353            .bold()
2354        )?;
2355        for program in self.programs.iter() {
2356            writeln!(
2357                f,
2358                "{}",
2359                &format!(
2360                    "{:<44} | {:<9} | {:<44} | {}",
2361                    program.program_id,
2362                    program.last_deploy_slot,
2363                    program.authority,
2364                    build_balance_message(program.lamports, self.use_lamports_unit, true)
2365                )
2366            )?;
2367        }
2368        Ok(())
2369    }
2370}
2371
2372#[derive(Serialize, Deserialize)]
2373#[serde(rename_all = "camelCase")]
2374pub struct CliUpgradeableProgramClosed {
2375    pub program_id: String,
2376    pub lamports: u64,
2377    #[serde(skip_serializing)]
2378    pub use_lamports_unit: bool,
2379}
2380impl QuietDisplay for CliUpgradeableProgramClosed {}
2381impl VerboseDisplay for CliUpgradeableProgramClosed {}
2382impl fmt::Display for CliUpgradeableProgramClosed {
2383    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2384        writeln!(f)?;
2385        writeln!(
2386            f,
2387            "Closed Program Id {}, {} reclaimed",
2388            &self.program_id,
2389            &build_balance_message(self.lamports, self.use_lamports_unit, true)
2390        )?;
2391        Ok(())
2392    }
2393}
2394
2395#[derive(Serialize, Deserialize)]
2396#[serde(rename_all = "camelCase")]
2397pub struct CliUpgradeableProgramExtended {
2398    pub program_id: String,
2399    pub additional_bytes: u32,
2400}
2401impl QuietDisplay for CliUpgradeableProgramExtended {}
2402impl VerboseDisplay for CliUpgradeableProgramExtended {}
2403impl fmt::Display for CliUpgradeableProgramExtended {
2404    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2405        writeln!(f)?;
2406        writeln!(
2407            f,
2408            "Extended Program Id {} by {} bytes",
2409            &self.program_id, self.additional_bytes,
2410        )?;
2411        Ok(())
2412    }
2413}
2414
2415#[derive(Clone, Serialize, Deserialize)]
2416#[serde(rename_all = "camelCase")]
2417pub struct CliUpgradeableBuffer {
2418    pub address: String,
2419    pub authority: String,
2420    pub data_len: usize,
2421    pub lamports: u64,
2422    #[serde(skip_serializing)]
2423    pub use_lamports_unit: bool,
2424}
2425impl QuietDisplay for CliUpgradeableBuffer {}
2426impl VerboseDisplay for CliUpgradeableBuffer {}
2427impl fmt::Display for CliUpgradeableBuffer {
2428    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2429        writeln!(f)?;
2430        writeln_name_value(f, "Buffer Address:", &self.address)?;
2431        writeln_name_value(f, "Authority:", &self.authority)?;
2432        writeln_name_value(
2433            f,
2434            "Balance:",
2435            &build_balance_message(self.lamports, self.use_lamports_unit, true),
2436        )?;
2437        writeln_name_value(
2438            f,
2439            "Data Length:",
2440            &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2441        )?;
2442
2443        Ok(())
2444    }
2445}
2446
2447#[derive(Serialize, Deserialize)]
2448#[serde(rename_all = "camelCase")]
2449pub struct CliUpgradeableBuffers {
2450    pub buffers: Vec<CliUpgradeableBuffer>,
2451    #[serde(skip_serializing)]
2452    pub use_lamports_unit: bool,
2453}
2454impl QuietDisplay for CliUpgradeableBuffers {}
2455impl VerboseDisplay for CliUpgradeableBuffers {}
2456impl fmt::Display for CliUpgradeableBuffers {
2457    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2458        writeln!(f)?;
2459        writeln!(
2460            f,
2461            "{}",
2462            style(format!(
2463                "{:<44} | {:<44} | {}",
2464                "Buffer Address", "Authority", "Balance"
2465            ))
2466            .bold()
2467        )?;
2468        for buffer in self.buffers.iter() {
2469            writeln!(
2470                f,
2471                "{}",
2472                &format!(
2473                    "{:<44} | {:<44} | {}",
2474                    buffer.address,
2475                    buffer.authority,
2476                    build_balance_message(buffer.lamports, self.use_lamports_unit, true)
2477                )
2478            )?;
2479        }
2480        Ok(())
2481    }
2482}
2483
2484#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
2485#[serde(rename_all = "camelCase")]
2486pub struct CliAddressLookupTable {
2487    pub lookup_table_address: String,
2488    pub authority: Option<String>,
2489    pub deactivation_slot: u64,
2490    pub last_extended_slot: u64,
2491    pub addresses: Vec<String>,
2492}
2493impl QuietDisplay for CliAddressLookupTable {}
2494impl VerboseDisplay for CliAddressLookupTable {}
2495impl fmt::Display for CliAddressLookupTable {
2496    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2497        writeln!(f)?;
2498        writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
2499        if let Some(authority) = &self.authority {
2500            writeln_name_value(f, "Authority:", authority)?;
2501        } else {
2502            writeln_name_value(f, "Authority:", "None (frozen)")?;
2503        }
2504        if self.deactivation_slot == u64::MAX {
2505            writeln_name_value(f, "Deactivation Slot:", "None (still active)")?;
2506        } else {
2507            writeln_name_value(f, "Deactivation Slot:", &self.deactivation_slot.to_string())?;
2508        }
2509        if self.last_extended_slot == 0 {
2510            writeln_name_value(f, "Last Extended Slot:", "None (empty)")?;
2511        } else {
2512            writeln_name_value(
2513                f,
2514                "Last Extended Slot:",
2515                &self.last_extended_slot.to_string(),
2516            )?;
2517        }
2518        if self.addresses.is_empty() {
2519            writeln_name_value(f, "Address Table Entries:", "None (empty)")?;
2520        } else {
2521            writeln!(f, "{}", style("Address Table Entries:".to_string()).bold())?;
2522            writeln!(f)?;
2523            writeln!(
2524                f,
2525                "{}",
2526                style(format!("  {:<5}  {}", "Index", "Address")).bold()
2527            )?;
2528            for (index, address) in self.addresses.iter().enumerate() {
2529                writeln!(f, "  {index:<5}  {address}")?;
2530            }
2531        }
2532        Ok(())
2533    }
2534}
2535
2536#[derive(Serialize, Deserialize)]
2537#[serde(rename_all = "camelCase")]
2538pub struct CliAddressLookupTableCreated {
2539    pub lookup_table_address: String,
2540    pub signature: String,
2541}
2542impl QuietDisplay for CliAddressLookupTableCreated {}
2543impl VerboseDisplay for CliAddressLookupTableCreated {}
2544impl fmt::Display for CliAddressLookupTableCreated {
2545    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2546        writeln!(f)?;
2547        writeln_name_value(f, "Signature:", &self.signature)?;
2548        writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
2549        Ok(())
2550    }
2551}
2552
2553#[derive(Debug, Default)]
2554pub struct ReturnSignersConfig {
2555    pub dump_transaction_message: bool,
2556}
2557
2558pub fn return_signers(
2559    tx: &Transaction,
2560    output_format: &OutputFormat,
2561) -> Result<String, Box<dyn std::error::Error>> {
2562    return_signers_with_config(tx, output_format, &ReturnSignersConfig::default())
2563}
2564
2565pub fn return_signers_with_config(
2566    tx: &Transaction,
2567    output_format: &OutputFormat,
2568    config: &ReturnSignersConfig,
2569) -> Result<String, Box<dyn std::error::Error>> {
2570    let cli_command = return_signers_data(tx, config);
2571    Ok(output_format.formatted_string(&cli_command))
2572}
2573
2574pub fn return_signers_data(tx: &Transaction, config: &ReturnSignersConfig) -> CliSignOnlyData {
2575    let verify_results = tx.verify_with_results();
2576    let mut signers = Vec::new();
2577    let mut absent = Vec::new();
2578    let mut bad_sig = Vec::new();
2579    tx.signatures
2580        .iter()
2581        .zip(tx.message.account_keys.iter())
2582        .zip(verify_results)
2583        .for_each(|((sig, key), res)| {
2584            if res {
2585                signers.push(format!("{key}={sig}"))
2586            } else if *sig == Signature::default() {
2587                absent.push(key.to_string());
2588            } else {
2589                bad_sig.push(key.to_string());
2590            }
2591        });
2592    let message = if config.dump_transaction_message {
2593        let message_data = tx.message_data();
2594        Some(BASE64_STANDARD.encode(message_data))
2595    } else {
2596        None
2597    };
2598
2599    CliSignOnlyData {
2600        blockhash: tx.message.recent_blockhash.to_string(),
2601        message,
2602        signers,
2603        absent,
2604        bad_sig,
2605    }
2606}
2607
2608pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly {
2609    let object: Value = serde_json::from_str(reply).unwrap();
2610    let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
2611    let blockhash = blockhash_str.parse::<Hash>().unwrap();
2612    let mut present_signers: Vec<(Pubkey, Signature)> = Vec::new();
2613    let signer_strings = object.get("signers");
2614    if let Some(sig_strings) = signer_strings {
2615        present_signers = sig_strings
2616            .as_array()
2617            .unwrap()
2618            .iter()
2619            .map(|signer_string| {
2620                let mut signer = signer_string.as_str().unwrap().split('=');
2621                let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
2622                let sig = Signature::from_str(signer.next().unwrap()).unwrap();
2623                (key, sig)
2624            })
2625            .collect();
2626    }
2627    let mut absent_signers: Vec<Pubkey> = Vec::new();
2628    let signer_strings = object.get("absent");
2629    if let Some(sig_strings) = signer_strings {
2630        absent_signers = sig_strings
2631            .as_array()
2632            .unwrap()
2633            .iter()
2634            .map(|val| {
2635                let s = val.as_str().unwrap();
2636                Pubkey::from_str(s).unwrap()
2637            })
2638            .collect();
2639    }
2640    let mut bad_signers: Vec<Pubkey> = Vec::new();
2641    let signer_strings = object.get("badSig");
2642    if let Some(sig_strings) = signer_strings {
2643        bad_signers = sig_strings
2644            .as_array()
2645            .unwrap()
2646            .iter()
2647            .map(|val| {
2648                let s = val.as_str().unwrap();
2649                Pubkey::from_str(s).unwrap()
2650            })
2651            .collect();
2652    }
2653
2654    let message = object
2655        .get("message")
2656        .and_then(|o| o.as_str())
2657        .map(|m| m.to_string());
2658
2659    SignOnly {
2660        blockhash,
2661        message,
2662        present_signers,
2663        absent_signers,
2664        bad_signers,
2665    }
2666}
2667
2668#[derive(Debug, Serialize, Deserialize)]
2669#[serde(rename_all = "camelCase")]
2670pub enum CliSignatureVerificationStatus {
2671    None,
2672    Pass,
2673    Fail,
2674}
2675
2676impl CliSignatureVerificationStatus {
2677    pub fn verify_transaction(tx: &VersionedTransaction) -> Vec<Self> {
2678        tx.verify_with_results()
2679            .iter()
2680            .zip(&tx.signatures)
2681            .map(|(stat, sig)| match stat {
2682                true => CliSignatureVerificationStatus::Pass,
2683                false if sig == &Signature::default() => CliSignatureVerificationStatus::None,
2684                false => CliSignatureVerificationStatus::Fail,
2685            })
2686            .collect()
2687    }
2688}
2689
2690impl fmt::Display for CliSignatureVerificationStatus {
2691    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2692        match self {
2693            Self::None => write!(f, "none"),
2694            Self::Pass => write!(f, "pass"),
2695            Self::Fail => write!(f, "fail"),
2696        }
2697    }
2698}
2699
2700#[derive(Serialize, Deserialize)]
2701#[serde(rename_all = "camelCase")]
2702pub struct CliBlock {
2703    #[serde(flatten)]
2704    pub encoded_confirmed_block: EncodedConfirmedBlock,
2705    #[serde(skip_serializing)]
2706    pub slot: Slot,
2707}
2708
2709impl QuietDisplay for CliBlock {}
2710impl VerboseDisplay for CliBlock {}
2711
2712impl fmt::Display for CliBlock {
2713    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2714        writeln!(f, "Slot: {}", self.slot)?;
2715        writeln!(
2716            f,
2717            "Parent Slot: {}",
2718            self.encoded_confirmed_block.parent_slot
2719        )?;
2720        writeln!(f, "Blockhash: {}", self.encoded_confirmed_block.blockhash)?;
2721        writeln!(
2722            f,
2723            "Previous Blockhash: {}",
2724            self.encoded_confirmed_block.previous_blockhash
2725        )?;
2726        if let Some(block_time) = self.encoded_confirmed_block.block_time {
2727            writeln!(
2728                f,
2729                "Block Time: {:?}",
2730                Local.timestamp_opt(block_time, 0).unwrap()
2731            )?;
2732        }
2733        if let Some(block_height) = self.encoded_confirmed_block.block_height {
2734            writeln!(f, "Block Height: {block_height:?}")?;
2735        }
2736        if !self.encoded_confirmed_block.rewards.is_empty() {
2737            let mut rewards = self.encoded_confirmed_block.rewards.clone();
2738            rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
2739            let mut total_rewards = 0;
2740            writeln!(f, "Rewards:")?;
2741            writeln!(
2742                f,
2743                "  {:<44}  {:^15}  {:<15}  {:<20}  {:>14}  {:>10}",
2744                "Address", "Type", "Amount", "New Balance", "Percent Change", "Commission"
2745            )?;
2746            for reward in rewards {
2747                let sign = if reward.lamports < 0 { "-" } else { "" };
2748
2749                total_rewards += reward.lamports;
2750                #[allow(clippy::format_in_format_args)]
2751                writeln!(
2752                    f,
2753                    "  {:<44}  {:^15}  {:>15}  {}  {}",
2754                    reward.pubkey,
2755                    if let Some(reward_type) = reward.reward_type {
2756                        format!("{reward_type}")
2757                    } else {
2758                        "-".to_string()
2759                    },
2760                    format!(
2761                        "{}◎{:<14.9}",
2762                        sign,
2763                        lamports_to_sol(reward.lamports.unsigned_abs())
2764                    ),
2765                    if reward.post_balance == 0 {
2766                        "          -                 -".to_string()
2767                    } else {
2768                        format!(
2769                            "◎{:<19.9}  {:>13.9}%",
2770                            lamports_to_sol(reward.post_balance),
2771                            (reward.lamports.abs() as f64
2772                                / (reward.post_balance as f64 - reward.lamports as f64))
2773                                * 100.0
2774                        )
2775                    },
2776                    reward
2777                        .commission
2778                        .map(|commission| format!("{commission:>9}%"))
2779                        .unwrap_or_else(|| "    -".to_string())
2780                )?;
2781            }
2782
2783            let sign = if total_rewards < 0 { "-" } else { "" };
2784            writeln!(
2785                f,
2786                "Total Rewards: {}◎{:<12.9}",
2787                sign,
2788                lamports_to_sol(total_rewards.unsigned_abs())
2789            )?;
2790        }
2791        for (index, transaction_with_meta) in
2792            self.encoded_confirmed_block.transactions.iter().enumerate()
2793        {
2794            writeln!(f, "Transaction {index}:")?;
2795            writeln_transaction(
2796                f,
2797                &transaction_with_meta.transaction.decode().unwrap(),
2798                transaction_with_meta.meta.as_ref(),
2799                "  ",
2800                None,
2801                None,
2802            )?;
2803        }
2804        Ok(())
2805    }
2806}
2807
2808#[derive(Serialize, Deserialize)]
2809#[serde(rename_all = "camelCase")]
2810pub struct CliTransaction {
2811    pub transaction: EncodedTransaction,
2812    pub meta: Option<UiTransactionStatusMeta>,
2813    pub block_time: Option<UnixTimestamp>,
2814    #[serde(skip_serializing_if = "Option::is_none")]
2815    pub slot: Option<Slot>,
2816    #[serde(skip_serializing)]
2817    pub decoded_transaction: VersionedTransaction,
2818    #[serde(skip_serializing)]
2819    pub prefix: String,
2820    #[serde(skip_serializing_if = "Vec::is_empty")]
2821    pub sigverify_status: Vec<CliSignatureVerificationStatus>,
2822}
2823
2824impl QuietDisplay for CliTransaction {}
2825impl VerboseDisplay for CliTransaction {}
2826
2827impl fmt::Display for CliTransaction {
2828    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2829        writeln_transaction(
2830            f,
2831            &self.decoded_transaction,
2832            self.meta.as_ref(),
2833            &self.prefix,
2834            if !self.sigverify_status.is_empty() {
2835                Some(&self.sigverify_status)
2836            } else {
2837                None
2838            },
2839            self.block_time,
2840        )
2841    }
2842}
2843
2844#[derive(Serialize, Deserialize)]
2845#[serde(rename_all = "camelCase")]
2846pub struct CliTransactionConfirmation {
2847    pub confirmation_status: Option<TransactionConfirmationStatus>,
2848    #[serde(flatten, skip_serializing_if = "Option::is_none")]
2849    pub transaction: Option<CliTransaction>,
2850    #[serde(skip_serializing)]
2851    pub get_transaction_error: Option<String>,
2852    #[serde(skip_serializing_if = "Option::is_none")]
2853    pub err: Option<TransactionError>,
2854}
2855
2856impl QuietDisplay for CliTransactionConfirmation {}
2857impl VerboseDisplay for CliTransactionConfirmation {
2858    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
2859        if let Some(transaction) = &self.transaction {
2860            writeln!(
2861                w,
2862                "\nTransaction executed in slot {}:",
2863                transaction.slot.expect("slot should exist")
2864            )?;
2865            write!(w, "{transaction}")?;
2866        } else if let Some(confirmation_status) = &self.confirmation_status {
2867            if confirmation_status != &TransactionConfirmationStatus::Finalized {
2868                writeln!(w)?;
2869                writeln!(
2870                    w,
2871                    "Unable to get finalized transaction details: not yet finalized"
2872                )?;
2873            } else if let Some(err) = &self.get_transaction_error {
2874                writeln!(w)?;
2875                writeln!(w, "Unable to get finalized transaction details: {err}")?;
2876            }
2877        }
2878        writeln!(w)?;
2879        write!(w, "{self}")
2880    }
2881}
2882
2883impl fmt::Display for CliTransactionConfirmation {
2884    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2885        match &self.confirmation_status {
2886            None => write!(f, "Not found"),
2887            Some(confirmation_status) => {
2888                if let Some(err) = &self.err {
2889                    write!(f, "Transaction failed: {err}")
2890                } else {
2891                    write!(f, "{confirmation_status:?}")
2892                }
2893            }
2894        }
2895    }
2896}
2897
2898#[derive(Serialize, Deserialize)]
2899#[serde(rename_all = "camelCase")]
2900pub struct CliGossipNode {
2901    #[serde(skip_serializing_if = "Option::is_none")]
2902    pub ip_address: Option<String>,
2903    #[serde(skip_serializing_if = "Option::is_none")]
2904    pub identity_label: Option<String>,
2905    pub identity_pubkey: String,
2906    #[serde(skip_serializing_if = "Option::is_none")]
2907    pub gossip_port: Option<u16>,
2908    #[serde(skip_serializing_if = "Option::is_none")]
2909    pub tpu_port: Option<u16>,
2910    #[serde(skip_serializing_if = "Option::is_none")]
2911    pub rpc_host: Option<String>,
2912    #[serde(skip_serializing_if = "Option::is_none")]
2913    pub pubsub_host: Option<String>,
2914    #[serde(skip_serializing_if = "Option::is_none")]
2915    pub version: Option<String>,
2916    #[serde(skip_serializing_if = "Option::is_none")]
2917    pub feature_set: Option<u32>,
2918    #[serde(skip_serializing_if = "Option::is_none")]
2919    pub tpu_quic_port: Option<u16>,
2920}
2921
2922impl CliGossipNode {
2923    pub fn new(info: RpcContactInfo, labels: &HashMap<String, String>) -> Self {
2924        Self {
2925            ip_address: info.gossip.map(|addr| addr.ip().to_string()),
2926            identity_label: labels.get(&info.pubkey).cloned(),
2927            identity_pubkey: info.pubkey,
2928            gossip_port: info.gossip.map(|addr| addr.port()),
2929            tpu_port: info.tpu.map(|addr| addr.port()),
2930            rpc_host: info.rpc.map(|addr| addr.to_string()),
2931            pubsub_host: info.pubsub.map(|addr| addr.to_string()),
2932            version: info.version,
2933            feature_set: info.feature_set,
2934            tpu_quic_port: info.tpu_quic.map(|addr| addr.port()),
2935        }
2936    }
2937}
2938
2939fn unwrap_to_string_or_none<T>(option: Option<T>) -> String
2940where
2941    T: std::string::ToString,
2942{
2943    unwrap_to_string_or_default(option, "none")
2944}
2945
2946fn unwrap_to_string_or_default<T>(option: Option<T>, default: &str) -> String
2947where
2948    T: std::string::ToString,
2949{
2950    option
2951        .as_ref()
2952        .map(|v| v.to_string())
2953        .unwrap_or_else(|| default.to_string())
2954}
2955
2956impl fmt::Display for CliGossipNode {
2957    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2958        write!(
2959            f,
2960            "{:15} | {:44} | {:6} | {:5} | {:8} | {:21} | {:8}| {}",
2961            unwrap_to_string_or_none(self.ip_address.as_ref()),
2962            self.identity_label
2963                .as_ref()
2964                .unwrap_or(&self.identity_pubkey),
2965            unwrap_to_string_or_none(self.gossip_port.as_ref()),
2966            unwrap_to_string_or_none(self.tpu_port.as_ref()),
2967            unwrap_to_string_or_none(self.tpu_quic_port.as_ref()),
2968            unwrap_to_string_or_none(self.rpc_host.as_ref()),
2969            unwrap_to_string_or_default(self.version.as_ref(), "unknown"),
2970            unwrap_to_string_or_default(self.feature_set.as_ref(), "unknown"),
2971        )
2972    }
2973}
2974
2975impl QuietDisplay for CliGossipNode {}
2976impl VerboseDisplay for CliGossipNode {}
2977
2978#[derive(Serialize, Deserialize)]
2979pub struct CliGossipNodes(pub Vec<CliGossipNode>);
2980
2981impl fmt::Display for CliGossipNodes {
2982    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2983        writeln!(
2984            f,
2985            "IP Address      | Identity                                     \
2986             | Gossip | TPU   | TPU-QUIC | RPC Address           | Version | Feature Set\n\
2987             ----------------+----------------------------------------------+\
2988             --------+-------+----------+-----------------------+---------+----------------",
2989        )?;
2990        for node in self.0.iter() {
2991            writeln!(f, "{node}")?;
2992        }
2993        writeln!(f, "Nodes: {}", self.0.len())
2994    }
2995}
2996
2997impl QuietDisplay for CliGossipNodes {}
2998impl VerboseDisplay for CliGossipNodes {}
2999
3000#[derive(Serialize, Deserialize)]
3001#[serde(rename_all = "camelCase")]
3002pub struct CliPing {
3003    pub source_pubkey: String,
3004    #[serde(skip_serializing_if = "Option::is_none")]
3005    pub fixed_blockhash: Option<String>,
3006    #[serde(skip_serializing)]
3007    pub blockhash_from_cluster: bool,
3008    pub pings: Vec<CliPingData>,
3009    pub transaction_stats: CliPingTxStats,
3010    #[serde(skip_serializing_if = "Option::is_none")]
3011    pub confirmation_stats: Option<CliPingConfirmationStats>,
3012}
3013
3014impl fmt::Display for CliPing {
3015    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3016        writeln!(f)?;
3017        writeln_name_value(f, "Source Account:", &self.source_pubkey)?;
3018        if let Some(fixed_blockhash) = &self.fixed_blockhash {
3019            let blockhash_origin = if self.blockhash_from_cluster {
3020                "fetched from cluster"
3021            } else {
3022                "supplied from cli arguments"
3023            };
3024            writeln!(
3025                f,
3026                "Fixed blockhash is used: {fixed_blockhash} ({blockhash_origin})"
3027            )?;
3028        }
3029        writeln!(f)?;
3030        for ping in &self.pings {
3031            write!(f, "{ping}")?;
3032        }
3033        writeln!(f)?;
3034        writeln!(f, "--- transaction statistics ---")?;
3035        write!(f, "{}", self.transaction_stats)?;
3036        if let Some(confirmation_stats) = &self.confirmation_stats {
3037            write!(f, "{confirmation_stats}")?;
3038        }
3039        Ok(())
3040    }
3041}
3042
3043impl QuietDisplay for CliPing {}
3044impl VerboseDisplay for CliPing {}
3045
3046#[derive(Serialize, Deserialize)]
3047#[serde(rename_all = "camelCase")]
3048pub struct CliPingData {
3049    pub success: bool,
3050    #[serde(skip_serializing_if = "Option::is_none")]
3051    pub signature: Option<String>,
3052    #[serde(skip_serializing_if = "Option::is_none")]
3053    pub ms: Option<u64>,
3054    #[serde(skip_serializing_if = "Option::is_none")]
3055    pub error: Option<String>,
3056    #[serde(skip_serializing)]
3057    pub print_timestamp: bool,
3058    pub timestamp: String,
3059    pub sequence: u64,
3060    #[serde(skip_serializing_if = "Option::is_none")]
3061    pub lamports: Option<u64>,
3062}
3063impl fmt::Display for CliPingData {
3064    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3065        let (mark, msg) = if let Some(signature) = &self.signature {
3066            if self.success {
3067                (
3068                    CHECK_MARK,
3069                    format!(
3070                        "{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}",
3071                        self.lamports.unwrap(),
3072                        self.sequence,
3073                        self.ms.unwrap(),
3074                        signature
3075                    ),
3076                )
3077            } else if let Some(error) = &self.error {
3078                (
3079                    CROSS_MARK,
3080                    format!(
3081                        "Transaction failed:    seq={:<3} error={:?} signature={}",
3082                        self.sequence, error, signature
3083                    ),
3084                )
3085            } else {
3086                (
3087                    CROSS_MARK,
3088                    format!(
3089                        "Confirmation timeout:  seq={:<3}             signature={}",
3090                        self.sequence, signature
3091                    ),
3092                )
3093            }
3094        } else {
3095            (
3096                CROSS_MARK,
3097                format!(
3098                    "Submit failed:         seq={:<3} error={:?}",
3099                    self.sequence,
3100                    self.error.as_ref().unwrap(),
3101                ),
3102            )
3103        };
3104
3105        writeln!(
3106            f,
3107            "{}{}{}",
3108            if self.print_timestamp {
3109                &self.timestamp
3110            } else {
3111                ""
3112            },
3113            mark,
3114            msg
3115        )
3116    }
3117}
3118
3119impl QuietDisplay for CliPingData {}
3120impl VerboseDisplay for CliPingData {}
3121
3122#[derive(Serialize, Deserialize)]
3123#[serde(rename_all = "camelCase")]
3124pub struct CliPingTxStats {
3125    pub num_transactions: u32,
3126    pub num_transaction_confirmed: u32,
3127}
3128impl fmt::Display for CliPingTxStats {
3129    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3130        writeln!(
3131            f,
3132            "{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
3133            self.num_transactions,
3134            self.num_transaction_confirmed,
3135            (100.
3136                - f64::from(self.num_transaction_confirmed) / f64::from(self.num_transactions)
3137                    * 100.)
3138        )
3139    }
3140}
3141
3142impl QuietDisplay for CliPingTxStats {}
3143impl VerboseDisplay for CliPingTxStats {}
3144
3145#[derive(Serialize, Deserialize)]
3146#[serde(rename_all = "camelCase")]
3147pub struct CliPingConfirmationStats {
3148    pub min: f64,
3149    pub mean: f64,
3150    pub max: f64,
3151    pub std_dev: f64,
3152}
3153impl fmt::Display for CliPingConfirmationStats {
3154    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3155        writeln!(
3156            f,
3157            "confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
3158            self.min, self.mean, self.max, self.std_dev,
3159        )
3160    }
3161}
3162impl QuietDisplay for CliPingConfirmationStats {}
3163impl VerboseDisplay for CliPingConfirmationStats {}
3164
3165#[derive(Serialize, Deserialize, Debug)]
3166#[serde(rename_all = "camelCase")]
3167pub struct CliBalance {
3168    pub lamports: u64,
3169    #[serde(skip)]
3170    pub config: BuildBalanceMessageConfig,
3171}
3172
3173impl QuietDisplay for CliBalance {
3174    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3175        let config = BuildBalanceMessageConfig {
3176            show_unit: false,
3177            trim_trailing_zeros: true,
3178            ..self.config
3179        };
3180        let balance_message = build_balance_message_with_config(self.lamports, &config);
3181        write!(w, "{balance_message}")
3182    }
3183}
3184
3185impl VerboseDisplay for CliBalance {
3186    fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3187        let config = BuildBalanceMessageConfig {
3188            show_unit: true,
3189            trim_trailing_zeros: false,
3190            ..self.config
3191        };
3192        let balance_message = build_balance_message_with_config(self.lamports, &config);
3193        write!(w, "{balance_message}")
3194    }
3195}
3196
3197impl fmt::Display for CliBalance {
3198    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3199        let balance_message = build_balance_message_with_config(self.lamports, &self.config);
3200        write!(f, "{balance_message}")
3201    }
3202}
3203
3204#[derive(Serialize, Deserialize)]
3205#[serde(rename_all = "camelCase")]
3206pub struct CliFindProgramDerivedAddress {
3207    pub address: String,
3208    pub bump_seed: u8,
3209}
3210
3211impl QuietDisplay for CliFindProgramDerivedAddress {}
3212impl VerboseDisplay for CliFindProgramDerivedAddress {}
3213
3214impl fmt::Display for CliFindProgramDerivedAddress {
3215    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3216        write!(f, "{}", self.address)?;
3217        Ok(())
3218    }
3219}
3220
3221#[cfg(test)]
3222mod tests {
3223    use {
3224        super::*,
3225        clap::{App, Arg},
3226        solana_sdk::{
3227            message::Message,
3228            pubkey::Pubkey,
3229            signature::{keypair_from_seed, NullSigner, Signature, Signer, SignerError},
3230            system_instruction,
3231            transaction::Transaction,
3232        },
3233    };
3234
3235    #[test]
3236    fn test_return_signers() {
3237        struct BadSigner {
3238            pubkey: Pubkey,
3239        }
3240
3241        impl BadSigner {
3242            pub fn new(pubkey: Pubkey) -> Self {
3243                Self { pubkey }
3244            }
3245        }
3246
3247        impl Signer for BadSigner {
3248            fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
3249                Ok(self.pubkey)
3250            }
3251
3252            fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
3253                Ok(Signature::from([1u8; 64]))
3254            }
3255
3256            fn is_interactive(&self) -> bool {
3257                false
3258            }
3259        }
3260
3261        let present: Box<dyn Signer> = Box::new(keypair_from_seed(&[2u8; 32]).unwrap());
3262        let absent: Box<dyn Signer> = Box::new(NullSigner::new(&Pubkey::from([3u8; 32])));
3263        let bad: Box<dyn Signer> = Box::new(BadSigner::new(Pubkey::from([4u8; 32])));
3264        let to = Pubkey::from([5u8; 32]);
3265        let nonce = Pubkey::from([6u8; 32]);
3266        let from = present.pubkey();
3267        let fee_payer = absent.pubkey();
3268        let nonce_auth = bad.pubkey();
3269        let mut tx = Transaction::new_unsigned(Message::new_with_nonce(
3270            vec![system_instruction::transfer(&from, &to, 42)],
3271            Some(&fee_payer),
3272            &nonce,
3273            &nonce_auth,
3274        ));
3275
3276        let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()];
3277        let blockhash = Hash::new(&[7u8; 32]);
3278        tx.try_partial_sign(&signers, blockhash).unwrap();
3279        let res = return_signers(&tx, &OutputFormat::JsonCompact).unwrap();
3280        let sign_only = parse_sign_only_reply_string(&res);
3281        assert_eq!(sign_only.blockhash, blockhash);
3282        assert_eq!(sign_only.message, None);
3283        assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3284        assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3285        assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3286
3287        let res_data = return_signers_data(&tx, &ReturnSignersConfig::default());
3288        assert_eq!(
3289            res_data,
3290            CliSignOnlyData {
3291                blockhash: blockhash.to_string(),
3292                message: None,
3293                signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3294                absent: vec![absent.pubkey().to_string()],
3295                bad_sig: vec![bad.pubkey().to_string()],
3296            }
3297        );
3298
3299        let expected_msg = "AwECBwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgTl3Dqh9\
3300            F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\
3301            BAQEBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBgYGBgYGBgYGBgYGBgYGBgYG\
3302            BgYGBgYGBgYGBgYGBgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAan1RcZLFaO\
3303            4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH\
3304            BwcCBQMEBgIEBAAAAAUCAQMMAgAAACoAAAAAAAAA"
3305            .to_string();
3306        let config = ReturnSignersConfig {
3307            dump_transaction_message: true,
3308        };
3309        let res = return_signers_with_config(&tx, &OutputFormat::JsonCompact, &config).unwrap();
3310        let sign_only = parse_sign_only_reply_string(&res);
3311        assert_eq!(sign_only.blockhash, blockhash);
3312        assert_eq!(sign_only.message, Some(expected_msg.clone()));
3313        assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3314        assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3315        assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3316
3317        let res_data = return_signers_data(&tx, &config);
3318        assert_eq!(
3319            res_data,
3320            CliSignOnlyData {
3321                blockhash: blockhash.to_string(),
3322                message: Some(expected_msg),
3323                signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3324                absent: vec![absent.pubkey().to_string()],
3325                bad_sig: vec![bad.pubkey().to_string()],
3326            }
3327        );
3328    }
3329
3330    #[test]
3331    fn test_verbose_quiet_output_formats() {
3332        #[derive(Deserialize, Serialize)]
3333        struct FallbackToDisplay {}
3334        impl std::fmt::Display for FallbackToDisplay {
3335            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3336                write!(f, "display")
3337            }
3338        }
3339        impl QuietDisplay for FallbackToDisplay {}
3340        impl VerboseDisplay for FallbackToDisplay {}
3341
3342        let f = FallbackToDisplay {};
3343        assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3344        assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "display");
3345        assert_eq!(
3346            &OutputFormat::DisplayVerbose.formatted_string(&f),
3347            "display"
3348        );
3349
3350        #[derive(Deserialize, Serialize)]
3351        struct DiscreteVerbosityDisplay {}
3352        impl std::fmt::Display for DiscreteVerbosityDisplay {
3353            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3354                write!(f, "display")
3355            }
3356        }
3357        impl QuietDisplay for DiscreteVerbosityDisplay {
3358            fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3359                write!(w, "quiet")
3360            }
3361        }
3362        impl VerboseDisplay for DiscreteVerbosityDisplay {
3363            fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3364                write!(w, "verbose")
3365            }
3366        }
3367
3368        let f = DiscreteVerbosityDisplay {};
3369        assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3370        assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "quiet");
3371        assert_eq!(
3372            &OutputFormat::DisplayVerbose.formatted_string(&f),
3373            "verbose"
3374        );
3375    }
3376
3377    #[test]
3378    fn test_output_format_from_matches() {
3379        let app = App::new("test").arg(
3380            Arg::with_name("output_format")
3381                .long("output")
3382                .value_name("FORMAT")
3383                .global(true)
3384                .takes_value(true)
3385                .possible_values(&["json", "json-compact"])
3386                .help("Return information in specified output format"),
3387        );
3388        let matches = app
3389            .clone()
3390            .get_matches_from(vec!["test", "--output", "json"]);
3391        assert_eq!(
3392            OutputFormat::from_matches(&matches, "output_format", false),
3393            OutputFormat::Json
3394        );
3395        assert_eq!(
3396            OutputFormat::from_matches(&matches, "output_format", true),
3397            OutputFormat::Json
3398        );
3399
3400        let matches = app
3401            .clone()
3402            .get_matches_from(vec!["test", "--output", "json-compact"]);
3403        assert_eq!(
3404            OutputFormat::from_matches(&matches, "output_format", false),
3405            OutputFormat::JsonCompact
3406        );
3407        assert_eq!(
3408            OutputFormat::from_matches(&matches, "output_format", true),
3409            OutputFormat::JsonCompact
3410        );
3411
3412        let matches = app.clone().get_matches_from(vec!["test"]);
3413        assert_eq!(
3414            OutputFormat::from_matches(&matches, "output_format", false),
3415            OutputFormat::Display
3416        );
3417        assert_eq!(
3418            OutputFormat::from_matches(&matches, "output_format", true),
3419            OutputFormat::DisplayVerbose
3420        );
3421    }
3422
3423    #[test]
3424    fn test_format_vote_account() {
3425        let epoch_rewards = vec![
3426            CliEpochReward {
3427                percent_change: 11.0,
3428                post_balance: 100,
3429                commission: Some(1),
3430                effective_slot: 100,
3431                epoch: 1,
3432                amount: 10,
3433                block_time: 0,
3434                apr: Some(10.0),
3435            },
3436            CliEpochReward {
3437                percent_change: 11.0,
3438                post_balance: 100,
3439                commission: Some(1),
3440                effective_slot: 200,
3441                epoch: 2,
3442                amount: 12,
3443                block_time: 1_000_000,
3444                apr: Some(13.0),
3445            },
3446        ];
3447
3448        let mut c = CliVoteAccount {
3449            account_balance: 10000,
3450            validator_identity: Pubkey::default().to_string(),
3451            epoch_rewards: Some(epoch_rewards),
3452            recent_timestamp: BlockTimestamp::default(),
3453            ..CliVoteAccount::default()
3454        };
3455        let s = format!("{c}");
3456        assert_eq!(s, "Account Balance: 0.00001 SOL\nValidator Identity: 11111111111111111111111111111111\nVote Authority: None\nWithdraw Authority: \nCredits: 0\nCommission: 0%\nRoot Slot: ~\nRecent Timestamp: 1970-01-01T00:00:00Z from slot 0\nEpoch Rewards:\n  Epoch   Reward Slot  Time                        Amount              New Balance         Percent Change             APR  Commission\n  1       100          1970-01-01 00:00:00 UTC  ◎0.000000010        ◎0.000000100               11.000%          10.00%          1%\n  2       200          1970-01-12 13:46:40 UTC  ◎0.000000012        ◎0.000000100               11.000%          13.00%          1%\n");
3457        println!("{s}");
3458
3459        c.use_csv = true;
3460        let s = format!("{c}");
3461        assert_eq!(s, "Account Balance: 0.00001 SOL\nValidator Identity: 11111111111111111111111111111111\nVote Authority: None\nWithdraw Authority: \nCredits: 0\nCommission: 0%\nRoot Slot: ~\nRecent Timestamp: 1970-01-01T00:00:00Z from slot 0\nEpoch Rewards:\nEpoch,Reward Slot,Time,Amount,New Balance,Percent Change,APR,Commission\n1,100,1970-01-01 00:00:00 UTC,0.00000001,0.0000001,11%,10.00%,1%\n2,200,1970-01-12 13:46:40 UTC,0.000000012,0.0000001,11%,13.00%,1%\n");
3462        println!("{s}");
3463    }
3464}