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