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