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