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