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