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