1use {
2 crate::{
3 cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult},
4 compute_budget::{
5 simulate_for_compute_unit_limit, ComputeUnitConfig, WithComputeUnitConfig,
6 },
7 feature::get_feature_activation_epoch,
8 spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
9 },
10 clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand},
11 console::style,
12 crossbeam_channel::unbounded,
13 serde::{Deserialize, Serialize},
14 solana_account::{from_account, state_traits::StateMut, Account},
15 solana_clap_utils::{
16 compute_budget::{compute_unit_price_arg, ComputeUnitLimit, COMPUTE_UNIT_PRICE_ARG},
17 input_parsers::*,
18 input_validators::*,
19 keypair::DefaultSigner,
20 offline::{blockhash_arg, BLOCKHASH_ARG},
21 },
22 solana_cli_output::{
23 cli_version::CliVersion,
24 display::{
25 build_balance_message, format_labeled_address, new_spinner_progress_bar,
26 writeln_name_value,
27 },
28 *,
29 },
30 solana_clock::{self as clock, Clock, Epoch, Slot},
31 solana_commitment_config::CommitmentConfig,
32 solana_hash::Hash,
33 solana_message::Message,
34 solana_nonce::state::State as NonceState,
35 solana_pubkey::Pubkey,
36 solana_pubsub_client::pubsub_client::PubsubClient,
37 solana_remote_wallet::remote_wallet::RemoteWalletManager,
38 solana_rent::Rent,
39 solana_rpc_client::rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClient},
40 solana_rpc_client_api::{
41 client_error::ErrorKind as ClientErrorKind,
42 config::{
43 RpcAccountInfoConfig, RpcBlockConfig, RpcGetVoteAccountsConfig,
44 RpcLargestAccountsConfig, RpcLargestAccountsFilter, RpcProgramAccountsConfig,
45 RpcTransactionConfig, RpcTransactionLogsConfig, RpcTransactionLogsFilter,
46 },
47 filter::{Memcmp, RpcFilterType},
48 request::DELINQUENT_VALIDATOR_SLOT_DISTANCE,
49 response::{RpcPerfSample, RpcPrioritizationFee, SlotInfo},
50 },
51 solana_sdk_ids::sysvar::{self, stake_history},
52 solana_signature::Signature,
53 solana_slot_history::{self as slot_history, SlotHistory},
54 solana_stake_interface::{self as stake, state::StakeStateV2},
55 solana_system_interface::{instruction as system_instruction, MAX_PERMITTED_DATA_LENGTH},
56 solana_tps_client::TpsClient,
57 solana_transaction::Transaction,
58 solana_transaction_status::{
59 EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, UiTransactionEncoding,
60 },
61 solana_vote_program::vote_state::VoteStateV4,
62 std::{
63 collections::{BTreeMap, HashMap, HashSet, VecDeque},
64 fmt,
65 num::Saturating,
66 rc::Rc,
67 str::FromStr,
68 sync::{
69 atomic::{AtomicBool, Ordering},
70 Arc,
71 },
72 thread::sleep,
73 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
74 },
75 thiserror::Error,
76};
77
78const DEFAULT_RPC_PORT_STR: &str = "8899";
79
80pub trait ClusterQuerySubCommands {
81 fn cluster_query_subcommands(self) -> Self;
82}
83
84impl ClusterQuerySubCommands for App<'_, '_> {
85 fn cluster_query_subcommands(self) -> Self {
86 self.subcommand(
87 SubCommand::with_name("block")
88 .about("Get a confirmed block")
89 .arg(
90 Arg::with_name("slot")
91 .long("slot")
92 .validator(is_slot)
93 .value_name("SLOT")
94 .takes_value(true)
95 .index(1),
96 ),
97 )
98 .subcommand(
99 SubCommand::with_name("recent-prioritization-fees")
100 .about("Get recent prioritization fees")
101 .arg(
102 Arg::with_name("accounts")
103 .value_name("ACCOUNTS")
104 .takes_value(true)
105 .multiple(true)
106 .index(1)
107 .help(
108 "A list of accounts which if provided the fee response will represent \
109 the fee to land a transaction with those accounts as writable",
110 ),
111 )
112 .arg(
113 Arg::with_name("limit_num_slots")
114 .long("limit-num-slots")
115 .value_name("SLOTS")
116 .takes_value(true)
117 .help("Limit the number of slots to the last <N> slots"),
118 ),
119 )
120 .subcommand(
121 SubCommand::with_name("catchup")
122 .about("Wait for a validator to catch up to the cluster")
123 .arg(pubkey!(
124 Arg::with_name("node_pubkey")
125 .index(1)
126 .value_name("OUR_VALIDATOR_PUBKEY")
127 .required(false),
128 "Identity of the validator."
129 ))
130 .arg(
131 Arg::with_name("node_json_rpc_url")
132 .index(2)
133 .value_name("OUR_URL")
134 .takes_value(true)
135 .validator(is_url)
136 .help(
137 "JSON RPC URL for validator, which is useful for validators with a \
138 private RPC service",
139 ),
140 )
141 .arg(
142 Arg::with_name("follow")
143 .long("follow")
144 .takes_value(false)
145 .help("Continue reporting progress even after the validator has caught up"),
146 )
147 .arg(
148 Arg::with_name("our_localhost")
149 .long("our-localhost")
150 .takes_value(false)
151 .value_name("PORT")
152 .default_value(DEFAULT_RPC_PORT_STR)
153 .validator(is_port)
154 .help(
155 "Guess Identity pubkey and validator rpc node assuming local \
156 (possibly private) validator",
157 ),
158 )
159 .arg(Arg::with_name("log").long("log").takes_value(false).help(
160 "Don't update the progress inplace; instead show updates with its own new \
161 lines",
162 )),
163 )
164 .subcommand(SubCommand::with_name("cluster-date").about(
165 "Get current cluster date, computed from genesis creation time and network time",
166 ))
167 .subcommand(
168 SubCommand::with_name("cluster-version")
169 .about("Get the version of the cluster entrypoint"),
170 )
171 .subcommand(
172 SubCommand::with_name("first-available-block")
173 .about("Get the first available block in the storage"),
174 )
175 .subcommand(
176 SubCommand::with_name("block-time")
177 .about("Get estimated production time of a block")
178 .alias("get-block-time")
179 .arg(
180 Arg::with_name("slot")
181 .index(1)
182 .takes_value(true)
183 .value_name("SLOT")
184 .help("Slot number of the block to query"),
185 ),
186 )
187 .subcommand(
188 SubCommand::with_name("leader-schedule")
189 .about("Display leader schedule")
190 .arg(
191 Arg::with_name("epoch")
192 .long("epoch")
193 .takes_value(true)
194 .value_name("EPOCH")
195 .validator(is_epoch)
196 .help("Epoch to show leader schedule for [default: current]"),
197 ),
198 )
199 .subcommand(
200 SubCommand::with_name("epoch-info")
201 .about("Get information about the current epoch")
202 .alias("get-epoch-info"),
203 )
204 .subcommand(
205 SubCommand::with_name("genesis-hash")
206 .about("Get the genesis hash")
207 .alias("get-genesis-hash"),
208 )
209 .subcommand(
210 SubCommand::with_name("slot")
211 .about("Get current slot")
212 .alias("get-slot"),
213 )
214 .subcommand(SubCommand::with_name("block-height").about("Get current block height"))
215 .subcommand(SubCommand::with_name("epoch").about("Get current epoch"))
216 .subcommand(
217 SubCommand::with_name("largest-accounts")
218 .about("Get addresses of largest cluster accounts")
219 .arg(
220 Arg::with_name("circulating")
221 .long("circulating")
222 .takes_value(false)
223 .help("Filter address list to only circulating accounts"),
224 )
225 .arg(
226 Arg::with_name("non_circulating")
227 .long("non-circulating")
228 .takes_value(false)
229 .conflicts_with("circulating")
230 .help("Filter address list to only non-circulating accounts"),
231 ),
232 )
233 .subcommand(
234 SubCommand::with_name("supply")
235 .about("Get information about the cluster supply of SOL")
236 .arg(
237 Arg::with_name("print_accounts")
238 .long("print-accounts")
239 .takes_value(false)
240 .help("Print list of non-circulating account addresses"),
241 ),
242 )
243 .subcommand(
244 SubCommand::with_name("total-supply")
245 .about("Get total number of SOL")
246 .setting(AppSettings::Hidden),
247 )
248 .subcommand(
249 SubCommand::with_name("transaction-count")
250 .about("Get current transaction count")
251 .alias("get-transaction-count"),
252 )
253 .subcommand(
254 SubCommand::with_name("ping")
255 .about("Submit transactions sequentially")
256 .arg(
257 Arg::with_name("interval")
258 .short("i")
259 .long("interval")
260 .value_name("SECONDS")
261 .takes_value(true)
262 .default_value("2")
263 .help("Wait interval seconds between submitting the next transaction"),
264 )
265 .arg(
266 Arg::with_name("count")
267 .short("c")
268 .long("count")
269 .value_name("NUMBER")
270 .takes_value(true)
271 .help("Stop after submitting count transactions"),
272 )
273 .arg(
274 Arg::with_name("print_timestamp")
275 .short("D")
276 .long("print-timestamp")
277 .takes_value(false)
278 .help(
279 "Print timestamp (unix time + microseconds as in gettimeofday) before \
280 each line",
281 ),
282 )
283 .arg(
284 Arg::with_name("timeout")
285 .short("t")
286 .long("timeout")
287 .value_name("SECONDS")
288 .takes_value(true)
289 .default_value("15")
290 .help("Wait up to timeout seconds for transaction confirmation"),
291 )
292 .arg(compute_unit_price_arg())
293 .arg(blockhash_arg()),
294 )
295 .subcommand(
296 SubCommand::with_name("live-slots")
297 .about("Show information about the current slot progression"),
298 )
299 .subcommand(
300 SubCommand::with_name("logs")
301 .about("Stream transaction logs")
302 .arg(pubkey!(
303 Arg::with_name("address").index(1).value_name("ADDRESS"),
304 "Account to monitor [default: monitor all transactions except for votes]."
305 ))
306 .arg(
307 Arg::with_name("include_votes")
308 .long("include-votes")
309 .takes_value(false)
310 .conflicts_with("address")
311 .help("Include vote transactions when monitoring all transactions"),
312 ),
313 )
314 .subcommand(
315 SubCommand::with_name("block-production")
316 .about("Show information about block production")
317 .alias("show-block-production")
318 .arg(
319 Arg::with_name("epoch")
320 .long("epoch")
321 .takes_value(true)
322 .help("Epoch to show block production for [default: current epoch]"),
323 )
324 .arg(
325 Arg::with_name("slot_limit")
326 .long("slot-limit")
327 .takes_value(true)
328 .help(
329 "Limit results to this many slots from the end of the epoch [default: \
330 full epoch]",
331 ),
332 ),
333 )
334 .subcommand(
335 SubCommand::with_name("gossip")
336 .about("Show the current gossip network nodes")
337 .alias("show-gossip"),
338 )
339 .subcommand(
340 SubCommand::with_name("stakes")
341 .about("Show stake account information")
342 .arg(
343 Arg::with_name("lamports")
344 .long("lamports")
345 .takes_value(false)
346 .help("Display balance in lamports instead of SOL"),
347 )
348 .arg(pubkey!(
349 Arg::with_name("vote_account_pubkeys")
350 .index(1)
351 .value_name("VALIDATOR_ACCOUNT_PUBKEYS")
352 .multiple(true),
353 "Only show stake accounts delegated to the provided pubkeys. Accepts both \
354 vote and identity pubkeys."
355 ))
356 .arg(pubkey!(
357 Arg::with_name("withdraw_authority")
358 .value_name("PUBKEY")
359 .long("withdraw-authority"),
360 "Only show stake accounts with the provided withdraw authority."
361 )),
362 )
363 .subcommand(
364 SubCommand::with_name("validators")
365 .about("Show summary information about the current validators")
366 .alias("show-validators")
367 .arg(
368 Arg::with_name("lamports")
369 .long("lamports")
370 .takes_value(false)
371 .help("Display balance in lamports instead of SOL"),
372 )
373 .arg(
374 Arg::with_name("number")
375 .long("number")
376 .short("n")
377 .takes_value(false)
378 .help("Number the validators"),
379 )
380 .arg(
381 Arg::with_name("reverse")
382 .long("reverse")
383 .short("r")
384 .takes_value(false)
385 .help("Reverse order while sorting"),
386 )
387 .arg(
388 Arg::with_name("sort")
389 .long("sort")
390 .takes_value(true)
391 .possible_values(&[
392 "delinquent",
393 "commission",
394 "credits",
395 "identity",
396 "last-vote",
397 "root",
398 "skip-rate",
399 "stake",
400 "version",
401 "vote-account",
402 ])
403 .default_value("stake")
404 .help("Sort order (does not affect JSON output)"),
405 )
406 .arg(
407 Arg::with_name("keep_unstaked_delinquents")
408 .long("keep-unstaked-delinquents")
409 .takes_value(false)
410 .help("Don't discard unstaked, delinquent validators"),
411 )
412 .arg(
413 Arg::with_name("delinquent_slot_distance")
414 .long("delinquent-slot-distance")
415 .takes_value(true)
416 .value_name("SLOT_DISTANCE")
417 .validator(is_slot)
418 .help(concatcp!(
419 "Minimum slot distance from the tip to consider a validator \
420 delinquent [default: ",
421 DELINQUENT_VALIDATOR_SLOT_DISTANCE,
422 "]",
423 )),
424 ),
425 )
426 .subcommand(
427 SubCommand::with_name("transaction-history")
428 .about(
429 "Show historical transactions affecting the given address from newest to \
430 oldest",
431 )
432 .arg(pubkey!(
433 Arg::with_name("address")
434 .index(1)
435 .value_name("ADDRESS")
436 .required(true),
437 "Account to query for transactions."
438 ))
439 .arg(
440 Arg::with_name("limit")
441 .long("limit")
442 .takes_value(true)
443 .value_name("LIMIT")
444 .validator(is_slot)
445 .default_value("1000")
446 .help("Maximum number of transaction signatures to return"),
447 )
448 .arg(
449 Arg::with_name("before")
450 .long("before")
451 .value_name("TRANSACTION_SIGNATURE")
452 .takes_value(true)
453 .help("Start with the first signature older than this one"),
454 )
455 .arg(
456 Arg::with_name("until")
457 .long("until")
458 .value_name("TRANSACTION_SIGNATURE")
459 .takes_value(true)
460 .help(
461 "List until this transaction signature, if found before limit reached",
462 ),
463 )
464 .arg(
465 Arg::with_name("show_transactions")
466 .long("show-transactions")
467 .takes_value(false)
468 .help("Display the full transactions"),
469 ),
470 )
471 .subcommand(
472 SubCommand::with_name("wait-for-max-stake")
473 .about(
474 "Wait for the max stake of any one node to drop below a percentage of total.",
475 )
476 .arg(
477 Arg::with_name("max_percent")
478 .long("max-percent")
479 .value_name("PERCENT")
480 .takes_value(true)
481 .index(1),
482 ),
483 )
484 .subcommand(
485 SubCommand::with_name("rent")
486 .about("Calculate rent-exempt-minimum value for a given account data field length.")
487 .arg(
488 Arg::with_name("data_length")
489 .index(1)
490 .value_name("DATA_LENGTH_OR_MONIKER")
491 .required(true)
492 .validator(|s| {
493 RentLengthValue::from_str(&s)
494 .map(|_| ())
495 .map_err(|e| e.to_string())
496 })
497 .help(
498 "Length of data field in the account to calculate rent for, or \
499 moniker: [nonce, stake, system, vote]",
500 ),
501 )
502 .arg(
503 Arg::with_name("lamports")
504 .long("lamports")
505 .takes_value(false)
506 .help("Display rent in lamports instead of SOL"),
507 ),
508 )
509 }
510}
511
512pub fn parse_catchup(
513 matches: &ArgMatches<'_>,
514 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
515) -> Result<CliCommandInfo, CliError> {
516 let node_pubkey = pubkey_of_signer(matches, "node_pubkey", wallet_manager)?;
517 let mut our_localhost_port = value_t!(matches, "our_localhost", u16).ok();
518 if matches.occurrences_of("our_localhost") == 0 {
521 our_localhost_port = None
522 }
523 let node_json_rpc_url = value_t!(matches, "node_json_rpc_url", String).ok();
524 if our_localhost_port.is_none() && node_pubkey.is_none() {
526 return Err(CliError::BadParameter(
527 "OUR_VALIDATOR_PUBKEY (and possibly OUR_URL) must be specified unless --our-localhost \
528 is given"
529 .into(),
530 ));
531 }
532 let follow = matches.is_present("follow");
533 let log = matches.is_present("log");
534 Ok(CliCommandInfo::without_signers(CliCommand::Catchup {
535 node_pubkey,
536 node_json_rpc_url,
537 follow,
538 our_localhost_port,
539 log,
540 }))
541}
542
543pub fn parse_cluster_ping(
544 matches: &ArgMatches<'_>,
545 default_signer: &DefaultSigner,
546 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
547) -> Result<CliCommandInfo, CliError> {
548 let interval = Duration::from_secs(value_t_or_exit!(matches, "interval", u64));
549 let count = if matches.is_present("count") {
550 Some(value_t_or_exit!(matches, "count", u64))
551 } else {
552 None
553 };
554 let timeout = Duration::from_secs(value_t_or_exit!(matches, "timeout", u64));
555 let blockhash = value_of(matches, BLOCKHASH_ARG.name);
556 let print_timestamp = matches.is_present("print_timestamp");
557 let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name);
558 Ok(CliCommandInfo {
559 command: CliCommand::Ping {
560 interval,
561 count,
562 timeout,
563 blockhash,
564 print_timestamp,
565 compute_unit_price,
566 },
567 signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
568 })
569}
570
571pub fn parse_get_block(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
572 let slot = value_of(matches, "slot");
573 Ok(CliCommandInfo::without_signers(CliCommand::GetBlock {
574 slot,
575 }))
576}
577
578pub fn parse_get_recent_prioritization_fees(
579 matches: &ArgMatches<'_>,
580) -> Result<CliCommandInfo, CliError> {
581 let accounts = values_of(matches, "accounts").unwrap_or(vec![]);
582 let limit_num_slots = value_of(matches, "limit_num_slots");
583 Ok(CliCommandInfo::without_signers(
584 CliCommand::GetRecentPrioritizationFees {
585 accounts,
586 limit_num_slots,
587 },
588 ))
589}
590
591pub fn parse_get_block_time(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
592 let slot = value_of(matches, "slot");
593 Ok(CliCommandInfo::without_signers(CliCommand::GetBlockTime {
594 slot,
595 }))
596}
597
598pub fn parse_get_epoch(_matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
599 Ok(CliCommandInfo::without_signers(CliCommand::GetEpoch))
600}
601
602pub fn parse_get_epoch_info(_matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
603 Ok(CliCommandInfo::without_signers(CliCommand::GetEpochInfo))
604}
605
606pub fn parse_get_slot(_matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
607 Ok(CliCommandInfo::without_signers(CliCommand::GetSlot))
608}
609
610pub fn parse_get_block_height(_matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
611 Ok(CliCommandInfo::without_signers(CliCommand::GetBlockHeight))
612}
613
614pub fn parse_largest_accounts(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
615 let filter = if matches.is_present("circulating") {
616 Some(RpcLargestAccountsFilter::Circulating)
617 } else if matches.is_present("non_circulating") {
618 Some(RpcLargestAccountsFilter::NonCirculating)
619 } else {
620 None
621 };
622 Ok(CliCommandInfo::without_signers(
623 CliCommand::LargestAccounts { filter },
624 ))
625}
626
627pub fn parse_supply(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
628 let print_accounts = matches.is_present("print_accounts");
629 Ok(CliCommandInfo::without_signers(CliCommand::Supply {
630 print_accounts,
631 }))
632}
633
634pub fn parse_total_supply(_matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
635 Ok(CliCommandInfo::without_signers(CliCommand::TotalSupply))
636}
637
638pub fn parse_get_transaction_count(_matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
639 Ok(CliCommandInfo::without_signers(
640 CliCommand::GetTransactionCount,
641 ))
642}
643
644pub fn parse_show_stakes(
645 matches: &ArgMatches<'_>,
646 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
647) -> Result<CliCommandInfo, CliError> {
648 let use_lamports_unit = matches.is_present("lamports");
649 let vote_account_pubkeys =
650 pubkeys_of_multiple_signers(matches, "vote_account_pubkeys", wallet_manager)?;
651 let withdraw_authority = pubkey_of(matches, "withdraw_authority");
652 Ok(CliCommandInfo::without_signers(CliCommand::ShowStakes {
653 use_lamports_unit,
654 vote_account_pubkeys,
655 withdraw_authority,
656 }))
657}
658
659pub fn parse_show_validators(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
660 let use_lamports_unit = matches.is_present("lamports");
661 let number_validators = matches.is_present("number");
662 let reverse_sort = matches.is_present("reverse");
663 let keep_unstaked_delinquents = matches.is_present("keep_unstaked_delinquents");
664 let delinquent_slot_distance = value_of(matches, "delinquent_slot_distance");
665
666 let sort_order = match value_t_or_exit!(matches, "sort", String).as_str() {
667 "delinquent" => CliValidatorsSortOrder::Delinquent,
668 "commission" => CliValidatorsSortOrder::Commission,
669 "credits" => CliValidatorsSortOrder::EpochCredits,
670 "identity" => CliValidatorsSortOrder::Identity,
671 "last-vote" => CliValidatorsSortOrder::LastVote,
672 "root" => CliValidatorsSortOrder::Root,
673 "skip-rate" => CliValidatorsSortOrder::SkipRate,
674 "stake" => CliValidatorsSortOrder::Stake,
675 "vote-account" => CliValidatorsSortOrder::VoteAccount,
676 "version" => CliValidatorsSortOrder::Version,
677 _ => unreachable!(),
678 };
679
680 Ok(CliCommandInfo::without_signers(
681 CliCommand::ShowValidators {
682 use_lamports_unit,
683 sort_order,
684 reverse_sort,
685 number_validators,
686 keep_unstaked_delinquents,
687 delinquent_slot_distance,
688 },
689 ))
690}
691
692pub fn parse_transaction_history(
693 matches: &ArgMatches<'_>,
694 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
695) -> Result<CliCommandInfo, CliError> {
696 let address = pubkey_of_signer(matches, "address", wallet_manager)?.unwrap();
697
698 let before = match matches.value_of("before") {
699 Some(signature) => Some(
700 signature
701 .parse()
702 .map_err(|err| CliError::BadParameter(format!("Invalid signature: {err}")))?,
703 ),
704 None => None,
705 };
706 let until = match matches.value_of("until") {
707 Some(signature) => Some(
708 signature
709 .parse()
710 .map_err(|err| CliError::BadParameter(format!("Invalid signature: {err}")))?,
711 ),
712 None => None,
713 };
714 let limit = value_t_or_exit!(matches, "limit", usize);
715 let show_transactions = matches.is_present("show_transactions");
716
717 Ok(CliCommandInfo::without_signers(
718 CliCommand::TransactionHistory {
719 address,
720 before,
721 until,
722 limit,
723 show_transactions,
724 },
725 ))
726}
727
728pub fn process_catchup(
729 rpc_client: &RpcClient,
730 config: &CliConfig,
731 node_pubkey: Option<Pubkey>,
732 mut node_json_rpc_url: Option<String>,
733 follow: bool,
734 our_localhost_port: Option<u16>,
735 log: bool,
736) -> ProcessResult {
737 let sleep_interval = Duration::from_secs(2);
738
739 let progress_bar = new_spinner_progress_bar();
740 progress_bar.set_message("Connecting...");
741
742 if let Some(our_localhost_port) = our_localhost_port {
743 let gussed_default = Some(format!("http://localhost:{our_localhost_port}"));
744 if node_json_rpc_url.is_some() && node_json_rpc_url != gussed_default {
745 println!(
747 "Preferring explicitly given rpc ({}) as us, although --our-localhost is given\n",
748 node_json_rpc_url.as_ref().unwrap()
749 );
750 } else {
751 node_json_rpc_url = gussed_default;
752 }
753 }
754
755 let (node_client, node_pubkey) = if our_localhost_port.is_some() {
756 let client = RpcClient::new(node_json_rpc_url.unwrap());
757 let guessed_default = Some(client.get_identity()?);
758 (
759 client,
760 (if node_pubkey.is_some() && node_pubkey != guessed_default {
761 println!(
763 "Preferring explicitly given node pubkey ({}) as us, although --our-localhost \
764 is given\n",
765 node_pubkey.unwrap()
766 );
767 node_pubkey
768 } else {
769 guessed_default
770 })
771 .unwrap(),
772 )
773 } else if let Some(node_pubkey) = node_pubkey {
774 if let Some(node_json_rpc_url) = node_json_rpc_url {
775 (RpcClient::new(node_json_rpc_url), node_pubkey)
776 } else {
777 let rpc_addr = loop {
778 let cluster_nodes = rpc_client.get_cluster_nodes()?;
779 if let Some(contact_info) = cluster_nodes
780 .iter()
781 .find(|contact_info| contact_info.pubkey == node_pubkey.to_string())
782 {
783 if let Some(rpc_addr) = contact_info.rpc {
784 break rpc_addr;
785 }
786 progress_bar.set_message(format!("RPC service not found for {node_pubkey}"));
787 } else {
788 progress_bar
789 .set_message(format!("Contact information not found for {node_pubkey}"));
790 }
791 sleep(sleep_interval);
792 };
793
794 (RpcClient::new_socket(rpc_addr), node_pubkey)
795 }
796 } else {
797 unreachable!()
798 };
799
800 let reported_node_pubkey = loop {
801 match node_client.get_identity() {
802 Ok(reported_node_pubkey) => break reported_node_pubkey,
803 Err(err) => {
804 if let ClientErrorKind::Reqwest(err) = err.kind() {
805 progress_bar.set_message(format!("Connection failed: {err}"));
806 sleep(sleep_interval);
807 continue;
808 }
809 return Err(Box::new(err));
810 }
811 }
812 };
813
814 if reported_node_pubkey != node_pubkey {
815 return Err(format!(
816 "The identity reported by node RPC URL does not match. Expected: {node_pubkey:?}. \
817 Reported: {reported_node_pubkey:?}"
818 )
819 .into());
820 }
821
822 if rpc_client.get_identity()? == node_pubkey {
823 return Err(
824 "Both RPC URLs reference the same node, unable to monitor for catchup. Try a \
825 different --url"
826 .into(),
827 );
828 }
829
830 let mut previous_rpc_slot = i64::MAX;
831 let mut previous_slot_distance: i64 = 0;
832 let mut retry_count: u64 = 0;
833 let max_retry_count = 5;
834 let mut get_slot_while_retrying = |client: &RpcClient| {
835 loop {
836 match client.get_slot_with_commitment(config.commitment) {
837 Ok(r) => {
838 retry_count = 0;
839 return Ok(r);
840 }
841 Err(e) => {
842 if retry_count >= max_retry_count {
843 return Err(e);
844 }
845 retry_count = retry_count.saturating_add(1);
846 if log {
847 println!("Retrying({retry_count}/{max_retry_count}): {e}\n");
849 }
850 sleep(Duration::from_secs(1));
851 }
852 };
853 }
854 };
855
856 let start_node_slot: i64 = get_slot_while_retrying(&node_client)?.try_into()?;
857 let start_rpc_slot: i64 = get_slot_while_retrying(rpc_client)?.try_into()?;
858 let start_slot_distance = start_rpc_slot.saturating_sub(start_node_slot);
859 let mut total_sleep_interval = Duration::ZERO;
860 loop {
861 let rpc_slot: i64 = get_slot_while_retrying(rpc_client)?.try_into()?;
864 let node_slot: i64 = get_slot_while_retrying(&node_client)?.try_into()?;
865 if !follow && node_slot > std::cmp::min(previous_rpc_slot, rpc_slot) {
866 progress_bar.finish_and_clear();
867 return Ok(format!(
868 "{node_pubkey} has caught up (us:{node_slot} them:{rpc_slot})",
869 ));
870 }
871
872 let slot_distance = rpc_slot.saturating_sub(node_slot);
873 let slots_per_second = previous_slot_distance.saturating_sub(slot_distance) as f64
874 / sleep_interval.as_secs_f64();
875
876 let average_time_remaining = if slot_distance == 0 || total_sleep_interval.is_zero() {
877 "".to_string()
878 } else {
879 let distance_delta = start_slot_distance.saturating_sub(slot_distance);
880 let average_catchup_slots_per_second =
881 distance_delta as f64 / total_sleep_interval.as_secs_f64();
882 let average_time_remaining =
883 (slot_distance as f64 / average_catchup_slots_per_second).round();
884 if !average_time_remaining.is_normal() {
885 "".to_string()
886 } else if average_time_remaining < 0.0 {
887 format!(" (AVG: {average_catchup_slots_per_second:.1} slots/second (falling))")
888 } else {
889 let total_node_slot_delta = node_slot.saturating_sub(start_node_slot);
891 let average_node_slots_per_second =
892 total_node_slot_delta as f64 / total_sleep_interval.as_secs_f64();
893 let expected_finish_slot = (node_slot as f64
894 + average_time_remaining * average_node_slots_per_second)
895 .round();
896 format!(
897 " (AVG: {:.1} slots/second, ETA: slot {} in {})",
898 average_catchup_slots_per_second,
899 expected_finish_slot,
900 humantime::format_duration(Duration::from_secs_f64(average_time_remaining))
901 )
902 }
903 };
904
905 progress_bar.set_message(format!(
906 "{} slot(s) {} (us:{} them:{}){}",
907 slot_distance.abs(),
908 if slot_distance >= 0 {
909 "behind"
910 } else {
911 "ahead"
912 },
913 node_slot,
914 rpc_slot,
915 if slot_distance == 0 || previous_rpc_slot == i64::MAX {
916 "".to_string()
917 } else {
918 format!(
919 ", {} node is {} at {:.1} slots/second{}",
920 if slot_distance >= 0 { "our" } else { "their" },
921 if slots_per_second < 0.0 {
922 "falling behind"
923 } else {
924 "gaining"
925 },
926 slots_per_second,
927 average_time_remaining
928 )
929 },
930 ));
931 if log {
932 println!();
933 }
934
935 sleep(sleep_interval);
936 previous_rpc_slot = rpc_slot;
937 previous_slot_distance = slot_distance;
938 total_sleep_interval = total_sleep_interval.saturating_add(sleep_interval);
939 }
940}
941
942pub fn process_cluster_date(rpc_client: &RpcClient, config: &CliConfig) -> ProcessResult {
943 let result = rpc_client.get_account_with_commitment(&sysvar::clock::id(), config.commitment)?;
944 if let Some(clock_account) = result.value {
945 let clock: Clock = from_account(&clock_account).ok_or_else(|| {
946 CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string())
947 })?;
948 let block_time = CliBlockTime {
949 slot: result.context.slot,
950 timestamp: clock.unix_timestamp,
951 };
952 Ok(config.output_format.formatted_string(&block_time))
953 } else {
954 Err(format!("AccountNotFound: pubkey={}", sysvar::clock::id()).into())
955 }
956}
957
958pub fn process_cluster_version(rpc_client: &RpcClient, config: &CliConfig) -> ProcessResult {
959 let remote_version = rpc_client.get_version()?;
960
961 if config.verbose {
962 Ok(format!("{remote_version:?}"))
963 } else {
964 Ok(remote_version.to_string())
965 }
966}
967
968pub fn process_first_available_block(rpc_client: &RpcClient) -> ProcessResult {
969 let first_available_block = rpc_client.get_first_available_block()?;
970 Ok(format!("{first_available_block}"))
971}
972
973pub fn parse_leader_schedule(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
974 let epoch = value_of(matches, "epoch");
975 Ok(CliCommandInfo::without_signers(
976 CliCommand::LeaderSchedule { epoch },
977 ))
978}
979
980pub fn process_leader_schedule(
981 rpc_client: &RpcClient,
982 config: &CliConfig,
983 epoch: Option<Epoch>,
984) -> ProcessResult {
985 let epoch_info = rpc_client.get_epoch_info()?;
986 let epoch = epoch.unwrap_or(epoch_info.epoch);
987 if epoch > epoch_info.epoch.saturating_add(1) {
988 return Err(format!("Epoch {epoch} is more than one epoch in the future").into());
989 }
990
991 let epoch_schedule = rpc_client.get_epoch_schedule()?;
992 let first_slot_in_epoch = epoch_schedule.get_first_slot_in_epoch(epoch);
993
994 let leader_schedule = rpc_client.get_leader_schedule(Some(first_slot_in_epoch))?;
995 if leader_schedule.is_none() {
996 return Err(
997 format!("Unable to fetch leader schedule for slot {first_slot_in_epoch}").into(),
998 );
999 }
1000 let leader_schedule = leader_schedule.unwrap();
1001
1002 let mut leader_per_slot_index = Vec::new();
1003 for (pubkey, leader_slots) in leader_schedule.iter() {
1004 for slot_index in leader_slots.iter() {
1005 if *slot_index >= leader_per_slot_index.len() {
1006 leader_per_slot_index.resize(slot_index.saturating_add(1), "?");
1007 }
1008 leader_per_slot_index[*slot_index] = pubkey;
1009 }
1010 }
1011
1012 let mut leader_schedule_entries = vec![];
1013 for (slot_index, leader) in leader_per_slot_index.iter().enumerate() {
1014 leader_schedule_entries.push(CliLeaderScheduleEntry {
1015 slot: first_slot_in_epoch.saturating_add(slot_index as u64),
1016 leader: leader.to_string(),
1017 });
1018 }
1019
1020 Ok(config.output_format.formatted_string(&CliLeaderSchedule {
1021 epoch,
1022 leader_schedule_entries,
1023 }))
1024}
1025
1026pub fn process_get_recent_priority_fees(
1027 rpc_client: &RpcClient,
1028 config: &CliConfig,
1029 accounts: &[Pubkey],
1030 limit_num_slots: Option<Slot>,
1031) -> ProcessResult {
1032 let fees = rpc_client.get_recent_prioritization_fees(accounts)?;
1033 let mut min = u64::MAX;
1034 let mut max = 0;
1035 let mut total = Saturating(0);
1036 let fees_len: u64 = fees.len().try_into().unwrap();
1037 let num_slots = limit_num_slots.unwrap_or(fees_len).min(fees_len).max(1);
1038
1039 let mut cli_fees = Vec::with_capacity(fees.len());
1040 for RpcPrioritizationFee {
1041 slot,
1042 prioritization_fee,
1043 } in fees
1044 .into_iter()
1045 .skip(fees_len.saturating_sub(num_slots) as usize)
1046 {
1047 min = min.min(prioritization_fee);
1048 max = max.max(prioritization_fee);
1049 total += prioritization_fee;
1050 cli_fees.push(CliPrioritizationFee {
1051 slot,
1052 prioritization_fee,
1053 });
1054 }
1055 Ok(config
1056 .output_format
1057 .formatted_string(&CliPrioritizationFeeStats {
1058 fees: cli_fees,
1059 min,
1060 max,
1061 average: total.0.checked_div(num_slots).unwrap_or(0),
1062 num_slots,
1063 }))
1064}
1065
1066pub fn process_get_block(
1067 rpc_client: &RpcClient,
1068 config: &CliConfig,
1069 slot: Option<Slot>,
1070) -> ProcessResult {
1071 let slot = if let Some(slot) = slot {
1072 slot
1073 } else {
1074 rpc_client.get_slot_with_commitment(CommitmentConfig::finalized())?
1075 };
1076
1077 let encoded_confirmed_block = rpc_client
1078 .get_block_with_config(
1079 slot,
1080 RpcBlockConfig {
1081 encoding: Some(UiTransactionEncoding::Base64),
1082 commitment: Some(CommitmentConfig::confirmed()),
1083 max_supported_transaction_version: Some(0),
1084 ..RpcBlockConfig::default()
1085 },
1086 )?
1087 .into();
1088 let cli_block = CliBlock {
1089 encoded_confirmed_block,
1090 slot,
1091 };
1092 Ok(config.output_format.formatted_string(&cli_block))
1093}
1094
1095pub fn process_get_block_time(
1096 rpc_client: &RpcClient,
1097 config: &CliConfig,
1098 slot: Option<Slot>,
1099) -> ProcessResult {
1100 let slot = if let Some(slot) = slot {
1101 slot
1102 } else {
1103 rpc_client.get_slot_with_commitment(CommitmentConfig::finalized())?
1104 };
1105 let timestamp = rpc_client.get_block_time(slot)?;
1106 let block_time = CliBlockTime { slot, timestamp };
1107 Ok(config.output_format.formatted_string(&block_time))
1108}
1109
1110pub fn process_get_epoch(rpc_client: &RpcClient, _config: &CliConfig) -> ProcessResult {
1111 let epoch_info = rpc_client.get_epoch_info()?;
1112 Ok(epoch_info.epoch.to_string())
1113}
1114
1115pub fn process_get_epoch_info(rpc_client: &RpcClient, config: &CliConfig) -> ProcessResult {
1116 let epoch_info = rpc_client.get_epoch_info()?;
1117 let epoch_completed_percent =
1118 epoch_info.slot_index as f64 / epoch_info.slots_in_epoch as f64 * 100_f64;
1119 let mut cli_epoch_info = CliEpochInfo {
1120 epoch_info,
1121 epoch_completed_percent,
1122 average_slot_time_ms: 0,
1123 start_block_time: None,
1124 current_block_time: None,
1125 };
1126 match config.output_format {
1127 OutputFormat::Json | OutputFormat::JsonCompact => {}
1128 _ => {
1129 let epoch_info = &cli_epoch_info.epoch_info;
1130 let average_slot_time_ms = rpc_client
1131 .get_recent_performance_samples(Some(60))
1132 .ok()
1133 .and_then(|samples| {
1134 let (slots, secs) = samples.iter().fold(
1135 (0, 0u64),
1136 |(slots, secs): (u64, u64),
1137 RpcPerfSample {
1138 num_slots,
1139 sample_period_secs,
1140 ..
1141 }| {
1142 (
1143 slots.saturating_add(*num_slots),
1144 secs.saturating_add((*sample_period_secs).into()),
1145 )
1146 },
1147 );
1148 secs.saturating_mul(1000).checked_div(slots)
1149 })
1150 .unwrap_or(clock::DEFAULT_MS_PER_SLOT);
1151 let epoch_expected_start_slot = epoch_info
1152 .absolute_slot
1153 .saturating_sub(epoch_info.slot_index);
1154 let first_block_in_epoch = rpc_client
1155 .get_blocks_with_limit(epoch_expected_start_slot, 1)
1156 .ok()
1157 .and_then(|slot_vec| slot_vec.first().cloned())
1158 .unwrap_or(epoch_expected_start_slot);
1159 let start_block_time =
1160 rpc_client
1161 .get_block_time(first_block_in_epoch)
1162 .ok()
1163 .map(|time| {
1164 time.saturating_sub(
1165 first_block_in_epoch
1166 .saturating_sub(epoch_expected_start_slot)
1167 .saturating_mul(average_slot_time_ms)
1168 .saturating_div(1000) as i64,
1169 )
1170 });
1171 let current_block_time = rpc_client.get_block_time(epoch_info.absolute_slot).ok();
1172
1173 cli_epoch_info.average_slot_time_ms = average_slot_time_ms;
1174 cli_epoch_info.start_block_time = start_block_time;
1175 cli_epoch_info.current_block_time = current_block_time;
1176 }
1177 }
1178 Ok(config.output_format.formatted_string(&cli_epoch_info))
1179}
1180
1181pub fn process_get_genesis_hash(rpc_client: &RpcClient) -> ProcessResult {
1182 let genesis_hash = rpc_client.get_genesis_hash()?;
1183 Ok(genesis_hash.to_string())
1184}
1185
1186pub fn process_get_slot(rpc_client: &RpcClient, _config: &CliConfig) -> ProcessResult {
1187 let slot = rpc_client.get_slot()?;
1188 Ok(slot.to_string())
1189}
1190
1191pub fn process_get_block_height(rpc_client: &RpcClient, _config: &CliConfig) -> ProcessResult {
1192 let block_height = rpc_client.get_block_height()?;
1193 Ok(block_height.to_string())
1194}
1195
1196pub fn parse_show_block_production(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
1197 let epoch = value_t!(matches, "epoch", Epoch).ok();
1198 let slot_limit = value_t!(matches, "slot_limit", u64).ok();
1199
1200 Ok(CliCommandInfo::without_signers(
1201 CliCommand::ShowBlockProduction { epoch, slot_limit },
1202 ))
1203}
1204
1205pub fn process_show_block_production(
1206 rpc_client: &RpcClient,
1207 config: &CliConfig,
1208 epoch: Option<Epoch>,
1209 slot_limit: Option<u64>,
1210) -> ProcessResult {
1211 let epoch_schedule = rpc_client.get_epoch_schedule()?;
1212 let epoch_info = rpc_client.get_epoch_info_with_commitment(CommitmentConfig::finalized())?;
1213
1214 let epoch = epoch.unwrap_or(epoch_info.epoch);
1215 if epoch > epoch_info.epoch {
1216 return Err(format!("Epoch {epoch} is in the future").into());
1217 }
1218
1219 let first_slot_in_epoch = epoch_schedule.get_first_slot_in_epoch(epoch);
1220 let end_slot = std::cmp::min(
1221 epoch_info.absolute_slot,
1222 epoch_schedule.get_last_slot_in_epoch(epoch),
1223 );
1224
1225 let mut start_slot = if let Some(slot_limit) = slot_limit {
1226 std::cmp::max(end_slot.saturating_sub(slot_limit), first_slot_in_epoch)
1227 } else {
1228 first_slot_in_epoch
1229 };
1230
1231 let progress_bar = new_spinner_progress_bar();
1232 progress_bar.set_message(format!(
1233 "Fetching confirmed blocks between slots {start_slot} and {end_slot}..."
1234 ));
1235
1236 let slot_history_account = rpc_client
1237 .get_account_with_commitment(&sysvar::slot_history::id(), CommitmentConfig::finalized())?
1238 .value
1239 .unwrap();
1240
1241 let slot_history: SlotHistory = from_account(&slot_history_account).ok_or_else(|| {
1242 CliError::RpcRequestError("Failed to deserialize slot history".to_string())
1243 })?;
1244
1245 let (confirmed_blocks, start_slot) =
1246 if start_slot >= slot_history.oldest() && end_slot <= slot_history.newest() {
1247 let confirmed_blocks: Vec<_> = (start_slot..=end_slot)
1250 .filter(|slot| slot_history.check(*slot) == slot_history::Check::Found)
1251 .collect();
1252 (confirmed_blocks, start_slot)
1253 } else {
1254 let minimum_ledger_slot = rpc_client.minimum_ledger_slot()?;
1261 if minimum_ledger_slot > end_slot {
1262 return Err(format!(
1263 "Ledger data not available for slots {start_slot} to {end_slot} (minimum \
1264 ledger slot is {minimum_ledger_slot})"
1265 )
1266 .into());
1267 }
1268
1269 if minimum_ledger_slot > start_slot {
1270 progress_bar.println(format!(
1271 "{}",
1272 style(format!(
1273 "Note: Requested start slot was {start_slot} but minimum ledger slot is \
1274 {minimum_ledger_slot}"
1275 ))
1276 .italic(),
1277 ));
1278 start_slot = minimum_ledger_slot;
1279 }
1280
1281 let confirmed_blocks = rpc_client.get_blocks(start_slot, Some(end_slot))?;
1282 (confirmed_blocks, start_slot)
1283 };
1284
1285 let start_slot_index = start_slot.saturating_sub(first_slot_in_epoch) as usize;
1286 let end_slot_index = end_slot.saturating_sub(first_slot_in_epoch) as usize;
1287 let total_slots = end_slot_index
1288 .saturating_sub(start_slot_index)
1289 .saturating_add(1);
1290 let total_blocks_produced = confirmed_blocks.len();
1291 assert!(total_blocks_produced <= total_slots);
1292 let total_slots_skipped = total_slots.saturating_sub(total_blocks_produced);
1293 let mut leader_slot_count = HashMap::new();
1294 let mut leader_skipped_slots = HashMap::new();
1295
1296 progress_bar.set_message(format!("Fetching leader schedule for epoch {epoch}..."));
1297 let leader_schedule = rpc_client
1298 .get_leader_schedule_with_commitment(Some(start_slot), CommitmentConfig::finalized())?;
1299 if leader_schedule.is_none() {
1300 return Err(format!("Unable to fetch leader schedule for slot {start_slot}").into());
1301 }
1302 let leader_schedule = leader_schedule.unwrap();
1303
1304 let mut leader_per_slot_index = Vec::new();
1305 leader_per_slot_index.resize(total_slots, "?".to_string());
1306 for (pubkey, leader_slots) in leader_schedule.iter() {
1307 let pubkey = format_labeled_address(pubkey, &config.address_labels);
1308 for slot_index in leader_slots.iter() {
1309 if *slot_index >= start_slot_index && *slot_index <= end_slot_index {
1310 leader_per_slot_index[slot_index.saturating_sub(start_slot_index)]
1311 .clone_from(&pubkey);
1312 }
1313 }
1314 }
1315
1316 progress_bar.set_message(format!(
1317 "Processing {total_slots} slots containing {total_blocks_produced} blocks and \
1318 {total_slots_skipped} empty slots..."
1319 ));
1320
1321 let mut confirmed_blocks_index = 0;
1322 let mut individual_slot_status = vec![];
1323 for (leader, slot_index) in leader_per_slot_index.iter().zip(0u64..) {
1324 let slot = start_slot.saturating_add(slot_index);
1325 let slot_count: &mut u64 = leader_slot_count.entry(leader).or_insert(0);
1326 *slot_count = slot_count.saturating_add(1);
1327 let skipped_slots: &mut u64 = leader_skipped_slots.entry(leader).or_insert(0);
1328
1329 loop {
1330 if confirmed_blocks_index < confirmed_blocks.len() {
1331 let slot_of_next_confirmed_block = confirmed_blocks[confirmed_blocks_index];
1332 if slot_of_next_confirmed_block < slot {
1333 confirmed_blocks_index = confirmed_blocks_index.saturating_add(1);
1334 continue;
1335 }
1336 if slot_of_next_confirmed_block == slot {
1337 individual_slot_status.push(CliSlotStatus {
1338 slot,
1339 leader: (*leader).to_string(),
1340 skipped: false,
1341 });
1342 break;
1343 }
1344 }
1345 *skipped_slots = skipped_slots.saturating_add(1);
1346 individual_slot_status.push(CliSlotStatus {
1347 slot,
1348 leader: (*leader).to_string(),
1349 skipped: true,
1350 });
1351 break;
1352 }
1353 }
1354
1355 progress_bar.finish_and_clear();
1356
1357 let mut leaders: Vec<CliBlockProductionEntry> = leader_slot_count
1358 .iter()
1359 .map(|(leader, leader_slots)| {
1360 let skipped_slots = *leader_skipped_slots.get(leader).unwrap();
1361 let blocks_produced = leader_slots.saturating_sub(skipped_slots);
1362 CliBlockProductionEntry {
1363 identity_pubkey: (**leader).to_string(),
1364 leader_slots: *leader_slots,
1365 blocks_produced,
1366 skipped_slots,
1367 }
1368 })
1369 .collect();
1370 leaders.sort_by(|a, b| a.identity_pubkey.partial_cmp(&b.identity_pubkey).unwrap());
1371 let block_production = CliBlockProduction {
1372 epoch,
1373 start_slot,
1374 end_slot,
1375 total_slots,
1376 total_blocks_produced,
1377 total_slots_skipped,
1378 leaders,
1379 individual_slot_status,
1380 verbose: config.verbose,
1381 };
1382 Ok(config.output_format.formatted_string(&block_production))
1383}
1384
1385pub fn process_largest_accounts(
1386 rpc_client: &RpcClient,
1387 config: &CliConfig,
1388 filter: Option<RpcLargestAccountsFilter>,
1389) -> ProcessResult {
1390 let accounts = rpc_client
1391 .get_largest_accounts_with_config(RpcLargestAccountsConfig {
1392 commitment: Some(config.commitment),
1393 filter,
1394 sort_results: None,
1395 })?
1396 .value;
1397 let largest_accounts = CliAccountBalances { accounts };
1398 Ok(config.output_format.formatted_string(&largest_accounts))
1399}
1400
1401pub fn process_supply(
1402 rpc_client: &RpcClient,
1403 config: &CliConfig,
1404 print_accounts: bool,
1405) -> ProcessResult {
1406 let supply_response = rpc_client.supply()?;
1407 let mut supply: CliSupply = supply_response.value.into();
1408 supply.print_accounts = print_accounts;
1409 Ok(config.output_format.formatted_string(&supply))
1410}
1411
1412pub fn process_total_supply(rpc_client: &RpcClient, _config: &CliConfig) -> ProcessResult {
1413 let supply = rpc_client.supply()?.value;
1414 Ok(format!(
1415 "{} SOL",
1416 build_balance_message(supply.total, false, false)
1417 ))
1418}
1419
1420pub fn process_get_transaction_count(rpc_client: &RpcClient, _config: &CliConfig) -> ProcessResult {
1421 let transaction_count = rpc_client.get_transaction_count()?;
1422 Ok(transaction_count.to_string())
1423}
1424
1425pub fn process_ping(
1426 tps_client: &Arc<dyn TpsClient>,
1427 config: &CliConfig,
1428 interval: &Duration,
1429 count: &Option<u64>,
1430 timeout: &Duration,
1431 fixed_blockhash: &Option<Hash>,
1432 print_timestamp: bool,
1433 compute_unit_price: Option<u64>,
1434 rpc_client: &RpcClient,
1435) -> ProcessResult {
1436 let (signal_sender, signal_receiver) = unbounded();
1437 let handler = move || {
1438 let _ = signal_sender.send(());
1439 };
1440 match ctrlc::try_set_handler(handler) {
1441 Err(ctrlc::Error::MultipleHandlers) => {}
1444 result => result.expect("Error setting Ctrl-C handler"),
1445 }
1446
1447 let mut cli_pings = vec![];
1448
1449 let mut submit_count: u32 = 0;
1450 let mut confirmed_count: u32 = 0;
1451 let mut confirmation_time: VecDeque<u64> = VecDeque::with_capacity(1024);
1452
1453 let mut blockhash = tps_client.get_latest_blockhash()?;
1454 let mut lamports: u64 = 0;
1455 let mut blockhash_acquired = Instant::now();
1456 let mut blockhash_from_cluster = false;
1457 if let Some(fixed_blockhash) = fixed_blockhash {
1458 if *fixed_blockhash != Hash::default() {
1459 blockhash = *fixed_blockhash;
1460 } else {
1461 blockhash_from_cluster = true;
1462 }
1463 }
1464
1465 let to = config.signers[0].pubkey();
1466 let compute_unit_limit = if compute_unit_price.is_some() {
1467 let ixs = vec![system_instruction::transfer(
1468 &config.signers[0].pubkey(),
1469 &to,
1470 lamports,
1471 )]
1472 .with_compute_unit_config(&ComputeUnitConfig {
1473 compute_unit_price,
1474 compute_unit_limit: ComputeUnitLimit::Simulated,
1475 });
1476 let message = Message::new(&ixs, Some(&config.signers[0].pubkey()));
1477 ComputeUnitLimit::Static(simulate_for_compute_unit_limit(rpc_client, &message)?)
1478 } else {
1479 ComputeUnitLimit::Default
1480 };
1481
1482 'mainloop: for seq in 0..count.unwrap_or(u64::MAX) {
1483 let now = Instant::now();
1484 if fixed_blockhash.is_none() && now.duration_since(blockhash_acquired).as_secs() > 60 {
1485 let new_blockhash = tps_client.get_new_latest_blockhash(&blockhash)?;
1487 blockhash = new_blockhash;
1488 lamports = 0;
1489 blockhash_acquired = Instant::now();
1490 }
1491
1492 lamports = lamports.saturating_add(1);
1493
1494 let build_message = |lamports| {
1495 let ixs = vec![system_instruction::transfer(
1496 &config.signers[0].pubkey(),
1497 &to,
1498 lamports,
1499 )]
1500 .with_compute_unit_config(&ComputeUnitConfig {
1501 compute_unit_price,
1502 compute_unit_limit,
1503 });
1504 Message::new(&ixs, Some(&config.signers[0].pubkey()))
1505 };
1506 let (message, _) = resolve_spend_tx_and_check_account_balance(
1507 rpc_client,
1508 false,
1509 SpendAmount::Some(lamports),
1510 &blockhash,
1511 &config.signers[0].pubkey(),
1512 compute_unit_limit,
1513 build_message,
1514 config.commitment,
1515 )?;
1516 let mut tx = Transaction::new_unsigned(message);
1517 tx.try_sign(&config.signers, blockhash)?;
1518
1519 let timestamp = || {
1520 let micros = SystemTime::now()
1521 .duration_since(UNIX_EPOCH)
1522 .unwrap()
1523 .as_micros();
1524 format!("[{}.{:06}] ", micros / 1_000_000, micros % 1_000_000)
1525 };
1526
1527 match tps_client.send_transaction(tx) {
1528 Ok(signature) => {
1529 let transaction_sent = Instant::now();
1530 loop {
1531 let signature_status = tps_client.get_signature_status(&signature)?;
1532 let elapsed_time = Instant::now().duration_since(transaction_sent);
1533 if let Some(transaction_status) = signature_status {
1534 match transaction_status {
1535 Ok(()) => {
1536 let elapsed_time_millis = elapsed_time.as_millis() as u64;
1537 confirmation_time.push_back(elapsed_time_millis);
1538 let cli_ping_data = CliPingData {
1539 success: true,
1540 signature: Some(signature.to_string()),
1541 ms: Some(elapsed_time_millis),
1542 error: None,
1543 timestamp: timestamp(),
1544 print_timestamp,
1545 sequence: seq,
1546 lamports: Some(lamports),
1547 };
1548 eprint!("{cli_ping_data}");
1549 cli_pings.push(cli_ping_data);
1550 confirmed_count = confirmed_count.saturating_add(1);
1551 }
1552 Err(err) => {
1553 let cli_ping_data = CliPingData {
1554 success: false,
1555 signature: Some(signature.to_string()),
1556 ms: None,
1557 error: Some(err.to_string()),
1558 timestamp: timestamp(),
1559 print_timestamp,
1560 sequence: seq,
1561 lamports: None,
1562 };
1563 eprint!("{cli_ping_data}");
1564 cli_pings.push(cli_ping_data);
1565 }
1566 }
1567 break;
1568 }
1569
1570 if elapsed_time >= *timeout {
1571 let cli_ping_data = CliPingData {
1572 success: false,
1573 signature: Some(signature.to_string()),
1574 ms: None,
1575 error: None,
1576 timestamp: timestamp(),
1577 print_timestamp,
1578 sequence: seq,
1579 lamports: None,
1580 };
1581 eprint!("{cli_ping_data}");
1582 cli_pings.push(cli_ping_data);
1583 break;
1584 }
1585
1586 if signal_receiver
1588 .recv_timeout(Duration::from_millis(clock::DEFAULT_MS_PER_SLOT / 2))
1589 .is_ok()
1590 {
1591 break 'mainloop;
1592 }
1593 }
1594 }
1595 Err(err) => {
1596 let cli_ping_data = CliPingData {
1597 success: false,
1598 signature: None,
1599 ms: None,
1600 error: Some(err.to_string()),
1601 timestamp: timestamp(),
1602 print_timestamp,
1603 sequence: seq,
1604 lamports: None,
1605 };
1606 eprint!("{cli_ping_data}");
1607 cli_pings.push(cli_ping_data);
1608 }
1609 }
1610 submit_count = submit_count.saturating_add(1);
1611
1612 if signal_receiver.recv_timeout(*interval).is_ok() {
1613 break 'mainloop;
1614 }
1615 }
1616
1617 let transaction_stats = CliPingTxStats {
1618 num_transactions: submit_count,
1619 num_transaction_confirmed: confirmed_count,
1620 };
1621 let confirmation_stats = if !confirmation_time.is_empty() {
1622 let samples: Vec<f64> = confirmation_time.iter().map(|t| *t as f64).collect();
1623 let dist = criterion_stats::Distribution::from(samples.into_boxed_slice());
1624 let mean = dist.mean();
1625 Some(CliPingConfirmationStats {
1626 min: dist.min(),
1627 mean,
1628 max: dist.max(),
1629 std_dev: dist.std_dev(Some(mean)),
1630 })
1631 } else {
1632 None
1633 };
1634
1635 let cli_ping = CliPing {
1636 source_pubkey: config.signers[0].pubkey().to_string(),
1637 fixed_blockhash: fixed_blockhash.map(|_| blockhash.to_string()),
1638 blockhash_from_cluster,
1639 pings: cli_pings,
1640 transaction_stats,
1641 confirmation_stats,
1642 };
1643
1644 Ok(config.output_format.formatted_string(&cli_ping))
1645}
1646
1647pub fn parse_logs(
1648 matches: &ArgMatches<'_>,
1649 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
1650) -> Result<CliCommandInfo, CliError> {
1651 let address = pubkey_of_signer(matches, "address", wallet_manager)?;
1652 let include_votes = matches.is_present("include_votes");
1653
1654 let filter = match address {
1655 None => {
1656 if include_votes {
1657 RpcTransactionLogsFilter::AllWithVotes
1658 } else {
1659 RpcTransactionLogsFilter::All
1660 }
1661 }
1662 Some(address) => RpcTransactionLogsFilter::Mentions(vec![address.to_string()]),
1663 };
1664
1665 Ok(CliCommandInfo::without_signers(CliCommand::Logs { filter }))
1666}
1667
1668pub fn process_logs(config: &CliConfig, filter: &RpcTransactionLogsFilter) -> ProcessResult {
1669 println!(
1670 "Streaming transaction logs{}. {:?} commitment",
1671 match filter {
1672 RpcTransactionLogsFilter::All => "".into(),
1673 RpcTransactionLogsFilter::AllWithVotes => " (including votes)".into(),
1674 RpcTransactionLogsFilter::Mentions(addresses) =>
1675 format!(" mentioning {}", addresses.join(",")),
1676 },
1677 config.commitment.commitment
1678 );
1679
1680 let (_client, receiver) = PubsubClient::logs_subscribe(
1681 &config.websocket_url,
1682 filter.clone(),
1683 RpcTransactionLogsConfig {
1684 commitment: Some(config.commitment),
1685 },
1686 )?;
1687
1688 loop {
1689 match receiver.recv() {
1690 Ok(logs) => {
1691 println!("Transaction executed in slot {}:", logs.context.slot);
1692 println!(" Signature: {}", logs.value.signature);
1693 println!(
1694 " Status: {}",
1695 logs.value
1696 .err
1697 .map(|err| err.to_string())
1698 .unwrap_or_else(|| "Ok".to_string())
1699 );
1700 println!(" Log Messages:");
1701 for log in logs.value.logs {
1702 println!(" {log}");
1703 }
1704 }
1705 Err(err) => {
1706 return Ok(format!("Disconnected: {err}"));
1707 }
1708 }
1709 }
1710}
1711
1712pub fn process_live_slots(config: &CliConfig) -> ProcessResult {
1713 let exit = Arc::new(AtomicBool::new(false));
1714
1715 let mut current: Option<SlotInfo> = None;
1716 let mut message = "".to_string();
1717
1718 let slot_progress = new_spinner_progress_bar();
1719 slot_progress.set_message("Connecting...");
1720 let (mut client, receiver) = PubsubClient::slot_subscribe(&config.websocket_url)?;
1721 slot_progress.set_message("Connected.");
1722
1723 let spacer = "|";
1724 slot_progress.println(spacer);
1725
1726 let mut last_root = u64::MAX;
1727 let mut last_root_update = Instant::now();
1728 let mut slots_per_second = f64::NAN;
1729 loop {
1730 if exit.load(Ordering::Relaxed) {
1731 eprintln!("{message}");
1732 client.shutdown().unwrap();
1733 break;
1734 }
1735
1736 match receiver.recv() {
1737 Ok(new_info) => {
1738 if last_root == u64::MAX {
1739 last_root = new_info.root;
1740 last_root_update = Instant::now();
1741 }
1742 if last_root_update.elapsed().as_secs() >= 5 {
1743 let root = new_info.root;
1744 slots_per_second = root.saturating_sub(last_root) as f64
1745 / last_root_update.elapsed().as_secs() as f64;
1746 last_root_update = Instant::now();
1747 last_root = root;
1748 }
1749
1750 message = if slots_per_second.is_nan() {
1751 format!("{new_info:?}")
1752 } else {
1753 format!(
1754 "{new_info:?} | root slot advancing at {slots_per_second:.2} slots/second"
1755 )
1756 };
1757 slot_progress.set_message(message.clone());
1758
1759 if let Some(previous) = current {
1760 let slot_delta = (new_info.slot as i64).saturating_sub(previous.slot as i64);
1761 let root_delta = (new_info.root as i64).saturating_sub(previous.root as i64);
1762
1763 if slot_delta != root_delta {
1768 let prev_root = format!(
1769 "|<--- {} <- … <- {} <- {} (prev)",
1770 previous.root, previous.parent, previous.slot
1771 );
1772 slot_progress.println(&prev_root);
1773
1774 let new_root = format!(
1775 "| '- {} <- … <- {} <- {} (next)",
1776 new_info.root, new_info.parent, new_info.slot
1777 );
1778
1779 slot_progress.println(prev_root);
1780 slot_progress.println(new_root);
1781 slot_progress.println(spacer);
1782 }
1783 }
1784 current = Some(new_info);
1785 }
1786 Err(err) => {
1787 eprintln!("disconnected: {err}");
1788 break;
1789 }
1790 }
1791 }
1792
1793 Ok("".to_string())
1794}
1795
1796pub fn process_show_gossip(rpc_client: &RpcClient, config: &CliConfig) -> ProcessResult {
1797 let cluster_nodes = rpc_client.get_cluster_nodes()?;
1798
1799 let nodes: Vec<_> = cluster_nodes
1800 .into_iter()
1801 .map(|node| CliGossipNode::new(node, &config.address_labels))
1802 .collect();
1803
1804 Ok(config
1805 .output_format
1806 .formatted_string(&CliGossipNodes(nodes)))
1807}
1808
1809pub fn process_show_stakes(
1810 rpc_client: &RpcClient,
1811 config: &CliConfig,
1812 use_lamports_unit: bool,
1813 vote_account_pubkeys: Option<&[Pubkey]>,
1814 withdraw_authority_pubkey: Option<&Pubkey>,
1815) -> ProcessResult {
1816 use crate::stake::build_stake_state;
1817
1818 let vote_account_pubkeys = match vote_account_pubkeys {
1821 Some(pubkeys) => {
1822 let vote_account_progress_bar = new_spinner_progress_bar();
1823 vote_account_progress_bar.set_message("Searching for matching vote accounts...");
1824
1825 let vote_accounts = rpc_client.get_vote_accounts()?;
1826
1827 let mut pubkeys: HashSet<String> =
1828 pubkeys.iter().map(|pubkey| pubkey.to_string()).collect();
1829
1830 let vote_account_pubkeys: HashSet<String> = vote_accounts
1831 .current
1832 .into_iter()
1833 .chain(vote_accounts.delinquent)
1834 .filter_map(|vote_acc| {
1835 (pubkeys.remove(&vote_acc.node_pubkey) || pubkeys.remove(&vote_acc.vote_pubkey))
1836 .then_some(vote_acc.vote_pubkey)
1837 })
1838 .collect();
1839
1840 if !pubkeys.is_empty() {
1841 return Err(CliError::RpcRequestError(format!(
1842 "Failed to retrieve matching vote account for {pubkeys:?}."
1843 ))
1844 .into());
1845 }
1846 vote_account_progress_bar.finish_and_clear();
1847 vote_account_pubkeys
1848 }
1849 None => HashSet::new(),
1850 };
1851
1852 let mut program_accounts_config = RpcProgramAccountsConfig {
1853 account_config: RpcAccountInfoConfig {
1854 encoding: Some(solana_account_decoder::UiAccountEncoding::Base64),
1855 ..RpcAccountInfoConfig::default()
1856 },
1857 ..RpcProgramAccountsConfig::default()
1858 };
1859
1860 let stake_account_progress_bar = new_spinner_progress_bar();
1861 stake_account_progress_bar.set_message("Fetching stake accounts...");
1862
1863 if vote_account_pubkeys.len() == 1 {
1865 program_accounts_config.filters = Some(vec![
1866 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &[2, 0, 0, 0])),
1868 RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
1870 124,
1871 Pubkey::from_str(vote_account_pubkeys.iter().next().unwrap())
1872 .unwrap()
1873 .as_ref(),
1874 )),
1875 ]);
1876 }
1877
1878 if let Some(withdraw_authority_pubkey) = withdraw_authority_pubkey {
1879 let withdrawer_filter = RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
1881 44,
1882 withdraw_authority_pubkey.as_ref(),
1883 ));
1884 let filters = program_accounts_config.filters.get_or_insert(vec![]);
1885 filters.push(withdrawer_filter);
1886 }
1887
1888 let all_stake_accounts = rpc_client
1889 .get_program_ui_accounts_with_config(&stake::program::id(), program_accounts_config)?;
1890 let stake_history_account = rpc_client.get_account(&stake_history::id())?;
1891 let clock_account = rpc_client.get_account(&sysvar::clock::id())?;
1892 let clock: Clock = from_account(&clock_account).ok_or_else(|| {
1893 CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string())
1894 })?;
1895 let stake_history = from_account(&stake_history_account).ok_or_else(|| {
1896 CliError::RpcRequestError("Failed to deserialize stake history".to_string())
1897 })?;
1898 let new_rate_activation_epoch = get_feature_activation_epoch(
1899 rpc_client,
1900 &agave_feature_set::reduce_stake_warmup_cooldown::id(),
1901 )?;
1902 stake_account_progress_bar.finish_and_clear();
1903
1904 let mut stake_accounts: Vec<CliKeyedStakeState> = vec![];
1905 for (stake_pubkey, stake_ui_account) in all_stake_accounts {
1906 let stake_account: Account = stake_ui_account.decode().expect(
1907 "It should be impossible at this point for the account data not to be decodable. \
1908 Ensure that the account was fetched using a binary encoding.",
1909 );
1910 if let Ok(stake_state) = stake_account.state() {
1911 match stake_state {
1912 StakeStateV2::Initialized(_) => {
1913 if vote_account_pubkeys.is_empty() {
1914 stake_accounts.push(CliKeyedStakeState {
1915 stake_pubkey: stake_pubkey.to_string(),
1916 stake_state: build_stake_state(
1917 stake_account.lamports,
1918 &stake_state,
1919 use_lamports_unit,
1920 &stake_history,
1921 &clock,
1922 new_rate_activation_epoch,
1923 false,
1924 ),
1925 });
1926 }
1927 }
1928 StakeStateV2::Stake(_, stake, _) => {
1929 if vote_account_pubkeys.is_empty()
1930 || vote_account_pubkeys.contains(&stake.delegation.voter_pubkey.to_string())
1931 {
1932 stake_accounts.push(CliKeyedStakeState {
1933 stake_pubkey: stake_pubkey.to_string(),
1934 stake_state: build_stake_state(
1935 stake_account.lamports,
1936 &stake_state,
1937 use_lamports_unit,
1938 &stake_history,
1939 &clock,
1940 new_rate_activation_epoch,
1941 false,
1942 ),
1943 });
1944 }
1945 }
1946 _ => {}
1947 }
1948 }
1949 }
1950 if stake_accounts.is_empty() {
1951 Ok("No stake accounts found".into())
1952 } else {
1953 Ok(config
1954 .output_format
1955 .formatted_string(&CliStakeVec::new(stake_accounts)))
1956 }
1957}
1958
1959pub fn process_wait_for_max_stake(
1960 rpc_client: &RpcClient,
1961 config: &CliConfig,
1962 max_stake_percent: f32,
1963) -> ProcessResult {
1964 let now = std::time::Instant::now();
1965 rpc_client.wait_for_max_stake(config.commitment, max_stake_percent)?;
1966 Ok(format!("Done waiting, took: {}s", now.elapsed().as_secs()))
1967}
1968
1969pub fn process_show_validators(
1970 rpc_client: &RpcClient,
1971 config: &CliConfig,
1972 use_lamports_unit: bool,
1973 validators_sort_order: CliValidatorsSortOrder,
1974 validators_reverse_sort: bool,
1975 number_validators: bool,
1976 keep_unstaked_delinquents: bool,
1977 delinquent_slot_distance: Option<Slot>,
1978) -> ProcessResult {
1979 let progress_bar = new_spinner_progress_bar();
1980 progress_bar.set_message("Fetching vote accounts...");
1981 let epoch_info = rpc_client.get_epoch_info()?;
1982 let vote_accounts = rpc_client.get_vote_accounts_with_config(RpcGetVoteAccountsConfig {
1983 keep_unstaked_delinquents: Some(keep_unstaked_delinquents),
1984 delinquent_slot_distance,
1985 ..RpcGetVoteAccountsConfig::default()
1986 })?;
1987
1988 progress_bar.set_message("Fetching block production...");
1989 let skip_rate: HashMap<_, _> = rpc_client
1990 .get_block_production()?
1991 .value
1992 .by_identity
1993 .into_iter()
1994 .map(|(identity, (leader_slots, blocks_produced))| {
1995 (
1996 identity,
1997 100. * (leader_slots.saturating_sub(blocks_produced)) as f64 / leader_slots as f64,
1998 )
1999 })
2000 .collect();
2001
2002 progress_bar.set_message("Fetching version information...");
2003 let mut node_version = HashMap::new();
2004 for contact_info in rpc_client.get_cluster_nodes()? {
2005 node_version.insert(
2006 contact_info.pubkey,
2007 contact_info
2008 .version
2009 .and_then(|version| CliVersion::from_str(&version).ok())
2010 .unwrap_or_else(CliVersion::unknown_version),
2011 );
2012 }
2013
2014 progress_bar.finish_and_clear();
2015
2016 let total_active_stake = vote_accounts
2017 .current
2018 .iter()
2019 .chain(vote_accounts.delinquent.iter())
2020 .map(|vote_account| vote_account.activated_stake)
2021 .sum::<u64>();
2022
2023 let total_delinquent_stake = vote_accounts
2024 .delinquent
2025 .iter()
2026 .map(|vote_account| vote_account.activated_stake)
2027 .sum();
2028 let total_current_stake = total_active_stake.saturating_sub(total_delinquent_stake);
2029
2030 let current_validators: Vec<CliValidator> = vote_accounts
2031 .current
2032 .iter()
2033 .map(|vote_account| {
2034 CliValidator::new(
2035 vote_account,
2036 epoch_info.epoch,
2037 node_version
2038 .get(&vote_account.node_pubkey)
2039 .cloned()
2040 .unwrap_or_else(CliVersion::unknown_version),
2041 skip_rate.get(&vote_account.node_pubkey).cloned(),
2042 &config.address_labels,
2043 )
2044 })
2045 .collect();
2046 let delinquent_validators: Vec<CliValidator> = vote_accounts
2047 .delinquent
2048 .iter()
2049 .map(|vote_account| {
2050 CliValidator::new_delinquent(
2051 vote_account,
2052 epoch_info.epoch,
2053 node_version
2054 .get(&vote_account.node_pubkey)
2055 .cloned()
2056 .unwrap_or_else(CliVersion::unknown_version),
2057 skip_rate.get(&vote_account.node_pubkey).cloned(),
2058 &config.address_labels,
2059 )
2060 })
2061 .collect();
2062
2063 let mut stake_by_version: BTreeMap<CliVersion, CliValidatorsStakeByVersion> = BTreeMap::new();
2064 for validator in current_validators.iter() {
2065 let CliValidatorsStakeByVersion {
2066 current_validators,
2067 current_active_stake,
2068 ..
2069 } = stake_by_version
2070 .entry(validator.version.clone())
2071 .or_default();
2072 *current_validators = current_validators.saturating_add(1);
2073 *current_active_stake = current_active_stake.saturating_add(validator.activated_stake);
2074 }
2075 for validator in delinquent_validators.iter() {
2076 let CliValidatorsStakeByVersion {
2077 delinquent_validators,
2078 delinquent_active_stake,
2079 ..
2080 } = stake_by_version
2081 .entry(validator.version.clone())
2082 .or_default();
2083 *delinquent_validators = delinquent_validators.saturating_add(1);
2084 *delinquent_active_stake =
2085 delinquent_active_stake.saturating_add(validator.activated_stake);
2086 }
2087
2088 let validators: Vec<_> = current_validators
2089 .into_iter()
2090 .chain(delinquent_validators)
2091 .collect();
2092
2093 let (average_skip_rate, average_stake_weighted_skip_rate) = {
2094 let mut skip_rate_len: u64 = 0;
2095 let mut skip_rate_sum = 0.;
2096 let mut skip_rate_weighted_sum = 0.;
2097 for validator in validators.iter() {
2098 if let Some(skip_rate) = validator.skip_rate {
2099 skip_rate_sum += skip_rate;
2100 skip_rate_len = skip_rate_len.saturating_add(1);
2101 skip_rate_weighted_sum += skip_rate * validator.activated_stake as f64;
2102 }
2103 }
2104
2105 if skip_rate_len > 0 && total_active_stake > 0 {
2106 (
2107 skip_rate_sum / skip_rate_len as f64,
2108 skip_rate_weighted_sum / total_active_stake as f64,
2109 )
2110 } else {
2111 (100., 100.) }
2113 };
2114
2115 let cli_validators = CliValidators {
2116 total_active_stake,
2117 total_current_stake,
2118 total_delinquent_stake,
2119 validators,
2120 average_skip_rate,
2121 average_stake_weighted_skip_rate,
2122 validators_sort_order,
2123 validators_reverse_sort,
2124 number_validators,
2125 stake_by_version,
2126 use_lamports_unit,
2127 };
2128 Ok(config.output_format.formatted_string(&cli_validators))
2129}
2130
2131pub fn process_transaction_history(
2132 rpc_client: &RpcClient,
2133 config: &CliConfig,
2134 address: &Pubkey,
2135 before: Option<Signature>,
2136 until: Option<Signature>,
2137 limit: usize,
2138 show_transactions: bool,
2139) -> ProcessResult {
2140 let results = rpc_client.get_signatures_for_address_with_config(
2141 address,
2142 GetConfirmedSignaturesForAddress2Config {
2143 before,
2144 until,
2145 limit: Some(limit),
2146 commitment: Some(CommitmentConfig::confirmed()),
2147 },
2148 )?;
2149
2150 if !show_transactions {
2151 let cli_signatures: Vec<_> = results
2152 .into_iter()
2153 .map(|result| {
2154 let mut signature = CliHistorySignature {
2155 signature: result.signature,
2156 ..CliHistorySignature::default()
2157 };
2158 if config.verbose {
2159 signature.verbose = Some(CliHistoryVerbose {
2160 slot: result.slot,
2161 block_time: result.block_time,
2162 err: result.err,
2163 confirmation_status: result.confirmation_status,
2164 memo: result.memo,
2165 });
2166 }
2167 signature
2168 })
2169 .collect();
2170 Ok(config
2171 .output_format
2172 .formatted_string(&CliHistorySignatureVec::new(cli_signatures)))
2173 } else {
2174 let mut cli_transactions = vec![];
2175 for result in results {
2176 if let Ok(signature) = result.signature.parse::<Signature>() {
2177 let mut transaction = None;
2178 let mut get_transaction_error = None;
2179 match rpc_client.get_transaction_with_config(
2180 &signature,
2181 RpcTransactionConfig {
2182 encoding: Some(UiTransactionEncoding::Base64),
2183 commitment: Some(CommitmentConfig::confirmed()),
2184 max_supported_transaction_version: Some(0),
2185 },
2186 ) {
2187 Ok(confirmed_transaction) => {
2188 let EncodedConfirmedTransactionWithStatusMeta {
2189 block_time,
2190 slot,
2191 transaction: transaction_with_meta,
2192 } = confirmed_transaction;
2193
2194 let decoded_transaction =
2195 transaction_with_meta.transaction.decode().unwrap();
2196 let json_transaction = decoded_transaction.json_encode();
2197
2198 transaction = Some(CliTransaction {
2199 transaction: json_transaction,
2200 meta: transaction_with_meta.meta,
2201 block_time,
2202 slot: Some(slot),
2203 decoded_transaction,
2204 prefix: " ".to_string(),
2205 sigverify_status: vec![],
2206 });
2207 }
2208 Err(err) => {
2209 get_transaction_error = Some(format!("{err:?}"));
2210 }
2211 };
2212 cli_transactions.push(CliTransactionConfirmation {
2213 confirmation_status: result.confirmation_status,
2214 transaction,
2215 get_transaction_error,
2216 err: result.err,
2217 });
2218 }
2219 }
2220 Ok(config
2221 .output_format
2222 .formatted_string(&CliHistoryTransactionVec::new(cli_transactions)))
2223 }
2224}
2225
2226#[derive(Serialize, Deserialize)]
2227#[serde(rename_all = "camelCase")]
2228struct CliRentCalculation {
2229 pub lamports_per_byte_year: u64,
2232 pub lamports_per_epoch: u64,
2233 pub rent_exempt_minimum_lamports: u64,
2234 #[serde(skip)]
2235 pub use_lamports_unit: bool,
2236}
2237
2238impl CliRentCalculation {
2239 fn build_balance_message(&self, lamports: u64) -> String {
2240 build_balance_message(lamports, self.use_lamports_unit, true)
2241 }
2242}
2243
2244impl fmt::Display for CliRentCalculation {
2245 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2246 let exempt_minimum = self.build_balance_message(self.rent_exempt_minimum_lamports);
2247 writeln_name_value(f, "Rent-exempt minimum:", &exempt_minimum)
2248 }
2249}
2250
2251impl QuietDisplay for CliRentCalculation {}
2252impl VerboseDisplay for CliRentCalculation {}
2253
2254#[derive(Debug, PartialEq, Eq)]
2255pub enum RentLengthValue {
2256 Nonce,
2257 Stake,
2258 System,
2259 Vote,
2260 Bytes(usize),
2261}
2262
2263impl RentLengthValue {
2264 pub fn length(&self) -> usize {
2265 match self {
2266 Self::Nonce => NonceState::size(),
2267 Self::Stake => StakeStateV2::size_of(),
2268 Self::System => 0,
2269 Self::Vote => VoteStateV4::size_of(),
2270 Self::Bytes(l) => *l,
2271 }
2272 }
2273}
2274
2275#[derive(Debug, Error)]
2276#[error("expected number or moniker, got \"{0}\"")]
2277pub struct RentLengthValueError(pub String);
2278
2279impl FromStr for RentLengthValue {
2280 type Err = RentLengthValueError;
2281 fn from_str(s: &str) -> Result<Self, Self::Err> {
2282 let s = s.to_ascii_lowercase();
2283 match s.as_str() {
2284 "nonce" => Ok(Self::Nonce),
2285 "stake" => Ok(Self::Stake),
2286 "system" => Ok(Self::System),
2287 "vote" => Ok(Self::Vote),
2288 _ => usize::from_str(&s)
2289 .map(Self::Bytes)
2290 .map_err(|_| RentLengthValueError(s)),
2291 }
2292 }
2293}
2294
2295pub fn process_calculate_rent(
2296 rpc_client: &RpcClient,
2297 config: &CliConfig,
2298 data_length: usize,
2299 use_lamports_unit: bool,
2300) -> ProcessResult {
2301 if data_length > MAX_PERMITTED_DATA_LENGTH.try_into().unwrap() {
2302 eprintln!(
2303 "Warning: Maximum account size is {MAX_PERMITTED_DATA_LENGTH} bytes, {data_length} \
2304 provided"
2305 );
2306 }
2307 let rent_account = rpc_client.get_account(&sysvar::rent::id())?;
2308 let rent: Rent = rent_account.deserialize_data()?;
2309 let rent_exempt_minimum_lamports = rent.minimum_balance(data_length);
2310 let cli_rent_calculation = CliRentCalculation {
2311 lamports_per_byte_year: 0,
2312 lamports_per_epoch: 0,
2313 rent_exempt_minimum_lamports,
2314 use_lamports_unit,
2315 };
2316
2317 Ok(config.output_format.formatted_string(&cli_rent_calculation))
2318}
2319
2320#[cfg(test)]
2321mod tests {
2322 use {
2323 super::*,
2324 crate::{clap_app::get_clap_app, cli::parse_command},
2325 solana_keypair::{write_keypair, Keypair},
2326 std::str::FromStr,
2327 tempfile::NamedTempFile,
2328 };
2329
2330 fn make_tmp_file() -> (String, NamedTempFile) {
2331 let tmp_file = NamedTempFile::new().unwrap();
2332 (String::from(tmp_file.path().to_str().unwrap()), tmp_file)
2333 }
2334
2335 #[test]
2336 fn test_parse_command() {
2337 let test_commands = get_clap_app("test", "desc", "version");
2338 let default_keypair = Keypair::new();
2339 let (default_keypair_file, mut tmp_file) = make_tmp_file();
2340 write_keypair(&default_keypair, tmp_file.as_file_mut()).unwrap();
2341 let default_signer = DefaultSigner::new("", default_keypair_file);
2342
2343 let test_cluster_version = test_commands
2344 .clone()
2345 .get_matches_from(vec!["test", "cluster-date"]);
2346 assert_eq!(
2347 parse_command(&test_cluster_version, &default_signer, &mut None).unwrap(),
2348 CliCommandInfo::without_signers(CliCommand::ClusterDate)
2349 );
2350
2351 let test_cluster_version = test_commands
2352 .clone()
2353 .get_matches_from(vec!["test", "cluster-version"]);
2354 assert_eq!(
2355 parse_command(&test_cluster_version, &default_signer, &mut None).unwrap(),
2356 CliCommandInfo::without_signers(CliCommand::ClusterVersion)
2357 );
2358
2359 let slot = 100;
2360 let test_get_block_time =
2361 test_commands
2362 .clone()
2363 .get_matches_from(vec!["test", "block-time", &slot.to_string()]);
2364 assert_eq!(
2365 parse_command(&test_get_block_time, &default_signer, &mut None).unwrap(),
2366 CliCommandInfo::without_signers(CliCommand::GetBlockTime { slot: Some(slot) })
2367 );
2368
2369 let test_get_epoch = test_commands
2370 .clone()
2371 .get_matches_from(vec!["test", "epoch"]);
2372 assert_eq!(
2373 parse_command(&test_get_epoch, &default_signer, &mut None).unwrap(),
2374 CliCommandInfo::without_signers(CliCommand::GetEpoch)
2375 );
2376
2377 let test_get_epoch_info = test_commands
2378 .clone()
2379 .get_matches_from(vec!["test", "epoch-info"]);
2380 assert_eq!(
2381 parse_command(&test_get_epoch_info, &default_signer, &mut None).unwrap(),
2382 CliCommandInfo::without_signers(CliCommand::GetEpochInfo)
2383 );
2384
2385 let test_get_genesis_hash = test_commands
2386 .clone()
2387 .get_matches_from(vec!["test", "genesis-hash"]);
2388 assert_eq!(
2389 parse_command(&test_get_genesis_hash, &default_signer, &mut None).unwrap(),
2390 CliCommandInfo::without_signers(CliCommand::GetGenesisHash)
2391 );
2392
2393 let test_get_slot = test_commands.clone().get_matches_from(vec!["test", "slot"]);
2394 assert_eq!(
2395 parse_command(&test_get_slot, &default_signer, &mut None).unwrap(),
2396 CliCommandInfo::without_signers(CliCommand::GetSlot)
2397 );
2398
2399 let test_total_supply = test_commands
2400 .clone()
2401 .get_matches_from(vec!["test", "total-supply"]);
2402 assert_eq!(
2403 parse_command(&test_total_supply, &default_signer, &mut None).unwrap(),
2404 CliCommandInfo::without_signers(CliCommand::TotalSupply)
2405 );
2406
2407 let test_transaction_count = test_commands
2408 .clone()
2409 .get_matches_from(vec!["test", "transaction-count"]);
2410 assert_eq!(
2411 parse_command(&test_transaction_count, &default_signer, &mut None).unwrap(),
2412 CliCommandInfo::without_signers(CliCommand::GetTransactionCount)
2413 );
2414
2415 let test_ping = test_commands.clone().get_matches_from(vec![
2416 "test",
2417 "ping",
2418 "-i",
2419 "1",
2420 "-c",
2421 "2",
2422 "-t",
2423 "3",
2424 "-D",
2425 "--blockhash",
2426 "4CCNp28j6AhGq7PkjPDP4wbQWBS8LLbQin2xV5n8frKX",
2427 ]);
2428 assert_eq!(
2429 parse_command(&test_ping, &default_signer, &mut None).unwrap(),
2430 CliCommandInfo {
2431 command: CliCommand::Ping {
2432 interval: Duration::from_secs(1),
2433 count: Some(2),
2434 timeout: Duration::from_secs(3),
2435 blockhash: Some(
2436 Hash::from_str("4CCNp28j6AhGq7PkjPDP4wbQWBS8LLbQin2xV5n8frKX").unwrap()
2437 ),
2438 print_timestamp: true,
2439 compute_unit_price: None,
2440 },
2441 signers: vec![Box::new(default_keypair)],
2442 }
2443 );
2444 }
2445}