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: improve up ux #248

Merged
merged 3 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions crates/pop-cli/src/commands/build/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use crate::cli;
use clap::Args;
use pop_contracts::build_smart_contract;
use pop_contracts::{build_smart_contract, Verbosity};
use std::path::PathBuf;
#[cfg(not(test))]
use std::{thread::sleep, time::Duration};
Expand Down Expand Up @@ -42,8 +42,9 @@ impl BuildContractCommand {
}

// Build contract.
let build_result = build_smart_contract(self.path.as_deref(), self.release)?;
cli.success(build_result)?;
let build_result =
build_smart_contract(self.path.as_deref(), self.release, Verbosity::Default)?;
cli.success(build_result.display())?;
cli.outro("Build completed successfully!")?;
Ok("contract")
}
Expand Down
184 changes: 142 additions & 42 deletions crates/pop-cli/src/commands/up/contract.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
// SPDX-License-Identifier: GPL-3.0

use crate::style::style;
use crate::{
cli::{traits::Cli as _, Cli},
style::style,
};
use clap::Args;
use cliclack::{clear_screen, confirm, intro, log, log::success, outro, outro_cancel};
use cliclack::{confirm, log, log::error, spinner};
use console::{Emoji, Style};
use pop_contracts::{
build_smart_contract, dry_run_gas_estimate_instantiate, dry_run_upload,
instantiate_smart_contract, is_chain_alive, parse_hex_bytes, run_contracts_node,
set_up_deployment, set_up_upload, upload_smart_contract, UpOpts,
set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, Verbosity,
};
use sp_core::Bytes;
use sp_weights::Weight;
use std::path::PathBuf;
use std::process::Child;
use tempfile::NamedTempFile;
use url::Url;

const COMPLETE: &str = "🚀 Deployment complete";
const DEFAULT_URL: &str = "ws://localhost:9944/";
const FAILED: &str = "🚫 Deployment failed.";

#[derive(Args, Clone)]
pub struct UpContractCommand {
Expand Down Expand Up @@ -39,8 +50,8 @@ pub struct UpContractCommand {
/// instances of the same contract code from the same account.
#[clap(long, value_parser = parse_hex_bytes)]
salt: Option<Bytes>,
/// Websocket endpoint of a node.
#[clap(name = "url", long, value_parser, default_value = "ws://localhost:9944")]
/// Websocket endpoint of a chain.
#[clap(name = "url", long, value_parser, default_value = DEFAULT_URL)]
url: url::Url,
/// Secret key URI for the account deploying the contract.
///
Expand All @@ -55,54 +66,105 @@ pub struct UpContractCommand {
/// Uploads the contract only, without instantiation.
#[clap(short('u'), long)]
upload_only: bool,
/// Before start a local node, do not ask the user for confirmation.
/// Before starting a local node, do not ask the user for confirmation.
#[clap(short('y'), long)]
skip_confirm: bool,
}

impl UpContractCommand {
/// Executes the command.
pub(crate) async fn execute(self) -> anyhow::Result<()> {
clear_screen()?;
pub(crate) async fn execute(mut self) -> anyhow::Result<()> {
Cli.intro("Deploy a smart contract")?;

// Check if build exists in the specified "Contract build folder"
let build_path = PathBuf::from(
self.path.clone().unwrap_or("/.".into()).to_string_lossy().to_string() + "/target/ink",
);

if !build_path.as_path().exists() {
log::warning("NOTE: contract has not yet been built.")?;
intro(format!("{}: Building a contract", style(" Pop CLI ").black().on_magenta()))?;
// Build the contract in release mode
let result = build_smart_contract(self.path.as_deref(), true)?;
log::success(result.to_string())?;
Cli.warning("NOTE: contract has not yet been built.")?;
let spinner = spinner();
spinner.start("Building contract in RELEASE mode...");
let result = match build_smart_contract(self.path.as_deref(), true, Verbosity::Quiet) {
Ok(result) => result,
Err(e) => {
Cli.outro_cancel(format!("🚫 An error occurred building your contract: {e}\nUse `pop build` to retry with build output."))?;
return Ok(());
},
};
spinner.stop(format!(
"Your contract artifacts are ready. You can find them in: {}",
result.target_directory.display()
));
}

if !is_chain_alive(self.url.clone()).await? {
// Check if specified chain is accessible
let process = if !is_chain_alive(self.url.clone()).await? {
if !self.skip_confirm {
let chain = if self.url.as_str() == DEFAULT_URL {
"No endpoint was specified.".into()
} else {
format!("The specified endpoint of {} is inaccessible.", self.url)
};

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(());
}
"{chain} Would you like to start a local node in the background for testing?",
))
.initial_value(true)
.interact()?
{
Cli.outro_cancel(
"🚫 You need to specify an accessible endpoint 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()))?;
// Update url to that of the launched node
self.url = Url::parse(DEFAULT_URL).expect("default url is valid");

let spinner = spinner();
spinner.start("Starting local node...");
let log = tempfile::NamedTempFile::new()?;
let process = run_contracts_node(crate::cache()?, Some(log.as_file())).await?;
let bar = Style::new().magenta().dim().apply_to(Emoji("│", "|"));
spinner.stop(format!(
"Local node started successfully:{}",
style(format!(
"
{bar} {}
{bar} {}",
style(format!(
"portal: https://polkadot.js.org/apps/?rpc={}#/explorer",
self.url
))
.dim(),
style(format!("logs: tail -f {}", log.path().display())).dim(),
))
.dim()
));
Some((process, log))
} else {
None
};

// Check for upload only.
if self.upload_only {
return self.upload_contract().await;
let result = self.upload_contract().await;
Self::terminate_node(process)?;
match result {
Ok(_) => {
Cli.outro(COMPLETE)?;
},
Err(_) => {
Cli.outro_cancel(FAILED)?;
},
}
return Ok(());
}

let instantiate_exec = set_up_deployment(UpOpts {
// Otherwise instantiate.
let instantiate_exec = match set_up_deployment(UpOpts {
path: self.path.clone(),
constructor: self.constructor.clone(),
args: self.args.clone(),
Expand All @@ -113,37 +175,51 @@ impl UpContractCommand {
url: self.url.clone(),
suri: self.suri.clone(),
})
.await?;
.await
{
Ok(i) => i,
Err(e) => {
error(format!("An error occurred instantiating the contract: {e}"))?;
Self::terminate_node(process)?;
Cli.outro_cancel(FAILED)?;
return Ok(());
},
};

let weight_limit;
if self.gas_limit.is_some() && self.proof_size.is_some() {
weight_limit = Weight::from_parts(self.gas_limit.unwrap(), self.proof_size.unwrap());
} else {
let spinner = cliclack::spinner();
let spinner = spinner();
spinner.start("Doing a dry run to estimate the gas...");
weight_limit = match dry_run_gas_estimate_instantiate(&instantiate_exec).await {
Ok(w) => {
log::info(format!("Gas limit: {:?}", w))?;
spinner.stop(format!("Gas limit estimate: {:?}", w));
w
},
Err(e) => {
spinner.error(format!("{e}"));
outro_cancel("Deployment failed.")?;
Self::terminate_node(process)?;
Cli.outro_cancel(FAILED)?;
return Ok(());
},
};
}

// Finally upload and instantiate.
if !self.dry_run {
let spinner = cliclack::spinner();
let spinner = spinner();
spinner.start("Uploading and instantiating the contract...");
let contract_address =
instantiate_smart_contract(instantiate_exec, weight_limit).await?;
spinner.stop(format!(
"Contract deployed and instantiated: The Contract Address is {:?}",
contract_address
));
outro("Deployment complete")?;
Self::terminate_node(process)?;
Cli.outro(COMPLETE)?;
}

Ok(())
}

Expand All @@ -161,23 +237,47 @@ impl UpContractCommand {
style(format!("{} {s}", console::Emoji("●", ">"))).dim().to_string()
})
.collect();
success(format!("Dry run successful!\n{}", result.join("\n")))?;
Cli.success(format!("Dry run successful!\n{}", result.join("\n")))?;
},
Err(_) => {
outro_cancel("Deployment failed.")?;
Cli.outro_cancel(FAILED)?;
return Ok(());
},
};
} else {
let spinner = cliclack::spinner();
spinner.start("Uploading the contract...");
let code_hash = upload_smart_contract(&upload_exec).await?;
let spinner = spinner();
spinner.start("Uploading your contract...");
let code_hash = match upload_smart_contract(&upload_exec).await {
Ok(r) => r,
Err(e) => {
spinner.error(format!("An error occurred uploading your contract: {e}"));
return Err(e.into());
},
};
spinner.stop(format!("Contract uploaded: The code hash is {:?}", code_hash));
outro("Deployment complete")?;
log::warning("NOTE: The contract has not been instantiated.")?;
}
return Ok(());
}

/// Handles the optional termination of a local running node.
fn terminate_node(process: Option<(Child, NamedTempFile)>) -> anyhow::Result<()> {
// Prompt to close any launched node
let Some((mut process, log)) = process else {
return Ok(());
};
if confirm("Would you like to terminate the local node?")
.initial_value(true)
.interact()?
{
process.kill()?
} else {
log.keep()?;
log::warning(format!("NOTE: The node is running in the background with process ID {}. Please terminate it manually when done.", process.id()))?;
}

Ok(())
}
}

impl From<UpContractCommand> for UpOpts {
Expand Down
2 changes: 1 addition & 1 deletion crates/pop-cli/src/commands/up/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub(crate) enum Command {
#[clap(alias = "p")]
Parachain(parachain::ZombienetCommand),
#[cfg(feature = "contract")]
/// Deploy a smart contract to a node.
/// Deploy a smart contract.
#[clap(alias = "c")]
Contract(contract::UpContractCommand),
}
18 changes: 11 additions & 7 deletions crates/pop-contracts/src/build.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
// SPDX-License-Identifier: GPL-3.0

use crate::{errors::Error, utils::helpers::get_manifest_path};
use contract_build::{execute, BuildMode, ExecuteArgs};
pub use contract_build::Verbosity;
use contract_build::{execute, BuildMode, BuildResult, ExecuteArgs};
use std::path::Path;

/// Build the smart contract located at the specified `path` in `build_release` mode.
///
/// # Arguments
/// * `path` - The optional path to the smart contract manifest, defaulting to the current directory if not specified.
/// * `release` - Whether the smart contract should be built without any debugging functionality.
pub fn build_smart_contract(path: Option<&Path>, release: bool) -> anyhow::Result<String> {
/// * `verbosity` - The build output verbosity.
pub fn build_smart_contract(
path: Option<&Path>,
release: bool,
verbosity: Verbosity,
) -> anyhow::Result<BuildResult> {
let manifest_path = get_manifest_path(path)?;

let build_mode = match release {
true => BuildMode::Release,
false => BuildMode::Debug,
};

// Default values
let args = ExecuteArgs { manifest_path, build_mode, ..Default::default() };
let args = ExecuteArgs { manifest_path, build_mode, verbosity, ..Default::default() };

// Execute the build and log the output of the build
let result = execute(args)?;
let formatted_result = result.display();

Ok(formatted_result)
Ok(execute(args)?)
}

/// Determines whether the manifest at the supplied path is a supported smart contract project.
Expand Down
2 changes: 1 addition & 1 deletion crates/pop-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod test;
mod up;
mod utils;

pub use build::{build_smart_contract, is_supported};
pub use build::{build_smart_contract, is_supported, Verbosity};
pub use call::{
call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts,
};
Expand Down
2 changes: 1 addition & 1 deletion crates/pop-contracts/src/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ mod tests {
mock_build_process(temp_dir.path().join("testing"))?;
// Run contracts-node
let cache = temp_dir.path().join("cache");
let mut process = run_contracts_node(cache).await?;
let mut process = run_contracts_node(cache, None).await?;

let upload_exec = set_up_upload(UpOpts {
path: Some(temp_dir.path().join("testing")),
Expand Down
Loading
Loading