From b40633e6f3dd3588f3e0f244fb5ad60b5301e7c1 Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Fri, 22 May 2026 22:41:45 +0200 Subject: [PATCH 1/6] feat(station): support wasm_memory_persistence for external canister upgrades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChangeExternalCanisterOperationInput gains two optional fields, `wasm_memory_persistence` and `skip_pre_upgrade`, which are forwarded to the IC management canister as `CanisterUpgradeOptions`. Without this, Motoko canisters that use Enhanced Orthogonal Persistence cannot be safely upgraded through Orbit — the IC defaults `wasm_memory_persistence` to `replace`, wiping their main memory. The fields are additive (`opt`) so existing callers and stored requests remain backward-compatible; the legacy `CanisterUpgradeModeArgs` CBOR shape (empty map) still deserializes via `#[serde(default)]`. Covered by new unit tests for mapper round-trip, mgmt translation, and legacy deserialization. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/wallet/src/generated/station/station.did | 22 +++ .../src/generated/station/station.did.d.ts | 30 ++++ .../src/generated/station/station.did.js | 8 + core/station/api/spec.did | 22 +++ core/station/api/src/common.rs | 8 + core/station/api/src/external_canister.rs | 11 +- .../impl/src/mappers/request_operation.rs | 160 +++++++++++++++++- .../impl/src/models/request_operation.rs | 40 ++++- core/station/impl/src/services/system.rs | 2 +- .../src/external_canister_tests.rs | 6 + tools/dfx-orbit/src/canister/install.rs | 2 + 11 files changed, 299 insertions(+), 12 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index c099fc892..0b3ed2854 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -578,6 +578,15 @@ type CanisterInstallMode = variant { upgrade; }; +// WASM memory persistence setting passed to `install_code` when the install +// mode is `upgrade`. `keep` is required for Motoko canisters that use +// Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing +// main memory) otherwise. +type WasmMemoryPersistence = variant { + keep; + replace; +}; + type SystemUpgradeTarget = variant { UpgradeStation; UpgradeUpgrader; @@ -666,6 +675,13 @@ type ChangeExternalCanisterOperationInput = record { module_extra_chunks : opt WasmModuleExtraChunks; // The initial argument passed to the new wasm module. arg : opt blob; + // WASM memory persistence setting. Only applicable when `mode` is `upgrade`. + // Required as `keep` for upgrading Motoko canisters that use Enhanced + // Orthogonal Persistence; otherwise the IC clears their main memory. + wasm_memory_persistence : opt WasmMemoryPersistence; + // If `true`, the `pre_upgrade` hook is skipped. Only applicable when `mode` + // is `upgrade`. + skip_pre_upgrade : opt bool; }; type ChangeExternalCanisterOperation = record { @@ -677,6 +693,12 @@ type ChangeExternalCanisterOperation = record { module_checksum : Sha256Hash; // The checksum of the arg blob. arg_checksum : opt Sha256Hash; + // WASM memory persistence setting recorded for this upgrade, if any. + // Only meaningful when `mode` is `upgrade`. + wasm_memory_persistence : opt WasmMemoryPersistence; + // Whether the `pre_upgrade` hook was skipped. Only meaningful when `mode` + // is `upgrade`. + skip_pre_upgrade : opt bool; }; type SubnetFilter = record { diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index a27a2bf4b..b9556d29a 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -681,6 +681,14 @@ export interface CanisterExecutionAndValidationMethodPair { export type CanisterInstallMode = { 'reinstall' : null } | { 'upgrade' : null } | { 'install' : null }; +/** + * WASM memory persistence setting passed to `install_code` when the install + * mode is `upgrade`. `keep` is required for Motoko canisters that use + * Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing + * main memory) otherwise. + */ +export type WasmMemoryPersistence = { 'keep' : null } | + { 'replace' : null }; export interface CanisterMethod { /** * The canister to call. @@ -836,6 +844,16 @@ export interface ChangeExternalCanisterOperation { * The checksum of the arg blob. */ 'arg_checksum' : [] | [Sha256Hash], + /** + * WASM memory persistence setting recorded for this upgrade, if any. + * Only meaningful when `mode` is `upgrade`. + */ + 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], + /** + * Whether the `pre_upgrade` hook was skipped. Only meaningful when `mode` + * is `upgrade`. + */ + 'skip_pre_upgrade' : [] | [boolean], } export interface ChangeExternalCanisterOperationInput { /** @@ -858,6 +876,18 @@ export interface ChangeExternalCanisterOperationInput { * The wasm module to install. */ 'module' : Uint8Array | number[], + /** + * WASM memory persistence setting. Only applicable when `mode` is + * `upgrade`. Required as `keep` for upgrading Motoko canisters that use + * Enhanced Orthogonal Persistence; otherwise the IC clears their main + * memory. + */ + 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], + /** + * If `true`, the `pre_upgrade` hook is skipped. Only applicable when + * `mode` is `upgrade`. + */ + 'skip_pre_upgrade' : [] | [boolean], } /** * Type for instructions to update the address book entry's metadata. diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 14192b713..7aa4b2349 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -454,12 +454,18 @@ export const idlFactory = ({ IDL }) => { 'upgrade' : IDL.Null, 'install' : IDL.Null, }); + const WasmMemoryPersistence = IDL.Variant({ + 'keep' : IDL.Null, + 'replace' : IDL.Null, + }); const Sha256Hash = IDL.Text; const ChangeExternalCanisterOperation = IDL.Record({ 'mode' : CanisterInstallMode, 'canister_id' : IDL.Principal, 'module_checksum' : Sha256Hash, 'arg_checksum' : IDL.Opt(Sha256Hash), + 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), + 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), }); const CycleObtainStrategyInput = IDL.Variant({ 'Disabled' : IDL.Null, @@ -975,6 +981,8 @@ export const idlFactory = ({ IDL }) => { 'mode' : CanisterInstallMode, 'canister_id' : IDL.Principal, 'module' : IDL.Vec(IDL.Nat8), + 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), + 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), }); const SetDisasterRecoveryOperationInput = IDL.Record({ 'committee' : IDL.Opt(DisasterRecoveryCommittee), diff --git a/core/station/api/spec.did b/core/station/api/spec.did index c099fc892..0b3ed2854 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -578,6 +578,15 @@ type CanisterInstallMode = variant { upgrade; }; +// WASM memory persistence setting passed to `install_code` when the install +// mode is `upgrade`. `keep` is required for Motoko canisters that use +// Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing +// main memory) otherwise. +type WasmMemoryPersistence = variant { + keep; + replace; +}; + type SystemUpgradeTarget = variant { UpgradeStation; UpgradeUpgrader; @@ -666,6 +675,13 @@ type ChangeExternalCanisterOperationInput = record { module_extra_chunks : opt WasmModuleExtraChunks; // The initial argument passed to the new wasm module. arg : opt blob; + // WASM memory persistence setting. Only applicable when `mode` is `upgrade`. + // Required as `keep` for upgrading Motoko canisters that use Enhanced + // Orthogonal Persistence; otherwise the IC clears their main memory. + wasm_memory_persistence : opt WasmMemoryPersistence; + // If `true`, the `pre_upgrade` hook is skipped. Only applicable when `mode` + // is `upgrade`. + skip_pre_upgrade : opt bool; }; type ChangeExternalCanisterOperation = record { @@ -677,6 +693,12 @@ type ChangeExternalCanisterOperation = record { module_checksum : Sha256Hash; // The checksum of the arg blob. arg_checksum : opt Sha256Hash; + // WASM memory persistence setting recorded for this upgrade, if any. + // Only meaningful when `mode` is `upgrade`. + wasm_memory_persistence : opt WasmMemoryPersistence; + // Whether the `pre_upgrade` hook was skipped. Only meaningful when `mode` + // is `upgrade`. + skip_pre_upgrade : opt bool; }; type SubnetFilter = record { diff --git a/core/station/api/src/common.rs b/core/station/api/src/common.rs index f839e6461..d9a10c471 100644 --- a/core/station/api/src/common.rs +++ b/core/station/api/src/common.rs @@ -40,6 +40,14 @@ pub enum CanisterInstallMode { Upgrade = 3, } +#[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum WasmMemoryPersistence { + #[serde(rename = "keep")] + Keep, + #[serde(rename = "replace")] + Replace, +} + #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] pub struct Snapshot { pub snapshot_id: String, diff --git a/core/station/api/src/external_canister.rs b/core/station/api/src/external_canister.rs index 71c38256b..b98d753a4 100644 --- a/core/station/api/src/external_canister.rs +++ b/core/station/api/src/external_canister.rs @@ -1,7 +1,7 @@ use crate::{ AllowDTO, CanisterInstallMode, ChangeMetadataDTO, CycleObtainStrategyInput, MetadataDTO, PaginationInput, RequestPolicyRuleDTO, Sha256HashDTO, SortDirection, TimestampRfc3339, UuidDTO, - ValidationMethodResourceTargetDTO, + ValidationMethodResourceTargetDTO, WasmMemoryPersistence, }; use candid::{CandidType, Deserialize, Nat, Principal}; use orbit_essentials::cmc::SubnetSelection; @@ -131,6 +131,13 @@ pub struct ChangeExternalCanisterOperationInput { pub module_extra_chunks: Option, #[serde(deserialize_with = "orbit_essentials::deserialize::deserialize_option_blob")] pub arg: Option>, + /// WASM memory persistence setting. Only applicable when `mode` is `upgrade`. + /// Required as `keep` for upgrading Motoko canisters that use Enhanced + /// Orthogonal Persistence; otherwise the IC clears their main memory. + pub wasm_memory_persistence: Option, + /// If `true`, the `pre_upgrade` hook is skipped. Only applicable when + /// `mode` is `upgrade`. + pub skip_pre_upgrade: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] @@ -139,6 +146,8 @@ pub struct ChangeExternalCanisterOperationDTO { pub mode: CanisterInstallMode, pub module_checksum: Sha256HashDTO, pub arg_checksum: Option, + pub wasm_memory_persistence: Option, + pub skip_pre_upgrade: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] diff --git a/core/station/impl/src/mappers/request_operation.rs b/core/station/impl/src/mappers/request_operation.rs index 5b2d3141a..7e32b3e7f 100644 --- a/core/station/impl/src/mappers/request_operation.rs +++ b/core/station/impl/src/mappers/request_operation.rs @@ -51,7 +51,7 @@ use crate::{ SnapshotExternalCanisterOperation, SnapshotExternalCanisterOperationInput, SystemRestoreOperation, SystemRestoreOperationInput, SystemRestoreTarget, SystemUpgradeOperation, SystemUpgradeOperationInput, SystemUpgradeTarget, - TransferOperation, User, WasmModuleExtraChunks, + TransferOperation, User, WasmMemoryPersistence, WasmModuleExtraChunks, }, repositories::{ AccountRepository, AddressBookRepository, AssetRepository, NamedRuleRepository, @@ -526,7 +526,7 @@ impl From for CanisterInstallMode { CanisterInstallMode::Reinstall(CanisterReinstallModeArgs {}) } station_api::CanisterInstallMode::Upgrade => { - CanisterInstallMode::Upgrade(CanisterUpgradeModeArgs {}) + CanisterInstallMode::Upgrade(CanisterUpgradeModeArgs::default()) } } } @@ -552,9 +552,25 @@ impl From for station_api::CanisterInstallMode { CanisterInstallMode::Reinstall(CanisterReinstallModeArgs {}) => { station_api::CanisterInstallMode::Reinstall } - CanisterInstallMode::Upgrade(CanisterUpgradeModeArgs {}) => { - station_api::CanisterInstallMode::Upgrade - } + CanisterInstallMode::Upgrade(_) => station_api::CanisterInstallMode::Upgrade, + } + } +} + +impl From for WasmMemoryPersistence { + fn from(value: station_api::WasmMemoryPersistence) -> Self { + match value { + station_api::WasmMemoryPersistence::Keep => WasmMemoryPersistence::Keep, + station_api::WasmMemoryPersistence::Replace => WasmMemoryPersistence::Replace, + } + } +} + +impl From for station_api::WasmMemoryPersistence { + fn from(value: WasmMemoryPersistence) -> Self { + match value { + WasmMemoryPersistence::Keep => station_api::WasmMemoryPersistence::Keep, + WasmMemoryPersistence::Replace => station_api::WasmMemoryPersistence::Replace, } } } @@ -586,12 +602,15 @@ impl From fn from( input: ChangeExternalCanisterOperationInput, ) -> station_api::ChangeExternalCanisterOperationInput { + let (wasm_memory_persistence, skip_pre_upgrade) = upgrade_flags_to_api(&input.mode); station_api::ChangeExternalCanisterOperationInput { canister_id: input.canister_id, mode: input.mode.into(), module: input.module, module_extra_chunks: input.module_extra_chunks.map(|c| c.into()), arg: input.arg, + wasm_memory_persistence, + skip_pre_upgrade, } } } @@ -602,9 +621,14 @@ impl From fn from( input: station_api::ChangeExternalCanisterOperationInput, ) -> ChangeExternalCanisterOperationInput { + let mut mode: CanisterInstallMode = input.mode.into(); + if let CanisterInstallMode::Upgrade(ref mut args) = mode { + args.wasm_memory_persistence = input.wasm_memory_persistence.map(Into::into); + args.skip_pre_upgrade = input.skip_pre_upgrade; + } ChangeExternalCanisterOperationInput { canister_id: input.canister_id, - mode: input.mode.into(), + mode, module: input.module, module_extra_chunks: input.module_extra_chunks.map(|c| c.into()), arg: input.arg, @@ -614,15 +638,31 @@ impl From impl From for ChangeExternalCanisterOperationDTO { fn from(operation: ChangeExternalCanisterOperation) -> ChangeExternalCanisterOperationDTO { + let (wasm_memory_persistence, skip_pre_upgrade) = + upgrade_flags_to_api(&operation.input.mode); ChangeExternalCanisterOperationDTO { canister_id: operation.input.canister_id, mode: operation.input.mode.into(), module_checksum: hex::encode(operation.module_checksum), arg_checksum: operation.arg_checksum.map(hex::encode), + wasm_memory_persistence, + skip_pre_upgrade, } } } +fn upgrade_flags_to_api( + mode: &CanisterInstallMode, +) -> (Option, Option) { + match mode { + CanisterInstallMode::Upgrade(args) => ( + args.wasm_memory_persistence.map(Into::into), + args.skip_pre_upgrade, + ), + _ => (None, None), + } +} + impl From for station_api::ConfigureExternalCanisterOperationDTO { @@ -2411,3 +2451,111 @@ impl RequestOperation { } } } + +#[cfg(test)] +mod tests { + use super::*; + use candid::Principal; + use orbit_essentials::cdk::api::management_canister::main::{self as mgmt}; + + fn upgrade_input( + wasm_memory_persistence: Option, + skip_pre_upgrade: Option, + ) -> station_api::ChangeExternalCanisterOperationInput { + station_api::ChangeExternalCanisterOperationInput { + canister_id: Principal::management_canister(), + mode: station_api::CanisterInstallMode::Upgrade, + module: vec![1, 2, 3], + module_extra_chunks: None, + arg: None, + wasm_memory_persistence, + skip_pre_upgrade, + } + } + + #[test] + fn upgrade_flags_roundtrip_through_mapper() { + let api_input = upgrade_input(Some(station_api::WasmMemoryPersistence::Keep), Some(true)); + let internal: ChangeExternalCanisterOperationInput = api_input.into(); + + let CanisterInstallMode::Upgrade(args) = &internal.mode else { + panic!("expected upgrade mode"); + }; + assert_eq!( + args.wasm_memory_persistence, + Some(WasmMemoryPersistence::Keep) + ); + assert_eq!(args.skip_pre_upgrade, Some(true)); + + let api_back: station_api::ChangeExternalCanisterOperationInput = internal.into(); + assert!(matches!( + api_back.wasm_memory_persistence, + Some(station_api::WasmMemoryPersistence::Keep) + )); + assert_eq!(api_back.skip_pre_upgrade, Some(true)); + } + + #[test] + fn unset_upgrade_flags_map_to_unit_upgrade() { + let api_input = upgrade_input(None, None); + let internal: ChangeExternalCanisterOperationInput = api_input.into(); + + let mgmt_mode: mgmt::CanisterInstallMode = internal.mode.into(); + assert!(matches!( + mgmt_mode, + mgmt::CanisterInstallMode::Upgrade(None) + )); + } + + #[test] + fn set_upgrade_flags_populate_mgmt_upgrade_options() { + let api_input = upgrade_input(Some(station_api::WasmMemoryPersistence::Keep), None); + let internal: ChangeExternalCanisterOperationInput = api_input.into(); + + let mgmt_mode: mgmt::CanisterInstallMode = internal.mode.into(); + match mgmt_mode { + mgmt::CanisterInstallMode::Upgrade(Some(flags)) => { + assert!(matches!( + flags.wasm_memory_persistence, + Some(mgmt::WasmPersistenceMode::Keep) + )); + assert_eq!(flags.skip_pre_upgrade, None); + } + other => panic!("expected upgrade with flags, got {other:?}"), + } + } + + #[test] + fn install_and_reinstall_ignore_upgrade_flags() { + for mode in [ + station_api::CanisterInstallMode::Install, + station_api::CanisterInstallMode::Reinstall, + ] { + let api_input = station_api::ChangeExternalCanisterOperationInput { + canister_id: Principal::management_canister(), + mode: mode.clone(), + module: vec![], + module_extra_chunks: None, + arg: None, + wasm_memory_persistence: Some(station_api::WasmMemoryPersistence::Keep), + skip_pre_upgrade: Some(true), + }; + let internal: ChangeExternalCanisterOperationInput = api_input.into(); + let api_back: station_api::ChangeExternalCanisterOperationInput = internal.into(); + assert!(api_back.wasm_memory_persistence.is_none()); + assert!(api_back.skip_pre_upgrade.is_none()); + } + } + + #[test] + fn legacy_upgrade_args_deserialize_with_defaults() { + // Historical on-disk shape: CanisterUpgradeModeArgs was a unit struct + // with no fields. Verify that records written before this change + // (an empty CBOR map) still deserialize into the new struct, with + // both flag fields defaulting to None. + let empty_map_cbor = vec![0xa0u8]; // CBOR encoding of an empty map. + let args: CanisterUpgradeModeArgs = + serde_cbor::from_slice(&empty_map_cbor).expect("legacy bytes must deserialize"); + assert_eq!(args, CanisterUpgradeModeArgs::default()); + } +} diff --git a/core/station/impl/src/models/request_operation.rs b/core/station/impl/src/models/request_operation.rs index ca1eaf402..a241d46e7 100644 --- a/core/station/impl/src/models/request_operation.rs +++ b/core/station/impl/src/models/request_operation.rs @@ -460,8 +460,29 @@ pub struct CanisterInstallModeArgs {} pub struct CanisterReinstallModeArgs {} #[storable] -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct CanisterUpgradeModeArgs {} +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct CanisterUpgradeModeArgs { + #[serde(default)] + pub wasm_memory_persistence: Option, + #[serde(default)] + pub skip_pre_upgrade: Option, +} + +#[storable] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum WasmMemoryPersistence { + Keep, + Replace, +} + +impl From for mgmt::WasmPersistenceMode { + fn from(value: WasmMemoryPersistence) -> Self { + match value { + WasmMemoryPersistence::Keep => mgmt::WasmPersistenceMode::Keep, + WasmMemoryPersistence::Replace => mgmt::WasmPersistenceMode::Replace, + } + } +} #[storable] #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -476,7 +497,18 @@ impl From for mgmt::CanisterInstallMode { match mode { CanisterInstallMode::Install(_) => mgmt::CanisterInstallMode::Install, CanisterInstallMode::Reinstall(_) => mgmt::CanisterInstallMode::Reinstall, - CanisterInstallMode::Upgrade(_) => mgmt::CanisterInstallMode::Upgrade(None), + CanisterInstallMode::Upgrade(args) => { + let flags = + if args.wasm_memory_persistence.is_some() || args.skip_pre_upgrade.is_some() { + Some(mgmt::UpgradeFlags { + skip_pre_upgrade: args.skip_pre_upgrade, + wasm_memory_persistence: args.wasm_memory_persistence.map(Into::into), + }) + } else { + None + }; + mgmt::CanisterInstallMode::Upgrade(flags) + } } } } @@ -1591,7 +1623,7 @@ mod test { input: crate::models::ChangeExternalCanisterOperationInput { canister_id: upgrader_id, mode: crate::models::CanisterInstallMode::Upgrade( - crate::models::CanisterUpgradeModeArgs {}, + crate::models::CanisterUpgradeModeArgs::default(), ), module: vec![], module_extra_chunks: None, diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 755ada0e4..60062ac79 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -238,7 +238,7 @@ impl SystemService { .change_canister_service .install_canister( upgrader_canister_id, - CanisterInstallMode::Upgrade(CanisterUpgradeModeArgs {}), + CanisterInstallMode::Upgrade(CanisterUpgradeModeArgs::default()), module, module_extra_chunks, arg, diff --git a/tests/integration/src/external_canister_tests.rs b/tests/integration/src/external_canister_tests.rs index aff3dc065..1e74f0b6d 100644 --- a/tests/integration/src/external_canister_tests.rs +++ b/tests/integration/src/external_canister_tests.rs @@ -77,6 +77,8 @@ fn successful_four_eyes_upgrade() { module: base_chunk, module_extra_chunks: Some(module_extra_chunks), arg: None, + wasm_memory_persistence: None, + skip_pre_upgrade: None, }); let change_canister_operation_request = submit_request( @@ -222,6 +224,8 @@ fn upgrade_reinstall_list_test() { module: base_chunk.clone(), module_extra_chunks: Some(module_extra_chunks.clone()), arg: None, + wasm_memory_persistence: None, + skip_pre_upgrade: None, }); execute_request( &env, @@ -247,6 +251,8 @@ fn upgrade_reinstall_list_test() { module: base_chunk, module_extra_chunks: Some(module_extra_chunks), arg: None, + wasm_memory_persistence: None, + skip_pre_upgrade: None, }); execute_request( &env, diff --git a/tools/dfx-orbit/src/canister/install.rs b/tools/dfx-orbit/src/canister/install.rs index 17c709c14..e4dd6f01b 100644 --- a/tools/dfx-orbit/src/canister/install.rs +++ b/tools/dfx-orbit/src/canister/install.rs @@ -103,6 +103,8 @@ impl RequestCanisterInstallArgs { module, module_extra_chunks, arg, + wasm_memory_persistence: None, + skip_pre_upgrade: None, }; Ok(RequestOperationInput::ChangeExternalCanister(operation)) } From 80af1dbba36445f5e63f26b2b62257f86f463fb9 Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Fri, 22 May 2026 23:13:23 +0200 Subject: [PATCH 2/6] chore(wallet): regenerate station.did.{js,d.ts} via dfx generate Field ordering produced by `dfx generate station` differs from my hand-edited bindings; matching the generator output so the CI `validate-did-bindings` check passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/generated/station/station.did.d.ts | 57 +++++++++---------- .../src/generated/station/station.did.js | 16 +++--- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index b9556d29a..b414fb947 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -681,14 +681,6 @@ export interface CanisterExecutionAndValidationMethodPair { export type CanisterInstallMode = { 'reinstall' : null } | { 'upgrade' : null } | { 'install' : null }; -/** - * WASM memory persistence setting passed to `install_code` when the install - * mode is `upgrade`. `keep` is required for Motoko canisters that use - * Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing - * main memory) otherwise. - */ -export type WasmMemoryPersistence = { 'keep' : null } | - { 'replace' : null }; export interface CanisterMethod { /** * The canister to call. @@ -828,6 +820,11 @@ export type ChangeExternalCanisterMetadata = { 'ReplaceAllBy' : Array }; export interface ChangeExternalCanisterOperation { + /** + * WASM memory persistence setting recorded for this upgrade, if any. + * Only meaningful when `mode` is `upgrade`. + */ + 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], /** * The canister installation mode. */ @@ -840,20 +837,15 @@ export interface ChangeExternalCanisterOperation { * The checksum of the wasm module. */ 'module_checksum' : Sha256Hash, - /** - * The checksum of the arg blob. - */ - 'arg_checksum' : [] | [Sha256Hash], - /** - * WASM memory persistence setting recorded for this upgrade, if any. - * Only meaningful when `mode` is `upgrade`. - */ - 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], /** * Whether the `pre_upgrade` hook was skipped. Only meaningful when `mode` * is `upgrade`. */ 'skip_pre_upgrade' : [] | [boolean], + /** + * The checksum of the arg blob. + */ + 'arg_checksum' : [] | [Sha256Hash], } export interface ChangeExternalCanisterOperationInput { /** @@ -864,6 +856,12 @@ export interface ChangeExternalCanisterOperationInput { * Additional wasm module chunks to append to the wasm module. */ 'module_extra_chunks' : [] | [WasmModuleExtraChunks], + /** + * WASM memory persistence setting. Only applicable when `mode` is `upgrade`. + * Required as `keep` for upgrading Motoko canisters that use Enhanced + * Orthogonal Persistence; otherwise the IC clears their main memory. + */ + 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], /** * The canister installation mode. */ @@ -873,21 +871,14 @@ export interface ChangeExternalCanisterOperationInput { */ 'canister_id' : Principal, /** - * The wasm module to install. - */ - 'module' : Uint8Array | number[], - /** - * WASM memory persistence setting. Only applicable when `mode` is - * `upgrade`. Required as `keep` for upgrading Motoko canisters that use - * Enhanced Orthogonal Persistence; otherwise the IC clears their main - * memory. + * If `true`, the `pre_upgrade` hook is skipped. Only applicable when `mode` + * is `upgrade`. */ - 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], + 'skip_pre_upgrade' : [] | [boolean], /** - * If `true`, the `pre_upgrade` hook is skipped. Only applicable when - * `mode` is `upgrade`. + * The wasm module to install. */ - 'skip_pre_upgrade' : [] | [boolean], + 'module' : Uint8Array | number[], } /** * Type for instructions to update the address book entry's metadata. @@ -5690,6 +5681,14 @@ export type UserStatus = { */ export type ValidationMethodResourceTarget = { 'No' : null } | { 'ValidationMethod' : CanisterMethod }; +/** + * WASM memory persistence setting passed to `install_code` when the install + * mode is `upgrade`. `keep` is required for Motoko canisters that use + * Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing + * main memory) otherwise. + */ +export type WasmMemoryPersistence = { 'keep' : null } | + { 'replace' : null }; export interface WasmModuleExtraChunks { /** * The hash of the assembled wasm module. diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 7aa4b2349..352800187 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -449,23 +449,23 @@ export const idlFactory = ({ IDL }) => { 'canister_id' : IDL.Principal, }); const ConfigureExternalCanisterOperation = ConfigureExternalCanisterOperationInput; + const WasmMemoryPersistence = IDL.Variant({ + 'keep' : IDL.Null, + 'replace' : IDL.Null, + }); const CanisterInstallMode = IDL.Variant({ 'reinstall' : IDL.Null, 'upgrade' : IDL.Null, 'install' : IDL.Null, }); - const WasmMemoryPersistence = IDL.Variant({ - 'keep' : IDL.Null, - 'replace' : IDL.Null, - }); const Sha256Hash = IDL.Text; const ChangeExternalCanisterOperation = IDL.Record({ + 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), 'mode' : CanisterInstallMode, 'canister_id' : IDL.Principal, 'module_checksum' : Sha256Hash, - 'arg_checksum' : IDL.Opt(Sha256Hash), - 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), + 'arg_checksum' : IDL.Opt(Sha256Hash), }); const CycleObtainStrategyInput = IDL.Variant({ 'Disabled' : IDL.Null, @@ -978,11 +978,11 @@ export const idlFactory = ({ IDL }) => { const ChangeExternalCanisterOperationInput = IDL.Record({ 'arg' : IDL.Opt(IDL.Vec(IDL.Nat8)), 'module_extra_chunks' : IDL.Opt(WasmModuleExtraChunks), + 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), 'mode' : CanisterInstallMode, 'canister_id' : IDL.Principal, - 'module' : IDL.Vec(IDL.Nat8), - 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), + 'module' : IDL.Vec(IDL.Nat8), }); const SetDisasterRecoveryOperationInput = IDL.Record({ 'committee' : IDL.Opt(DisasterRecoveryCommittee), From a46254c48239741b81e95969c334c384b12c1d1f Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Mon, 25 May 2026 15:03:12 +0200 Subject: [PATCH 3/6] fix(wallet): pass new upgrade flags when building change-canister request CanisterInstallDialog wasn't updated for the new `wasm_memory_persistence` / `skip_pre_upgrade` fields on ChangeExternalCanisterOperationInput; passing empty `[]` (None) for both, matching the existing integration tests and dfx-orbit CLI. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/external-canisters/CanisterInstallDialog.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue b/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue index 5695d4c92..c597626f1 100644 --- a/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue +++ b/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue @@ -165,6 +165,8 @@ const submit = async (input: CanisterInstallModel) => { module: assertAndReturn(input.wasmModule, 'wasm module required'), arg: input.wasmInstallArg !== undefined ? [input.wasmInstallArg] : [], module_extra_chunks: [], + wasm_memory_persistence: [], + skip_pre_upgrade: [], }, { comment: From 25514cac72e6065fdc06fdb5e1610f0955ae4f25 Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Tue, 26 May 2026 09:28:50 +0200 Subject: [PATCH 4/6] refactor(station): move upgrade flags into CanisterInstallMode::Upgrade Per reviewer feedback, the upgrade flags `wasm_memory_persistence` and `skip_pre_upgrade` now live inside the `upgrade` variant of `CanisterInstallMode` (as `opt CanisterUpgradeOptionsInput`), matching the IC management canister's shape. The flat fields previously added to `ChangeExternalCanisterOperationInput` and `ChangeExternalCanisterOperation` are removed. This is a wire-incompatible Candid change to `CanisterInstallMode` (the `upgrade` tag goes from unit to `opt CanisterUpgradeOptionsInput`) but the API surface is otherwise unchanged. The internal storage shape (`CanisterUpgradeModeArgs` with `#[serde(default)]` fields) is unchanged, so legacy stored requests still deserialize. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CanisterInstallDialog.vue | 4 +- .../inputs/CanisterInstallModeSelect.vue | 2 +- apps/wallet/src/generated/station/station.did | 27 ++-- .../src/generated/station/station.did.d.ts | 44 +++---- .../src/generated/station/station.did.js | 10 +- core/station/api/spec.did | 27 ++-- core/station/api/src/common.rs | 20 ++- core/station/api/src/external_canister.rs | 11 +- .../impl/src/mappers/request_operation.rs | 119 ++++++++++-------- .../src/external_canister_tests.rs | 10 +- tools/dfx-orbit/src/canister/install.rs | 8 +- 11 files changed, 146 insertions(+), 136 deletions(-) diff --git a/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue b/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue index c597626f1..c5f6155f6 100644 --- a/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue +++ b/apps/wallet/src/components/external-canisters/CanisterInstallDialog.vue @@ -130,7 +130,7 @@ const dialogTitle = computed(() => props.title || i18n.t('external_canisters.ins const initialModel = (): CanisterInstallModel => { const model: CanisterInstallModel = {}; - model.mode = props.canisterModuleHash ? { upgrade: null } : { install: null }; + model.mode = props.canisterModuleHash ? { upgrade: [] } : { install: null }; model.canisterId = props.canisterId ? Principal.fromUint8Array(props.canisterId.toUint8Array()) : undefined; @@ -165,8 +165,6 @@ const submit = async (input: CanisterInstallModel) => { module: assertAndReturn(input.wasmModule, 'wasm module required'), arg: input.wasmInstallArg !== undefined ? [input.wasmInstallArg] : [], module_extra_chunks: [], - wasm_memory_persistence: [], - skip_pre_upgrade: [], }, { comment: diff --git a/apps/wallet/src/components/inputs/CanisterInstallModeSelect.vue b/apps/wallet/src/components/inputs/CanisterInstallModeSelect.vue index e8e501deb..07a4f27d0 100644 --- a/apps/wallet/src/components/inputs/CanisterInstallModeSelect.vue +++ b/apps/wallet/src/components/inputs/CanisterInstallModeSelect.vue @@ -58,6 +58,6 @@ const modes = computed< >(() => [ { title: i18n.t('external_canisters.install_mode.install'), value: { install: null } }, { title: i18n.t('external_canisters.install_mode.reinstall'), value: { reinstall: null } }, - { title: i18n.t('external_canisters.install_mode.upgrade'), value: { upgrade: null } }, + { title: i18n.t('external_canisters.install_mode.upgrade'), value: { upgrade: [] } }, ]); diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 0b3ed2854..7db17d076 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -575,7 +575,19 @@ type RemoveUserGroupOperation = record { type CanisterInstallMode = variant { install; reinstall; - upgrade; + // Upgrade an existing canister. Carries optional flags that mirror the + // IC management canister's `CanisterUpgradeOptions`. + upgrade : opt CanisterUpgradeOptionsInput; +}; + +// Optional upgrade flags forwarded to the IC management canister's +// `install_code` method when `mode` is `upgrade`. +type CanisterUpgradeOptionsInput = record { + // Required as `keep` for upgrading Motoko canisters that use Enhanced + // Orthogonal Persistence; otherwise the IC clears their main memory. + wasm_memory_persistence : opt WasmMemoryPersistence; + // If `true`, the `pre_upgrade` hook is skipped during the canister upgrade. + skip_pre_upgrade : opt bool; }; // WASM memory persistence setting passed to `install_code` when the install @@ -675,13 +687,6 @@ type ChangeExternalCanisterOperationInput = record { module_extra_chunks : opt WasmModuleExtraChunks; // The initial argument passed to the new wasm module. arg : opt blob; - // WASM memory persistence setting. Only applicable when `mode` is `upgrade`. - // Required as `keep` for upgrading Motoko canisters that use Enhanced - // Orthogonal Persistence; otherwise the IC clears their main memory. - wasm_memory_persistence : opt WasmMemoryPersistence; - // If `true`, the `pre_upgrade` hook is skipped. Only applicable when `mode` - // is `upgrade`. - skip_pre_upgrade : opt bool; }; type ChangeExternalCanisterOperation = record { @@ -693,12 +698,6 @@ type ChangeExternalCanisterOperation = record { module_checksum : Sha256Hash; // The checksum of the arg blob. arg_checksum : opt Sha256Hash; - // WASM memory persistence setting recorded for this upgrade, if any. - // Only meaningful when `mode` is `upgrade`. - wasm_memory_persistence : opt WasmMemoryPersistence; - // Whether the `pre_upgrade` hook was skipped. Only meaningful when `mode` - // is `upgrade`. - skip_pre_upgrade : opt bool; }; type SubnetFilter = record { diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index b414fb947..a241d1b4d 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -679,7 +679,13 @@ export interface CanisterExecutionAndValidationMethodPair { 'validation_method' : ValidationMethodResourceTarget, } export type CanisterInstallMode = { 'reinstall' : null } | - { 'upgrade' : null } | + { + /** + * Upgrade an existing canister. Carries optional flags that mirror the + * IC management canister's `CanisterUpgradeOptions`. + */ + 'upgrade' : [] | [CanisterUpgradeOptionsInput] + } | { 'install' : null }; export interface CanisterMethod { /** @@ -719,6 +725,21 @@ export interface CanisterStatusResponse { 'module_hash' : [] | [Uint8Array | number[]], 'reserved_cycles' : bigint, } +/** + * Optional upgrade flags forwarded to the IC management canister's + * `install_code` method when `mode` is `upgrade`. + */ +export interface CanisterUpgradeOptionsInput { + /** + * Required as `keep` for upgrading Motoko canisters that use Enhanced + * Orthogonal Persistence; otherwise the IC clears their main memory. + */ + 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], + /** + * If `true`, the `pre_upgrade` hook is skipped during the canister upgrade. + */ + 'skip_pre_upgrade' : [] | [boolean], +} /** * A record type that is used to show the current capabilities of the station. */ @@ -820,11 +841,6 @@ export type ChangeExternalCanisterMetadata = { 'ReplaceAllBy' : Array }; export interface ChangeExternalCanisterOperation { - /** - * WASM memory persistence setting recorded for this upgrade, if any. - * Only meaningful when `mode` is `upgrade`. - */ - 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], /** * The canister installation mode. */ @@ -837,11 +853,6 @@ export interface ChangeExternalCanisterOperation { * The checksum of the wasm module. */ 'module_checksum' : Sha256Hash, - /** - * Whether the `pre_upgrade` hook was skipped. Only meaningful when `mode` - * is `upgrade`. - */ - 'skip_pre_upgrade' : [] | [boolean], /** * The checksum of the arg blob. */ @@ -856,12 +867,6 @@ export interface ChangeExternalCanisterOperationInput { * Additional wasm module chunks to append to the wasm module. */ 'module_extra_chunks' : [] | [WasmModuleExtraChunks], - /** - * WASM memory persistence setting. Only applicable when `mode` is `upgrade`. - * Required as `keep` for upgrading Motoko canisters that use Enhanced - * Orthogonal Persistence; otherwise the IC clears their main memory. - */ - 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], /** * The canister installation mode. */ @@ -870,11 +875,6 @@ export interface ChangeExternalCanisterOperationInput { * The canister to install. */ 'canister_id' : Principal, - /** - * If `true`, the `pre_upgrade` hook is skipped. Only applicable when `mode` - * is `upgrade`. - */ - 'skip_pre_upgrade' : [] | [boolean], /** * The wasm module to install. */ diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 352800187..2b4777633 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -453,18 +453,20 @@ export const idlFactory = ({ IDL }) => { 'keep' : IDL.Null, 'replace' : IDL.Null, }); + const CanisterUpgradeOptionsInput = IDL.Record({ + 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), + 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), + }); const CanisterInstallMode = IDL.Variant({ 'reinstall' : IDL.Null, - 'upgrade' : IDL.Null, + 'upgrade' : IDL.Opt(CanisterUpgradeOptionsInput), 'install' : IDL.Null, }); const Sha256Hash = IDL.Text; const ChangeExternalCanisterOperation = IDL.Record({ - 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), 'mode' : CanisterInstallMode, 'canister_id' : IDL.Principal, 'module_checksum' : Sha256Hash, - 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), 'arg_checksum' : IDL.Opt(Sha256Hash), }); const CycleObtainStrategyInput = IDL.Variant({ @@ -978,10 +980,8 @@ export const idlFactory = ({ IDL }) => { const ChangeExternalCanisterOperationInput = IDL.Record({ 'arg' : IDL.Opt(IDL.Vec(IDL.Nat8)), 'module_extra_chunks' : IDL.Opt(WasmModuleExtraChunks), - 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), 'mode' : CanisterInstallMode, 'canister_id' : IDL.Principal, - 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), 'module' : IDL.Vec(IDL.Nat8), }); const SetDisasterRecoveryOperationInput = IDL.Record({ diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 0b3ed2854..7db17d076 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -575,7 +575,19 @@ type RemoveUserGroupOperation = record { type CanisterInstallMode = variant { install; reinstall; - upgrade; + // Upgrade an existing canister. Carries optional flags that mirror the + // IC management canister's `CanisterUpgradeOptions`. + upgrade : opt CanisterUpgradeOptionsInput; +}; + +// Optional upgrade flags forwarded to the IC management canister's +// `install_code` method when `mode` is `upgrade`. +type CanisterUpgradeOptionsInput = record { + // Required as `keep` for upgrading Motoko canisters that use Enhanced + // Orthogonal Persistence; otherwise the IC clears their main memory. + wasm_memory_persistence : opt WasmMemoryPersistence; + // If `true`, the `pre_upgrade` hook is skipped during the canister upgrade. + skip_pre_upgrade : opt bool; }; // WASM memory persistence setting passed to `install_code` when the install @@ -675,13 +687,6 @@ type ChangeExternalCanisterOperationInput = record { module_extra_chunks : opt WasmModuleExtraChunks; // The initial argument passed to the new wasm module. arg : opt blob; - // WASM memory persistence setting. Only applicable when `mode` is `upgrade`. - // Required as `keep` for upgrading Motoko canisters that use Enhanced - // Orthogonal Persistence; otherwise the IC clears their main memory. - wasm_memory_persistence : opt WasmMemoryPersistence; - // If `true`, the `pre_upgrade` hook is skipped. Only applicable when `mode` - // is `upgrade`. - skip_pre_upgrade : opt bool; }; type ChangeExternalCanisterOperation = record { @@ -693,12 +698,6 @@ type ChangeExternalCanisterOperation = record { module_checksum : Sha256Hash; // The checksum of the arg blob. arg_checksum : opt Sha256Hash; - // WASM memory persistence setting recorded for this upgrade, if any. - // Only meaningful when `mode` is `upgrade`. - wasm_memory_persistence : opt WasmMemoryPersistence; - // Whether the `pre_upgrade` hook was skipped. Only meaningful when `mode` - // is `upgrade`. - skip_pre_upgrade : opt bool; }; type SubnetFilter = record { diff --git a/core/station/api/src/common.rs b/core/station/api/src/common.rs index d9a10c471..831725fda 100644 --- a/core/station/api/src/common.rs +++ b/core/station/api/src/common.rs @@ -33,11 +33,25 @@ pub enum SortDirection { #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] pub enum CanisterInstallMode { #[serde(rename = "install")] - Install = 1, + Install, #[serde(rename = "reinstall")] - Reinstall = 2, + Reinstall, + /// Upgrade an existing canister. Carries optional flags that mirror the + /// IC management canister's `CanisterUpgradeOptions`. #[serde(rename = "upgrade")] - Upgrade = 3, + Upgrade(Option), +} + +/// Optional upgrade flags forwarded to the IC management canister's +/// `install_code` method when `mode` is `upgrade`. +#[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct CanisterUpgradeOptionsInput { + /// Required as `keep` for upgrading Motoko canisters that use Enhanced + /// Orthogonal Persistence; otherwise the IC clears their main memory. + pub wasm_memory_persistence: Option, + /// If `true`, the `pre_upgrade` hook is skipped during the canister + /// upgrade. + pub skip_pre_upgrade: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] diff --git a/core/station/api/src/external_canister.rs b/core/station/api/src/external_canister.rs index b98d753a4..71c38256b 100644 --- a/core/station/api/src/external_canister.rs +++ b/core/station/api/src/external_canister.rs @@ -1,7 +1,7 @@ use crate::{ AllowDTO, CanisterInstallMode, ChangeMetadataDTO, CycleObtainStrategyInput, MetadataDTO, PaginationInput, RequestPolicyRuleDTO, Sha256HashDTO, SortDirection, TimestampRfc3339, UuidDTO, - ValidationMethodResourceTargetDTO, WasmMemoryPersistence, + ValidationMethodResourceTargetDTO, }; use candid::{CandidType, Deserialize, Nat, Principal}; use orbit_essentials::cmc::SubnetSelection; @@ -131,13 +131,6 @@ pub struct ChangeExternalCanisterOperationInput { pub module_extra_chunks: Option, #[serde(deserialize_with = "orbit_essentials::deserialize::deserialize_option_blob")] pub arg: Option>, - /// WASM memory persistence setting. Only applicable when `mode` is `upgrade`. - /// Required as `keep` for upgrading Motoko canisters that use Enhanced - /// Orthogonal Persistence; otherwise the IC clears their main memory. - pub wasm_memory_persistence: Option, - /// If `true`, the `pre_upgrade` hook is skipped. Only applicable when - /// `mode` is `upgrade`. - pub skip_pre_upgrade: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] @@ -146,8 +139,6 @@ pub struct ChangeExternalCanisterOperationDTO { pub mode: CanisterInstallMode, pub module_checksum: Sha256HashDTO, pub arg_checksum: Option, - pub wasm_memory_persistence: Option, - pub skip_pre_upgrade: Option, } #[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] diff --git a/core/station/impl/src/mappers/request_operation.rs b/core/station/impl/src/mappers/request_operation.rs index 7e32b3e7f..8212a1b09 100644 --- a/core/station/impl/src/mappers/request_operation.rs +++ b/core/station/impl/src/mappers/request_operation.rs @@ -525,9 +525,18 @@ impl From for CanisterInstallMode { station_api::CanisterInstallMode::Reinstall => { CanisterInstallMode::Reinstall(CanisterReinstallModeArgs {}) } - station_api::CanisterInstallMode::Upgrade => { - CanisterInstallMode::Upgrade(CanisterUpgradeModeArgs::default()) - } + station_api::CanisterInstallMode::Upgrade(opts) => CanisterInstallMode::Upgrade( + opts.map(CanisterUpgradeModeArgs::from).unwrap_or_default(), + ), + } + } +} + +impl From for CanisterUpgradeModeArgs { + fn from(opts: station_api::CanisterUpgradeOptionsInput) -> Self { + CanisterUpgradeModeArgs { + wasm_memory_persistence: opts.wasm_memory_persistence.map(Into::into), + skip_pre_upgrade: opts.skip_pre_upgrade, } } } @@ -552,7 +561,24 @@ impl From for station_api::CanisterInstallMode { CanisterInstallMode::Reinstall(CanisterReinstallModeArgs {}) => { station_api::CanisterInstallMode::Reinstall } - CanisterInstallMode::Upgrade(_) => station_api::CanisterInstallMode::Upgrade, + CanisterInstallMode::Upgrade(args) => { + let opts = + if args.wasm_memory_persistence.is_some() || args.skip_pre_upgrade.is_some() { + Some(args.into()) + } else { + None + }; + station_api::CanisterInstallMode::Upgrade(opts) + } + } + } +} + +impl From for station_api::CanisterUpgradeOptionsInput { + fn from(args: CanisterUpgradeModeArgs) -> Self { + station_api::CanisterUpgradeOptionsInput { + wasm_memory_persistence: args.wasm_memory_persistence.map(Into::into), + skip_pre_upgrade: args.skip_pre_upgrade, } } } @@ -602,15 +628,12 @@ impl From fn from( input: ChangeExternalCanisterOperationInput, ) -> station_api::ChangeExternalCanisterOperationInput { - let (wasm_memory_persistence, skip_pre_upgrade) = upgrade_flags_to_api(&input.mode); station_api::ChangeExternalCanisterOperationInput { canister_id: input.canister_id, mode: input.mode.into(), module: input.module, module_extra_chunks: input.module_extra_chunks.map(|c| c.into()), arg: input.arg, - wasm_memory_persistence, - skip_pre_upgrade, } } } @@ -621,14 +644,9 @@ impl From fn from( input: station_api::ChangeExternalCanisterOperationInput, ) -> ChangeExternalCanisterOperationInput { - let mut mode: CanisterInstallMode = input.mode.into(); - if let CanisterInstallMode::Upgrade(ref mut args) = mode { - args.wasm_memory_persistence = input.wasm_memory_persistence.map(Into::into); - args.skip_pre_upgrade = input.skip_pre_upgrade; - } ChangeExternalCanisterOperationInput { canister_id: input.canister_id, - mode, + mode: input.mode.into(), module: input.module, module_extra_chunks: input.module_extra_chunks.map(|c| c.into()), arg: input.arg, @@ -638,31 +656,15 @@ impl From impl From for ChangeExternalCanisterOperationDTO { fn from(operation: ChangeExternalCanisterOperation) -> ChangeExternalCanisterOperationDTO { - let (wasm_memory_persistence, skip_pre_upgrade) = - upgrade_flags_to_api(&operation.input.mode); ChangeExternalCanisterOperationDTO { canister_id: operation.input.canister_id, mode: operation.input.mode.into(), module_checksum: hex::encode(operation.module_checksum), arg_checksum: operation.arg_checksum.map(hex::encode), - wasm_memory_persistence, - skip_pre_upgrade, } } } -fn upgrade_flags_to_api( - mode: &CanisterInstallMode, -) -> (Option, Option) { - match mode { - CanisterInstallMode::Upgrade(args) => ( - args.wasm_memory_persistence.map(Into::into), - args.skip_pre_upgrade, - ), - _ => (None, None), - } -} - impl From for station_api::ConfigureExternalCanisterOperationDTO { @@ -2459,23 +2461,23 @@ mod tests { use orbit_essentials::cdk::api::management_canister::main::{self as mgmt}; fn upgrade_input( - wasm_memory_persistence: Option, - skip_pre_upgrade: Option, + opts: Option, ) -> station_api::ChangeExternalCanisterOperationInput { station_api::ChangeExternalCanisterOperationInput { canister_id: Principal::management_canister(), - mode: station_api::CanisterInstallMode::Upgrade, + mode: station_api::CanisterInstallMode::Upgrade(opts), module: vec![1, 2, 3], module_extra_chunks: None, arg: None, - wasm_memory_persistence, - skip_pre_upgrade, } } #[test] fn upgrade_flags_roundtrip_through_mapper() { - let api_input = upgrade_input(Some(station_api::WasmMemoryPersistence::Keep), Some(true)); + let api_input = upgrade_input(Some(station_api::CanisterUpgradeOptionsInput { + wasm_memory_persistence: Some(station_api::WasmMemoryPersistence::Keep), + skip_pre_upgrade: Some(true), + })); let internal: ChangeExternalCanisterOperationInput = api_input.into(); let CanisterInstallMode::Upgrade(args) = &internal.mode else { @@ -2488,16 +2490,21 @@ mod tests { assert_eq!(args.skip_pre_upgrade, Some(true)); let api_back: station_api::ChangeExternalCanisterOperationInput = internal.into(); - assert!(matches!( - api_back.wasm_memory_persistence, - Some(station_api::WasmMemoryPersistence::Keep) - )); - assert_eq!(api_back.skip_pre_upgrade, Some(true)); + match api_back.mode { + station_api::CanisterInstallMode::Upgrade(Some(opts)) => { + assert!(matches!( + opts.wasm_memory_persistence, + Some(station_api::WasmMemoryPersistence::Keep) + )); + assert_eq!(opts.skip_pre_upgrade, Some(true)); + } + other => panic!("expected upgrade with options, got {other:?}"), + } } #[test] fn unset_upgrade_flags_map_to_unit_upgrade() { - let api_input = upgrade_input(None, None); + let api_input = upgrade_input(None); let internal: ChangeExternalCanisterOperationInput = api_input.into(); let mgmt_mode: mgmt::CanisterInstallMode = internal.mode.into(); @@ -2509,7 +2516,10 @@ mod tests { #[test] fn set_upgrade_flags_populate_mgmt_upgrade_options() { - let api_input = upgrade_input(Some(station_api::WasmMemoryPersistence::Keep), None); + let api_input = upgrade_input(Some(station_api::CanisterUpgradeOptionsInput { + wasm_memory_persistence: Some(station_api::WasmMemoryPersistence::Keep), + skip_pre_upgrade: None, + })); let internal: ChangeExternalCanisterOperationInput = api_input.into(); let mgmt_mode: mgmt::CanisterInstallMode = internal.mode.into(); @@ -2526,24 +2536,31 @@ mod tests { } #[test] - fn install_and_reinstall_ignore_upgrade_flags() { - for mode in [ - station_api::CanisterInstallMode::Install, - station_api::CanisterInstallMode::Reinstall, + fn install_and_reinstall_map_to_unit_variants() { + for (mode, expected) in [ + ( + station_api::CanisterInstallMode::Install, + station_api::CanisterInstallMode::Install, + ), + ( + station_api::CanisterInstallMode::Reinstall, + station_api::CanisterInstallMode::Reinstall, + ), ] { let api_input = station_api::ChangeExternalCanisterOperationInput { canister_id: Principal::management_canister(), - mode: mode.clone(), + mode, module: vec![], module_extra_chunks: None, arg: None, - wasm_memory_persistence: Some(station_api::WasmMemoryPersistence::Keep), - skip_pre_upgrade: Some(true), }; let internal: ChangeExternalCanisterOperationInput = api_input.into(); let api_back: station_api::ChangeExternalCanisterOperationInput = internal.into(); - assert!(api_back.wasm_memory_persistence.is_none()); - assert!(api_back.skip_pre_upgrade.is_none()); + assert!( + std::mem::discriminant(&api_back.mode) == std::mem::discriminant(&expected), + "expected {expected:?}, got {:?}", + api_back.mode, + ); } } diff --git a/tests/integration/src/external_canister_tests.rs b/tests/integration/src/external_canister_tests.rs index 1e74f0b6d..41d8b7b81 100644 --- a/tests/integration/src/external_canister_tests.rs +++ b/tests/integration/src/external_canister_tests.rs @@ -73,12 +73,10 @@ fn successful_four_eyes_upgrade() { let change_canister_operation = RequestOperationInput::ChangeExternalCanister(ChangeExternalCanisterOperationInput { canister_id, - mode: CanisterInstallMode::Upgrade, + mode: CanisterInstallMode::Upgrade(None), module: base_chunk, module_extra_chunks: Some(module_extra_chunks), arg: None, - wasm_memory_persistence: None, - skip_pre_upgrade: None, }); let change_canister_operation_request = submit_request( @@ -220,12 +218,10 @@ fn upgrade_reinstall_list_test() { let change_canister_operation = RequestOperationInput::ChangeExternalCanister(ChangeExternalCanisterOperationInput { canister_id, - mode: CanisterInstallMode::Upgrade, + mode: CanisterInstallMode::Upgrade(None), module: base_chunk.clone(), module_extra_chunks: Some(module_extra_chunks.clone()), arg: None, - wasm_memory_persistence: None, - skip_pre_upgrade: None, }); execute_request( &env, @@ -251,8 +247,6 @@ fn upgrade_reinstall_list_test() { module: base_chunk, module_extra_chunks: Some(module_extra_chunks), arg: None, - wasm_memory_persistence: None, - skip_pre_upgrade: None, }); execute_request( &env, diff --git a/tools/dfx-orbit/src/canister/install.rs b/tools/dfx-orbit/src/canister/install.rs index e4dd6f01b..d4978e210 100644 --- a/tools/dfx-orbit/src/canister/install.rs +++ b/tools/dfx-orbit/src/canister/install.rs @@ -103,8 +103,6 @@ impl RequestCanisterInstallArgs { module, module_extra_chunks, arg, - wasm_memory_persistence: None, - skip_pre_upgrade: None, }; Ok(RequestOperationInput::ChangeExternalCanister(operation)) } @@ -180,7 +178,7 @@ impl From for CanisterInstallMode { match mode { CanisterInstallModeArgs::Install => Self::Install, CanisterInstallModeArgs::Reinstall => Self::Reinstall, - CanisterInstallModeArgs::Upgrade => Self::Upgrade, + CanisterInstallModeArgs::Upgrade => Self::Upgrade(None), } } } @@ -190,7 +188,7 @@ impl From for CanisterInstallModeArgs { match mode { CanisterInstallMode::Install => Self::Install, CanisterInstallMode::Reinstall => Self::Reinstall, - CanisterInstallMode::Upgrade => Self::Upgrade, + CanisterInstallMode::Upgrade(_) => Self::Upgrade, } } } @@ -211,7 +209,7 @@ impl DfxOrbit { let mode = match op.mode { CanisterInstallMode::Install => "Install", CanisterInstallMode::Reinstall => "Reinstall", - CanisterInstallMode::Upgrade => "Upgrade", + CanisterInstallMode::Upgrade(_) => "Upgrade", }; writeln!(output, "Mode: {mode}")?; From 9161346aaf6d48a31e474b073694d7ca3b5f5389 Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Tue, 26 May 2026 09:32:33 +0200 Subject: [PATCH 5/6] refactor(station): inline upgrade options in spec.did MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per reviewer feedback, the `upgrade` variant in `CanisterInstallMode` now carries an inline anonymous `record { skip_pre_upgrade; wasm_memory_persistence }` with an inline `variant { keep; replace }`, matching the IC management canister's shape and avoiding new named types in the public Candid surface. Regenerated wallet station.did{,.js,.d.ts} via `dfx generate`. The station-api Rust types (`CanisterUpgradeOptionsInput`, `WasmMemoryPersistence`) remain named — Rust requires it — but `service_equal` confirms structural equivalence with the inline spec. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/wallet/src/generated/station/station.did | 34 ++++++---------- .../src/generated/station/station.did.d.ts | 40 +++++++------------ .../src/generated/station/station.did.js | 17 ++++---- core/station/api/spec.did | 34 ++++++---------- 4 files changed, 46 insertions(+), 79 deletions(-) diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 7db17d076..369776c83 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -575,28 +575,18 @@ type RemoveUserGroupOperation = record { type CanisterInstallMode = variant { install; reinstall; - // Upgrade an existing canister. Carries optional flags that mirror the - // IC management canister's `CanisterUpgradeOptions`. - upgrade : opt CanisterUpgradeOptionsInput; -}; - -// Optional upgrade flags forwarded to the IC management canister's -// `install_code` method when `mode` is `upgrade`. -type CanisterUpgradeOptionsInput = record { - // Required as `keep` for upgrading Motoko canisters that use Enhanced - // Orthogonal Persistence; otherwise the IC clears their main memory. - wasm_memory_persistence : opt WasmMemoryPersistence; - // If `true`, the `pre_upgrade` hook is skipped during the canister upgrade. - skip_pre_upgrade : opt bool; -}; - -// WASM memory persistence setting passed to `install_code` when the install -// mode is `upgrade`. `keep` is required for Motoko canisters that use -// Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing -// main memory) otherwise. -type WasmMemoryPersistence = variant { - keep; - replace; + // Upgrade an existing canister. The optional record mirrors the IC + // management canister's `CanisterUpgradeOptions`. + // `wasm_memory_persistence = keep` is required for Motoko canisters that + // use Enhanced Orthogonal Persistence; otherwise the IC clears their main + // memory. + upgrade : opt record { + skip_pre_upgrade : opt bool; + wasm_memory_persistence : opt variant { + keep; + replace; + }; + }; }; type SystemUpgradeTarget = variant { diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index a241d1b4d..47a4f7dd4 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -681,10 +681,21 @@ export interface CanisterExecutionAndValidationMethodPair { export type CanisterInstallMode = { 'reinstall' : null } | { /** - * Upgrade an existing canister. Carries optional flags that mirror the - * IC management canister's `CanisterUpgradeOptions`. + * Upgrade an existing canister. The optional record mirrors the IC + * management canister's `CanisterUpgradeOptions`. + * `wasm_memory_persistence = keep` is required for Motoko canisters that + * use Enhanced Orthogonal Persistence; otherwise the IC clears their main + * memory. */ - 'upgrade' : [] | [CanisterUpgradeOptionsInput] + 'upgrade' : [] | [ + { + 'wasm_memory_persistence' : [] | [ + { 'keep' : null } | + { 'replace' : null } + ], + 'skip_pre_upgrade' : [] | [boolean], + } + ] } | { 'install' : null }; export interface CanisterMethod { @@ -725,21 +736,6 @@ export interface CanisterStatusResponse { 'module_hash' : [] | [Uint8Array | number[]], 'reserved_cycles' : bigint, } -/** - * Optional upgrade flags forwarded to the IC management canister's - * `install_code` method when `mode` is `upgrade`. - */ -export interface CanisterUpgradeOptionsInput { - /** - * Required as `keep` for upgrading Motoko canisters that use Enhanced - * Orthogonal Persistence; otherwise the IC clears their main memory. - */ - 'wasm_memory_persistence' : [] | [WasmMemoryPersistence], - /** - * If `true`, the `pre_upgrade` hook is skipped during the canister upgrade. - */ - 'skip_pre_upgrade' : [] | [boolean], -} /** * A record type that is used to show the current capabilities of the station. */ @@ -5681,14 +5677,6 @@ export type UserStatus = { */ export type ValidationMethodResourceTarget = { 'No' : null } | { 'ValidationMethod' : CanisterMethod }; -/** - * WASM memory persistence setting passed to `install_code` when the install - * mode is `upgrade`. `keep` is required for Motoko canisters that use - * Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing - * main memory) otherwise. - */ -export type WasmMemoryPersistence = { 'keep' : null } | - { 'replace' : null }; export interface WasmModuleExtraChunks { /** * The hash of the assembled wasm module. diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index 2b4777633..f76911c40 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -449,17 +449,16 @@ export const idlFactory = ({ IDL }) => { 'canister_id' : IDL.Principal, }); const ConfigureExternalCanisterOperation = ConfigureExternalCanisterOperationInput; - const WasmMemoryPersistence = IDL.Variant({ - 'keep' : IDL.Null, - 'replace' : IDL.Null, - }); - const CanisterUpgradeOptionsInput = IDL.Record({ - 'wasm_memory_persistence' : IDL.Opt(WasmMemoryPersistence), - 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), - }); const CanisterInstallMode = IDL.Variant({ 'reinstall' : IDL.Null, - 'upgrade' : IDL.Opt(CanisterUpgradeOptionsInput), + 'upgrade' : IDL.Opt( + IDL.Record({ + 'wasm_memory_persistence' : IDL.Opt( + IDL.Variant({ 'keep' : IDL.Null, 'replace' : IDL.Null }) + ), + 'skip_pre_upgrade' : IDL.Opt(IDL.Bool), + }) + ), 'install' : IDL.Null, }); const Sha256Hash = IDL.Text; diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 7db17d076..369776c83 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -575,28 +575,18 @@ type RemoveUserGroupOperation = record { type CanisterInstallMode = variant { install; reinstall; - // Upgrade an existing canister. Carries optional flags that mirror the - // IC management canister's `CanisterUpgradeOptions`. - upgrade : opt CanisterUpgradeOptionsInput; -}; - -// Optional upgrade flags forwarded to the IC management canister's -// `install_code` method when `mode` is `upgrade`. -type CanisterUpgradeOptionsInput = record { - // Required as `keep` for upgrading Motoko canisters that use Enhanced - // Orthogonal Persistence; otherwise the IC clears their main memory. - wasm_memory_persistence : opt WasmMemoryPersistence; - // If `true`, the `pre_upgrade` hook is skipped during the canister upgrade. - skip_pre_upgrade : opt bool; -}; - -// WASM memory persistence setting passed to `install_code` when the install -// mode is `upgrade`. `keep` is required for Motoko canisters that use -// Enhanced Orthogonal Persistence; the IC defaults to `replace` (clearing -// main memory) otherwise. -type WasmMemoryPersistence = variant { - keep; - replace; + // Upgrade an existing canister. The optional record mirrors the IC + // management canister's `CanisterUpgradeOptions`. + // `wasm_memory_persistence = keep` is required for Motoko canisters that + // use Enhanced Orthogonal Persistence; otherwise the IC clears their main + // memory. + upgrade : opt record { + skip_pre_upgrade : opt bool; + wasm_memory_persistence : opt variant { + keep; + replace; + }; + }; }; type SystemUpgradeTarget = variant { From 24382a3e9daf42b0924e22fb74d83692124013f7 Mon Sep 17 00:00:00 2001 From: Arshavir Ter-Gabrielyan Date: Tue, 26 May 2026 12:09:40 +0200 Subject: [PATCH 6/6] refactor(station): use Option::map(Into::into) for Upgrade conversions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per reviewer feedback: - Widen the internal CanisterInstallMode::Upgrade variant from CanisterUpgradeModeArgs to Option, matching the IC management canister's `Upgrade(Option)` shape. - Implement From for mgmt::UpgradeFlags, then drop the ad-hoc conditional and just `.map(Into::into)` over the Option in both directions (model→mgmt and impl↔api mappers). Storage migration: legacy `Upgrade(CanisterUpgradeModeArgs {})` CBOR deserializes as `Upgrade(Some(CanisterUpgradeModeArgs::default()))` — semantically equivalent to "no flags". Covered by a new test that encodes the historical enum-variant shape and confirms it round-trips. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../impl/src/mappers/request_operation.rs | 49 ++++++++++++++----- .../impl/src/models/request_operation.rs | 26 +++++----- core/station/impl/src/services/system.rs | 10 ++-- 3 files changed, 54 insertions(+), 31 deletions(-) diff --git a/core/station/impl/src/mappers/request_operation.rs b/core/station/impl/src/mappers/request_operation.rs index 8212a1b09..2ab5aa8bd 100644 --- a/core/station/impl/src/mappers/request_operation.rs +++ b/core/station/impl/src/mappers/request_operation.rs @@ -525,9 +525,9 @@ impl From for CanisterInstallMode { station_api::CanisterInstallMode::Reinstall => { CanisterInstallMode::Reinstall(CanisterReinstallModeArgs {}) } - station_api::CanisterInstallMode::Upgrade(opts) => CanisterInstallMode::Upgrade( - opts.map(CanisterUpgradeModeArgs::from).unwrap_or_default(), - ), + station_api::CanisterInstallMode::Upgrade(opts) => { + CanisterInstallMode::Upgrade(opts.map(Into::into)) + } } } } @@ -562,13 +562,7 @@ impl From for station_api::CanisterInstallMode { station_api::CanisterInstallMode::Reinstall } CanisterInstallMode::Upgrade(args) => { - let opts = - if args.wasm_memory_persistence.is_some() || args.skip_pre_upgrade.is_some() { - Some(args.into()) - } else { - None - }; - station_api::CanisterInstallMode::Upgrade(opts) + station_api::CanisterInstallMode::Upgrade(args.map(Into::into)) } } } @@ -2480,8 +2474,8 @@ mod tests { })); let internal: ChangeExternalCanisterOperationInput = api_input.into(); - let CanisterInstallMode::Upgrade(args) = &internal.mode else { - panic!("expected upgrade mode"); + let CanisterInstallMode::Upgrade(Some(args)) = &internal.mode else { + panic!("expected upgrade mode with args"); }; assert_eq!( args.wasm_memory_persistence, @@ -2507,6 +2501,8 @@ mod tests { let api_input = upgrade_input(None); let internal: ChangeExternalCanisterOperationInput = api_input.into(); + assert!(matches!(internal.mode, CanisterInstallMode::Upgrade(None))); + let mgmt_mode: mgmt::CanisterInstallMode = internal.mode.into(); assert!(matches!( mgmt_mode, @@ -2575,4 +2571,33 @@ mod tests { serde_cbor::from_slice(&empty_map_cbor).expect("legacy bytes must deserialize"); assert_eq!(args, CanisterUpgradeModeArgs::default()); } + + #[test] + fn legacy_upgrade_variant_deserializes_to_some_default() { + // Historical on-disk shape of the enum variant was + // `Upgrade(CanisterUpgradeModeArgs {})`. After widening the payload + // to `Option`, the legacy bytes (variant + // tag + empty map) should still deserialize, surfacing as + // `Upgrade(Some(default))` — semantically equivalent to "no flags". + let cbor = serde_cbor::to_vec(&CanisterInstallModeLegacy::Upgrade( + CanisterUpgradeModeArgs::default(), + )) + .unwrap(); + let mode: CanisterInstallMode = + serde_cbor::from_slice(&cbor).expect("legacy upgrade variant must deserialize"); + assert!(matches!( + mode, + CanisterInstallMode::Upgrade(Some(args)) if args == CanisterUpgradeModeArgs::default() + )); + } + + // Mirror of the pre-refactor enum used only to encode a legacy fixture. + #[derive(serde::Serialize)] + enum CanisterInstallModeLegacy { + #[allow(dead_code)] + Install(CanisterInstallModeArgs), + #[allow(dead_code)] + Reinstall(CanisterReinstallModeArgs), + Upgrade(CanisterUpgradeModeArgs), + } } diff --git a/core/station/impl/src/models/request_operation.rs b/core/station/impl/src/models/request_operation.rs index a241d46e7..d09306642 100644 --- a/core/station/impl/src/models/request_operation.rs +++ b/core/station/impl/src/models/request_operation.rs @@ -489,7 +489,16 @@ impl From for mgmt::WasmPersistenceMode { pub enum CanisterInstallMode { Install(CanisterInstallModeArgs), Reinstall(CanisterReinstallModeArgs), - Upgrade(CanisterUpgradeModeArgs), + Upgrade(Option), +} + +impl From for mgmt::UpgradeFlags { + fn from(args: CanisterUpgradeModeArgs) -> Self { + mgmt::UpgradeFlags { + skip_pre_upgrade: args.skip_pre_upgrade, + wasm_memory_persistence: args.wasm_memory_persistence.map(Into::into), + } + } } impl From for mgmt::CanisterInstallMode { @@ -498,16 +507,7 @@ impl From for mgmt::CanisterInstallMode { CanisterInstallMode::Install(_) => mgmt::CanisterInstallMode::Install, CanisterInstallMode::Reinstall(_) => mgmt::CanisterInstallMode::Reinstall, CanisterInstallMode::Upgrade(args) => { - let flags = - if args.wasm_memory_persistence.is_some() || args.skip_pre_upgrade.is_some() { - Some(mgmt::UpgradeFlags { - skip_pre_upgrade: args.skip_pre_upgrade, - wasm_memory_persistence: args.wasm_memory_persistence.map(Into::into), - }) - } else { - None - }; - mgmt::CanisterInstallMode::Upgrade(flags) + mgmt::CanisterInstallMode::Upgrade(args.map(Into::into)) } } } @@ -1622,9 +1622,7 @@ mod test { crate::models::ChangeExternalCanisterOperation { input: crate::models::ChangeExternalCanisterOperationInput { canister_id: upgrader_id, - mode: crate::models::CanisterInstallMode::Upgrade( - crate::models::CanisterUpgradeModeArgs::default(), - ), + mode: crate::models::CanisterInstallMode::Upgrade(None), module: vec![], module_extra_chunks: None, arg: None, diff --git a/core/station/impl/src/services/system.rs b/core/station/impl/src/services/system.rs index 60062ac79..623deeec1 100644 --- a/core/station/impl/src/services/system.rs +++ b/core/station/impl/src/services/system.rs @@ -10,10 +10,10 @@ use crate::{ errors::SystemError, models::{ system::{DisasterRecoveryCommittee, SystemInfo, SystemState}, - Asset, Blockchain, CanisterInstallMode, CanisterUpgradeModeArgs, - ManageSystemInfoOperationInput, Metadata, RequestId, RequestKey, RequestOperation, - RequestStatus, SystemRestoreTarget, SystemUpgradeTarget, TokenStandard, - WasmModuleExtraChunks, ADMIN_GROUP_ID, OPERATOR_GROUP_ID, + Asset, Blockchain, CanisterInstallMode, ManageSystemInfoOperationInput, Metadata, + RequestId, RequestKey, RequestOperation, RequestStatus, SystemRestoreTarget, + SystemUpgradeTarget, TokenStandard, WasmModuleExtraChunks, ADMIN_GROUP_ID, + OPERATOR_GROUP_ID, }, repositories::{ permission::PERMISSION_REPOSITORY, RequestRepository, ASSET_REPOSITORY, @@ -238,7 +238,7 @@ impl SystemService { .change_canister_service .install_canister( upgrader_canister_id, - CanisterInstallMode::Upgrade(CanisterUpgradeModeArgs::default()), + CanisterInstallMode::Upgrade(None), module, module_extra_chunks, arg,