Skip to main content

solana_cli/
cluster_query.rs

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 there is no explicitly specified --our-localhost,
453    // disable the guess mode (= our_localhost_port)
454    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    // requirement of node_pubkey is relaxed only if our_localhost_port
459    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                // go to new line to leave this message on console
654                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                    // go to new line to leave this message on console
673                    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                        // go to new line to leave this message on console
759                        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        // humbly retry; the reference node (rpc_client) could be spotty,
794        // especially if pointing to api.meinnet-beta.solana.com at times
795        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                // important not to miss next scheduled lead slots
838                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            // Fast, more reliable path using the SlotHistory sysvar
1222
1223            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            // Slow, less reliable path using `getBlocks`.
1229            //
1230            // "less reliable" because if the RPC node has holds in its ledger then the block production data will be
1231            // incorrect.  This condition currently can't be detected over RPC
1232            //
1233
1234            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                    //
1524                    // if slot has advanced out of step with the root, we detect
1525                    // a mismatch and output the slot information
1526                    //
1527                    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    // Both vote and identity pubkeys are supported to identify validator stakes.
1579    // For identity pubkeys, fetch corresponding vote pubkey.
1580    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    // Use server-side filtering if only one vote account is provided
1629    if vote_account_pubkeys.len() == 1 {
1630        let filter_pubkey = vote_account_pubkeys.iter().next().unwrap();
1631        program_accounts_config.filters = Some(vec![
1632            // Filter by `StakeStateV2::Stake(_, _)`
1633            RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &[2, 0, 0, 0])),
1634            // Filter by `Delegation::voter_pubkey`, which begins at byte offset 124
1635            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        // withdrawer filter
1641        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 clock: Clock = from_account(&clock_account).ok_or_else(|| {
1655        CliError::RpcRequestError("Failed to deserialize clock sysvar".to_string())
1656    })?;
1657    let stake_history = from_account(&stake_history_account).ok_or_else(|| {
1658        CliError::RpcRequestError("Failed to deserialize stake history".to_string())
1659    })?;
1660    let new_rate_activation_epoch = get_feature_activation_epoch(
1661        rpc_client,
1662        &agave_feature_set::reduce_stake_warmup_cooldown::id(),
1663    )
1664    .await?;
1665    stake_account_progress_bar.finish_and_clear();
1666
1667    let mut stake_accounts: Vec<CliKeyedStakeState> = vec![];
1668    for (stake_pubkey, stake_ui_account) in all_stake_accounts {
1669        let stake_account = stake_ui_account.to_account().expect(
1670            "It should be impossible at this point for the account data not to be decodable. \
1671             Ensure that the account was fetched using a binary encoding.",
1672        );
1673        if let Ok(stake_state) = stake_account.state() {
1674            let rent_exempt_balance = rpc_client
1675                .get_minimum_balance_for_rent_exemption(stake_account.data.len())
1676                .await?;
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.) // Impossible?
1907        }
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    // lamports_per_* fields are deprecated since all accounts must be rent
2032    // exempt; however, they are kept here for the sake of compatibility.
2033    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}