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