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