Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
96cf198
feat: Add an optional root-key argument to canister commands
raymondk Feb 20, 2026
90cc69b
Also check key size
raymondk Feb 20, 2026
1f280f3
Merge branch 'main' into rk/root-key
raymondk Feb 20, 2026
c30d029
add tests
raymondk Feb 20, 2026
95ab490
fmt
raymondk Feb 20, 2026
0be4b05
lint
raymondk Feb 20, 2026
3e0b5f2
remove the environment variable for the root key
raymondk Feb 20, 2026
3b0f20b
use a parse for the network arg
raymondk Feb 20, 2026
96f5574
--root-key can only be used with --network
raymondk Feb 20, 2026
383ff00
Validate when we parse the arguments
raymondk Feb 20, 2026
e0a634d
Merge branch 'main' into rk/root-key
raymondk Feb 21, 2026
69dab77
update cli docs
raymondk Feb 21, 2026
c194fab
make the test pass
raymondk Feb 21, 2026
b463165
fmt
raymondk Feb 21, 2026
3d61d5e
error for name+rootkey
raymondk Feb 21, 2026
f75334e
Merge branch 'main' into rk/root-key
raymondk Feb 21, 2026
1686504
lint
raymondk Feb 21, 2026
4dd6996
feat: add the ability to create an unrecorded canister on a network
raymondk Feb 23, 2026
b3408a8
Merge branch 'main' into rk/canister-create-bkp
raymondk Feb 23, 2026
1dff2e5
add --detach and tests
raymondk Feb 24, 2026
a710be3
Merge branch 'main' into rk/canister-create-bkp
raymondk Feb 24, 2026
d781874
Merge branch 'main' into rk/canister-create-bkp
raymondk Feb 24, 2026
72226b3
Merge branch 'main' into rk/canister-create-bkp
raymondk Feb 24, 2026
3215843
compilation errors after merge
raymondk Feb 24, 2026
12c73aa
Merge branch 'main' into rk/canister-create-bkp
raymondk Feb 24, 2026
f768a13
use quiet instead of --id-only
raymondk Feb 24, 2026
9df08d4
use --detach to determine the path
raymondk Feb 24, 2026
3bbb51f
flip it
raymondk Feb 24, 2026
cda9bb4
don't show the panic
raymondk Feb 24, 2026
083b2f1
better help msg
adamspofford-dfinity Feb 24, 2026
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
57 changes: 50 additions & 7 deletions crates/icp-cli/src/commands/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ use icrc_ledger_types::icrc1::account::Account;

use crate::options::{EnvironmentOpt, IdentityOpt, NetworkOpt};

/// Selections derived from CanisterCommandArgs
pub(crate) struct CommandSelections {
pub(crate) canister: CanisterSelection,
pub(crate) environment: EnvironmentSelection,
pub(crate) network: NetworkSelection,
pub(crate) identity: IdentitySelection,
}

#[derive(Args, Debug)]
pub(crate) struct CanisterCommandArgs {
// Note: Could have flattened CanisterEnvironmentArg to avoid adding child field
Expand All @@ -27,23 +35,41 @@ pub(crate) struct CanisterCommandArgs {
pub(crate) identity: IdentityOpt,
}

/// Selections derived from CanisterCommandArgs
pub(crate) struct CommandSelections {
pub(crate) canister: CanisterSelection,
impl CanisterCommandArgs {
/// Convert command arguments into selection enums
pub(crate) fn selections(&self) -> CommandSelections {
let canister_selection: CanisterSelection = self.canister.clone().into();
let environment_selection: EnvironmentSelection = self.environment.clone().into();
let network_selection: NetworkSelection = self.network.clone().into();
let identity_selection: IdentitySelection = self.identity.clone().into();

CommandSelections {
canister: canister_selection,
environment: environment_selection,
network: network_selection,
identity: identity_selection,
}
}
}

/// Selections derived from OptionalCanisterCommandArgs
pub(crate) struct OptionalCanisterCommandSelections {
pub(crate) canister: Option<CanisterSelection>,
pub(crate) environment: EnvironmentSelection,
pub(crate) network: NetworkSelection,
pub(crate) identity: IdentitySelection,
}

impl CanisterCommandArgs {
impl OptionalCanisterCommandArgs {
/// Convert command arguments into selection enums
pub(crate) fn selections(&self) -> CommandSelections {
let canister_selection: CanisterSelection = self.canister.clone().into();
pub(crate) fn selections(&self) -> OptionalCanisterCommandSelections {
let canister_selection: Option<CanisterSelection> =
self.canister.as_ref().map(|c| c.clone().into());
let environment_selection: EnvironmentSelection = self.environment.clone().into();
let network_selection: NetworkSelection = self.network.clone().into();
let identity_selection: IdentitySelection = self.identity.clone().into();

CommandSelections {
OptionalCanisterCommandSelections {
canister: canister_selection,
environment: environment_selection,
network: network_selection,
Expand All @@ -52,6 +78,23 @@ impl CanisterCommandArgs {
}
}

// Like the CanisterCommandArgs but canister is optional
#[derive(Args, Debug)]
pub(crate) struct OptionalCanisterCommandArgs {
/// Name or principal of canister to target.
/// When using a name an environment must be specified.
pub(crate) canister: Option<Canister>,

#[command(flatten)]
pub(crate) network: NetworkOpt,

#[command(flatten)]
pub(crate) environment: EnvironmentOpt,

#[command(flatten)]
pub(crate) identity: IdentityOpt,
}

// Common argument used for Token and Cycles commands
#[derive(Args, Clone, Debug)]
pub(crate) struct TokenCommandArgs {
Expand Down
140 changes: 116 additions & 24 deletions crates/icp-cli/src/commands/canister/create.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
use anyhow::anyhow;
use candid::{Nat, Principal};
use clap::Args;
use clap::{ArgGroup, Args, Parser};
use icp::context::Context;
use icp::parsers::{CyclesAmount, MemoryAmount};
use icp::{Canister, context::CanisterSelection, prelude::*};
use icp_canister_interfaces::cycles_ledger::CanisterSettingsArg;

use crate::{
commands::args,
operations::create::CreateOperation,
progress::{ProgressManager, ProgressManagerSettings},
};
use crate::{commands::args, operations::create::CreateOperation};

pub(crate) const DEFAULT_CANISTER_CYCLES: u128 = 2 * TRILLION;

Expand All @@ -36,11 +32,31 @@ pub(crate) struct CanisterSettings {
pub(crate) reserved_cycles_limit: Option<CyclesAmount>,
}

/// Create a canister on a network
#[derive(Debug, Args)]
/// Create a canister on a network.
#[derive(Debug, Parser)]
#[command(after_long_help = "\
This command can be used to create canisters defined in a project
or a \"detached\" canister on a network.

Examples:

# Create on a network by url
icp canister create -n http://localhost:8000 -k $ROOT_KEY --detached

# Create on mainnet outside of a project context
icp canister create -n ic --detached

# Create a detached canister inside the scope of a project
icp canister create -n mynetwork --detached
")]
#[command(group(
ArgGroup::new("canister_sel")
.args(["canister", "detached"])
.required(true)
))]
pub(crate) struct CreateArgs {
#[command(flatten)]
pub(crate) cmd_args: args::CanisterCommandArgs,
pub(crate) cmd_args: args::OptionalCanisterCommandArgs,

/// One or more controllers for the canister. Repeat `--controller` to specify multiple.
#[arg(long)]
Expand All @@ -62,6 +78,16 @@ pub(crate) struct CreateArgs {
/// The subnet to create canisters on.
#[arg(long)]
pub(crate) subnet: Option<Principal>,

/// Create a canister detached from any project configuration. The canister id will be
/// printed out but not recorded in the project configuration. Not valid if `Canister`
/// is provided.
#[arg(
long,
conflicts_with = "canister",
required_unless_present = "canister"
)]
pub detached: bool,
}

impl CreateArgs {
Expand All @@ -83,6 +109,7 @@ impl CreateArgs {
.clone()
.or(default.settings.reserved_cycles_limit.clone())
.map(|c| Nat::from(c.get())),
// TODO This should be configurable from the CLI
log_visibility: default.settings.log_visibility.clone().map(Into::into),
memory_allocation: self
.settings
Expand All @@ -97,14 +124,84 @@ impl CreateArgs {
.map(Nat::from),
}
}

pub(crate) fn canister_settings(&self) -> CanisterSettingsArg {
CanisterSettingsArg {
freezing_threshold: self.settings.freezing_threshold.map(Nat::from),
controllers: if self.controller.is_empty() {
None
} else {
Some(self.controller.clone())
},
reserved_cycles_limit: self
.settings
.reserved_cycles_limit
.clone()
.map(|c| Nat::from(c.get())),
// TODO This should be configurable from the CLI
log_visibility: None,
memory_allocation: self
.settings
.memory_allocation
.clone()
.map(|m| Nat::from(m.get())),
compute_allocation: self.settings.compute_allocation.map(Nat::from),
}
}
}

// Creates canister(s) by asking the cycles ledger to create them.
// The cycles ledger will take cycles out of the user's account, and attaches them to a call to CMC::create_canister.
// The CMC will then pick a subnet according to the user's preferences and permissions, and create a canister on that subnet.
pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow::Error> {
if args.detached {
create_canister(ctx, args).await
} else {
create_project_canister(ctx, args).await
}
}

// Attemtps to create a canister on the target network without recording it in the project metadata
async fn create_canister(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow::Error> {
let selections = args.cmd_args.selections();
let canister = match selections.canister {
assert!(
selections.canister.is_none(),
"This path should not be called if canister is_some()"
);

let agent = ctx
.get_agent(
&selections.identity,
&selections.network,
&selections.environment,
)
.await?;

let create_operation = CreateOperation::new(agent, args.subnet, args.cycles.get(), vec![]);

let canister_settings = args.canister_settings();

let id = create_operation.create(&canister_settings).await?;

if args.quiet {
let _ = ctx.term.write_line(&format!("{id}"));
} else {
let _ = ctx
.term
.write_line(&format!("Created canister with ID {id}"));
}

Ok(())
}

// Attempts to create a canister and record it in the project metadata
async fn create_project_canister(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow::Error> {
let selections = args.cmd_args.selections();

let canister = match selections
.canister
.expect("Canister must be Some() when --detached is not used")
Comment thread
adamspofford-dfinity marked this conversation as resolved.
{
CanisterSelection::Named(name) => name,
CanisterSelection::Principal(_) => Err(anyhow!("Cannot create a canister by principal"))?,
};
Expand Down Expand Up @@ -135,28 +232,23 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow:
.map_err(|e| anyhow!(e))?
.into_values()
.collect();
let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: ctx.debug });

let create_operation =
CreateOperation::new(agent, args.subnet, args.cycles.get(), existing_canisters);

let canister_settings = args.canister_settings_with_default(&canister_info);
let pb = progress_manager.create_progress_bar(&canister);
pb.set_message("Creating...");
let id = ProgressManager::execute_with_custom_progress(
&pb,
create_operation.create(&canister_settings),
|| "Created successfully".to_string(),
|err: &_| err.to_string(),
|_| false,
)
.await?;
let id = create_operation.create(&canister_settings).await?;

ctx.set_canister_id_for_env(&canister, id, &selections.environment)
.await?;

let _ = ctx
.term
.write_line(&format!("Created canister {canister} with ID {id}"));
if args.quiet {
let _ = ctx.term.write_line(&format!("{id}"));
} else {
let _ = ctx
.term
.write_line(&format!("Created canister {canister} with ID {id}"));
}

Ok(())
}
72 changes: 72 additions & 0 deletions crates/icp-cli/tests/canister_create_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -465,3 +465,75 @@ fn canister_create_with_valid_principal() {
.failure()
.stderr(contains("Cannot create a canister by principal"));
}

#[tokio::test]
async fn canister_create_detached() {
let ctx = TestContext::new();

// Setup project
let project_dir = ctx.create_project_dir("icp");

// Project manifest
let pm = formatdoc! {r#"
{NETWORK_RANDOM_PORT}
"#};
write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest");

// Start network
let _g = ctx.start_network_in(&project_dir, "random-network").await;
ctx.ping_until_healthy(&project_dir, "random-network");

// Get the network information so we can call the network directly
let assert = ctx
.icp()
.current_dir(&project_dir)
.args(["network", "status", "random-network", "--json"])
.assert()
.success();
let output = assert.get_output();

let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let gateway_url = json["gateway_url"].as_str().expect("Should be a string");
let root_key = json["root_key"].as_str().expect("Should be a string");

// Test creating outside a project
ctx.icp()
.args([
"canister",
"create",
"--network",
gateway_url,
"--root-key",
root_key,
"--detached",
])
.assert()
.success()
.stdout(starts_with("Created canister with ID"));

// Test creating inside a project
ctx.icp()
.current_dir(&project_dir)
.args([
"canister",
"create",
"--network",
"random-network",
"--detached",
])
.assert()
.success()
.stdout(starts_with("Created canister with ID"));

// Test it fails outside of a project
ctx.icp()
.args([
"canister",
"create",
"--network",
"random-network",
"--detached",
])
.assert()
.failure();
}
18 changes: 17 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,22 @@ Make a canister call

Create a canister on a network

**Usage:** `icp canister create [OPTIONS] <CANISTER>`
**Usage:** `icp canister create [OPTIONS] <CANISTER|--detached>`

This command can be used to create canisters defined in a project
or a "detached" canister on a network.

Examples:

# Create on a network by url
icp canister create -n http://localhost:8000 -k $ROOT_KEY --detached

# Create on mainnet outside of a project context
icp canister create -n ic --detached

# Create a detached canister inside the scope of a project
icp canister create -n mynetwork --detached


###### **Arguments:**

Expand All @@ -208,6 +223,7 @@ Create a canister on a network

Default value: `2000000000000`
* `--subnet <SUBNET>` — The subnet to create canisters on
* `--detached` — Create a canister detached from any project configuration. The canister id will be printed out but not recorded in the project configuration. Not valid if `Canister` is provided



Expand Down
Loading