diff --git a/CHANGELOG.md b/CHANGELOG.md index 09443bfd49..938e9dedac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ # UNRELEASED +### feat: support for canister ID migration + +Canister ID migration can be performed using `dfx canister migrate-id` +and its status can be checked out using `dfx canister migration-status`. + # 0.30.1 ### feat: support dogecoin for the local dev environment diff --git a/Cargo.lock b/Cargo.lock index 842031d6b2..c07bcb587f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1663,8 +1663,8 @@ dependencies = [ "ic-asset", "ic-cdk", "ic-identity-hsm", - "ic-management-canister-types 0.4.1", - "ic-utils 0.44.2", + "ic-management-canister-types 0.5.0", + "ic-utils 0.44.3", "ic-wasm", "icrc-ledger-types", "idl2json", @@ -1742,7 +1742,7 @@ dependencies = [ "humantime-serde", "ic-agent", "ic-identity-hsm", - "ic-utils 0.44.2", + "ic-utils 0.44.3", "itertools 0.10.5", "k256 0.11.6", "keyring", @@ -2860,9 +2860,8 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6777a6893e52254747abfd0a218aeafc03ada83faa9bff67ee8801739241b5ca" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "arc-swap", "async-channel", @@ -2884,7 +2883,7 @@ dependencies = [ "http-body-util", "ic-certification 3.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "ic-ed25519", - "ic-transport-types 0.44.2", + "ic-transport-types 0.44.3", "ic-verify-bls-signature", "k256 0.13.4", "leb128", @@ -2923,7 +2922,7 @@ dependencies = [ "globset", "hex", "ic-agent", - "ic-utils 0.44.2", + "ic-utils 0.44.3", "itertools 0.10.5", "json5", "mime", @@ -3370,9 +3369,8 @@ dependencies = [ [[package]] name = "ic-identity-hsm" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14ea5968ef808815e917f9a8bfab2bb2c7fbe6e822911276128295b16ce2bbff" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "hex", "ic-agent", @@ -3530,9 +3528,8 @@ dependencies = [ [[package]] name = "ic-transport-types" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "827e8e1da3e75382137f552b088c124618e770974e670fc4f40add86858a4743" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "candid", "hex", @@ -3601,15 +3598,14 @@ dependencies = [ [[package]] name = "ic-utils" -version = "0.44.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b28a3fdfbf4bf4ef470a4d3980f823db299a2e021819171266827d9725efea5a" +version = "0.44.3" +source = "git+https://github.com/dfinity/agent-rs?rev=57befac12a96a90868744f48660f31aff3835739#57befac12a96a90868744f48660f31aff3835739" dependencies = [ "async-trait", "candid", "futures-util", "ic-agent", - "ic-management-canister-types 0.4.1", + "ic-management-canister-types 0.5.0", "once_cell", "semver", "serde", @@ -3619,7 +3615,6 @@ dependencies = [ "strum_macros 0.26.4", "thiserror 2.0.12", "time", - "tokio", ] [[package]] @@ -3838,7 +3833,7 @@ dependencies = [ "humantime", "ic-agent", "ic-asset", - "ic-utils 0.44.2", + "ic-utils 0.44.3", "libflate 1.4.0", "num-traits", "pem 1.1.1", diff --git a/Cargo.toml b/Cargo.toml index 6827bcdb02..588ccad883 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,12 @@ future_not_send = "warn" candid = "0.10.18" candid_parser = "0.2.2" dfx-core = { path = "src/dfx-core", version = "0.2.0" } -ic-agent = "0.44.2" +ic-agent = { git = "https://github.com/dfinity/agent-rs", rev = "57befac12a96a90868744f48660f31aff3835739" } ic-asset = { path = "src/canisters/frontend/ic-asset", version = "0.25.0" } ic-cdk = "0.19.0-beta.2" -ic-identity-hsm = "0.44.2" -ic-utils = "0.44.2" -ic-management-canister-types = "0.4.1" +ic-identity-hsm = { git = "https://github.com/dfinity/agent-rs", rev = "57befac12a96a90868744f48660f31aff3835739" } +ic-utils = { git = "https://github.com/dfinity/agent-rs", rev = "57befac12a96a90868744f48660f31aff3835739" } +ic-management-canister-types = "0.5.0" aes-gcm = { version = "0.10.3", features = ["std"] } anyhow = "1.0.56" diff --git a/docs/cli-reference/dfx-canister.mdx b/docs/cli-reference/dfx-canister.mdx index 2033b71f1d..ae65accbf1 100644 --- a/docs/cli-reference/dfx-canister.mdx +++ b/docs/cli-reference/dfx-canister.mdx @@ -34,6 +34,8 @@ For reference information and examples that illustrate using `dfx canister` comm | [`install`](#dfx-canister-install) | Installs compiled code in a canister. | | [`logs`](#dfx-canister-logs) | Returns the logs from a canister. | | [`metadata`](#dfx-canister-metadata) | Displays metadata of a canister. | +| [`migrate-id`](#dfx-canister-migrate-id) | Performs canister ID migration. | +| [`migration-status`](#dfx-canister-migration-status) | Displays the current status for a canister ID migration. | | [`request-status`](#dfx-canister-request-status) | Requests the status of a call to a canister. | | [`send`](#dfx-canister-send) | Send a previously-signed message. | | [`set-id`](#dfx-canister-id) | Sets the identifier of a canister. | @@ -718,6 +720,88 @@ service : { } ``` +## dfx canister migrate-id + +Use the `dfx canister migrate-id` command to perform canister ID migration +of a canister on one subnet (called the "migrated" canister) +to another subnet replacing the canister ID of a canister on that other subnet. + +### Basic usage + +``` bash +dfx canister migrate-id --replace +``` + +### Arguments + +You can use the following arguments with the `dfx canister migrate-id` command. + +| Argument | Description | +|-----------------|----------------------------------------------------------------------------------------------------------| +| `canister` | Specifies the name or id of the canister whose canister ID you want to migrate. | +| `replace` | Specifies the name or id of the canister whose canister ID will be replaced by the migrated canister ID. | + +### Examples + +To migrate the canister ID of the canister called `migrated` and +replace the canister ID of the canister called `replaced`, +you can run the following command: + +```bash +$ dfx canister migrate-id migrated --replace replaced +``` + +The command displays output similar to the following: + +``` +WARNING! +Canister 'migrated' will be removed from its own subnet. Continue? +Do you want to proceed? yes/No +yes +Migration succeeded at 2025-11-26 08:57:41 UTC +``` + +## dfx canister migration-status + +Use the `dfx canister migration-status` command to display the current status +for a canister ID migration (triggered by a separate command [`migrate-id`](#dfx-canister-migrate-id)) +of a canister on one subnet (called the "migrated" canister) +to another subnet replacing the canister ID of a canister on that other subnet. + +### Basic usage + +``` bash +dfx canister migration-status --replace +``` + +### Arguments + +You can use the following arguments with the `dfx canister migration-status` command. + +| Argument | Description | +|-----------------|----------------------------------------------------------------------------------------------------------| +| `canister` | Specifies the name or id of the canister whose canister ID you want to migrate. | +| `replace` | Specifies the name or id of the canister whose canister ID will be replaced by the migrated canister ID. | + +### Examples + +To display the current status for a canister ID migration +of the canister called `migrated` and +replacing the canister ID of the canister called `replaced`, +you can run the following command: + +```bash +$ dfx canister migration-status migrated --replace replaced +``` + +The command displays output similar to the following: + +``` +| Canister | Canister To Be Replaced | Migration Status | +| --------------------------- | --------------------------- | -------------------------- | +| uqqxf-5h777-77774-qaaaa-cai | ahree-maaaa-aaaar-q777q-cai | In progress: SourceDeleted | +``` + ## dfx canister request-status Use the `dfx canister request-status` command to request the status of a call to a canister. This command diff --git a/e2e/tests-dfx/canister_migration.bash b/e2e/tests-dfx/canister_migration.bash new file mode 100755 index 0000000000..e34a52e488 --- /dev/null +++ b/e2e/tests-dfx/canister_migration.bash @@ -0,0 +1,41 @@ +#!/usr/bin/env bats + +load ../utils/_ + +setup() { + standard_setup + dfx_new hello +} + +teardown() { + dfx_stop + standard_teardown +} + +@test "canister migrate canister id" { + dfx_start --system-canisters + install_asset counter + + # Update dfx.json: rename hello_backend -> source, and add target canister + jq '.canisters.source = .canisters.hello_backend | del(.canisters.hello_backend)' dfx.json | sponge dfx.json + jq '.canisters.target = { "main": "counter.mo", "type": "motoko" }' dfx.json | sponge dfx.json + + # Deploy the source to the application subnet. + dfx deploy source + + # Create the target canister on the fiduciary subnet. + dfx canister create target --subnet-type fiduciary + + dfx canister stop source + dfx canister stop target + + # Make sure the source has enough cycles to do the migration. + dfx ledger fabricate-cycles --canister source --cycles 10000000000000 + + # The migration will take a few minutes to complete. + assert_command dfx canister migrate-id source --replace target --yes + assert_contains "Migration succeeded" + + assert_command dfx canister status source + assert_command_fail dfx canister status target +} diff --git a/src/dfx/src/actors/pocketic.rs b/src/dfx/src/actors/pocketic.rs index 638de5b1dd..fb24bc672d 100644 --- a/src/dfx/src/actors/pocketic.rs +++ b/src/dfx/src/actors/pocketic.rs @@ -394,7 +394,7 @@ async fn initialize_pocketic( ii: Some(IcpFeaturesConfig::default()), nns_ui: Some(IcpFeaturesConfig::default()), bitcoin: icp_features.bitcoin, - canister_migration: None, + canister_migration: Some(IcpFeaturesConfig::default()), dogecoin: icp_features.dogecoin, } } else { diff --git a/src/dfx/src/commands/canister/migrate_id.rs b/src/dfx/src/commands/canister/migrate_id.rs new file mode 100644 index 0000000000..24f3b4fc53 --- /dev/null +++ b/src/dfx/src/commands/canister/migrate_id.rs @@ -0,0 +1,212 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::ic_attributes::CanisterSettings; +use crate::lib::operations::canister::{ + get_canister_status, list_canister_snapshots, update_settings, +}; +use crate::lib::operations::canister_migration::{ + MigrationStatus, NNS_MIGRATION_CANISTER_ID, migrate_canister, migration_status, +}; +use crate::lib::root_key::fetch_root_key_if_needed; +use crate::lib::subnet::get_subnet_for_canister; +use crate::util::ask_for_consent; +use anyhow::{Context, bail}; +use candid::Principal; +use clap::Parser; +use dfx_core::identity::CallSender; +use ic_management_canister_types::CanisterStatusType; +use num_traits::ToPrimitive; +use slog::{debug, error, info}; +use std::time::Duration; +use time::{OffsetDateTime, macros::format_description}; + +/// Migrate a canister ID from one subnet to another. +#[derive(Parser)] +#[command(override_usage = "dfx canister migrate-id [OPTIONS] --replace ")] +pub struct CanisterMigrateIdOpts { + /// Specifies the name or id of the canister to migrate. + canister: String, + + /// Specifies the name or id of the canister to replace. + #[arg(long)] + replace: String, + + /// Skips yes/no checks by answering 'yes'. Not recommended outside of CI. + #[arg(long, short)] + yes: bool, +} + +pub async fn exec( + env: &dyn Environment, + opts: CanisterMigrateIdOpts, + call_sender: &CallSender, +) -> DfxResult { + fetch_root_key_if_needed(env).await?; + + let log = env.get_logger(); + let agent = env.get_agent(); + let canister_id_store = env.get_canister_id_store()?; + + // Get the canister IDs. + let source_canister = opts.canister.as_str(); + let target_canister = opts.replace.as_str(); + let source_canister_id = Principal::from_text(source_canister) + .or_else(|_| canister_id_store.get(source_canister))?; + let target_canister_id = Principal::from_text(target_canister) + .or_else(|_| canister_id_store.get(target_canister))?; + + if source_canister_id == target_canister_id { + bail!("The canisters to migrate and replace are identical."); + } + + if !opts.yes { + ask_for_consent( + env, + &format!("Canister '{source_canister}' will be removed from its own subnet. Continue?"), + )?; + } + + let source_status = get_canister_status(env, source_canister_id, call_sender) + .await + .with_context(|| format!("Could not retrieve status of canister {source_canister}"))?; + let target_status = get_canister_status(env, target_canister_id, call_sender) + .await + .with_context(|| format!("Could not retrieve status of canister {target_canister}"))?; + + // Check that the two canisters are stopped. + ensure_canister_stopped(source_status.status, source_canister)?; + ensure_canister_stopped(target_status.status, target_canister)?; + + // Check that the canister is ready for migration. + if !source_status.ready_for_migration { + bail!( + "Canister '{source_canister}' is not ready for migration. Wait a few seconds and try again" + ); + } + + // Check the cycles balance of source_canister. + let cycles = source_status + .cycles + .0 + .to_u128() + .expect("Unable to parse cycles"); + if cycles < 10_000_000_000_000 { + bail!("Canister '{source_canister}' has less than 10T cycles"); + } + if !opts.yes && cycles > 15_000_000_000_000 { + ask_for_consent( + env, + &format!( + "Canister '{source_canister}' has more than 15T cycles. The extra cycles will get burned during the migration. Continue?" + ), + )?; + } + + // Check that the target canister has no snapshots. + let snapshots = list_canister_snapshots(env, target_canister_id, call_sender).await?; + if !snapshots.is_empty() { + bail!( + "The canister '{}' whose canister ID will be replaced has snapshots", + target_canister + ); + } + + // Check that the two canisters are on different subnets. + let source_subnet = get_subnet_for_canister(agent, source_canister_id).await?; + let target_subnet = get_subnet_for_canister(agent, target_canister_id).await?; + if source_subnet == target_subnet { + bail!("The canisters '{source_canister}' and '{target_canister}' are on the same subnet"); + } + + // Add the NNS migration canister as a controller to the source canister. + let mut controllers = source_status.settings.controllers.clone(); + if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { + controllers.push(NNS_MIGRATION_CANISTER_ID); + let settings = CanisterSettings { + controllers: Some(controllers), + compute_allocation: None, + memory_allocation: None, + freezing_threshold: None, + reserved_cycles_limit: None, + wasm_memory_limit: None, + wasm_memory_threshold: None, + log_visibility: None, + environment_variables: None, + }; + update_settings(env, source_canister_id, settings, call_sender).await?; + } + + // Add the NNS migration canister as a controller to the target canister. + let mut controllers = target_status.settings.controllers.clone(); + if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) { + controllers.push(NNS_MIGRATION_CANISTER_ID); + let settings = CanisterSettings { + controllers: Some(controllers), + compute_allocation: None, + memory_allocation: None, + freezing_threshold: None, + reserved_cycles_limit: None, + wasm_memory_limit: None, + wasm_memory_threshold: None, + log_visibility: None, + environment_variables: None, + }; + update_settings(env, target_canister_id, settings, call_sender).await?; + } + + // Migrate the from canister to the rename_to canister. + debug!(log, "Migrate '{source_canister}' to '{target_canister}'"); + migrate_canister(agent, source_canister_id, target_canister_id).await?; + + // Wait for migration to complete. + let spinner = env.new_spinner("Waiting for migration to complete...".into()); + loop { + match migration_status(agent, source_canister_id, target_canister_id).await { + Ok(statuses) => match statuses.first() { + Some(MigrationStatus::InProgress { status }) => { + spinner.set_message(format!("Migration in progress: {status}").into()); + } + Some(MigrationStatus::Succeeded { time }) => { + spinner.finish_and_clear(); + info!(log, "Migration succeeded at {}", format_time(time)); + break; + } + Some(MigrationStatus::Failed { reason, time }) => { + spinner.finish_and_clear(); + error!(log, "Migration failed at {}: {}", format_time(time), reason); + break; + } + None => (), + }, + Err(e) => { + spinner.set_message(format!("Could not fetch migration status: {e}").into()); + } + }; + + tokio::time::sleep(Duration::from_secs(1)).await; + } + + canister_id_store.remove(log, target_canister)?; + + Ok(()) +} + +fn ensure_canister_stopped(status: CanisterStatusType, canister: &str) -> DfxResult { + match status { + CanisterStatusType::Stopped => Ok(()), + CanisterStatusType::Running => { + bail!("Canister {canister} is running. Run 'dfx canister stop' first"); + } + CanisterStatusType::Stopping => { + bail!("Canister {canister} is stopping. Wait a few seconds and try again"); + } + } +} + +fn format_time(time: &u64) -> String { + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] UTC"); + OffsetDateTime::from_unix_timestamp_nanos(*time as i128) + .unwrap() + .format(&format) + .unwrap() +} diff --git a/src/dfx/src/commands/canister/migration_status.rs b/src/dfx/src/commands/canister/migration_status.rs new file mode 100644 index 0000000000..ec606d50df --- /dev/null +++ b/src/dfx/src/commands/canister/migration_status.rs @@ -0,0 +1,135 @@ +use crate::lib::environment::Environment; +use crate::lib::error::DfxResult; +use crate::lib::operations::canister_migration::{MigrationStatus, migration_status}; +use crate::lib::root_key::fetch_root_key_if_needed; + +use candid::Principal; +use clap::Parser; +use slog::info; +use time::{OffsetDateTime, macros::format_description}; + +/// Show the status of a migration. +#[derive(Parser)] +#[command( + override_usage = "dfx canister migration-status [OPTIONS] --replace " +)] +pub struct CanisterMigrationStatusOpts { + /// Specifies the name or id of the canister to migrate. + canister: String, + + /// Specifies the name or id of the canister to replace. + #[arg(long)] + replace: String, +} + +pub async fn exec(env: &dyn Environment, opts: CanisterMigrationStatusOpts) -> DfxResult { + fetch_root_key_if_needed(env).await?; + + let log = env.get_logger(); + let agent = env.get_agent(); + let canister_id_store = env.get_canister_id_store()?; + + // Get the canister IDs. + let source_canister = opts.canister.as_str(); + let target_canister = opts.replace.as_str(); + let source_canister_id = Principal::from_text(source_canister) + .or_else(|_| canister_id_store.get(source_canister)) + .map_err(|_| { + anyhow::anyhow!( + "Cannot find canister '{source_canister}'. Please use canister id instead" + ) + })?; + let target_canister_id = Principal::from_text(target_canister) + .or_else(|_| canister_id_store.get(target_canister)) + .map_err(|_| { + anyhow::anyhow!( + "Cannot find canister '{target_canister}'. Please use canister id instead" + ) + })?; + + let statuses = migration_status(agent, source_canister_id, target_canister_id).await?; + + if statuses.is_empty() { + info!( + log, + "No migration status found for canister '{source_canister}' to '{target_canister}'" + ); + return Ok(()); + } + + // Print the statuses in a table with aligned columns. + let source_text = source_canister_id.to_text(); + let target_text = target_canister_id.to_text(); + let status_strings: Vec = statuses.iter().map(format_status).collect(); + + let header_source = "Canister"; + let header_target = "Canister To Be Replaced"; + let header_status = "Migration Status"; + + let source_width = header_source.len().max(source_text.len()); + let target_width = header_target.len().max(target_text.len()); + let status_width = header_status + .len() + .max(status_strings.iter().map(|s| s.len()).max().unwrap_or(0)); + + let sep_source = "-".repeat(source_width); + let sep_target = "-".repeat(target_width); + let sep_status = "-".repeat(status_width); + + info!( + log, + "| {: String { + match status { + MigrationStatus::InProgress { status } => { + format!("In progress: {status}") + } + MigrationStatus::Failed { reason, time } => { + format!("Failed: {reason} at {}", format_time(time)) + } + MigrationStatus::Succeeded { time } => { + format!("Succeeded at {}", format_time(time)) + } + } +} + +fn format_time(time: &u64) -> String { + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] UTC"); + OffsetDateTime::from_unix_timestamp_nanos(*time as i128) + .unwrap() + .format(&format) + .unwrap() +} diff --git a/src/dfx/src/commands/canister/mod.rs b/src/dfx/src/commands/canister/mod.rs index 03beb19828..ccc0520cd5 100644 --- a/src/dfx/src/commands/canister/mod.rs +++ b/src/dfx/src/commands/canister/mod.rs @@ -15,6 +15,8 @@ mod info; mod install; mod logs; mod metadata; +mod migrate_id; +mod migration_status; mod request_status; mod send; mod set_id; @@ -53,6 +55,8 @@ pub enum SubCommand { Info(info::InfoOpts), Install(install::CanisterInstallOpts), Metadata(metadata::CanisterMetadataOpts), + MigrateId(migrate_id::CanisterMigrateIdOpts), + MigrationStatus(migration_status::CanisterMigrationStatusOpts), RequestStatus(request_status::RequestStatusOpts), Send(send::CanisterSendOpts), SetId(set_id::CanisterSetIdOpts), @@ -88,6 +92,8 @@ pub fn exec(env: &dyn Environment, opts: CanisterOpts) -> DfxResult { SubCommand::Install(v) => install::exec(env, v, &call_sender()?).await, SubCommand::Info(v) => info::exec(env, v).await, SubCommand::Metadata(v) => metadata::exec(env, v).await, + SubCommand::MigrateId(v) => migrate_id::exec(env, v, &call_sender()?).await, + SubCommand::MigrationStatus(v) => migration_status::exec(env, v).await, SubCommand::RequestStatus(v) => request_status::exec(env, v).await, SubCommand::Send(v) => send::exec(env, v, &call_sender()?).await, SubCommand::SetId(v) => set_id::exec(env, v).await, diff --git a/src/dfx/src/commands/canister/snapshot.rs b/src/dfx/src/commands/canister/snapshot.rs index 05fb75cc4b..f7d3f258c3 100644 --- a/src/dfx/src/commands/canister/snapshot.rs +++ b/src/dfx/src/commands/canister/snapshot.rs @@ -506,7 +506,11 @@ async fn upload( canister_id, replace_snapshot: replace.as_ref().map(|x| x.0.clone()), wasm_module_size: metadata.wasm_module_size, - globals: metadata.globals, + globals: metadata + .globals + .into_iter() + .map(|x| x.ok_or(anyhow!("Could not parse global in snapshot metadata"))) + .collect::, _>>()?, wasm_memory_size: metadata.wasm_memory_size, stable_memory_size: metadata.stable_memory_size, certified_data: metadata.certified_data, diff --git a/src/dfx/src/lib/operations/canister_migration.rs b/src/dfx/src/lib/operations/canister_migration.rs new file mode 100644 index 0000000000..1ba4fae299 --- /dev/null +++ b/src/dfx/src/lib/operations/canister_migration.rs @@ -0,0 +1,149 @@ +use crate::lib::error::DfxResult; +use candid::{CandidType, Principal, Reserved}; +use ic_agent::Agent; +use ic_utils::Canister; +use serde::Deserialize; +use std::fmt; + +pub const NNS_MIGRATION_CANISTER_ID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x01, 0x01]); +const MIGRATE_CANISTER_METHOD: &str = "migrate_canister"; +const MIGRATION_STATUS_METHOD: &str = "migration_status"; + +#[derive(Clone, CandidType, Deserialize)] +pub struct MigrateCanisterArgs { + pub canister_id: Principal, + pub replace_canister_id: Principal, +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub enum ValidationError { + MigrationsDisabled(Reserved), + RateLimited(Reserved), + ValidationInProgress { canister: Principal }, + MigrationInProgress { canister: Principal }, + CanisterNotFound { canister: Principal }, + SameSubnet(Reserved), + CallerNotController { canister: Principal }, + NotController { canister: Principal }, + SourceNotStopped(Reserved), + SourceNotReady(Reserved), + TargetNotStopped(Reserved), + TargetHasSnapshots(Reserved), + SourceInsufficientCycles(Reserved), + CallFailed { reason: String }, +} + +impl fmt::Display for ValidationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ValidationError::MigrationsDisabled(Reserved) => write!(f, "MigrationsDisabled"), + ValidationError::RateLimited(Reserved) => write!(f, "RateLimited"), + ValidationError::ValidationInProgress { canister } => write!( + f, + "ValidationError::ValidationInProgress {{ canister: {canister} }}", + ), + ValidationError::MigrationInProgress { canister } => write!( + f, + "ValidationError::MigrationInProgress {{ canister: {canister} }}", + ), + ValidationError::CanisterNotFound { canister } => write!( + f, + "ValidationError::CanisterNotFound {{ canister: {canister} }}", + ), + ValidationError::SameSubnet(Reserved) => write!(f, "SameSubnet"), + ValidationError::CallerNotController { canister } => write!( + f, + "ValidationError::CallerNotController {{ canister: {canister} }}", + ), + ValidationError::NotController { canister } => write!( + f, + "ValidationError::NotController {{ canister: {canister} }}", + ), + ValidationError::SourceNotStopped(Reserved) => write!(f, "SourceNotStopped"), + ValidationError::SourceNotReady(Reserved) => write!(f, "SourceNotReady"), + ValidationError::TargetNotStopped(Reserved) => write!(f, "TargetNotStopped"), + ValidationError::TargetHasSnapshots(Reserved) => write!(f, "TargetHasSnapshots"), + ValidationError::SourceInsufficientCycles(Reserved) => { + write!(f, "SourceInsufficientCycles") + } + ValidationError::CallFailed { reason } => { + write!(f, "ValidationError::CallFailed {{ reason: {reason} }}") + } + } + } +} + +#[derive(Clone, CandidType, Deserialize, Debug)] +pub enum MigrationStatus { + InProgress { status: String }, + Failed { reason: String, time: u64 }, + Succeeded { time: u64 }, +} + +impl fmt::Display for MigrationStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MigrationStatus::InProgress { status } => { + write!(f, "MigrationStatus::InProgress {{ status: {status} }}") + } + MigrationStatus::Failed { reason, time } => { + write!( + f, + "MigrationStatus::Failed {{ reason: {reason}, time: {time} }}", + ) + } + MigrationStatus::Succeeded { time } => { + write!(f, "MigrationStatus::Succeeded {{ time: {time} }}") + } + } + } +} + +pub async fn migrate_canister( + agent: &Agent, + from_canister: Principal, + to_canister: Principal, +) -> DfxResult { + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(NNS_MIGRATION_CANISTER_ID) + .build()?; + + let arg = MigrateCanisterArgs { + canister_id: from_canister, + replace_canister_id: to_canister, + }; + + let _: () = canister + .update(MIGRATE_CANISTER_METHOD) + .with_arg(arg) + .build() + .await?; + + Ok(()) +} + +pub async fn migration_status( + agent: &Agent, + from_canister: Principal, + to_canister: Principal, +) -> DfxResult> { + let canister = Canister::builder() + .with_agent(agent) + .with_canister_id(NNS_MIGRATION_CANISTER_ID) + .build()?; + + let arg = MigrateCanisterArgs { + canister_id: from_canister, + replace_canister_id: to_canister, + }; + + let (result,): (Vec,) = canister + .query(MIGRATION_STATUS_METHOD) + .with_arg(arg) + .build() + .await?; + + Ok(result) +} diff --git a/src/dfx/src/lib/operations/mod.rs b/src/dfx/src/lib/operations/mod.rs index 9c2838deee..d68b65d657 100644 --- a/src/dfx/src/lib/operations/mod.rs +++ b/src/dfx/src/lib/operations/mod.rs @@ -1,4 +1,5 @@ pub mod canister; +pub mod canister_migration; pub mod cmc; pub mod cycles_ledger; pub mod ledger;