miraland_cli/
feature.rs

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; // ~90days
36
37#[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
395/// Check an RPC's reported genesis hash against the ClusterType's known genesis hash
396fn 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
699// Feature activation is only allowed when 95% of the active stake is on the current feature set
700fn 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}