Skip to main content

solana_cli/
cluster_query.rs

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