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 message_data = tx.message_data();
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 .for_each(|(sig, key)| {
2981 if *sig == Signature::default() {
2982 absent.push(key.to_string());
2983 } else if sig.verify(key.as_ref(), &message_data) {
2984 signers.push(format!("{key}={sig}"));
2985 } else {
2986 bad_sig.push(key.to_string());
2987 }
2988 });
2989 let message = if config.dump_transaction_message {
2990 Some(BASE64_STANDARD.encode(message_data))
2991 } else {
2992 None
2993 };
2994
2995 CliSignOnlyData {
2996 blockhash: tx.message.recent_blockhash.to_string(),
2997 message,
2998 signers,
2999 absent,
3000 bad_sig,
3001 }
3002}
3003
3004pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly {
3005 let object: Value = serde_json::from_str(reply).unwrap();
3006 let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
3007 let blockhash = blockhash_str.parse::<Hash>().unwrap();
3008 let mut present_signers: Vec<(Pubkey, Signature)> = Vec::new();
3009 let signer_strings = object.get("signers");
3010 if let Some(sig_strings) = signer_strings {
3011 present_signers = sig_strings
3012 .as_array()
3013 .unwrap()
3014 .iter()
3015 .map(|signer_string| {
3016 let mut signer = signer_string.as_str().unwrap().split('=');
3017 let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
3018 let sig = Signature::from_str(signer.next().unwrap()).unwrap();
3019 (key, sig)
3020 })
3021 .collect();
3022 }
3023 let mut absent_signers: Vec<Pubkey> = Vec::new();
3024 let signer_strings = object.get("absent");
3025 if let Some(sig_strings) = signer_strings {
3026 absent_signers = sig_strings
3027 .as_array()
3028 .unwrap()
3029 .iter()
3030 .map(|val| {
3031 let s = val.as_str().unwrap();
3032 Pubkey::from_str(s).unwrap()
3033 })
3034 .collect();
3035 }
3036 let mut bad_signers: Vec<Pubkey> = Vec::new();
3037 let signer_strings = object.get("badSig");
3038 if let Some(sig_strings) = signer_strings {
3039 bad_signers = sig_strings
3040 .as_array()
3041 .unwrap()
3042 .iter()
3043 .map(|val| {
3044 let s = val.as_str().unwrap();
3045 Pubkey::from_str(s).unwrap()
3046 })
3047 .collect();
3048 }
3049
3050 let message = object
3051 .get("message")
3052 .and_then(|o| o.as_str())
3053 .map(|m| m.to_string());
3054
3055 SignOnly {
3056 blockhash,
3057 message,
3058 present_signers,
3059 absent_signers,
3060 bad_signers,
3061 }
3062}
3063
3064#[derive(Debug, Serialize, Deserialize)]
3065#[serde(rename_all = "camelCase")]
3066pub enum CliSignatureVerificationStatus {
3067 None,
3068 Pass,
3069 Fail,
3070}
3071
3072impl CliSignatureVerificationStatus {
3073 pub fn verify_transaction(tx: &VersionedTransaction) -> Vec<Self> {
3074 let message_bytes = tx.message.serialize();
3075 tx.message
3076 .static_account_keys()
3077 .iter()
3078 .zip(&tx.signatures)
3079 .map(|(key, sig)| {
3080 if sig == &Signature::default() {
3081 CliSignatureVerificationStatus::None
3082 } else if sig.verify(key.as_ref(), &message_bytes) {
3083 CliSignatureVerificationStatus::Pass
3084 } else {
3085 CliSignatureVerificationStatus::Fail
3086 }
3087 })
3088 .collect()
3089 }
3090}
3091
3092impl fmt::Display for CliSignatureVerificationStatus {
3093 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3094 match self {
3095 Self::None => write!(f, "none"),
3096 Self::Pass => write!(f, "pass"),
3097 Self::Fail => write!(f, "fail"),
3098 }
3099 }
3100}
3101
3102#[derive(Serialize, Deserialize)]
3103#[serde(rename_all = "camelCase")]
3104pub struct CliBlock {
3105 #[serde(flatten)]
3106 pub encoded_confirmed_block: EncodedConfirmedBlock,
3107 #[serde(skip_serializing)]
3108 pub slot: Slot,
3109}
3110
3111impl CliBlock {
3112 pub fn display_block_meta(
3113 f: &mut fmt::Formatter,
3114 slot: Slot,
3115 parent_slot: Slot,
3116 blockhash: &str,
3117 previous_blockhash: &str,
3118 block_time: Option<i64>,
3119 block_height: Option<u64>,
3120 rewards: &Rewards,
3121 ) -> fmt::Result {
3122 writeln!(f, "Slot: {slot}")?;
3123 writeln!(f, "Parent Slot: {parent_slot}")?;
3124 writeln!(f, "Blockhash: {blockhash}")?;
3125 writeln!(f, "Previous Blockhash: {previous_blockhash}")?;
3126 if let Some(block_time) = block_time {
3127 writeln!(
3128 f,
3129 "Block Time: {:?}",
3130 Local.timestamp_opt(block_time, 0).unwrap()
3131 )?;
3132 }
3133 if let Some(block_height) = block_height {
3134 writeln!(f, "Block Height: {block_height:?}")?;
3135 }
3136 if !rewards.is_empty() {
3137 let mut rewards = rewards.clone();
3138 rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
3139 let mut total_rewards = 0;
3140 writeln!(f, "Rewards:")?;
3141 writeln!(
3142 f,
3143 " {:<44} {:^15} {:<15} {:<20} {:>14} {:>10}",
3144 "Address", "Type", "Amount", "New Balance", "Percent Change", "Commission"
3145 )?;
3146 for reward in rewards {
3147 let sign = if reward.lamports < 0 { "-" } else { "" };
3148
3149 total_rewards += reward.lamports;
3150 #[allow(clippy::format_in_format_args)]
3151 writeln!(
3152 f,
3153 " {:<44} {:^15} {:>15} {} {}",
3154 reward.pubkey,
3155 if let Some(reward_type) = reward.reward_type {
3156 format!("{reward_type}")
3157 } else {
3158 "-".to_string()
3159 },
3160 format!(
3161 "{}◎{:<14.9}",
3162 sign,
3163 build_balance_message(reward.lamports.unsigned_abs(), false, false)
3164 ),
3165 if reward.post_balance == 0 {
3166 " - -".to_string()
3167 } else {
3168 format!(
3169 "◎{:<19.9} {:>13.9}%",
3170 build_balance_message(reward.post_balance, false, false),
3171 (reward.lamports.abs() as f64
3172 / (reward.post_balance as f64 - reward.lamports as f64))
3173 * 100.0
3174 )
3175 },
3176 reward
3177 .commission_bps
3178 .map(|bps| format!("{:>6}.{:02}%", bps / 100, bps % 100))
3179 .or_else(|| reward.commission.map(|c| format!("{c:>9}%")))
3180 .unwrap_or_else(|| " -".to_string())
3181 )?;
3182 }
3183
3184 let sign = if total_rewards < 0 { "-" } else { "" };
3185 writeln!(
3186 f,
3187 "Total Rewards: {}◎{:<12.9}",
3188 sign,
3189 build_balance_message(total_rewards.unsigned_abs(), false, false)
3190 )?;
3191 }
3192 Ok(())
3193 }
3194}
3195
3196impl QuietDisplay for CliBlock {}
3197impl VerboseDisplay for CliBlock {}
3198
3199impl fmt::Display for CliBlock {
3200 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3201 Self::display_block_meta(
3202 f,
3203 self.slot,
3204 self.encoded_confirmed_block.parent_slot,
3205 &self.encoded_confirmed_block.blockhash,
3206 &self.encoded_confirmed_block.previous_blockhash,
3207 self.encoded_confirmed_block.block_time,
3208 self.encoded_confirmed_block.block_height,
3209 &self.encoded_confirmed_block.rewards,
3210 )?;
3211
3212 for (index, transaction_with_meta) in
3213 self.encoded_confirmed_block.transactions.iter().enumerate()
3214 {
3215 writeln!(f, "Transaction {index}:")?;
3216 writeln_transaction(
3217 f,
3218 &transaction_with_meta.transaction.decode().unwrap(),
3219 transaction_with_meta.meta.as_ref(),
3220 " ",
3221 None,
3222 None,
3223 )?;
3224 }
3225 Ok(())
3226 }
3227}
3228
3229#[derive(Serialize, Deserialize)]
3230#[serde(rename_all = "camelCase")]
3231pub struct CliTransaction {
3232 pub transaction: EncodedTransaction,
3233 pub meta: Option<UiTransactionStatusMeta>,
3234 pub block_time: Option<UnixTimestamp>,
3235 #[serde(skip_serializing_if = "Option::is_none")]
3236 pub slot: Option<Slot>,
3237 #[serde(skip_serializing)]
3238 pub decoded_transaction: VersionedTransaction,
3239 #[serde(skip_serializing)]
3240 pub prefix: String,
3241 #[serde(skip_serializing_if = "Vec::is_empty")]
3242 pub sigverify_status: Vec<CliSignatureVerificationStatus>,
3243}
3244
3245impl QuietDisplay for CliTransaction {}
3246impl VerboseDisplay for CliTransaction {}
3247
3248impl fmt::Display for CliTransaction {
3249 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3250 writeln_transaction(
3251 f,
3252 &self.decoded_transaction,
3253 self.meta.as_ref(),
3254 &self.prefix,
3255 if !self.sigverify_status.is_empty() {
3256 Some(&self.sigverify_status)
3257 } else {
3258 None
3259 },
3260 self.block_time,
3261 )
3262 }
3263}
3264
3265#[derive(Serialize, Deserialize)]
3266#[serde(rename_all = "camelCase")]
3267pub struct CliTransactionConfirmation {
3268 pub confirmation_status: Option<TransactionConfirmationStatus>,
3269 #[serde(flatten, skip_serializing_if = "Option::is_none")]
3270 pub transaction: Option<CliTransaction>,
3271 #[serde(skip_serializing)]
3272 pub get_transaction_error: Option<String>,
3273 #[serde(skip_serializing_if = "Option::is_none")]
3274 pub err: Option<UiTransactionError>,
3275}
3276
3277impl QuietDisplay for CliTransactionConfirmation {}
3278impl VerboseDisplay for CliTransactionConfirmation {
3279 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3280 if let Some(transaction) = &self.transaction {
3281 writeln!(
3282 w,
3283 "\nTransaction executed in slot {}:",
3284 transaction.slot.expect("slot should exist")
3285 )?;
3286 write!(w, "{transaction}")?;
3287 } else if let Some(confirmation_status) = &self.confirmation_status {
3288 if confirmation_status != &TransactionConfirmationStatus::Finalized {
3289 writeln!(w)?;
3290 writeln!(
3291 w,
3292 "Unable to get finalized transaction details: not yet finalized"
3293 )?;
3294 } else if let Some(err) = &self.get_transaction_error {
3295 writeln!(w)?;
3296 writeln!(w, "Unable to get finalized transaction details: {err}")?;
3297 }
3298 }
3299 writeln!(w)?;
3300 write!(w, "{self}")
3301 }
3302}
3303
3304impl fmt::Display for CliTransactionConfirmation {
3305 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3306 match &self.confirmation_status {
3307 None => write!(f, "Not found"),
3308 Some(confirmation_status) => {
3309 if let Some(err) = &self.err {
3310 write!(f, "Transaction failed: {err}")
3311 } else {
3312 write!(f, "{confirmation_status:?}")
3313 }
3314 }
3315 }
3316 }
3317}
3318
3319#[derive(Serialize, Deserialize)]
3320#[serde(rename_all = "camelCase")]
3321pub struct CliGossipNode {
3322 #[serde(skip_serializing_if = "Option::is_none")]
3323 pub ip_address: Option<String>,
3324 #[serde(skip_serializing_if = "Option::is_none")]
3325 pub identity_label: Option<String>,
3326 pub identity_pubkey: String,
3327 #[serde(skip_serializing_if = "Option::is_none")]
3328 pub gossip_port: Option<u16>,
3329 #[serde(skip_serializing_if = "Option::is_none")]
3330 pub tpu_port: Option<u16>,
3331 #[serde(skip_serializing_if = "Option::is_none")]
3332 pub rpc_host: Option<String>,
3333 #[serde(skip_serializing_if = "Option::is_none")]
3334 pub pubsub_host: Option<String>,
3335 #[serde(skip_serializing_if = "Option::is_none")]
3336 pub version: Option<String>,
3337 #[serde(skip_serializing_if = "Option::is_none")]
3338 pub feature_set: Option<u32>,
3339 #[serde(skip_serializing_if = "Option::is_none")]
3340 pub tpu_quic_port: Option<u16>,
3341}
3342
3343impl CliGossipNode {
3344 pub fn new(info: RpcContactInfo, labels: &HashMap<String, String>) -> Self {
3345 Self {
3346 ip_address: info.gossip.map(|addr| addr.ip().to_string()),
3347 identity_label: labels.get(&info.pubkey).cloned(),
3348 identity_pubkey: info.pubkey,
3349 gossip_port: info.gossip.map(|addr| addr.port()),
3350 tpu_port: info.tpu.map(|addr| addr.port()),
3351 rpc_host: info.rpc.map(|addr| addr.to_string()),
3352 pubsub_host: info.pubsub.map(|addr| addr.to_string()),
3353 version: info.version,
3354 feature_set: info.feature_set,
3355 tpu_quic_port: info.tpu_quic.map(|addr| addr.port()),
3356 }
3357 }
3358}
3359
3360fn unwrap_to_string_or_none<T>(option: Option<T>) -> String
3361where
3362 T: std::string::ToString,
3363{
3364 unwrap_to_string_or_default(option, "none")
3365}
3366
3367fn unwrap_to_string_or_default<T>(option: Option<T>, default: &str) -> String
3368where
3369 T: std::string::ToString,
3370{
3371 option
3372 .as_ref()
3373 .map(|v| v.to_string())
3374 .unwrap_or_else(|| default.to_string())
3375}
3376
3377impl fmt::Display for CliGossipNode {
3378 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3379 write!(
3380 f,
3381 "{:15} | {:44} | {:6} | {:5} | {:8} | {:21} | {:8}| {}",
3382 unwrap_to_string_or_none(self.ip_address.as_ref()),
3383 self.identity_label
3384 .as_ref()
3385 .unwrap_or(&self.identity_pubkey),
3386 unwrap_to_string_or_none(self.gossip_port.as_ref()),
3387 unwrap_to_string_or_none(self.tpu_port.as_ref()),
3388 unwrap_to_string_or_none(self.tpu_quic_port.as_ref()),
3389 unwrap_to_string_or_none(self.rpc_host.as_ref()),
3390 unwrap_to_string_or_default(self.version.as_ref(), "unknown"),
3391 unwrap_to_string_or_default(self.feature_set.as_ref(), "unknown"),
3392 )
3393 }
3394}
3395
3396impl QuietDisplay for CliGossipNode {}
3397impl VerboseDisplay for CliGossipNode {}
3398
3399#[derive(Serialize, Deserialize)]
3400pub struct CliGossipNodes(pub Vec<CliGossipNode>);
3401
3402impl fmt::Display for CliGossipNodes {
3403 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3404 writeln!(
3405 f,
3406 "IP Address | Identity \
3407 | Gossip | TPU | TPU-QUIC | RPC Address | Version | Feature Set\n\
3408 ----------------+----------------------------------------------+\
3409 --------+-------+----------+-----------------------+---------+----------------",
3410 )?;
3411 for node in self.0.iter() {
3412 writeln!(f, "{node}")?;
3413 }
3414 writeln!(f, "Nodes: {}", self.0.len())
3415 }
3416}
3417
3418impl QuietDisplay for CliGossipNodes {}
3419impl VerboseDisplay for CliGossipNodes {}
3420
3421#[derive(Serialize, Deserialize)]
3422#[serde(rename_all = "camelCase")]
3423pub struct CliPing {
3424 pub source_pubkey: String,
3425 #[serde(skip_serializing_if = "Option::is_none")]
3426 pub fixed_blockhash: Option<String>,
3427 #[serde(skip_serializing)]
3428 pub blockhash_from_cluster: bool,
3429 pub pings: Vec<CliPingData>,
3430 pub transaction_stats: CliPingTxStats,
3431 #[serde(skip_serializing_if = "Option::is_none")]
3432 pub confirmation_stats: Option<CliPingConfirmationStats>,
3433}
3434
3435impl fmt::Display for CliPing {
3436 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3437 writeln!(f)?;
3438 writeln_name_value(f, "Source Account:", &self.source_pubkey)?;
3439 if let Some(fixed_blockhash) = &self.fixed_blockhash {
3440 let blockhash_origin = if self.blockhash_from_cluster {
3441 "fetched from cluster"
3442 } else {
3443 "supplied from cli arguments"
3444 };
3445 writeln!(
3446 f,
3447 "Fixed blockhash is used: {fixed_blockhash} ({blockhash_origin})"
3448 )?;
3449 }
3450 writeln!(f)?;
3451 for ping in &self.pings {
3452 write!(f, "{ping}")?;
3453 }
3454 writeln!(f)?;
3455 writeln!(f, "--- transaction statistics ---")?;
3456 write!(f, "{}", self.transaction_stats)?;
3457 if let Some(confirmation_stats) = &self.confirmation_stats {
3458 write!(f, "{confirmation_stats}")?;
3459 }
3460 Ok(())
3461 }
3462}
3463
3464impl QuietDisplay for CliPing {}
3465impl VerboseDisplay for CliPing {}
3466
3467#[derive(Serialize, Deserialize)]
3468#[serde(rename_all = "camelCase")]
3469pub struct CliPingData {
3470 pub success: bool,
3471 #[serde(skip_serializing_if = "Option::is_none")]
3472 pub signature: Option<String>,
3473 #[serde(skip_serializing_if = "Option::is_none")]
3474 pub ms: Option<u64>,
3475 #[serde(skip_serializing_if = "Option::is_none")]
3476 pub error: Option<String>,
3477 #[serde(skip_serializing)]
3478 pub print_timestamp: bool,
3479 pub timestamp: String,
3480 pub sequence: u64,
3481 #[serde(skip_serializing_if = "Option::is_none")]
3482 pub lamports: Option<u64>,
3483}
3484impl fmt::Display for CliPingData {
3485 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3486 let (mark, msg) = if let Some(signature) = &self.signature {
3487 if self.success {
3488 (
3489 CHECK_MARK,
3490 format!(
3491 "{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}",
3492 self.lamports.unwrap(),
3493 self.sequence,
3494 self.ms.unwrap(),
3495 signature
3496 ),
3497 )
3498 } else if let Some(error) = &self.error {
3499 (
3500 CROSS_MARK,
3501 format!(
3502 "Transaction failed: seq={:<3} error={:?} signature={}",
3503 self.sequence, error, signature
3504 ),
3505 )
3506 } else {
3507 (
3508 CROSS_MARK,
3509 format!(
3510 "Confirmation timeout: seq={:<3} signature={}",
3511 self.sequence, signature
3512 ),
3513 )
3514 }
3515 } else {
3516 (
3517 CROSS_MARK,
3518 format!(
3519 "Submit failed: seq={:<3} error={:?}",
3520 self.sequence,
3521 self.error.as_ref().unwrap(),
3522 ),
3523 )
3524 };
3525
3526 writeln!(
3527 f,
3528 "{}{}{}",
3529 if self.print_timestamp {
3530 &self.timestamp
3531 } else {
3532 ""
3533 },
3534 mark,
3535 msg
3536 )
3537 }
3538}
3539
3540impl QuietDisplay for CliPingData {}
3541impl VerboseDisplay for CliPingData {}
3542
3543#[derive(Serialize, Deserialize)]
3544#[serde(rename_all = "camelCase")]
3545pub struct CliPingTxStats {
3546 pub num_transactions: u32,
3547 pub num_transaction_confirmed: u32,
3548}
3549impl fmt::Display for CliPingTxStats {
3550 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3551 writeln!(
3552 f,
3553 "{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
3554 self.num_transactions,
3555 self.num_transaction_confirmed,
3556 (100.
3557 - f64::from(self.num_transaction_confirmed) / f64::from(self.num_transactions)
3558 * 100.)
3559 )
3560 }
3561}
3562
3563impl QuietDisplay for CliPingTxStats {}
3564impl VerboseDisplay for CliPingTxStats {}
3565
3566#[derive(Serialize, Deserialize)]
3567#[serde(rename_all = "camelCase")]
3568pub struct CliPingConfirmationStats {
3569 pub min: f64,
3570 pub mean: f64,
3571 pub max: f64,
3572 pub std_dev: f64,
3573}
3574impl fmt::Display for CliPingConfirmationStats {
3575 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3576 writeln!(
3577 f,
3578 "confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
3579 self.min, self.mean, self.max, self.std_dev,
3580 )
3581 }
3582}
3583impl QuietDisplay for CliPingConfirmationStats {}
3584impl VerboseDisplay for CliPingConfirmationStats {}
3585
3586#[derive(Serialize, Deserialize, Debug)]
3587#[serde(rename_all = "camelCase")]
3588pub struct CliBalance {
3589 pub lamports: u64,
3590 #[serde(skip)]
3591 pub config: BuildBalanceMessageConfig,
3592}
3593
3594impl QuietDisplay for CliBalance {
3595 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3596 let config = BuildBalanceMessageConfig {
3597 show_unit: false,
3598 trim_trailing_zeros: true,
3599 ..self.config
3600 };
3601 let balance_message = build_balance_message_with_config(self.lamports, &config);
3602 write!(w, "{balance_message}")
3603 }
3604}
3605
3606impl VerboseDisplay for CliBalance {
3607 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3608 let config = BuildBalanceMessageConfig {
3609 show_unit: true,
3610 trim_trailing_zeros: false,
3611 ..self.config
3612 };
3613 let balance_message = build_balance_message_with_config(self.lamports, &config);
3614 write!(w, "{balance_message}")
3615 }
3616}
3617
3618impl fmt::Display for CliBalance {
3619 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3620 let balance_message = build_balance_message_with_config(self.lamports, &self.config);
3621 write!(f, "{balance_message}")
3622 }
3623}
3624
3625#[derive(Serialize, Deserialize)]
3626#[serde(rename_all = "camelCase")]
3627pub struct CliFindProgramDerivedAddress {
3628 pub address: String,
3629 pub seeds: Vec<Vec<u8>>,
3630 pub bump_seed: u8,
3631}
3632
3633impl QuietDisplay for CliFindProgramDerivedAddress {}
3634impl VerboseDisplay for CliFindProgramDerivedAddress {
3635 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3636 writeln!(w, "Seeds: {:?}", self.seeds)?;
3637 writeln!(w, "Bump: {}", self.bump_seed)?;
3638 write!(w, "{}", self.address)?;
3639 Ok(())
3640 }
3641}
3642
3643impl fmt::Display for CliFindProgramDerivedAddress {
3644 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3645 write!(f, "{}", self.address)?;
3646 Ok(())
3647 }
3648}
3649
3650#[cfg(test)]
3651mod tests {
3652 use {
3653 super::*,
3654 clap::{App, Arg},
3655 solana_keypair::keypair_from_seed,
3656 solana_message::Message,
3657 solana_pubkey::Pubkey,
3658 solana_signature::Signature,
3659 solana_signer::{Signer, SignerError, null_signer::NullSigner},
3660 solana_system_interface::instruction::transfer,
3661 solana_transaction::Transaction,
3662 };
3663
3664 #[test]
3665 fn test_return_signers() {
3666 struct BadSigner {
3667 pubkey: Pubkey,
3668 }
3669
3670 impl BadSigner {
3671 pub fn new(pubkey: Pubkey) -> Self {
3672 Self { pubkey }
3673 }
3674 }
3675
3676 impl Signer for BadSigner {
3677 fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
3678 Ok(self.pubkey)
3679 }
3680
3681 fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
3682 Ok(Signature::from([1u8; 64]))
3683 }
3684
3685 fn is_interactive(&self) -> bool {
3686 false
3687 }
3688 }
3689
3690 let present: Box<dyn Signer> = Box::new(keypair_from_seed(&[2u8; 32]).unwrap());
3691 let absent: Box<dyn Signer> = Box::new(NullSigner::new(&Pubkey::from([3u8; 32])));
3692 let bad: Box<dyn Signer> = Box::new(BadSigner::new(Pubkey::from([4u8; 32])));
3693 let to = Pubkey::from([5u8; 32]);
3694 let nonce = Pubkey::from([6u8; 32]);
3695 let from = present.pubkey();
3696 let fee_payer = absent.pubkey();
3697 let nonce_auth = bad.pubkey();
3698 let mut tx = Transaction::new_unsigned(Message::new_with_nonce(
3699 vec![transfer(&from, &to, 42)],
3700 Some(&fee_payer),
3701 &nonce,
3702 &nonce_auth,
3703 ));
3704
3705 let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()];
3706 let blockhash = Hash::new_from_array([7u8; 32]);
3707 tx.try_partial_sign(&signers, blockhash).unwrap();
3708 let res = return_signers(&tx, &OutputFormat::JsonCompact).unwrap();
3709 let sign_only = parse_sign_only_reply_string(&res);
3710 assert_eq!(sign_only.blockhash, blockhash);
3711 assert_eq!(sign_only.message, None);
3712 assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3713 assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3714 assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3715
3716 let res_data = return_signers_data(&tx, &ReturnSignersConfig::default());
3717 assert_eq!(
3718 res_data,
3719 CliSignOnlyData {
3720 blockhash: blockhash.to_string(),
3721 message: None,
3722 signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3723 absent: vec![absent.pubkey().to_string()],
3724 bad_sig: vec![bad.pubkey().to_string()],
3725 }
3726 );
3727
3728 let expected_msg = "AwECBwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgTl3Dqh9\
3729 F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\
3730 BAQEBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBgYGBgYGBgYGBgYGBgYGBgYG\
3731 BgYGBgYGBgYGBgYGBgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAan1RcZLFaO\
3732 4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH\
3733 BwcCBQMEBgIEBAAAAAUCAQMMAgAAACoAAAAAAAAA"
3734 .to_string();
3735 let config = ReturnSignersConfig {
3736 dump_transaction_message: true,
3737 };
3738 let res = return_signers_with_config(&tx, &OutputFormat::JsonCompact, &config).unwrap();
3739 let sign_only = parse_sign_only_reply_string(&res);
3740 assert_eq!(sign_only.blockhash, blockhash);
3741 assert_eq!(sign_only.message, Some(expected_msg.clone()));
3742 assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3743 assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3744 assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3745
3746 let res_data = return_signers_data(&tx, &config);
3747 assert_eq!(
3748 res_data,
3749 CliSignOnlyData {
3750 blockhash: blockhash.to_string(),
3751 message: Some(expected_msg),
3752 signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3753 absent: vec![absent.pubkey().to_string()],
3754 bad_sig: vec![bad.pubkey().to_string()],
3755 }
3756 );
3757 }
3758
3759 #[test]
3760 fn test_verbose_quiet_output_formats() {
3761 #[derive(Deserialize, Serialize)]
3762 struct FallbackToDisplay {}
3763 impl std::fmt::Display for FallbackToDisplay {
3764 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3765 write!(f, "display")
3766 }
3767 }
3768 impl QuietDisplay for FallbackToDisplay {}
3769 impl VerboseDisplay for FallbackToDisplay {}
3770
3771 let f = FallbackToDisplay {};
3772 assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3773 assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "display");
3774 assert_eq!(
3775 &OutputFormat::DisplayVerbose.formatted_string(&f),
3776 "display"
3777 );
3778
3779 #[derive(Deserialize, Serialize)]
3780 struct DiscreteVerbosityDisplay {}
3781 impl std::fmt::Display for DiscreteVerbosityDisplay {
3782 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3783 write!(f, "display")
3784 }
3785 }
3786 impl QuietDisplay for DiscreteVerbosityDisplay {
3787 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3788 write!(w, "quiet")
3789 }
3790 }
3791 impl VerboseDisplay for DiscreteVerbosityDisplay {
3792 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3793 write!(w, "verbose")
3794 }
3795 }
3796
3797 let f = DiscreteVerbosityDisplay {};
3798 assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3799 assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "quiet");
3800 assert_eq!(
3801 &OutputFormat::DisplayVerbose.formatted_string(&f),
3802 "verbose"
3803 );
3804 }
3805
3806 #[test]
3807 fn test_output_format_from_matches() {
3808 let app = App::new("test").arg(
3809 Arg::with_name("output_format")
3810 .long("output")
3811 .value_name("FORMAT")
3812 .global(true)
3813 .takes_value(true)
3814 .possible_values(&["json", "json-compact"])
3815 .help("Return information in specified output format"),
3816 );
3817 let matches = app
3818 .clone()
3819 .get_matches_from(vec!["test", "--output", "json"]);
3820 assert_eq!(
3821 OutputFormat::from_matches(&matches, "output_format", false),
3822 OutputFormat::Json
3823 );
3824 assert_eq!(
3825 OutputFormat::from_matches(&matches, "output_format", true),
3826 OutputFormat::Json
3827 );
3828
3829 let matches = app
3830 .clone()
3831 .get_matches_from(vec!["test", "--output", "json-compact"]);
3832 assert_eq!(
3833 OutputFormat::from_matches(&matches, "output_format", false),
3834 OutputFormat::JsonCompact
3835 );
3836 assert_eq!(
3837 OutputFormat::from_matches(&matches, "output_format", true),
3838 OutputFormat::JsonCompact
3839 );
3840
3841 let matches = app.clone().get_matches_from(vec!["test"]);
3842 assert_eq!(
3843 OutputFormat::from_matches(&matches, "output_format", false),
3844 OutputFormat::Display
3845 );
3846 assert_eq!(
3847 OutputFormat::from_matches(&matches, "output_format", true),
3848 OutputFormat::DisplayVerbose
3849 );
3850 }
3851
3852 #[test]
3853 fn test_format_vote_account() {
3854 let epoch_rewards = vec![
3855 CliEpochReward {
3856 percent_change: 11.0,
3857 post_balance: 100,
3858 commission: Some(1),
3859 effective_slot: 100,
3860 epoch: 1,
3861 amount: 10,
3862 block_time: 0,
3863 apr: Some(10.0),
3864 },
3865 CliEpochReward {
3866 percent_change: 11.0,
3867 post_balance: 100,
3868 commission: Some(1),
3869 effective_slot: 200,
3870 epoch: 2,
3871 amount: 12,
3872 block_time: 1_000_000,
3873 apr: Some(13.0),
3874 },
3875 ];
3876
3877 let mut c = CliVoteAccount {
3878 account_balance: 10000,
3879 validator_identity: Pubkey::default().to_string(),
3880 epoch_rewards: Some(epoch_rewards),
3881 recent_timestamp: BlockTimestamp::default(),
3882 ..CliVoteAccount::default()
3883 };
3884 #[rustfmt::skip]
3885 let expected_output_common =
3886 "Account Balance: 0.00001 SOL\n\
3887 Validator Identity: 11111111111111111111111111111111\n\
3888 Vote Authority: None\n\
3889 Withdraw Authority: \n\
3890 Credits: 0\n\
3891 Commission: 0%\n\
3892 Inflation Rewards Commission: 0 basis points\n\
3893 Inflation Rewards Collector: \n\
3894 Block Revenue Collector: \n\
3895 Block Revenue Commission: 0 basis points\n\
3896 Pending Delegator Rewards: 0 SOL\n\
3897 Root Slot: ~\n\
3898 Recent Timestamp: 1970-01-01T00:00:00Z from slot 0\n";
3899
3900 let s = format!("{c}");
3901 #[rustfmt::skip]
3902 let expected_epoch_rewards_output =
3903 "Epoch Rewards:\n \
3904 Epoch Reward Slot Time Amount New Balance Percent Change APR Commission\n \
3905 1 100 1970-01-01 00:00:00 UTC ◎0.00000001 ◎0.0000001 11.000% 10.00% 1%\n \
3906 2 200 1970-01-12 13:46:40 UTC ◎0.000000012 ◎0.0000001 11.000% 13.00% 1%\n";
3907 assert_eq!(
3908 s,
3909 format!("{expected_output_common}{expected_epoch_rewards_output}")
3910 );
3911 println!("{s}");
3912
3913 c.use_csv = true;
3914 let s = format!("{c}");
3915 #[rustfmt::skip]
3916 let expected_epoch_rewards_output =
3917 "Epoch Rewards:\n\
3918 Epoch,Reward Slot,Time,Amount,New Balance,Percent Change,APR,Commission\n\
3919 1,100,1970-01-01 00:00:00 UTC,0.00000001,0.0000001,11%,10.00%,1%\n\
3920 2,200,1970-01-12 13:46:40 UTC,0.000000012,0.0000001,11%,13.00%,1%\n";
3921 assert_eq!(
3922 s,
3923 format!("{expected_output_common}{expected_epoch_rewards_output}")
3924 );
3925 println!("{s}");
3926 }
3927}