diff --git a/crates/icp-cli/src/commands/args.rs b/crates/icp-cli/src/commands/args.rs index 8719489a8..aaa497583 100644 --- a/crates/icp-cli/src/commands/args.rs +++ b/crates/icp-cli/src/commands/args.rs @@ -68,6 +68,41 @@ impl CanisterCommandArgs { } } +// Common argument used for Token and Cycles commands +#[derive(Args, Clone, Debug)] +pub(crate) struct TokenCommandArgs { + #[command(flatten)] + pub(crate) network: NetworkOpt, + + #[command(flatten)] + pub(crate) environment: EnvironmentOpt, + + #[command(flatten)] + pub(crate) identity: IdentityOpt, +} + +/// Selections derived from TokenCommandArgs +pub(crate) struct TokenCommandSelections { + pub(crate) environment: EnvironmentSelection, + pub(crate) network: NetworkSelection, + pub(crate) identity: IdentitySelection, +} + +impl TokenCommandArgs { + /// Convert command arguments into selection enums + pub(crate) fn selections(&self) -> TokenCommandSelections { + let environment_selection: EnvironmentSelection = self.environment.clone().into(); + let network_selection: NetworkSelection = self.network.clone().into(); + let identity_selection: IdentitySelection = self.identity.clone().into(); + + TokenCommandSelections { + environment: environment_selection, + network: network_selection, + identity: identity_selection, + } + } +} + #[derive(Clone, Debug, PartialEq)] pub(crate) enum Canister { Name(String), diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index 4db5f4f96..5d6e838b6 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -4,7 +4,7 @@ use candid::IDLArgs; use clap::Args; use dialoguer::console::Term; -use icp::context::Context; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::commands::args; @@ -35,18 +35,27 @@ pub(crate) enum CommandError { Call(#[from] ic_agent::AgentError), #[error(transparent)] - GetCanisterIdAndAgent(#[from] icp::context::GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/create.rs b/crates/icp-cli/src/commands/canister/create.rs index 976621a96..b7ee59483 100644 --- a/crates/icp-cli/src/commands/canister/create.rs +++ b/crates/icp-cli/src/commands/canister/create.rs @@ -154,7 +154,10 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), Command let (_, canister_info) = env.get_canister_info(&canister).map_err(|e| anyhow!(e))?; if ctx - .get_canister_id_for_env(&canister, &selections.environment) + .get_canister_id_for_env( + &icp::context::CanisterSelection::Named(canister.clone()), + &selections.environment, + ) .await .is_ok() { diff --git a/crates/icp-cli/src/commands/canister/delete.rs b/crates/icp-cli/src/commands/canister/delete.rs index 364ad17ac..9d3f4ea38 100644 --- a/crates/icp-cli/src/commands/canister/delete.rs +++ b/crates/icp-cli/src/commands/canister/delete.rs @@ -1,7 +1,7 @@ use clap::Args; use ic_agent::AgentError; -use icp::context::Context; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::commands::args; @@ -17,18 +17,27 @@ pub(crate) enum CommandError { Delete(#[from] AgentError), #[error(transparent)] - GetCanisterIdAndAgent(#[from] icp::context::GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/info.rs b/crates/icp-cli/src/commands/canister/info.rs index 478575073..eca60feba 100644 --- a/crates/icp-cli/src/commands/canister/info.rs +++ b/crates/icp-cli/src/commands/canister/info.rs @@ -1,10 +1,10 @@ use clap::Args; use ic_agent::AgentError; use ic_utils::interfaces::management_canister::CanisterStatusResult; -use icp::{agent, context::GetCanisterIdAndAgentError, identity, network}; +use icp::{agent, identity, network}; use itertools::Itertools; -use icp::context::Context; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::commands::args; use icp::store_id::LookupIdError; @@ -36,17 +36,26 @@ pub(crate) enum CommandError { Status(#[from] AgentError), #[error(transparent)] - GetCanisterIdAndAgent(#[from] GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &InfoArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index 49a4b3931..7d5685028 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -4,7 +4,7 @@ use ic_utils::interfaces::ManagementCanister; use icp::fs; use icp::{context::CanisterSelection, prelude::*}; -use icp::context::Context; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::{ commands::args, @@ -34,7 +34,10 @@ pub(crate) enum CommandError { ReadWasmFile(#[from] fs::Error), #[error(transparent)] - GetCanisterIdAndAgent(#[from] icp::context::GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), #[error(transparent)] Unexpected(#[from] anyhow::Error), @@ -62,12 +65,18 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), Comman .map_err(|e| anyhow!(e))? }; - let (canister_id, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let canister_id = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/settings/show.rs b/crates/icp-cli/src/commands/canister/settings/show.rs index bbeb8c8ee..73425a24c 100644 --- a/crates/icp-cli/src/commands/canister/settings/show.rs +++ b/crates/icp-cli/src/commands/canister/settings/show.rs @@ -3,7 +3,7 @@ use ic_agent::{AgentError, export::Principal}; use ic_management_canister_types::{CanisterStatusResult, LogVisibility}; use icp::{agent, identity, network}; -use icp::context::{Context, GetAgentForEnvError, GetCanisterIdAndAgentError}; +use icp::context::{Context, GetAgentError, GetAgentForEnvError, GetCanisterIdError}; use crate::commands::args; use icp::store_id::LookupIdError; @@ -38,17 +38,26 @@ pub(crate) enum CommandError { GetAgentForEnv(#[from] GetAgentForEnvError), #[error(transparent)] - GetCanisterIdAndAgent(#[from] GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/settings/sync.rs b/crates/icp-cli/src/commands/canister/settings/sync.rs index 4e1a79742..dd738c230 100644 --- a/crates/icp-cli/src/commands/canister/settings/sync.rs +++ b/crates/icp-cli/src/commands/canister/settings/sync.rs @@ -5,7 +5,7 @@ use crate::{ use clap::Args; use ic_utils::interfaces::ManagementCanister; use icp::context::{ - CanisterSelection, Context, GetCanisterIdAndAgentError, GetEnvCanisterError, + CanisterSelection, Context, GetAgentError, GetCanisterIdError, GetEnvCanisterError, GetEnvironmentError, }; use snafu::Snafu; @@ -19,7 +19,10 @@ pub(crate) struct SyncArgs { #[derive(Debug, Snafu)] pub(crate) enum CommandError { #[snafu(transparent)] - GetIdAndAgent { source: GetCanisterIdAndAgentError }, + GetAgent { source: GetAgentError }, + + #[snafu(transparent)] + GetCanisterId { source: GetCanisterIdError }, #[snafu(transparent)] GetEnvironment { source: GetEnvironmentError }, @@ -44,12 +47,18 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandEr .get_canister_and_path_for_env(name, &selections.environment) .await?; - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index fa7555cce..26b75cd37 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -8,7 +8,7 @@ use ic_agent::{AgentError, export::Principal}; use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; use icp::{LoadError, agent, identity, network}; -use icp::context::{CanisterSelection, Context, GetCanisterIdAndAgentError}; +use icp::context::{CanisterSelection, Context, GetAgentError, GetCanisterIdError}; use snafu::{ResultExt, Snafu}; use crate::commands::args; @@ -131,7 +131,10 @@ pub(crate) enum CommandError { Update { source: AgentError }, #[snafu(transparent)] - GetCanisterIdAndAgent { source: GetCanisterIdAndAgentError }, + GetAgent { source: GetAgentError }, + + #[snafu(transparent)] + GetCanisterId { source: GetCanisterIdError }, #[snafu(display("failed to write to terminal"))] WriteTerm { source: std::io::Error }, @@ -139,12 +142,18 @@ pub(crate) enum CommandError { pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/show.rs b/crates/icp-cli/src/commands/canister/show.rs index a8205e7c6..cb5840ac8 100644 --- a/crates/icp-cli/src/commands/canister/show.rs +++ b/crates/icp-cli/src/commands/canister/show.rs @@ -13,14 +13,14 @@ pub(crate) struct ShowArgs { #[derive(Debug, thiserror::Error)] pub(crate) enum CommandError { #[error(transparent)] - GetCanisterId(#[from] icp::context::GetCanisterIdError), + GetCanisterId(#[from] icp::context::GetCanisterIdForEnvError), } pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandError> { let (canister_selection, environment_selection) = args.cmd_args.selections(); let cid = ctx - .get_canister_id(&canister_selection, &environment_selection) + .get_canister_id_for_env(&canister_selection, &environment_selection) .await?; println!("{cid} => {}", args.cmd_args.canister); diff --git a/crates/icp-cli/src/commands/canister/start.rs b/crates/icp-cli/src/commands/canister/start.rs index e4c48f12f..d6e0176a1 100644 --- a/crates/icp-cli/src/commands/canister/start.rs +++ b/crates/icp-cli/src/commands/canister/start.rs @@ -2,7 +2,7 @@ use clap::Args; use ic_agent::AgentError; use icp::{agent, identity, network}; -use icp::context::{Context, GetCanisterIdAndAgentError}; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::commands::args; use icp::store_id::LookupIdError; @@ -34,17 +34,26 @@ pub(crate) enum CommandError { Start(#[from] AgentError), #[error(transparent)] - GetCanisterIdAndAgent(#[from] GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/status.rs b/crates/icp-cli/src/commands/canister/status.rs index aca3f5e6c..556ea9f00 100644 --- a/crates/icp-cli/src/commands/canister/status.rs +++ b/crates/icp-cli/src/commands/canister/status.rs @@ -2,7 +2,7 @@ use clap::Args; use ic_agent::{AgentError, export::Principal}; use ic_management_canister_types::{CanisterStatusResult, LogVisibility}; -use icp::context::Context; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::commands::args; @@ -18,18 +18,27 @@ pub(crate) enum CommandError { Status(#[from] AgentError), #[error(transparent)] - GetCanisterIdAndAgent(#[from] icp::context::GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/stop.rs b/crates/icp-cli/src/commands/canister/stop.rs index 7ea40b570..9a69119f5 100644 --- a/crates/icp-cli/src/commands/canister/stop.rs +++ b/crates/icp-cli/src/commands/canister/stop.rs @@ -2,7 +2,7 @@ use clap::Args; use ic_agent::AgentError; use icp::{agent, identity, network}; -use icp::context::{Context, GetCanisterIdAndAgentError}; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::commands::args; use icp::store_id::LookupIdError; @@ -34,17 +34,26 @@ pub(crate) enum CommandError { Stop(#[from] AgentError), #[error(transparent)] - GetCanisterIdAndAgent(#[from] GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/top_up.rs b/crates/icp-cli/src/commands/canister/top_up.rs index 8e4c042f9..36b74bbf8 100644 --- a/crates/icp-cli/src/commands/canister/top_up.rs +++ b/crates/icp-cli/src/commands/canister/top_up.rs @@ -2,12 +2,12 @@ use bigdecimal::BigDecimal; use candid::{Decode, Encode, Nat}; use clap::Args; use ic_agent::AgentError; -use icp::{agent, context::GetCanisterIdAndAgentError, identity, network}; +use icp::{agent, identity, network}; use icp_canister_interfaces::cycles_ledger::{ CYCLES_LEDGER_DECIMALS, CYCLES_LEDGER_PRINCIPAL, WithdrawArgs, WithdrawError, WithdrawResponse, }; -use icp::context::Context; +use icp::context::{Context, GetAgentError, GetCanisterIdError}; use crate::commands::args; use icp::store_id::LookupIdError; @@ -49,17 +49,26 @@ pub(crate) enum CommandError { Withdraw { err: WithdrawError, amount: u128 }, #[error(transparent)] - GetCanisterIdAndAgent(#[from] GetCanisterIdAndAgentError), + GetAgent(#[from] GetAgentError), + + #[error(transparent)] + GetCanisterId(#[from] GetCanisterIdError), } pub(crate) async fn exec(ctx: &Context, args: &TopUpArgs) -> Result<(), CommandError> { let selections = args.cmd_args.selections(); - let (cid, agent) = ctx - .get_canister_id_and_agent( - &selections.canister, + let agent = ctx + .get_agent( + &selections.identity, + &selections.network, &selections.environment, + ) + .await?; + let cid = ctx + .get_canister_id( + &selections.canister, &selections.network, - &selections.identity, + &selections.environment, ) .await?; diff --git a/crates/icp-cli/src/commands/cycles/mint.rs b/crates/icp-cli/src/commands/cycles/mint.rs index bbbcb2255..577eb0277 100644 --- a/crates/icp-cli/src/commands/cycles/mint.rs +++ b/crates/icp-cli/src/commands/cycles/mint.rs @@ -5,11 +5,7 @@ use ic_agent::AgentError; use ic_ledger_types::{ AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, TransferError, TransferResult, }; -use icp::{ - agent, - context::{EnvironmentSelection, GetAgentForEnvError}, - identity, network, -}; +use icp::{agent, context::GetAgentError, identity, network}; use icp_canister_interfaces::{ cycles_ledger::CYCLES_LEDGER_BLOCK_FEE, cycles_minting_canister::{ @@ -21,7 +17,7 @@ use icp_canister_interfaces::{ use icp::context::Context; -use crate::options::{EnvironmentOpt, IdentityOpt}; +use crate::commands::args::TokenCommandArgs; #[derive(Debug, Args)] pub(crate) struct MintArgs { @@ -34,10 +30,7 @@ pub(crate) struct MintArgs { pub(crate) cycles: Option, #[command(flatten)] - pub(crate) environment: EnvironmentOpt, - - #[command(flatten)] - pub(crate) identity: IdentityOpt, + pub(crate) token_command_args: TokenCommandArgs, } #[derive(Debug, thiserror::Error)] @@ -82,15 +75,19 @@ pub(crate) enum CommandError { NotifyMintError { src: NotifyMintErr }, #[error(transparent)] - GetAgentForEnv(#[from] GetAgentForEnvError), + GetAgent(#[from] GetAgentError), } pub(crate) async fn exec(ctx: &Context, args: &MintArgs) -> Result<(), CommandError> { - let environment_selection: EnvironmentSelection = args.environment.clone().into(); + let selections = args.token_command_args.selections(); // Agent let agent = ctx - .get_agent_for_env(&args.identity.clone().into(), &environment_selection) + .get_agent( + &selections.identity, + &selections.network, + &selections.environment, + ) .await?; // Prepare deposit diff --git a/crates/icp-cli/src/commands/deploy/mod.rs b/crates/icp-cli/src/commands/deploy/mod.rs index 9f962fd61..25bf99cdf 100644 --- a/crates/icp-cli/src/commands/deploy/mod.rs +++ b/crates/icp-cli/src/commands/deploy/mod.rs @@ -3,7 +3,7 @@ use clap::Args; use futures::{StreamExt, future::try_join_all, stream::FuturesOrdered}; use ic_agent::export::Principal; use icp::{ - context::{Context, EnvironmentSelection, GetEnvCanisterError}, + context::{CanisterSelection, Context, EnvironmentSelection, GetEnvCanisterError}, identity::IdentitySelection, }; use std::sync::Arc; @@ -207,7 +207,10 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), Command let environment_selection = environment_selection.clone(); async move { let cid = ctx - .get_canister_id_for_env(name, &environment_selection) + .get_canister_id_for_env( + &CanisterSelection::Named(name.clone()), + &environment_selection, + ) .await .map_err(|e| anyhow!(e))?; let (_, info) = env_canisters @@ -244,7 +247,10 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), Command let environment_selection = environment_selection.clone(); async move { let cid = ctx - .get_canister_id_for_env(name, &environment_selection) + .get_canister_id_for_env( + &CanisterSelection::Named(name.clone()), + &environment_selection, + ) .await .map_err(|e| anyhow!(e))?; Ok::<_, anyhow::Error>((name.clone(), cid)) @@ -275,7 +281,10 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), Command let environment_selection = environment_selection.clone(); async move { let cid = ctx - .get_canister_id_for_env(name, &environment_selection) + .get_canister_id_for_env( + &CanisterSelection::Named(name.clone()), + &environment_selection, + ) .await .map_err(|e| anyhow!(e))?; let (canister_path, info) = env_canisters diff --git a/crates/icp-cli/src/commands/sync.rs b/crates/icp-cli/src/commands/sync.rs index a84c5e7ef..de57d3445 100644 --- a/crates/icp-cli/src/commands/sync.rs +++ b/crates/icp-cli/src/commands/sync.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use clap::Args; use futures::future::try_join_all; -use icp::context::{Context, EnvironmentSelection}; +use icp::context::{CanisterSelection, Context, EnvironmentSelection}; use icp::identity::IdentitySelection; use std::sync::Arc; @@ -77,7 +77,10 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandEr .get_canister_and_path_for_env(name, &environment_selection) .await?; let cid = ctx - .get_canister_id_for_env(name, &environment_selection) + .get_canister_id_for_env( + &CanisterSelection::Named(name.clone()), + &environment_selection, + ) .await?; Ok::<_, anyhow::Error>((cid, canister_path.clone(), info.clone())) })) diff --git a/crates/icp-cli/src/commands/token/balance.rs b/crates/icp-cli/src/commands/token/balance.rs index 2f2a03740..4705b1355 100644 --- a/crates/icp-cli/src/commands/token/balance.rs +++ b/crates/icp-cli/src/commands/token/balance.rs @@ -2,27 +2,17 @@ use bigdecimal::BigDecimal; use candid::{Decode, Encode, Nat, Principal}; use clap::Args; use ic_agent::AgentError; -use icp::{ - agent, - context::{EnvironmentSelection, GetAgentForEnvError}, - identity, network, -}; +use icp::{agent, context::GetAgentError, identity, network}; use icrc_ledger_types::icrc1::account::Account; use icp::context::Context; -use crate::{ - commands::token::TOKEN_LEDGER_CIDS, - options::{EnvironmentOpt, IdentityOpt}, -}; +use crate::commands::{args::TokenCommandArgs, token::TOKEN_LEDGER_CIDS}; #[derive(Args, Clone, Debug)] pub(crate) struct BalanceArgs { #[command(flatten)] - pub(crate) identity: IdentityOpt, - - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + pub(crate) token_command_args: TokenCommandArgs, } #[derive(Debug, thiserror::Error)] @@ -49,7 +39,7 @@ pub(crate) enum CommandError { Candid(#[from] candid::Error), #[error(transparent)] - GetAgentForEnv(#[from] GetAgentForEnvError), + GetAgent(#[from] GetAgentError), } /// Check the token balance of a given identity @@ -62,11 +52,15 @@ pub(crate) async fn exec( token: &str, args: &BalanceArgs, ) -> Result<(), CommandError> { - let environment_selection: EnvironmentSelection = args.environment.clone().into(); + let selections = args.token_command_args.selections(); // Agent let agent = ctx - .get_agent_for_env(&args.identity.clone().into(), &environment_selection) + .get_agent( + &selections.identity, + &selections.network, + &selections.environment, + ) .await?; // Obtain ledger address diff --git a/crates/icp-cli/src/commands/token/transfer.rs b/crates/icp-cli/src/commands/token/transfer.rs index 1d11e3db4..0956a78d0 100644 --- a/crates/icp-cli/src/commands/token/transfer.rs +++ b/crates/icp-cli/src/commands/token/transfer.rs @@ -2,11 +2,7 @@ use bigdecimal::{BigDecimal, num_bigint::ToBigInt}; use candid::{Decode, Encode, Nat, Principal}; use clap::Args; use ic_agent::AgentError; -use icp::{ - agent, - context::{EnvironmentSelection, GetAgentForEnvError}, - identity, network, -}; +use icp::{agent, context::GetAgentError, identity, network}; use icrc_ledger_types::icrc1::{ account::Account, transfer::{TransferArg, TransferError}, @@ -14,10 +10,7 @@ use icrc_ledger_types::icrc1::{ use icp::context::Context; -use crate::{ - commands::token::TOKEN_LEDGER_CIDS, - options::{EnvironmentOpt, IdentityOpt}, -}; +use crate::commands::{args::TokenCommandArgs, token::TOKEN_LEDGER_CIDS}; #[derive(Debug, Args)] pub(crate) struct TransferArgs { @@ -28,10 +21,7 @@ pub(crate) struct TransferArgs { pub(crate) receiver: Principal, #[command(flatten)] - pub(crate) identity: IdentityOpt, - - #[command(flatten)] - pub(crate) environment: EnvironmentOpt, + pub(crate) token_command_args: TokenCommandArgs, } #[derive(Debug, thiserror::Error)] @@ -71,7 +61,7 @@ pub(crate) enum CommandError { }, #[error(transparent)] - GetAgentForEnv(#[from] GetAgentForEnvError), + GetAgent(#[from] GetAgentError), } pub(crate) async fn exec( @@ -79,11 +69,15 @@ pub(crate) async fn exec( token: &str, args: &TransferArgs, ) -> Result<(), CommandError> { - let environment_selection: EnvironmentSelection = args.environment.clone().into(); + let selections = args.token_command_args.selections(); // Agent let agent = ctx - .get_agent_for_env(&args.identity.clone().into(), &environment_selection) + .get_agent( + &selections.identity, + &selections.network, + &selections.environment, + ) .await?; // Obtain ledger address diff --git a/crates/icp-cli/src/options.rs b/crates/icp-cli/src/options.rs index 6a9f1a56a..e4298f130 100644 --- a/crates/icp-cli/src/options.rs +++ b/crates/icp-cli/src/options.rs @@ -1,7 +1,9 @@ use clap::{ArgGroup, Args}; use icp::context::{EnvironmentSelection, NetworkSelection}; use icp::identity::IdentitySelection; -use icp::project::{DEFAULT_LOCAL_ENVIRONMENT_NAME, DEFAULT_MAINNET_ENVIRONMENT_NAME}; +use icp::project::{ + DEFAULT_LOCAL_ENVIRONMENT_NAME, DEFAULT_MAINNET_ENVIRONMENT_NAME, DEFAULT_MAINNET_NETWORK_NAME, +}; use url::Url; #[derive(Args, Clone, Debug, Default)] @@ -79,12 +81,19 @@ impl From for EnvironmentSelection { #[clap(group(ArgGroup::new("network-select").multiple(false)))] pub(crate) struct NetworkOpt { /// Name of the network to target, conflicts with environment argument - #[arg(long, group = "network-select")] + #[arg(long, env = "ICP_NETWORK", group = "network-select")] network: Option, + + /// Shorthand for --network=mainnet + #[arg(long, group = "network-select")] + mainnet: bool, } impl From for NetworkSelection { fn from(v: NetworkOpt) -> Self { + if v.mainnet { + return NetworkSelection::Named(DEFAULT_MAINNET_NETWORK_NAME.to_string()); + } match v.network { Some(network) => match Url::parse(&network) { Ok(url) => NetworkSelection::Url(url), diff --git a/crates/icp-cli/tests/cycles_tests.rs b/crates/icp-cli/tests/cycles_tests.rs index 6a3b83447..e1f5027da 100644 --- a/crates/icp-cli/tests/cycles_tests.rs +++ b/crates/icp-cli/tests/cycles_tests.rs @@ -2,7 +2,7 @@ use indoc::formatdoc; use predicates::str::contains; use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients}; -use icp::fs::write_string; +use icp::{fs::write_string, prelude::IC_MAINNET_NETWORK_URL}; mod common; @@ -100,3 +100,79 @@ async fn cycles_balance() { )) .success(); } + +#[tokio::test] +async fn cycles_mint_with_explicit_network() { + let ctx = TestContext::new(); + + // Setup project + let project_dir = ctx.create_project_dir("icp"); + + // Project manifest with network definition + let pm = formatdoc! {r#" + {NETWORK_RANDOM_PORT} + "#}; + write_string( + &project_dir.join("icp.yaml"), // path + &pm, // contents + ) + .expect("failed to write project manifest"); + + // Start network + let _g = ctx.start_network_in(&project_dir, "my-network"); + ctx.ping_until_healthy(&project_dir, "my-network"); + + // Create identity and mint ICP + let identity = clients::icp(&ctx, &project_dir, None).use_new_random_identity(); + clients::ledger(&ctx) + .mint_icp(identity, None, 123456789_u64) + .await; + + // Run mint command with explicit --network flag + ctx.icp() + .current_dir(&project_dir) + .args(["cycles", "mint", "--icp", "1", "--network", "my-network"]) + .assert() + .stdout(contains( + "Minted 3.519900000000 TCYCLES to your account, new balance: 3.519900000000 TCYCLES.", + )) + .success(); +} + +#[tokio::test] +async fn cycles_mint_with_mainnet() { + let ctx = TestContext::new(); + + // Setup project + let project_dir = ctx.create_project_dir("icp"); + + // Create identity + clients::icp(&ctx, &project_dir, None).use_new_random_identity(); + + // Run mint command with explicit --mainnet flag + ctx.icp() + .current_dir(&project_dir) + .args(["cycles", "mint", "--icp", "1", "--mainnet"]) + .assert() + .stderr(contains( + "Error: Insufficient funds: 1.00010000 ICP required, 0 ICP available.", + )) + .failure(); + + // Run mint command with --network + ctx.icp() + .current_dir(&project_dir) + .args([ + "cycles", + "mint", + "--icp", + "1", + "--network", + IC_MAINNET_NETWORK_URL, + ]) + .assert() + .stderr(contains( + "Error: Insufficient funds: 1.00010000 ICP required, 0 ICP available.", + )) + .failure(); +} diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index b7a83b492..13886b39f 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -10,7 +10,9 @@ use crate::{ identity::IdentitySelection, network::{Configuration as NetworkConfiguration, access::NetworkAccess}, prelude::*, - project::DEFAULT_LOCAL_ENVIRONMENT_NAME, + project::{ + DEFAULT_LOCAL_ENVIRONMENT_NAME, DEFAULT_MAINNET_NETWORK_NAME, DEFAULT_MAINNET_NETWORK_URL, + }, store_id::{IdMapping, LookupIdError}, }; use candid::Principal; @@ -143,14 +145,38 @@ impl Context { ) -> Result { match network_selection { NetworkSelection::Named(network_name) => { - let p = self.project.load().await?; - let net = p.networks.get(network_name).context(NetworkNotFoundSnafu { - name: network_name.to_owned(), - })?; - Ok(net.clone()) + if self.project.exists().await? { + let p = self.project.load().await?; + let net = p.networks.get(network_name).context(NetworkNotFoundSnafu { + name: network_name.to_owned(), + })?; + Ok(net.clone()) + } else if network_name == DEFAULT_MAINNET_NETWORK_NAME { + Ok(crate::Network { + name: DEFAULT_MAINNET_NETWORK_NAME.to_string(), + configuration: crate::network::Configuration::Connected { + connected: crate::network::Connected { + url: DEFAULT_MAINNET_NETWORK_URL.to_string(), + root_key: None, + }, + }, + }) + } else { + Err(GetNetworkError::NetworkNotFound { + name: network_name.to_owned(), + }) + } } NetworkSelection::Default => Err(GetNetworkError::DefaultNetwork), - NetworkSelection::Url(_) => Err(GetNetworkError::UrlSpecifiedNetwork), + NetworkSelection::Url(url) => Ok(crate::Network { + name: url.to_string(), + configuration: crate::network::Configuration::Connected { + connected: crate::network::Connected { + url: url.to_string(), + root_key: None, + }, + }, + }), } } @@ -176,39 +202,47 @@ impl Context { Ok((path.clone(), canister.clone())) } - /// Gets the canister ID for a given canister name in a specified environment. + /// Gets the canister ID for a given canister selection in a specified environment. /// /// # Errors /// /// Returns an error if the environment cannot be loaded or if the canister ID cannot be found. pub async fn get_canister_id_for_env( &self, - canister_name: &str, + canister: &CanisterSelection, environment: &EnvironmentSelection, ) -> Result { - let env = self.get_environment(environment).await?; - let is_cache = match env.network.configuration { - NetworkConfiguration::Managed { .. } => true, - NetworkConfiguration::Connected { .. } => false, + let principal = match canister { + CanisterSelection::Named(canister_name) => { + let env = self.get_environment(environment).await?; + let is_cache = match env.network.configuration { + NetworkConfiguration::Managed { .. } => true, + NetworkConfiguration::Connected { .. } => false, + }; + + if !env.canisters.contains_key(canister_name) { + return Err(GetCanisterIdForEnvError::CanisterNotFoundInEnv { + canister_name: canister_name.to_owned(), + environment_name: environment.name().to_owned(), + }); + } + + // Lookup the canister id + self.ids + .lookup(is_cache, &env.name, canister_name) + .context(CanisterIdLookupSnafu { + canister_name: canister_name.to_owned(), + environment_name: environment.name().to_owned(), + })? + } + CanisterSelection::Principal(principal) => { + // Make sure a valid environment was requested + let _ = self.get_environment(environment).await?; + *principal + } }; - if !env.canisters.contains_key(canister_name) { - return Err(GetCanisterIdForEnvError::CanisterNotFoundInEnv { - canister_name: canister_name.to_owned(), - environment_name: environment.name().to_owned(), - }); - } - - // Lookup the canister id - let cid = self - .ids - .lookup(is_cache, &env.name, canister_name) - .context(CanisterIdLookupSnafu { - canister_name: canister_name.to_owned(), - environment_name: environment.name().to_owned(), - })?; - - Ok(cid) + Ok(principal) } /// Sets the canister ID for a given canister name in a specified environment. @@ -296,97 +330,76 @@ impl Context { Ok(agent) } - /// Gets a canister ID for a given canister and environment selection. - /// - /// This method validates that the environment exists when using a principal, - /// or looks up the canister ID when using a name. - pub async fn get_canister_id( + pub async fn get_agent( &self, - canister: &CanisterSelection, - environment: &EnvironmentSelection, - ) -> Result { - let principal = match canister { - CanisterSelection::Named(canister_name) => { - self.get_canister_id_for_env(canister_name, environment) - .await? - } - CanisterSelection::Principal(principal) => { - // Make sure a valid environment was requested - let _ = self.get_environment(environment).await?; - *principal - } - }; - - Ok(principal) - } - - /// Gets a canister ID and agent for the given selections. - /// - /// This is the main entry point for commands that need to interact with a canister. - /// It handles all the different combinations of canister, environment, and network selections. - pub async fn get_canister_id_and_agent( - &self, - canister: &CanisterSelection, - environment: &EnvironmentSelection, - network: &NetworkSelection, identity: &IdentitySelection, - ) -> Result<(Principal, Agent), GetCanisterIdAndAgentError> { - let (cid, agent) = match (canister, environment, network) { + network: &NetworkSelection, + environment: &EnvironmentSelection, + ) -> Result { + match (environment, network) { // Error: Both environment and network specified - (_, EnvironmentSelection::Named(_), NetworkSelection::Named(_)) - | (_, EnvironmentSelection::Named(_), NetworkSelection::Url(_)) => { - return Err(GetCanisterIdAndAgentError::EnvironmentAndNetworkSpecified); + (EnvironmentSelection::Named(_), NetworkSelection::Named(_)) + | (EnvironmentSelection::Named(_), NetworkSelection::Url(_)) => { + Err(GetAgentError::EnvironmentAndNetworkSpecified) } - // Error: Canister by name with default environment and explicit network - ( - CanisterSelection::Named(_), - EnvironmentSelection::Default, - NetworkSelection::Named(_), - ) - | ( - CanisterSelection::Named(_), - EnvironmentSelection::Default, - NetworkSelection::Url(_), - ) => { - return Err(GetCanisterIdAndAgentError::AmbiguousCanisterName); + // Default environment + default network + (EnvironmentSelection::Default, NetworkSelection::Default) => { + // Try to get agent from the default environment if project exists + match self.get_agent_for_env(identity, environment).await { + Ok(agent) => Ok(agent), + Err(GetAgentForEnvError::GetEnvironment { + source: + GetEnvironmentError::ProjectLoad { + source: crate::LoadError::Locate, + }, + }) => Err(GetAgentError::NoProjectOrNetwork), + Err(e) => Err(e.into()), + } } - // Canister by name, use environment - (CanisterSelection::Named(cname), _, NetworkSelection::Default) => { - let agent = self.get_agent_for_env(identity, environment).await?; - let cid = self.get_canister_id_for_env(cname, environment).await?; - (cid, agent) + // Environment specified + (EnvironmentSelection::Named(_), NetworkSelection::Default) => { + Ok(self.get_agent_for_env(identity, environment).await?) } - // Canister by principal, use environment - (CanisterSelection::Principal(principal), _, NetworkSelection::Default) => { - let agent = self.get_agent_for_env(identity, environment).await?; - (*principal, agent) - } - - // Canister by principal, use named network (environment must be default) - ( - CanisterSelection::Principal(principal), - EnvironmentSelection::Default, - NetworkSelection::Named(_), - ) => { - let agent = self.get_agent_for_network(identity, network).await?; - (*principal, agent) + // Network specified + (EnvironmentSelection::Default, NetworkSelection::Named(_)) + | (EnvironmentSelection::Default, NetworkSelection::Url(_)) => { + Ok(self.get_agent_for_network(identity, network).await?) } + } + } - // Canister by principal, use URL network (environment must be default) - ( - CanisterSelection::Principal(principal), - EnvironmentSelection::Default, - NetworkSelection::Url(url), - ) => { - let agent = self.get_agent_for_url(identity, url).await?; - (*principal, agent) + pub async fn get_canister_id( + &self, + canister: &CanisterSelection, + network: &NetworkSelection, + environment: &EnvironmentSelection, + ) -> Result { + match canister { + CanisterSelection::Principal(principal) => Ok(*principal), + CanisterSelection::Named(_) => { + match (environment, network) { + // Error: Both environment and network specified + (EnvironmentSelection::Named(_), NetworkSelection::Named(_)) + | (EnvironmentSelection::Named(_), NetworkSelection::Url(_)) => { + Err(GetCanisterIdError::CanisterEnvironmentAndNetworkSpecified) + } + + // Error: Canister by name with explicit network but no environment + (EnvironmentSelection::Default, NetworkSelection::Named(_)) + | (EnvironmentSelection::Default, NetworkSelection::Url(_)) => { + Err(GetCanisterIdError::AmbiguousCanisterName) + } + + // Only environment specified + (_, NetworkSelection::Default) => { + Ok(self.get_canister_id_for_env(canister, environment).await?) + } + } } - }; - - Ok((cid, agent)) + } } pub async fn ids_by_environment( @@ -558,26 +571,17 @@ pub enum GetAgentForUrlError { } #[derive(Debug, Snafu)] -pub enum GetCanisterIdError { - #[snafu(transparent)] - GetCanisterIdForEnv { source: GetCanisterIdForEnvError }, - +pub enum GetAgentError { #[snafu(transparent)] - GetEnvironment { source: GetEnvironmentError }, -} + ProjectExists { source: crate::LoadError }, -#[derive(Debug, Snafu)] -pub enum GetCanisterIdAndAgentError { #[snafu(display("You can't specify both an environment and a network"))] EnvironmentAndNetworkSpecified, #[snafu(display( - "Specifying a network is not supported if you are targeting a canister by name, specify an environment instead" + "No project found and no network specified. Either run this command inside a project or specify a network with --network" ))] - AmbiguousCanisterName, - - #[snafu(transparent)] - GetCanisterIdForEnv { source: GetCanisterIdForEnvError }, + NoProjectOrNetwork, #[snafu(transparent)] GetAgentForEnv { source: GetAgentForEnvError }, @@ -589,6 +593,20 @@ pub enum GetCanisterIdAndAgentError { GetAgentForUrl { source: GetAgentForUrlError }, } +#[derive(Debug, Snafu)] +pub enum GetCanisterIdError { + #[snafu(display("You can't specify both an environment and a network"))] + CanisterEnvironmentAndNetworkSpecified, + + #[snafu(display( + "Specifying a network is not supported if you are targeting a canister by name, specify an environment instead" + ))] + AmbiguousCanisterName, + + #[snafu(transparent)] + GetCanisterIdForEnv { source: GetCanisterIdForEnvError }, +} + #[derive(Debug, Snafu)] pub enum GetIdsByEnvironmentError { #[snafu(transparent)] diff --git a/crates/icp/src/context/tests.rs b/crates/icp/src/context/tests.rs index 3ab3e0511..7a76a280e 100644 --- a/crates/icp/src/context/tests.rs +++ b/crates/icp/src/context/tests.rs @@ -1,7 +1,13 @@ use super::*; -use crate::store_id::Access; -use crate::store_id::mock::MockInMemoryIdStore; -use crate::{MockProjectLoader, identity::MockIdentityLoader, network::MockNetworkAccessor}; +use crate::{ + Environment, MockProjectLoader, Network, Project, + identity::MockIdentityLoader, + network::{Configuration, Gateway, Managed, MockNetworkAccessor, Port, access::NetworkAccess}, + project::{DEFAULT_LOCAL_NETWORK_NAME, DEFAULT_LOCAL_NETWORK_URL}, + store_id::{Access as IdAccess, mock::MockInMemoryIdStore}, +}; +use candid::Principal; +use std::collections::HashMap; #[tokio::test] async fn test_get_identity_default() { @@ -93,11 +99,13 @@ async fn test_get_network_success() { }; let network = ctx - .get_network(&NetworkSelection::Named("local".to_string())) + .get_network(&NetworkSelection::Named( + DEFAULT_LOCAL_NETWORK_NAME.to_string(), + )) .await .unwrap(); - assert_eq!(network.name, "local"); + assert_eq!(network.name, DEFAULT_LOCAL_NETWORK_NAME); } #[tokio::test] @@ -116,9 +124,6 @@ async fn test_get_network_not_found() { #[tokio::test] async fn test_get_canister_id_for_env_success() { - use crate::store_id::Access as IdAccess; - use candid::Principal; - let ids_store = Arc::new(MockInMemoryIdStore::new()); // Register a canister ID for the dev environment @@ -134,7 +139,10 @@ async fn test_get_canister_id_for_env_success() { }; let cid = ctx - .get_canister_id_for_env("backend", &EnvironmentSelection::Named("dev".to_string())) + .get_canister_id_for_env( + &CanisterSelection::Named("backend".to_string()), + &EnvironmentSelection::Named("dev".to_string()), + ) .await .unwrap(); @@ -150,7 +158,10 @@ async fn test_get_canister_id_for_env_canister_not_in_env() { // "database" is only in "dev" environment, not in "test" let result = ctx - .get_canister_id_for_env("database", &EnvironmentSelection::Named("test".to_string())) + .get_canister_id_for_env( + &CanisterSelection::Named("database".to_string()), + &EnvironmentSelection::Named("test".to_string()), + ) .await; assert!(matches!( @@ -171,7 +182,10 @@ async fn test_get_canister_id_for_env_id_not_registered() { // Environment exists and canister is in it, but ID not registered let result = ctx - .get_canister_id_for_env("backend", &EnvironmentSelection::Named("dev".to_string())) + .get_canister_id_for_env( + &CanisterSelection::Named("backend".to_string()), + &EnvironmentSelection::Named("dev".to_string()), + ) .await; assert!(matches!( @@ -186,9 +200,6 @@ async fn test_get_canister_id_for_env_id_not_registered() { #[tokio::test] async fn test_set_canister_id_for_env_success() { - use crate::store_id::Access as IdAccess; - use candid::Principal; - let ids_store = Arc::new(MockInMemoryIdStore::new()); let ctx = Context { @@ -216,8 +227,6 @@ async fn test_set_canister_id_for_env_success() { #[tokio::test] async fn test_set_canister_id_for_env_canister_not_in_env() { - use candid::Principal; - let ctx = Context { project: Arc::new(MockProjectLoader::complex()), ..Context::mocked() @@ -245,9 +254,6 @@ async fn test_set_canister_id_for_env_canister_not_in_env() { #[tokio::test] async fn test_set_canister_id_for_env_already_registered() { - use crate::store_id::Access as IdAccess; - use candid::Principal; - let ids_store = Arc::new(MockInMemoryIdStore::new()); // Pre-register a canister ID @@ -280,8 +286,6 @@ async fn test_set_canister_id_for_env_already_registered() { #[tokio::test] async fn test_get_agent_for_env_uses_environment_network() { - use crate::network::access::NetworkAccess; - let staging_root_key = vec![1, 2, 3]; // Complex project has "test" environment which uses "staging" network @@ -366,8 +370,6 @@ async fn test_get_agent_for_env_network_not_configured() { #[tokio::test] async fn test_get_agent_for_network_success() { - use crate::network::access::NetworkAccess; - let root_key = vec![1, 2, 3]; let ctx = Context { @@ -444,7 +446,7 @@ async fn test_get_agent_for_url_success() { let result = ctx .get_agent_for_url( &IdentitySelection::Anonymous, - &Url::parse("http://localhost:8000").unwrap(), + &Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), ) .await; @@ -452,10 +454,7 @@ async fn test_get_agent_for_url_success() { } #[tokio::test] -async fn test_get_canister_id() { - use crate::store_id::Access as IdAccess; - use candid::Principal; - +async fn test_get_canister_id_for_env() { let ids_store = Arc::new(MockInMemoryIdStore::new()); // Register a canister ID for the dev environment @@ -474,14 +473,14 @@ async fn test_get_canister_id() { let environment_selection = EnvironmentSelection::Named("dev".to_string()); assert!( - matches!(ctx.get_canister_id(&canister_selection, &environment_selection).await, Ok(id) if id == canister_id) + matches!(ctx.get_canister_id_for_env(&canister_selection, &environment_selection).await, Ok(id) if id == canister_id) ); let canister_selection = CanisterSelection::Named("INVALID".to_string()); let environment_selection = EnvironmentSelection::Named("dev".to_string()); let res = ctx - .get_canister_id(&canister_selection, &environment_selection) + .get_canister_id_for_env(&canister_selection, &environment_selection) .await; assert!( res.is_err(), @@ -518,3 +517,327 @@ async fn test_ids_by_environment() { assert_eq!(result.get("backend"), Some(&backend_id)); assert_eq!(result.get("frontend"), Some(&frontend_id)); } + +#[tokio::test] +async fn test_get_agent_defaults_outside_project() { + let ctx = Context { + project: Arc::new(crate::NoProjectLoader), + ..Context::mocked() + }; + + // Default environment + default network outside project should error + let error = ctx + .get_agent( + &IdentitySelection::Anonymous, + &NetworkSelection::Default, + &EnvironmentSelection::Default, + ) + .await + .unwrap_err(); + + // Should fail with NoProjectOrNetwork error + assert!(matches!(error, GetAgentError::NoProjectOrNetwork)); +} + +#[tokio::test] +async fn test_get_agent_defaults_inside_project_with_default_local() { + let local_root_key = vec![1, 1, 1]; + + // Create a project with a "local" environment (the default environment name) + let local_network = Network { + name: DEFAULT_LOCAL_NETWORK_NAME.to_string(), + configuration: Configuration::Managed { + managed: Managed { + gateway: Gateway { + host: "localhost".to_string(), + port: Port::Fixed(8000), + }, + }, + }, + }; + + let mut networks = HashMap::new(); + networks.insert( + DEFAULT_LOCAL_NETWORK_NAME.to_string(), + local_network.clone(), + ); + + let local_env = Environment { + name: DEFAULT_LOCAL_NETWORK_NAME.to_string(), + network: local_network, + canisters: HashMap::new(), // No canisters needed for get_agent test + }; + + let mut environments = HashMap::new(); + environments.insert(DEFAULT_LOCAL_NETWORK_NAME.to_string(), local_env); + + let project = Project { + dir: "/project".into(), + canisters: HashMap::new(), // No canisters needed for get_agent test + networks, + environments, + }; + + let ctx = Context { + project: Arc::new(crate::MockProjectLoader::new(project)), + network: Arc::new(MockNetworkAccessor::new().with_network( + DEFAULT_LOCAL_NETWORK_NAME, + NetworkAccess { + default_effective_canister_id: None, + root_key: Some(local_root_key.clone()), + url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), + }, + )), + ..Context::mocked() + }; + + let agent = ctx + .get_agent( + &IdentitySelection::Anonymous, + &NetworkSelection::Default, + &EnvironmentSelection::Default, + ) + .await + .unwrap(); + + // Should successfully create agent using project's default environment + assert_eq!(agent.read_root_key(), local_root_key); +} + +#[tokio::test] +async fn test_get_agent_defaults_with_overridden_local_network() { + // Create a project where "local" network is overridden to use port 9000 + let custom_local_network = Network { + name: DEFAULT_LOCAL_NETWORK_NAME.to_string(), + configuration: Configuration::Managed { + managed: Managed { + gateway: Gateway { + host: "localhost".to_string(), + port: Port::Fixed(9000), + }, + }, + }, + }; + + let mut networks = HashMap::new(); + networks.insert( + DEFAULT_LOCAL_NETWORK_NAME.to_string(), + custom_local_network.clone(), + ); + + let local_env = Environment { + name: DEFAULT_LOCAL_NETWORK_NAME.to_string(), + network: custom_local_network, + canisters: HashMap::new(), // No canisters needed for get_agent test + }; + + let mut environments = HashMap::new(); + environments.insert(DEFAULT_LOCAL_NETWORK_NAME.to_string(), local_env); + + let project = Project { + dir: "/project".into(), + canisters: HashMap::new(), // No canisters needed for get_agent test + networks, + environments, + }; + + let custom_root_key = vec![1, 2, 3, 4]; + + let ctx = Context { + project: Arc::new(crate::MockProjectLoader::new(project)), + network: Arc::new(MockNetworkAccessor::new().with_network( + DEFAULT_LOCAL_NETWORK_NAME, + NetworkAccess { + default_effective_canister_id: None, + root_key: Some(custom_root_key.clone()), + url: Url::parse("http://localhost:9000").unwrap(), // Custom port + }, + )), + ..Context::mocked() + }; + + let agent = ctx + .get_agent( + &IdentitySelection::Anonymous, + &NetworkSelection::Default, + &EnvironmentSelection::Default, + ) + .await + .unwrap(); + + // Should use the custom network configuration + assert_eq!(agent.read_root_key(), custom_root_key); +} + +#[tokio::test] +async fn test_get_agent_defaults_with_overridden_local_environment() { + // Create project where "local" environment uses a custom network + let default_local_network = Network { + name: DEFAULT_LOCAL_NETWORK_NAME.to_string(), + configuration: Configuration::Managed { + managed: Managed { + gateway: Gateway { + host: "localhost".to_string(), + port: Port::Fixed(8000), + }, + }, + }, + }; + + let custom_network = Network { + name: "custom".to_string(), + configuration: Configuration::Managed { + managed: Managed { + gateway: Gateway { + host: "localhost".to_string(), + port: Port::Fixed(7000), + }, + }, + }, + }; + + let mut networks = HashMap::new(); + networks.insert( + DEFAULT_LOCAL_NETWORK_NAME.to_string(), + default_local_network, + ); + networks.insert("custom".to_string(), custom_network.clone()); + + // "local" environment uses "custom" network + let local_env = Environment { + name: DEFAULT_LOCAL_NETWORK_NAME.to_string(), + network: custom_network, + canisters: HashMap::new(), // No canisters needed for get_agent test + }; + + let mut environments = HashMap::new(); + environments.insert(DEFAULT_LOCAL_NETWORK_NAME.to_string(), local_env); + + let project = Project { + dir: "/project".into(), + canisters: HashMap::new(), // No canisters needed for get_agent test + networks, + environments, + }; + + let custom_root_key = vec![5, 6, 7, 8]; + + let ctx = Context { + project: Arc::new(crate::MockProjectLoader::new(project)), + network: Arc::new( + MockNetworkAccessor::new() + .with_network( + DEFAULT_LOCAL_NETWORK_NAME, + NetworkAccess { + default_effective_canister_id: None, + root_key: None, + url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), + }, + ) + .with_network( + "custom", + NetworkAccess { + default_effective_canister_id: None, + root_key: Some(custom_root_key.clone()), + url: Url::parse("http://localhost:7000").unwrap(), + }, + ), + ), + ..Context::mocked() + }; + + let agent = ctx + .get_agent( + &IdentitySelection::Anonymous, + &NetworkSelection::Default, + &EnvironmentSelection::Default, + ) + .await + .unwrap(); + + // Should use the custom network from the overridden environment + assert_eq!(agent.read_root_key(), custom_root_key); +} + +#[tokio::test] +async fn test_get_agent_explicit_network_inside_project() { + let staging_root_key = vec![12, 13, 14]; + + let ctx = Context { + project: Arc::new(MockProjectLoader::complex()), + network: Arc::new( + MockNetworkAccessor::new() + .with_network( + DEFAULT_LOCAL_NETWORK_NAME, + NetworkAccess { + default_effective_canister_id: None, + root_key: None, + url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), + }, + ) + .with_network( + "staging", + NetworkAccess { + default_effective_canister_id: None, + root_key: Some(staging_root_key.clone()), + url: Url::parse("http://localhost:8001").unwrap(), + }, + ), + ), + ..Context::mocked() + }; + + let agent = ctx + .get_agent( + &IdentitySelection::Anonymous, + &NetworkSelection::Named("staging".to_string()), + &EnvironmentSelection::Default, + ) + .await + .unwrap(); + + // Should use the explicitly specified network, regardless of project + assert_eq!(agent.read_root_key(), staging_root_key); +} + +#[tokio::test] +async fn test_get_agent_explicit_environment_inside_project() { + let staging_root_key = vec![15, 16, 17]; + + // complex() has "test" environment using "staging" network + let ctx = Context { + project: Arc::new(MockProjectLoader::complex()), + network: Arc::new( + MockNetworkAccessor::new() + .with_network( + DEFAULT_LOCAL_NETWORK_NAME, + NetworkAccess { + default_effective_canister_id: None, + root_key: None, + url: Url::parse(DEFAULT_LOCAL_NETWORK_URL).unwrap(), + }, + ) + .with_network( + "staging", + NetworkAccess { + default_effective_canister_id: None, + root_key: Some(staging_root_key.clone()), + url: Url::parse("http://localhost:8001").unwrap(), + }, + ), + ), + ..Context::mocked() + }; + + let agent = ctx + .get_agent( + &IdentitySelection::Anonymous, + &NetworkSelection::Default, + &EnvironmentSelection::Named("test".to_string()), + ) + .await + .unwrap(); + + // Should use the network from the "test" environment (which is "staging") + assert_eq!(agent.read_root_key(), staging_root_key); +} diff --git a/crates/icp/src/lib.rs b/crates/icp/src/lib.rs index 5163058cd..73337215c 100644 --- a/crates/icp/src/lib.rs +++ b/crates/icp/src/lib.rs @@ -8,7 +8,9 @@ use tracing::debug; use crate::{ canister::{Settings, build, sync}, - manifest::{PROJECT_MANIFEST, ProjectRootLocate, project::ProjectManifest}, + manifest::{ + PROJECT_MANIFEST, ProjectRootLocate, ProjectRootLocateError, project::ProjectManifest, + }, network::Configuration, prelude::*, }; @@ -116,6 +118,7 @@ pub enum LoadError { #[async_trait] pub trait Load: Sync + Send { async fn load(&self) -> Result; + async fn exists(&self) -> Result; } #[async_trait] @@ -172,6 +175,14 @@ impl Load for Loader { Ok(p) } + + async fn exists(&self) -> Result { + match self.project_root_locate.locate() { + Ok(_) => Ok(true), + Err(ProjectRootLocateError::NotFound(_)) => Ok(false), + Err(ProjectRootLocateError::Unexpected(e)) => Err(LoadError::Unexpected(e)), + } + } } pub struct Lazy(T, Arc>>); @@ -198,6 +209,15 @@ impl Load for Lazy { Ok(v) } + + async fn exists(&self) -> Result { + if self.1.lock().await.as_ref().is_some() { + return Ok(true); + } + + let v = self.0.exists().await?; + Ok(v) + } } #[cfg(test)] @@ -480,4 +500,25 @@ impl Load for MockProjectLoader { async fn load(&self) -> Result { Ok(self.project.clone()) } + + async fn exists(&self) -> Result { + Ok(true) + } +} + +#[cfg(test)] +/// Mock project loader that always fails with a Locate error. +/// Useful for testing scenarios where no project exists. +pub struct NoProjectLoader; + +#[cfg(test)] +#[async_trait] +impl Load for NoProjectLoader { + async fn load(&self) -> Result { + Err(LoadError::Locate) + } + + async fn exists(&self) -> Result { + Ok(false) + } } diff --git a/crates/icp/src/network/access.rs b/crates/icp/src/network/access.rs index 4a72bee1b..9b519ebe7 100644 --- a/crates/icp/src/network/access.rs +++ b/crates/icp/src/network/access.rs @@ -3,9 +3,8 @@ use snafu::{OptionExt, ResultExt, Snafu}; use url::Url; use crate::{ - Network, network::{ - Configuration, NetworkDirectory, access::GetNetworkAccessError::DecodeRootKey, + Connected, NetworkDirectory, access::GetNetworkAccessError::DecodeRootKey, directory::LoadNetworkFileError, }, prelude::*, @@ -74,75 +73,66 @@ pub enum GetNetworkAccessError { }, } -pub async fn get_network_access( +pub async fn get_managed_network_access( nd: NetworkDirectory, - network: &Network, ) -> Result { - let access = match &network.configuration { - // - // Managed - Configuration::Managed { managed: _ } => { - // Load network descriptor - let desc = nd - .load_network_descriptor() - .await - .context(LoadNetworkDescriptorSnafu)? - .ok_or(GetNetworkAccessError::NetworkNotRunning { - network: nd.network_name.to_owned(), - })?; - - // Specify port - let port = desc.gateway.port; - - // Apply gateway configuration - if desc.gateway.fixed { - let pdesc = nd - .load_port_descriptor(port) - .await - .context(LoadPortDescriptorSnafu { port })? - .context(NoPortDescriptorSnafu { port })?; - - if desc.id != pdesc.id { - return Err(GetNetworkAccessError::NetworkRunningOtherProject { - network: pdesc.network, - port: pdesc.gateway.port, - project_dir: pdesc.project_dir, - }); - } - } - - // Specify effective canister ID - let default_effective_canister_id = Some(desc.default_effective_canister_id); - - // Specify root-key - let root_key = hex::decode(desc.root_key).map_err(|source| DecodeRootKey { source })?; - - NetworkAccess { - default_effective_canister_id, - root_key: Some(root_key), - url: Url::parse(&format!("http://localhost:{port}")).unwrap(), - } + // Load network descriptor + let desc = nd + .load_network_descriptor() + .await + .context(LoadNetworkDescriptorSnafu)? + .ok_or(GetNetworkAccessError::NetworkNotRunning { + network: nd.network_name.to_owned(), + })?; + + // Specify port + let port = desc.gateway.port; + + // Apply gateway configuration + if desc.gateway.fixed { + let pdesc = nd + .load_port_descriptor(port) + .await + .context(LoadPortDescriptorSnafu { port })? + .context(NoPortDescriptorSnafu { port })?; + + if desc.id != pdesc.id { + return Err(GetNetworkAccessError::NetworkRunningOtherProject { + network: pdesc.network, + port: pdesc.gateway.port, + project_dir: pdesc.project_dir, + }); } + } - // - // Connected - Configuration::Connected { connected: cfg } => { - let root_key = cfg - .root_key - .as_ref() - .map(hex::decode) - .transpose() - .map_err(|err| DecodeRootKey { source: err })?; - - NetworkAccess { - default_effective_canister_id: None, - root_key, - url: Url::parse(&cfg.url).context(ParseUrlSnafu { - url: cfg.url.clone(), - })?, - } - } - }; + // Specify effective canister ID + let default_effective_canister_id = Some(desc.default_effective_canister_id); + + // Specify root-key + let root_key = hex::decode(desc.root_key).map_err(|source| DecodeRootKey { source })?; - Ok(access) + Ok(NetworkAccess { + default_effective_canister_id, + root_key: Some(root_key), + url: Url::parse(&format!("http://localhost:{port}")).unwrap(), + }) +} + +pub async fn get_connected_network_access( + connected: &Connected, +) -> Result { + let root_key = connected + .root_key + .as_ref() + .map(hex::decode) + .transpose() + .map_err(|err| DecodeRootKey { source: err })?; + + Ok(NetworkAccess { + default_effective_canister_id: None, + root_key, + url: Url::parse(&connected.url).context(ParseUrlSnafu { + url: connected.url.clone(), + })?, + }) } diff --git a/crates/icp/src/network/mod.rs b/crates/icp/src/network/mod.rs index 0317fb70f..29fd5d515 100644 --- a/crates/icp/src/network/mod.rs +++ b/crates/icp/src/network/mod.rs @@ -14,7 +14,7 @@ use crate::{ ProjectRootLocate, ProjectRootLocateError, network::{Connected as ManifestConnected, Gateway as ManifestGateway, Mode}, }, - network::access::{NetworkAccess, get_network_access}, + network::access::{NetworkAccess, get_connected_network_access, get_managed_network_access}, prelude::*, }; @@ -186,15 +186,17 @@ impl Access for Accessor { )) } async fn access(&self, network: &Network) -> Result { - // NetworkDirectory - let nd = self.get_network_directory(network)?; - - // NetworkAccess - let access = get_network_access(nd, network) - .await - .context("failed to load network access")?; - - Ok(access) + match &network.configuration { + Configuration::Managed { managed: _ } => { + let nd = self.get_network_directory(network)?; + Ok(get_managed_network_access(nd) + .await + .context("failed to load managed network access")?) + } + Configuration::Connected { connected: cfg } => Ok(get_connected_network_access(cfg) + .await + .context("failed to load connected network access")?), + } } } diff --git a/crates/icp/src/project.rs b/crates/icp/src/project.rs index 460e59871..d746005b8 100644 --- a/crates/icp/src/project.rs +++ b/crates/icp/src/project.rs @@ -25,6 +25,10 @@ pub const DEFAULT_LOCAL_ENVIRONMENT_NAME: &str = "local"; pub const DEFAULT_MAINNET_ENVIRONMENT_NAME: &str = "ic"; pub const DEFAULT_LOCAL_NETWORK_NAME: &str = "local"; pub const DEFAULT_MAINNET_NETWORK_NAME: &str = "mainnet"; +pub const DEFAULT_LOCAL_NETWORK_HOST: &str = "localhost"; +pub const DEFAULT_LOCAL_NETWORK_PORT: u16 = 8000; +pub const DEFAULT_LOCAL_NETWORK_URL: &str = "http://localhost:8000"; +pub const DEFAULT_MAINNET_NETWORK_URL: &str = IC_MAINNET_NETWORK_URL; #[derive(Debug, thiserror::Error)] pub enum LoadPathError { @@ -121,8 +125,8 @@ fn default_networks() -> Vec { configuration: Configuration::Managed { managed: Managed { gateway: Gateway { - host: "localhost".to_string(), - port: Port::Fixed(8000), + host: DEFAULT_LOCAL_NETWORK_HOST.to_string(), + port: Port::Fixed(DEFAULT_LOCAL_NETWORK_PORT), }, }, }, diff --git a/docs/cli-reference.md b/docs/cli-reference.md index cddd77de2..71f234a82 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -124,6 +124,7 @@ Make a canister call ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -143,6 +144,7 @@ Create a canister on a network ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -172,6 +174,7 @@ Delete a canister from a network ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -191,6 +194,7 @@ Display a canister's information ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -217,6 +221,7 @@ Install a built WASM to a canister on a network * `--wasm ` — Path to the WASM file to install. Uses the build output if not explicitly provided * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -263,6 +268,7 @@ Display a canister's settings ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -282,6 +288,7 @@ Change a canister's settings to specified values ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -316,6 +323,7 @@ Synchronize a canister's settings with those defined in the project ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -352,6 +360,7 @@ Start a canister on a network ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -371,6 +380,7 @@ Show the status of a canister ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -390,6 +400,7 @@ Stop a canister on a network ###### **Options:** * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -410,6 +421,7 @@ Top up a canister with cycles * `--amount ` — Amount of cycles to top up * `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -437,9 +449,11 @@ Display the cycles balance ###### **Options:** -* `--identity ` — The user identity to run this command as +* `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -453,6 +467,8 @@ Convert icp to cycles * `--icp ` — Amount of ICP to mint to cycles * `--cycles ` — Amount of cycles to mint. Automatically determines the amount of ICP needed +* `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -708,9 +724,11 @@ Perform token transactions ###### **Options:** -* `--identity ` — The user identity to run this command as +* `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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 @@ -725,9 +743,11 @@ Perform token transactions ###### **Options:** -* `--identity ` — The user identity to run this command as +* `--network ` — Name of the network to target, conflicts with environment argument +* `--mainnet` — Shorthand for --network=mainnet * `--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