Skip to main content

solana_cli/
cluster_query.rs

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