Skip to main content

solana_cli/
cluster_query.rs

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