Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: run substrate-contracts-node in pop up contract if it does not exist #206

Merged
merged 10 commits into from
Jun 17, 2024
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ assert_cmd = "2.0.14"
predicates = "3.1.0"
dirs = "5.0"
env_logger = "0.11.1"
flate2 = "1.0.30"
duct = "0.13"
git2 = { version = "0.18", features = ["vendored-openssl"] }
log = "0.4.20"
mockito = "1.4.0"
tar = "0.4.40"
tempfile = "3.10"
thiserror = "1.0.58"

Expand Down
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,20 +145,14 @@ Build the Smart Contract:
pop build contract -p ./my_contract
```

To deploy a Smart Contract you need a chain running. For testing purposes you can simply spawn a contract node:

```sh
pop up contracts-node
```

> :information_source: We plan to automate this in the future.

Deploy and instantiate the Smart Contract:

```sh
pop up contract -p ./my_contract --constructor new --args "false" --suri //Alice
```

> :information_source: If you don't specify a live chain, `pop` will automatically spawn a local node for testing purposes.

Some of the options available are:

- Specify the contract `constructor `to use, which in this example is `new()`.
Expand Down Expand Up @@ -198,10 +192,11 @@ pop call contract -p ./my_contract --contract $INSTANTIATED_CONTRACT_ADDRESS --m
## E2E testing

For end-to-end testing you will need to have a Substrate node with `pallet contracts`.
Pop provides the latest version out-of-the-box by running:
You do not need to run it in the background since the node is started for each test independently.
To install the latest version:

```sh
pop up contracts-node
```
cargo install contracts-node --git https://github.com/paritytech/substrate-contracts-node.git
```

If you want to run any other node with `pallet-contracts` you need to change `CONTRACTS_NODE` environment variable:
Expand Down
28 changes: 25 additions & 3 deletions crates/pop-cli/src/commands/up/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

use anyhow::anyhow;
use clap::Args;
use cliclack::{clear_screen, intro, log, outro, outro_cancel};
use cliclack::{clear_screen, confirm, intro, log, outro, outro_cancel};
use pop_contracts::{
build_smart_contract, dry_run_gas_estimate_instantiate, instantiate_smart_contract,
parse_hex_bytes, set_up_deployment, UpOpts,
is_chain_alive, parse_hex_bytes, run_contracts_node, set_up_deployment, UpOpts,
};
use sp_core::Bytes;
use sp_weights::Weight;
Expand Down Expand Up @@ -48,8 +48,11 @@ pub struct UpContractCommand {
/// e.g.
/// - for a dev account "//Alice"
/// - with a password "//Alice///SECRET_PASSWORD"
#[clap(name = "suri", long, short)]
#[clap(name = "suri", long, short, default_value = "//Alice")]
suri: String,
/// Before start a local node, do not ask the user for confirmation.
#[clap(short('y'), long)]
skip_confirm: bool,
}
impl UpContractCommand {
pub(crate) async fn execute(&self) -> anyhow::Result<()> {
Expand All @@ -68,9 +71,28 @@ impl UpContractCommand {
log::success(result.to_string())?;
}

if !is_chain_alive(self.url.clone()).await? {
if !self.skip_confirm {
if !confirm(format!(
"The chain \"{}\" is not live. Would you like pop to start a local node in the background for testing?",
self.url.to_string()
))
.interact()?
{
outro_cancel("You need to specify a live chain to deploy the contract.")?;
return Ok(());
}
}
let process = run_contracts_node(crate::cache()?).await?;
log::success("Local node started successfully in the background.")?;
log::warning(format!("NOTE: The contracts node is running in the background with process ID {}. Please close it manually when done testing.", process.id()))?;
}

// if build exists then proceed
intro(format!("{}: Deploy a smart contract", style(" Pop CLI ").black().on_magenta()))?;

println!("{}: Deploying a smart contract", style(" Pop CLI ").black().on_magenta());

let instantiate_exec = set_up_deployment(UpOpts {
path: self.path.clone(),
constructor: self.constructor.clone(),
Expand Down
31 changes: 0 additions & 31 deletions crates/pop-cli/src/commands/up/contracts_node.rs

This file was deleted.

6 changes: 0 additions & 6 deletions crates/pop-cli/src/commands/up/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

#[cfg(feature = "contract")]
mod contract;
#[cfg(feature = "contract")]
mod contracts_node;
#[cfg(feature = "parachain")]
mod parachain;

Expand All @@ -26,8 +24,4 @@ pub(crate) enum UpCommands {
/// Deploy a smart contract to a node.
#[clap(alias = "c")]
Contract(contract::UpContractCommand),
#[cfg(feature = "contract")]
/// Deploy a contracts node.
#[clap(alias = "n")]
ContractsNode(contracts_node::ContractsNodeCommand),
}
2 changes: 0 additions & 2 deletions crates/pop-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ async fn main() -> Result<()> {
up::UpCommands::Parachain(cmd) => cmd.execute().await.map(|_| Value::Null),
#[cfg(feature = "contract")]
up::UpCommands::Contract(cmd) => cmd.execute().await.map(|_| Value::Null),
#[cfg(feature = "contract")]
up::UpCommands::ContractsNode(cmd) => cmd.execute().await.map(|_| Value::Null),
},
#[cfg(feature = "contract")]
Commands::Test(args) => match &args.command {
Expand Down
9 changes: 8 additions & 1 deletion crates/pop-contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ repository.workspace = true
[dependencies]
anyhow.workspace = true
duct.workspace = true
flate2.workspace = true
reqwest.workspace = true
serde_json.workspace = true
serde.workspace = true
tar.workspace = true
tempfile.workspace = true
thiserror.workspace = true
tokio.workspace = true
url.workspace = true
Expand All @@ -27,4 +33,5 @@ contract-build.workspace = true
contract-extrinsics.workspace = true

[dev-dependencies]
tempfile.workspace = true
mockito.workspace = true

18 changes: 18 additions & 0 deletions crates/pop-contracts/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,22 @@ pub enum Error {

#[error("Failed to parse hex encoded bytes: {0}")]
HexParsing(String),

#[error("Failed to install {0}")]
InstallContractsNode(String),

#[error("Failed to run {0}")]
UpContractsNode(String),

#[error("Unsupported platform: {os}")]
UnsupportedPlatform { os: &'static str },

#[error("Anyhow error: {0}")]
AnyhowError(#[from] anyhow::Error),

#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),

#[error("a git error occurred: {0}")]
Git(String),
}
5 changes: 4 additions & 1 deletion crates/pop-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ pub use test::{test_e2e_smart_contract, test_smart_contract};
pub use up::{
dry_run_gas_estimate_instantiate, instantiate_smart_contract, set_up_deployment, UpOpts,
};
pub use utils::signer::parse_hex_bytes;
pub use utils::{
contracts_node::{is_chain_alive, run_contracts_node},
signer::parse_hex_bytes,
};
163 changes: 163 additions & 0 deletions crates/pop-contracts/src/utils/contracts_node.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use crate::{errors::Error, utils::git::GitHub};
use contract_extrinsics::{RawParams, RpcRequest};
use flate2::read::GzDecoder;
use std::{
env::consts::OS,
io::{Seek, SeekFrom, Write},
path::PathBuf,
process::{Child, Command},
time::Duration,
};
use tar::Archive;
use tempfile::tempfile;
use tokio::time::sleep;

const SUBSTRATE_CONTRACT_NODE: &str = "https://github.com/paritytech/substrate-contracts-node";
const BIN_NAME: &str = "substrate-contracts-node";
const STABLE_VERSION: &str = "v0.41.0";

/// Checks if the specified node is alive and responsive.
///
/// # Arguments
///
/// * `url` - Endpoint of the node.
///
pub async fn is_chain_alive(url: url::Url) -> Result<bool, Error> {
let request = RpcRequest::new(&url).await;
match request {
Ok(request) => {
let params = RawParams::new(&[])?;
let result = request.raw_call("system_health", params).await;
match result {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
},
Err(_) => Ok(false),
}
}

/// Runs the latest version of the `substracte-contracts-node` in the background.
///
/// # Arguments
///
/// * `cache` - The path where the binary will be stored.
///
pub async fn run_contracts_node(cache: PathBuf) -> Result<Child, Error> {
let cached_file = cache.join(release_folder_by_target()?).join(BIN_NAME);
if !cached_file.exists() {
let archive = archive_name_by_target()?;

let latest_version = latest_contract_node_release().await?;
let releases_url =
format!("{SUBSTRATE_CONTRACT_NODE}/releases/download/{latest_version}/{archive}");
// Download archive
let response = reqwest::get(releases_url.as_str()).await?.error_for_status()?;
let mut file = tempfile()?;
file.write_all(&response.bytes().await?)?;
file.seek(SeekFrom::Start(0))?;
// Extract contents
let tar = GzDecoder::new(file);
let mut archive = Archive::new(tar);
archive.unpack(cache.clone())?;
}
let process = Command::new(cached_file.display().to_string().as_str()).spawn()?;

// Wait 5 secs until the node is ready
sleep(Duration::from_millis(5000)).await;
Ok(process)
}

async fn latest_contract_node_release() -> Result<String, Error> {
let repo = GitHub::parse(SUBSTRATE_CONTRACT_NODE)?;
match repo.get_latest_releases().await {
Ok(releases) => {
// Fetching latest releases
for release in releases {
if !release.prerelease {
return Ok(release.tag_name);
}
}
// It should never reach this point, but in case we download a default version of polkadot
Ok(STABLE_VERSION.to_string())
},
// If an error with GitHub API return the STABLE_VERSION
Err(_) => Ok(STABLE_VERSION.to_string()),
}
}

fn archive_name_by_target() -> Result<String, Error> {
match OS {
"macos" => Ok(format!("{}-mac-universal.tar.gz", BIN_NAME)),
"linux" => Ok(format!("{}-linux.tar.gz", BIN_NAME)),
_ => Err(Error::UnsupportedPlatform { os: OS }),
}
}
fn release_folder_by_target() -> Result<&'static str, Error> {
match OS {
"macos" => Ok("artifacts/substrate-contracts-node-mac"),
"linux" => Ok("artifacts/substrate-contracts-node-linux"),
_ => Err(Error::UnsupportedPlatform { os: OS }),
}
}

#[cfg(test)]
mod tests {
use super::*;
use anyhow::{Error, Result};

#[tokio::test]
async fn test_latest_polkadot_release() -> Result<()> {
let version = latest_contract_node_release().await?;
// Result will change all the time to the current version, check at least starts with v
assert!(version.starts_with("v"));
Ok(())
}
#[tokio::test]
async fn release_folder_by_target_works() -> Result<()> {
let path = release_folder_by_target();
if cfg!(target_os = "macos") {
assert_eq!(path?, "artifacts/substrate-contracts-node-mac");
} else if cfg!(target_os = "linux") {
assert_eq!(path?, "artifacts/substrate-contracts-node-linux");
} else {
assert!(path.is_err())
}
Ok(())
}
#[tokio::test]
async fn folder_path_by_target() -> Result<()> {
let archive = archive_name_by_target();
if cfg!(target_os = "macos") {
assert_eq!(archive?, "substrate-contracts-node-mac-universal.tar.gz");
} else if cfg!(target_os = "linux") {
assert_eq!(archive?, "substrate-contracts-node-linux.tar.gz");
} else {
assert!(archive.is_err())
}
Ok(())
}

#[tokio::test]
async fn is_chain_alive_works() -> Result<(), Error> {
let local_url = url::Url::parse("ws://localhost:9944")?;
assert!(!is_chain_alive(local_url).await?);
let polkadot_url = url::Url::parse("wss://polkadot-rpc.dwellir.com")?;
assert!(is_chain_alive(polkadot_url).await?);
Ok(())
}

#[tokio::test]
async fn run_contracts_node_works() -> Result<(), Error> {
let local_url = url::Url::parse("ws://localhost:9944")?;
assert!(!is_chain_alive(local_url.clone()).await?);
// Run the contracts node
let temp_dir = tempfile::tempdir().expect("Could not create temp dir");
let cache = temp_dir.path().join("cache");
let mut process = run_contracts_node(cache).await?;
// Check if the node is alive
assert!(is_chain_alive(local_url).await?);
process.kill()?;
Ok(())
}
}
Loading
Loading