From 336544da4584a9e058ba2957fd51fcfe84f103e5 Mon Sep 17 00:00:00 2001 From: Alex Bean Date: Fri, 26 Jul 2024 14:51:45 +0200 Subject: [PATCH] feat: consistency with `pop up parachains` to handle versioning for `contracts-node` (#262) * feat(contracts-e2e): auto-source contracts-node binary if not present * chore(contract-e2e): deprecate --features e2e-tests * fix: remove unnecessary async function * refactor(contracts-node): reduce duplicated code with binary sourcing (#255) * refactor(up-contract): use Binary struct from parachains -> up -> sourcing to auto-launch contracts-node * refactor(contracts_node): reduce duplicated code -- checkpoint, not working * refactor(contracts_node): use Binary and Source structs for substrate-contracts-node * chore: small changes * chore: small changes * chore: add e2e help, prevent cmd output, and cleanup * refactor(contracts_node): introduce helper to download contracts node if it does not exist * refactor(sourcing): move sourcing functionality to `pop-common` (#258) * refactor(sourcing): move sourcing to pop-common * refactor(sourcing-tests): move sourcing tests to pop-common * refactor(sourcing): better imports * refactor(sourcing): clean sourcing module with better component categorization. Other cleanups * fix: failing tests * fix: needed to download contracts_node for test * refactor: consistency with pop up parachains to recover contracts-node * refactor: improvements * test: unit test for new methods * fix: comment * refactor: remove support for standalone binary * refactor: duplicate code after merge and docs * refactor: remove code from merge * fix: unit test * fix: merge regressions * fix: ui * chore: add license missing in new files * chore: add license missing in new files * refactor: imports * refactor: remove warnings --------- Co-authored-by: Peter White Co-authored-by: Peter White <23270067+peterwht@users.noreply.github.com> --- crates/pop-cli/src/commands/test/contract.rs | 36 +++-- crates/pop-cli/src/commands/up/contract.rs | 25 ++- crates/pop-cli/src/common/contracts.rs | 66 +++++--- crates/pop-cli/src/common/mod.rs | 2 + crates/pop-cli/tests/contract.rs | 10 +- crates/pop-contracts/src/call.rs | 7 +- crates/pop-contracts/src/lib.rs | 4 +- crates/pop-contracts/src/node/mod.rs | 157 +++++++++++++------ crates/pop-contracts/src/up.rs | 7 +- 9 files changed, 198 insertions(+), 116 deletions(-) diff --git a/crates/pop-cli/src/commands/test/contract.rs b/crates/pop-cli/src/commands/test/contract.rs index 97b67cb5..7c5edfa7 100644 --- a/crates/pop-cli/src/commands/test/contract.rs +++ b/crates/pop-cli/src/commands/test/contract.rs @@ -1,8 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::{common::contracts::check_contracts_node_and_prompt, style::style}; +use crate::{ + cli::{traits::Cli as _, Cli}, + common::contracts::check_contracts_node_and_prompt, +}; use clap::Args; -use cliclack::{clear_screen, intro, log::warning, outro}; +use cliclack::{clear_screen, log::warning, outro}; use pop_contracts::{test_e2e_smart_contract, test_smart_contract}; use std::path::PathBuf; #[cfg(not(test))] @@ -24,6 +27,9 @@ pub(crate) struct TestContractCommand { help = "Path to the contracts node to run e2e tests [default: none]" )] node: Option, + /// Automatically source the needed binary required without prompting for confirmation. + #[clap(short('y'), long)] + skip_confirm: bool, } impl TestContractCommand { @@ -35,34 +41,30 @@ impl TestContractCommand { if self.features.is_some() && self.features.clone().unwrap().contains("e2e-tests") { show_deprecated = true; self.e2e = true; - #[cfg(not(test))] - sleep(Duration::from_secs(3)).await; } if self.e2e { - intro(format!( - "{}: Starting end-to-end tests", - style(" Pop CLI ").black().on_magenta() - ))?; + Cli.intro("Starting end-to-end tests")?; if show_deprecated { warning("NOTE: --features e2e-tests is deprecated. Use --e2e instead.")?; + #[cfg(not(test))] + sleep(Duration::from_secs(3)).await; } - let maybe_node_path = check_contracts_node_and_prompt().await?; - if let Some(node_path) = maybe_node_path { - if node_path != PathBuf::new() { - self.node = Some(node_path); - } - } else { - warning("đŸšĢ substrate-contracts-node is necessary to run e2e tests. Will try to run tests anyway...")?; - } + self.node = match check_contracts_node_and_prompt(self.skip_confirm).await { + Ok(binary_path) => Some(binary_path), + Err(_) => { + warning("đŸšĢ substrate-contracts-node is necessary to run e2e tests. Will try to run tests anyway...")?; + Some(PathBuf::new()) + }, + }; test_e2e_smart_contract(self.path.as_deref(), self.node.as_deref())?; outro("End-to-end testing complete")?; Ok("e2e") } else { - intro(format!("{}: Starting unit tests", style(" Pop CLI ").black().on_magenta()))?; + Cli.intro("Starting unit tests")?; test_smart_contract(self.path.as_deref())?; outro("Unit testing complete")?; Ok("unit") diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index 26162637..bf24cf73 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -71,7 +71,7 @@ pub struct UpContractCommand { /// Uploads the contract only, without instantiation. #[clap(short('u'), long)] upload_only: bool, - /// Before starting a local node, do not ask the user for confirmation. + /// Automatically source or update the needed binary required without prompting for confirmation. #[clap(short('y'), long)] skip_confirm: bool, } @@ -127,21 +127,16 @@ impl UpContractCommand { let log = NamedTempFile::new()?; - // default to standalone binary, if it exists. - let mut binary_path = PathBuf::from("substrate-contracts-node"); - // uses the cache location - let maybe_node_path = check_contracts_node_and_prompt().await?; - if let Some(node_path) = maybe_node_path { - if node_path != PathBuf::new() { - binary_path = node_path; - } - } else { - Cli.outro_cancel( - "đŸšĢ You need to specify an accessible endpoint to deploy the contract.", - )?; - return Ok(()); - } + let binary_path = match check_contracts_node_and_prompt(self.skip_confirm).await { + Ok(binary_path) => binary_path, + Err(_) => { + Cli.outro_cancel( + "đŸšĢ You need to specify an accessible endpoint to deploy the contract.", + )?; + return Ok(()); + }, + }; let spinner = spinner(); spinner.start("Starting local node..."); diff --git a/crates/pop-cli/src/common/contracts.rs b/crates/pop-cli/src/common/contracts.rs index f0750563..02e6b838 100644 --- a/crates/pop-cli/src/common/contracts.rs +++ b/crates/pop-cli/src/common/contracts.rs @@ -1,18 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0 + use cliclack::{confirm, log::warning, spinner}; -use pop_contracts::{does_contracts_node_exist, download_contracts_node}; +use pop_contracts::contracts_node_generator; use std::path::PathBuf; -/// Helper function to check if the contracts node binary exists, and if not download it. -/// returns: -/// - Some("") if the standalone binary exists -/// - Some(binary_cache_location) if the binary exists in pop's cache -/// - None if the binary does not exist -pub async fn check_contracts_node_and_prompt() -> anyhow::Result> { - let mut node_path = None; - - // if the contracts node binary does not exist, prompt the user to download it - let maybe_contract_node_path = does_contracts_node_exist(crate::cache()?); - if maybe_contract_node_path == None { +/// Checks the status of the `substrate-contracts-node` binary, sources it if necessary, and prompts the user to update it if the existing binary is not the latest version. +/// +/// # Arguments +/// * `skip_confirm`: A boolean indicating whether to skip confirmation prompts. +pub async fn check_contracts_node_and_prompt(skip_confirm: bool) -> anyhow::Result { + let cache_path: PathBuf = crate::cache()?; + let mut binary = contracts_node_generator(cache_path, None).await?; + let mut node_path = binary.path(); + if !binary.exists() { warning("⚠ī¸ The substrate-contracts-node binary is not found.")?; if confirm("đŸ“Ļ Would you like to source it automatically now?") .initial_value(true) @@ -21,19 +21,45 @@ pub async fn check_contracts_node_and_prompt() -> anyhow::Result let spinner = spinner(); spinner.start("đŸ“Ļ Sourcing substrate-contracts-node..."); - let cache_path = crate::cache()?; - let binary = download_contracts_node(cache_path.clone()).await?; + binary.source(false, &(), true).await?; spinner.stop(format!( - "✅ substrate-contracts-node successfully sourced. Cached at: {}", + "✅ substrate-contracts-node successfully sourced. Cached at: {}", binary.path().to_str().unwrap() )); - node_path = Some(binary.path()); + node_path = binary.path(); + } + } + if binary.stale() { + warning(format!( + "ℹī¸ There is a newer version of {} available:\n {} -> {}", + binary.name(), + binary.version().unwrap_or("None"), + binary.latest().unwrap_or("None") + ))?; + let latest; + if !skip_confirm { + latest = confirm( + "đŸ“Ļ Would you like to source it automatically now? It may take some time..." + .to_string(), + ) + .initial_value(true) + .interact()?; + } else { + latest = true; } - } else { - if let Some(contract_node_path) = maybe_contract_node_path { - // If the node_path is not empty (cached binary). Otherwise, the standalone binary will be used by cargo-contract. - node_path = Some(contract_node_path.0); + if latest { + let spinner = spinner(); + spinner.start("đŸ“Ļ Sourcing substrate-contracts-node..."); + + binary.use_latest(); + binary.source(false, &(), true).await?; + + spinner.stop(format!( + "✅ substrate-contracts-node successfully sourced. Cached at: {}", + binary.path().to_str().unwrap() + )); + node_path = binary.path(); } } diff --git a/crates/pop-cli/src/common/mod.rs b/crates/pop-cli/src/common/mod.rs index 0cd72aa9..1f513a16 100644 --- a/crates/pop-cli/src/common/mod.rs +++ b/crates/pop-cli/src/common/mod.rs @@ -1,2 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0 + #[cfg(feature = "contract")] pub mod contracts; diff --git a/crates/pop-cli/tests/contract.rs b/crates/pop-cli/tests/contract.rs index f017d9dc..07dbcda4 100644 --- a/crates/pop-cli/tests/contract.rs +++ b/crates/pop-cli/tests/contract.rs @@ -4,10 +4,10 @@ use anyhow::Result; use assert_cmd::Command; use pop_common::templates::Template; use pop_contracts::{ - download_contracts_node, dry_run_gas_estimate_instantiate, instantiate_smart_contract, + contracts_node_generator, dry_run_gas_estimate_instantiate, instantiate_smart_contract, run_contracts_node, set_up_deployment, Contract, UpOpts, }; -use std::{env::temp_dir, path::Path, process::Command as Cmd}; +use std::{path::Path, process::Command as Cmd}; use strum::VariantArray; use url::Url; @@ -42,9 +42,9 @@ async fn contract_lifecycle() -> Result<()> { assert!(temp_dir.join("test_contract/target/ink/test_contract.wasm").exists()); assert!(temp_dir.join("test_contract/target/ink/test_contract.json").exists()); - // Run the contracts node - let node_path = download_contracts_node(temp_dir.to_path_buf().clone()).await?; - let process = run_contracts_node(node_path.path(), None).await?; + let binary = contracts_node_generator(temp_dir.to_path_buf().clone(), None).await?; + binary.source(false, &(), true).await?; + let process = run_contracts_node(binary.path(), None).await?; // Only upload the contract // pop up contract --upload-only diff --git a/crates/pop-contracts/src/call.rs b/crates/pop-contracts/src/call.rs index 615f2269..451e8a09 100644 --- a/crates/pop-contracts/src/call.rs +++ b/crates/pop-contracts/src/call.rs @@ -162,7 +162,7 @@ pub async fn call_smart_contract( mod tests { use super::*; use crate::{ - create_smart_contract, download_contracts_node, dry_run_gas_estimate_instantiate, + contracts_node_generator, create_smart_contract, dry_run_gas_estimate_instantiate, errors::Error, instantiate_smart_contract, run_contracts_node, set_up_deployment, Contract, UpOpts, }; @@ -313,8 +313,9 @@ mod tests { let cache = temp_dir.path().join(""); - let node_path = download_contracts_node(cache.clone()).await?; - let process = run_contracts_node(node_path.path(), None).await?; + let binary = contracts_node_generator(cache.clone(), None).await?; + binary.source(false, &(), true).await?; + let process = run_contracts_node(binary.path(), None).await?; // Instantiate a Smart Contract. let instantiate_exec = set_up_deployment(UpOpts { path: Some(temp_dir.path().join("testing")), diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 4b701581..1d558de3 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -16,9 +16,7 @@ pub use call::{ call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts, }; pub use new::{create_smart_contract, is_valid_contract_name}; -pub use node::{ - does_contracts_node_exist, download_contracts_node, is_chain_alive, run_contracts_node, -}; +pub use node::{contracts_node_generator, is_chain_alive, run_contracts_node}; pub use templates::{Contract, ContractType}; pub use test::{test_e2e_smart_contract, test_smart_contract}; pub use up::{ diff --git a/crates/pop-contracts/src/node/mod.rs b/crates/pop-contracts/src/node/mod.rs index 4ce4fb96..8353d158 100644 --- a/crates/pop-contracts/src/node/mod.rs +++ b/crates/pop-contracts/src/node/mod.rs @@ -1,11 +1,19 @@ -use crate::errors::Error; +// SPDX-License-Identifier: GPL-3.0 + use contract_extrinsics::{RawParams, RpcRequest}; -use duct::cmd; -use pop_common::sourcing::{ - Binary, {GitHub as GitHubSource, Source}, +use pop_common::{ + sourcing::{ + traits::{Source as _, *}, + Binary, + GitHub::ReleaseArchive, + Source, + }, + Error, GitHub, }; +use strum::{EnumProperty, VariantArray}; + use std::{ - env::consts::OS, + env::consts::{ARCH, OS}, fs::File, path::PathBuf, process::{Child, Command, Stdio}, @@ -36,50 +44,71 @@ pub async fn is_chain_alive(url: url::Url) -> Result { } } -/// Checks if the `substrate-contracts-node` binary exists -/// or if the binary exists in pop's cache. -/// returns: -/// - Some("", ) if the standalone binary exists -/// - Some(binary_cache_location, "") if the binary exists in pop's cache -/// - None if the binary does not exist -pub fn does_contracts_node_exist(cache: PathBuf) -> Option<(PathBuf, String)> { - let cached_location = cache.join(BIN_NAME); - let standalone_output = cmd(BIN_NAME, vec!["--version"]).read(); - - if standalone_output.is_ok() { - Some((PathBuf::new(), standalone_output.unwrap())) - } else if cached_location.exists() { - Some((cached_location, "".to_string())) - } else { - None +/// A supported chain. +#[derive(Debug, EnumProperty, PartialEq, VariantArray)] +pub(super) enum Chain { + /// Minimal Substrate node configured for smart contracts via pallet-contracts. + #[strum(props( + Repository = "https://github.com/paritytech/substrate-contracts-node", + Binary = "substrate-contracts-node", + TagFormat = "{tag}", + Fallback = "v0.41.0" + ))] + ContractsNode, +} + +impl TryInto for Chain { + /// Attempt the conversion. + /// + /// # Arguments + /// * `tag` - If applicable, a tag used to determine a specific release. + /// * `latest` - If applicable, some specifier used to determine the latest source. + fn try_into(&self, tag: Option, latest: Option) -> Result { + let archive = archive_name_by_target()?; + let archive_bin_path = release_folder_by_target()?; + Ok(match self { + &Chain::ContractsNode => { + // Source from GitHub release asset + let repo = GitHub::parse(self.repository())?; + Source::GitHub(ReleaseArchive { + owner: repo.org, + repository: repo.name, + tag, + tag_format: self.tag_format().map(|t| t.into()), + archive, + contents: vec![(archive_bin_path, Some(self.binary().to_string()))], + latest, + }) + }, + }) } } -/// Downloads the latest version of the `substrate-contracts-node` binary -/// into the specified cache location. -pub async fn download_contracts_node(cache: PathBuf) -> Result { - let archive = archive_name_by_target()?; - let archive_bin_path = release_folder_by_target()?; - - let source = Source::GitHub(GitHubSource::ReleaseArchive { - owner: "paritytech".into(), - repository: "substrate-contracts-node".into(), - tag: None, - tag_format: None, - archive, - contents: vec![(archive_bin_path, Some(BIN_NAME.to_string()))], - latest: None, - }); - - let contracts_node = - Binary::Source { name: "substrate-contracts-node".into(), source, cache: cache.clone() }; - - // source the substrate-contracts-node binary - contracts_node - .source(false, &(), true) - .await - .map_err(|err| Error::SourcingError(err))?; +impl pop_common::sourcing::traits::Source for Chain {} +/// Retrieves the latest release of the contracts node binary, resolves its version, and constructs a `Binary::Source` +/// with the specified cache path. +/// +/// # Arguments +/// * `cache` - The cache directory path. +/// * `version` - The specific version used for the substrate-contracts-node (`None` will use the latest available version). +pub async fn contracts_node_generator( + cache: PathBuf, + version: Option<&str>, +) -> Result { + let chain = &Chain::ContractsNode; + let name = chain.binary(); + let releases = chain.releases().await?; + let tag = Binary::resolve_version(name, version, &releases, &cache); + let latest = version + .is_none() + .then(|| releases.iter().nth(0).map(|v| v.to_string())) + .flatten(); + let contracts_node = Binary::Source { + name: name.to_string(), + source: TryInto::try_into(chain, tag.clone(), latest)?, + cache: cache.to_path_buf(), + }; Ok(contracts_node) } @@ -112,7 +141,7 @@ fn archive_name_by_target() -> Result { match OS { "macos" => Ok(format!("{}-mac-universal.tar.gz", BIN_NAME)), "linux" => Ok(format!("{}-linux.tar.gz", BIN_NAME)), - _ => Err(Error::UnsupportedPlatform { os: OS }), + _ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }), } } @@ -120,7 +149,7 @@ fn release_folder_by_target() -> Result<&'static str, Error> { match OS { "macos" => Ok("artifacts/substrate-contracts-node-mac/substrate-contracts-node"), "linux" => Ok("artifacts/substrate-contracts-node-linux/substrate-contracts-node"), - _ => Err(Error::UnsupportedPlatform { os: OS }), + _ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }), } } @@ -152,6 +181,32 @@ mod tests { Ok(()) } + #[tokio::test] + async fn contracts_node_generator_works() -> anyhow::Result<()> { + let expected = Chain::ContractsNode; + let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); + let cache = temp_dir.path().join("cache"); + let version = "v0.40.0"; + let binary = contracts_node_generator(cache.clone(), Some(version)).await?; + let archive = archive_name_by_target()?; + let archive_bin_path = release_folder_by_target()?; + assert!(matches!(binary, Binary::Source { name, source, cache} + if name == expected.binary() && + source == Source::GitHub(ReleaseArchive { + owner: "paritytech".to_string(), + repository: "substrate-contracts-node".to_string(), + tag: Some(version.to_string()), + tag_format: expected.tag_format().map(|t| t.into()), + archive: archive, + contents: vec![(archive_bin_path, Some(binary.name().to_string()))], + latest: None, + }) + && + cache == cache + )); + Ok(()) + } + #[tokio::test] async fn run_contracts_node_works() -> Result<(), Error> { let local_url = url::Url::parse("ws://localhost:9944")?; @@ -159,12 +214,14 @@ mod tests { let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); let cache = temp_dir.path().join(""); - let node_path = download_contracts_node(cache.clone()).await?; - let process = run_contracts_node(node_path.path(), None).await?; + let version = "v0.40.0"; + let binary = contracts_node_generator(cache.clone(), Some(version)).await?; + binary.source(false, &(), true).await?; + let process = run_contracts_node(binary.path(), None).await?; // Check if the node is alive assert!(is_chain_alive(local_url).await?); - assert!(cache.join("substrate-contracts-node").exists()); + assert!(cache.join("substrate-contracts-node-v0.40.0").exists()); assert!(!cache.join("artifacts").exists()); // Stop the process contracts-node Command::new("kill") diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index e21d61a0..e6ac04d1 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -200,7 +200,7 @@ pub async fn upload_smart_contract( mod tests { use super::*; use crate::{ - create_smart_contract, download_contracts_node, errors::Error, run_contracts_node, + contracts_node_generator, create_smart_contract, errors::Error, run_contracts_node, templates::Contract, }; use anyhow::Result; @@ -341,8 +341,9 @@ mod tests { let cache = temp_dir.path().join(""); - let node_path = download_contracts_node(cache.clone()).await?; - let process = run_contracts_node(node_path.path(), None).await?; + let binary = contracts_node_generator(cache.clone(), None).await?; + binary.source(false, &(), true).await?; + let process = run_contracts_node(binary.path(), None).await?; let upload_exec = set_up_upload(UpOpts { path: Some(temp_dir.path().join("testing")),