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