Skip to main content

solana_cli_output/
cli_output.rs

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