From 698ffc581ca0569d9e50bf3a12ce4f845d3d0040 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Feb 2026 10:53:40 -0500 Subject: [PATCH 1/2] feat: install proxy canister on managed network initialization Install the proxy canister when starting a managed network and record its ID in the network descriptor. Set controllers to all identities, or just anonymous and default identity if more than 10 exist (IC protocol limit of 10 controllers per canister). Co-Authored-By: Claude Sonnet 4.5 --- crates/icp-cli/src/commands/network/start.rs | 22 ++- crates/icp-cli/tests/network_tests.rs | 3 +- crates/icp/src/network/config.rs | 2 + crates/icp/src/network/managed/run.rs | 163 ++++++++++++++++--- 4 files changed, 163 insertions(+), 27 deletions(-) diff --git a/crates/icp-cli/src/commands/network/start.rs b/crates/icp-cli/src/commands/network/start.rs index 0e8d00917..1b488979b 100644 --- a/crates/icp-cli/src/commands/network/start.rs +++ b/crates/icp-cli/src/commands/network/start.rs @@ -1,4 +1,5 @@ use anyhow::{Context as _, bail}; +use candid::Principal; use clap::Args; use icp::prelude::*; use icp::{ @@ -81,19 +82,28 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: } // Identities - let ids = ctx + let (ids, defaults) = ctx .dirs .identity()? - .with_read(async |dirs| IdentityList::load_from(dirs)) + .with_read(async |dirs| { + let ids = IdentityList::load_from(dirs)?; + let defaults = icp::identity::manifest::IdentityDefaults::load_from(dirs)?; + Ok::<_, anyhow::Error>((ids, defaults)) + }) .await??; - // Determine ICP accounts to seed - let seed_accounts = ids.identities.values().map(|id| id.principal()); + let all_identities: Vec = ids.identities.values().map(|id| id.principal()).collect(); + + let default_identity = ids + .identities + .get(&defaults.default) + .map(|id| id.principal()); debug!("Project root: {pdir}"); debug!("Network root: {}", nd.network_root); let candid_ui_wasm = crate::artifacts::get_candid_ui_wasm(); + let proxy_wasm = crate::artifacts::get_proxy_wasm(); let network_launcher_path = if let Ok(var) = std::env::var("ICP_CLI_NETWORK_LAUNCHER_PATH") { Some(PathBuf::from(var)) @@ -128,8 +138,10 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: cfg, nd, pdir, - seed_accounts, + all_identities, + default_identity, Some(candid_ui_wasm), + Some(proxy_wasm), args.background, ctx.debug, network_launcher_path.as_deref(), diff --git a/crates/icp-cli/tests/network_tests.rs b/crates/icp-cli/tests/network_tests.rs index b811c203a..3a771aac6 100644 --- a/crates/icp-cli/tests/network_tests.rs +++ b/crates/icp-cli/tests/network_tests.rs @@ -329,7 +329,8 @@ async fn network_run_and_stop_background() { .assert() .success() .stderr(contains("Seeding ICP and cycles")) - .stdout(contains("Installed Candid UI canister with ID")); + .stdout(contains("Installed Candid UI canister with ID")) + .stdout(contains("Installed proxy canister with ID")); let network = ctx.wait_for_network_descriptor(&project_dir, "random-network"); diff --git a/crates/icp/src/network/config.rs b/crates/icp/src/network/config.rs index 6ef56bf6c..84bc3c95f 100644 --- a/crates/icp/src/network/config.rs +++ b/crates/icp/src/network/config.rs @@ -59,6 +59,8 @@ pub struct NetworkDescriptorModel { pub pocketic_instance_id: Option, /// Canister ID of the deployed Candid UI, if any. pub candid_ui_canister_id: Option, + /// Canister ID of the deployed proxy canister, if any. + pub proxy_canister_id: Option, } /// Identifies the process or container running a managed network. diff --git a/crates/icp/src/network/managed/run.rs b/crates/icp/src/network/managed/run.rs index b27cdb448..d4ee07dbf 100644 --- a/crates/icp/src/network/managed/run.rs +++ b/crates/icp/src/network/managed/run.rs @@ -10,8 +10,8 @@ use ic_ledger_types::{AccountIdentifier, Memo, Subaccount, Tokens, TransferArgs, use ic_utils::interfaces::management_canister::builders::CanisterInstallMode; use icp_canister_interfaces::{ cycles_ledger::{ - CYCLES_LEDGER_BLOCK_FEE, CYCLES_LEDGER_PRINCIPAL, CreateCanisterArgs, - CreateCanisterResponse, + CYCLES_LEDGER_BLOCK_FEE, CYCLES_LEDGER_PRINCIPAL, CanisterSettingsArg, CreateCanisterArgs, + CreateCanisterResponse, CreationArgs, }, cycles_minting_canister::{ CYCLES_MINTING_CANISTER_PRINCIPAL, ConversionRateResponse, MEMO_MINT_CYCLES, @@ -53,8 +53,10 @@ pub async fn run_network( config: &Managed, nd: NetworkDirectory, project_root: &Path, - seed_accounts: impl Iterator + Clone, + all_identities: Vec, + default_identity: Option, candid_ui_wasm: Option<&[u8]>, + proxy_wasm: Option<&[u8]>, background: bool, verbose: bool, network_launcher_path: Option<&Path>, @@ -66,8 +68,10 @@ pub async fn run_network( config, &nd, project_root, - seed_accounts, + all_identities, + default_identity, candid_ui_wasm, + proxy_wasm, background, verbose, ) @@ -117,8 +121,10 @@ async fn run_network_launcher( config: &Managed, nd: &NetworkDirectory, project_root: &Path, - seed_accounts: impl Iterator + Clone, + all_identities: Vec, + default_identity: Option, candid_ui_wasm: Option<&[u8]>, + proxy_wasm: Option<&[u8]>, background: bool, verbose: bool, ) -> Result<(), RunNetworkLauncherError> { @@ -216,13 +222,16 @@ async fn run_network_launcher( // background means we're using stdio files - otherwise the launcher already prints this eprintln!("Network started on port {}", instance.gateway_port); } - let candid_ui_canister_id = initialize_network( + + let (candid_ui_canister_id, proxy_canister_id) = initialize_network( &format!("http://localhost:{}", instance.gateway_port) .parse() .unwrap(), &instance.root_key, - seed_accounts, + all_identities, + default_identity, candid_ui_wasm, + proxy_wasm, ) .await?; @@ -246,6 +255,7 @@ async fn run_network_launcher( pocketic_config_port: instance.pocketic_config_port, pocketic_instance_id: instance.pocketic_instance_id, candid_ui_canister_id, + proxy_canister_id, }; // Save descriptor to project root and all fixed port directories @@ -469,14 +479,17 @@ pub enum WaitForPortError { /// Initialize the network: /// - Seed ICP and cycles to the given accounts /// - Install the candid UI canister +/// - Install the proxy canister /// -/// Returns the canister id of the candid ui canister +/// Returns a tuple of (candid_ui_canister_id, proxy_canister_id) pub async fn initialize_network( gateway_url: &Url, root_key: &[u8], - seed_accounts: impl IntoIterator + Clone, + all_identities: Vec, + default_identity: Option, candid_ui_wasm: Option<&[u8]>, -) -> Result, InitializeNetworkError> { + proxy_wasm: Option<&[u8]>, +) -> Result<(Option, Option), InitializeNetworkError> { eprintln!("Seeding ICP and cycles account balances"); let agent = Agent::builder() .with_url(gateway_url.as_str()) @@ -486,28 +499,28 @@ pub async fn initialize_network( url: gateway_url.as_str(), })?; agent.set_root_key(root_key.to_vec()); + let icp_xdr_conversion_rate = get_icp_xdr_conversion_rate(&agent).await?; let icp_amount = 100_000_000_000_000u64; let display_icp_amount = BigDecimal::new(icp_amount.into(), 8).normalized(); let seed_icp = join_all( - seed_accounts - .clone() - .into_iter() - .filter(|account| *account != Principal::anonymous()) // Anon gets seeded by pocket-ic (or whatever the launcher is doing) + all_identities + .iter() + .filter(|account| **account != Principal::anonymous()) // Anon gets seeded by pocket-ic (or whatever the launcher is doing) .map(|account| { debug!("Seeding {} ICP to account {}", display_icp_amount, account); - acquire_icp_to_account(&agent, account, icp_amount) + acquire_icp_to_account(&agent, *account, icp_amount) }), ); let cycles_amount = 1_000_000_000_000_000u128; // 1_000T cycles let display_cycles_amount = BigDecimal::new(cycles_amount.into(), 12).normalized(); - let seed_cycles = join_all(seed_accounts.into_iter().map(|account| { + let seed_cycles = join_all(all_identities.iter().map(|account| { debug!( "Seeding {}T cycles to account {}", display_cycles_amount, account ); - mint_cycles_to_account(&agent, account, cycles_amount, icp_xdr_conversion_rate) + mint_cycles_to_account(&agent, *account, cycles_amount, icp_xdr_conversion_rate) })); let (seed_icp_results, seed_cycles_results) = join(seed_icp, seed_cycles).await; seed_icp_results @@ -517,11 +530,44 @@ pub async fn initialize_network( .into_iter() .collect::, _>>()?; - if let Some(candid_ui_wasm) = candid_ui_wasm { - Ok(Some(install_candid_ui(&agent, candid_ui_wasm).await?)) + // Install Candid UI if provided + let candid_ui_id = if let Some(candid_ui_wasm) = candid_ui_wasm { + Some(install_candid_ui(&agent, candid_ui_wasm).await?) } else { - Ok(None) - } + None + }; + + // Install proxy canister if provided + let proxy_id = if let Some(proxy_wasm) = proxy_wasm { + // Determine controllers based on the number of identities + // IC protocol limits: max 10 controllers per canister + let controllers = if all_identities.len() <= 10 { + // Use all identities as controllers + all_identities + } else { + // Use only anonymous and default identity + debug!( + "More than 10 identities detected ({} total). IC protocol limits canisters to 10 controllers. \ + Proxy canister will be created with only anonymous and default identity as controllers.", + all_identities.len() + ); + let mut limited_controllers = vec![Principal::anonymous()]; + if let Some(default) = default_identity { + // Only add default if it's different from anonymous + if default != Principal::anonymous() { + limited_controllers.push(default); + } + } + + limited_controllers + }; + + Some(install_proxy(&agent, proxy_wasm, controllers).await?) + } else { + None + }; + + Ok((candid_ui_id, proxy_id)) } #[derive(Debug, Snafu)] @@ -534,6 +580,9 @@ pub enum InitializeNetworkError { #[snafu(display("Failed to install Candid UI canister: {error}"))] CandidUI { error: String }, + + #[snafu(display("Failed to install proxy canister: {error}"))] + Proxy { error: String }, } async fn mint_cycles_to_account( @@ -747,3 +796,75 @@ async fn install_candid_ui( Ok(canister_id) } + +async fn install_proxy( + agent: &Agent, + proxy_wasm: &[u8], + controllers: Vec, +) -> Result { + debug!("Creating canister for proxy"); + let amount = 10 * TRILLION; + + // Prepare controller settings + let creation_args = if !controllers.is_empty() { + Some(CreationArgs { + subnet_selection: None, + settings: Some(CanisterSettingsArg { + controllers: Some(controllers.clone()), + freezing_threshold: None, + reserved_cycles_limit: None, + log_visibility: None, + memory_allocation: None, + compute_allocation: None, + }), + }) + } else { + None + }; + + let response = agent + .update(&CYCLES_LEDGER_PRINCIPAL, "create_canister") + .with_arg( + Encode!(&CreateCanisterArgs { + from_subaccount: None, + created_at_time: None, + amount: Nat::from(amount), + creation_args, + }) + .unwrap(), + ) + .await + .map_err(|e| InitializeNetworkError::Proxy { + error: format!("Failed to create canister for proxy: {e}"), + })?; + let response = + Decode!(&response, CreateCanisterResponse).map_err(|e| InitializeNetworkError::Proxy { + error: format!("Failed to decode create canister response for proxy: {e}"), + })?; + let canister_id = match response { + CreateCanisterResponse::Ok { canister_id, .. } => canister_id, + CreateCanisterResponse::Err(err) => { + return Err(InitializeNetworkError::Proxy { + error: format!( + "Failed to create canister for proxy: {}", + err.format_error(amount) + ), + }); + } + }; + debug!("Installing proxy wasm into canister {}", canister_id); + + let mgmt = ic_utils::interfaces::ManagementCanister::create(agent); + mgmt.install_code(&canister_id, proxy_wasm) + .with_mode(CanisterInstallMode::Install) + .await + .map_err(|e| InitializeNetworkError::Proxy { + error: format!("Failed to install proxy canister: {e}"), + })?; + debug!( + "Installed proxy canister with ID {} and controllers: {:?}", + canister_id, controllers + ); + + Ok(canister_id) +} From a069cdce9aba8f47bff5c9d8e7392c2fcc79a53d Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Mon, 9 Feb 2026 11:01:01 -0500 Subject: [PATCH 2/2] chore: update changelog for proxy canister installation Co-Authored-By: Claude Sonnet 4.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c26ae783..e3ad0892e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased * feat: `icp canister migrate-id` - initiate canister ID migration across subnets +* feat: install proxy canister when starting managed networks with all identities as controllers (or anonymous + default if more than 10 identities) # v0.1.0