diff --git a/Cargo.lock b/Cargo.lock index dc0d5c79..2586906d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2497,6 +2497,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assert_cmd", + "async-trait", "bigdecimal", "bip32 0.5.3", "byte-unit 5.1.6", diff --git a/crates/icp-cli/Cargo.toml b/crates/icp-cli/Cargo.toml index d70d162d..52fba068 100644 --- a/crates/icp-cli/Cargo.toml +++ b/crates/icp-cli/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" [dependencies] anyhow.workspace = true +async-trait.workspace = true bigdecimal.workspace = true bip32.workspace = true byte-unit.workspace = true diff --git a/crates/icp-cli/src/commands/args.rs b/crates/icp-cli/src/commands/args.rs new file mode 100644 index 00000000..fb94aa68 --- /dev/null +++ b/crates/icp-cli/src/commands/args.rs @@ -0,0 +1,93 @@ +use candid::Principal; + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Canister { + Name(String), + Principal(Principal), +} + +impl From<&str> for Canister { + fn from(v: &str) -> Self { + if let Ok(p) = Principal::from_text(v) { + return Self::Principal(p); + } + + Self::Name(v.to_string()) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Network { + Name(String), + Url(String), +} + +impl From<&str> for Network { + fn from(v: &str) -> Self { + if v.starts_with("http://") || v.starts_with("https://") { + return Self::Url(v.to_string()); + } + + Self::Name(v.to_string()) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Environment { + Name(String), +} + +impl Default for Environment { + fn default() -> Self { + Self::Name("local".to_string()) + } +} + +impl From<&str> for Environment { + fn from(v: &str) -> Self { + Self::Name(v.to_string()) + } +} + +#[cfg(test)] +mod tests { + use candid::Principal; + + use crate::commands::args::{Canister, Network}; + + #[test] + fn canister_by_name() { + assert_eq!( + Canister::from("my-canister"), + Canister::Name("my-canister".to_string()), + ); + } + + #[test] + fn canister_by_principal() { + let cid = "ntyui-iatoh-pfi3f-27wnk-vgdqt-mq3cl-ld7jh-743kl-sde6i-tbm7g-tqe"; + + assert_eq!( + Canister::from(cid), + Canister::Principal(Principal::from_text(cid).expect("failed to parse principal")), + ); + } + + #[test] + fn network_by_name() { + assert_eq!( + Network::from("my-network"), + Network::Name("my-network".to_string()), + ); + } + + #[test] + fn network_by_url_http() { + let url = "http://www.example.com"; + + assert_eq!( + Network::from(url), + Network::Url("http://www.example.com".to_string()), + ); + } +} diff --git a/crates/icp-cli/src/commands/build/mod.rs b/crates/icp-cli/src/commands/build/mod.rs index 7c2ddcf4..857f330e 100644 --- a/crates/icp-cli/src/commands/build/mod.rs +++ b/crates/icp-cli/src/commands/build/mod.rs @@ -22,6 +22,9 @@ pub(crate) struct BuildArgs { #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error(transparent)] + Project(#[from] icp::LoadError), + #[error("project does not contain a canister named '{name}'")] CanisterNotFound { name: String }, @@ -51,9 +54,9 @@ pub(crate) async fn exec(ctx: &Context, args: &BuildArgs) -> Result<(), CommandE unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { // Load the project manifest, which defines the canisters to be built. - let p = ctx.project.load().await.context("failed to load project")?; + let p = ctx.project.load(pdir).await?; // Choose canisters to build let cnames = match args.names.is_empty() { diff --git a/crates/icp-cli/src/commands/canister/binding_env_vars.rs b/crates/icp-cli/src/commands/canister/binding_env_vars.rs index c68b9b45..b745aef3 100644 --- a/crates/icp-cli/src/commands/canister/binding_env_vars.rs +++ b/crates/icp-cli/src/commands/canister/binding_env_vars.rs @@ -12,8 +12,8 @@ use icp::{agent, identity, network}; use tracing::debug; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, progress::{ProgressManager, ProgressManagerSettings}, store_artifact::LookupError as LookupArtifactError, store_id::{Key, LookupError as LookupIdError}, @@ -27,8 +27,11 @@ pub(crate) struct BindingArgs { #[command(flatten)] pub(crate) identity: IdentityOpt, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] @@ -82,19 +85,21 @@ pub(crate) async fn exec(ctx: &Context, args: &BindingArgs) -> Result<(), Comman unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load the project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index b1c48be4..8247eaf8 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -6,15 +6,14 @@ use dialoguer::console::Term; use icp::{agent, identity, network}; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, store_id::{Key, LookupError}, }; #[derive(Args, Debug)] pub(crate) struct CallArgs { - /// Name of canister to call to - pub(crate) name: String, + pub(crate) canister: args::Canister, /// Name of canister method to call into pub(crate) method: String, @@ -23,14 +22,20 @@ pub(crate) struct CallArgs { pub(crate) args: String, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -71,22 +76,32 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -99,10 +114,10 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), CommandEr } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -110,7 +125,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), CommandEr let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; // Parse candid arguments @@ -131,7 +146,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), CommandEr } /// Pretty-prints IDLArgs detecting the terminal's width to avoid the 80-column default. -pub(crate) fn print_candid_for_term(term: &mut Term, args: &IDLArgs) -> io::Result<()> { +fn print_candid_for_term(term: &mut Term, args: &IDLArgs) -> io::Result<()> { if term.is_term() { let width = term.size().1 as usize; let pp_args = candid_parser::pretty::candid::value::pp_args(args); diff --git a/crates/icp-cli/src/commands/canister/create.rs b/crates/icp-cli/src/commands/canister/create.rs index b24b824d..44a89512 100644 --- a/crates/icp-cli/src/commands/canister/create.rs +++ b/crates/icp-cli/src/commands/canister/create.rs @@ -17,8 +17,8 @@ use icp_canister_interfaces::{ use rand::seq::IndexedRandom; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, progress::{ProgressManager, ProgressManagerSettings}, store_id::{Key, LookupError, RegisterError}, }; @@ -52,8 +52,8 @@ pub(crate) struct CreateArgs { #[command(flatten)] pub(crate) identity: IdentityOpt, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) environment: Option, /// One or more controllers for the canister. Repeat `--controller` to specify multiple. #[arg(long)] @@ -136,19 +136,21 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), Command unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Collect environment canisters let cnames = match args.names.is_empty() { diff --git a/crates/icp-cli/src/commands/canister/delete.rs b/crates/icp-cli/src/commands/canister/delete.rs index 4201962f..ab52704d 100644 --- a/crates/icp-cli/src/commands/canister/delete.rs +++ b/crates/icp-cli/src/commands/canister/delete.rs @@ -3,25 +3,30 @@ use ic_agent::AgentError; use icp::{agent, identity, network}; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, store_id::{Key, LookupError as LookupIdError}, }; #[derive(Debug, Args)] pub(crate) struct DeleteArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -53,22 +58,32 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -81,10 +96,10 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), Command } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -92,7 +107,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), Command let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; // Management Interface diff --git a/crates/icp-cli/src/commands/canister/info.rs b/crates/icp-cli/src/commands/canister/info.rs index 909c1bc8..facdd70a 100644 --- a/crates/icp-cli/src/commands/canister/info.rs +++ b/crates/icp-cli/src/commands/canister/info.rs @@ -5,25 +5,30 @@ use icp::{agent, identity, network}; use itertools::Itertools; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, store_id::{Key, LookupError as LookupIdError}, }; #[derive(Debug, Args)] pub(crate) struct InfoArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -55,22 +60,32 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &InfoArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -83,10 +98,10 @@ pub(crate) async fn exec(ctx: &Context, args: &InfoArgs) -> Result<(), CommandEr } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -94,7 +109,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InfoArgs) -> Result<(), CommandEr let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; // Management Interface @@ -111,7 +126,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InfoArgs) -> Result<(), CommandEr Ok(()) } -pub(crate) fn print_info(result: &CanisterStatusResult) { +fn print_info(result: &CanisterStatusResult) { let controllers: Vec = result .settings .controllers diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index a7d2d370..979543c6 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -8,8 +8,8 @@ use icp::{agent, identity, network}; use tracing::debug; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, progress::{ProgressManager, ProgressManagerSettings}, store_artifact::LookupError as LookupArtifactError, store_id::{Key, LookupError as LookupIdError}, @@ -27,8 +27,8 @@ pub(crate) struct InstallArgs { #[command(flatten)] pub(crate) identity: IdentityOpt, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] @@ -76,19 +76,21 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), Comman unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load the project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; diff --git a/crates/icp-cli/src/commands/canister/list.rs b/crates/icp-cli/src/commands/canister/list.rs index 00d54ba3..25d4b481 100644 --- a/crates/icp-cli/src/commands/canister/list.rs +++ b/crates/icp-cli/src/commands/canister/list.rs @@ -1,14 +1,11 @@ use clap::Args; -use crate::{ - commands::{Context, Mode}, - options::EnvironmentOpt, -}; +use crate::commands::{Context, Mode, args}; #[derive(Debug, Args)] pub(crate) struct ListArgs { - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] @@ -26,16 +23,18 @@ pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), CommandEr unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; for (_, c) in env.canisters.values() { let _ = ctx.term.write_line(&format!("{c:?}")); diff --git a/crates/icp-cli/src/commands/canister/mod.rs b/crates/icp-cli/src/commands/canister/mod.rs index b1346e42..aae29553 100644 --- a/crates/icp-cli/src/commands/canister/mod.rs +++ b/crates/icp-cli/src/commands/canister/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod status; pub(crate) mod stop; pub(crate) mod top_up; +#[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub(crate) enum Command { Call(call::CallArgs), diff --git a/crates/icp-cli/src/commands/canister/settings/show.rs b/crates/icp-cli/src/commands/canister/settings/show.rs index 8050de3f..15397b48 100644 --- a/crates/icp-cli/src/commands/canister/settings/show.rs +++ b/crates/icp-cli/src/commands/canister/settings/show.rs @@ -4,25 +4,30 @@ use ic_management_canister_types::{CanisterStatusResult, LogVisibility}; use icp::{agent, identity, network}; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, store_id::{Key, LookupError as LookupIdError}, }; #[derive(Debug, Args)] pub(crate) struct ShowArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -54,22 +59,32 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -82,10 +97,10 @@ pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandEr } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -93,7 +108,7 @@ pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandEr let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; // Management Interface @@ -108,7 +123,7 @@ pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandEr Ok(()) } -pub(crate) fn print_settings(result: &CanisterStatusResult) { +fn print_settings(result: &CanisterStatusResult) { eprintln!("Canister Settings:"); let settings = &result.settings; diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index ffb42987..1f2789b4 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -7,8 +7,8 @@ use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, Lo use icp::{agent, identity, network}; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, store_id::{Key, LookupError as LookupIdError}, }; @@ -74,45 +74,50 @@ impl EnvironmentVariableOpt { #[derive(Debug, Args)] pub(crate) struct UpdateArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, #[command(flatten)] - controllers: Option, + pub(crate) controllers: Option, #[arg(long, value_parser = compute_allocation_parser)] - compute_allocation: Option, + pub(crate) compute_allocation: Option, #[arg(long, value_parser = memory_parser)] - memory_allocation: Option, + pub(crate) memory_allocation: Option, #[arg(long, value_parser = freezing_threshold_parser)] - freezing_threshold: Option, + pub(crate) freezing_threshold: Option, #[arg(long, value_parser = reserved_cycles_limit_parser)] - reserved_cycles_limit: Option, + pub(crate) reserved_cycles_limit: Option, #[arg(long, value_parser = memory_parser)] - wasm_memory_limit: Option, + pub(crate) wasm_memory_limit: Option, #[arg(long, value_parser = memory_parser)] - wasm_memory_threshold: Option, + pub(crate) wasm_memory_threshold: Option, #[command(flatten)] - log_visibility: Option, + pub(crate) log_visibility: Option, #[command(flatten)] - environment_variables: Option, + pub(crate) environment_variables: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -147,22 +152,32 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -175,10 +190,10 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), Command } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -186,7 +201,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), Command let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; // Management Interface diff --git a/crates/icp-cli/src/commands/canister/show.rs b/crates/icp-cli/src/commands/canister/show.rs index 6468cb8f..b20d774f 100644 --- a/crates/icp-cli/src/commands/canister/show.rs +++ b/crates/icp-cli/src/commands/canister/show.rs @@ -1,22 +1,23 @@ use clap::Args; use crate::{ - commands::{Context, Mode}, - options::EnvironmentOpt, + commands::{Context, Mode, args}, store_id::{Key, LookupError as LookupIdError}, }; #[derive(Debug, Args)] pub(crate) struct ShowArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -36,25 +37,35 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -62,10 +73,10 @@ pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandEr let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; - println!("{cid} => {}", args.name); + println!("{cid} => {}", name); // TODO(or.ricon): Show canister details // Things we might want to show (do we need to sub-command this?) diff --git a/crates/icp-cli/src/commands/canister/start.rs b/crates/icp-cli/src/commands/canister/start.rs index 91e45cdc..eebd0bc5 100644 --- a/crates/icp-cli/src/commands/canister/start.rs +++ b/crates/icp-cli/src/commands/canister/start.rs @@ -1,27 +1,59 @@ +use anyhow::{Context as _, anyhow}; use clap::Args; use ic_agent::AgentError; use icp::{agent, identity, network}; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{ + Context, Mode, args, + validation::{self, Validate, ValidateError}, + }, + impl_from_args, + options::IdentityOpt, store_id::{Key, LookupError as LookupIdError}, }; #[derive(Debug, Args)] pub(crate) struct StartArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, +} + +impl_from_args!(StartArgs, canister: args::Canister); +impl_from_args!(StartArgs, network: Option); +impl_from_args!(StartArgs, environment: Option); +impl_from_args!(StartArgs, network: Option, environment: Option); + +impl Validate for StartArgs { + fn validate(&self, mode: &Mode) -> Result<(), ValidateError> { + for test in [ + validation::a_canister_id_is_required_in_global_mode, + validation::a_network_url_is_required_in_global_mode, + validation::environments_are_not_available_in_a_global_mode, + validation::network_or_environment_not_both, + ] { + test(self, mode) + .map(|msg| anyhow::format_err!(msg)) + .map_or(Ok(()), Err)?; + } + + Ok(()) + } } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -48,27 +80,53 @@ pub(crate) enum CommandError { #[error(transparent)] Start(#[from] AgentError), + + #[error(transparent)] + Unexpected(#[from] anyhow::Error), } pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), CommandError> { - match &ctx.mode { + let (agent, cid) = match &ctx.mode { Mode::Global => { - unimplemented!("global mode is not implemented yet"); + // Argument (Canister) + let args::Canister::Principal(cid) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Network) + let Some(args::Network::Url(url)) = args.network.clone() else { + return Err(CommandError::Args); + }; + + // Load identity + let id = ctx.identity.load(args.identity.clone().into()).await?; + + // Agent + let agent = ctx.agent.create(id, &url).await?; + + (agent, cid.to_owned()) } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Canister) + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -81,10 +139,10 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), CommandE } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -92,16 +150,17 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), CommandE let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to start canister - mgmt.start_canister(&cid).await?; + (agent, cid) } - } + }; + + (ctx.ops.canister.start)(&agent) + .start(&cid) + .await + .context(anyhow!("failed to start canister {cid}"))?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/status.rs b/crates/icp-cli/src/commands/canister/status.rs index df0ae3f3..d7bfafde 100644 --- a/crates/icp-cli/src/commands/canister/status.rs +++ b/crates/icp-cli/src/commands/canister/status.rs @@ -4,25 +4,30 @@ use ic_management_canister_types::{CanisterStatusResult, LogVisibility}; use icp::{agent, identity, network}; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, store_id::{Key, LookupError as LookupIdError}, }; #[derive(Debug, Args)] pub(crate) struct StatusArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -54,22 +59,32 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -82,10 +97,10 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), Command } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -93,7 +108,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), Command let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; // Management Interface @@ -110,7 +125,8 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), Command Ok(()) } -pub(crate) fn print_status(result: &CanisterStatusResult) { +// TODO(or.ricon): Convert this to indoc? +fn print_status(result: &CanisterStatusResult) { eprintln!("Canister Status Report:"); eprintln!(" Status: {:?}", result.status); diff --git a/crates/icp-cli/src/commands/canister/stop.rs b/crates/icp-cli/src/commands/canister/stop.rs index 4dcd26e6..88934212 100644 --- a/crates/icp-cli/src/commands/canister/stop.rs +++ b/crates/icp-cli/src/commands/canister/stop.rs @@ -1,27 +1,60 @@ +use anyhow::{Context as _, anyhow}; use clap::Args; use ic_agent::AgentError; use icp::{agent, identity, network}; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{ + Context, Mode, args, + validation::{self, Validate, ValidateError}, + }, + impl_from_args, + options::IdentityOpt, store_id::{Key, LookupError as LookupIdError}, }; #[derive(Debug, Args)] pub(crate) struct StopArgs { /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, +} + +impl_from_args!(StopArgs, canister: args::Canister); +impl_from_args!(StopArgs, network: Option); +impl_from_args!(StopArgs, environment: Option); +impl_from_args!(StopArgs, network: Option, environment: Option); + +impl Validate for StopArgs { + fn validate(&self, mode: &Mode) -> Result<(), ValidateError> { + for test in [ + validation::a_canister_id_is_required_in_global_mode, + validation::a_network_url_is_required_in_global_mode, + validation::environments_are_not_available_in_a_global_mode, + validation::network_or_environment_not_both, + ] { + test(self, mode) + .map(|msg| anyhow::format_err!(msg)) + .map_or(Ok(()), Err)?; + } + + Ok(()) + } } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -48,27 +81,52 @@ pub(crate) enum CommandError { #[error(transparent)] Stop(#[from] AgentError), + + #[error(transparent)] + Unexpected(#[from] anyhow::Error), } pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), CommandError> { - match &ctx.mode { + let (agent, cid) = match &ctx.mode { Mode::Global => { - unimplemented!("global mode is not implemented yet"); + // Argument (Canister) + let args::Canister::Principal(cid) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Network) + let Some(args::Network::Url(url)) = args.network.clone() else { + return Err(CommandError::Args); + }; + + // Load identity + let id = ctx.identity.load(args.identity.clone().into()).await?; + + // Agent + let agent = ctx.agent.create(id, &url).await?; + + (agent, cid.to_owned()) } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -81,10 +139,10 @@ pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), CommandEr } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -92,16 +150,17 @@ pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), CommandEr let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to stop canister - mgmt.stop_canister(&cid).await?; + (agent, cid) } - } + }; + + (ctx.ops.canister.stop)(&agent) + .stop(&cid) + .await + .context(anyhow!("failed to stop canister {cid}"))?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/top_up.rs b/crates/icp-cli/src/commands/canister/top_up.rs index eefaaca9..96cead3f 100644 --- a/crates/icp-cli/src/commands/canister/top_up.rs +++ b/crates/icp-cli/src/commands/canister/top_up.rs @@ -8,29 +8,34 @@ use icp_canister_interfaces::cycles_ledger::{ }; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, store_id::{Key, LookupError}, }; #[derive(Debug, Args)] pub(crate) struct TopUpArgs { - /// The name of the canister within the current project - pub(crate) name: String, + pub(crate) canister: args::Canister, /// Amount of cycles to top up #[arg(long)] pub(crate) amount: u128, #[command(flatten)] - identity: IdentityOpt, + pub(crate) identity: IdentityOpt, - #[command(flatten)] - environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -68,22 +73,32 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &TopUpArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global => { + let args::Canister::Principal(_) = &args.canister else { + return Err(CommandError::Args); + }; + unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + let args::Canister::Name(name) = &args.canister else { + return Err(CommandError::Args); + }; + + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -96,10 +111,10 @@ pub(crate) async fn exec(ctx: &Context, args: &TopUpArgs) -> Result<(), CommandE } // Ensure canister is included in the environment - if !env.canisters.contains_key(&args.name) { + if !env.canisters.contains_key(name) { return Err(CommandError::EnvironmentCanister { environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), }); } @@ -107,7 +122,7 @@ pub(crate) async fn exec(ctx: &Context, args: &TopUpArgs) -> Result<(), CommandE let cid = ctx.ids.lookup(&Key { network: env.network.name.to_owned(), environment: env.name.to_owned(), - canister: args.name.to_owned(), + canister: name.to_owned(), })?; let cargs = WithdrawArgs { @@ -130,7 +145,7 @@ pub(crate) async fn exec(ctx: &Context, args: &TopUpArgs) -> Result<(), CommandE let _ = ctx.term.write_line(&format!( "Topped up canister {} with {}T cycles", - args.name, + name, BigDecimal::new(args.amount.into(), CYCLES_LEDGER_DECIMALS) )); } diff --git a/crates/icp-cli/src/commands/cycles/mint.rs b/crates/icp-cli/src/commands/cycles/mint.rs index 7b69ddd7..e2317588 100644 --- a/crates/icp-cli/src/commands/cycles/mint.rs +++ b/crates/icp-cli/src/commands/cycles/mint.rs @@ -16,8 +16,8 @@ use icp_canister_interfaces::{ }; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, }; #[derive(Debug, Args)] @@ -30,11 +30,14 @@ pub(crate) struct MintArgs { #[arg(long, conflicts_with = "icp")] pub(crate) cycles: Option, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, - #[command(flatten)] pub(crate) identity: IdentityOpt, + + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] @@ -88,19 +91,21 @@ pub(crate) async fn exec(ctx: &Context, args: &MintArgs) -> Result<(), CommandEr unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; diff --git a/crates/icp-cli/src/commands/deploy/mod.rs b/crates/icp-cli/src/commands/deploy/mod.rs index d4e9a61b..0904dec7 100644 --- a/crates/icp-cli/src/commands/deploy/mod.rs +++ b/crates/icp-cli/src/commands/deploy/mod.rs @@ -3,7 +3,7 @@ use ic_agent::export::Principal; use crate::{ commands::{ - Context, Mode, build, + Context, Mode, args, build, canister::{ binding_env_vars, create::{self, CanisterSettings}, @@ -11,7 +11,7 @@ use crate::{ }, sync, }, - options::{EnvironmentOpt, IdentityOpt}, + options::IdentityOpt, }; #[derive(Args, Debug)] @@ -38,8 +38,8 @@ pub(crate) struct DeployArgs { #[command(flatten)] pub(crate) identity: IdentityOpt, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] @@ -81,16 +81,18 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), Command unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load the project manifest, which defines the canisters to be built. - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; let cnames = match args.names.is_empty() { // No canisters specified @@ -166,6 +168,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), Command &binding_env_vars::BindingArgs { names: cnames.to_owned(), identity: args.identity.clone(), + network: None, environment: args.environment.clone(), }, ) diff --git a/crates/icp-cli/src/commands/environment/list.rs b/crates/icp-cli/src/commands/environment/list.rs index fa91fdca..87626cda 100644 --- a/crates/icp-cli/src/commands/environment/list.rs +++ b/crates/icp-cli/src/commands/environment/list.rs @@ -17,12 +17,12 @@ pub(crate) async fn exec(ctx: &Context, _: &ListArgs) -> Result<(), CommandError unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { // Load project - let pm = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // List environments - for e in &pm.environments { + for e in &p.environments { let _ = ctx.term.write_line(&format!("{e:?}")); } } diff --git a/crates/icp-cli/src/commands/identity/default.rs b/crates/icp-cli/src/commands/identity/default.rs index 7b27d7c2..e03e6822 100644 --- a/crates/icp-cli/src/commands/identity/default.rs +++ b/crates/icp-cli/src/commands/identity/default.rs @@ -8,7 +8,7 @@ use crate::commands::{Context, Mode}; #[derive(Debug, Args)] pub(crate) struct DefaultArgs { - name: Option, + pub(crate) name: Option, } #[derive(Debug, thiserror::Error)] @@ -23,7 +23,6 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &DefaultArgs) -> Result<(), CommandError> { match &ctx.mode { Mode::Global | Mode::Project(_) => { - // Load project directories let dir = ctx.dirs.identity(); match &args.name { diff --git a/crates/icp-cli/src/commands/macros.rs b/crates/icp-cli/src/commands/macros.rs new file mode 100644 index 00000000..ad02d8ec --- /dev/null +++ b/crates/icp-cli/src/commands/macros.rs @@ -0,0 +1,21 @@ +/// Implements `From<&'a Struct>` for a tuple of references to its fields. +#[macro_export] +macro_rules! impl_from_args { + // Rule for a single field conversion + ($struct_name:ident, $field:ident: $type:ty) => { + impl<'a> From<&'a $struct_name> for (&'a $type,) { + fn from(args: &'a $struct_name) -> Self { + (&args.$field,) + } + } + }; + + // Rule for multiple field conversions + ($struct_name:ident, $($field:ident: $type:ty),+) => { + impl<'a> From<&'a $struct_name> for ($(&'a $type),+) { + fn from(args: &'a $struct_name) -> Self { + ($(&args.$field),+) + } + } + }; +} diff --git a/crates/icp-cli/src/commands/mod.rs b/crates/icp-cli/src/commands/mod.rs index cbc358de..8ddd8496 100644 --- a/crates/icp-cli/src/commands/mod.rs +++ b/crates/icp-cli/src/commands/mod.rs @@ -8,20 +8,23 @@ use icp::{ prelude::*, }; -use crate::{store_artifact::ArtifactStore, store_id::IdStore}; +use crate::{operations::Initializers, store_artifact::ArtifactStore, store_id::IdStore}; +pub(crate) mod args; pub(crate) mod build; pub(crate) mod canister; pub(crate) mod cycles; pub(crate) mod deploy; pub(crate) mod environment; pub(crate) mod identity; +pub(crate) mod macros; pub(crate) mod network; pub(crate) mod project; pub(crate) mod sync; pub(crate) mod token; +pub(crate) mod validation; -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub(crate) enum Mode { Global, Project(PathBuf), @@ -102,6 +105,9 @@ pub(crate) struct Context { /// Canister synchronizer pub(crate) syncer: Arc, + /// Operations initializers + pub(crate) ops: Initializers, + /// Whether debug is enabled pub(crate) debug: bool, } diff --git a/crates/icp-cli/src/commands/network/list.rs b/crates/icp-cli/src/commands/network/list.rs index 22117757..e8b5aea3 100644 --- a/crates/icp-cli/src/commands/network/list.rs +++ b/crates/icp-cli/src/commands/network/list.rs @@ -21,9 +21,9 @@ pub(crate) async fn exec(ctx: &Context, _: &ListArgs) -> Result<(), CommandError unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // List networks for (name, cfg) in &p.networks { diff --git a/crates/icp-cli/src/commands/network/ping.rs b/crates/icp-cli/src/commands/network/ping.rs index 73fe37f3..e03995c7 100644 --- a/crates/icp-cli/src/commands/network/ping.rs +++ b/crates/icp-cli/src/commands/network/ping.rs @@ -52,9 +52,13 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &PingArgs) -> Result<(), CommandError> { match &ctx.mode { - Mode::Global | Mode::Project(_) => { + Mode::Global => { + unimplemented!("global mode is not implemented"); + } + + Mode::Project(pdir) => { // Load Project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Identity let id = ctx.identity.load(IdentitySelection::Anonymous).await?; diff --git a/crates/icp-cli/src/commands/network/run.rs b/crates/icp-cli/src/commands/network/run.rs index 39b716db..dc3f0a71 100644 --- a/crates/icp-cli/src/commands/network/run.rs +++ b/crates/icp-cli/src/commands/network/run.rs @@ -72,7 +72,7 @@ pub(crate) async fn exec(ctx: &Context, args: &RunArgs) -> Result<(), CommandErr Mode::Project(pdir) => { // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Obtain network configuration let network = p.networks.get(&args.name).ok_or(CommandError::Network { diff --git a/crates/icp-cli/src/commands/network/stop.rs b/crates/icp-cli/src/commands/network/stop.rs index bbfbb42f..3d7fe48a 100644 --- a/crates/icp-cli/src/commands/network/stop.rs +++ b/crates/icp-cli/src/commands/network/stop.rs @@ -45,7 +45,7 @@ pub async fn exec(ctx: &Context, cmd: &Cmd) -> Result<(), CommandError> { Mode::Project(pdir) => { // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Check network exists p.networks.get(&cmd.name).ok_or(CommandError::Network { diff --git a/crates/icp-cli/src/commands/project/show.rs b/crates/icp-cli/src/commands/project/show.rs index 2644e4da..dfaaa48c 100644 --- a/crates/icp-cli/src/commands/project/show.rs +++ b/crates/icp-cli/src/commands/project/show.rs @@ -1,4 +1,3 @@ -use anyhow::Context as _; use clap::Args; use crate::commands::{Context, Mode}; @@ -8,6 +7,9 @@ pub(crate) struct ShowArgs; #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error(transparent)] + Project(#[from] icp::LoadError), + #[error(transparent)] Unexpected(#[from] anyhow::Error), } @@ -20,9 +22,9 @@ pub(crate) async fn exec(ctx: &Context, _: &ShowArgs) -> Result<(), CommandError unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { // Load the project manifest, which defines the canisters to be built. - let p = ctx.project.load().await.context("failed to load project")?; + let p = ctx.project.load(pdir).await?; let yaml = serde_yaml::to_string(&p).expect("Serializing to yaml failed"); println!("{yaml}"); diff --git a/crates/icp-cli/src/commands/sync/mod.rs b/crates/icp-cli/src/commands/sync/mod.rs index 86abb784..194036a0 100644 --- a/crates/icp-cli/src/commands/sync/mod.rs +++ b/crates/icp-cli/src/commands/sync/mod.rs @@ -10,8 +10,8 @@ use icp::{ }; use crate::{ - commands::{Context, Mode}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args}, + options::IdentityOpt, progress::{ProgressManager, ProgressManagerSettings}, store_id::{Key, LookupError}, }; @@ -24,8 +24,8 @@ pub(crate) struct SyncArgs { #[command(flatten)] pub(crate) identity: IdentityOpt, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] @@ -70,19 +70,21 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandEr unimplemented!("global mode is not implemented yet"); } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load the project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; diff --git a/crates/icp-cli/src/commands/token/balance.rs b/crates/icp-cli/src/commands/token/balance.rs index 420aeca0..8eb54950 100644 --- a/crates/icp-cli/src/commands/token/balance.rs +++ b/crates/icp-cli/src/commands/token/balance.rs @@ -6,8 +6,8 @@ use icp::{agent, identity, network}; use icrc_ledger_types::icrc1::account::Account; use crate::{ - commands::{Context, Mode, token::TOKEN_LEDGER_CIDS}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args, token::TOKEN_LEDGER_CIDS}, + options::IdentityOpt, }; #[derive(Args, Clone, Debug)] @@ -15,12 +15,18 @@ pub(crate) struct BalanceArgs { #[command(flatten)] pub(crate) identity: IdentityOpt, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -56,24 +62,43 @@ pub(crate) async fn exec( token: &str, args: &BalanceArgs, ) -> Result<(), CommandError> { - match &ctx.mode { + let (agent, owner) = match &ctx.mode { Mode::Global => { - unimplemented!("global mode is not implemented yet"); + // Argument (Network) + let Some(args::Network::Url(url)) = args.network.clone() else { + return Err(CommandError::Args); + }; + + // Load identity + let id = ctx.identity.load(args.identity.clone().into()).await?; + + // Convert identity to sender principal + let owner = id.sender().map_err(|err| CommandError::Principal { err })?; + + // Agent + let agent = ctx.agent.create(id, &url).await?; + + (agent, owner) } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; + // Convert identity to sender principal + let owner = id.sender().map_err(|err| CommandError::Principal { err })?; + // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -85,83 +110,80 @@ pub(crate) async fn exec( agent.set_root_key(k); } - // Obtain ledger address - let cid = match TOKEN_LEDGER_CIDS.get(token) { - // Given token matched known token names - Some(cid) => cid.to_string(), - - // Given token is not known, indicating it's either already a canister id - // or is simply a name of a token we do not know of - None => token.to_string(), - }; - - // Parse the canister id - let cid = Principal::from_text(cid).map_err(|err| CommandError::Principal { - err: err.to_string(), - })?; - - // Perform the required ledger calls - let (balance, decimals, symbol) = tokio::join!( - // - // Obtain token balance - async { - // Convert identity to sender principal - let owner = id.sender().map_err(|err| CommandError::Principal { err })?; - - // Specify sub-account - let subaccount = None; - - // Perform query - let resp = agent - .query(&cid, "icrc1_balance_of") - .with_arg( - Encode!(&Account { owner, subaccount }).expect("failed to encode arg"), - ) - .await?; - - // Decode response - Ok::<_, CommandError>(Decode!(&resp, Nat)?) - }, - // - // Obtain the number of decimals the token uses - async { - // Perform query - let resp = agent - .query(&cid, "icrc1_decimals") - .with_arg(Encode!(&()).expect("failed to encode arg")) - .await?; - - // Decode response - Ok::<_, CommandError>(Decode!(&resp, u8)?) - }, - // - // Obtain the symbol of the token - async { - // Perform query - let resp = agent - .query(&cid, "icrc1_symbol") - .with_arg(Encode!(&()).expect("failed to encode arg")) - .await?; - - // Decode response - Ok::<_, CommandError>(Decode!(&resp, String)?) - }, - ); - - // Check for errors - let (Nat(balance), decimals, symbol) = ( - balance?, // - decimals? as i64, // - symbol?, // - ); - - // Calculate amount - let amount = BigDecimal::from_biguint(balance, decimals); - - // Output information - let _ = ctx.term.write_line(&format!("Balance: {amount} {symbol}")); + (agent, owner) } - } + }; + + // Obtain ledger address + let cid = match TOKEN_LEDGER_CIDS.get(token) { + // Given token matched known token names + Some(cid) => cid.to_string(), + + // Given token is not known, indicating it's either already a canister id + // or is simply a name of a token we do not know of + None => token.to_string(), + }; + + // Parse the canister id + let cid = Principal::from_text(cid).map_err(|err| CommandError::Principal { + err: err.to_string(), + })?; + + // Perform the required ledger calls + let (balance, decimals, symbol) = tokio::join!( + // + // Obtain token balance + async { + // Specify sub-account + let subaccount = None; + + // Perform query + let resp = agent + .query(&cid, "icrc1_balance_of") + .with_arg(Encode!(&Account { owner, subaccount }).expect("failed to encode arg")) + .await?; + + // Decode response + Ok::<_, CommandError>(Decode!(&resp, Nat)?) + }, + // + // Obtain the number of decimals the token uses + async { + // Perform query + let resp = agent + .query(&cid, "icrc1_decimals") + .with_arg(Encode!(&()).expect("failed to encode arg")) + .await?; + + // Decode response + Ok::<_, CommandError>(Decode!(&resp, u8)?) + }, + // + // Obtain the symbol of the token + async { + // Perform query + let resp = agent + .query(&cid, "icrc1_symbol") + .with_arg(Encode!(&()).expect("failed to encode arg")) + .await?; + + // Decode response + Ok::<_, CommandError>(Decode!(&resp, String)?) + }, + ); + + // Check for errors + let (Nat(balance), decimals, symbol) = ( + balance?, // + decimals? as i64, // + symbol?, // + ); + + // Calculate amount + let amount = BigDecimal::from_biguint(balance, decimals); + + // Output information + let _ = ctx.term.write_line(&format!("Balance: {amount} {symbol}")); Ok(()) } diff --git a/crates/icp-cli/src/commands/token/transfer.rs b/crates/icp-cli/src/commands/token/transfer.rs index 32ad9ea8..d89a91da 100644 --- a/crates/icp-cli/src/commands/token/transfer.rs +++ b/crates/icp-cli/src/commands/token/transfer.rs @@ -9,8 +9,8 @@ use icrc_ledger_types::icrc1::{ }; use crate::{ - commands::{Context, Mode, token::TOKEN_LEDGER_CIDS}, - options::{EnvironmentOpt, IdentityOpt}, + commands::{Context, Mode, args, token::TOKEN_LEDGER_CIDS}, + options::IdentityOpt, }; #[derive(Debug, Args)] @@ -24,12 +24,18 @@ pub(crate) struct TransferArgs { #[command(flatten)] pub(crate) identity: IdentityOpt, - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + #[arg(long)] + pub(crate) network: Option, + + #[arg(long)] + pub(crate) environment: Option, } #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { + #[error("an invalid argument was provided")] + Args, + #[error(transparent)] Project(#[from] icp::LoadError), @@ -73,24 +79,37 @@ pub(crate) async fn exec( token: &str, args: &TransferArgs, ) -> Result<(), CommandError> { - match &ctx.mode { + let (agent,) = match &ctx.mode { Mode::Global => { - unimplemented!("global mode is not implemented yet"); + // Argument (Network) + let Some(args::Network::Url(url)) = args.network.clone() else { + return Err(CommandError::Args); + }; + + // Load identity + let id = ctx.identity.load(args.identity.clone().into()).await?; + + // Agent + let agent = ctx.agent.create(id, &url).await?; + + (agent,) } - Mode::Project(_) => { + Mode::Project(pdir) => { + // Argument (Environment) + let args::Environment::Name(env) = args.environment.clone().unwrap_or_default(); + // Load project - let p = ctx.project.load().await?; + let p = ctx.project.load(pdir).await?; // Load identity let id = ctx.identity.load(args.identity.clone().into()).await?; // Load target environment - let env = p.environments.get(args.environment.name()).ok_or( - CommandError::EnvironmentNotFound { - name: args.environment.name().to_owned(), - }, - )?; + let env = p + .environments + .get(&env) + .ok_or(CommandError::EnvironmentNotFound { name: env })?; // Access network let access = ctx.network.access(&env.network).await?; @@ -102,144 +121,146 @@ pub(crate) async fn exec( agent.set_root_key(k); } - // Obtain ledger address - let cid = match TOKEN_LEDGER_CIDS.get(token) { - // Given token matched known token names - Some(cid) => cid.to_string(), + (agent,) + } + }; + + // Obtain ledger address + let cid = match TOKEN_LEDGER_CIDS.get(token) { + // Given token matched known token names + Some(cid) => cid.to_string(), + + // Given token is not known, indicating it's either already a canister id + // or is simply a name of a token we do not know of + None => token.to_string(), + }; + + // Parse the canister id + let cid = Principal::from_text(cid).map_err(|err| CommandError::Principal { + err: err.to_string(), + })?; + + // Perform the required ledger calls + let (fee, decimals, symbol) = tokio::join!( + // + // Obtain token transfer fee + async { + // Perform query + let resp = agent + .query(&cid, "icrc1_fee") + .with_arg(Encode!(&()).expect("failed to encode arg")) + .await?; - // Given token is not known, indicating it's either already a canister id - // or is simply a name of a token we do not know of - None => token.to_string(), - }; + // Decode response + Ok::<_, CommandError>(Decode!(&resp, Nat)?) + }, + // + // Obtain the number of decimals the token uses + async { + // Perform query + let resp = agent + .query(&cid, "icrc1_decimals") + .with_arg(Encode!(&()).expect("failed to encode arg")) + .await?; - // Parse the canister id - let cid = Principal::from_text(cid).map_err(|err| CommandError::Principal { - err: err.to_string(), - })?; - - // Perform the required ledger calls - let (fee, decimals, symbol) = tokio::join!( - // - // Obtain token transfer fee - async { - // Perform query - let resp = agent - .query(&cid, "icrc1_fee") - .with_arg(Encode!(&()).expect("failed to encode arg")) - .await?; - - // Decode response - Ok::<_, CommandError>(Decode!(&resp, Nat)?) - }, - // - // Obtain the number of decimals the token uses - async { - // Perform query - let resp = agent - .query(&cid, "icrc1_decimals") - .with_arg(Encode!(&()).expect("failed to encode arg")) - .await?; - - // Decode response - Ok::<_, CommandError>(Decode!(&resp, u8)?) - }, - // - // Obtain the symbol of the token - async { - // Perform query - let resp = agent - .query(&cid, "icrc1_symbol") - .with_arg(Encode!(&()).expect("failed to encode arg")) - .await?; - - // Decode response - Ok::<_, CommandError>(Decode!(&resp, String)?) - }, - ); + // Decode response + Ok::<_, CommandError>(Decode!(&resp, u8)?) + }, + // + // Obtain the symbol of the token + async { + // Perform query + let resp = agent + .query(&cid, "icrc1_symbol") + .with_arg(Encode!(&()).expect("failed to encode arg")) + .await?; - // Check for errors - let (Nat(fee), decimals, symbol) = ( - fee?, // - decimals? as u32, // - symbol?, // + // Decode response + Ok::<_, CommandError>(Decode!(&resp, String)?) + }, + ); + + // Check for errors + let (Nat(fee), decimals, symbol) = ( + fee?, // + decimals? as u32, // + symbol?, // + ); + + // Calculate units of token to transfer + // Ledgers do not work in decimals and instead use the smallest non-divisible unit of the token + let ledger_amount = args.amount.clone() * 10u128.pow(decimals); + + // Convert amount to big decimal + let ledger_amount = ledger_amount + .to_bigint() + .ok_or(CommandError::Amount)? + .to_biguint() + .ok_or(CommandError::Amount)?; + + let ledger_amount = Nat::from(ledger_amount); + let display_amount = BigDecimal::from_biguint(ledger_amount.0.clone(), decimals as i64); + + // Prepare transfer + let receiver = Account { + owner: args.receiver, + subaccount: None, + }; + + let arg = TransferArg { + // Transfer amount + amount: ledger_amount.clone(), + + // Transfer destination + to: receiver, + + // Other + from_subaccount: None, + fee: None, + created_at_time: None, + memo: None, + }; + + // Perform transfer + let resp = agent + .update(&cid, "icrc1_transfer") + .with_arg(Encode!(&arg)?) + .call_and_wait() + .await?; + + // Parse response + let resp = Decode!(&resp, Result)?; + + // Process response + let idx = resp.map_err(|err| match err { + // Special case for insufficient funds + TransferError::InsufficientFunds { balance } => { + let balance = BigDecimal::from_biguint( + balance.0, // balance + decimals as i64, // decimals ); - // Calculate units of token to transfer - // Ledgers do not work in decimals and instead use the smallest non-divisible unit of the token - let ledger_amount = args.amount.clone() * 10u128.pow(decimals); - - // Convert amount to big decimal - let ledger_amount = ledger_amount - .to_bigint() - .ok_or(CommandError::Amount)? - .to_biguint() - .ok_or(CommandError::Amount)?; - - let ledger_amount = Nat::from(ledger_amount); - let display_amount = BigDecimal::from_biguint(ledger_amount.0.clone(), decimals as i64); - - // Prepare transfer - let receiver = Account { - owner: args.receiver, - subaccount: None, - }; - - let arg = TransferArg { - // Transfer amount - amount: ledger_amount.clone(), - - // Transfer destination - to: receiver, + let fee = BigDecimal::from_biguint( + fee, // fee + decimals as i64, // decimals + ); - // Other - from_subaccount: None, - fee: None, - created_at_time: None, - memo: None, - }; + CommandError::InsufficientFunds { + symbol: symbol.clone(), + balance, + required: args.amount.clone() + fee, + } + } - // Perform transfer - let resp = agent - .update(&cid, "icrc1_transfer") - .with_arg(Encode!(&arg)?) - .call_and_wait() - .await?; + _ => CommandError::Transfer { + err: err.to_string(), + }, + })?; - // Parse response - let resp = Decode!(&resp, Result)?; - - // Process response - let idx = resp.map_err(|err| match err { - // Special case for insufficient funds - TransferError::InsufficientFunds { balance } => { - let balance = BigDecimal::from_biguint( - balance.0, // balance - decimals as i64, // decimals - ); - - let fee = BigDecimal::from_biguint( - fee, // fee - decimals as i64, // decimals - ); - - CommandError::InsufficientFunds { - symbol: symbol.clone(), - balance, - required: args.amount.clone() + fee, - } - } - - _ => CommandError::Transfer { - err: err.to_string(), - }, - })?; - - // Output information - let _ = ctx.term.write_line(&format!( - "Transferred {display_amount} {symbol} to {receiver} in block {idx}" - )); - } - } + // Output information + let _ = ctx.term.write_line(&format!( + "Transferred {display_amount} {symbol} to {receiver} in block {idx}" + )); Ok(()) } diff --git a/crates/icp-cli/src/commands/validation.rs b/crates/icp-cli/src/commands/validation.rs new file mode 100644 index 00000000..e41d3039 --- /dev/null +++ b/crates/icp-cli/src/commands/validation.rs @@ -0,0 +1,245 @@ +use crate::commands::{ + Mode, + args::{Canister, Environment, Network}, +}; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum ValidateError { + #[error(transparent)] + Unexpected(#[from] anyhow::Error), +} + +pub(crate) trait Validate { + fn validate(&self, mode: &Mode) -> Result<(), ValidateError>; +} + +#[cfg(test)] +pub(crate) mod helpers { + use crate::commands::Mode; + + // #[cfg(test)] + // pub(crate) trait IntoOptions { + // fn into_options(self) -> Vec>; + // } + + // #[cfg(test)] + // impl IntoOptions for Vec { + // fn into_options(self) -> Vec> { + // self.into_iter().fold(vec![None], |mut acc, cur| { + // acc.push(Some(cur)); + // acc + // }) + // } + // } + + pub(crate) fn all_modes() -> Vec { + vec![Mode::Global, Mode::Project("dir".into())] + } + + // pub(crate) fn all_networks() -> Vec { + // vec![ + // args::Network::Name("my-network".to_string()), + // args::Network::Url("http::/www.example.com".to_string()), + // ] + // } +} + +pub(crate) fn a_canister_id_is_required_in_global_mode<'a>( + canister: impl Into<(&'a Canister,)>, + m: &Mode, +) -> Option<&'static str> { + let (canister,) = canister.into(); + (matches!(m, Mode::Global) && !matches!(canister, Canister::Principal(_))) + .then_some(PLEASE_PROVIDE_A_CANISTER_PRINCIPAL_IN_GLOBAL_MODE) +} + +const PLEASE_PROVIDE_A_CANISTER_PRINCIPAL_IN_GLOBAL_MODE: &str = r#" + Please provide a canister principal in global mode. +"#; + +pub(crate) fn network_or_environment_not_both<'a>( + network_environment: impl Into<(&'a Option, &'a Option)>, + m: &Mode, +) -> Option<&'static str> { + let (network, environment) = network_environment.into(); + (matches!(m, _) && network.is_some() && environment.is_some()) + .then_some(PLEASE_PROVIDE_EITHER_A_NETWORK_OR_AN_ENVIRONMENT_BUT_NOT_BOTH) +} + +const PLEASE_PROVIDE_EITHER_A_NETWORK_OR_AN_ENVIRONMENT_BUT_NOT_BOTH: &str = r#" + Please provide either a network or an environment, but not both. +"#; + +pub(crate) fn environments_are_not_available_in_a_global_mode<'a>( + environment: impl Into<(&'a Option,)>, + m: &Mode, +) -> Option<&'static str> { + let (environment,) = environment.into(); + (matches!(m, Mode::Global) && environment.is_some()) + .then_some(ENVIRONMENTS_ARE_NOT_AVAILABLE_IN_GLOBAL_MODE) +} + +const ENVIRONMENTS_ARE_NOT_AVAILABLE_IN_GLOBAL_MODE: &str = r#" + Environments are not available in global mode. +"#; + +pub(crate) fn a_network_url_is_required_in_global_mode<'a>( + network: impl Into<(&'a Option,)>, + m: &Mode, +) -> Option<&'static str> { + let (network,) = network.into(); + (matches!(m, Mode::Global) && !matches!(network, Some(Network::Url(_)))) + .then_some(A_NETWORK_URL_IS_REQUIRED_IN_GLOBAL_MODE) +} + +const A_NETWORK_URL_IS_REQUIRED_IN_GLOBAL_MODE: &str = r#" + A network `url` is required in global mode. +"#; + +#[cfg(test)] +mod test_a_canister_id_is_required_in_global_mode { + use crate::impl_from_args; + + use super::*; + + struct Args { + canister: Canister, + } + + impl_from_args!(Args, canister: Canister); + + #[test] + fn test() { + let out = a_canister_id_is_required_in_global_mode( + // + // Args + &Args { + canister: Canister::Name("my-canister".to_string()), + }, + // + // Mode + &Mode::Global, + ); + match out { + Some(msg) if msg == PLEASE_PROVIDE_A_CANISTER_PRINCIPAL_IN_GLOBAL_MODE => {} + _ => panic!("invalid validation output: {out:?}"), + } + } +} + +#[cfg(test)] +mod test_network_or_environment_not_both { + use crate::{ + commands::{args, validation}, + impl_from_args, + }; + + use super::*; + + struct Args { + network: Option, + environment: Option, + } + + impl_from_args!(Args, network: Option, environment: Option); + + #[test] + fn test() { + for (args, modes) in [ + ( + // + // Args + &Args { + network: Some(args::Network::Name("my-network".to_string())), + environment: Some(args::Environment::Name("my-environment".to_string())), + }, + // + // Modes + validation::helpers::all_modes(), + ), + ( + // + // Args + &Args { + network: Some(args::Network::Url("http://www.example.com".to_string())), + environment: Some(args::Environment::Name("my-environment".to_string())), + }, + // + // Modes + validation::helpers::all_modes(), + ), + ] { + for mode in &modes { + let out = network_or_environment_not_both(args, mode); + match out { + Some(msg) + if msg + == PLEASE_PROVIDE_EITHER_A_NETWORK_OR_AN_ENVIRONMENT_BUT_NOT_BOTH => {} + _ => panic!("invalid validation output: {out:?}"), + } + } + } + } +} + +#[cfg(test)] +mod test_environments_are_not_available_in_a_global_mode { + use crate::{commands::args, impl_from_args}; + + use super::*; + + struct Args { + environment: Option, + } + + impl_from_args!(Args, environment: Option); + + #[test] + fn test() { + let out = environments_are_not_available_in_a_global_mode( + // + // Args + &Args { + environment: Some(args::Environment::Name("my-environment".to_string())), + }, + // + // Mode + &Mode::Global, + ); + match out { + Some(msg) if msg == ENVIRONMENTS_ARE_NOT_AVAILABLE_IN_GLOBAL_MODE => {} + _ => panic!("invalid validation output: {out:?}"), + } + } +} + +#[cfg(test)] +mod test_a_network_url_is_required_in_global_mode { + use crate::{commands::args, impl_from_args}; + + use super::*; + + struct Args { + network: Option, + } + + impl_from_args!(Args, network: Option); + + #[test] + fn test() { + let out = a_network_url_is_required_in_global_mode( + // + // Args + &Args { + network: Some(args::Network::Name("my-network".to_string())), + }, + // + // Mode + &Mode::Global, + ); + match out { + Some(msg) if msg == A_NETWORK_URL_IS_REQUIRED_IN_GLOBAL_MODE => {} + _ => panic!("invalid validation output: {out:?}"), + } + } +} diff --git a/crates/icp-cli/src/main.rs b/crates/icp-cli/src/main.rs index 397b7061..8373ec56 100644 --- a/crates/icp-cli/src/main.rs +++ b/crates/icp-cli/src/main.rs @@ -1,6 +1,6 @@ use std::{env::current_dir, sync::Arc}; -use anyhow::Error; +use anyhow::{Context as _, Error}; use clap::{CommandFactory, Parser}; use commands::{Command, Context}; use console::Term; @@ -24,8 +24,9 @@ use tracing_subscriber::{ }; use crate::{ - commands::Mode, + commands::{Mode, validation::Validate}, logging::{TermWriter, debug_layer}, + operations::canister::{Starter, Stopper}, store_artifact::ArtifactStore, store_id::IdStore, telemetry::EventLayer, @@ -34,6 +35,7 @@ use crate::{ mod commands; mod logging; +mod operations; mod options; mod progress; mod store_artifact; @@ -194,11 +196,7 @@ async fn main() -> Result<(), Error> { }), }; - let pload = icp::Loader { - locate: mloc.clone(), - project: ploaders, - }; - + let pload = icp::Loader::new(ploaders); let pload = icp::Lazy::new(pload); let pload = Arc::new(pload); @@ -216,9 +214,17 @@ async fn main() -> Result<(), Error> { // Agent creator let agent_creator = Arc::new(agent::Creator); + // Operations + let ops = operations::Initializers { + canister: operations::canister::Initializers { + start: Box::new(Starter::arc), + stop: Box::new(Stopper::arc), + }, + }; + // Setup environment let ctx = Context { - mode, + mode: mode.clone(), term, dirs, ids, @@ -229,6 +235,7 @@ async fn main() -> Result<(), Error> { agent: agent_creator, builder, syncer, + ops, debug: cli.debug, }; @@ -313,6 +320,9 @@ async fn main() -> Result<(), Error> { } commands::canister::Command::Start(args) => { + args.validate(&mode) + .context("canister start: invalid command arguments")?; + commands::canister::start::exec(&ctx, &args) .instrument(trace_span) .await? diff --git a/crates/icp-cli/src/operations/canister/mod.rs b/crates/icp-cli/src/operations/canister/mod.rs new file mode 100644 index 00000000..96430693 --- /dev/null +++ b/crates/icp-cli/src/operations/canister/mod.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use ic_agent::Agent; + +mod start; +pub(crate) use start::*; + +mod stop; +pub(crate) use stop::*; + +#[allow(clippy::type_complexity)] +pub(crate) struct Initializers { + pub(crate) start: Box Arc>, + pub(crate) stop: Box Arc>, +} + +impl Default for Initializers { + fn default() -> Self { + Self { + start: Box::new(|_| unimplemented!()), + stop: Box::new(|_| unimplemented!()), + } + } +} diff --git a/crates/icp-cli/src/operations/canister/start.rs b/crates/icp-cli/src/operations/canister/start.rs new file mode 100644 index 00000000..e412d097 --- /dev/null +++ b/crates/icp-cli/src/operations/canister/start.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use anyhow::Context; +use async_trait::async_trait; +use candid::Principal; +use ic_agent::Agent; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum StartError { + #[error(transparent)] + Unexpected(#[from] anyhow::Error), +} + +#[async_trait] +pub(crate) trait Start: Sync + Send { + async fn start(&self, cid: &Principal) -> Result<(), StartError>; +} + +pub(crate) struct Starter { + agent: Agent, +} + +impl Starter { + pub(crate) fn arc(agent: &Agent) -> Arc { + Arc::new(Starter { + agent: agent.to_owned(), + }) + } +} + +#[async_trait] +impl Start for Starter { + async fn start(&self, cid: &Principal) -> Result<(), StartError> { + // Management Interface + let mgmt = ic_utils::interfaces::ManagementCanister::create(&self.agent); + + // Instruct management canister to start canister + mgmt.start_canister(cid) + .await + .context("failed to start canister")?; + + Ok(()) + } +} diff --git a/crates/icp-cli/src/operations/canister/stop.rs b/crates/icp-cli/src/operations/canister/stop.rs new file mode 100644 index 00000000..0decbb64 --- /dev/null +++ b/crates/icp-cli/src/operations/canister/stop.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use anyhow::Context; +use async_trait::async_trait; +use candid::Principal; +use ic_agent::Agent; + +#[derive(Debug, thiserror::Error)] +pub(crate) enum StopError { + #[error(transparent)] + Unexpected(#[from] anyhow::Error), +} + +#[async_trait] +pub(crate) trait Stop: Sync + Send { + async fn stop(&self, cid: &Principal) -> Result<(), StopError>; +} + +pub(crate) struct Stopper { + agent: Agent, +} + +impl Stopper { + pub(crate) fn arc(agent: &Agent) -> Arc { + Arc::new(Stopper { + agent: agent.to_owned(), + }) + } +} + +#[async_trait] +impl Stop for Stopper { + async fn stop(&self, cid: &Principal) -> Result<(), StopError> { + // Management Interface + let mgmt = ic_utils::interfaces::ManagementCanister::create(&self.agent); + + // Instruct management canister to stop canister + mgmt.stop_canister(cid) + .await + .context("failed to stop canister")?; + + Ok(()) + } +} diff --git a/crates/icp-cli/src/operations/mod.rs b/crates/icp-cli/src/operations/mod.rs new file mode 100644 index 00000000..ea26097c --- /dev/null +++ b/crates/icp-cli/src/operations/mod.rs @@ -0,0 +1,6 @@ +pub(crate) mod canister; + +#[derive(Default)] +pub(crate) struct Initializers { + pub(crate) canister: canister::Initializers, +} diff --git a/crates/icp-cli/src/options.rs b/crates/icp-cli/src/options.rs index 7140b0d4..f303d4d6 100644 --- a/crates/icp-cli/src/options.rs +++ b/crates/icp-cli/src/options.rs @@ -1,4 +1,4 @@ -use clap::{ArgGroup, Args}; +use clap::Args; use icp::identity::IdentitySelection; #[derive(Args, Clone, Debug, Default)] @@ -22,32 +22,3 @@ impl From for IdentitySelection { } } } - -#[derive(Args, Clone, Debug, Default)] -#[clap(group(ArgGroup::new("environment-select").multiple(false)))] -pub(crate) struct EnvironmentOpt { - /// Override the environment to connect to. By default, the local environment is used. - #[arg( - long, - env = "ICP_ENVIRONMENT", - global(true), - group = "environment-select" - )] - environment: Option, - - /// Shorthand for --environment=ic. - #[arg(long, global(true), group = "environment-select")] - ic: bool, -} - -impl EnvironmentOpt { - pub(crate) fn name(&self) -> &str { - // Support --ic - if self.ic { - return "ic"; - } - - // Otherwise, default to `local` - self.environment.as_deref().unwrap_or("local") - } -} diff --git a/crates/icp-cli/tests/network_ping_tests.rs b/crates/icp-cli/tests/network_ping_tests.rs index d6da7a8c..9c72a323 100644 --- a/crates/icp-cli/tests/network_ping_tests.rs +++ b/crates/icp-cli/tests/network_ping_tests.rs @@ -1,5 +1,5 @@ use icp::fs::write_string; -use predicates::str::{PredicateStrExt, contains}; +use predicates::str::contains; use serde_json::Value; mod common; @@ -74,14 +74,3 @@ fn ping_not_running() { "the local network for this project is not running", )); } - -#[test] -fn ping_not_in_project() { - let ctx = TestContext::new(); - - ctx.icp() - .args(["network", "ping"]) - .assert() - .failure() - .stderr(contains("Error: failed to locate project directory").trim()); -} diff --git a/crates/icp/src/identity/mod.rs b/crates/icp/src/identity/mod.rs index da026447..504c1e0f 100644 --- a/crates/icp/src/identity/mod.rs +++ b/crates/icp/src/identity/mod.rs @@ -82,6 +82,7 @@ pub trait Load: Sync + Send { } pub struct Loader { + /// Directory for identities pub dir: PathBuf, } diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index d7a8c715..acce6279 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -8,7 +8,7 @@ use tokio::sync::Mutex; use crate::{ canister::{Settings, build, sync}, - manifest::{Locate, PROJECT_MANIFEST, project::ProjectManifest}, + manifest::{PROJECT_MANIFEST, project::ProjectManifest}, network::Configuration, prelude::*, }; @@ -79,7 +79,7 @@ pub enum LoadError { #[async_trait] pub trait Load: Sync + Send { - async fn load(&self) -> Result; + async fn load(&self, dir: &Path) -> Result; } #[async_trait] @@ -98,21 +98,23 @@ pub struct ProjectLoaders { } pub struct Loader { - pub locate: Arc, - pub project: ProjectLoaders, + project: ProjectLoaders, +} + +impl Loader { + pub fn new(project: ProjectLoaders) -> Self { + Self { project } + } } #[async_trait] impl Load for Loader { - async fn load(&self) -> Result { - // Locate project root - let pdir = self.locate.locate().context(LoadError::Locate)?; - + async fn load(&self, dir: &Path) -> Result { // Load project manifest let m = self .project .path - .load(&pdir.join(PROJECT_MANIFEST)) + .load(&dir.join(PROJECT_MANIFEST)) .await .context(LoadError::Path)?; @@ -138,12 +140,12 @@ impl Lazy { #[async_trait] impl Load for Lazy { - async fn load(&self) -> Result { + async fn load(&self, dir: &Path) -> Result { if let Some(v) = self.1.lock().await.as_ref() { return Ok(v.to_owned()); } - let v = self.0.load().await?; + let v = self.0.load(dir).await?; let mut g = self.1.lock().await; if g.is_none() { diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 6f1cf348..ef054805 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -111,19 +111,19 @@ Perform canister operations against a network ## `icp-cli canister call` -**Usage:** `icp-cli canister call [OPTIONS] ` +**Usage:** `icp-cli canister call [OPTIONS] ` ###### **Arguments:** -* `` — Name of canister to call to +* `` * `` — Name of canister method to call into * `` — String representation of canister call arguments ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` @@ -138,8 +138,7 @@ Perform canister operations against a network ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--environment ` * `--controller ` — One or more controllers for the canister. Repeat `--controller` to specify multiple * `--compute-allocation ` — Optional compute allocation (0 to 100). Represents guaranteed compute capacity * `--memory-allocation ` — Optional memory allocation in bytes. If unset, memory is allocated dynamically @@ -155,33 +154,33 @@ Perform canister operations against a network ## `icp-cli canister delete` -**Usage:** `icp-cli canister delete [OPTIONS] ` +**Usage:** `icp-cli canister delete [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` ## `icp-cli canister info` -**Usage:** `icp-cli canister info [OPTIONS] ` +**Usage:** `icp-cli canister info [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` @@ -202,8 +201,7 @@ Perform canister operations against a network Possible values: `auto`, `install`, `reinstall`, `upgrade` * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--environment ` @@ -213,8 +211,7 @@ Perform canister operations against a network ###### **Options:** -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--environment ` @@ -231,33 +228,33 @@ Perform canister operations against a network ## `icp-cli canister settings show` -**Usage:** `icp-cli canister settings show [OPTIONS] ` +**Usage:** `icp-cli canister settings show [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` ## `icp-cli canister settings update` -**Usage:** `icp-cli canister settings update [OPTIONS] ` +**Usage:** `icp-cli canister settings update [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` * `--add-controller ` * `--remove-controller ` * `--set-controller ` @@ -278,81 +275,80 @@ Perform canister operations against a network ## `icp-cli canister show` -**Usage:** `icp-cli canister show [OPTIONS] ` +**Usage:** `icp-cli canister show [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--environment ` ## `icp-cli canister start` -**Usage:** `icp-cli canister start [OPTIONS] ` +**Usage:** `icp-cli canister start [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` ## `icp-cli canister status` -**Usage:** `icp-cli canister status [OPTIONS] ` +**Usage:** `icp-cli canister status [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` ## `icp-cli canister stop` -**Usage:** `icp-cli canister stop [OPTIONS] ` +**Usage:** `icp-cli canister stop [OPTIONS] ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` — The name of the canister within the current project ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` ## `icp-cli canister top-up` -**Usage:** `icp-cli canister top-up [OPTIONS] --amount ` +**Usage:** `icp-cli canister top-up [OPTIONS] --amount ` ###### **Arguments:** -* `` — The name of the canister within the current project +* `` ###### **Options:** * `--amount ` — Amount of cycles to top up * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` @@ -376,8 +372,8 @@ Mint and manage cycles ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` @@ -389,9 +385,9 @@ Mint and manage cycles * `--icp ` — Amount of ICP to mint to cycles * `--cycles ` — Amount of cycles to mint. Automatically determines the amount of ICP needed -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic * `--identity ` — The user identity to run this command as +* `--network ` +* `--environment ` @@ -419,8 +415,7 @@ Deploy a project to an environment Default value: `2000000000000` * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--environment ` @@ -602,8 +597,7 @@ Synchronize canisters in the current environment ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--environment ` @@ -633,8 +627,8 @@ Perform token transactions ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment ` @@ -650,8 +644,8 @@ Perform token transactions ###### **Options:** * `--identity ` — The user identity to run this command as -* `--environment ` — Override the environment to connect to. By default, the local environment is used -* `--ic` — Shorthand for --environment=ic +* `--network ` +* `--environment `