1#![allow(clippy::to_string_in_format_args)]
2use {
3 crate::{
4 QuietDisplay, VerboseDisplay,
5 cli_clientid::CliClientId,
6 cli_version::CliVersion,
7 display::{
8 BuildBalanceMessageConfig, build_balance_message, build_balance_message_with_config,
9 format_labeled_address, unix_timestamp_to_string, writeln_name_value,
10 writeln_transaction,
11 },
12 },
13 agave_votor_messages::{migration::AG_MIGRATION_EPOCH_CREDIT, wire::WireBlockCertMessage},
14 base64::{Engine, prelude::BASE64_STANDARD},
15 bitvec::vec::BitVec,
16 chrono::{Local, TimeZone, Utc},
17 clap::ArgMatches,
18 console::{Emoji, style},
19 inflector::cases::titlecase::to_title_case,
20 serde::{Deserialize, Serialize},
21 serde_json::{Map, Value},
22 solana_account::ReadableAccount,
23 solana_account_decoder::{
24 UiAccountEncoding, UiDataSliceConfig, encode_ui_account,
25 parse_account_data::AccountAdditionalDataV3, parse_token::UiTokenAccount,
26 },
27 solana_bls_signatures::Signature as BLSSignature,
28 solana_clap_utils::keypair::SignOnly,
29 solana_clock::{Epoch, Slot, UnixTimestamp},
30 solana_epoch_info::EpochInfo,
31 solana_epoch_schedule::EpochSchedule,
32 solana_hash::Hash,
33 solana_native_token::LAMPORTS_PER_SOL,
34 solana_pubkey::Pubkey,
35 solana_rpc_client_api::response::{
36 RpcAccountBalance, RpcContactInfo, RpcInflationGovernor, RpcInflationRate, RpcKeyedAccount,
37 RpcSupply, RpcVoteAccountInfo,
38 },
39 solana_signature::Signature,
40 solana_stake_history::StakeHistoryItem,
41 solana_stake_interface::state::{Authorized, Lockup},
42 solana_transaction::{Transaction, versioned::VersionedTransaction},
43 solana_transaction_status::{
44 EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus,
45 UiTransactionStatusMeta,
46 },
47 solana_transaction_status_client_types::{Rewards, UiTransactionError},
48 solana_vote_program::{
49 authorized_voters::AuthorizedVoters,
50 vote_state::{
51 BlockTimestamp, LandedVote, MAX_EPOCH_CREDITS_HISTORY, MAX_LOCKOUT_HISTORY,
52 VOTE_CREDITS_MAXIMUM_PER_SLOT, VoteStateV4,
53 },
54 },
55 std::{
56 cmp::Ordering,
57 collections::{BTreeMap, HashMap},
58 fmt,
59 str::FromStr,
60 time::Duration,
61 },
62};
63
64static CHECK_MARK: Emoji = Emoji("✅ ", "");
65static CROSS_MARK: Emoji = Emoji("❌ ", "");
66static WARNING: Emoji = Emoji("⚠️", "!");
67
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum OutputFormat {
70 Display,
71 Json,
72 JsonCompact,
73 DisplayQuiet,
74 DisplayVerbose,
75}
76
77impl OutputFormat {
78 pub fn formatted_string<T>(&self, item: &T) -> String
79 where
80 T: Serialize + fmt::Display + QuietDisplay + VerboseDisplay,
81 {
82 match self {
83 OutputFormat::Display => format!("{item}"),
84 OutputFormat::DisplayQuiet => {
85 let mut s = String::new();
86 QuietDisplay::write_str(item, &mut s).unwrap();
87 s
88 }
89 OutputFormat::DisplayVerbose => {
90 let mut s = String::new();
91 VerboseDisplay::write_str(item, &mut s).unwrap();
92 s
93 }
94 OutputFormat::Json => serde_json::to_string_pretty(item).unwrap(),
95 OutputFormat::JsonCompact => serde_json::to_value(item).unwrap().to_string(),
96 }
97 }
98
99 pub fn from_matches(matches: &ArgMatches<'_>, output_name: &str, verbose: bool) -> Self {
100 matches
101 .value_of(output_name)
102 .map(|value| match value {
103 "json" => OutputFormat::Json,
104 "json-compact" => OutputFormat::JsonCompact,
105 _ => unreachable!(),
106 })
107 .unwrap_or(if verbose {
108 OutputFormat::DisplayVerbose
109 } else {
110 OutputFormat::Display
111 })
112 }
113}
114
115#[derive(Serialize)]
116pub struct CliPrioritizationFeeStats {
117 pub fees: Vec<CliPrioritizationFee>,
118 pub min: u64,
119 pub max: u64,
120 pub average: u64,
121 pub num_slots: u64,
122}
123
124impl QuietDisplay for CliPrioritizationFeeStats {}
125impl VerboseDisplay for CliPrioritizationFeeStats {
126 fn write_str(&self, f: &mut dyn std::fmt::Write) -> fmt::Result {
127 writeln!(f, "{:<11} prioritization_fee", "slot")?;
128 for fee in &self.fees {
129 write!(f, "{fee}")?;
130 }
131 write!(f, "{self}")
132 }
133}
134
135impl fmt::Display for CliPrioritizationFeeStats {
136 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
137 writeln!(
138 f,
139 "Fees in recent {} slots: Min: {} Max: {} Average: {}",
140 self.num_slots, self.min, self.max, self.average
141 )
142 }
143}
144
145#[derive(Serialize)]
146pub struct CliPrioritizationFee {
147 pub slot: Slot,
148 pub prioritization_fee: u64,
149}
150
151impl QuietDisplay for CliPrioritizationFee {}
152impl VerboseDisplay for CliPrioritizationFee {}
153
154impl fmt::Display for CliPrioritizationFee {
155 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
156 writeln!(f, "{:<11} {}", self.slot, self.prioritization_fee)
157 }
158}
159
160#[derive(Serialize, Deserialize)]
161pub struct CliAccount {
162 #[serde(flatten)]
163 pub keyed_account: RpcKeyedAccount,
164 #[serde(skip_serializing, skip_deserializing)]
165 pub use_lamports_unit: bool,
166}
167
168pub struct CliAccountNewConfig {
169 pub data_encoding: UiAccountEncoding,
170 pub additional_data: Option<AccountAdditionalDataV3>,
171 pub data_slice_config: Option<UiDataSliceConfig>,
172 pub use_lamports_unit: bool,
173}
174
175impl Default for CliAccountNewConfig {
176 fn default() -> Self {
177 Self {
178 data_encoding: UiAccountEncoding::Base64,
179 additional_data: None,
180 data_slice_config: None,
181 use_lamports_unit: false,
182 }
183 }
184}
185
186impl CliAccount {
187 pub fn new<T: ReadableAccount>(address: &Pubkey, account: &T, use_lamports_unit: bool) -> Self {
188 Self::new_with_config(
189 address,
190 account,
191 &CliAccountNewConfig {
192 use_lamports_unit,
193 ..CliAccountNewConfig::default()
194 },
195 )
196 }
197
198 pub fn new_with_config<T: ReadableAccount>(
199 address: &Pubkey,
200 account: &T,
201 config: &CliAccountNewConfig,
202 ) -> Self {
203 let CliAccountNewConfig {
204 data_encoding,
205 additional_data,
206 data_slice_config,
207 use_lamports_unit,
208 } = *config;
209 Self {
210 keyed_account: RpcKeyedAccount {
211 pubkey: address.to_string(),
212 account: encode_ui_account(
213 address,
214 account,
215 data_encoding,
216 additional_data,
217 data_slice_config,
218 ),
219 },
220 use_lamports_unit,
221 }
222 }
223}
224
225impl QuietDisplay for CliAccount {}
226impl VerboseDisplay for CliAccount {}
227
228impl fmt::Display for CliAccount {
229 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
230 writeln!(f)?;
231 writeln_name_value(f, "Public Key:", &self.keyed_account.pubkey)?;
232 writeln_name_value(
233 f,
234 "Balance:",
235 &build_balance_message(
236 self.keyed_account.account.lamports,
237 self.use_lamports_unit,
238 true,
239 ),
240 )?;
241 writeln_name_value(f, "Owner:", &self.keyed_account.account.owner)?;
242 writeln_name_value(
243 f,
244 "Executable:",
245 &self.keyed_account.account.executable.to_string(),
246 )?;
247 writeln_name_value(
248 f,
249 "Rent Epoch:",
250 &self.keyed_account.account.rent_epoch.to_string(),
251 )?;
252 Ok(())
253 }
254}
255
256#[derive(Default, Serialize, Deserialize)]
257pub struct CliBlockProduction {
258 pub epoch: Epoch,
259 pub start_slot: Slot,
260 pub end_slot: Slot,
261 pub total_slots: usize,
262 pub total_blocks_produced: usize,
263 pub total_slots_skipped: usize,
264 pub leaders: Vec<CliBlockProductionEntry>,
265 pub individual_slot_status: Vec<CliSlotStatus>,
266 #[serde(skip_serializing)]
267 pub verbose: bool,
268}
269
270impl QuietDisplay for CliBlockProduction {}
271impl VerboseDisplay for CliBlockProduction {}
272
273impl fmt::Display for CliBlockProduction {
274 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
275 writeln!(f)?;
276 writeln!(
277 f,
278 "{}",
279 style(format!(
280 " {:<44} {:>15} {:>15} {:>15} {:>15}",
281 "Identity", "Leader Slots", "Blocks Produced", "Skipped Slots", "Skip Rate",
282 ))
283 .bold()
284 )?;
285 for leader in &self.leaders {
286 writeln!(
287 f,
288 " {:<44} {:>15} {:>15} {:>15} {:>22.2}%",
289 leader.identity_pubkey,
290 leader.leader_slots,
291 leader.blocks_produced,
292 leader.skipped_slots,
293 leader.skipped_slots as f64 / leader.leader_slots as f64 * 100.
294 )?;
295 }
296 writeln!(f)?;
297 writeln!(
298 f,
299 " {:<44} {:>15} {:>15} {:>15} {:>22.2}%",
300 format!("Epoch {} total:", self.epoch),
301 self.total_slots,
302 self.total_blocks_produced,
303 self.total_slots_skipped,
304 self.total_slots_skipped as f64 / self.total_slots as f64 * 100.
305 )?;
306 writeln!(
307 f,
308 " (using data from {} slots: {} to {})",
309 self.total_slots, self.start_slot, self.end_slot
310 )?;
311 if self.verbose {
312 writeln!(f)?;
313 writeln!(f)?;
314 writeln!(
315 f,
316 "{}",
317 style(format!(" {:<15} {:<44}", "Slot", "Identity Pubkey")).bold(),
318 )?;
319 for status in &self.individual_slot_status {
320 if status.skipped {
321 writeln!(
322 f,
323 "{}",
324 style(format!(
325 " {:<15} {:<44} SKIPPED",
326 status.slot, status.leader
327 ))
328 .red()
329 )?;
330 } else {
331 writeln!(
332 f,
333 "{}",
334 style(format!(" {:<15} {:<44}", status.slot, status.leader))
335 )?;
336 }
337 }
338 }
339 Ok(())
340 }
341}
342
343#[derive(Default, Serialize, Deserialize)]
344#[serde(rename_all = "camelCase")]
345pub struct CliBlockProductionEntry {
346 pub identity_pubkey: String,
347 pub leader_slots: u64,
348 pub blocks_produced: u64,
349 pub skipped_slots: u64,
350}
351
352#[derive(Default, Serialize, Deserialize)]
353#[serde(rename_all = "camelCase")]
354pub struct CliSlotStatus {
355 pub slot: Slot,
356 pub leader: String,
357 pub skipped: bool,
358}
359
360#[derive(Serialize, Deserialize)]
361#[serde(rename_all = "camelCase")]
362pub struct CliEpochInfo {
363 #[serde(flatten)]
364 pub epoch_info: EpochInfo,
365 pub epoch_completed_percent: f64,
366 #[serde(skip)]
367 pub average_slot_time_ms: u64,
368 #[serde(skip)]
369 pub start_block_time: Option<UnixTimestamp>,
370 #[serde(skip)]
371 pub current_block_time: Option<UnixTimestamp>,
372}
373
374impl QuietDisplay for CliEpochInfo {}
375impl VerboseDisplay for CliEpochInfo {}
376
377impl fmt::Display for CliEpochInfo {
378 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
379 writeln!(f)?;
380 writeln_name_value(
381 f,
382 "Block height:",
383 &self.epoch_info.block_height.to_string(),
384 )?;
385 writeln_name_value(f, "Slot:", &self.epoch_info.absolute_slot.to_string())?;
386 writeln_name_value(f, "Epoch:", &self.epoch_info.epoch.to_string())?;
387 if let Some(transaction_count) = &self.epoch_info.transaction_count {
388 writeln_name_value(f, "Transaction Count:", &transaction_count.to_string())?;
389 }
390 let start_slot = self.epoch_info.absolute_slot - self.epoch_info.slot_index;
391 let end_slot = start_slot + self.epoch_info.slots_in_epoch;
392 writeln_name_value(
393 f,
394 "Epoch Slot Range:",
395 &format!("[{start_slot}..{end_slot})"),
396 )?;
397 writeln_name_value(
398 f,
399 "Epoch Completed Percent:",
400 &format!("{:>3.3}%", self.epoch_completed_percent),
401 )?;
402 let remaining_slots_in_epoch = self.epoch_info.slots_in_epoch - self.epoch_info.slot_index;
403 writeln_name_value(
404 f,
405 "Epoch Completed Slots:",
406 &format!(
407 "{}/{} ({} remaining)",
408 self.epoch_info.slot_index,
409 self.epoch_info.slots_in_epoch,
410 remaining_slots_in_epoch
411 ),
412 )?;
413 let (time_elapsed, annotation) = if let (Some(start_block_time), Some(current_block_time)) =
414 (self.start_block_time, self.current_block_time)
415 {
416 (
417 Duration::from_secs((current_block_time - start_block_time) as u64),
418 None,
419 )
420 } else {
421 (
422 slot_to_duration(self.epoch_info.slot_index, self.average_slot_time_ms),
423 Some("* estimated based on current slot durations"),
424 )
425 };
426 let time_remaining = slot_to_duration(remaining_slots_in_epoch, self.average_slot_time_ms);
427 writeln_name_value(
428 f,
429 "Epoch Completed Time:",
430 &format!(
431 "{}{}/{} ({} remaining)",
432 humantime::format_duration(time_elapsed),
433 if annotation.is_some() { "*" } else { "" },
434 humantime::format_duration(time_elapsed + time_remaining),
435 humantime::format_duration(time_remaining),
436 ),
437 )?;
438 if let Some(annotation) = annotation {
439 writeln!(f)?;
440 writeln!(f, "{annotation}")?;
441 }
442 Ok(())
443 }
444}
445
446fn slot_to_duration(slot: Slot, slot_time_ms: u64) -> Duration {
447 Duration::from_secs((slot * slot_time_ms) / 1000)
448}
449
450#[derive(Serialize, Deserialize, Default, Debug)]
451#[serde(rename_all = "camelCase")]
452pub struct CliValidatorsStakeByVersion {
453 pub current_validators: usize,
454 pub delinquent_validators: usize,
455 pub current_active_stake: u64,
456 pub delinquent_active_stake: u64,
457}
458
459#[derive(Serialize, Deserialize, Default, Debug)]
460#[serde(rename_all = "camelCase")]
461pub struct CliValidatorsStakeByClientId {
462 pub current_validators: usize,
463 pub delinquent_validators: usize,
464 pub current_active_stake: u64,
465 pub delinquent_active_stake: u64,
466}
467
468#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone, Copy)]
469pub enum CliValidatorsSortOrder {
470 Delinquent,
471 Commission,
472 EpochCredits,
473 Identity,
474 LastVote,
475 Root,
476 SkipRate,
477 #[default]
478 Stake,
479 VoteAccount,
480 Version,
481 ClientId,
482}
483
484#[derive(Serialize, Deserialize, Debug)]
485#[serde(rename_all = "camelCase")]
486pub struct CliValidators {
487 pub total_active_stake: u64,
488 pub total_current_stake: u64,
489 pub total_delinquent_stake: u64,
490 pub validators: Vec<CliValidator>,
491 pub average_skip_rate: f64,
492 pub average_stake_weighted_skip_rate: f64,
493 #[serde(skip_serializing, skip_deserializing)]
494 pub validators_sort_order: CliValidatorsSortOrder,
495 #[serde(skip_serializing, skip_deserializing)]
496 pub validators_reverse_sort: bool,
497 #[serde(skip_serializing, skip_deserializing)]
498 pub number_validators: bool,
499 pub stake_by_version: BTreeMap<CliVersion, CliValidatorsStakeByVersion>,
500 pub stake_by_client_id: BTreeMap<CliClientId, CliValidatorsStakeByClientId>,
501 #[serde(skip_serializing, skip_deserializing)]
502 pub use_lamports_unit: bool,
503}
504
505impl QuietDisplay for CliValidators {}
506impl VerboseDisplay for CliValidators {}
507
508impl fmt::Display for CliValidators {
509 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
510 fn write_vote_account(
511 f: &mut fmt::Formatter,
512 validator: &CliValidator,
513 total_active_stake: u64,
514 use_lamports_unit: bool,
515 highest_last_vote: u64,
516 highest_root: u64,
517 ) -> fmt::Result {
518 fn non_zero_or_dash(v: u64, max_v: u64) -> String {
519 if v == 0 {
520 " - ".into()
521 } else if v == max_v {
522 format!("{v:>9} ( 0)")
523 } else if v > max_v.saturating_sub(100) {
524 format!("{:>9} ({:>3})", v, -(max_v.saturating_sub(v) as isize))
525 } else {
526 format!("{v:>9} ")
527 }
528 }
529
530 writeln!(
531 f,
532 "{} {:<44} {:<44} {:>7} {:>14} {:>14} {:>7} {:>8} {:>7} {:<14} {:>22} \
533 ({:.2}%)",
534 if validator.delinquent {
535 WARNING.to_string()
536 } else {
537 "\u{a0}".to_string()
538 },
539 validator.identity_pubkey,
540 validator.vote_account_pubkey,
541 format!("{:.2}%", validator.commission_bps as f64 / 100.0),
542 non_zero_or_dash(validator.last_vote, highest_last_vote),
543 non_zero_or_dash(validator.root_slot, highest_root),
544 if let Some(skip_rate) = validator.skip_rate {
545 format!("{skip_rate:.2}%")
546 } else {
547 "- ".to_string()
548 },
549 validator.epoch_credits,
550 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<&StakeHistoryItem> for CliStakeHistoryEntry {
1688 fn from(StakeHistoryItem { epoch, entry }: &StakeHistoryItem) -> Self {
1689 Self {
1690 epoch: *epoch,
1691 effective_stake: entry.effective,
1692 activating_stake: entry.activating,
1693 deactivating_stake: entry.deactivating,
1694 }
1695 }
1696}
1697
1698#[derive(Serialize, Deserialize)]
1699#[serde(rename_all = "camelCase")]
1700pub struct CliStakeHistoryEntry {
1701 pub epoch: Epoch,
1702 pub effective_stake: u64,
1703 pub activating_stake: u64,
1704 pub deactivating_stake: u64,
1705}
1706
1707#[derive(Serialize, Deserialize)]
1708#[serde(rename_all = "camelCase")]
1709pub struct CliAuthorized {
1710 pub staker: String,
1711 pub withdrawer: String,
1712}
1713
1714impl From<&Authorized> for CliAuthorized {
1715 fn from(authorized: &Authorized) -> Self {
1716 Self {
1717 staker: authorized.staker.to_string(),
1718 withdrawer: authorized.withdrawer.to_string(),
1719 }
1720 }
1721}
1722
1723#[derive(Serialize, Deserialize)]
1724#[serde(rename_all = "camelCase")]
1725pub struct CliLockup {
1726 pub unix_timestamp: UnixTimestamp,
1727 pub epoch: Epoch,
1728 pub custodian: String,
1729}
1730
1731impl From<&Lockup> for CliLockup {
1732 fn from(lockup: &Lockup) -> Self {
1733 Self {
1734 unix_timestamp: lockup.unix_timestamp,
1735 epoch: lockup.epoch,
1736 custodian: lockup.custodian.to_string(),
1737 }
1738 }
1739}
1740
1741#[derive(Serialize, Deserialize)]
1742pub struct CliValidatorInfoVec(Vec<CliValidatorInfo>);
1743
1744impl CliValidatorInfoVec {
1745 pub fn new(list: Vec<CliValidatorInfo>) -> Self {
1746 Self(list)
1747 }
1748}
1749
1750impl QuietDisplay for CliValidatorInfoVec {}
1751impl VerboseDisplay for CliValidatorInfoVec {}
1752
1753impl fmt::Display for CliValidatorInfoVec {
1754 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1755 if self.0.is_empty() {
1756 writeln!(f, "No validator info accounts found")?;
1757 }
1758 for validator_info in &self.0 {
1759 writeln!(f)?;
1760 write!(f, "{validator_info}")?;
1761 }
1762 Ok(())
1763 }
1764}
1765
1766#[derive(Serialize, Deserialize)]
1767#[serde(rename_all = "camelCase")]
1768pub struct CliValidatorInfo {
1769 pub identity_pubkey: String,
1770 pub info_pubkey: String,
1771 pub is_signed: bool,
1772 pub info: Map<String, Value>,
1773}
1774
1775impl QuietDisplay for CliValidatorInfo {}
1776impl VerboseDisplay for CliValidatorInfo {}
1777
1778impl fmt::Display for CliValidatorInfo {
1779 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1780 writeln_name_value(f, "Validator Identity:", &self.identity_pubkey)?;
1781 writeln_name_value(f, " Info Address:", &self.info_pubkey)?;
1782
1783 for (key, value) in self.info.iter() {
1784 writeln_name_value(
1785 f,
1786 &format!(" {}:", to_title_case(key)),
1787 value.as_str().unwrap_or("?"),
1788 )?;
1789 }
1790
1791 if !self.is_signed {
1792 writeln!(
1793 f,
1794 "/!\\ This validator info account is NOT verified as owned by the validator"
1795 )?;
1796 }
1797
1798 Ok(())
1799 }
1800}
1801
1802#[derive(Serialize, Deserialize)]
1803pub enum VotesObserved {
1804 Alpenglow(Option<CliLandedVote>),
1805 Tower(Vec<CliLandedVote>),
1806}
1807
1808impl VotesObserved {
1809 pub fn new(vote_state: &VoteStateV4, ag_genesis_cert: &Option<WireBlockCertMessage>) -> Self {
1810 match ag_genesis_cert {
1811 None => Self::Tower(vote_state.votes.iter().map(CliLandedVote::from).collect()),
1812 Some(cert) => match vote_state.votes.iter().last() {
1813 None => Self::Alpenglow(None),
1814 Some(vote) => {
1815 if vote.lockout.slot() <= cert.block.slot {
1816 Self::Tower(vote_state.votes.iter().map(CliLandedVote::from).collect())
1817 } else {
1818 Self::Alpenglow(Some(CliLandedVote::from(vote)))
1819 }
1820 }
1821 },
1822 }
1823 }
1824}
1825
1826impl Default for VotesObserved {
1827 fn default() -> Self {
1828 Self::Tower(vec![])
1829 }
1830}
1831
1832#[derive(Default, Serialize, Deserialize)]
1833#[serde(rename_all = "camelCase")]
1834pub struct CliVoteAccount {
1835 pub account_balance: u64,
1836 pub validator_identity: String,
1837 #[serde(flatten)]
1838 pub authorized_voters: CliAuthorizedVoters,
1839 pub authorized_withdrawer: String,
1840 pub credits: u64,
1841 pub commission: u8,
1842 pub root_slot: Option<Slot>,
1843 pub recent_timestamp: BlockTimestamp,
1844 pub votes_observed: VotesObserved,
1845 pub epoch_voting_history: Vec<CliEpochVotingHistory>,
1846 #[serde(skip_serializing)]
1847 pub use_lamports_unit: bool,
1848 #[serde(skip_serializing)]
1849 pub use_csv: bool,
1850 #[serde(skip_serializing_if = "Option::is_none")]
1851 pub epoch_rewards: Option<Vec<CliEpochReward>>,
1852 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,
2134 bitvec,
2135 signature,
2136 } = payload;
2137 writeln!(f, "Alpenglow genesis information:")?;
2138 writeln!(f, " Feature flag activation: Epoch {epoch}")?;
2139 writeln!(f, " Genesis Block - Slot {slot}, Block ID {block_id}")?;
2140 writeln!(
2141 f,
2142 " Genesis Vote - {} validators participated, Signature {signature}",
2143 bitvec.count_ones(),
2144 )
2145 }
2146 }
2147 }
2148}
2149
2150#[derive(Serialize, Deserialize)]
2151#[serde(rename_all = "camelCase")]
2152pub struct CliBlockTime {
2153 pub slot: Slot,
2154 pub timestamp: UnixTimestamp,
2155}
2156
2157impl QuietDisplay for CliBlockTime {}
2158impl VerboseDisplay for CliBlockTime {}
2159
2160impl fmt::Display for CliBlockTime {
2161 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2162 writeln_name_value(f, "Block:", &self.slot.to_string())?;
2163 writeln_name_value(f, "Date:", &unix_timestamp_to_string(self.timestamp))
2164 }
2165}
2166
2167#[derive(Serialize, Deserialize)]
2168#[serde(rename_all = "camelCase")]
2169pub struct CliLeaderSchedule {
2170 pub epoch: Epoch,
2171 pub leader_schedule_entries: Vec<CliLeaderScheduleEntry>,
2172}
2173
2174impl QuietDisplay for CliLeaderSchedule {}
2175impl VerboseDisplay for CliLeaderSchedule {}
2176
2177impl fmt::Display for CliLeaderSchedule {
2178 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2179 for entry in &self.leader_schedule_entries {
2180 writeln!(f, " {:<15} {:<44}", entry.slot, entry.leader)?;
2181 }
2182 Ok(())
2183 }
2184}
2185
2186#[derive(Serialize, Deserialize)]
2187#[serde(rename_all = "camelCase")]
2188pub struct CliLeaderScheduleEntry {
2189 pub slot: Slot,
2190 pub leader: String,
2191}
2192
2193#[derive(Serialize, Deserialize)]
2194#[serde(rename_all = "camelCase")]
2195pub struct CliInflation {
2196 pub governor: RpcInflationGovernor,
2197 pub current_rate: RpcInflationRate,
2198}
2199
2200impl QuietDisplay for CliInflation {}
2201impl VerboseDisplay for CliInflation {}
2202
2203impl fmt::Display for CliInflation {
2204 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2205 writeln!(f, "{}", style("Inflation Governor:").bold())?;
2206 if (self.governor.initial - self.governor.terminal).abs() < f64::EPSILON {
2207 writeln!(
2208 f,
2209 "Fixed rate: {:>5.2}%",
2210 self.governor.terminal * 100.
2211 )?;
2212 } else {
2213 writeln!(
2214 f,
2215 "Initial rate: {:>5.2}%",
2216 self.governor.initial * 100.
2217 )?;
2218 writeln!(
2219 f,
2220 "Terminal rate: {:>5.2}%",
2221 self.governor.terminal * 100.
2222 )?;
2223 writeln!(
2224 f,
2225 "Rate reduction per year: {:>5.2}%",
2226 self.governor.taper * 100.
2227 )?;
2228 writeln!(
2229 f,
2230 "* Rate reduction is derived using the target slot time in genesis config"
2231 )?;
2232 }
2233 if self.governor.foundation_term > 0. {
2234 writeln!(
2235 f,
2236 "Foundation percentage: {:>5.2}%",
2237 self.governor.foundation
2238 )?;
2239 writeln!(
2240 f,
2241 "Foundation term: {:.1} years",
2242 self.governor.foundation_term
2243 )?;
2244 }
2245
2246 writeln!(
2247 f,
2248 "\n{}",
2249 style(format!("Inflation for Epoch {}:", self.current_rate.epoch)).bold()
2250 )?;
2251 writeln!(
2252 f,
2253 "Total rate: {:>5.2}%",
2254 self.current_rate.total * 100.
2255 )?;
2256 writeln!(
2257 f,
2258 "Staking rate: {:>5.2}%",
2259 self.current_rate.validator * 100.
2260 )?;
2261
2262 if self.current_rate.foundation > 0. {
2263 writeln!(
2264 f,
2265 "Foundation rate: {:>5.2}%",
2266 self.current_rate.foundation * 100.
2267 )?;
2268 }
2269 Ok(())
2270 }
2271}
2272
2273#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
2274#[serde(rename_all = "camelCase")]
2275pub struct CliSignOnlyData {
2276 pub blockhash: String,
2277 #[serde(skip_serializing_if = "Option::is_none")]
2278 pub message: Option<String>,
2279 #[serde(skip_serializing_if = "Vec::is_empty", default)]
2280 pub signers: Vec<String>,
2281 #[serde(skip_serializing_if = "Vec::is_empty", default)]
2282 pub absent: Vec<String>,
2283 #[serde(skip_serializing_if = "Vec::is_empty", default)]
2284 pub bad_sig: Vec<String>,
2285}
2286
2287impl QuietDisplay for CliSignOnlyData {}
2288impl VerboseDisplay for CliSignOnlyData {}
2289
2290impl fmt::Display for CliSignOnlyData {
2291 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2292 writeln!(f)?;
2293 writeln_name_value(f, "Blockhash:", &self.blockhash)?;
2294 if let Some(message) = self.message.as_ref() {
2295 writeln_name_value(f, "Transaction Message:", message)?;
2296 }
2297 if !self.signers.is_empty() {
2298 writeln!(f, "{}", style("Signers (Pubkey=Signature):").bold())?;
2299 for signer in self.signers.iter() {
2300 writeln!(f, " {signer}")?;
2301 }
2302 }
2303 if !self.absent.is_empty() {
2304 writeln!(f, "{}", style("Absent Signers (Pubkey):").bold())?;
2305 for pubkey in self.absent.iter() {
2306 writeln!(f, " {pubkey}")?;
2307 }
2308 }
2309 if !self.bad_sig.is_empty() {
2310 writeln!(f, "{}", style("Bad Signatures (Pubkey):").bold())?;
2311 for pubkey in self.bad_sig.iter() {
2312 writeln!(f, " {pubkey}")?;
2313 }
2314 }
2315 Ok(())
2316 }
2317}
2318
2319#[derive(Serialize, Deserialize)]
2320#[serde(rename_all = "camelCase")]
2321pub struct CliSignature {
2322 pub signature: String,
2323}
2324
2325impl QuietDisplay for CliSignature {}
2326impl VerboseDisplay for CliSignature {}
2327
2328impl fmt::Display for CliSignature {
2329 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2330 writeln!(f)?;
2331 writeln_name_value(f, "Signature:", &self.signature)?;
2332 Ok(())
2333 }
2334}
2335
2336#[derive(Serialize, Deserialize)]
2337#[serde(rename_all = "camelCase")]
2338pub struct CliAccountBalances {
2339 pub accounts: Vec<RpcAccountBalance>,
2340}
2341
2342impl QuietDisplay for CliAccountBalances {}
2343impl VerboseDisplay for CliAccountBalances {}
2344
2345impl fmt::Display for CliAccountBalances {
2346 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2347 writeln!(
2348 f,
2349 "{}",
2350 style(format!("{:<44} {}", "Address", "Balance")).bold()
2351 )?;
2352 for account in &self.accounts {
2353 writeln!(
2354 f,
2355 "{:<44} {} SOL",
2356 account.address,
2357 build_balance_message(account.lamports, false, false)
2358 )?;
2359 }
2360 Ok(())
2361 }
2362}
2363
2364#[derive(Serialize, Deserialize)]
2365#[serde(rename_all = "camelCase")]
2366pub struct CliSupply {
2367 pub total: u64,
2368 pub circulating: u64,
2369 pub non_circulating: u64,
2370 pub non_circulating_accounts: Vec<String>,
2371 #[serde(skip_serializing)]
2372 pub print_accounts: bool,
2373}
2374
2375impl From<RpcSupply> for CliSupply {
2376 fn from(rpc_supply: RpcSupply) -> Self {
2377 Self {
2378 total: rpc_supply.total,
2379 circulating: rpc_supply.circulating,
2380 non_circulating: rpc_supply.non_circulating,
2381 non_circulating_accounts: rpc_supply.non_circulating_accounts,
2382 print_accounts: false,
2383 }
2384 }
2385}
2386
2387impl QuietDisplay for CliSupply {}
2388impl VerboseDisplay for CliSupply {}
2389
2390impl fmt::Display for CliSupply {
2391 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2392 writeln_name_value(
2393 f,
2394 "Total:",
2395 &format!("{} SOL", build_balance_message(self.total, false, false)),
2396 )?;
2397 writeln_name_value(
2398 f,
2399 "Circulating:",
2400 &format!(
2401 "{} SOL",
2402 build_balance_message(self.circulating, false, false)
2403 ),
2404 )?;
2405 writeln_name_value(
2406 f,
2407 "Non-Circulating:",
2408 &format!(
2409 "{} SOL",
2410 build_balance_message(self.non_circulating, false, false)
2411 ),
2412 )?;
2413 if self.print_accounts {
2414 writeln!(f)?;
2415 writeln_name_value(f, "Non-Circulating Accounts:", " ")?;
2416 for account in &self.non_circulating_accounts {
2417 writeln!(f, " {account}")?;
2418 }
2419 }
2420 Ok(())
2421 }
2422}
2423
2424#[derive(Serialize, Deserialize)]
2425#[serde(rename_all = "camelCase")]
2426pub struct CliFeesInner {
2427 pub slot: Slot,
2428 pub blockhash: String,
2429 pub lamports_per_signature: u64,
2430 pub last_valid_slot: Option<Slot>,
2431 pub last_valid_block_height: Option<Slot>,
2432}
2433
2434impl QuietDisplay for CliFeesInner {}
2435impl VerboseDisplay for CliFeesInner {}
2436
2437impl fmt::Display for CliFeesInner {
2438 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2439 writeln_name_value(f, "Blockhash:", &self.blockhash)?;
2440 writeln_name_value(
2441 f,
2442 "Lamports per signature:",
2443 &self.lamports_per_signature.to_string(),
2444 )?;
2445 let last_valid_block_height = self
2446 .last_valid_block_height
2447 .map(|s| s.to_string())
2448 .unwrap_or_default();
2449 writeln_name_value(f, "Last valid block height:", &last_valid_block_height)
2450 }
2451}
2452
2453#[derive(Serialize, Deserialize)]
2454#[serde(rename_all = "camelCase")]
2455pub struct CliFees {
2456 #[serde(flatten, skip_serializing_if = "Option::is_none")]
2457 pub inner: Option<CliFeesInner>,
2458}
2459
2460impl QuietDisplay for CliFees {}
2461impl VerboseDisplay for CliFees {}
2462
2463impl fmt::Display for CliFees {
2464 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2465 match self.inner.as_ref() {
2466 Some(inner) => write!(f, "{inner}"),
2467 None => write!(f, "Fees unavailable"),
2468 }
2469 }
2470}
2471
2472impl CliFees {
2473 pub fn some(
2474 slot: Slot,
2475 blockhash: Hash,
2476 lamports_per_signature: u64,
2477 last_valid_slot: Option<Slot>,
2478 last_valid_block_height: Option<Slot>,
2479 ) -> Self {
2480 Self {
2481 inner: Some(CliFeesInner {
2482 slot,
2483 blockhash: blockhash.to_string(),
2484 lamports_per_signature,
2485 last_valid_slot,
2486 last_valid_block_height,
2487 }),
2488 }
2489 }
2490 pub fn none() -> Self {
2491 Self { inner: None }
2492 }
2493}
2494
2495#[derive(Serialize, Deserialize)]
2496#[serde(rename_all = "camelCase")]
2497pub struct CliTokenAccount {
2498 pub address: String,
2499 #[serde(flatten)]
2500 pub token_account: UiTokenAccount,
2501}
2502
2503impl QuietDisplay for CliTokenAccount {}
2504impl VerboseDisplay for CliTokenAccount {}
2505
2506impl fmt::Display for CliTokenAccount {
2507 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2508 writeln!(f)?;
2509 writeln_name_value(f, "Address:", &self.address)?;
2510 let account = &self.token_account;
2511 writeln_name_value(
2512 f,
2513 "Balance:",
2514 &account.token_amount.real_number_string_trimmed(),
2515 )?;
2516 let mint = format!(
2517 "{}{}",
2518 account.mint,
2519 if account.is_native { " (native)" } else { "" }
2520 );
2521 writeln_name_value(f, "Mint:", &mint)?;
2522 writeln_name_value(f, "Owner:", &account.owner)?;
2523 writeln_name_value(f, "State:", &format!("{:?}", account.state))?;
2524 if let Some(delegate) = &account.delegate {
2525 writeln!(f, "Delegation:")?;
2526 writeln_name_value(f, " Delegate:", delegate)?;
2527 let allowance = account.delegated_amount.as_ref().unwrap();
2528 writeln_name_value(f, " Allowance:", &allowance.real_number_string_trimmed())?;
2529 }
2530 writeln_name_value(
2531 f,
2532 "Close authority:",
2533 account.close_authority.as_ref().unwrap_or(&String::new()),
2534 )?;
2535 Ok(())
2536 }
2537}
2538
2539#[derive(Serialize, Deserialize)]
2540#[serde(rename_all = "camelCase")]
2541pub struct CliProgramId {
2542 pub program_id: String,
2543 pub signature: Option<String>,
2544}
2545
2546impl QuietDisplay for CliProgramId {}
2547impl VerboseDisplay for CliProgramId {}
2548
2549impl fmt::Display for CliProgramId {
2550 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2551 writeln_name_value(f, "Program Id:", &self.program_id)?;
2552 if let Some(ref signature) = self.signature {
2553 writeln!(f)?;
2554 writeln_name_value(f, "Signature:", signature)?;
2555 }
2556 Ok(())
2557 }
2558}
2559
2560#[derive(Serialize, Deserialize)]
2561#[serde(rename_all = "camelCase")]
2562pub struct CliProgramBuffer {
2563 pub buffer: String,
2564}
2565
2566impl QuietDisplay for CliProgramBuffer {}
2567impl VerboseDisplay for CliProgramBuffer {}
2568
2569impl fmt::Display for CliProgramBuffer {
2570 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2571 writeln_name_value(f, "Buffer:", &self.buffer)
2572 }
2573}
2574
2575#[derive(Debug, Serialize, Deserialize)]
2576#[serde(rename_all = "camelCase")]
2577pub enum CliProgramAccountType {
2578 Buffer,
2579 Program,
2580}
2581
2582#[derive(Serialize, Deserialize)]
2583#[serde(rename_all = "camelCase")]
2584pub struct CliProgramAuthority {
2585 pub authority: String,
2586 pub account_type: CliProgramAccountType,
2587}
2588
2589impl QuietDisplay for CliProgramAuthority {}
2590impl VerboseDisplay for CliProgramAuthority {}
2591
2592impl fmt::Display for CliProgramAuthority {
2593 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2594 writeln_name_value(f, "Account Type:", &format!("{:?}", self.account_type))?;
2595 writeln_name_value(f, "Authority:", &self.authority)
2596 }
2597}
2598
2599#[derive(Serialize, Deserialize)]
2600#[serde(rename_all = "camelCase")]
2601pub struct CliProgram {
2602 pub program_id: String,
2603 pub owner: String,
2604 pub data_len: usize,
2605}
2606impl QuietDisplay for CliProgram {}
2607impl VerboseDisplay for CliProgram {}
2608impl fmt::Display for CliProgram {
2609 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2610 writeln!(f)?;
2611 writeln_name_value(f, "Program Id:", &self.program_id)?;
2612 writeln_name_value(f, "Owner:", &self.owner)?;
2613 writeln_name_value(
2614 f,
2615 "Data Length:",
2616 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2617 )?;
2618 Ok(())
2619 }
2620}
2621
2622#[derive(Serialize, Deserialize)]
2623#[serde(rename_all = "camelCase")]
2624pub struct CliProgramV4 {
2625 pub program_id: String,
2626 pub owner: String,
2627 pub authority: String,
2628 pub last_deploy_slot: u64,
2629 pub status: String,
2630 pub data_len: usize,
2631}
2632impl QuietDisplay for CliProgramV4 {}
2633impl VerboseDisplay for CliProgramV4 {}
2634impl fmt::Display for CliProgramV4 {
2635 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2636 writeln!(f)?;
2637 writeln_name_value(f, "Program Id:", &self.program_id)?;
2638 writeln_name_value(f, "Owner:", &self.owner)?;
2639 writeln_name_value(f, "Authority:", &self.authority)?;
2640 writeln_name_value(
2641 f,
2642 "Last Deployed In Slot:",
2643 &self.last_deploy_slot.to_string(),
2644 )?;
2645 writeln_name_value(f, "Status:", &self.status)?;
2646 writeln_name_value(
2647 f,
2648 "Data Length:",
2649 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2650 )?;
2651 Ok(())
2652 }
2653}
2654
2655#[derive(Serialize, Deserialize)]
2656#[serde(rename_all = "camelCase")]
2657pub struct CliProgramsV4 {
2658 pub programs: Vec<CliProgramV4>,
2659}
2660impl QuietDisplay for CliProgramsV4 {}
2661impl VerboseDisplay for CliProgramsV4 {}
2662impl fmt::Display for CliProgramsV4 {
2663 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2664 writeln!(f)?;
2665 writeln!(
2666 f,
2667 "{}",
2668 style(format!(
2669 "{:<44} | {:<9} | {:<44} | {:<10}",
2670 "Program Id", "Slot", "Authority", "Status"
2671 ))
2672 .bold()
2673 )?;
2674 for program in self.programs.iter() {
2675 writeln!(
2676 f,
2677 "{:<44} | {:<9} | {:<44} | {:<10}",
2678 program.program_id, program.last_deploy_slot, program.authority, program.status,
2679 )?;
2680 }
2681 Ok(())
2682 }
2683}
2684
2685#[derive(Serialize, Deserialize)]
2686#[serde(rename_all = "camelCase")]
2687pub struct CliUpgradeableProgram {
2688 pub program_id: String,
2689 pub owner: String,
2690 pub programdata_address: String,
2691 pub authority: String,
2692 pub last_deploy_slot: u64,
2693 pub data_len: usize,
2694 pub lamports: u64,
2695 #[serde(skip_serializing)]
2696 pub use_lamports_unit: bool,
2697}
2698impl QuietDisplay for CliUpgradeableProgram {}
2699impl VerboseDisplay for CliUpgradeableProgram {}
2700impl fmt::Display for CliUpgradeableProgram {
2701 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2702 writeln!(f)?;
2703 writeln_name_value(f, "Program Id:", &self.program_id)?;
2704 writeln_name_value(f, "Owner:", &self.owner)?;
2705 writeln_name_value(f, "ProgramData Address:", &self.programdata_address)?;
2706 writeln_name_value(f, "Authority:", &self.authority)?;
2707 writeln_name_value(
2708 f,
2709 "Last Deployed In Slot:",
2710 &self.last_deploy_slot.to_string(),
2711 )?;
2712 writeln_name_value(
2713 f,
2714 "Data Length:",
2715 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2716 )?;
2717 writeln_name_value(
2718 f,
2719 "Balance:",
2720 &build_balance_message(self.lamports, self.use_lamports_unit, true),
2721 )?;
2722 Ok(())
2723 }
2724}
2725
2726#[derive(Serialize, Deserialize)]
2727#[serde(rename_all = "camelCase")]
2728pub struct CliUpgradeablePrograms {
2729 pub programs: Vec<CliUpgradeableProgram>,
2730 #[serde(skip_serializing)]
2731 pub use_lamports_unit: bool,
2732}
2733impl QuietDisplay for CliUpgradeablePrograms {}
2734impl VerboseDisplay for CliUpgradeablePrograms {}
2735impl fmt::Display for CliUpgradeablePrograms {
2736 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2737 writeln!(f)?;
2738 writeln!(
2739 f,
2740 "{}",
2741 style(format!(
2742 "{:<44} | {:<9} | {:<44} | {}",
2743 "Program Id", "Slot", "Authority", "Balance"
2744 ))
2745 .bold()
2746 )?;
2747 for program in self.programs.iter() {
2748 writeln!(
2749 f,
2750 "{:<44} | {:<9} | {:<44} | {}",
2751 program.program_id,
2752 program.last_deploy_slot,
2753 program.authority,
2754 build_balance_message(program.lamports, self.use_lamports_unit, true)
2755 )?;
2756 }
2757 Ok(())
2758 }
2759}
2760
2761#[derive(Serialize, Deserialize)]
2762#[serde(rename_all = "camelCase")]
2763pub struct CliUpgradeableProgramClosed {
2764 pub program_id: String,
2765 pub lamports: u64,
2766 #[serde(skip_serializing)]
2767 pub use_lamports_unit: bool,
2768}
2769impl QuietDisplay for CliUpgradeableProgramClosed {}
2770impl VerboseDisplay for CliUpgradeableProgramClosed {}
2771impl fmt::Display for CliUpgradeableProgramClosed {
2772 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2773 writeln!(f)?;
2774 writeln!(
2775 f,
2776 "Closed Program Id {}, {} reclaimed",
2777 self.program_id,
2778 build_balance_message(self.lamports, self.use_lamports_unit, true)
2779 )?;
2780 Ok(())
2781 }
2782}
2783
2784#[derive(Serialize, Deserialize)]
2785#[serde(rename_all = "camelCase")]
2786pub struct CliUpgradeableProgramExtended {
2787 pub program_id: String,
2788 pub additional_bytes: u32,
2789}
2790impl QuietDisplay for CliUpgradeableProgramExtended {}
2791impl VerboseDisplay for CliUpgradeableProgramExtended {}
2792impl fmt::Display for CliUpgradeableProgramExtended {
2793 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2794 writeln!(f)?;
2795 writeln!(
2796 f,
2797 "Extended Program Id {} by {} bytes",
2798 self.program_id, self.additional_bytes,
2799 )?;
2800 Ok(())
2801 }
2802}
2803
2804#[derive(Clone, Serialize, Deserialize)]
2805#[serde(rename_all = "camelCase")]
2806pub struct CliUpgradeableBuffer {
2807 pub address: String,
2808 pub authority: String,
2809 pub data_len: usize,
2810 pub lamports: u64,
2811 #[serde(skip_serializing)]
2812 pub use_lamports_unit: bool,
2813}
2814impl QuietDisplay for CliUpgradeableBuffer {}
2815impl VerboseDisplay for CliUpgradeableBuffer {}
2816impl fmt::Display for CliUpgradeableBuffer {
2817 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2818 writeln!(f)?;
2819 writeln_name_value(f, "Buffer Address:", &self.address)?;
2820 writeln_name_value(f, "Authority:", &self.authority)?;
2821 writeln_name_value(
2822 f,
2823 "Balance:",
2824 &build_balance_message(self.lamports, self.use_lamports_unit, true),
2825 )?;
2826 writeln_name_value(
2827 f,
2828 "Data Length:",
2829 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2830 )?;
2831
2832 Ok(())
2833 }
2834}
2835
2836#[derive(Serialize, Deserialize)]
2837#[serde(rename_all = "camelCase")]
2838pub struct CliUpgradeableBuffers {
2839 pub buffers: Vec<CliUpgradeableBuffer>,
2840 #[serde(skip_serializing)]
2841 pub use_lamports_unit: bool,
2842}
2843impl QuietDisplay for CliUpgradeableBuffers {}
2844impl VerboseDisplay for CliUpgradeableBuffers {}
2845impl fmt::Display for CliUpgradeableBuffers {
2846 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2847 writeln!(f)?;
2848 writeln!(
2849 f,
2850 "{}",
2851 style(format!(
2852 "{:<44} | {:<44} | {}",
2853 "Buffer Address", "Authority", "Balance"
2854 ))
2855 .bold()
2856 )?;
2857 for buffer in self.buffers.iter() {
2858 writeln!(
2859 f,
2860 "{:<44} | {:<44} | {}",
2861 buffer.address,
2862 buffer.authority,
2863 build_balance_message(buffer.lamports, self.use_lamports_unit, true)
2864 )?;
2865 }
2866 Ok(())
2867 }
2868}
2869
2870#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
2871#[serde(rename_all = "camelCase")]
2872pub struct CliAddressLookupTable {
2873 pub lookup_table_address: String,
2874 pub authority: Option<String>,
2875 pub deactivation_slot: u64,
2876 pub last_extended_slot: u64,
2877 pub addresses: Vec<String>,
2878}
2879impl QuietDisplay for CliAddressLookupTable {}
2880impl VerboseDisplay for CliAddressLookupTable {}
2881impl fmt::Display for CliAddressLookupTable {
2882 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2883 writeln!(f)?;
2884 writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
2885 if let Some(authority) = &self.authority {
2886 writeln_name_value(f, "Authority:", authority)?;
2887 } else {
2888 writeln_name_value(f, "Authority:", "None (frozen)")?;
2889 }
2890 if self.deactivation_slot == u64::MAX {
2891 writeln_name_value(f, "Deactivation Slot:", "None (still active)")?;
2892 } else {
2893 writeln_name_value(f, "Deactivation Slot:", &self.deactivation_slot.to_string())?;
2894 }
2895 if self.last_extended_slot == 0 {
2896 writeln_name_value(f, "Last Extended Slot:", "None (empty)")?;
2897 } else {
2898 writeln_name_value(
2899 f,
2900 "Last Extended Slot:",
2901 &self.last_extended_slot.to_string(),
2902 )?;
2903 }
2904 if self.addresses.is_empty() {
2905 writeln_name_value(f, "Address Table Entries:", "None (empty)")?;
2906 } else {
2907 writeln!(f, "{}", style("Address Table Entries:".to_string()).bold())?;
2908 writeln!(f)?;
2909 writeln!(
2910 f,
2911 "{}",
2912 style(format!(" {:<5} {}", "Index", "Address")).bold()
2913 )?;
2914 for (index, address) in self.addresses.iter().enumerate() {
2915 writeln!(f, " {index:<5} {address}")?;
2916 }
2917 }
2918 Ok(())
2919 }
2920}
2921
2922#[derive(Serialize, Deserialize)]
2923#[serde(rename_all = "camelCase")]
2924pub struct CliAddressLookupTableCreated {
2925 pub lookup_table_address: String,
2926 pub signature: String,
2927}
2928impl QuietDisplay for CliAddressLookupTableCreated {}
2929impl VerboseDisplay for CliAddressLookupTableCreated {}
2930impl fmt::Display for CliAddressLookupTableCreated {
2931 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2932 writeln!(f)?;
2933 writeln_name_value(f, "Signature:", &self.signature)?;
2934 writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
2935 Ok(())
2936 }
2937}
2938
2939#[derive(Debug, Default)]
2940pub struct ReturnSignersConfig {
2941 pub dump_transaction_message: bool,
2942}
2943
2944pub fn return_signers(
2945 tx: &Transaction,
2946 output_format: &OutputFormat,
2947) -> Result<String, Box<dyn std::error::Error>> {
2948 return_signers_with_config(tx, output_format, &ReturnSignersConfig::default())
2949}
2950
2951pub fn return_signers_with_config(
2952 tx: &Transaction,
2953 output_format: &OutputFormat,
2954 config: &ReturnSignersConfig,
2955) -> Result<String, Box<dyn std::error::Error>> {
2956 let cli_command = return_signers_data(tx, config);
2957 Ok(output_format.formatted_string(&cli_command))
2958}
2959
2960pub fn return_signers_data(tx: &Transaction, config: &ReturnSignersConfig) -> CliSignOnlyData {
2961 let message_data = tx.message_data();
2962 let mut signers = Vec::new();
2963 let mut absent = Vec::new();
2964 let mut bad_sig = Vec::new();
2965 tx.signatures
2966 .iter()
2967 .zip(tx.message.account_keys.iter())
2968 .for_each(|(sig, key)| {
2969 if *sig == Signature::default() {
2970 absent.push(key.to_string());
2971 } else if sig.verify(key.as_ref(), &message_data) {
2972 signers.push(format!("{key}={sig}"));
2973 } else {
2974 bad_sig.push(key.to_string());
2975 }
2976 });
2977 let message = if config.dump_transaction_message {
2978 Some(BASE64_STANDARD.encode(message_data))
2979 } else {
2980 None
2981 };
2982
2983 CliSignOnlyData {
2984 blockhash: tx.message.recent_blockhash.to_string(),
2985 message,
2986 signers,
2987 absent,
2988 bad_sig,
2989 }
2990}
2991
2992pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly {
2993 let object: Value = serde_json::from_str(reply).unwrap();
2994 let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
2995 let blockhash = blockhash_str.parse::<Hash>().unwrap();
2996 let mut present_signers: Vec<(Pubkey, Signature)> = Vec::new();
2997 let signer_strings = object.get("signers");
2998 if let Some(sig_strings) = signer_strings {
2999 present_signers = sig_strings
3000 .as_array()
3001 .unwrap()
3002 .iter()
3003 .map(|signer_string| {
3004 let mut signer = signer_string.as_str().unwrap().split('=');
3005 let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
3006 let sig = Signature::from_str(signer.next().unwrap()).unwrap();
3007 (key, sig)
3008 })
3009 .collect();
3010 }
3011 let mut absent_signers: Vec<Pubkey> = Vec::new();
3012 let signer_strings = object.get("absent");
3013 if let Some(sig_strings) = signer_strings {
3014 absent_signers = sig_strings
3015 .as_array()
3016 .unwrap()
3017 .iter()
3018 .map(|val| {
3019 let s = val.as_str().unwrap();
3020 Pubkey::from_str(s).unwrap()
3021 })
3022 .collect();
3023 }
3024 let mut bad_signers: Vec<Pubkey> = Vec::new();
3025 let signer_strings = object.get("badSig");
3026 if let Some(sig_strings) = signer_strings {
3027 bad_signers = sig_strings
3028 .as_array()
3029 .unwrap()
3030 .iter()
3031 .map(|val| {
3032 let s = val.as_str().unwrap();
3033 Pubkey::from_str(s).unwrap()
3034 })
3035 .collect();
3036 }
3037
3038 let message = object
3039 .get("message")
3040 .and_then(|o| o.as_str())
3041 .map(|m| m.to_string());
3042
3043 SignOnly {
3044 blockhash,
3045 message,
3046 present_signers,
3047 absent_signers,
3048 bad_signers,
3049 }
3050}
3051
3052#[derive(Debug, Serialize, Deserialize)]
3053#[serde(rename_all = "camelCase")]
3054pub enum CliSignatureVerificationStatus {
3055 None,
3056 Pass,
3057 Fail,
3058}
3059
3060impl CliSignatureVerificationStatus {
3061 pub fn verify_transaction(tx: &VersionedTransaction) -> Vec<Self> {
3062 let message_bytes = tx.message.serialize();
3063 tx.message
3064 .static_account_keys()
3065 .iter()
3066 .zip(&tx.signatures)
3067 .map(|(key, sig)| {
3068 if sig == &Signature::default() {
3069 CliSignatureVerificationStatus::None
3070 } else if sig.verify(key.as_ref(), &message_bytes) {
3071 CliSignatureVerificationStatus::Pass
3072 } else {
3073 CliSignatureVerificationStatus::Fail
3074 }
3075 })
3076 .collect()
3077 }
3078}
3079
3080impl fmt::Display for CliSignatureVerificationStatus {
3081 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3082 match self {
3083 Self::None => write!(f, "none"),
3084 Self::Pass => write!(f, "pass"),
3085 Self::Fail => write!(f, "fail"),
3086 }
3087 }
3088}
3089
3090#[derive(Serialize, Deserialize)]
3091#[serde(rename_all = "camelCase")]
3092pub struct CliBlock {
3093 #[serde(flatten)]
3094 pub encoded_confirmed_block: EncodedConfirmedBlock,
3095 #[serde(skip_serializing)]
3096 pub slot: Slot,
3097}
3098
3099impl CliBlock {
3100 pub fn display_block_meta(
3101 f: &mut fmt::Formatter,
3102 slot: Slot,
3103 parent_slot: Slot,
3104 blockhash: &str,
3105 previous_blockhash: &str,
3106 block_time: Option<i64>,
3107 block_height: Option<u64>,
3108 rewards: &Rewards,
3109 ) -> fmt::Result {
3110 writeln!(f, "Slot: {slot}")?;
3111 writeln!(f, "Parent Slot: {parent_slot}")?;
3112 writeln!(f, "Blockhash: {blockhash}")?;
3113 writeln!(f, "Previous Blockhash: {previous_blockhash}")?;
3114 if let Some(block_time) = block_time {
3115 writeln!(
3116 f,
3117 "Block Time: {:?}",
3118 Local.timestamp_opt(block_time, 0).unwrap()
3119 )?;
3120 }
3121 if let Some(block_height) = block_height {
3122 writeln!(f, "Block Height: {block_height:?}")?;
3123 }
3124 if !rewards.is_empty() {
3125 let mut rewards = rewards.clone();
3126 rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
3127 let mut total_rewards = 0;
3128 writeln!(f, "Rewards:")?;
3129 writeln!(
3130 f,
3131 " {:<44} {:^15} {:<15} {:<20} {:>14} {:>10}",
3132 "Address", "Type", "Amount", "New Balance", "Percent Change", "Commission"
3133 )?;
3134 for reward in rewards {
3135 let sign = if reward.lamports < 0 { "-" } else { "" };
3136
3137 total_rewards += reward.lamports;
3138 #[allow(clippy::format_in_format_args)]
3139 writeln!(
3140 f,
3141 " {:<44} {:^15} {:>15} {} {}",
3142 reward.pubkey,
3143 if let Some(reward_type) = reward.reward_type {
3144 format!("{reward_type}")
3145 } else {
3146 "-".to_string()
3147 },
3148 format!(
3149 "{}◎{:<14.9}",
3150 sign,
3151 build_balance_message(reward.lamports.unsigned_abs(), false, false)
3152 ),
3153 if reward.post_balance == 0 {
3154 " - -".to_string()
3155 } else {
3156 format!(
3157 "◎{:<19.9} {:>13.9}%",
3158 build_balance_message(reward.post_balance, false, false),
3159 (reward.lamports.abs() as f64
3160 / (reward.post_balance as f64 - reward.lamports as f64))
3161 * 100.0
3162 )
3163 },
3164 reward
3165 .commission_bps
3166 .map(|bps| format!("{:>6}.{:02}%", bps / 100, bps % 100))
3167 .or_else(|| reward.commission.map(|c| format!("{c:>9}%")))
3168 .unwrap_or_else(|| " -".to_string())
3169 )?;
3170 }
3171
3172 let sign = if total_rewards < 0 { "-" } else { "" };
3173 writeln!(
3174 f,
3175 "Total Rewards: {}◎{:<12.9}",
3176 sign,
3177 build_balance_message(total_rewards.unsigned_abs(), false, false)
3178 )?;
3179 }
3180 Ok(())
3181 }
3182}
3183
3184impl QuietDisplay for CliBlock {}
3185impl VerboseDisplay for CliBlock {}
3186
3187impl fmt::Display for CliBlock {
3188 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3189 Self::display_block_meta(
3190 f,
3191 self.slot,
3192 self.encoded_confirmed_block.parent_slot,
3193 &self.encoded_confirmed_block.blockhash,
3194 &self.encoded_confirmed_block.previous_blockhash,
3195 self.encoded_confirmed_block.block_time,
3196 self.encoded_confirmed_block.block_height,
3197 &self.encoded_confirmed_block.rewards,
3198 )?;
3199
3200 for (index, transaction_with_meta) in
3201 self.encoded_confirmed_block.transactions.iter().enumerate()
3202 {
3203 writeln!(f, "Transaction {index}:")?;
3204 writeln_transaction(
3205 f,
3206 &transaction_with_meta.transaction.decode().unwrap(),
3207 transaction_with_meta.meta.as_ref(),
3208 " ",
3209 None,
3210 None,
3211 )?;
3212 }
3213 Ok(())
3214 }
3215}
3216
3217#[derive(Serialize, Deserialize)]
3218#[serde(rename_all = "camelCase")]
3219pub struct CliTransaction {
3220 pub transaction: EncodedTransaction,
3221 pub meta: Option<UiTransactionStatusMeta>,
3222 pub block_time: Option<UnixTimestamp>,
3223 #[serde(skip_serializing_if = "Option::is_none")]
3224 pub slot: Option<Slot>,
3225 #[serde(skip_serializing)]
3226 pub decoded_transaction: VersionedTransaction,
3227 #[serde(skip_serializing)]
3228 pub prefix: String,
3229 #[serde(skip_serializing_if = "Vec::is_empty")]
3230 pub sigverify_status: Vec<CliSignatureVerificationStatus>,
3231}
3232
3233impl QuietDisplay for CliTransaction {}
3234impl VerboseDisplay for CliTransaction {}
3235
3236impl fmt::Display for CliTransaction {
3237 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3238 writeln_transaction(
3239 f,
3240 &self.decoded_transaction,
3241 self.meta.as_ref(),
3242 &self.prefix,
3243 if !self.sigverify_status.is_empty() {
3244 Some(&self.sigverify_status)
3245 } else {
3246 None
3247 },
3248 self.block_time,
3249 )
3250 }
3251}
3252
3253#[derive(Serialize, Deserialize)]
3254#[serde(rename_all = "camelCase")]
3255pub struct CliTransactionConfirmation {
3256 pub confirmation_status: Option<TransactionConfirmationStatus>,
3257 #[serde(flatten, skip_serializing_if = "Option::is_none")]
3258 pub transaction: Option<CliTransaction>,
3259 #[serde(skip_serializing)]
3260 pub get_transaction_error: Option<String>,
3261 #[serde(skip_serializing_if = "Option::is_none")]
3262 pub err: Option<UiTransactionError>,
3263}
3264
3265impl QuietDisplay for CliTransactionConfirmation {}
3266impl VerboseDisplay for CliTransactionConfirmation {
3267 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3268 if let Some(transaction) = &self.transaction {
3269 writeln!(
3270 w,
3271 "\nTransaction executed in slot {}:",
3272 transaction.slot.expect("slot should exist")
3273 )?;
3274 write!(w, "{transaction}")?;
3275 } else if let Some(confirmation_status) = &self.confirmation_status {
3276 if confirmation_status != &TransactionConfirmationStatus::Finalized {
3277 writeln!(w)?;
3278 writeln!(
3279 w,
3280 "Unable to get finalized transaction details: not yet finalized"
3281 )?;
3282 } else if let Some(err) = &self.get_transaction_error {
3283 writeln!(w)?;
3284 writeln!(w, "Unable to get finalized transaction details: {err}")?;
3285 }
3286 }
3287 writeln!(w)?;
3288 write!(w, "{self}")
3289 }
3290}
3291
3292impl fmt::Display for CliTransactionConfirmation {
3293 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3294 match &self.confirmation_status {
3295 None => write!(f, "Not found"),
3296 Some(confirmation_status) => {
3297 if let Some(err) = &self.err {
3298 write!(f, "Transaction failed: {err}")
3299 } else {
3300 write!(f, "{confirmation_status:?}")
3301 }
3302 }
3303 }
3304 }
3305}
3306
3307#[derive(Serialize, Deserialize)]
3308#[serde(rename_all = "camelCase")]
3309pub struct CliGossipNode {
3310 #[serde(skip_serializing_if = "Option::is_none")]
3311 pub ip_address: Option<String>,
3312 #[serde(skip_serializing_if = "Option::is_none")]
3313 pub identity_label: Option<String>,
3314 pub identity_pubkey: String,
3315 #[serde(skip_serializing_if = "Option::is_none")]
3316 pub gossip_port: Option<u16>,
3317 #[serde(skip_serializing_if = "Option::is_none")]
3318 pub tpu_port: Option<u16>,
3319 #[serde(skip_serializing_if = "Option::is_none")]
3320 pub rpc_host: Option<String>,
3321 #[serde(skip_serializing_if = "Option::is_none")]
3322 pub pubsub_host: Option<String>,
3323 #[serde(skip_serializing_if = "Option::is_none")]
3324 pub version: Option<String>,
3325 #[serde(skip_serializing_if = "Option::is_none")]
3326 pub feature_set: Option<u32>,
3327 #[serde(skip_serializing_if = "Option::is_none")]
3328 pub tpu_quic_port: Option<u16>,
3329}
3330
3331impl CliGossipNode {
3332 pub fn new(info: RpcContactInfo, labels: &HashMap<String, String>) -> Self {
3333 Self {
3334 ip_address: info.gossip.map(|addr| addr.ip().to_string()),
3335 identity_label: labels.get(&info.pubkey).cloned(),
3336 identity_pubkey: info.pubkey,
3337 gossip_port: info.gossip.map(|addr| addr.port()),
3338 tpu_port: info.tpu.map(|addr| addr.port()),
3339 rpc_host: info.rpc.map(|addr| addr.to_string()),
3340 pubsub_host: info.pubsub.map(|addr| addr.to_string()),
3341 version: info.version,
3342 feature_set: info.feature_set,
3343 tpu_quic_port: info.tpu_quic.map(|addr| addr.port()),
3344 }
3345 }
3346}
3347
3348fn unwrap_to_string_or_none<T>(option: Option<T>) -> String
3349where
3350 T: std::string::ToString,
3351{
3352 unwrap_to_string_or_default(option, "none")
3353}
3354
3355fn unwrap_to_string_or_default<T>(option: Option<T>, default: &str) -> String
3356where
3357 T: std::string::ToString,
3358{
3359 option
3360 .as_ref()
3361 .map(|v| v.to_string())
3362 .unwrap_or_else(|| default.to_string())
3363}
3364
3365impl fmt::Display for CliGossipNode {
3366 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3367 write!(
3368 f,
3369 "{:15} | {:44} | {:6} | {:5} | {:8} | {:21} | {:8}| {}",
3370 unwrap_to_string_or_none(self.ip_address.as_ref()),
3371 self.identity_label
3372 .as_ref()
3373 .unwrap_or(&self.identity_pubkey),
3374 unwrap_to_string_or_none(self.gossip_port.as_ref()),
3375 unwrap_to_string_or_none(self.tpu_port.as_ref()),
3376 unwrap_to_string_or_none(self.tpu_quic_port.as_ref()),
3377 unwrap_to_string_or_none(self.rpc_host.as_ref()),
3378 unwrap_to_string_or_default(self.version.as_ref(), "unknown"),
3379 unwrap_to_string_or_default(self.feature_set.as_ref(), "unknown"),
3380 )
3381 }
3382}
3383
3384impl QuietDisplay for CliGossipNode {}
3385impl VerboseDisplay for CliGossipNode {}
3386
3387#[derive(Serialize, Deserialize)]
3388pub struct CliGossipNodes(pub Vec<CliGossipNode>);
3389
3390impl fmt::Display for CliGossipNodes {
3391 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3392 writeln!(
3393 f,
3394 "IP Address | Identity \
3395 | Gossip | TPU | TPU-QUIC | RPC Address | Version | Feature Set\n\
3396 ----------------+----------------------------------------------+\
3397 --------+-------+----------+-----------------------+---------+----------------",
3398 )?;
3399 for node in self.0.iter() {
3400 writeln!(f, "{node}")?;
3401 }
3402 writeln!(f, "Nodes: {}", self.0.len())
3403 }
3404}
3405
3406impl QuietDisplay for CliGossipNodes {}
3407impl VerboseDisplay for CliGossipNodes {}
3408
3409#[derive(Serialize, Deserialize)]
3410#[serde(rename_all = "camelCase")]
3411pub struct CliPing {
3412 pub source_pubkey: String,
3413 #[serde(skip_serializing_if = "Option::is_none")]
3414 pub fixed_blockhash: Option<String>,
3415 #[serde(skip_serializing)]
3416 pub blockhash_from_cluster: bool,
3417 pub pings: Vec<CliPingData>,
3418 pub transaction_stats: CliPingTxStats,
3419 #[serde(skip_serializing_if = "Option::is_none")]
3420 pub confirmation_stats: Option<CliPingConfirmationStats>,
3421}
3422
3423impl fmt::Display for CliPing {
3424 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3425 writeln!(f)?;
3426 writeln_name_value(f, "Source Account:", &self.source_pubkey)?;
3427 if let Some(fixed_blockhash) = &self.fixed_blockhash {
3428 let blockhash_origin = if self.blockhash_from_cluster {
3429 "fetched from cluster"
3430 } else {
3431 "supplied from cli arguments"
3432 };
3433 writeln!(
3434 f,
3435 "Fixed blockhash is used: {fixed_blockhash} ({blockhash_origin})"
3436 )?;
3437 }
3438 writeln!(f)?;
3439 for ping in &self.pings {
3440 write!(f, "{ping}")?;
3441 }
3442 writeln!(f)?;
3443 writeln!(f, "--- transaction statistics ---")?;
3444 write!(f, "{}", self.transaction_stats)?;
3445 if let Some(confirmation_stats) = &self.confirmation_stats {
3446 write!(f, "{confirmation_stats}")?;
3447 }
3448 Ok(())
3449 }
3450}
3451
3452impl QuietDisplay for CliPing {}
3453impl VerboseDisplay for CliPing {}
3454
3455#[derive(Serialize, Deserialize)]
3456#[serde(rename_all = "camelCase")]
3457pub struct CliPingData {
3458 pub success: bool,
3459 #[serde(skip_serializing_if = "Option::is_none")]
3460 pub signature: Option<String>,
3461 #[serde(skip_serializing_if = "Option::is_none")]
3462 pub ms: Option<u64>,
3463 #[serde(skip_serializing_if = "Option::is_none")]
3464 pub error: Option<String>,
3465 #[serde(skip_serializing)]
3466 pub print_timestamp: bool,
3467 pub timestamp: String,
3468 pub sequence: u64,
3469 #[serde(skip_serializing_if = "Option::is_none")]
3470 pub lamports: Option<u64>,
3471}
3472impl fmt::Display for CliPingData {
3473 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3474 let (mark, msg) = if let Some(signature) = &self.signature {
3475 if self.success {
3476 (
3477 CHECK_MARK,
3478 format!(
3479 "{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}",
3480 self.lamports.unwrap(),
3481 self.sequence,
3482 self.ms.unwrap(),
3483 signature
3484 ),
3485 )
3486 } else if let Some(error) = &self.error {
3487 (
3488 CROSS_MARK,
3489 format!(
3490 "Transaction failed: seq={:<3} error={:?} signature={}",
3491 self.sequence, error, signature
3492 ),
3493 )
3494 } else {
3495 (
3496 CROSS_MARK,
3497 format!(
3498 "Confirmation timeout: seq={:<3} signature={}",
3499 self.sequence, signature
3500 ),
3501 )
3502 }
3503 } else {
3504 (
3505 CROSS_MARK,
3506 format!(
3507 "Submit failed: seq={:<3} error={:?}",
3508 self.sequence,
3509 self.error.as_ref().unwrap(),
3510 ),
3511 )
3512 };
3513
3514 writeln!(
3515 f,
3516 "{}{}{}",
3517 if self.print_timestamp {
3518 &self.timestamp
3519 } else {
3520 ""
3521 },
3522 mark,
3523 msg
3524 )
3525 }
3526}
3527
3528impl QuietDisplay for CliPingData {}
3529impl VerboseDisplay for CliPingData {}
3530
3531#[derive(Serialize, Deserialize)]
3532#[serde(rename_all = "camelCase")]
3533pub struct CliPingTxStats {
3534 pub num_transactions: u32,
3535 pub num_transaction_confirmed: u32,
3536}
3537impl fmt::Display for CliPingTxStats {
3538 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3539 writeln!(
3540 f,
3541 "{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
3542 self.num_transactions,
3543 self.num_transaction_confirmed,
3544 (100.
3545 - f64::from(self.num_transaction_confirmed) / f64::from(self.num_transactions)
3546 * 100.)
3547 )
3548 }
3549}
3550
3551impl QuietDisplay for CliPingTxStats {}
3552impl VerboseDisplay for CliPingTxStats {}
3553
3554#[derive(Serialize, Deserialize)]
3555#[serde(rename_all = "camelCase")]
3556pub struct CliPingConfirmationStats {
3557 pub min: f64,
3558 pub mean: f64,
3559 pub max: f64,
3560 pub std_dev: f64,
3561}
3562impl fmt::Display for CliPingConfirmationStats {
3563 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3564 writeln!(
3565 f,
3566 "confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
3567 self.min, self.mean, self.max, self.std_dev,
3568 )
3569 }
3570}
3571impl QuietDisplay for CliPingConfirmationStats {}
3572impl VerboseDisplay for CliPingConfirmationStats {}
3573
3574#[derive(Serialize, Deserialize, Debug)]
3575#[serde(rename_all = "camelCase")]
3576pub struct CliBalance {
3577 pub lamports: u64,
3578 #[serde(skip)]
3579 pub config: BuildBalanceMessageConfig,
3580}
3581
3582impl QuietDisplay for CliBalance {
3583 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3584 let config = BuildBalanceMessageConfig {
3585 show_unit: false,
3586 trim_trailing_zeros: true,
3587 ..self.config
3588 };
3589 let balance_message = build_balance_message_with_config(self.lamports, &config);
3590 write!(w, "{balance_message}")
3591 }
3592}
3593
3594impl VerboseDisplay for CliBalance {
3595 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3596 let config = BuildBalanceMessageConfig {
3597 show_unit: true,
3598 trim_trailing_zeros: false,
3599 ..self.config
3600 };
3601 let balance_message = build_balance_message_with_config(self.lamports, &config);
3602 write!(w, "{balance_message}")
3603 }
3604}
3605
3606impl fmt::Display for CliBalance {
3607 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3608 let balance_message = build_balance_message_with_config(self.lamports, &self.config);
3609 write!(f, "{balance_message}")
3610 }
3611}
3612
3613#[derive(Serialize, Deserialize)]
3614#[serde(rename_all = "camelCase")]
3615pub struct CliFindProgramDerivedAddress {
3616 pub address: String,
3617 pub seeds: Vec<Vec<u8>>,
3618 pub bump_seed: u8,
3619}
3620
3621impl QuietDisplay for CliFindProgramDerivedAddress {}
3622impl VerboseDisplay for CliFindProgramDerivedAddress {
3623 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3624 writeln!(w, "Seeds: {:?}", self.seeds)?;
3625 writeln!(w, "Bump: {}", self.bump_seed)?;
3626 write!(w, "{}", self.address)?;
3627 Ok(())
3628 }
3629}
3630
3631impl fmt::Display for CliFindProgramDerivedAddress {
3632 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3633 write!(f, "{}", self.address)?;
3634 Ok(())
3635 }
3636}
3637
3638#[cfg(test)]
3639mod tests {
3640 use {
3641 super::*,
3642 clap::{App, Arg},
3643 solana_keypair::keypair_from_seed,
3644 solana_message::Message,
3645 solana_pubkey::Pubkey,
3646 solana_signature::Signature,
3647 solana_signer::{Signer, SignerError, null_signer::NullSigner},
3648 solana_system_interface::instruction::transfer,
3649 solana_transaction::Transaction,
3650 };
3651
3652 #[test]
3653 fn test_return_signers() {
3654 struct BadSigner {
3655 pubkey: Pubkey,
3656 }
3657
3658 impl BadSigner {
3659 pub fn new(pubkey: Pubkey) -> Self {
3660 Self { pubkey }
3661 }
3662 }
3663
3664 impl Signer for BadSigner {
3665 fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
3666 Ok(self.pubkey)
3667 }
3668
3669 fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
3670 Ok(Signature::from([1u8; 64]))
3671 }
3672
3673 fn is_interactive(&self) -> bool {
3674 false
3675 }
3676 }
3677
3678 let present: Box<dyn Signer> = Box::new(keypair_from_seed(&[2u8; 32]).unwrap());
3679 let absent: Box<dyn Signer> = Box::new(NullSigner::new(&Pubkey::from([3u8; 32])));
3680 let bad: Box<dyn Signer> = Box::new(BadSigner::new(Pubkey::from([4u8; 32])));
3681 let to = Pubkey::from([5u8; 32]);
3682 let nonce = Pubkey::from([6u8; 32]);
3683 let from = present.pubkey();
3684 let fee_payer = absent.pubkey();
3685 let nonce_auth = bad.pubkey();
3686 let mut tx = Transaction::new_unsigned(Message::new_with_nonce(
3687 vec![transfer(&from, &to, 42)],
3688 Some(&fee_payer),
3689 &nonce,
3690 &nonce_auth,
3691 ));
3692
3693 let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()];
3694 let blockhash = Hash::new_from_array([7u8; 32]);
3695 tx.try_partial_sign(&signers, blockhash).unwrap();
3696 let res = return_signers(&tx, &OutputFormat::JsonCompact).unwrap();
3697 let sign_only = parse_sign_only_reply_string(&res);
3698 assert_eq!(sign_only.blockhash, blockhash);
3699 assert_eq!(sign_only.message, None);
3700 assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3701 assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3702 assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3703
3704 let res_data = return_signers_data(&tx, &ReturnSignersConfig::default());
3705 assert_eq!(
3706 res_data,
3707 CliSignOnlyData {
3708 blockhash: blockhash.to_string(),
3709 message: None,
3710 signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3711 absent: vec![absent.pubkey().to_string()],
3712 bad_sig: vec![bad.pubkey().to_string()],
3713 }
3714 );
3715
3716 let expected_msg = "AwECBwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgTl3Dqh9\
3717 F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\
3718 BAQEBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBgYGBgYGBgYGBgYGBgYGBgYG\
3719 BgYGBgYGBgYGBgYGBgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAan1RcZLFaO\
3720 4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH\
3721 BwcCBQMEBgIEBAAAAAUCAQMMAgAAACoAAAAAAAAA"
3722 .to_string();
3723 let config = ReturnSignersConfig {
3724 dump_transaction_message: true,
3725 };
3726 let res = return_signers_with_config(&tx, &OutputFormat::JsonCompact, &config).unwrap();
3727 let sign_only = parse_sign_only_reply_string(&res);
3728 assert_eq!(sign_only.blockhash, blockhash);
3729 assert_eq!(sign_only.message, Some(expected_msg.clone()));
3730 assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3731 assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3732 assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3733
3734 let res_data = return_signers_data(&tx, &config);
3735 assert_eq!(
3736 res_data,
3737 CliSignOnlyData {
3738 blockhash: blockhash.to_string(),
3739 message: Some(expected_msg),
3740 signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3741 absent: vec![absent.pubkey().to_string()],
3742 bad_sig: vec![bad.pubkey().to_string()],
3743 }
3744 );
3745 }
3746
3747 #[test]
3748 fn test_verbose_quiet_output_formats() {
3749 #[derive(Deserialize, Serialize)]
3750 struct FallbackToDisplay {}
3751 impl std::fmt::Display for FallbackToDisplay {
3752 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3753 write!(f, "display")
3754 }
3755 }
3756 impl QuietDisplay for FallbackToDisplay {}
3757 impl VerboseDisplay for FallbackToDisplay {}
3758
3759 let f = FallbackToDisplay {};
3760 assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3761 assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "display");
3762 assert_eq!(
3763 &OutputFormat::DisplayVerbose.formatted_string(&f),
3764 "display"
3765 );
3766
3767 #[derive(Deserialize, Serialize)]
3768 struct DiscreteVerbosityDisplay {}
3769 impl std::fmt::Display for DiscreteVerbosityDisplay {
3770 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3771 write!(f, "display")
3772 }
3773 }
3774 impl QuietDisplay for DiscreteVerbosityDisplay {
3775 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3776 write!(w, "quiet")
3777 }
3778 }
3779 impl VerboseDisplay for DiscreteVerbosityDisplay {
3780 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3781 write!(w, "verbose")
3782 }
3783 }
3784
3785 let f = DiscreteVerbosityDisplay {};
3786 assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3787 assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "quiet");
3788 assert_eq!(
3789 &OutputFormat::DisplayVerbose.formatted_string(&f),
3790 "verbose"
3791 );
3792 }
3793
3794 #[test]
3795 fn test_output_format_from_matches() {
3796 let app = App::new("test").arg(
3797 Arg::with_name("output_format")
3798 .long("output")
3799 .value_name("FORMAT")
3800 .global(true)
3801 .takes_value(true)
3802 .possible_values(&["json", "json-compact"])
3803 .help("Return information in specified output format"),
3804 );
3805 let matches = app
3806 .clone()
3807 .get_matches_from(vec!["test", "--output", "json"]);
3808 assert_eq!(
3809 OutputFormat::from_matches(&matches, "output_format", false),
3810 OutputFormat::Json
3811 );
3812 assert_eq!(
3813 OutputFormat::from_matches(&matches, "output_format", true),
3814 OutputFormat::Json
3815 );
3816
3817 let matches = app
3818 .clone()
3819 .get_matches_from(vec!["test", "--output", "json-compact"]);
3820 assert_eq!(
3821 OutputFormat::from_matches(&matches, "output_format", false),
3822 OutputFormat::JsonCompact
3823 );
3824 assert_eq!(
3825 OutputFormat::from_matches(&matches, "output_format", true),
3826 OutputFormat::JsonCompact
3827 );
3828
3829 let matches = app.clone().get_matches_from(vec!["test"]);
3830 assert_eq!(
3831 OutputFormat::from_matches(&matches, "output_format", false),
3832 OutputFormat::Display
3833 );
3834 assert_eq!(
3835 OutputFormat::from_matches(&matches, "output_format", true),
3836 OutputFormat::DisplayVerbose
3837 );
3838 }
3839
3840 #[test]
3841 fn test_format_vote_account() {
3842 let epoch_rewards = vec![
3843 CliEpochReward {
3844 percent_change: 11.0,
3845 post_balance: 100,
3846 commission: Some(1),
3847 effective_slot: 100,
3848 epoch: 1,
3849 amount: 10,
3850 block_time: 0,
3851 apr: Some(10.0),
3852 },
3853 CliEpochReward {
3854 percent_change: 11.0,
3855 post_balance: 100,
3856 commission: Some(1),
3857 effective_slot: 200,
3858 epoch: 2,
3859 amount: 12,
3860 block_time: 1_000_000,
3861 apr: Some(13.0),
3862 },
3863 ];
3864
3865 let mut c = CliVoteAccount {
3866 account_balance: 10000,
3867 validator_identity: Pubkey::default().to_string(),
3868 epoch_rewards: Some(epoch_rewards),
3869 recent_timestamp: BlockTimestamp::default(),
3870 ..CliVoteAccount::default()
3871 };
3872 #[rustfmt::skip]
3873 let expected_output_common =
3874 "Account Balance: 0.00001 SOL\n\
3875 Validator Identity: 11111111111111111111111111111111\n\
3876 Vote Authority: None\n\
3877 Withdraw Authority: \n\
3878 Credits: 0\n\
3879 Commission: 0%\n\
3880 Inflation Rewards Commission: 0 basis points\n\
3881 Inflation Rewards Collector: \n\
3882 Block Revenue Collector: \n\
3883 Block Revenue Commission: 0 basis points\n\
3884 Pending Delegator Rewards: 0 SOL\n\
3885 Root Slot: ~\n\
3886 Recent Timestamp: 1970-01-01T00:00:00Z from slot 0\n";
3887
3888 let s = format!("{c}");
3889 #[rustfmt::skip]
3890 let expected_epoch_rewards_output =
3891 "Epoch Rewards:\n \
3892 Epoch Reward Slot Time Amount New Balance Percent Change APR Commission\n \
3893 1 100 1970-01-01 00:00:00 UTC ◎0.00000001 ◎0.0000001 11.000% 10.00% 1%\n \
3894 2 200 1970-01-12 13:46:40 UTC ◎0.000000012 ◎0.0000001 11.000% 13.00% 1%\n";
3895 assert_eq!(
3896 s,
3897 format!("{expected_output_common}{expected_epoch_rewards_output}")
3898 );
3899 println!("{s}");
3900
3901 c.use_csv = true;
3902 let s = format!("{c}");
3903 #[rustfmt::skip]
3904 let expected_epoch_rewards_output =
3905 "Epoch Rewards:\n\
3906 Epoch,Reward Slot,Time,Amount,New Balance,Percent Change,APR,Commission\n\
3907 1,100,1970-01-01 00:00:00 UTC,0.00000001,0.0000001,11%,10.00%,1%\n\
3908 2,200,1970-01-12 13:46:40 UTC,0.000000012,0.0000001,11%,13.00%,1%\n";
3909 assert_eq!(
3910 s,
3911 format!("{expected_output_common}{expected_epoch_rewards_output}")
3912 );
3913 println!("{s}");
3914 }
3915}