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