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