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