Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
93 changes: 93 additions & 0 deletions crates/icp-cli/src/commands/args.rs
Original file line number Diff line number Diff line change
@@ -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()),
);
}
}
7 changes: 5 additions & 2 deletions crates/icp-cli/src/commands/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },

Expand Down Expand Up @@ -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() {
Expand Down
27 changes: 16 additions & 11 deletions crates/icp-cli/src/commands/canister/binding_env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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<args::Network>,

#[arg(long)]
pub(crate) environment: Option<args::Environment>,
}

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -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?;
Expand Down
51 changes: 33 additions & 18 deletions crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<args::Network>,

#[arg(long)]
pub(crate) environment: Option<args::Environment>,
}

#[derive(Debug, thiserror::Error)]
pub(crate) enum CommandError {
#[error("an invalid argument was provided")]
Args,

#[error(transparent)]
Project(#[from] icp::LoadError),

Expand Down Expand Up @@ -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?;
Expand All @@ -99,18 +114,18 @@ 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(),
});
}

// Lookup the canister id
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
Expand All @@ -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);
Expand Down
24 changes: 13 additions & 11 deletions crates/icp-cli/src/commands/canister/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -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<args::Environment>,

/// One or more controllers for the canister. Repeat `--controller` to specify multiple.
#[arg(long)]
Expand Down Expand Up @@ -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() {
Expand Down
Loading