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