Skip to main content

solana_cli/
cluster_query.rs

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