1use {
2 crate::{
3 cli::{
4 log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError,
5 ProcessResult,
6 },
7 spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
8 },
9 clap::{value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand},
10 console::style,
11 miraland_clap_utils::{
12 fee_payer::*, hidden_unless_forced, input_parsers::*, input_validators::*, keypair::*,
13 },
14 miraland_cli_output::{cli_version::CliVersion, QuietDisplay, VerboseDisplay},
15 miraland_remote_wallet::remote_wallet::RemoteWalletManager,
16 miraland_rpc_client::rpc_client::RpcClient,
17 miraland_rpc_client_api::{client_error::Error as ClientError, request::MAX_MULTIPLE_ACCOUNTS},
18 serde::{Deserialize, Serialize},
19 miraland_sdk::{
20 account::Account,
21 clock::Slot,
22 epoch_schedule::EpochSchedule,
23 feature::{self, Feature},
24 feature_set::FEATURE_NAMES,
25 genesis_config::ClusterType,
26 message::Message,
27 pubkey::Pubkey,
28 stake_history::Epoch,
29 system_instruction::SystemError,
30 transaction::Transaction,
31 },
32 std::{cmp::Ordering, collections::HashMap, fmt, rc::Rc, str::FromStr},
33};
34
35const DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS: Slot = 15_000_000; #[derive(Copy, Clone, Debug, PartialEq, Eq)]
38pub enum ForceActivation {
39 No,
40 Almost,
41 Yes,
42}
43
44#[derive(Debug, PartialEq, Eq)]
45pub enum FeatureCliCommand {
46 Status {
47 features: Vec<Pubkey>,
48 display_all: bool,
49 },
50 Activate {
51 feature: Pubkey,
52 cluster: ClusterType,
53 force: ForceActivation,
54 fee_payer: SignerIndex,
55 },
56}
57
58#[derive(Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "camelCase", tag = "status", content = "sinceSlot")]
60pub enum CliFeatureStatus {
61 Inactive,
62 Pending,
63 Active(Slot),
64}
65
66impl PartialOrd for CliFeatureStatus {
67 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
68 Some(self.cmp(other))
69 }
70}
71
72impl Ord for CliFeatureStatus {
73 fn cmp(&self, other: &Self) -> Ordering {
74 match (self, other) {
75 (Self::Inactive, Self::Inactive) => Ordering::Equal,
76 (Self::Inactive, _) => Ordering::Greater,
77 (_, Self::Inactive) => Ordering::Less,
78 (Self::Pending, Self::Pending) => Ordering::Equal,
79 (Self::Pending, _) => Ordering::Greater,
80 (_, Self::Pending) => Ordering::Less,
81 (Self::Active(self_active_slot), Self::Active(other_active_slot)) => {
82 self_active_slot.cmp(other_active_slot)
83 }
84 }
85 }
86}
87
88#[derive(Serialize, Deserialize, PartialEq, Eq)]
89#[serde(rename_all = "camelCase")]
90pub struct CliFeature {
91 pub id: String,
92 pub description: String,
93 #[serde(flatten)]
94 pub status: CliFeatureStatus,
95}
96
97impl PartialOrd for CliFeature {
98 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
99 Some(self.cmp(other))
100 }
101}
102
103impl Ord for CliFeature {
104 fn cmp(&self, other: &Self) -> Ordering {
105 (&self.status, &self.id).cmp(&(&other.status, &other.id))
106 }
107}
108
109#[derive(Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct CliFeatures {
112 pub features: Vec<CliFeature>,
113 #[serde(skip)]
114 pub epoch_schedule: EpochSchedule,
115 #[serde(skip)]
116 pub current_slot: Slot,
117 pub feature_activation_allowed: bool,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub cluster_feature_sets: Option<CliClusterFeatureSets>,
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub cluster_software_versions: Option<CliClusterSoftwareVersions>,
122 #[serde(skip)]
123 pub inactive: bool,
124}
125
126impl fmt::Display for CliFeatures {
127 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
128 if !self.features.is_empty() {
129 writeln!(
130 f,
131 "{}",
132 style(format!(
133 "{:<44} | {:<23} | {} | {}",
134 "Feature", "Status", "Activation Slot", "Description"
135 ))
136 .bold()
137 )?;
138 }
139 for feature in &self.features {
140 writeln!(
141 f,
142 "{:<44} | {:<23} | {:<15} | {}",
143 feature.id,
144 match feature.status {
145 CliFeatureStatus::Inactive => style("inactive".to_string()).red(),
146 CliFeatureStatus::Pending => {
147 let current_epoch = self.epoch_schedule.get_epoch(self.current_slot);
148 style(format!("pending until epoch {}", current_epoch + 1)).yellow()
149 }
150 CliFeatureStatus::Active(activation_slot) => {
151 let activation_epoch = self.epoch_schedule.get_epoch(activation_slot);
152 style(format!("active since epoch {activation_epoch}")).green()
153 }
154 },
155 match feature.status {
156 CliFeatureStatus::Active(activation_slot) => activation_slot.to_string(),
157 _ => "NA".to_string(),
158 },
159 feature.description,
160 )?;
161 }
162
163 if let Some(software_versions) = &self.cluster_software_versions {
164 write!(f, "{software_versions}")?;
165 }
166
167 if let Some(feature_sets) = &self.cluster_feature_sets {
168 write!(f, "{feature_sets}")?;
169 }
170
171 if self.inactive && !self.feature_activation_allowed {
172 writeln!(
173 f,
174 "{}",
175 style("\nFeature activation is not allowed at this time")
176 .bold()
177 .red()
178 )?;
179 }
180 Ok(())
181 }
182}
183
184impl QuietDisplay for CliFeatures {}
185impl VerboseDisplay for CliFeatures {}
186
187#[derive(Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct CliClusterFeatureSets {
190 pub tool_feature_set: u32,
191 pub feature_sets: Vec<CliFeatureSetStats>,
192 #[serde(skip)]
193 pub stake_allowed: bool,
194 #[serde(skip)]
195 pub rpc_allowed: bool,
196}
197
198#[derive(Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct CliClusterSoftwareVersions {
201 tool_software_version: CliVersion,
202 software_versions: Vec<CliSoftwareVersionStats>,
203}
204
205impl fmt::Display for CliClusterSoftwareVersions {
206 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
207 let software_version_title = "Software Version";
208 let stake_percent_title = "Stake";
209 let rpc_percent_title = "RPC";
210 let mut max_software_version_len = software_version_title.len();
211 let mut max_stake_percent_len = stake_percent_title.len();
212 let mut max_rpc_percent_len = rpc_percent_title.len();
213
214 let software_versions: Vec<_> = self
215 .software_versions
216 .iter()
217 .map(|software_version_stats| {
218 let stake_percent = format!("{:.2}%", software_version_stats.stake_percent);
219 let rpc_percent = format!("{:.2}%", software_version_stats.rpc_percent);
220 let software_version = software_version_stats.software_version.to_string();
221
222 max_software_version_len = max_software_version_len.max(software_version.len());
223 max_stake_percent_len = max_stake_percent_len.max(stake_percent.len());
224 max_rpc_percent_len = max_rpc_percent_len.max(rpc_percent.len());
225
226 (software_version, stake_percent, rpc_percent)
227 })
228 .collect();
229
230 writeln!(
231 f,
232 "\n\n{}",
233 style(format!(
234 "Tool Software Version: {}",
235 self.tool_software_version
236 ))
237 .bold()
238 )?;
239 writeln!(
240 f,
241 "{}",
242 style(format!(
243 "{software_version_title:<max_software_version_len$} {stake_percent_title:>max_stake_percent_len$} {rpc_percent_title:>max_rpc_percent_len$}",
244 ))
245 .bold(),
246 )?;
247 for (software_version, stake_percent, rpc_percent) in software_versions {
248 let me = self.tool_software_version.to_string() == software_version;
249 writeln!(
250 f,
251 "{1:<0$} {3:>2$} {5:>4$} {6}",
252 max_software_version_len,
253 software_version,
254 max_stake_percent_len,
255 stake_percent,
256 max_rpc_percent_len,
257 rpc_percent,
258 if me { "<-- me" } else { "" },
259 )?;
260 }
261 writeln!(f)
262 }
263}
264
265impl fmt::Display for CliClusterFeatureSets {
266 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
267 let mut tool_feature_set_matches_cluster = false;
268
269 let software_versions_title = "Software Version";
270 let feature_set_title = "Feature Set";
271 let stake_percent_title = "Stake";
272 let rpc_percent_title = "RPC";
273 let mut max_software_versions_len = software_versions_title.len();
274 let mut max_feature_set_len = feature_set_title.len();
275 let mut max_stake_percent_len = stake_percent_title.len();
276 let mut max_rpc_percent_len = rpc_percent_title.len();
277
278 let feature_sets: Vec<_> = self
279 .feature_sets
280 .iter()
281 .map(|feature_set_info| {
282 let me = if self.tool_feature_set == feature_set_info.feature_set {
283 tool_feature_set_matches_cluster = true;
284 true
285 } else {
286 false
287 };
288 let software_versions: Vec<_> = feature_set_info
289 .software_versions
290 .iter()
291 .map(ToString::to_string)
292 .collect();
293 let software_versions = software_versions.join(", ");
294 let feature_set = if feature_set_info.feature_set == 0 {
295 "unknown".to_string()
296 } else {
297 feature_set_info.feature_set.to_string()
298 };
299 let stake_percent = format!("{:.2}%", feature_set_info.stake_percent);
300 let rpc_percent = format!("{:.2}%", feature_set_info.rpc_percent);
301
302 max_software_versions_len = max_software_versions_len.max(software_versions.len());
303 max_feature_set_len = max_feature_set_len.max(feature_set.len());
304 max_stake_percent_len = max_stake_percent_len.max(stake_percent.len());
305 max_rpc_percent_len = max_rpc_percent_len.max(rpc_percent.len());
306
307 (
308 software_versions,
309 feature_set,
310 stake_percent,
311 rpc_percent,
312 me,
313 )
314 })
315 .collect();
316
317 if !tool_feature_set_matches_cluster {
318 writeln!(
319 f,
320 "\n{}",
321 style("To activate features the tool and cluster feature sets must match, select a tool version that matches the cluster")
322 .bold())?;
323 } else {
324 if !self.stake_allowed {
325 write!(
326 f,
327 "\n{}",
328 style("To activate features the stake must be >= 95%")
329 .bold()
330 .red()
331 )?;
332 }
333 if !self.rpc_allowed {
334 write!(
335 f,
336 "\n{}",
337 style("To activate features the RPC nodes must be >= 95%")
338 .bold()
339 .red()
340 )?;
341 }
342 }
343 writeln!(
344 f,
345 "\n\n{}",
346 style(format!("Tool Feature Set: {}", self.tool_feature_set)).bold()
347 )?;
348 writeln!(
349 f,
350 "{}",
351 style(format!(
352 "{software_versions_title:<max_software_versions_len$} {feature_set_title:<max_feature_set_len$} {stake_percent_title:>max_stake_percent_len$} {rpc_percent_title:>max_rpc_percent_len$}",
353 ))
354 .bold(),
355 )?;
356 for (software_versions, feature_set, stake_percent, rpc_percent, me) in feature_sets {
357 writeln!(
358 f,
359 "{1:<0$} {3:>2$} {5:>4$} {7:>6$} {8}",
360 max_software_versions_len,
361 software_versions,
362 max_feature_set_len,
363 feature_set,
364 max_stake_percent_len,
365 stake_percent,
366 max_rpc_percent_len,
367 rpc_percent,
368 if me { "<-- me" } else { "" },
369 )?;
370 }
371 writeln!(f)
372 }
373}
374
375impl QuietDisplay for CliClusterFeatureSets {}
376impl VerboseDisplay for CliClusterFeatureSets {}
377
378#[derive(Serialize, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct CliFeatureSetStats {
381 software_versions: Vec<CliVersion>,
382 feature_set: u32,
383 stake_percent: f64,
384 rpc_percent: f32,
385}
386
387#[derive(Serialize, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct CliSoftwareVersionStats {
390 software_version: CliVersion,
391 stake_percent: f64,
392 rpc_percent: f32,
393}
394
395fn check_rpc_genesis_hash(
397 cluster_type: &ClusterType,
398 rpc_client: &RpcClient,
399) -> Result<(), Box<dyn std::error::Error>> {
400 if let Some(genesis_hash) = cluster_type.get_genesis_hash() {
401 let rpc_genesis_hash = rpc_client.get_genesis_hash()?;
402 if rpc_genesis_hash != genesis_hash {
403 return Err(format!(
404 "The genesis hash for the specified cluster {cluster_type:?} does not match the \
405 genesis hash reported by the specified RPC. Cluster genesis hash: {genesis_hash}, \
406 RPC reported genesis hash: {rpc_genesis_hash}"
407 )
408 .into());
409 }
410 }
411 Ok(())
412}
413
414pub trait FeatureSubCommands {
415 fn feature_subcommands(self) -> Self;
416}
417
418impl FeatureSubCommands for App<'_, '_> {
419 fn feature_subcommands(self) -> Self {
420 self.subcommand(
421 SubCommand::with_name("feature")
422 .about("Runtime feature management")
423 .setting(AppSettings::SubcommandRequiredElseHelp)
424 .subcommand(
425 SubCommand::with_name("status")
426 .about("Query runtime feature status")
427 .arg(
428 Arg::with_name("features")
429 .value_name("ADDRESS")
430 .validator(is_valid_pubkey)
431 .index(1)
432 .multiple(true)
433 .help("Feature status to query [default: all known features]"),
434 )
435 .arg(
436 Arg::with_name("display_all")
437 .long("display-all")
438 .help("display all features regardless of age"),
439 ),
440 )
441 .subcommand(
442 SubCommand::with_name("activate")
443 .about("Activate a runtime feature")
444 .arg(
445 Arg::with_name("feature")
446 .value_name("FEATURE_KEYPAIR")
447 .validator(is_valid_signer)
448 .index(1)
449 .required(true)
450 .help("The signer for the feature to activate"),
451 )
452 .arg(
453 Arg::with_name("cluster")
454 .value_name("CLUSTER")
455 .possible_values(&ClusterType::STRINGS)
456 .required(true)
457 .help("The cluster to activate the feature on"),
458 )
459 .arg(
460 Arg::with_name("force")
461 .long("yolo")
462 .hidden(hidden_unless_forced())
463 .multiple(true)
464 .help("Override activation sanity checks. Don't use this flag"),
465 )
466 .arg(fee_payer_arg()),
467 ),
468 )
469 }
470}
471
472fn known_feature(feature: &Pubkey) -> Result<(), CliError> {
473 if FEATURE_NAMES.contains_key(feature) {
474 Ok(())
475 } else {
476 Err(CliError::BadParameter(format!(
477 "Unknown feature: {feature}"
478 )))
479 }
480}
481
482pub fn parse_feature_subcommand(
483 matches: &ArgMatches<'_>,
484 default_signer: &DefaultSigner,
485 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
486) -> Result<CliCommandInfo, CliError> {
487 let response = match matches.subcommand() {
488 ("activate", Some(matches)) => {
489 let cluster = value_t_or_exit!(matches, "cluster", ClusterType);
490 let (feature_signer, feature) = signer_of(matches, "feature", wallet_manager)?;
491 let (fee_payer, fee_payer_pubkey) =
492 signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?;
493
494 let force = match matches.occurrences_of("force") {
495 2 => ForceActivation::Yes,
496 1 => ForceActivation::Almost,
497 _ => ForceActivation::No,
498 };
499
500 let signer_info = default_signer.generate_unique_signers(
501 vec![fee_payer, feature_signer],
502 matches,
503 wallet_manager,
504 )?;
505
506 let feature = feature.unwrap();
507
508 known_feature(&feature)?;
509
510 CliCommandInfo {
511 command: CliCommand::Feature(FeatureCliCommand::Activate {
512 feature,
513 cluster,
514 force,
515 fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
516 }),
517 signers: signer_info.signers,
518 }
519 }
520 ("status", Some(matches)) => {
521 let mut features = if let Some(features) = pubkeys_of(matches, "features") {
522 for feature in &features {
523 known_feature(feature)?;
524 }
525 features
526 } else {
527 FEATURE_NAMES.keys().cloned().collect()
528 };
529 let display_all =
530 matches.is_present("display_all") || features.len() < FEATURE_NAMES.len();
531 features.sort();
532 CliCommandInfo {
533 command: CliCommand::Feature(FeatureCliCommand::Status {
534 features,
535 display_all,
536 }),
537 signers: vec![],
538 }
539 }
540 _ => unreachable!(),
541 };
542 Ok(response)
543}
544
545pub fn process_feature_subcommand(
546 rpc_client: &RpcClient,
547 config: &CliConfig,
548 feature_subcommand: &FeatureCliCommand,
549) -> ProcessResult {
550 match feature_subcommand {
551 FeatureCliCommand::Status {
552 features,
553 display_all,
554 } => process_status(rpc_client, config, features, *display_all),
555 FeatureCliCommand::Activate {
556 feature,
557 cluster,
558 force,
559 fee_payer,
560 } => process_activate(rpc_client, config, *feature, *cluster, *force, *fee_payer),
561 }
562}
563
564#[derive(Debug, Default)]
565struct FeatureSetStatsEntry {
566 stake_percent: f64,
567 rpc_nodes_percent: f32,
568 software_versions: Vec<CliVersion>,
569}
570
571#[derive(Debug, Default, Clone, Copy)]
572struct ClusterInfoStatsEntry {
573 stake_percent: f64,
574 rpc_percent: f32,
575}
576
577struct ClusterInfoStats {
578 stats_map: HashMap<(u32, CliVersion), ClusterInfoStatsEntry>,
579}
580
581impl ClusterInfoStats {
582 fn aggregate_by_feature_set(&self) -> HashMap<u32, FeatureSetStatsEntry> {
583 let mut feature_set_map = HashMap::<u32, FeatureSetStatsEntry>::new();
584 for ((feature_set, software_version), stats_entry) in &self.stats_map {
585 let map_entry = feature_set_map.entry(*feature_set).or_default();
586 map_entry.rpc_nodes_percent += stats_entry.rpc_percent;
587 map_entry.stake_percent += stats_entry.stake_percent;
588 map_entry.software_versions.push(software_version.clone());
589 }
590 for stats_entry in feature_set_map.values_mut() {
591 stats_entry
592 .software_versions
593 .sort_by(|l, r| l.cmp(r).reverse());
594 }
595 feature_set_map
596 }
597
598 fn aggregate_by_software_version(&self) -> HashMap<CliVersion, ClusterInfoStatsEntry> {
599 let mut software_version_map = HashMap::<CliVersion, ClusterInfoStatsEntry>::new();
600 for ((_feature_set, software_version), stats_entry) in &self.stats_map {
601 let map_entry = software_version_map
602 .entry(software_version.clone())
603 .or_default();
604 map_entry.rpc_percent += stats_entry.rpc_percent;
605 map_entry.stake_percent += stats_entry.stake_percent;
606 }
607 software_version_map
608 }
609}
610
611fn cluster_info_stats(rpc_client: &RpcClient) -> Result<ClusterInfoStats, ClientError> {
612 #[derive(Default)]
613 struct StatsEntry {
614 stake_lamports: u64,
615 rpc_nodes_count: u32,
616 }
617
618 let cluster_info_list = rpc_client
619 .get_cluster_nodes()?
620 .into_iter()
621 .map(|contact_info| {
622 (
623 contact_info.pubkey,
624 contact_info.feature_set,
625 contact_info.rpc.is_some(),
626 contact_info
627 .version
628 .and_then(|v| CliVersion::from_str(&v).ok())
629 .unwrap_or_else(CliVersion::unknown_version),
630 )
631 })
632 .collect::<Vec<_>>();
633
634 let vote_accounts = rpc_client.get_vote_accounts()?;
635
636 let mut total_active_stake: u64 = vote_accounts
637 .delinquent
638 .iter()
639 .map(|vote_account| vote_account.activated_stake)
640 .sum();
641
642 let vote_stakes = vote_accounts
643 .current
644 .into_iter()
645 .map(|vote_account| {
646 total_active_stake += vote_account.activated_stake;
647 (vote_account.node_pubkey, vote_account.activated_stake)
648 })
649 .collect::<HashMap<_, _>>();
650
651 let mut cluster_info_stats: HashMap<(u32, CliVersion), StatsEntry> = HashMap::new();
652 let mut total_rpc_nodes = 0;
653 for (node_id, feature_set, is_rpc, version) in cluster_info_list {
654 let feature_set = feature_set.unwrap_or(0);
655 let stats_entry = cluster_info_stats
656 .entry((feature_set, version))
657 .or_default();
658
659 if let Some(vote_stake) = vote_stakes.get(&node_id) {
660 stats_entry.stake_lamports += *vote_stake;
661 }
662
663 if is_rpc {
664 stats_entry.rpc_nodes_count += 1;
665 total_rpc_nodes += 1;
666 }
667 }
668
669 Ok(ClusterInfoStats {
670 stats_map: cluster_info_stats
671 .into_iter()
672 .filter_map(
673 |(
674 cluster_config,
675 StatsEntry {
676 stake_lamports,
677 rpc_nodes_count,
678 },
679 )| {
680 let stake_percent = (stake_lamports as f64 / total_active_stake as f64) * 100.;
681 let rpc_percent = (rpc_nodes_count as f32 / total_rpc_nodes as f32) * 100.;
682 if stake_percent >= 0.001 || rpc_percent >= 0.001 {
683 Some((
684 cluster_config,
685 ClusterInfoStatsEntry {
686 stake_percent,
687 rpc_percent,
688 },
689 ))
690 } else {
691 None
692 }
693 },
694 )
695 .collect(),
696 })
697}
698
699fn feature_activation_allowed(
701 rpc_client: &RpcClient,
702 quiet: bool,
703) -> Result<
704 (
705 bool,
706 Option<CliClusterFeatureSets>,
707 Option<CliClusterSoftwareVersions>,
708 ),
709 ClientError,
710> {
711 let cluster_info_stats = cluster_info_stats(rpc_client)?;
712 let feature_set_stats = cluster_info_stats.aggregate_by_feature_set();
713
714 let tool_version = miraland_version::Version::default();
715 let tool_feature_set = tool_version.feature_set;
716 let tool_software_version = CliVersion::from(semver::Version::new(
717 tool_version.major as u64,
718 tool_version.minor as u64,
719 tool_version.patch as u64,
720 ));
721 let (stake_allowed, rpc_allowed) = feature_set_stats
722 .get(&tool_feature_set)
723 .map(
724 |FeatureSetStatsEntry {
725 stake_percent,
726 rpc_nodes_percent,
727 ..
728 }| (*stake_percent >= 95., *rpc_nodes_percent >= 95.),
729 )
730 .unwrap_or_default();
731
732 let cluster_software_versions = if quiet {
733 None
734 } else {
735 let mut software_versions: Vec<_> = cluster_info_stats
736 .aggregate_by_software_version()
737 .into_iter()
738 .map(|(software_version, stats)| CliSoftwareVersionStats {
739 software_version,
740 stake_percent: stats.stake_percent,
741 rpc_percent: stats.rpc_percent,
742 })
743 .collect();
744 software_versions.sort_by(|l, r| l.software_version.cmp(&r.software_version).reverse());
745 Some(CliClusterSoftwareVersions {
746 software_versions,
747 tool_software_version,
748 })
749 };
750
751 let cluster_feature_sets = if quiet {
752 None
753 } else {
754 let mut feature_sets: Vec<_> = feature_set_stats
755 .into_iter()
756 .map(|(feature_set, stats_entry)| CliFeatureSetStats {
757 feature_set,
758 software_versions: stats_entry.software_versions,
759 rpc_percent: stats_entry.rpc_nodes_percent,
760 stake_percent: stats_entry.stake_percent,
761 })
762 .collect();
763
764 feature_sets.sort_by(|l, r| {
765 match l.software_versions[0]
766 .cmp(&r.software_versions[0])
767 .reverse()
768 {
769 Ordering::Equal => {
770 match l
771 .stake_percent
772 .partial_cmp(&r.stake_percent)
773 .unwrap()
774 .reverse()
775 {
776 Ordering::Equal => {
777 l.rpc_percent.partial_cmp(&r.rpc_percent).unwrap().reverse()
778 }
779 o => o,
780 }
781 }
782 o => o,
783 }
784 });
785 Some(CliClusterFeatureSets {
786 tool_feature_set,
787 feature_sets,
788 stake_allowed,
789 rpc_allowed,
790 })
791 };
792
793 Ok((
794 stake_allowed && rpc_allowed,
795 cluster_feature_sets,
796 cluster_software_versions,
797 ))
798}
799
800fn status_from_account(account: Account) -> Option<CliFeatureStatus> {
801 feature::from_account(&account).map(|feature| match feature.activated_at {
802 None => CliFeatureStatus::Pending,
803 Some(activation_slot) => CliFeatureStatus::Active(activation_slot),
804 })
805}
806
807fn get_feature_status(
808 rpc_client: &RpcClient,
809 feature_id: &Pubkey,
810) -> Result<Option<CliFeatureStatus>, Box<dyn std::error::Error>> {
811 rpc_client
812 .get_account(feature_id)
813 .map(status_from_account)
814 .map_err(|e| e.into())
815}
816
817pub fn get_feature_is_active(
818 rpc_client: &RpcClient,
819 feature_id: &Pubkey,
820) -> Result<bool, Box<dyn std::error::Error>> {
821 get_feature_status(rpc_client, feature_id)
822 .map(|status| matches!(status, Some(CliFeatureStatus::Active(_))))
823}
824
825pub fn get_feature_activation_epoch(
826 rpc_client: &RpcClient,
827 feature_id: &Pubkey,
828) -> Result<Option<Epoch>, ClientError> {
829 rpc_client
830 .get_feature_activation_slot(feature_id)
831 .and_then(|activation_slot: Option<Slot>| {
832 rpc_client
833 .get_epoch_schedule()
834 .map(|epoch_schedule| (activation_slot, epoch_schedule))
835 })
836 .map(|(activation_slot, epoch_schedule)| {
837 activation_slot.map(|slot| epoch_schedule.get_epoch(slot))
838 })
839}
840
841fn process_status(
842 rpc_client: &RpcClient,
843 config: &CliConfig,
844 feature_ids: &[Pubkey],
845 display_all: bool,
846) -> ProcessResult {
847 let current_slot = rpc_client.get_slot()?;
848 let filter = if !display_all {
849 current_slot.checked_sub(DEFAULT_MAX_ACTIVE_DISPLAY_AGE_SLOTS)
850 } else {
851 None
852 };
853 let mut inactive = false;
854 let mut features = vec![];
855 for feature_ids in feature_ids.chunks(MAX_MULTIPLE_ACCOUNTS) {
856 let mut feature_chunk = rpc_client
857 .get_multiple_accounts(feature_ids)?
858 .into_iter()
859 .zip(feature_ids)
860 .map(|(account, feature_id)| {
861 let feature_name = FEATURE_NAMES.get(feature_id).unwrap();
862 account
863 .and_then(status_from_account)
864 .map(|feature_status| CliFeature {
865 id: feature_id.to_string(),
866 description: feature_name.to_string(),
867 status: feature_status,
868 })
869 .unwrap_or_else(|| {
870 inactive = true;
871 CliFeature {
872 id: feature_id.to_string(),
873 description: feature_name.to_string(),
874 status: CliFeatureStatus::Inactive,
875 }
876 })
877 })
878 .filter(|feature| match (filter, &feature.status) {
879 (Some(min_activation), CliFeatureStatus::Active(activation)) => {
880 activation > &min_activation
881 }
882 _ => true,
883 })
884 .collect::<Vec<_>>();
885 features.append(&mut feature_chunk);
886 }
887
888 features.sort_unstable();
889
890 let (feature_activation_allowed, cluster_feature_sets, cluster_software_versions) =
891 feature_activation_allowed(rpc_client, features.len() <= 1)?;
892 let epoch_schedule = rpc_client.get_epoch_schedule()?;
893 let feature_set = CliFeatures {
894 features,
895 current_slot,
896 epoch_schedule,
897 feature_activation_allowed,
898 cluster_feature_sets,
899 cluster_software_versions,
900 inactive,
901 };
902 Ok(config.output_format.formatted_string(&feature_set))
903}
904
905fn process_activate(
906 rpc_client: &RpcClient,
907 config: &CliConfig,
908 feature_id: Pubkey,
909 cluster: ClusterType,
910 force: ForceActivation,
911 fee_payer: SignerIndex,
912) -> ProcessResult {
913 check_rpc_genesis_hash(&cluster, rpc_client)?;
914
915 let fee_payer = config.signers[fee_payer];
916 let account = rpc_client
917 .get_multiple_accounts(&[feature_id])?
918 .into_iter()
919 .next()
920 .unwrap();
921
922 if let Some(account) = account {
923 if feature::from_account(&account).is_some() {
924 return Err(format!("{feature_id} has already been activated").into());
925 }
926 }
927
928 if !feature_activation_allowed(rpc_client, false)?.0 {
929 match force {
930 ForceActivation::Almost =>
931 return Err("Add force argument once more to override the sanity check to force feature activation ".into()),
932 ForceActivation::Yes => println!("FEATURE ACTIVATION FORCED"),
933 ForceActivation::No =>
934 return Err("Feature activation is not allowed at this time".into()),
935 }
936 }
937
938 let rent = rpc_client.get_minimum_balance_for_rent_exemption(Feature::size_of())?;
939
940 let blockhash = rpc_client.get_latest_blockhash()?;
941 let (message, _) = resolve_spend_tx_and_check_account_balance(
942 rpc_client,
943 false,
944 SpendAmount::Some(rent),
945 &blockhash,
946 &fee_payer.pubkey(),
947 |lamports| {
948 Message::new(
949 &feature::activate_with_lamports(&feature_id, &fee_payer.pubkey(), lamports),
950 Some(&fee_payer.pubkey()),
951 )
952 },
953 config.commitment,
954 )?;
955 let mut transaction = Transaction::new_unsigned(message);
956 transaction.try_sign(&config.signers, blockhash)?;
957
958 println!(
959 "Activating {} ({})",
960 FEATURE_NAMES.get(&feature_id).unwrap(),
961 feature_id
962 );
963 let result = rpc_client.send_and_confirm_transaction_with_spinner(&transaction);
964 log_instruction_custom_error::<SystemError>(result, config)
965}