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_decoder::{
20 encode_ui_account, parse_account_data::AccountAdditionalDataV2,
21 parse_token::UiTokenAccount, UiAccountEncoding, UiDataSliceConfig,
22 },
23 solana_clap_utils::keypair::SignOnly,
24 solana_rpc_client_api::response::{
25 RpcAccountBalance, RpcContactInfo, RpcInflationGovernor, RpcInflationRate, RpcKeyedAccount,
26 RpcSupply, RpcVoteAccountInfo,
27 },
28 solana_sdk::{
29 account::ReadableAccount,
30 clock::{Epoch, Slot, UnixTimestamp},
31 epoch_info::EpochInfo,
32 hash::Hash,
33 native_token::lamports_to_sol,
34 pubkey::Pubkey,
35 signature::Signature,
36 stake::state::{Authorized, Lockup},
37 stake_history::StakeHistoryEntry,
38 transaction::{Transaction, TransactionError, VersionedTransaction},
39 },
40 solana_transaction_status::{
41 EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus,
42 UiTransactionStatusMeta,
43 },
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<AccountAdditionalDataV2>,
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<TransactionError>,
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)]
1025#[serde(rename_all = "camelCase")]
1026pub struct CliEpochRewardsMetadata {
1027 pub epoch: Epoch,
1028 pub effective_slot: Slot,
1029 pub block_time: UnixTimestamp,
1030}
1031
1032#[derive(Serialize, Deserialize)]
1033#[serde(rename_all = "camelCase")]
1034pub struct CliKeyedEpochRewards {
1035 #[serde(flatten, skip_serializing_if = "Option::is_none")]
1036 pub epoch_metadata: Option<CliEpochRewardsMetadata>,
1037 pub rewards: Vec<CliKeyedEpochReward>,
1038}
1039
1040impl QuietDisplay for CliKeyedEpochRewards {}
1041impl VerboseDisplay for CliKeyedEpochRewards {}
1042
1043impl fmt::Display for CliKeyedEpochRewards {
1044 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1045 if self.rewards.is_empty() {
1046 writeln!(f, "No rewards found in epoch")?;
1047 return Ok(());
1048 }
1049
1050 if let Some(metadata) = &self.epoch_metadata {
1051 writeln!(f, "Epoch: {}", metadata.epoch)?;
1052 writeln!(f, "Reward Slot: {}", metadata.effective_slot)?;
1053 let timestamp = metadata.block_time;
1054 writeln!(f, "Block Time: {}", unix_timestamp_to_string(timestamp))?;
1055 }
1056 writeln!(f, "Epoch Rewards:")?;
1057 writeln!(
1058 f,
1059 " {:<44} {:<18} {:<18} {:>14} {:>14} {:>10}",
1060 "Address", "Amount", "New Balance", "Percent Change", "APR", "Commission"
1061 )?;
1062 for keyed_reward in &self.rewards {
1063 match &keyed_reward.reward {
1064 Some(reward) => {
1065 writeln!(
1066 f,
1067 " {:<44} ◎{:<17.9} ◎{:<17.9} {:>13.9}% {:>14} {:>10}",
1068 keyed_reward.address,
1069 lamports_to_sol(reward.amount),
1070 lamports_to_sol(reward.post_balance),
1071 reward.percent_change,
1072 reward
1073 .apr
1074 .map(|apr| format!("{apr:.2}%"))
1075 .unwrap_or_default(),
1076 reward
1077 .commission
1078 .map(|commission| format!("{commission}%"))
1079 .unwrap_or_else(|| "-".to_string())
1080 )?;
1081 }
1082 None => {
1083 writeln!(f, " {:<44} No rewards in epoch", keyed_reward.address,)?;
1084 }
1085 }
1086 }
1087 Ok(())
1088 }
1089}
1090
1091fn show_votes_and_credits(
1092 f: &mut fmt::Formatter,
1093 votes: &[CliLandedVote],
1094 epoch_voting_history: &[CliEpochVotingHistory],
1095) -> fmt::Result {
1096 if votes.is_empty() {
1097 return Ok(());
1098 }
1099
1100 let newest_history_entry = epoch_voting_history.iter().next_back();
1102
1103 writeln!(
1104 f,
1105 "{} Votes (using {}/{} entries):",
1106 (if newest_history_entry.is_none() {
1107 "All"
1108 } else {
1109 "Recent"
1110 }),
1111 votes.len(),
1112 MAX_LOCKOUT_HISTORY
1113 )?;
1114
1115 for vote in votes.iter().rev() {
1116 write!(
1117 f,
1118 "- slot: {} (confirmation count: {})",
1119 vote.slot, vote.confirmation_count
1120 )?;
1121 if vote.latency == 0 {
1122 writeln!(f)?;
1123 } else {
1124 writeln!(f, " (latency {})", vote.latency)?;
1125 }
1126 }
1127 if let Some(newest) = newest_history_entry {
1128 writeln!(
1129 f,
1130 "- ... (truncated {} rooted votes, which have been credited)",
1131 newest.credits
1132 )?;
1133 }
1134
1135 if !epoch_voting_history.is_empty() {
1136 writeln!(
1137 f,
1138 "{} Epoch Voting History (using {}/{} entries):",
1139 (if epoch_voting_history.len() < MAX_EPOCH_CREDITS_HISTORY {
1140 "All"
1141 } else {
1142 "Recent"
1143 }),
1144 epoch_voting_history.len(),
1145 MAX_EPOCH_CREDITS_HISTORY
1146 )?;
1147 writeln!(
1148 f,
1149 "* missed credits include slots unavailable to vote on due to delinquent leaders",
1150 )?;
1151 }
1152
1153 for entry in epoch_voting_history.iter().rev() {
1154 writeln!(
1155 f, "- epoch: {}",
1157 entry.epoch
1158 )?;
1159 writeln!(
1160 f,
1161 " credits range: ({}..{}]",
1162 entry.prev_credits, entry.credits
1163 )?;
1164 writeln!(
1165 f,
1166 " credits/max credits: {}/{}",
1167 entry.credits_earned,
1168 entry.slots_in_epoch * u64::from(entry.max_credits_per_slot)
1169 )?;
1170 }
1171 if let Some(oldest) = epoch_voting_history.iter().next() {
1172 if oldest.prev_credits > 0 {
1173 let count = oldest.prev_credits;
1178
1179 writeln!(
1180 f,
1181 "- ... (omitting {count} past rooted votes, which have already been credited)"
1182 )?;
1183 }
1184 }
1185
1186 Ok(())
1187}
1188
1189enum Format {
1190 Csv,
1191 Human,
1192}
1193
1194macro_rules! format_as {
1195 ($target:expr, $fmt1:expr, $fmt2:expr, $which_fmt:expr, $($arg:tt)*) => {
1196 match $which_fmt {
1197 Format::Csv => {
1198 writeln!(
1199 $target,
1200 $fmt1,
1201 $($arg)*
1202 )
1203 },
1204 Format::Human => {
1205 writeln!(
1206 $target,
1207 $fmt2,
1208 $($arg)*
1209 )
1210 }
1211 }
1212 };
1213}
1214
1215fn show_epoch_rewards(
1216 f: &mut fmt::Formatter,
1217 epoch_rewards: &Option<Vec<CliEpochReward>>,
1218 use_csv: bool,
1219) -> fmt::Result {
1220 if let Some(epoch_rewards) = epoch_rewards {
1221 if epoch_rewards.is_empty() {
1222 return Ok(());
1223 }
1224
1225 writeln!(f, "Epoch Rewards:")?;
1226 let fmt = if use_csv { Format::Csv } else { Format::Human };
1227 format_as!(
1228 f,
1229 "{},{},{},{},{},{},{},{}",
1230 " {:<6} {:<11} {:<26} {:<18} {:<18} {:>14} {:>14} {:>10}",
1231 fmt,
1232 "Epoch",
1233 "Reward Slot",
1234 "Time",
1235 "Amount",
1236 "New Balance",
1237 "Percent Change",
1238 "APR",
1239 "Commission",
1240 )?;
1241 for reward in epoch_rewards {
1242 format_as!(
1243 f,
1244 "{},{},{},{},{},{}%,{},{}",
1245 " {:<6} {:<11} {:<26} ◎{:<17.9} ◎{:<17.9} {:>13.3}% {:>14} {:>10}",
1246 fmt,
1247 reward.epoch,
1248 reward.effective_slot,
1249 Utc.timestamp_opt(reward.block_time, 0).unwrap(),
1250 lamports_to_sol(reward.amount),
1251 lamports_to_sol(reward.post_balance),
1252 reward.percent_change,
1253 reward
1254 .apr
1255 .map(|apr| format!("{apr:.2}%"))
1256 .unwrap_or_default(),
1257 reward
1258 .commission
1259 .map(|commission| format!("{commission}%"))
1260 .unwrap_or_else(|| "-".to_string())
1261 )?;
1262 }
1263 }
1264 Ok(())
1265}
1266
1267#[derive(Default, Serialize, Deserialize)]
1268#[serde(rename_all = "camelCase")]
1269pub struct CliStakeState {
1270 pub stake_type: CliStakeType,
1271 pub account_balance: u64,
1272 #[serde(skip_serializing_if = "Option::is_none")]
1273 pub credits_observed: Option<u64>,
1274 #[serde(skip_serializing_if = "Option::is_none")]
1275 pub delegated_stake: Option<u64>,
1276 #[serde(skip_serializing_if = "Option::is_none")]
1277 pub delegated_vote_account_address: Option<String>,
1278 #[serde(skip_serializing_if = "Option::is_none")]
1279 pub activation_epoch: Option<Epoch>,
1280 #[serde(skip_serializing_if = "Option::is_none")]
1281 pub deactivation_epoch: Option<Epoch>,
1282 #[serde(flatten, skip_serializing_if = "Option::is_none")]
1283 pub authorized: Option<CliAuthorized>,
1284 #[serde(flatten, skip_serializing_if = "Option::is_none")]
1285 pub lockup: Option<CliLockup>,
1286 #[serde(skip_serializing)]
1287 pub use_lamports_unit: bool,
1288 #[serde(skip_serializing)]
1289 pub current_epoch: Epoch,
1290 #[serde(skip_serializing_if = "Option::is_none")]
1291 pub rent_exempt_reserve: Option<u64>,
1292 #[serde(skip_serializing_if = "Option::is_none")]
1293 pub active_stake: Option<u64>,
1294 #[serde(skip_serializing_if = "Option::is_none")]
1295 pub activating_stake: Option<u64>,
1296 #[serde(skip_serializing_if = "Option::is_none")]
1297 pub deactivating_stake: Option<u64>,
1298 #[serde(skip_serializing_if = "Option::is_none")]
1299 pub epoch_rewards: Option<Vec<CliEpochReward>>,
1300 #[serde(skip_serializing)]
1301 pub use_csv: bool,
1302}
1303
1304impl QuietDisplay for CliStakeState {}
1305impl VerboseDisplay for CliStakeState {
1306 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
1307 write!(w, "{self}")?;
1308 if let Some(credits) = self.credits_observed {
1309 writeln!(w, "Credits Observed: {credits}")?;
1310 }
1311 Ok(())
1312 }
1313}
1314
1315fn show_inactive_stake(
1316 me: &CliStakeState,
1317 f: &mut fmt::Formatter,
1318 delegated_stake: u64,
1319) -> fmt::Result {
1320 if let Some(deactivation_epoch) = me.deactivation_epoch {
1321 if me.current_epoch > deactivation_epoch {
1322 let deactivating_stake = me.deactivating_stake.or(me.active_stake);
1323 if let Some(deactivating_stake) = deactivating_stake {
1324 writeln!(
1325 f,
1326 "Inactive Stake: {}",
1327 build_balance_message(
1328 delegated_stake - deactivating_stake,
1329 me.use_lamports_unit,
1330 true
1331 ),
1332 )?;
1333 writeln!(
1334 f,
1335 "Deactivating Stake: {}",
1336 build_balance_message(deactivating_stake, me.use_lamports_unit, true),
1337 )?;
1338 }
1339 }
1340 writeln!(
1341 f,
1342 "Stake deactivates starting from epoch: {deactivation_epoch}"
1343 )?;
1344 }
1345 if let Some(delegated_vote_account_address) = &me.delegated_vote_account_address {
1346 writeln!(
1347 f,
1348 "Delegated Vote Account Address: {delegated_vote_account_address}"
1349 )?;
1350 }
1351 Ok(())
1352}
1353
1354fn show_active_stake(
1355 me: &CliStakeState,
1356 f: &mut fmt::Formatter,
1357 delegated_stake: u64,
1358) -> fmt::Result {
1359 if me
1360 .deactivation_epoch
1361 .map(|d| me.current_epoch <= d)
1362 .unwrap_or(true)
1363 {
1364 let active_stake = me.active_stake.unwrap_or(0);
1365 writeln!(
1366 f,
1367 "Active Stake: {}",
1368 build_balance_message(active_stake, me.use_lamports_unit, true),
1369 )?;
1370 let activating_stake = me.activating_stake.or_else(|| {
1371 if me.active_stake.is_none() {
1372 Some(delegated_stake)
1373 } else {
1374 None
1375 }
1376 });
1377 if let Some(activating_stake) = activating_stake {
1378 writeln!(
1379 f,
1380 "Activating Stake: {}",
1381 build_balance_message(activating_stake, me.use_lamports_unit, true),
1382 )?;
1383 writeln!(
1384 f,
1385 "Stake activates starting from epoch: {}",
1386 me.activation_epoch.unwrap()
1387 )?;
1388 }
1389 }
1390 Ok(())
1391}
1392
1393impl fmt::Display for CliStakeState {
1394 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1395 fn show_authorized(f: &mut fmt::Formatter, authorized: &CliAuthorized) -> fmt::Result {
1396 writeln!(f, "Stake Authority: {}", authorized.staker)?;
1397 writeln!(f, "Withdraw Authority: {}", authorized.withdrawer)?;
1398 Ok(())
1399 }
1400 fn show_lockup(f: &mut fmt::Formatter, lockup: Option<&CliLockup>) -> fmt::Result {
1401 if let Some(lockup) = lockup {
1402 if lockup.unix_timestamp != UnixTimestamp::default() {
1403 writeln!(
1404 f,
1405 "Lockup Timestamp: {}",
1406 unix_timestamp_to_string(lockup.unix_timestamp)
1407 )?;
1408 }
1409 if lockup.epoch != Epoch::default() {
1410 writeln!(f, "Lockup Epoch: {}", lockup.epoch)?;
1411 }
1412 writeln!(f, "Lockup Custodian: {}", lockup.custodian)?;
1413 }
1414 Ok(())
1415 }
1416
1417 writeln!(
1418 f,
1419 "Balance: {}",
1420 build_balance_message(self.account_balance, self.use_lamports_unit, true)
1421 )?;
1422
1423 if let Some(rent_exempt_reserve) = self.rent_exempt_reserve {
1424 writeln!(
1425 f,
1426 "Rent Exempt Reserve: {}",
1427 build_balance_message(rent_exempt_reserve, self.use_lamports_unit, true)
1428 )?;
1429 }
1430
1431 match self.stake_type {
1432 CliStakeType::RewardsPool => writeln!(f, "Stake account is a rewards pool")?,
1433 CliStakeType::Uninitialized => writeln!(f, "Stake account is uninitialized")?,
1434 CliStakeType::Initialized => {
1435 writeln!(f, "Stake account is undelegated")?;
1436 show_authorized(f, self.authorized.as_ref().unwrap())?;
1437 show_lockup(f, self.lockup.as_ref())?;
1438 }
1439 CliStakeType::Stake => {
1440 let show_delegation = {
1441 self.active_stake.is_some()
1442 || self.activating_stake.is_some()
1443 || self.deactivating_stake.is_some()
1444 || self
1445 .deactivation_epoch
1446 .map(|de| de > self.current_epoch)
1447 .unwrap_or(true)
1448 };
1449 if show_delegation {
1450 let delegated_stake = self.delegated_stake.unwrap();
1451 writeln!(
1452 f,
1453 "Delegated Stake: {}",
1454 build_balance_message(delegated_stake, self.use_lamports_unit, true)
1455 )?;
1456 show_active_stake(self, f, delegated_stake)?;
1457 show_inactive_stake(self, f, delegated_stake)?;
1458 } else {
1459 writeln!(f, "Stake account is undelegated")?;
1460 }
1461 show_authorized(f, self.authorized.as_ref().unwrap())?;
1462 show_lockup(f, self.lockup.as_ref())?;
1463 show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?
1464 }
1465 }
1466 Ok(())
1467 }
1468}
1469
1470#[derive(Serialize, Deserialize, PartialEq, Eq)]
1471pub enum CliStakeType {
1472 Stake,
1473 RewardsPool,
1474 Uninitialized,
1475 Initialized,
1476}
1477
1478impl Default for CliStakeType {
1479 fn default() -> Self {
1480 Self::Uninitialized
1481 }
1482}
1483
1484#[derive(Serialize, Deserialize)]
1485#[serde(rename_all = "camelCase")]
1486pub struct CliStakeHistory {
1487 pub entries: Vec<CliStakeHistoryEntry>,
1488 #[serde(skip_serializing)]
1489 pub use_lamports_unit: bool,
1490}
1491
1492impl QuietDisplay for CliStakeHistory {}
1493impl VerboseDisplay for CliStakeHistory {}
1494
1495impl fmt::Display for CliStakeHistory {
1496 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1497 writeln!(f)?;
1498 writeln!(
1499 f,
1500 "{}",
1501 style(format!(
1502 " {:<5} {:>20} {:>20} {:>20}",
1503 "Epoch", "Effective Stake", "Activating Stake", "Deactivating Stake",
1504 ))
1505 .bold()
1506 )?;
1507 let config = BuildBalanceMessageConfig {
1508 use_lamports_unit: self.use_lamports_unit,
1509 show_unit: false,
1510 trim_trailing_zeros: false,
1511 };
1512 for entry in &self.entries {
1513 writeln!(
1514 f,
1515 " {:>5} {:>20} {:>20} {:>20} {}",
1516 entry.epoch,
1517 build_balance_message_with_config(entry.effective_stake, &config),
1518 build_balance_message_with_config(entry.activating_stake, &config),
1519 build_balance_message_with_config(entry.deactivating_stake, &config),
1520 if self.use_lamports_unit {
1521 "lamports"
1522 } else {
1523 "SOL"
1524 }
1525 )?;
1526 }
1527 Ok(())
1528 }
1529}
1530
1531impl From<&(Epoch, StakeHistoryEntry)> for CliStakeHistoryEntry {
1532 fn from((epoch, entry): &(Epoch, StakeHistoryEntry)) -> Self {
1533 Self {
1534 epoch: *epoch,
1535 effective_stake: entry.effective,
1536 activating_stake: entry.activating,
1537 deactivating_stake: entry.deactivating,
1538 }
1539 }
1540}
1541
1542#[derive(Serialize, Deserialize)]
1543#[serde(rename_all = "camelCase")]
1544pub struct CliStakeHistoryEntry {
1545 pub epoch: Epoch,
1546 pub effective_stake: u64,
1547 pub activating_stake: u64,
1548 pub deactivating_stake: u64,
1549}
1550
1551#[derive(Serialize, Deserialize)]
1552#[serde(rename_all = "camelCase")]
1553pub struct CliAuthorized {
1554 pub staker: String,
1555 pub withdrawer: String,
1556}
1557
1558impl From<&Authorized> for CliAuthorized {
1559 fn from(authorized: &Authorized) -> Self {
1560 Self {
1561 staker: authorized.staker.to_string(),
1562 withdrawer: authorized.withdrawer.to_string(),
1563 }
1564 }
1565}
1566
1567#[derive(Serialize, Deserialize)]
1568#[serde(rename_all = "camelCase")]
1569pub struct CliLockup {
1570 pub unix_timestamp: UnixTimestamp,
1571 pub epoch: Epoch,
1572 pub custodian: String,
1573}
1574
1575impl From<&Lockup> for CliLockup {
1576 fn from(lockup: &Lockup) -> Self {
1577 Self {
1578 unix_timestamp: lockup.unix_timestamp,
1579 epoch: lockup.epoch,
1580 custodian: lockup.custodian.to_string(),
1581 }
1582 }
1583}
1584
1585#[derive(Serialize, Deserialize)]
1586pub struct CliValidatorInfoVec(Vec<CliValidatorInfo>);
1587
1588impl CliValidatorInfoVec {
1589 pub fn new(list: Vec<CliValidatorInfo>) -> Self {
1590 Self(list)
1591 }
1592}
1593
1594impl QuietDisplay for CliValidatorInfoVec {}
1595impl VerboseDisplay for CliValidatorInfoVec {}
1596
1597impl fmt::Display for CliValidatorInfoVec {
1598 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1599 if self.0.is_empty() {
1600 writeln!(f, "No validator info accounts found")?;
1601 }
1602 for validator_info in &self.0 {
1603 writeln!(f)?;
1604 write!(f, "{validator_info}")?;
1605 }
1606 Ok(())
1607 }
1608}
1609
1610#[derive(Serialize, Deserialize)]
1611#[serde(rename_all = "camelCase")]
1612pub struct CliValidatorInfo {
1613 pub identity_pubkey: String,
1614 pub info_pubkey: String,
1615 pub info: Map<String, Value>,
1616}
1617
1618impl QuietDisplay for CliValidatorInfo {}
1619impl VerboseDisplay for CliValidatorInfo {}
1620
1621impl fmt::Display for CliValidatorInfo {
1622 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1623 writeln_name_value(f, "Validator Identity:", &self.identity_pubkey)?;
1624 writeln_name_value(f, " Info Address:", &self.info_pubkey)?;
1625 for (key, value) in self.info.iter() {
1626 writeln_name_value(
1627 f,
1628 &format!(" {}:", to_title_case(key)),
1629 value.as_str().unwrap_or("?"),
1630 )?;
1631 }
1632 Ok(())
1633 }
1634}
1635
1636#[derive(Default, Serialize, Deserialize)]
1637#[serde(rename_all = "camelCase")]
1638pub struct CliVoteAccount {
1639 pub account_balance: u64,
1640 pub validator_identity: String,
1641 #[serde(flatten)]
1642 pub authorized_voters: CliAuthorizedVoters,
1643 pub authorized_withdrawer: String,
1644 pub credits: u64,
1645 pub commission: u8,
1646 pub root_slot: Option<Slot>,
1647 pub recent_timestamp: BlockTimestamp,
1648 pub votes: Vec<CliLandedVote>,
1649 pub epoch_voting_history: Vec<CliEpochVotingHistory>,
1650 #[serde(skip_serializing)]
1651 pub use_lamports_unit: bool,
1652 #[serde(skip_serializing)]
1653 pub use_csv: bool,
1654 #[serde(skip_serializing_if = "Option::is_none")]
1655 pub epoch_rewards: Option<Vec<CliEpochReward>>,
1656}
1657
1658impl QuietDisplay for CliVoteAccount {}
1659impl VerboseDisplay for CliVoteAccount {}
1660
1661impl fmt::Display for CliVoteAccount {
1662 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1663 writeln!(
1664 f,
1665 "Account Balance: {}",
1666 build_balance_message(self.account_balance, self.use_lamports_unit, true)
1667 )?;
1668 writeln!(f, "Validator Identity: {}", self.validator_identity)?;
1669 writeln!(f, "Vote Authority: {}", self.authorized_voters)?;
1670 writeln!(f, "Withdraw Authority: {}", self.authorized_withdrawer)?;
1671 writeln!(f, "Credits: {}", self.credits)?;
1672 writeln!(f, "Commission: {}%", self.commission)?;
1673 writeln!(
1674 f,
1675 "Root Slot: {}",
1676 match self.root_slot {
1677 Some(slot) => slot.to_string(),
1678 None => "~".to_string(),
1679 }
1680 )?;
1681 writeln!(
1682 f,
1683 "Recent Timestamp: {} from slot {}",
1684 unix_timestamp_to_string(self.recent_timestamp.timestamp),
1685 self.recent_timestamp.slot
1686 )?;
1687 show_votes_and_credits(f, &self.votes, &self.epoch_voting_history)?;
1688 show_epoch_rewards(f, &self.epoch_rewards, self.use_csv)?;
1689 Ok(())
1690 }
1691}
1692
1693#[derive(Default, Debug, Serialize, Deserialize)]
1694#[serde(rename_all = "camelCase")]
1695pub struct CliAuthorizedVoters {
1696 authorized_voters: BTreeMap<Epoch, String>,
1697}
1698
1699impl QuietDisplay for CliAuthorizedVoters {}
1700impl VerboseDisplay for CliAuthorizedVoters {}
1701
1702impl fmt::Display for CliAuthorizedVoters {
1703 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1704 if let Some((_epoch, current_authorized_voter)) = self.authorized_voters.first_key_value() {
1705 write!(f, "{current_authorized_voter}")?;
1706 } else {
1707 write!(f, "None")?;
1708 }
1709 if self.authorized_voters.len() > 1 {
1710 let (epoch, upcoming_authorized_voter) = self
1711 .authorized_voters
1712 .last_key_value()
1713 .expect("CliAuthorizedVoters::authorized_voters.len() > 1");
1714 writeln!(f)?;
1715 write!(
1716 f,
1717 " New Vote Authority as of Epoch {epoch}: {upcoming_authorized_voter}"
1718 )?;
1719 }
1720 Ok(())
1721 }
1722}
1723
1724impl From<&AuthorizedVoters> for CliAuthorizedVoters {
1725 fn from(authorized_voters: &AuthorizedVoters) -> Self {
1726 let mut voter_map: BTreeMap<Epoch, String> = BTreeMap::new();
1727 for (epoch, voter) in authorized_voters.iter() {
1728 voter_map.insert(*epoch, voter.to_string());
1729 }
1730 Self {
1731 authorized_voters: voter_map,
1732 }
1733 }
1734}
1735
1736#[derive(Serialize, Deserialize)]
1737#[serde(rename_all = "camelCase")]
1738pub struct CliEpochVotingHistory {
1739 pub epoch: Epoch,
1740 pub slots_in_epoch: u64,
1741 pub credits_earned: u64,
1742 pub credits: u64,
1743 pub prev_credits: u64,
1744 pub max_credits_per_slot: u8,
1745}
1746
1747#[derive(Serialize, Deserialize)]
1748#[serde(rename_all = "camelCase")]
1749pub struct CliLandedVote {
1750 pub latency: u8,
1751 pub slot: Slot,
1752 pub confirmation_count: u32,
1753}
1754
1755impl From<&LandedVote> for CliLandedVote {
1756 fn from(landed_vote: &LandedVote) -> Self {
1757 Self {
1758 latency: landed_vote.latency,
1759 slot: landed_vote.slot(),
1760 confirmation_count: landed_vote.confirmation_count(),
1761 }
1762 }
1763}
1764
1765#[derive(Serialize, Deserialize)]
1766#[serde(rename_all = "camelCase")]
1767pub struct CliBlockTime {
1768 pub slot: Slot,
1769 pub timestamp: UnixTimestamp,
1770}
1771
1772impl QuietDisplay for CliBlockTime {}
1773impl VerboseDisplay for CliBlockTime {}
1774
1775impl fmt::Display for CliBlockTime {
1776 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1777 writeln_name_value(f, "Block:", &self.slot.to_string())?;
1778 writeln_name_value(f, "Date:", &unix_timestamp_to_string(self.timestamp))
1779 }
1780}
1781
1782#[derive(Serialize, Deserialize)]
1783#[serde(rename_all = "camelCase")]
1784pub struct CliLeaderSchedule {
1785 pub epoch: Epoch,
1786 pub leader_schedule_entries: Vec<CliLeaderScheduleEntry>,
1787}
1788
1789impl QuietDisplay for CliLeaderSchedule {}
1790impl VerboseDisplay for CliLeaderSchedule {}
1791
1792impl fmt::Display for CliLeaderSchedule {
1793 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1794 for entry in &self.leader_schedule_entries {
1795 writeln!(f, " {:<15} {:<44}", entry.slot, entry.leader)?;
1796 }
1797 Ok(())
1798 }
1799}
1800
1801#[derive(Serialize, Deserialize)]
1802#[serde(rename_all = "camelCase")]
1803pub struct CliLeaderScheduleEntry {
1804 pub slot: Slot,
1805 pub leader: String,
1806}
1807
1808#[derive(Serialize, Deserialize)]
1809#[serde(rename_all = "camelCase")]
1810pub struct CliInflation {
1811 pub governor: RpcInflationGovernor,
1812 pub current_rate: RpcInflationRate,
1813}
1814
1815impl QuietDisplay for CliInflation {}
1816impl VerboseDisplay for CliInflation {}
1817
1818impl fmt::Display for CliInflation {
1819 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1820 writeln!(f, "{}", style("Inflation Governor:").bold())?;
1821 if (self.governor.initial - self.governor.terminal).abs() < f64::EPSILON {
1822 writeln!(
1823 f,
1824 "Fixed rate: {:>5.2}%",
1825 self.governor.terminal * 100.
1826 )?;
1827 } else {
1828 writeln!(
1829 f,
1830 "Initial rate: {:>5.2}%",
1831 self.governor.initial * 100.
1832 )?;
1833 writeln!(
1834 f,
1835 "Terminal rate: {:>5.2}%",
1836 self.governor.terminal * 100.
1837 )?;
1838 writeln!(
1839 f,
1840 "Rate reduction per year: {:>5.2}%",
1841 self.governor.taper * 100.
1842 )?;
1843 writeln!(
1844 f,
1845 "* Rate reduction is derived using the target slot time in genesis config"
1846 )?;
1847 }
1848 if self.governor.foundation_term > 0. {
1849 writeln!(
1850 f,
1851 "Foundation percentage: {:>5.2}%",
1852 self.governor.foundation
1853 )?;
1854 writeln!(
1855 f,
1856 "Foundation term: {:.1} years",
1857 self.governor.foundation_term
1858 )?;
1859 }
1860
1861 writeln!(
1862 f,
1863 "\n{}",
1864 style(format!("Inflation for Epoch {}:", self.current_rate.epoch)).bold()
1865 )?;
1866 writeln!(
1867 f,
1868 "Total rate: {:>5.2}%",
1869 self.current_rate.total * 100.
1870 )?;
1871 writeln!(
1872 f,
1873 "Staking rate: {:>5.2}%",
1874 self.current_rate.validator * 100.
1875 )?;
1876
1877 if self.current_rate.foundation > 0. {
1878 writeln!(
1879 f,
1880 "Foundation rate: {:>5.2}%",
1881 self.current_rate.foundation * 100.
1882 )?;
1883 }
1884 Ok(())
1885 }
1886}
1887
1888#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
1889#[serde(rename_all = "camelCase")]
1890pub struct CliSignOnlyData {
1891 pub blockhash: String,
1892 #[serde(skip_serializing_if = "Option::is_none")]
1893 pub message: Option<String>,
1894 #[serde(skip_serializing_if = "Vec::is_empty", default)]
1895 pub signers: Vec<String>,
1896 #[serde(skip_serializing_if = "Vec::is_empty", default)]
1897 pub absent: Vec<String>,
1898 #[serde(skip_serializing_if = "Vec::is_empty", default)]
1899 pub bad_sig: Vec<String>,
1900}
1901
1902impl QuietDisplay for CliSignOnlyData {}
1903impl VerboseDisplay for CliSignOnlyData {}
1904
1905impl fmt::Display for CliSignOnlyData {
1906 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1907 writeln!(f)?;
1908 writeln_name_value(f, "Blockhash:", &self.blockhash)?;
1909 if let Some(message) = self.message.as_ref() {
1910 writeln_name_value(f, "Transaction Message:", message)?;
1911 }
1912 if !self.signers.is_empty() {
1913 writeln!(f, "{}", style("Signers (Pubkey=Signature):").bold())?;
1914 for signer in self.signers.iter() {
1915 writeln!(f, " {signer}")?;
1916 }
1917 }
1918 if !self.absent.is_empty() {
1919 writeln!(f, "{}", style("Absent Signers (Pubkey):").bold())?;
1920 for pubkey in self.absent.iter() {
1921 writeln!(f, " {pubkey}")?;
1922 }
1923 }
1924 if !self.bad_sig.is_empty() {
1925 writeln!(f, "{}", style("Bad Signatures (Pubkey):").bold())?;
1926 for pubkey in self.bad_sig.iter() {
1927 writeln!(f, " {pubkey}")?;
1928 }
1929 }
1930 Ok(())
1931 }
1932}
1933
1934#[derive(Serialize, Deserialize)]
1935#[serde(rename_all = "camelCase")]
1936pub struct CliSignature {
1937 pub signature: String,
1938}
1939
1940impl QuietDisplay for CliSignature {}
1941impl VerboseDisplay for CliSignature {}
1942
1943impl fmt::Display for CliSignature {
1944 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1945 writeln!(f)?;
1946 writeln_name_value(f, "Signature:", &self.signature)?;
1947 Ok(())
1948 }
1949}
1950
1951#[derive(Serialize, Deserialize)]
1952#[serde(rename_all = "camelCase")]
1953pub struct CliAccountBalances {
1954 pub accounts: Vec<RpcAccountBalance>,
1955}
1956
1957impl QuietDisplay for CliAccountBalances {}
1958impl VerboseDisplay for CliAccountBalances {}
1959
1960impl fmt::Display for CliAccountBalances {
1961 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1962 writeln!(
1963 f,
1964 "{}",
1965 style(format!("{:<44} {}", "Address", "Balance")).bold()
1966 )?;
1967 for account in &self.accounts {
1968 writeln!(
1969 f,
1970 "{:<44} {}",
1971 account.address,
1972 &format!("{} SOL", lamports_to_sol(account.lamports))
1973 )?;
1974 }
1975 Ok(())
1976 }
1977}
1978
1979#[derive(Serialize, Deserialize)]
1980#[serde(rename_all = "camelCase")]
1981pub struct CliSupply {
1982 pub total: u64,
1983 pub circulating: u64,
1984 pub non_circulating: u64,
1985 pub non_circulating_accounts: Vec<String>,
1986 #[serde(skip_serializing)]
1987 pub print_accounts: bool,
1988}
1989
1990impl From<RpcSupply> for CliSupply {
1991 fn from(rpc_supply: RpcSupply) -> Self {
1992 Self {
1993 total: rpc_supply.total,
1994 circulating: rpc_supply.circulating,
1995 non_circulating: rpc_supply.non_circulating,
1996 non_circulating_accounts: rpc_supply.non_circulating_accounts,
1997 print_accounts: false,
1998 }
1999 }
2000}
2001
2002impl QuietDisplay for CliSupply {}
2003impl VerboseDisplay for CliSupply {}
2004
2005impl fmt::Display for CliSupply {
2006 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2007 writeln_name_value(f, "Total:", &format!("{} SOL", lamports_to_sol(self.total)))?;
2008 writeln_name_value(
2009 f,
2010 "Circulating:",
2011 &format!("{} SOL", lamports_to_sol(self.circulating)),
2012 )?;
2013 writeln_name_value(
2014 f,
2015 "Non-Circulating:",
2016 &format!("{} SOL", lamports_to_sol(self.non_circulating)),
2017 )?;
2018 if self.print_accounts {
2019 writeln!(f)?;
2020 writeln_name_value(f, "Non-Circulating Accounts:", " ")?;
2021 for account in &self.non_circulating_accounts {
2022 writeln!(f, " {account}")?;
2023 }
2024 }
2025 Ok(())
2026 }
2027}
2028
2029#[derive(Serialize, Deserialize)]
2030#[serde(rename_all = "camelCase")]
2031pub struct CliFeesInner {
2032 pub slot: Slot,
2033 pub blockhash: String,
2034 pub lamports_per_signature: u64,
2035 pub last_valid_slot: Option<Slot>,
2036 pub last_valid_block_height: Option<Slot>,
2037}
2038
2039impl QuietDisplay for CliFeesInner {}
2040impl VerboseDisplay for CliFeesInner {}
2041
2042impl fmt::Display for CliFeesInner {
2043 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2044 writeln_name_value(f, "Blockhash:", &self.blockhash)?;
2045 writeln_name_value(
2046 f,
2047 "Lamports per signature:",
2048 &self.lamports_per_signature.to_string(),
2049 )?;
2050 let last_valid_block_height = self
2051 .last_valid_block_height
2052 .map(|s| s.to_string())
2053 .unwrap_or_default();
2054 writeln_name_value(f, "Last valid block height:", &last_valid_block_height)
2055 }
2056}
2057
2058#[derive(Serialize, Deserialize)]
2059#[serde(rename_all = "camelCase")]
2060pub struct CliFees {
2061 #[serde(flatten, skip_serializing_if = "Option::is_none")]
2062 pub inner: Option<CliFeesInner>,
2063}
2064
2065impl QuietDisplay for CliFees {}
2066impl VerboseDisplay for CliFees {}
2067
2068impl fmt::Display for CliFees {
2069 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2070 match self.inner.as_ref() {
2071 Some(inner) => write!(f, "{inner}"),
2072 None => write!(f, "Fees unavailable"),
2073 }
2074 }
2075}
2076
2077impl CliFees {
2078 pub fn some(
2079 slot: Slot,
2080 blockhash: Hash,
2081 lamports_per_signature: u64,
2082 last_valid_slot: Option<Slot>,
2083 last_valid_block_height: Option<Slot>,
2084 ) -> Self {
2085 Self {
2086 inner: Some(CliFeesInner {
2087 slot,
2088 blockhash: blockhash.to_string(),
2089 lamports_per_signature,
2090 last_valid_slot,
2091 last_valid_block_height,
2092 }),
2093 }
2094 }
2095 pub fn none() -> Self {
2096 Self { inner: None }
2097 }
2098}
2099
2100#[derive(Serialize, Deserialize)]
2101#[serde(rename_all = "camelCase")]
2102pub struct CliTokenAccount {
2103 pub address: String,
2104 #[serde(flatten)]
2105 pub token_account: UiTokenAccount,
2106}
2107
2108impl QuietDisplay for CliTokenAccount {}
2109impl VerboseDisplay for CliTokenAccount {}
2110
2111impl fmt::Display for CliTokenAccount {
2112 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2113 writeln!(f)?;
2114 writeln_name_value(f, "Address:", &self.address)?;
2115 let account = &self.token_account;
2116 writeln_name_value(
2117 f,
2118 "Balance:",
2119 &account.token_amount.real_number_string_trimmed(),
2120 )?;
2121 let mint = format!(
2122 "{}{}",
2123 account.mint,
2124 if account.is_native { " (native)" } else { "" }
2125 );
2126 writeln_name_value(f, "Mint:", &mint)?;
2127 writeln_name_value(f, "Owner:", &account.owner)?;
2128 writeln_name_value(f, "State:", &format!("{:?}", account.state))?;
2129 if let Some(delegate) = &account.delegate {
2130 writeln!(f, "Delegation:")?;
2131 writeln_name_value(f, " Delegate:", delegate)?;
2132 let allowance = account.delegated_amount.as_ref().unwrap();
2133 writeln_name_value(f, " Allowance:", &allowance.real_number_string_trimmed())?;
2134 }
2135 writeln_name_value(
2136 f,
2137 "Close authority:",
2138 account.close_authority.as_ref().unwrap_or(&String::new()),
2139 )?;
2140 Ok(())
2141 }
2142}
2143
2144#[derive(Serialize, Deserialize)]
2145#[serde(rename_all = "camelCase")]
2146pub struct CliProgramId {
2147 pub program_id: String,
2148 pub signature: Option<String>,
2149}
2150
2151impl QuietDisplay for CliProgramId {}
2152impl VerboseDisplay for CliProgramId {}
2153
2154impl fmt::Display for CliProgramId {
2155 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2156 writeln_name_value(f, "Program Id:", &self.program_id)?;
2157 if let Some(ref signature) = self.signature {
2158 writeln!(f)?;
2159 writeln_name_value(f, "Signature:", signature)?;
2160 }
2161 Ok(())
2162 }
2163}
2164
2165#[derive(Serialize, Deserialize)]
2166#[serde(rename_all = "camelCase")]
2167pub struct CliProgramBuffer {
2168 pub buffer: String,
2169}
2170
2171impl QuietDisplay for CliProgramBuffer {}
2172impl VerboseDisplay for CliProgramBuffer {}
2173
2174impl fmt::Display for CliProgramBuffer {
2175 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2176 writeln_name_value(f, "Buffer:", &self.buffer)
2177 }
2178}
2179
2180#[derive(Debug, Serialize, Deserialize)]
2181#[serde(rename_all = "camelCase")]
2182pub enum CliProgramAccountType {
2183 Buffer,
2184 Program,
2185}
2186
2187#[derive(Serialize, Deserialize)]
2188#[serde(rename_all = "camelCase")]
2189pub struct CliProgramAuthority {
2190 pub authority: String,
2191 pub account_type: CliProgramAccountType,
2192}
2193
2194impl QuietDisplay for CliProgramAuthority {}
2195impl VerboseDisplay for CliProgramAuthority {}
2196
2197impl fmt::Display for CliProgramAuthority {
2198 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2199 writeln_name_value(f, "Account Type:", &format!("{:?}", self.account_type))?;
2200 writeln_name_value(f, "Authority:", &self.authority)
2201 }
2202}
2203
2204#[derive(Serialize, Deserialize)]
2205#[serde(rename_all = "camelCase")]
2206pub struct CliProgram {
2207 pub program_id: String,
2208 pub owner: String,
2209 pub data_len: usize,
2210}
2211impl QuietDisplay for CliProgram {}
2212impl VerboseDisplay for CliProgram {}
2213impl fmt::Display for CliProgram {
2214 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2215 writeln!(f)?;
2216 writeln_name_value(f, "Program Id:", &self.program_id)?;
2217 writeln_name_value(f, "Owner:", &self.owner)?;
2218 writeln_name_value(
2219 f,
2220 "Data Length:",
2221 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2222 )?;
2223 Ok(())
2224 }
2225}
2226
2227#[derive(Serialize, Deserialize)]
2228#[serde(rename_all = "camelCase")]
2229pub struct CliProgramV4 {
2230 pub program_id: String,
2231 pub owner: String,
2232 pub authority: String,
2233 pub last_deploy_slot: u64,
2234 pub status: String,
2235 pub data_len: usize,
2236}
2237impl QuietDisplay for CliProgramV4 {}
2238impl VerboseDisplay for CliProgramV4 {}
2239impl fmt::Display for CliProgramV4 {
2240 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2241 writeln!(f)?;
2242 writeln_name_value(f, "Program Id:", &self.program_id)?;
2243 writeln_name_value(f, "Owner:", &self.owner)?;
2244 writeln_name_value(f, "Authority:", &self.authority)?;
2245 writeln_name_value(
2246 f,
2247 "Last Deployed In Slot:",
2248 &self.last_deploy_slot.to_string(),
2249 )?;
2250 writeln_name_value(f, "Status:", &self.status)?;
2251 writeln_name_value(
2252 f,
2253 "Data Length:",
2254 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2255 )?;
2256 Ok(())
2257 }
2258}
2259
2260#[derive(Serialize, Deserialize)]
2261#[serde(rename_all = "camelCase")]
2262pub struct CliProgramsV4 {
2263 pub programs: Vec<CliProgramV4>,
2264}
2265impl QuietDisplay for CliProgramsV4 {}
2266impl VerboseDisplay for CliProgramsV4 {}
2267impl fmt::Display for CliProgramsV4 {
2268 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2269 writeln!(f)?;
2270 writeln!(
2271 f,
2272 "{}",
2273 style(format!(
2274 "{:<44} | {:<9} | {:<44} | {:<10}",
2275 "Program Id", "Slot", "Authority", "Status"
2276 ))
2277 .bold()
2278 )?;
2279 for program in self.programs.iter() {
2280 writeln!(
2281 f,
2282 "{}",
2283 &format!(
2284 "{:<44} | {:<9} | {:<44} | {:<10}",
2285 program.program_id, program.last_deploy_slot, program.authority, program.status,
2286 )
2287 )?;
2288 }
2289 Ok(())
2290 }
2291}
2292
2293#[derive(Serialize, Deserialize)]
2294#[serde(rename_all = "camelCase")]
2295pub struct CliUpgradeableProgram {
2296 pub program_id: String,
2297 pub owner: String,
2298 pub programdata_address: String,
2299 pub authority: String,
2300 pub last_deploy_slot: u64,
2301 pub data_len: usize,
2302 pub lamports: u64,
2303 #[serde(skip_serializing)]
2304 pub use_lamports_unit: bool,
2305}
2306impl QuietDisplay for CliUpgradeableProgram {}
2307impl VerboseDisplay for CliUpgradeableProgram {}
2308impl fmt::Display for CliUpgradeableProgram {
2309 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2310 writeln!(f)?;
2311 writeln_name_value(f, "Program Id:", &self.program_id)?;
2312 writeln_name_value(f, "Owner:", &self.owner)?;
2313 writeln_name_value(f, "ProgramData Address:", &self.programdata_address)?;
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(
2321 f,
2322 "Data Length:",
2323 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2324 )?;
2325 writeln_name_value(
2326 f,
2327 "Balance:",
2328 &build_balance_message(self.lamports, self.use_lamports_unit, true),
2329 )?;
2330 Ok(())
2331 }
2332}
2333
2334#[derive(Serialize, Deserialize)]
2335#[serde(rename_all = "camelCase")]
2336pub struct CliUpgradeablePrograms {
2337 pub programs: Vec<CliUpgradeableProgram>,
2338 #[serde(skip_serializing)]
2339 pub use_lamports_unit: bool,
2340}
2341impl QuietDisplay for CliUpgradeablePrograms {}
2342impl VerboseDisplay for CliUpgradeablePrograms {}
2343impl fmt::Display for CliUpgradeablePrograms {
2344 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2345 writeln!(f)?;
2346 writeln!(
2347 f,
2348 "{}",
2349 style(format!(
2350 "{:<44} | {:<9} | {:<44} | {}",
2351 "Program Id", "Slot", "Authority", "Balance"
2352 ))
2353 .bold()
2354 )?;
2355 for program in self.programs.iter() {
2356 writeln!(
2357 f,
2358 "{}",
2359 &format!(
2360 "{:<44} | {:<9} | {:<44} | {}",
2361 program.program_id,
2362 program.last_deploy_slot,
2363 program.authority,
2364 build_balance_message(program.lamports, self.use_lamports_unit, true)
2365 )
2366 )?;
2367 }
2368 Ok(())
2369 }
2370}
2371
2372#[derive(Serialize, Deserialize)]
2373#[serde(rename_all = "camelCase")]
2374pub struct CliUpgradeableProgramClosed {
2375 pub program_id: String,
2376 pub lamports: u64,
2377 #[serde(skip_serializing)]
2378 pub use_lamports_unit: bool,
2379}
2380impl QuietDisplay for CliUpgradeableProgramClosed {}
2381impl VerboseDisplay for CliUpgradeableProgramClosed {}
2382impl fmt::Display for CliUpgradeableProgramClosed {
2383 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2384 writeln!(f)?;
2385 writeln!(
2386 f,
2387 "Closed Program Id {}, {} reclaimed",
2388 &self.program_id,
2389 &build_balance_message(self.lamports, self.use_lamports_unit, true)
2390 )?;
2391 Ok(())
2392 }
2393}
2394
2395#[derive(Serialize, Deserialize)]
2396#[serde(rename_all = "camelCase")]
2397pub struct CliUpgradeableProgramExtended {
2398 pub program_id: String,
2399 pub additional_bytes: u32,
2400}
2401impl QuietDisplay for CliUpgradeableProgramExtended {}
2402impl VerboseDisplay for CliUpgradeableProgramExtended {}
2403impl fmt::Display for CliUpgradeableProgramExtended {
2404 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2405 writeln!(f)?;
2406 writeln!(
2407 f,
2408 "Extended Program Id {} by {} bytes",
2409 &self.program_id, self.additional_bytes,
2410 )?;
2411 Ok(())
2412 }
2413}
2414
2415#[derive(Clone, Serialize, Deserialize)]
2416#[serde(rename_all = "camelCase")]
2417pub struct CliUpgradeableBuffer {
2418 pub address: String,
2419 pub authority: String,
2420 pub data_len: usize,
2421 pub lamports: u64,
2422 #[serde(skip_serializing)]
2423 pub use_lamports_unit: bool,
2424}
2425impl QuietDisplay for CliUpgradeableBuffer {}
2426impl VerboseDisplay for CliUpgradeableBuffer {}
2427impl fmt::Display for CliUpgradeableBuffer {
2428 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2429 writeln!(f)?;
2430 writeln_name_value(f, "Buffer Address:", &self.address)?;
2431 writeln_name_value(f, "Authority:", &self.authority)?;
2432 writeln_name_value(
2433 f,
2434 "Balance:",
2435 &build_balance_message(self.lamports, self.use_lamports_unit, true),
2436 )?;
2437 writeln_name_value(
2438 f,
2439 "Data Length:",
2440 &format!("{:?} ({:#x?}) bytes", self.data_len, self.data_len),
2441 )?;
2442
2443 Ok(())
2444 }
2445}
2446
2447#[derive(Serialize, Deserialize)]
2448#[serde(rename_all = "camelCase")]
2449pub struct CliUpgradeableBuffers {
2450 pub buffers: Vec<CliUpgradeableBuffer>,
2451 #[serde(skip_serializing)]
2452 pub use_lamports_unit: bool,
2453}
2454impl QuietDisplay for CliUpgradeableBuffers {}
2455impl VerboseDisplay for CliUpgradeableBuffers {}
2456impl fmt::Display for CliUpgradeableBuffers {
2457 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2458 writeln!(f)?;
2459 writeln!(
2460 f,
2461 "{}",
2462 style(format!(
2463 "{:<44} | {:<44} | {}",
2464 "Buffer Address", "Authority", "Balance"
2465 ))
2466 .bold()
2467 )?;
2468 for buffer in self.buffers.iter() {
2469 writeln!(
2470 f,
2471 "{}",
2472 &format!(
2473 "{:<44} | {:<44} | {}",
2474 buffer.address,
2475 buffer.authority,
2476 build_balance_message(buffer.lamports, self.use_lamports_unit, true)
2477 )
2478 )?;
2479 }
2480 Ok(())
2481 }
2482}
2483
2484#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
2485#[serde(rename_all = "camelCase")]
2486pub struct CliAddressLookupTable {
2487 pub lookup_table_address: String,
2488 pub authority: Option<String>,
2489 pub deactivation_slot: u64,
2490 pub last_extended_slot: u64,
2491 pub addresses: Vec<String>,
2492}
2493impl QuietDisplay for CliAddressLookupTable {}
2494impl VerboseDisplay for CliAddressLookupTable {}
2495impl fmt::Display for CliAddressLookupTable {
2496 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2497 writeln!(f)?;
2498 writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
2499 if let Some(authority) = &self.authority {
2500 writeln_name_value(f, "Authority:", authority)?;
2501 } else {
2502 writeln_name_value(f, "Authority:", "None (frozen)")?;
2503 }
2504 if self.deactivation_slot == u64::MAX {
2505 writeln_name_value(f, "Deactivation Slot:", "None (still active)")?;
2506 } else {
2507 writeln_name_value(f, "Deactivation Slot:", &self.deactivation_slot.to_string())?;
2508 }
2509 if self.last_extended_slot == 0 {
2510 writeln_name_value(f, "Last Extended Slot:", "None (empty)")?;
2511 } else {
2512 writeln_name_value(
2513 f,
2514 "Last Extended Slot:",
2515 &self.last_extended_slot.to_string(),
2516 )?;
2517 }
2518 if self.addresses.is_empty() {
2519 writeln_name_value(f, "Address Table Entries:", "None (empty)")?;
2520 } else {
2521 writeln!(f, "{}", style("Address Table Entries:".to_string()).bold())?;
2522 writeln!(f)?;
2523 writeln!(
2524 f,
2525 "{}",
2526 style(format!(" {:<5} {}", "Index", "Address")).bold()
2527 )?;
2528 for (index, address) in self.addresses.iter().enumerate() {
2529 writeln!(f, " {index:<5} {address}")?;
2530 }
2531 }
2532 Ok(())
2533 }
2534}
2535
2536#[derive(Serialize, Deserialize)]
2537#[serde(rename_all = "camelCase")]
2538pub struct CliAddressLookupTableCreated {
2539 pub lookup_table_address: String,
2540 pub signature: String,
2541}
2542impl QuietDisplay for CliAddressLookupTableCreated {}
2543impl VerboseDisplay for CliAddressLookupTableCreated {}
2544impl fmt::Display for CliAddressLookupTableCreated {
2545 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2546 writeln!(f)?;
2547 writeln_name_value(f, "Signature:", &self.signature)?;
2548 writeln_name_value(f, "Lookup Table Address:", &self.lookup_table_address)?;
2549 Ok(())
2550 }
2551}
2552
2553#[derive(Debug, Default)]
2554pub struct ReturnSignersConfig {
2555 pub dump_transaction_message: bool,
2556}
2557
2558pub fn return_signers(
2559 tx: &Transaction,
2560 output_format: &OutputFormat,
2561) -> Result<String, Box<dyn std::error::Error>> {
2562 return_signers_with_config(tx, output_format, &ReturnSignersConfig::default())
2563}
2564
2565pub fn return_signers_with_config(
2566 tx: &Transaction,
2567 output_format: &OutputFormat,
2568 config: &ReturnSignersConfig,
2569) -> Result<String, Box<dyn std::error::Error>> {
2570 let cli_command = return_signers_data(tx, config);
2571 Ok(output_format.formatted_string(&cli_command))
2572}
2573
2574pub fn return_signers_data(tx: &Transaction, config: &ReturnSignersConfig) -> CliSignOnlyData {
2575 let verify_results = tx.verify_with_results();
2576 let mut signers = Vec::new();
2577 let mut absent = Vec::new();
2578 let mut bad_sig = Vec::new();
2579 tx.signatures
2580 .iter()
2581 .zip(tx.message.account_keys.iter())
2582 .zip(verify_results)
2583 .for_each(|((sig, key), res)| {
2584 if res {
2585 signers.push(format!("{key}={sig}"))
2586 } else if *sig == Signature::default() {
2587 absent.push(key.to_string());
2588 } else {
2589 bad_sig.push(key.to_string());
2590 }
2591 });
2592 let message = if config.dump_transaction_message {
2593 let message_data = tx.message_data();
2594 Some(BASE64_STANDARD.encode(message_data))
2595 } else {
2596 None
2597 };
2598
2599 CliSignOnlyData {
2600 blockhash: tx.message.recent_blockhash.to_string(),
2601 message,
2602 signers,
2603 absent,
2604 bad_sig,
2605 }
2606}
2607
2608pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly {
2609 let object: Value = serde_json::from_str(reply).unwrap();
2610 let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
2611 let blockhash = blockhash_str.parse::<Hash>().unwrap();
2612 let mut present_signers: Vec<(Pubkey, Signature)> = Vec::new();
2613 let signer_strings = object.get("signers");
2614 if let Some(sig_strings) = signer_strings {
2615 present_signers = sig_strings
2616 .as_array()
2617 .unwrap()
2618 .iter()
2619 .map(|signer_string| {
2620 let mut signer = signer_string.as_str().unwrap().split('=');
2621 let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
2622 let sig = Signature::from_str(signer.next().unwrap()).unwrap();
2623 (key, sig)
2624 })
2625 .collect();
2626 }
2627 let mut absent_signers: Vec<Pubkey> = Vec::new();
2628 let signer_strings = object.get("absent");
2629 if let Some(sig_strings) = signer_strings {
2630 absent_signers = sig_strings
2631 .as_array()
2632 .unwrap()
2633 .iter()
2634 .map(|val| {
2635 let s = val.as_str().unwrap();
2636 Pubkey::from_str(s).unwrap()
2637 })
2638 .collect();
2639 }
2640 let mut bad_signers: Vec<Pubkey> = Vec::new();
2641 let signer_strings = object.get("badSig");
2642 if let Some(sig_strings) = signer_strings {
2643 bad_signers = sig_strings
2644 .as_array()
2645 .unwrap()
2646 .iter()
2647 .map(|val| {
2648 let s = val.as_str().unwrap();
2649 Pubkey::from_str(s).unwrap()
2650 })
2651 .collect();
2652 }
2653
2654 let message = object
2655 .get("message")
2656 .and_then(|o| o.as_str())
2657 .map(|m| m.to_string());
2658
2659 SignOnly {
2660 blockhash,
2661 message,
2662 present_signers,
2663 absent_signers,
2664 bad_signers,
2665 }
2666}
2667
2668#[derive(Debug, Serialize, Deserialize)]
2669#[serde(rename_all = "camelCase")]
2670pub enum CliSignatureVerificationStatus {
2671 None,
2672 Pass,
2673 Fail,
2674}
2675
2676impl CliSignatureVerificationStatus {
2677 pub fn verify_transaction(tx: &VersionedTransaction) -> Vec<Self> {
2678 tx.verify_with_results()
2679 .iter()
2680 .zip(&tx.signatures)
2681 .map(|(stat, sig)| match stat {
2682 true => CliSignatureVerificationStatus::Pass,
2683 false if sig == &Signature::default() => CliSignatureVerificationStatus::None,
2684 false => CliSignatureVerificationStatus::Fail,
2685 })
2686 .collect()
2687 }
2688}
2689
2690impl fmt::Display for CliSignatureVerificationStatus {
2691 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2692 match self {
2693 Self::None => write!(f, "none"),
2694 Self::Pass => write!(f, "pass"),
2695 Self::Fail => write!(f, "fail"),
2696 }
2697 }
2698}
2699
2700#[derive(Serialize, Deserialize)]
2701#[serde(rename_all = "camelCase")]
2702pub struct CliBlock {
2703 #[serde(flatten)]
2704 pub encoded_confirmed_block: EncodedConfirmedBlock,
2705 #[serde(skip_serializing)]
2706 pub slot: Slot,
2707}
2708
2709impl QuietDisplay for CliBlock {}
2710impl VerboseDisplay for CliBlock {}
2711
2712impl fmt::Display for CliBlock {
2713 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2714 writeln!(f, "Slot: {}", self.slot)?;
2715 writeln!(
2716 f,
2717 "Parent Slot: {}",
2718 self.encoded_confirmed_block.parent_slot
2719 )?;
2720 writeln!(f, "Blockhash: {}", self.encoded_confirmed_block.blockhash)?;
2721 writeln!(
2722 f,
2723 "Previous Blockhash: {}",
2724 self.encoded_confirmed_block.previous_blockhash
2725 )?;
2726 if let Some(block_time) = self.encoded_confirmed_block.block_time {
2727 writeln!(
2728 f,
2729 "Block Time: {:?}",
2730 Local.timestamp_opt(block_time, 0).unwrap()
2731 )?;
2732 }
2733 if let Some(block_height) = self.encoded_confirmed_block.block_height {
2734 writeln!(f, "Block Height: {block_height:?}")?;
2735 }
2736 if !self.encoded_confirmed_block.rewards.is_empty() {
2737 let mut rewards = self.encoded_confirmed_block.rewards.clone();
2738 rewards.sort_by(|a, b| a.pubkey.cmp(&b.pubkey));
2739 let mut total_rewards = 0;
2740 writeln!(f, "Rewards:")?;
2741 writeln!(
2742 f,
2743 " {:<44} {:^15} {:<15} {:<20} {:>14} {:>10}",
2744 "Address", "Type", "Amount", "New Balance", "Percent Change", "Commission"
2745 )?;
2746 for reward in rewards {
2747 let sign = if reward.lamports < 0 { "-" } else { "" };
2748
2749 total_rewards += reward.lamports;
2750 #[allow(clippy::format_in_format_args)]
2751 writeln!(
2752 f,
2753 " {:<44} {:^15} {:>15} {} {}",
2754 reward.pubkey,
2755 if let Some(reward_type) = reward.reward_type {
2756 format!("{reward_type}")
2757 } else {
2758 "-".to_string()
2759 },
2760 format!(
2761 "{}◎{:<14.9}",
2762 sign,
2763 lamports_to_sol(reward.lamports.unsigned_abs())
2764 ),
2765 if reward.post_balance == 0 {
2766 " - -".to_string()
2767 } else {
2768 format!(
2769 "◎{:<19.9} {:>13.9}%",
2770 lamports_to_sol(reward.post_balance),
2771 (reward.lamports.abs() as f64
2772 / (reward.post_balance as f64 - reward.lamports as f64))
2773 * 100.0
2774 )
2775 },
2776 reward
2777 .commission
2778 .map(|commission| format!("{commission:>9}%"))
2779 .unwrap_or_else(|| " -".to_string())
2780 )?;
2781 }
2782
2783 let sign = if total_rewards < 0 { "-" } else { "" };
2784 writeln!(
2785 f,
2786 "Total Rewards: {}◎{:<12.9}",
2787 sign,
2788 lamports_to_sol(total_rewards.unsigned_abs())
2789 )?;
2790 }
2791 for (index, transaction_with_meta) in
2792 self.encoded_confirmed_block.transactions.iter().enumerate()
2793 {
2794 writeln!(f, "Transaction {index}:")?;
2795 writeln_transaction(
2796 f,
2797 &transaction_with_meta.transaction.decode().unwrap(),
2798 transaction_with_meta.meta.as_ref(),
2799 " ",
2800 None,
2801 None,
2802 )?;
2803 }
2804 Ok(())
2805 }
2806}
2807
2808#[derive(Serialize, Deserialize)]
2809#[serde(rename_all = "camelCase")]
2810pub struct CliTransaction {
2811 pub transaction: EncodedTransaction,
2812 pub meta: Option<UiTransactionStatusMeta>,
2813 pub block_time: Option<UnixTimestamp>,
2814 #[serde(skip_serializing_if = "Option::is_none")]
2815 pub slot: Option<Slot>,
2816 #[serde(skip_serializing)]
2817 pub decoded_transaction: VersionedTransaction,
2818 #[serde(skip_serializing)]
2819 pub prefix: String,
2820 #[serde(skip_serializing_if = "Vec::is_empty")]
2821 pub sigverify_status: Vec<CliSignatureVerificationStatus>,
2822}
2823
2824impl QuietDisplay for CliTransaction {}
2825impl VerboseDisplay for CliTransaction {}
2826
2827impl fmt::Display for CliTransaction {
2828 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2829 writeln_transaction(
2830 f,
2831 &self.decoded_transaction,
2832 self.meta.as_ref(),
2833 &self.prefix,
2834 if !self.sigverify_status.is_empty() {
2835 Some(&self.sigverify_status)
2836 } else {
2837 None
2838 },
2839 self.block_time,
2840 )
2841 }
2842}
2843
2844#[derive(Serialize, Deserialize)]
2845#[serde(rename_all = "camelCase")]
2846pub struct CliTransactionConfirmation {
2847 pub confirmation_status: Option<TransactionConfirmationStatus>,
2848 #[serde(flatten, skip_serializing_if = "Option::is_none")]
2849 pub transaction: Option<CliTransaction>,
2850 #[serde(skip_serializing)]
2851 pub get_transaction_error: Option<String>,
2852 #[serde(skip_serializing_if = "Option::is_none")]
2853 pub err: Option<TransactionError>,
2854}
2855
2856impl QuietDisplay for CliTransactionConfirmation {}
2857impl VerboseDisplay for CliTransactionConfirmation {
2858 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
2859 if let Some(transaction) = &self.transaction {
2860 writeln!(
2861 w,
2862 "\nTransaction executed in slot {}:",
2863 transaction.slot.expect("slot should exist")
2864 )?;
2865 write!(w, "{transaction}")?;
2866 } else if let Some(confirmation_status) = &self.confirmation_status {
2867 if confirmation_status != &TransactionConfirmationStatus::Finalized {
2868 writeln!(w)?;
2869 writeln!(
2870 w,
2871 "Unable to get finalized transaction details: not yet finalized"
2872 )?;
2873 } else if let Some(err) = &self.get_transaction_error {
2874 writeln!(w)?;
2875 writeln!(w, "Unable to get finalized transaction details: {err}")?;
2876 }
2877 }
2878 writeln!(w)?;
2879 write!(w, "{self}")
2880 }
2881}
2882
2883impl fmt::Display for CliTransactionConfirmation {
2884 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2885 match &self.confirmation_status {
2886 None => write!(f, "Not found"),
2887 Some(confirmation_status) => {
2888 if let Some(err) = &self.err {
2889 write!(f, "Transaction failed: {err}")
2890 } else {
2891 write!(f, "{confirmation_status:?}")
2892 }
2893 }
2894 }
2895 }
2896}
2897
2898#[derive(Serialize, Deserialize)]
2899#[serde(rename_all = "camelCase")]
2900pub struct CliGossipNode {
2901 #[serde(skip_serializing_if = "Option::is_none")]
2902 pub ip_address: Option<String>,
2903 #[serde(skip_serializing_if = "Option::is_none")]
2904 pub identity_label: Option<String>,
2905 pub identity_pubkey: String,
2906 #[serde(skip_serializing_if = "Option::is_none")]
2907 pub gossip_port: Option<u16>,
2908 #[serde(skip_serializing_if = "Option::is_none")]
2909 pub tpu_port: Option<u16>,
2910 #[serde(skip_serializing_if = "Option::is_none")]
2911 pub rpc_host: Option<String>,
2912 #[serde(skip_serializing_if = "Option::is_none")]
2913 pub pubsub_host: Option<String>,
2914 #[serde(skip_serializing_if = "Option::is_none")]
2915 pub version: Option<String>,
2916 #[serde(skip_serializing_if = "Option::is_none")]
2917 pub feature_set: Option<u32>,
2918 #[serde(skip_serializing_if = "Option::is_none")]
2919 pub tpu_quic_port: Option<u16>,
2920}
2921
2922impl CliGossipNode {
2923 pub fn new(info: RpcContactInfo, labels: &HashMap<String, String>) -> Self {
2924 Self {
2925 ip_address: info.gossip.map(|addr| addr.ip().to_string()),
2926 identity_label: labels.get(&info.pubkey).cloned(),
2927 identity_pubkey: info.pubkey,
2928 gossip_port: info.gossip.map(|addr| addr.port()),
2929 tpu_port: info.tpu.map(|addr| addr.port()),
2930 rpc_host: info.rpc.map(|addr| addr.to_string()),
2931 pubsub_host: info.pubsub.map(|addr| addr.to_string()),
2932 version: info.version,
2933 feature_set: info.feature_set,
2934 tpu_quic_port: info.tpu_quic.map(|addr| addr.port()),
2935 }
2936 }
2937}
2938
2939fn unwrap_to_string_or_none<T>(option: Option<T>) -> String
2940where
2941 T: std::string::ToString,
2942{
2943 unwrap_to_string_or_default(option, "none")
2944}
2945
2946fn unwrap_to_string_or_default<T>(option: Option<T>, default: &str) -> String
2947where
2948 T: std::string::ToString,
2949{
2950 option
2951 .as_ref()
2952 .map(|v| v.to_string())
2953 .unwrap_or_else(|| default.to_string())
2954}
2955
2956impl fmt::Display for CliGossipNode {
2957 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2958 write!(
2959 f,
2960 "{:15} | {:44} | {:6} | {:5} | {:8} | {:21} | {:8}| {}",
2961 unwrap_to_string_or_none(self.ip_address.as_ref()),
2962 self.identity_label
2963 .as_ref()
2964 .unwrap_or(&self.identity_pubkey),
2965 unwrap_to_string_or_none(self.gossip_port.as_ref()),
2966 unwrap_to_string_or_none(self.tpu_port.as_ref()),
2967 unwrap_to_string_or_none(self.tpu_quic_port.as_ref()),
2968 unwrap_to_string_or_none(self.rpc_host.as_ref()),
2969 unwrap_to_string_or_default(self.version.as_ref(), "unknown"),
2970 unwrap_to_string_or_default(self.feature_set.as_ref(), "unknown"),
2971 )
2972 }
2973}
2974
2975impl QuietDisplay for CliGossipNode {}
2976impl VerboseDisplay for CliGossipNode {}
2977
2978#[derive(Serialize, Deserialize)]
2979pub struct CliGossipNodes(pub Vec<CliGossipNode>);
2980
2981impl fmt::Display for CliGossipNodes {
2982 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2983 writeln!(
2984 f,
2985 "IP Address | Identity \
2986 | Gossip | TPU | TPU-QUIC | RPC Address | Version | Feature Set\n\
2987 ----------------+----------------------------------------------+\
2988 --------+-------+----------+-----------------------+---------+----------------",
2989 )?;
2990 for node in self.0.iter() {
2991 writeln!(f, "{node}")?;
2992 }
2993 writeln!(f, "Nodes: {}", self.0.len())
2994 }
2995}
2996
2997impl QuietDisplay for CliGossipNodes {}
2998impl VerboseDisplay for CliGossipNodes {}
2999
3000#[derive(Serialize, Deserialize)]
3001#[serde(rename_all = "camelCase")]
3002pub struct CliPing {
3003 pub source_pubkey: String,
3004 #[serde(skip_serializing_if = "Option::is_none")]
3005 pub fixed_blockhash: Option<String>,
3006 #[serde(skip_serializing)]
3007 pub blockhash_from_cluster: bool,
3008 pub pings: Vec<CliPingData>,
3009 pub transaction_stats: CliPingTxStats,
3010 #[serde(skip_serializing_if = "Option::is_none")]
3011 pub confirmation_stats: Option<CliPingConfirmationStats>,
3012}
3013
3014impl fmt::Display for CliPing {
3015 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3016 writeln!(f)?;
3017 writeln_name_value(f, "Source Account:", &self.source_pubkey)?;
3018 if let Some(fixed_blockhash) = &self.fixed_blockhash {
3019 let blockhash_origin = if self.blockhash_from_cluster {
3020 "fetched from cluster"
3021 } else {
3022 "supplied from cli arguments"
3023 };
3024 writeln!(
3025 f,
3026 "Fixed blockhash is used: {fixed_blockhash} ({blockhash_origin})"
3027 )?;
3028 }
3029 writeln!(f)?;
3030 for ping in &self.pings {
3031 write!(f, "{ping}")?;
3032 }
3033 writeln!(f)?;
3034 writeln!(f, "--- transaction statistics ---")?;
3035 write!(f, "{}", self.transaction_stats)?;
3036 if let Some(confirmation_stats) = &self.confirmation_stats {
3037 write!(f, "{confirmation_stats}")?;
3038 }
3039 Ok(())
3040 }
3041}
3042
3043impl QuietDisplay for CliPing {}
3044impl VerboseDisplay for CliPing {}
3045
3046#[derive(Serialize, Deserialize)]
3047#[serde(rename_all = "camelCase")]
3048pub struct CliPingData {
3049 pub success: bool,
3050 #[serde(skip_serializing_if = "Option::is_none")]
3051 pub signature: Option<String>,
3052 #[serde(skip_serializing_if = "Option::is_none")]
3053 pub ms: Option<u64>,
3054 #[serde(skip_serializing_if = "Option::is_none")]
3055 pub error: Option<String>,
3056 #[serde(skip_serializing)]
3057 pub print_timestamp: bool,
3058 pub timestamp: String,
3059 pub sequence: u64,
3060 #[serde(skip_serializing_if = "Option::is_none")]
3061 pub lamports: Option<u64>,
3062}
3063impl fmt::Display for CliPingData {
3064 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3065 let (mark, msg) = if let Some(signature) = &self.signature {
3066 if self.success {
3067 (
3068 CHECK_MARK,
3069 format!(
3070 "{} lamport(s) transferred: seq={:<3} time={:>4}ms signature={}",
3071 self.lamports.unwrap(),
3072 self.sequence,
3073 self.ms.unwrap(),
3074 signature
3075 ),
3076 )
3077 } else if let Some(error) = &self.error {
3078 (
3079 CROSS_MARK,
3080 format!(
3081 "Transaction failed: seq={:<3} error={:?} signature={}",
3082 self.sequence, error, signature
3083 ),
3084 )
3085 } else {
3086 (
3087 CROSS_MARK,
3088 format!(
3089 "Confirmation timeout: seq={:<3} signature={}",
3090 self.sequence, signature
3091 ),
3092 )
3093 }
3094 } else {
3095 (
3096 CROSS_MARK,
3097 format!(
3098 "Submit failed: seq={:<3} error={:?}",
3099 self.sequence,
3100 self.error.as_ref().unwrap(),
3101 ),
3102 )
3103 };
3104
3105 writeln!(
3106 f,
3107 "{}{}{}",
3108 if self.print_timestamp {
3109 &self.timestamp
3110 } else {
3111 ""
3112 },
3113 mark,
3114 msg
3115 )
3116 }
3117}
3118
3119impl QuietDisplay for CliPingData {}
3120impl VerboseDisplay for CliPingData {}
3121
3122#[derive(Serialize, Deserialize)]
3123#[serde(rename_all = "camelCase")]
3124pub struct CliPingTxStats {
3125 pub num_transactions: u32,
3126 pub num_transaction_confirmed: u32,
3127}
3128impl fmt::Display for CliPingTxStats {
3129 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3130 writeln!(
3131 f,
3132 "{} transactions submitted, {} transactions confirmed, {:.1}% transaction loss",
3133 self.num_transactions,
3134 self.num_transaction_confirmed,
3135 (100.
3136 - f64::from(self.num_transaction_confirmed) / f64::from(self.num_transactions)
3137 * 100.)
3138 )
3139 }
3140}
3141
3142impl QuietDisplay for CliPingTxStats {}
3143impl VerboseDisplay for CliPingTxStats {}
3144
3145#[derive(Serialize, Deserialize)]
3146#[serde(rename_all = "camelCase")]
3147pub struct CliPingConfirmationStats {
3148 pub min: f64,
3149 pub mean: f64,
3150 pub max: f64,
3151 pub std_dev: f64,
3152}
3153impl fmt::Display for CliPingConfirmationStats {
3154 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3155 writeln!(
3156 f,
3157 "confirmation min/mean/max/stddev = {:.0}/{:.0}/{:.0}/{:.0} ms",
3158 self.min, self.mean, self.max, self.std_dev,
3159 )
3160 }
3161}
3162impl QuietDisplay for CliPingConfirmationStats {}
3163impl VerboseDisplay for CliPingConfirmationStats {}
3164
3165#[derive(Serialize, Deserialize, Debug)]
3166#[serde(rename_all = "camelCase")]
3167pub struct CliBalance {
3168 pub lamports: u64,
3169 #[serde(skip)]
3170 pub config: BuildBalanceMessageConfig,
3171}
3172
3173impl QuietDisplay for CliBalance {
3174 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3175 let config = BuildBalanceMessageConfig {
3176 show_unit: false,
3177 trim_trailing_zeros: true,
3178 ..self.config
3179 };
3180 let balance_message = build_balance_message_with_config(self.lamports, &config);
3181 write!(w, "{balance_message}")
3182 }
3183}
3184
3185impl VerboseDisplay for CliBalance {
3186 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3187 let config = BuildBalanceMessageConfig {
3188 show_unit: true,
3189 trim_trailing_zeros: false,
3190 ..self.config
3191 };
3192 let balance_message = build_balance_message_with_config(self.lamports, &config);
3193 write!(w, "{balance_message}")
3194 }
3195}
3196
3197impl fmt::Display for CliBalance {
3198 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3199 let balance_message = build_balance_message_with_config(self.lamports, &self.config);
3200 write!(f, "{balance_message}")
3201 }
3202}
3203
3204#[derive(Serialize, Deserialize)]
3205#[serde(rename_all = "camelCase")]
3206pub struct CliFindProgramDerivedAddress {
3207 pub address: String,
3208 pub bump_seed: u8,
3209}
3210
3211impl QuietDisplay for CliFindProgramDerivedAddress {}
3212impl VerboseDisplay for CliFindProgramDerivedAddress {}
3213
3214impl fmt::Display for CliFindProgramDerivedAddress {
3215 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
3216 write!(f, "{}", self.address)?;
3217 Ok(())
3218 }
3219}
3220
3221#[cfg(test)]
3222mod tests {
3223 use {
3224 super::*,
3225 clap::{App, Arg},
3226 solana_sdk::{
3227 message::Message,
3228 pubkey::Pubkey,
3229 signature::{keypair_from_seed, NullSigner, Signature, Signer, SignerError},
3230 system_instruction,
3231 transaction::Transaction,
3232 },
3233 };
3234
3235 #[test]
3236 fn test_return_signers() {
3237 struct BadSigner {
3238 pubkey: Pubkey,
3239 }
3240
3241 impl BadSigner {
3242 pub fn new(pubkey: Pubkey) -> Self {
3243 Self { pubkey }
3244 }
3245 }
3246
3247 impl Signer for BadSigner {
3248 fn try_pubkey(&self) -> Result<Pubkey, SignerError> {
3249 Ok(self.pubkey)
3250 }
3251
3252 fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
3253 Ok(Signature::from([1u8; 64]))
3254 }
3255
3256 fn is_interactive(&self) -> bool {
3257 false
3258 }
3259 }
3260
3261 let present: Box<dyn Signer> = Box::new(keypair_from_seed(&[2u8; 32]).unwrap());
3262 let absent: Box<dyn Signer> = Box::new(NullSigner::new(&Pubkey::from([3u8; 32])));
3263 let bad: Box<dyn Signer> = Box::new(BadSigner::new(Pubkey::from([4u8; 32])));
3264 let to = Pubkey::from([5u8; 32]);
3265 let nonce = Pubkey::from([6u8; 32]);
3266 let from = present.pubkey();
3267 let fee_payer = absent.pubkey();
3268 let nonce_auth = bad.pubkey();
3269 let mut tx = Transaction::new_unsigned(Message::new_with_nonce(
3270 vec![system_instruction::transfer(&from, &to, 42)],
3271 Some(&fee_payer),
3272 &nonce,
3273 &nonce_auth,
3274 ));
3275
3276 let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()];
3277 let blockhash = Hash::new(&[7u8; 32]);
3278 tx.try_partial_sign(&signers, blockhash).unwrap();
3279 let res = return_signers(&tx, &OutputFormat::JsonCompact).unwrap();
3280 let sign_only = parse_sign_only_reply_string(&res);
3281 assert_eq!(sign_only.blockhash, blockhash);
3282 assert_eq!(sign_only.message, None);
3283 assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3284 assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3285 assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3286
3287 let res_data = return_signers_data(&tx, &ReturnSignersConfig::default());
3288 assert_eq!(
3289 res_data,
3290 CliSignOnlyData {
3291 blockhash: blockhash.to_string(),
3292 message: None,
3293 signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3294 absent: vec![absent.pubkey().to_string()],
3295 bad_sig: vec![bad.pubkey().to_string()],
3296 }
3297 );
3298
3299 let expected_msg = "AwECBwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDgTl3Dqh9\
3300 F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE\
3301 BAQEBAUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBgYGBgYGBgYGBgYGBgYGBgYG\
3302 BgYGBgYGBgYGBgYGBgYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAan1RcZLFaO\
3303 4IqEX3PSl4jPA1wxRbIas0TYBi6pQAAABwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcH\
3304 BwcCBQMEBgIEBAAAAAUCAQMMAgAAACoAAAAAAAAA"
3305 .to_string();
3306 let config = ReturnSignersConfig {
3307 dump_transaction_message: true,
3308 };
3309 let res = return_signers_with_config(&tx, &OutputFormat::JsonCompact, &config).unwrap();
3310 let sign_only = parse_sign_only_reply_string(&res);
3311 assert_eq!(sign_only.blockhash, blockhash);
3312 assert_eq!(sign_only.message, Some(expected_msg.clone()));
3313 assert_eq!(sign_only.present_signers[0].0, present.pubkey());
3314 assert_eq!(sign_only.absent_signers[0], absent.pubkey());
3315 assert_eq!(sign_only.bad_signers[0], bad.pubkey());
3316
3317 let res_data = return_signers_data(&tx, &config);
3318 assert_eq!(
3319 res_data,
3320 CliSignOnlyData {
3321 blockhash: blockhash.to_string(),
3322 message: Some(expected_msg),
3323 signers: vec![format!("{}={}", present.pubkey(), tx.signatures[1])],
3324 absent: vec![absent.pubkey().to_string()],
3325 bad_sig: vec![bad.pubkey().to_string()],
3326 }
3327 );
3328 }
3329
3330 #[test]
3331 fn test_verbose_quiet_output_formats() {
3332 #[derive(Deserialize, Serialize)]
3333 struct FallbackToDisplay {}
3334 impl std::fmt::Display for FallbackToDisplay {
3335 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3336 write!(f, "display")
3337 }
3338 }
3339 impl QuietDisplay for FallbackToDisplay {}
3340 impl VerboseDisplay for FallbackToDisplay {}
3341
3342 let f = FallbackToDisplay {};
3343 assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3344 assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "display");
3345 assert_eq!(
3346 &OutputFormat::DisplayVerbose.formatted_string(&f),
3347 "display"
3348 );
3349
3350 #[derive(Deserialize, Serialize)]
3351 struct DiscreteVerbosityDisplay {}
3352 impl std::fmt::Display for DiscreteVerbosityDisplay {
3353 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3354 write!(f, "display")
3355 }
3356 }
3357 impl QuietDisplay for DiscreteVerbosityDisplay {
3358 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3359 write!(w, "quiet")
3360 }
3361 }
3362 impl VerboseDisplay for DiscreteVerbosityDisplay {
3363 fn write_str(&self, w: &mut dyn std::fmt::Write) -> std::fmt::Result {
3364 write!(w, "verbose")
3365 }
3366 }
3367
3368 let f = DiscreteVerbosityDisplay {};
3369 assert_eq!(&OutputFormat::Display.formatted_string(&f), "display");
3370 assert_eq!(&OutputFormat::DisplayQuiet.formatted_string(&f), "quiet");
3371 assert_eq!(
3372 &OutputFormat::DisplayVerbose.formatted_string(&f),
3373 "verbose"
3374 );
3375 }
3376
3377 #[test]
3378 fn test_output_format_from_matches() {
3379 let app = App::new("test").arg(
3380 Arg::with_name("output_format")
3381 .long("output")
3382 .value_name("FORMAT")
3383 .global(true)
3384 .takes_value(true)
3385 .possible_values(&["json", "json-compact"])
3386 .help("Return information in specified output format"),
3387 );
3388 let matches = app
3389 .clone()
3390 .get_matches_from(vec!["test", "--output", "json"]);
3391 assert_eq!(
3392 OutputFormat::from_matches(&matches, "output_format", false),
3393 OutputFormat::Json
3394 );
3395 assert_eq!(
3396 OutputFormat::from_matches(&matches, "output_format", true),
3397 OutputFormat::Json
3398 );
3399
3400 let matches = app
3401 .clone()
3402 .get_matches_from(vec!["test", "--output", "json-compact"]);
3403 assert_eq!(
3404 OutputFormat::from_matches(&matches, "output_format", false),
3405 OutputFormat::JsonCompact
3406 );
3407 assert_eq!(
3408 OutputFormat::from_matches(&matches, "output_format", true),
3409 OutputFormat::JsonCompact
3410 );
3411
3412 let matches = app.clone().get_matches_from(vec!["test"]);
3413 assert_eq!(
3414 OutputFormat::from_matches(&matches, "output_format", false),
3415 OutputFormat::Display
3416 );
3417 assert_eq!(
3418 OutputFormat::from_matches(&matches, "output_format", true),
3419 OutputFormat::DisplayVerbose
3420 );
3421 }
3422
3423 #[test]
3424 fn test_format_vote_account() {
3425 let epoch_rewards = vec![
3426 CliEpochReward {
3427 percent_change: 11.0,
3428 post_balance: 100,
3429 commission: Some(1),
3430 effective_slot: 100,
3431 epoch: 1,
3432 amount: 10,
3433 block_time: 0,
3434 apr: Some(10.0),
3435 },
3436 CliEpochReward {
3437 percent_change: 11.0,
3438 post_balance: 100,
3439 commission: Some(1),
3440 effective_slot: 200,
3441 epoch: 2,
3442 amount: 12,
3443 block_time: 1_000_000,
3444 apr: Some(13.0),
3445 },
3446 ];
3447
3448 let mut c = CliVoteAccount {
3449 account_balance: 10000,
3450 validator_identity: Pubkey::default().to_string(),
3451 epoch_rewards: Some(epoch_rewards),
3452 recent_timestamp: BlockTimestamp::default(),
3453 ..CliVoteAccount::default()
3454 };
3455 let s = format!("{c}");
3456 assert_eq!(s, "Account Balance: 0.00001 SOL\nValidator Identity: 11111111111111111111111111111111\nVote Authority: None\nWithdraw Authority: \nCredits: 0\nCommission: 0%\nRoot Slot: ~\nRecent Timestamp: 1970-01-01T00:00:00Z from slot 0\nEpoch Rewards:\n Epoch Reward Slot Time Amount New Balance Percent Change APR Commission\n 1 100 1970-01-01 00:00:00 UTC ◎0.000000010 ◎0.000000100 11.000% 10.00% 1%\n 2 200 1970-01-12 13:46:40 UTC ◎0.000000012 ◎0.000000100 11.000% 13.00% 1%\n");
3457 println!("{s}");
3458
3459 c.use_csv = true;
3460 let s = format!("{c}");
3461 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");
3462 println!("{s}");
3463 }
3464}