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