diff --git a/utils/staking-miner/Cargo.toml b/utils/staking-miner/Cargo.toml index 24e5231614e3..92625f6604f0 100644 --- a/utils/staking-miner/Cargo.toml +++ b/utils/staking-miner/Cargo.toml @@ -43,4 +43,4 @@ westend-runtime = { path = "../../runtime/westend" } sub-tokens = { git = "https://github.com/paritytech/substrate-debug-kit", branch = "master" } [dev-dependencies] -assert_cmd = "2.0.2" +assert_cmd = "2.0.4" diff --git a/utils/staking-miner/README.md b/utils/staking-miner/README.md index 2190443003e7..599217e7e134 100644 --- a/utils/staking-miner/README.md +++ b/utils/staking-miner/README.md @@ -28,7 +28,7 @@ There are 2 options to build a staking-miner Docker image: ### Building the injected image First build the binary as documented [above](#building). -You may then inject the binary into a Docker base image usingfrom the root of the Polkadot repository: +You may then inject the binary into a Docker base image from the root of the Polkadot repository: ``` docker build -t staking-miner -f scripts/ci/dockerfiles/staking-miner/staking-miner_injected.Dockerfile target/release ``` diff --git a/utils/staking-miner/src/dry_run.rs b/utils/staking-miner/src/dry_run.rs index 19be1c4474f7..8b105d70eb3b 100644 --- a/utils/staking-miner/src/dry_run.rs +++ b/utils/staking-miner/src/dry_run.rs @@ -16,7 +16,7 @@ //! The dry-run command. -use crate::{prelude::*, rpc::*, signer::Signer, DryRunConfig, Error, SharedRpcClient}; +use crate::{opts::DryRunConfig, prelude::*, rpc::*, signer::Signer, Error, SharedRpcClient}; use codec::Encode; use frame_support::traits::Currency; use sp_core::Bytes; diff --git a/utils/staking-miner/src/main.rs b/utils/staking-miner/src/main.rs index 5a515c9de296..fd1281e2ec59 100644 --- a/utils/staking-miner/src/main.rs +++ b/utils/staking-miner/src/main.rs @@ -28,30 +28,34 @@ //! development. It is intended to run this bot with a `restart = true` way, so that it reports it //! crash, but resumes work thereafter. +// Silence erroneous warning about unsafe not being required whereas it is +// see https://github.com/rust-lang/rust/issues/49112 +#![allow(unused_unsafe)] + mod dry_run; mod emergency_solution; mod monitor; +mod opts; mod prelude; mod rpc; +mod runtime_versions; mod signer; -use std::str::FromStr; - pub(crate) use prelude::*; pub(crate) use signer::get_account_info; +use crate::opts::*; use clap::Parser; use frame_election_provider_support::NposSolver; use frame_support::traits::Get; use jsonrpsee::ws_client::{WsClient, WsClientBuilder}; use remote_externalities::{Builder, Mode, OnlineConfig}; use rpc::{RpcApiClient, SharedRpcClient}; +use runtime_versions::RuntimeVersions; use sp_npos_elections::BalancingConfig; -use sp_runtime::{traits::Block as BlockT, DeserializeOwned, Perbill}; -use tracing_subscriber::{fmt, EnvFilter}; - +use sp_runtime::{traits::Block as BlockT, DeserializeOwned}; use std::{ops::Deref, sync::Arc}; - +use tracing_subscriber::{fmt, EnvFilter}; pub(crate) enum AnyRuntime { Polkadot, Kusama, @@ -62,7 +66,6 @@ pub(crate) static mut RUNTIME: AnyRuntime = AnyRuntime::Polkadot; macro_rules! construct_runtime_prelude { ($runtime:ident) => { paste::paste! { - #[allow(unused_import)] pub(crate) mod [<$runtime _runtime_exports>] { pub(crate) use crate::prelude::EPM; pub(crate) use [<$runtime _runtime>]::*; @@ -278,70 +281,6 @@ impl std::fmt::Display for Error { } } -#[derive(Debug, Clone, Parser)] -#[cfg_attr(test, derive(PartialEq))] -enum Command { - /// Monitor for the phase being signed, then compute. - Monitor(MonitorConfig), - /// Just compute a solution now, and don't submit it. - DryRun(DryRunConfig), - /// Provide a solution that can be submitted to the chain as an emergency response. - EmergencySolution(EmergencySolutionConfig), -} - -#[derive(Debug, Clone, Parser)] -#[cfg_attr(test, derive(PartialEq))] -enum Solver { - SeqPhragmen { - #[clap(long, default_value = "10")] - iterations: usize, - }, - PhragMMS { - #[clap(long, default_value = "10")] - iterations: usize, - }, -} - -/// Submission strategy to use. -#[derive(Debug, Copy, Clone)] -#[cfg_attr(test, derive(PartialEq))] -enum SubmissionStrategy { - // Only submit if at the time, we are the best. - IfLeading, - // Always submit. - Always, - // Submit if we are leading, or if the solution that's leading is more that the given `Perbill` - // better than us. This helps detect obviously fake solutions and still combat them. - ClaimBetterThan(Perbill), -} - -/// Custom `impl` to parse `SubmissionStrategy` from CLI. -/// -/// Possible options: -/// * --submission-strategy if-leading: only submit if leading -/// * --submission-strategy always: always submit -/// * --submission-strategy "percent-better ": submit if submission is `n` percent better. -/// -impl FromStr for SubmissionStrategy { - type Err = String; - - fn from_str(s: &str) -> Result { - let s = s.trim(); - - let res = if s == "if-leading" { - Self::IfLeading - } else if s == "always" { - Self::Always - } else if s.starts_with("percent-better ") { - let percent: u32 = s[15..].parse().map_err(|e| format!("{:?}", e))?; - Self::ClaimBetterThan(Perbill::from_percent(percent)) - } else { - return Err(s.into()) - }; - Ok(res) - } -} - frame_support::parameter_types! { /// Number of balancing iterations for a solution algorithm. Set based on the [`Solvers`] CLI /// config. @@ -349,87 +288,6 @@ frame_support::parameter_types! { pub static Balancing: Option = Some( BalancingConfig { iterations: BalanceIterations::get(), tolerance: 0 } ); } -#[derive(Debug, Clone, Parser)] -#[cfg_attr(test, derive(PartialEq))] -struct MonitorConfig { - /// They type of event to listen to. - /// - /// Typically, finalized is safer and there is no chance of anything going wrong, but it can be - /// slower. It is recommended to use finalized, if the duration of the signed phase is longer - /// than the the finality delay. - #[clap(long, default_value = "head", possible_values = &["head", "finalized"])] - listen: String, - - /// The solver algorithm to use. - #[clap(subcommand)] - solver: Solver, - - /// Submission strategy to use. - /// - /// Possible options: - /// - /// `--submission-strategy if-leading`: only submit if leading. - /// - /// `--submission-strategy always`: always submit. - /// - /// `--submission-strategy "percent-better "`: submit if the submission is `n` percent better. - #[clap(long, parse(try_from_str), default_value = "if-leading")] - submission_strategy: SubmissionStrategy, -} - -#[derive(Debug, Clone, Parser)] -#[cfg_attr(test, derive(PartialEq))] -struct EmergencySolutionConfig { - /// The block hash at which scraping happens. If none is provided, the latest head is used. - #[clap(long)] - at: Option, - - /// The solver algorithm to use. - #[clap(subcommand)] - solver: Solver, - - /// The number of top backed winners to take. All are taken, if not provided. - take: Option, -} - -#[derive(Debug, Clone, Parser)] -#[cfg_attr(test, derive(PartialEq))] -struct DryRunConfig { - /// The block hash at which scraping happens. If none is provided, the latest head is used. - #[clap(long)] - at: Option, - - /// The solver algorithm to use. - #[clap(subcommand)] - solver: Solver, - - /// Force create a new snapshot, else expect one to exist onchain. - #[clap(long)] - force_snapshot: bool, -} - -#[derive(Debug, Clone, Parser)] -#[cfg_attr(test, derive(PartialEq))] -#[clap(author, version, about)] -struct Opt { - /// The `ws` node to connect to. - #[clap(long, short, default_value = DEFAULT_URI, env = "URI")] - uri: String, - - /// The path to a file containing the seed of the account. If the file is not found, the seed is - /// used as-is. - /// - /// Can also be provided via the `SEED` environment variable. - /// - /// WARNING: Don't use an account with a large stash for this. Based on how the bot is - /// configured, it might re-try and lose funds through transaction fees/deposits. - #[clap(long, short, env = "SEED")] - seed_or_path: String, - - #[clap(subcommand)] - command: Command, -} - /// Build the Ext at hash with all the data of `ElectionProviderMultiPhase` and any additional /// pallets. async fn create_election_ext( @@ -582,7 +440,7 @@ pub(crate) async fn check_versions( async fn main() { fmt().with_env_filter(EnvFilter::from_default_env()).init(); - let Opt { uri, seed_or_path, command } = Opt::parse(); + let Opt { uri, command } = Opt::parse(); log::debug!(target: LOG_TARGET, "attempting to connect to {:?}", uri); let rpc = loop { @@ -648,26 +506,51 @@ async fn main() { check_versions::(&rpc).await }; - let signer_account = any_runtime! { - signer::signer_uri_from_string::(&seed_or_path, &rpc) - .await - .expect("Provided account is invalid, terminating.") - }; - let outcome = any_runtime! { match command { - Command::Monitor(cmd) => monitor_cmd(rpc, cmd, signer_account).await + Command::Monitor(monitor_config) => + { + let signer_account = any_runtime! { + signer::signer_uri_from_string::(&monitor_config.seed_or_path , &rpc) + .await + .expect("Provided account is invalid, terminating.") + }; + monitor_cmd(rpc, monitor_config, signer_account).await .map_err(|e| { log::error!(target: LOG_TARGET, "Monitor error: {:?}", e); - }), - Command::DryRun(cmd) => dry_run_cmd(rpc, cmd, signer_account).await + })}, + Command::DryRun(dryrun_config) => { + let signer_account = any_runtime! { + signer::signer_uri_from_string::(&dryrun_config.seed_or_path , &rpc) + .await + .expect("Provided account is invalid, terminating.") + }; + dry_run_cmd(rpc, dryrun_config, signer_account).await .map_err(|e| { log::error!(target: LOG_TARGET, "DryRun error: {:?}", e); - }), - Command::EmergencySolution(cmd) => emergency_solution_cmd(rpc, cmd).await + })}, + Command::EmergencySolution(emergency_solution_config) => + emergency_solution_cmd(rpc, emergency_solution_config).await .map_err(|e| { log::error!(target: LOG_TARGET, "EmergencySolution error: {:?}", e); }), + Command::Info(info_opts) => { + let remote_runtime_version = rpc.runtime_version(None).await.expect("runtime_version infallible; qed."); + + let builtin_version = any_runtime! { + Version::get() + }; + + let versions = RuntimeVersions::new(&remote_runtime_version, &builtin_version); + + if !info_opts.json { + println!("{}", versions); + } else { + let versions = serde_json::to_string_pretty(&versions).expect("Failed serializing version info"); + println!("{}", versions); + } + Ok(()) + } } }; log::info!(target: LOG_TARGET, "round of execution finished. outcome = {:?}", outcome); @@ -696,102 +579,4 @@ mod tests { assert_eq!(polkadot_version.spec_name, "polkadot".into()); assert_eq!(kusama_version.spec_name, "kusama".into()); } - - #[test] - fn cli_monitor_works() { - let opt = Opt::try_parse_from([ - env!("CARGO_PKG_NAME"), - "--uri", - "hi", - "--seed-or-path", - "//Alice", - "monitor", - "--listen", - "head", - "seq-phragmen", - ]) - .unwrap(); - - assert_eq!( - opt, - Opt { - uri: "hi".to_string(), - seed_or_path: "//Alice".to_string(), - command: Command::Monitor(MonitorConfig { - listen: "head".to_string(), - solver: Solver::SeqPhragmen { iterations: 10 }, - submission_strategy: SubmissionStrategy::IfLeading, - }), - } - ); - } - - #[test] - fn cli_dry_run_works() { - let opt = Opt::try_parse_from([ - env!("CARGO_PKG_NAME"), - "--uri", - "hi", - "--seed-or-path", - "//Alice", - "dry-run", - "phrag-mms", - ]) - .unwrap(); - - assert_eq!( - opt, - Opt { - uri: "hi".to_string(), - seed_or_path: "//Alice".to_string(), - command: Command::DryRun(DryRunConfig { - at: None, - solver: Solver::PhragMMS { iterations: 10 }, - force_snapshot: false, - }), - } - ); - } - - #[test] - fn cli_emergency_works() { - let opt = Opt::try_parse_from([ - env!("CARGO_PKG_NAME"), - "--uri", - "hi", - "--seed-or-path", - "//Alice", - "emergency-solution", - "99", - "phrag-mms", - "--iterations", - "1337", - ]) - .unwrap(); - - assert_eq!( - opt, - Opt { - uri: "hi".to_string(), - seed_or_path: "//Alice".to_string(), - command: Command::EmergencySolution(EmergencySolutionConfig { - take: Some(99), - at: None, - solver: Solver::PhragMMS { iterations: 1337 } - }), - } - ); - } - - #[test] - fn submission_strategy_from_str_works() { - use std::str::FromStr; - - assert_eq!(SubmissionStrategy::from_str("if-leading"), Ok(SubmissionStrategy::IfLeading)); - assert_eq!(SubmissionStrategy::from_str("always"), Ok(SubmissionStrategy::Always)); - assert_eq!( - SubmissionStrategy::from_str(" percent-better 99 "), - Ok(SubmissionStrategy::ClaimBetterThan(Perbill::from_percent(99))) - ); - } } diff --git a/utils/staking-miner/src/opts.rs b/utils/staking-miner/src/opts.rs new file mode 100644 index 000000000000..63425b89a105 --- /dev/null +++ b/utils/staking-miner/src/opts.rs @@ -0,0 +1,298 @@ +// Copyright 2021 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +use crate::prelude::*; +use clap::Parser; +use sp_runtime::Perbill; +use std::str::FromStr; + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +#[clap(author, version, about)] +pub(crate) struct Opt { + /// The `ws` node to connect to. + #[clap(long, short, default_value = DEFAULT_URI, env = "URI", global = true)] + pub uri: String, + + #[clap(subcommand)] + pub command: Command, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) enum Command { + /// Monitor for the phase being signed, then compute. + Monitor(MonitorConfig), + + /// Just compute a solution now, and don't submit it. + DryRun(DryRunConfig), + + /// Provide a solution that can be submitted to the chain as an emergency response. + EmergencySolution(EmergencySolutionConfig), + + /// Return information about the current version + Info(InfoOpts), +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct MonitorConfig { + /// The path to a file containing the seed of the account. If the file is not found, the seed is + /// used as-is. + /// + /// Can also be provided via the `SEED` environment variable. + /// + /// WARNING: Don't use an account with a large stash for this. Based on how the bot is + /// configured, it might re-try and lose funds through transaction fees/deposits. + #[clap(long, short, env = "SEED")] + pub seed_or_path: String, + + /// They type of event to listen to. + /// + /// Typically, finalized is safer and there is no chance of anything going wrong, but it can be + /// slower. It is recommended to use finalized, if the duration of the signed phase is longer + /// than the the finality delay. + #[clap(long, default_value = "head", possible_values = &["head", "finalized"])] + pub listen: String, + + /// The solver algorithm to use. + #[clap(subcommand)] + pub solver: Solver, + + /// Submission strategy to use. + /// + /// Possible options: + /// + /// `--submission-strategy if-leading`: only submit if leading. + /// + /// `--submission-strategy always`: always submit. + /// + /// `--submission-strategy "percent-better "`: submit if the submission is `n` percent better. + #[clap(long, parse(try_from_str), default_value = "if-leading")] + pub submission_strategy: SubmissionStrategy, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct DryRunConfig { + /// The path to a file containing the seed of the account. If the file is not found, the seed is + /// used as-is. + /// + /// Can also be provided via the `SEED` environment variable. + /// + /// WARNING: Don't use an account with a large stash for this. Based on how the bot is + /// configured, it might re-try and lose funds through transaction fees/deposits. + #[clap(long, short, env = "SEED")] + pub seed_or_path: String, + + /// The block hash at which scraping happens. If none is provided, the latest head is used. + #[clap(long)] + pub at: Option, + + /// The solver algorithm to use. + #[clap(subcommand)] + pub solver: Solver, + + /// Force create a new snapshot, else expect one to exist onchain. + #[clap(long)] + pub force_snapshot: bool, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct EmergencySolutionConfig { + /// The block hash at which scraping happens. If none is provided, the latest head is used. + #[clap(long)] + pub at: Option, + + /// The solver algorithm to use. + #[clap(subcommand)] + pub solver: Solver, + + /// The number of top backed winners to take. All are taken, if not provided. + pub take: Option, +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) struct InfoOpts { + /// Serialize the output as json + #[clap(long, short)] + pub json: bool, +} + +/// Submission strategy to use. +#[derive(Debug, Copy, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum SubmissionStrategy { + // Only submit if at the time, we are the best. + IfLeading, + // Always submit. + Always, + // Submit if we are leading, or if the solution that's leading is more that the given `Perbill` + // better than us. This helps detect obviously fake solutions and still combat them. + ClaimBetterThan(Perbill), +} + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub(crate) enum Solver { + SeqPhragmen { + #[clap(long, default_value = "10")] + iterations: usize, + }, + PhragMMS { + #[clap(long, default_value = "10")] + iterations: usize, + }, +} + +/// Custom `impl` to parse `SubmissionStrategy` from CLI. +/// +/// Possible options: +/// * --submission-strategy if-leading: only submit if leading +/// * --submission-strategy always: always submit +/// * --submission-strategy "percent-better ": submit if submission is `n` percent better. +/// +impl FromStr for SubmissionStrategy { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + let res = if s == "if-leading" { + Self::IfLeading + } else if s == "always" { + Self::Always + } else if s.starts_with("percent-better ") { + let percent: u32 = s[15..].parse().map_err(|e| format!("{:?}", e))?; + Self::ClaimBetterThan(Perbill::from_percent(percent)) + } else { + return Err(s.into()) + }; + Ok(res) + } +} + +#[cfg(test)] +mod test_super { + use super::*; + + #[test] + fn cli_monitor_works() { + let opt = Opt::try_parse_from([ + env!("CARGO_PKG_NAME"), + "--uri", + "hi", + "monitor", + "--seed-or-path", + "//Alice", + "--listen", + "head", + "seq-phragmen", + ]) + .unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + command: Command::Monitor(MonitorConfig { + seed_or_path: "//Alice".to_string(), + listen: "head".to_string(), + solver: Solver::SeqPhragmen { iterations: 10 }, + submission_strategy: SubmissionStrategy::IfLeading, + }), + } + ); + } + + #[test] + fn cli_dry_run_works() { + let opt = Opt::try_parse_from([ + env!("CARGO_PKG_NAME"), + "--uri", + "hi", + "dry-run", + "--seed-or-path", + "//Alice", + "phrag-mms", + ]) + .unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + command: Command::DryRun(DryRunConfig { + seed_or_path: "//Alice".to_string(), + at: None, + solver: Solver::PhragMMS { iterations: 10 }, + force_snapshot: false, + }), + } + ); + } + + #[test] + fn cli_emergency_works() { + let opt = Opt::try_parse_from([ + env!("CARGO_PKG_NAME"), + "--uri", + "hi", + "emergency-solution", + "99", + "phrag-mms", + "--iterations", + "1337", + ]) + .unwrap(); + + assert_eq!( + opt, + Opt { + uri: "hi".to_string(), + command: Command::EmergencySolution(EmergencySolutionConfig { + take: Some(99), + at: None, + solver: Solver::PhragMMS { iterations: 1337 } + }), + } + ); + } + + #[test] + fn cli_info_works() { + let opt = Opt::try_parse_from([env!("CARGO_PKG_NAME"), "--uri", "hi", "info"]).unwrap(); + + assert_eq!( + opt, + Opt { uri: "hi".to_string(), command: Command::Info(InfoOpts { json: false }) } + ); + } + + #[test] + fn submission_strategy_from_str_works() { + use std::str::FromStr; + + assert_eq!(SubmissionStrategy::from_str("if-leading"), Ok(SubmissionStrategy::IfLeading)); + assert_eq!(SubmissionStrategy::from_str("always"), Ok(SubmissionStrategy::Always)); + assert_eq!( + SubmissionStrategy::from_str(" percent-better 99 "), + Ok(SubmissionStrategy::ClaimBetterThan(Perbill::from_percent(99))) + ); + } +} diff --git a/utils/staking-miner/src/runtime_versions.rs b/utils/staking-miner/src/runtime_versions.rs new file mode 100644 index 000000000000..20e4bd435a63 --- /dev/null +++ b/utils/staking-miner/src/runtime_versions.rs @@ -0,0 +1,90 @@ +// Copyright 2021 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +use sp_version::RuntimeVersion; +use std::fmt; + +#[derive(Debug, serde::Serialize)] +pub(crate) struct RuntimeWrapper<'a>(pub &'a RuntimeVersion); + +impl<'a> fmt::Display for RuntimeWrapper<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let width = 16; + + writeln!( + f, + r#" impl_name : {impl_name:>width$} + spec_name : {spec_name:>width$} + spec_version : {spec_version:>width$} + transaction_version : {transaction_version:>width$} + impl_version : {impl_version:>width$} + authoringVersion : {authoring_version:>width$} + state_version : {state_version:>width$}"#, + spec_name = self.0.spec_name.to_string(), + impl_name = self.0.impl_name.to_string(), + spec_version = self.0.spec_version, + impl_version = self.0.impl_version, + authoring_version = self.0.authoring_version, + transaction_version = self.0.transaction_version, + state_version = self.0.state_version, + ) + } +} + +impl<'a> From<&'a RuntimeVersion> for RuntimeWrapper<'a> { + fn from(r: &'a RuntimeVersion) -> Self { + RuntimeWrapper(r) + } +} + +#[derive(Debug, serde::Serialize)] +pub(crate) struct RuntimeVersions<'a> { + /// The `RuntimeVersion` linked in the staking-miner + pub linked: RuntimeWrapper<'a>, + + /// The `RuntimeVersion` reported by the node we connect to via RPC + pub remote: RuntimeWrapper<'a>, + + /// This `bool` reports whether both remote and linked `RuntimeVersion` are compatible + /// and if the staking-miner is expected to work properly against the remote runtime + compatible: bool, +} + +impl<'a> RuntimeVersions<'a> { + pub fn new( + remote_runtime_version: &'a RuntimeVersion, + linked_runtime_version: &'a RuntimeVersion, + ) -> Self { + Self { + remote: remote_runtime_version.into(), + linked: linked_runtime_version.into(), + compatible: are_runtimes_compatible(remote_runtime_version, linked_runtime_version), + } + } +} + +/// Check whether runtimes are compatible. Currently we only support equality. +fn are_runtimes_compatible(r1: &RuntimeVersion, r2: &RuntimeVersion) -> bool { + r1 == r2 +} + +impl<'a> fmt::Display for RuntimeVersions<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let _ = write!(f, "- linked:\n{}", self.linked); + let _ = write!(f, "- remote :\n{}", self.remote); + write!(f, "Compatible: {}", if self.compatible { "YES" } else { "NO" }) + } +} diff --git a/utils/staking-miner/tests/cli.rs b/utils/staking-miner/tests/cli.rs index 46f5cf0cb0b2..ebd5380f047b 100644 --- a/utils/staking-miner/tests/cli.rs +++ b/utils/staking-miner/tests/cli.rs @@ -1,4 +1,5 @@ use assert_cmd::{cargo::cargo_bin, Command}; +use serde_json::{Result, Value}; #[test] fn cli_version_works() { @@ -10,3 +11,23 @@ fn cli_version_works() { assert_eq!(version, format!("{} {}", crate_name, env!("CARGO_PKG_VERSION"))); } + +#[test] +fn cli_info_works() { + let crate_name = env!("CARGO_PKG_NAME"); + let output = Command::new(cargo_bin(crate_name)) + .arg("info") + .arg("--json") + .env("RUST_LOG", "none") + .output() + .unwrap(); + + assert!(output.status.success(), "command returned with non-success exit code"); + let info = String::from_utf8_lossy(&output.stdout).trim().to_owned(); + let v: Result = serde_json::from_str(&info); + let v = v.unwrap(); + assert!(!v["builtin"].to_string().is_empty()); + assert!(!v["builtin"]["spec_name"].to_string().is_empty()); + assert!(!v["builtin"]["spec_version"].to_string().is_empty()); + assert!(!v["remote"].to_string().is_empty()); +}