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