From 3c03ea9cd90fe60126cb33133ead79b678ffb603 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 09:58:43 +0200 Subject: [PATCH 01/16] feat(console): create ufo (raw canister) --- src/console/src/api/factory.rs | 19 +++++- src/console/src/constants.rs | 1 + src/console/src/factory/impls.rs | 13 +++- src/console/src/factory/mod.rs | 1 + src/console/src/factory/ufo.rs | 81 +++++++++++++++++++++++++ src/console/src/fees/init.rs | 10 ++- src/console/src/lib.rs | 1 + src/console/src/rates/init.rs | 7 +++ src/console/src/rates/services.rs | 4 ++ src/console/src/segments/store.rs | 2 +- src/console/src/types.rs | 1 + src/libs/shared/src/constants/shared.rs | 1 + src/libs/shared/src/impls.rs | 1 + src/libs/shared/src/types.rs | 15 +++++ src/observatory/src/impls.rs | 1 + 15 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/console/src/factory/ufo.rs diff --git a/src/console/src/api/factory.rs b/src/console/src/api/factory.rs index 3d1379194a..d112490e2e 100644 --- a/src/console/src/api/factory.rs +++ b/src/console/src/api/factory.rs @@ -1,12 +1,13 @@ use crate::factory::mission_control::create_mission_control as create_mission_control_console; use crate::factory::orbiter::create_orbiter as create_orbiter_console; use crate::factory::satellite::create_satellite as create_satellite_console; +use crate::factory::ufo::create_ufo; use candid::Principal; use ic_cdk_macros::update; use junobuild_shared::ic::api::caller; use junobuild_shared::ic::UnwrapOrTrap; use junobuild_shared::types::interface::{ - CreateMissionControlArgs, CreateOrbiterArgs, CreateSatelliteArgs, + CreateMissionControlArgs, CreateOrbiterArgs, CreateSatelliteArgs, CreateSegmentArgs, }; #[update] @@ -33,3 +34,19 @@ async fn create_orbiter(args: CreateOrbiterArgs) -> Principal { create_orbiter_console(caller, args).await.unwrap_or_trap() } + +#[update] +async fn create_segment(args: CreateSegmentArgs) -> Principal { + let caller = caller(); + + let result = match args { + CreateSegmentArgs::Satellite(args) => create_satellite_console(caller, args).await, + CreateSegmentArgs::MissionControl(args) => { + create_mission_control_console(caller, args).await + } + CreateSegmentArgs::Orbiter(args) => create_orbiter_console(caller, args).await, + CreateSegmentArgs::Ufo(args) => create_ufo(caller, args).await, + }; + + result.unwrap_or_trap() +} diff --git a/src/console/src/constants.rs b/src/console/src/constants.rs index 76b93a25b2..5dfbc771b2 100644 --- a/src/console/src/constants.rs +++ b/src/console/src/constants.rs @@ -13,6 +13,7 @@ pub const SATELLITE_CREATION_FEE_CYCLES: CyclesTokens = CyclesTokens::from_e12s( pub const ORBITER_CREATION_FEE_CYCLES: CyclesTokens = CyclesTokens::from_e12s(3_000_000_000_000); pub const MISSION_CONTROL_CREATION_FEE_CYCLES: CyclesTokens = CyclesTokens::from_e12s(3_000_000_000_000); +pub const UFO_CREATION_FEE_CYCLES: CyclesTokens = CyclesTokens::from_e12s(3_000_000_000_000); // 1 ICP but also the default credit - i.e. a mission control starts with one credit. // A credit which can be used to start one satellite or one orbiter. diff --git a/src/console/src/factory/impls.rs b/src/console/src/factory/impls.rs index 073c3c830d..e634bbc2cd 100644 --- a/src/console/src/factory/impls.rs +++ b/src/console/src/factory/impls.rs @@ -2,7 +2,7 @@ use crate::factory::types::CanisterCreator; use crate::factory::types::CreateSegmentArgs; use candid::Principal; use junobuild_shared::types::interface::{ - CreateMissionControlArgs, CreateOrbiterArgs, CreateSatelliteArgs, + CreateMissionControlArgs, CreateOrbiterArgs, CreateSatelliteArgs, CreateUfoArgs, }; use junobuild_shared::types::state::{AccessKeyId, UserId}; @@ -63,3 +63,14 @@ impl From for CreateSegmentArgs { } } } + +impl From for CreateSegmentArgs { + fn from(args: CreateUfoArgs) -> Self { + Self { + // Unlike Satellite and Orbiter, or same as Mission Control, Ufo (raw canister) can only be + // spin using credits or ICRC-2 transfer from. + block_index: None, + subnet_id: args.subnet_id, + } + } +} diff --git a/src/console/src/factory/mod.rs b/src/console/src/factory/mod.rs index 22157225a9..4be2a0bfa9 100644 --- a/src/console/src/factory/mod.rs +++ b/src/console/src/factory/mod.rs @@ -5,4 +5,5 @@ mod orchestrator; pub mod satellite; mod services; mod types; +pub mod ufo; mod utils; diff --git a/src/console/src/factory/ufo.rs b/src/console/src/factory/ufo.rs new file mode 100644 index 0000000000..1ce252e342 --- /dev/null +++ b/src/console/src/factory/ufo.rs @@ -0,0 +1,81 @@ +use crate::accounts::get_existing_account; +use crate::constants::FREEZING_THRESHOLD_ONE_YEAR; +use crate::factory::orchestrator::create_segment_with_account; +use crate::factory::services::payment::{process_payment_cycles, refund_payment_cycles}; +use crate::factory::types::CanisterCreator; +use crate::factory::utils::controllers::update_mission_control_controllers; +use crate::fees::get_factory_fee; +use crate::rates::increment_ufo_rate; +use crate::segments::add_segment as add_segment_store; +use crate::types::ledger::Fee; +use crate::types::state::{Segment, SegmentKey, StorableSegmentKind}; +use candid::{Nat, Principal}; +use junobuild_shared::constants::shared::CREATE_UFO_CYCLES; +use junobuild_shared::ic::api::id; +use junobuild_shared::mgmt::cmc::create_canister_with_cmc; +use junobuild_shared::mgmt::ic::create_canister_with_ic_mgmt; +use junobuild_shared::mgmt::types::cmc::SubnetId; +use junobuild_shared::mgmt::types::ic::CreateCanisterInitSettingsArg; +use junobuild_shared::types::interface::CreateUfoArgs; +use junobuild_shared::types::state::{SegmentKind, UserId}; + +pub async fn create_ufo(caller: Principal, args: CreateUfoArgs) -> Result { + let account = get_existing_account(&caller)?; + + let name = args.name.clone(); + let creator: CanisterCreator = CanisterCreator::User((account.owner, None)); + + let fee = get_factory_fee(&SegmentKind::Ufo)?.fee_cycles; + + let canister_id = create_segment_with_account( + create_raw_canister, + process_payment_cycles, + refund_payment_cycles, + &increment_ufo_rate, + Fee::Cycles(fee), + &account, + creator, + args.into(), + ) + .await?; + + add_segment(&account.owner, &canister_id, &name); + + Ok(canister_id) +} + +async fn create_raw_canister( + creator: CanisterCreator, + subnet_id: Option, +) -> Result { + let CanisterCreator::User((user_id, _)) = creator else { + return Err("Mission Control cannot create an UFO".to_string()); + }; + + // We temporarily use the Console as a controller to create the canister but + // remove it as soon as it is spin. + let temporary_init_controllers = Vec::from([id(), user_id]); + + let create_settings_arg = CreateCanisterInitSettingsArg { + controllers: temporary_init_controllers, + freezing_threshold: Nat::from(FREEZING_THRESHOLD_ONE_YEAR), + }; + + let ufo_id = if let Some(subnet_id) = subnet_id { + create_canister_with_cmc(&create_settings_arg, CREATE_UFO_CYCLES, &subnet_id).await + } else { + create_canister_with_ic_mgmt(&create_settings_arg, CREATE_UFO_CYCLES).await + }?; + + // TODO: update controllers only user_id + update_mission_control_controllers(&ufo_id, &user_id).await?; + + Ok(ufo_id) +} + +fn add_segment(user: &UserId, canister_id: &Principal, name: &Option) { + let metadata = Segment::init_metadata(name); + let canister = Segment::new(canister_id, Some(metadata)); + let key = SegmentKey::from(user, canister_id, StorableSegmentKind::Ufo); + add_segment_store(&key, &canister) +} diff --git a/src/console/src/fees/init.rs b/src/console/src/fees/init.rs index f5c1d74a95..40fb9d589f 100644 --- a/src/console/src/fees/init.rs +++ b/src/console/src/fees/init.rs @@ -1,6 +1,6 @@ use crate::constants::{ MISSION_CONTROL_CREATION_FEE_CYCLES, ORBITER_CREATION_FEE_CYCLES, ORBITER_CREATION_FEE_ICP, - SATELLITE_CREATION_FEE_CYCLES, SATELLITE_CREATION_FEE_ICP, + SATELLITE_CREATION_FEE_CYCLES, SATELLITE_CREATION_FEE_ICP, UFO_CREATION_FEE_CYCLES, }; use crate::types::state::{FactoryFee, FactoryFees}; use ic_cdk::api::time; @@ -35,5 +35,13 @@ pub fn init_factory_fees() -> FactoryFees { updated_at: now, }, ), + ( + SegmentKind::Ufo, + FactoryFee { + fee_cycles: UFO_CREATION_FEE_CYCLES, + fee_icp: None, + updated_at: now, + }, + ), ]) } diff --git a/src/console/src/lib.rs b/src/console/src/lib.rs index d91c6186d6..b6014c2b4e 100644 --- a/src/console/src/lib.rs +++ b/src/console/src/lib.rs @@ -62,6 +62,7 @@ use junobuild_shared::types::domain::CustomDomains; use junobuild_shared::types::interface::CreateMissionControlArgs; use junobuild_shared::types::interface::CreateOrbiterArgs; use junobuild_shared::types::interface::CreateSatelliteArgs; +use junobuild_shared::types::interface::CreateSegmentArgs; use junobuild_shared::types::interface::{ AssertMissionControlCenterArgs, DeleteControllersArgs, GetCreateCanisterFeeArgs, SetControllersArgs, diff --git a/src/console/src/rates/init.rs b/src/console/src/rates/init.rs index f51bf03649..857da0fef8 100644 --- a/src/console/src/rates/init.rs +++ b/src/console/src/rates/init.rs @@ -35,5 +35,12 @@ pub fn init_factory_rates() -> FactoryRates { tokens: tokens.clone(), }, ), + ( + SegmentKind::Ufo, + FactoryRate { + config: DEFAULT_RATE_CONFIG, + tokens: tokens.clone(), + }, + ), ]) } diff --git a/src/console/src/rates/services.rs b/src/console/src/rates/services.rs index 18413b2799..40b5969cea 100644 --- a/src/console/src/rates/services.rs +++ b/src/console/src/rates/services.rs @@ -12,3 +12,7 @@ pub fn increment_mission_controls_rate() -> Result<(), String> { pub fn increment_orbiters_rate() -> Result<(), String> { increment_rate(&SegmentKind::Orbiter) } + +pub fn increment_ufo_rate() -> Result<(), String> { + increment_rate(&SegmentKind::Ufo) +} diff --git a/src/console/src/segments/store.rs b/src/console/src/segments/store.rs index fc84b1c42d..ad81729372 100644 --- a/src/console/src/segments/store.rs +++ b/src/console/src/segments/store.rs @@ -98,7 +98,7 @@ fn filter_segments_range( let end_key = SegmentKey { user: *user, // Fallback to last enum - segment_kind: segment_kind.clone().unwrap_or(StorableSegmentKind::Orbiter), + segment_kind: segment_kind.clone().unwrap_or(StorableSegmentKind::Ufo), segment_id: segment_id.unwrap_or(PRINCIPAL_MAX), }; diff --git a/src/console/src/types.rs b/src/console/src/types.rs index 48c4730efd..e27f440660 100644 --- a/src/console/src/types.rs +++ b/src/console/src/types.rs @@ -152,6 +152,7 @@ pub mod state { // For historical reasons, MissionControl is not stored in the segments stable tree // but within the Account structure Orbiter, + Ufo, } // On Apr. 4, 2026, someone exploited the free tier to spin up free canisters. diff --git a/src/libs/shared/src/constants/shared.rs b/src/libs/shared/src/constants/shared.rs index 90eb869a05..f9a4c62082 100644 --- a/src/libs/shared/src/constants/shared.rs +++ b/src/libs/shared/src/constants/shared.rs @@ -19,6 +19,7 @@ pub const IC_CREATE_CANISTER_CYCLES: u128 = 500_000_000_000u128; pub const CREATE_SATELLITE_CYCLES: u128 = 1_000_000_000_000; pub const CREATE_MISSION_CONTROL_CYCLES: u128 = 1_000_000_000_000; pub const CREATE_ORBITER_CYCLES: u128 = 1_000_000_000_000; +pub const CREATE_UFO_CYCLES: u128 = 1_000_000_000_000; // Reverse (CREA -> AERC) -> ASCII -> HEX -> LittleEndian // NNS canister create: CREA 0x41455243 diff --git a/src/libs/shared/src/impls.rs b/src/libs/shared/src/impls.rs index b944d9fe29..e8b04454af 100644 --- a/src/libs/shared/src/impls.rs +++ b/src/libs/shared/src/impls.rs @@ -20,6 +20,7 @@ impl Display for SegmentKind { SegmentKind::Satellite => write!(f, "Satellite"), SegmentKind::MissionControl => write!(f, "Mission Control"), SegmentKind::Orbiter => write!(f, "Orbiter"), + SegmentKind::Ufo => write!(f, "Ufo"), } } } diff --git a/src/libs/shared/src/types.rs b/src/libs/shared/src/types.rs index 3b29bbcdce..b5ad9983a1 100644 --- a/src/libs/shared/src/types.rs +++ b/src/libs/shared/src/types.rs @@ -63,6 +63,7 @@ pub mod state { Satellite, MissionControl, Orbiter, + Ufo, } #[derive(CandidType, Serialize, Deserialize, Clone)] @@ -117,6 +118,14 @@ pub mod interface { use ic_ledger_types::BlockIndex; use serde::{Deserialize, Serialize}; + #[derive(CandidType, Deserialize)] + pub enum CreateSegmentArgs { + Satellite(CreateSatelliteArgs), + MissionControl(CreateMissionControlArgs), + Orbiter(CreateOrbiterArgs), + Ufo(CreateUfoArgs), + } + #[derive(CandidType, Deserialize)] pub struct CreateOrbiterArgs { pub user: UserId, @@ -139,6 +148,12 @@ pub mod interface { pub name: Option, } + #[derive(CandidType, Deserialize)] + pub struct CreateUfoArgs { + pub subnet_id: Option, + pub name: Option, + } + #[derive(CandidType, Deserialize)] pub struct GetCreateCanisterFeeArgs { pub user: UserId, diff --git a/src/observatory/src/impls.rs b/src/observatory/src/impls.rs index 2ff485988f..88a5632bff 100644 --- a/src/observatory/src/impls.rs +++ b/src/observatory/src/impls.rs @@ -136,6 +136,7 @@ impl Notification { "https://console.juno.build/satellite/?s={}", self.segment.id ), + SegmentKind::Ufo => format!("https://console.juno.build/ufo/?u={}", self.segment.id), } } From c9ba54521dc75cc6d6cb03d9401c61b7bdfaa696 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 10:05:34 +0200 Subject: [PATCH 02/16] feat: generate did --- src/console/console.did | 12 ++++++++++-- src/declarations/console/console.did.d.ts | 18 ++++++++++++++++-- .../console/console.factory.certified.did.js | 13 +++++++++++++ .../console/console.factory.did.js | 13 +++++++++++++ .../console/console.factory.did.mjs | 13 +++++++++++++ .../observatory/observatory.did.d.ts | 6 +++++- .../observatory.factory.certified.did.js | 1 + .../observatory/observatory.factory.did.js | 1 + .../observatory/observatory.factory.did.mjs | 1 + src/observatory/observatory.did | 2 +- 10 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/console/console.did b/src/console/console.did index 819a5ef5c8..c9ec7ccc5d 100644 --- a/src/console/console.did +++ b/src/console/console.did @@ -100,6 +100,13 @@ type CreateSatelliteArgs = record { name : opt text; user : principal; }; +type CreateSegmentArgs = variant { + Ufo : CreateUfoArgs; + Orbiter : CreateOrbiterArgs; + MissionControl : CreateMissionControlArgs; + Satellite : CreateSatelliteArgs; +}; +type CreateUfoArgs = record { subnet_id : opt principal; name : opt text }; type CustomDomain = record { updated_at : nat64; created_at : nat64; @@ -310,7 +317,7 @@ type SegmentKey = record { segment_id : principal; segment_kind : StorableSegmentKind; }; -type SegmentKind = variant { Orbiter; MissionControl; Satellite }; +type SegmentKind = variant { Ufo; Orbiter; MissionControl; Satellite }; type SegmentsDeploymentOptions = record { orbiter : opt text; mission_control_version : opt text; @@ -353,7 +360,7 @@ type SetStorageConfig = record { redirects : opt vec record { text; StorageConfigRedirect }; }; type SignedDelegation = record { signature : blob; delegation : Delegation }; -type StorableSegmentKind = variant { Orbiter; Satellite }; +type StorableSegmentKind = variant { Ufo; Orbiter; Satellite }; type StorageConfig = record { iframe : opt StorageConfigIFrame; updated_at : opt nat64; @@ -418,6 +425,7 @@ service : () -> { create_mission_control : (CreateMissionControlArgs) -> (principal); create_orbiter : (CreateOrbiterArgs) -> (principal); create_satellite : (CreateSatelliteArgs) -> (principal); + create_segment : (CreateSegmentArgs) -> (principal); del_controllers : (DeleteControllersArgs) -> (); del_custom_domain : (text) -> (); delete_proposal_assets : (DeleteProposalAssets) -> (); diff --git a/src/declarations/console/console.did.d.ts b/src/declarations/console/console.did.d.ts index 45a6204717..1cbf302b6f 100644 --- a/src/declarations/console/console.did.d.ts +++ b/src/declarations/console/console.did.d.ts @@ -128,6 +128,15 @@ export interface CreateSatelliteArgs { name: [] | [string]; user: Principal; } +export type CreateSegmentArgs = + | { Ufo: CreateUfoArgs } + | { Orbiter: CreateOrbiterArgs } + | { MissionControl: CreateMissionControlArgs } + | { Satellite: CreateSatelliteArgs }; +export interface CreateUfoArgs { + subnet_id: [] | [Principal]; + name: [] | [string]; +} export interface CustomDomain { updated_at: bigint; created_at: bigint; @@ -373,7 +382,11 @@ export interface SegmentKey { segment_id: Principal; segment_kind: StorableSegmentKind; } -export type SegmentKind = { Orbiter: null } | { MissionControl: null } | { Satellite: null }; +export type SegmentKind = + | { Ufo: null } + | { Orbiter: null } + | { MissionControl: null } + | { Satellite: null }; export interface SegmentsDeploymentOptions { orbiter: [] | [string]; mission_control_version: [] | [string]; @@ -422,7 +435,7 @@ export interface SignedDelegation { signature: Uint8Array; delegation: Delegation; } -export type StorableSegmentKind = { Orbiter: null } | { Satellite: null }; +export type StorableSegmentKind = { Ufo: null } | { Orbiter: null } | { Satellite: null }; export interface StorageConfig { iframe: [] | [StorageConfigIFrame]; updated_at: [] | [bigint]; @@ -498,6 +511,7 @@ export interface _SERVICE { create_mission_control: ActorMethod<[CreateMissionControlArgs], Principal>; create_orbiter: ActorMethod<[CreateOrbiterArgs], Principal>; create_satellite: ActorMethod<[CreateSatelliteArgs], Principal>; + create_segment: ActorMethod<[CreateSegmentArgs], Principal>; del_controllers: ActorMethod<[DeleteControllersArgs], undefined>; del_custom_domain: ActorMethod<[string], undefined>; delete_proposal_assets: ActorMethod<[DeleteProposalAssets], undefined>; diff --git a/src/declarations/console/console.factory.certified.did.js b/src/declarations/console/console.factory.certified.did.js index d142a29726..1278b4a512 100644 --- a/src/declarations/console/console.factory.certified.did.js +++ b/src/declarations/console/console.factory.certified.did.js @@ -128,6 +128,16 @@ export const idlFactory = ({ IDL }) => { name: IDL.Opt(IDL.Text), user: IDL.Principal }); + const CreateUfoArgs = IDL.Record({ + subnet_id: IDL.Opt(IDL.Principal), + name: IDL.Opt(IDL.Text) + }); + const CreateSegmentArgs = IDL.Variant({ + Ufo: CreateUfoArgs, + Orbiter: CreateOrbiterArgs, + MissionControl: CreateMissionControlArgs, + Satellite: CreateSatelliteArgs + }); const DeleteControllersArgs = IDL.Record({ controllers: IDL.Vec(IDL.Principal) }); @@ -230,6 +240,7 @@ export const idlFactory = ({ IDL }) => { Err: GetDelegationError }); const SegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, MissionControl: IDL.Null, Satellite: IDL.Null @@ -439,6 +450,7 @@ export const idlFactory = ({ IDL }) => { items_length: IDL.Nat64 }); const StorableSegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, Satellite: IDL.Null }); @@ -528,6 +540,7 @@ export const idlFactory = ({ IDL }) => { create_mission_control: IDL.Func([CreateMissionControlArgs], [IDL.Principal], []), create_orbiter: IDL.Func([CreateOrbiterArgs], [IDL.Principal], []), create_satellite: IDL.Func([CreateSatelliteArgs], [IDL.Principal], []), + create_segment: IDL.Func([CreateSegmentArgs], [IDL.Principal], []), del_controllers: IDL.Func([DeleteControllersArgs], [], []), del_custom_domain: IDL.Func([IDL.Text], [], []), delete_proposal_assets: IDL.Func([DeleteProposalAssets], [], []), diff --git a/src/declarations/console/console.factory.did.js b/src/declarations/console/console.factory.did.js index 1493a47c2b..6b93dd2935 100644 --- a/src/declarations/console/console.factory.did.js +++ b/src/declarations/console/console.factory.did.js @@ -128,6 +128,16 @@ export const idlFactory = ({ IDL }) => { name: IDL.Opt(IDL.Text), user: IDL.Principal }); + const CreateUfoArgs = IDL.Record({ + subnet_id: IDL.Opt(IDL.Principal), + name: IDL.Opt(IDL.Text) + }); + const CreateSegmentArgs = IDL.Variant({ + Ufo: CreateUfoArgs, + Orbiter: CreateOrbiterArgs, + MissionControl: CreateMissionControlArgs, + Satellite: CreateSatelliteArgs + }); const DeleteControllersArgs = IDL.Record({ controllers: IDL.Vec(IDL.Principal) }); @@ -230,6 +240,7 @@ export const idlFactory = ({ IDL }) => { Err: GetDelegationError }); const SegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, MissionControl: IDL.Null, Satellite: IDL.Null @@ -439,6 +450,7 @@ export const idlFactory = ({ IDL }) => { items_length: IDL.Nat64 }); const StorableSegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, Satellite: IDL.Null }); @@ -528,6 +540,7 @@ export const idlFactory = ({ IDL }) => { create_mission_control: IDL.Func([CreateMissionControlArgs], [IDL.Principal], []), create_orbiter: IDL.Func([CreateOrbiterArgs], [IDL.Principal], []), create_satellite: IDL.Func([CreateSatelliteArgs], [IDL.Principal], []), + create_segment: IDL.Func([CreateSegmentArgs], [IDL.Principal], []), del_controllers: IDL.Func([DeleteControllersArgs], [], []), del_custom_domain: IDL.Func([IDL.Text], [], []), delete_proposal_assets: IDL.Func([DeleteProposalAssets], [], []), diff --git a/src/declarations/console/console.factory.did.mjs b/src/declarations/console/console.factory.did.mjs index 1493a47c2b..6b93dd2935 100644 --- a/src/declarations/console/console.factory.did.mjs +++ b/src/declarations/console/console.factory.did.mjs @@ -128,6 +128,16 @@ export const idlFactory = ({ IDL }) => { name: IDL.Opt(IDL.Text), user: IDL.Principal }); + const CreateUfoArgs = IDL.Record({ + subnet_id: IDL.Opt(IDL.Principal), + name: IDL.Opt(IDL.Text) + }); + const CreateSegmentArgs = IDL.Variant({ + Ufo: CreateUfoArgs, + Orbiter: CreateOrbiterArgs, + MissionControl: CreateMissionControlArgs, + Satellite: CreateSatelliteArgs + }); const DeleteControllersArgs = IDL.Record({ controllers: IDL.Vec(IDL.Principal) }); @@ -230,6 +240,7 @@ export const idlFactory = ({ IDL }) => { Err: GetDelegationError }); const SegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, MissionControl: IDL.Null, Satellite: IDL.Null @@ -439,6 +450,7 @@ export const idlFactory = ({ IDL }) => { items_length: IDL.Nat64 }); const StorableSegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, Satellite: IDL.Null }); @@ -528,6 +540,7 @@ export const idlFactory = ({ IDL }) => { create_mission_control: IDL.Func([CreateMissionControlArgs], [IDL.Principal], []), create_orbiter: IDL.Func([CreateOrbiterArgs], [IDL.Principal], []), create_satellite: IDL.Func([CreateSatelliteArgs], [IDL.Principal], []), + create_segment: IDL.Func([CreateSegmentArgs], [IDL.Principal], []), del_controllers: IDL.Func([DeleteControllersArgs], [], []), del_custom_domain: IDL.Func([IDL.Text], [], []), delete_proposal_assets: IDL.Func([DeleteProposalAssets], [], []), diff --git a/src/declarations/observatory/observatory.did.d.ts b/src/declarations/observatory/observatory.did.d.ts index 1e7716f05a..4ed1423f75 100644 --- a/src/declarations/observatory/observatory.did.d.ts +++ b/src/declarations/observatory/observatory.did.d.ts @@ -119,7 +119,11 @@ export interface Segment { metadata: [] | [Array<[string, string]>]; kind: SegmentKind; } -export type SegmentKind = { Orbiter: null } | { MissionControl: null } | { Satellite: null }; +export type SegmentKind = + | { Ufo: null } + | { Orbiter: null } + | { MissionControl: null } + | { Satellite: null }; export interface SetAccessKey { metadata: Array<[string, string]>; kind: [] | [AccessKeyKind]; diff --git a/src/declarations/observatory/observatory.factory.certified.did.js b/src/declarations/observatory/observatory.factory.certified.did.js index d7fda8452e..6056063cab 100644 --- a/src/declarations/observatory/observatory.factory.certified.did.js +++ b/src/declarations/observatory/observatory.factory.certified.did.js @@ -104,6 +104,7 @@ export const idlFactory = ({ IDL }) => { FailedCyclesDepositEmail: FailedCyclesDepositEmailNotification }); const SegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, MissionControl: IDL.Null, Satellite: IDL.Null diff --git a/src/declarations/observatory/observatory.factory.did.js b/src/declarations/observatory/observatory.factory.did.js index 39c8f2f854..6e4ca1c9eb 100644 --- a/src/declarations/observatory/observatory.factory.did.js +++ b/src/declarations/observatory/observatory.factory.did.js @@ -104,6 +104,7 @@ export const idlFactory = ({ IDL }) => { FailedCyclesDepositEmail: FailedCyclesDepositEmailNotification }); const SegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, MissionControl: IDL.Null, Satellite: IDL.Null diff --git a/src/declarations/observatory/observatory.factory.did.mjs b/src/declarations/observatory/observatory.factory.did.mjs index 39c8f2f854..6e4ca1c9eb 100644 --- a/src/declarations/observatory/observatory.factory.did.mjs +++ b/src/declarations/observatory/observatory.factory.did.mjs @@ -104,6 +104,7 @@ export const idlFactory = ({ IDL }) => { FailedCyclesDepositEmail: FailedCyclesDepositEmailNotification }); const SegmentKind = IDL.Variant({ + Ufo: IDL.Null, Orbiter: IDL.Null, MissionControl: IDL.Null, Satellite: IDL.Null diff --git a/src/observatory/observatory.did b/src/observatory/observatory.did index ec8a59c924..86bc1d16e1 100644 --- a/src/observatory/observatory.did +++ b/src/observatory/observatory.did @@ -78,7 +78,7 @@ type Segment = record { metadata : opt vec record { text; text }; kind : SegmentKind; }; -type SegmentKind = variant { Orbiter; MissionControl; Satellite }; +type SegmentKind = variant { Ufo; Orbiter; MissionControl; Satellite }; type SetAccessKey = record { metadata : vec record { text; text }; kind : opt AccessKeyKind; From a3c1fc7d8b1cab260fb3f7ff3e2989e71343ef14 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 11:23:13 +0200 Subject: [PATCH 03/16] feat: insert rates --- src/console/src/memory/lifecycle.rs | 4 +++ src/console/src/memory/mod.rs | 1 + src/console/src/memory/upgrade.rs | 54 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 src/console/src/memory/upgrade.rs diff --git a/src/console/src/memory/lifecycle.rs b/src/console/src/memory/lifecycle.rs index b1284c7ce7..4ba9be9e7b 100644 --- a/src/console/src/memory/lifecycle.rs +++ b/src/console/src/memory/lifecycle.rs @@ -2,6 +2,7 @@ use crate::cdn::certified_assets::upgrade::defer_init_certified_assets; use crate::cdn::lifecycle::init_cdn_storage_heap_state; use crate::fees::init_factory_fees; use crate::memory::manager::{get_memory_upgrades, init_stable_state, STATE}; +use crate::memory::upgrade::upgrade_init_ufo_fees_and_rates; use crate::rates::init::init_factory_rates; use crate::types::state::{HeapState, ReleasesMetadata, State}; use ciborium::{from_reader, into_writer}; @@ -57,4 +58,7 @@ fn post_upgrade() { STATE.with(|s| *s.borrow_mut() = state); defer_init_certified_assets(); + + // TODO: to be removed, one time upgrade + upgrade_init_ufo_fees_and_rates(); } diff --git a/src/console/src/memory/mod.rs b/src/console/src/memory/mod.rs index fdc1fa1308..8c00715d56 100644 --- a/src/console/src/memory/mod.rs +++ b/src/console/src/memory/mod.rs @@ -1,2 +1,3 @@ pub mod lifecycle; pub mod manager; +mod upgrade; diff --git a/src/console/src/memory/upgrade.rs b/src/console/src/memory/upgrade.rs new file mode 100644 index 0000000000..6d043f2119 --- /dev/null +++ b/src/console/src/memory/upgrade.rs @@ -0,0 +1,54 @@ +use crate::constants::UFO_CREATION_FEE_CYCLES; +use crate::store::mutate_heap_state; +use crate::types::state::{FactoryFee, FactoryFees, FactoryRate, FactoryRates, HeapState}; +use ic_cdk::api::time; +use junobuild_shared::ic::api::print; +use junobuild_shared::rate::constants::DEFAULT_RATE_CONFIG; +use junobuild_shared::rate::types::RateTokens; +use junobuild_shared::types::state::SegmentKind; + +pub fn upgrade_init_ufo_fees_and_rates() { + mutate_heap_state(|state| { + upgrade_ufo_fees(&mut state.factory_fees) + .unwrap_or_else(|err| print(format!("Error upgrading the Ufo fee: {:?}", err))); + + upgrade_ufo_rates(&mut state.factory_rates) + .unwrap_or_else(|err| print(format!("Error upgrading the Ufo rate: {:?}", err))); + }); +} + +fn upgrade_ufo_fees(factory_fees: &mut Option) -> Result<(), String> { + let fees = factory_fees + .as_mut() + .ok_or_else(|| "Factory fees not initialized".to_string())?; + + let fee = FactoryFee { + fee_cycles: UFO_CREATION_FEE_CYCLES, + fee_icp: None, + updated_at: time(), + }; + + fees.insert(SegmentKind::Ufo, fee); + + Ok(()) +} + +fn upgrade_ufo_rates(factory_rates: &mut Option) -> Result<(), String> { + let rates = factory_rates + .as_mut() + .ok_or_else(|| "Factory rates not initialized".to_string())?; + + let tokens: RateTokens = RateTokens { + tokens: 1, + updated_at: time(), + }; + + let rate = FactoryRate { + config: DEFAULT_RATE_CONFIG, + tokens: tokens.clone(), + }; + + rates.insert(SegmentKind::Ufo, rate); + + Ok(()) +} From 2c3b91c4f956defc16596f9e83fe9d0520a98ebd Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 11:28:39 +0200 Subject: [PATCH 04/16] feat: controller --- src/console/src/factory/ufo.rs | 13 ++++++++----- src/console/src/memory/upgrade.rs | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/console/src/factory/ufo.rs b/src/console/src/factory/ufo.rs index 1ce252e342..b9ac8e8f93 100644 --- a/src/console/src/factory/ufo.rs +++ b/src/console/src/factory/ufo.rs @@ -3,7 +3,9 @@ use crate::constants::FREEZING_THRESHOLD_ONE_YEAR; use crate::factory::orchestrator::create_segment_with_account; use crate::factory::services::payment::{process_payment_cycles, refund_payment_cycles}; use crate::factory::types::CanisterCreator; -use crate::factory::utils::controllers::update_mission_control_controllers; +use crate::factory::utils::controllers::{ + remove_console_controller, update_mission_control_controllers, +}; use crate::fees::get_factory_fee; use crate::rates::increment_ufo_rate; use crate::segments::add_segment as add_segment_store; @@ -48,13 +50,15 @@ async fn create_raw_canister( creator: CanisterCreator, subnet_id: Option, ) -> Result { - let CanisterCreator::User((user_id, _)) = creator else { + let CanisterCreator::User(_) = creator else { return Err("Mission Control cannot create an UFO".to_string()); }; + let controllers = creator.controllers(); + // We temporarily use the Console as a controller to create the canister but // remove it as soon as it is spin. - let temporary_init_controllers = Vec::from([id(), user_id]); + let temporary_init_controllers = [id()].into_iter().chain(controllers.clone()).collect(); let create_settings_arg = CreateCanisterInitSettingsArg { controllers: temporary_init_controllers, @@ -67,8 +71,7 @@ async fn create_raw_canister( create_canister_with_ic_mgmt(&create_settings_arg, CREATE_UFO_CYCLES).await }?; - // TODO: update controllers only user_id - update_mission_control_controllers(&ufo_id, &user_id).await?; + remove_console_controller(&ufo_id, &controllers).await?; Ok(ufo_id) } diff --git a/src/console/src/memory/upgrade.rs b/src/console/src/memory/upgrade.rs index 6d043f2119..23a9165e90 100644 --- a/src/console/src/memory/upgrade.rs +++ b/src/console/src/memory/upgrade.rs @@ -1,6 +1,6 @@ use crate::constants::UFO_CREATION_FEE_CYCLES; use crate::store::mutate_heap_state; -use crate::types::state::{FactoryFee, FactoryFees, FactoryRate, FactoryRates, HeapState}; +use crate::types::state::{FactoryFee, FactoryFees, FactoryRate, FactoryRates}; use ic_cdk::api::time; use junobuild_shared::ic::api::print; use junobuild_shared::rate::constants::DEFAULT_RATE_CONFIG; From 2b58be18ad94b34c3d01f706ead384bad1b7dcf8 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 11:51:13 +0200 Subject: [PATCH 05/16] test: migration config --- src/console/src/factory/ufo.rs | 2 +- .../upgrade/console.upgrade-v0-4-1.spec.ts | 10 +- .../upgrade/console.upgrade-v0-4-4.spec.ts | 102 ++++++++++++++++++ src/tests/utils/observatory-tests.utils.ts | 10 +- 4 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts diff --git a/src/console/src/factory/ufo.rs b/src/console/src/factory/ufo.rs index b9ac8e8f93..828e8986ff 100644 --- a/src/console/src/factory/ufo.rs +++ b/src/console/src/factory/ufo.rs @@ -4,7 +4,7 @@ use crate::factory::orchestrator::create_segment_with_account; use crate::factory::services::payment::{process_payment_cycles, refund_payment_cycles}; use crate::factory::types::CanisterCreator; use crate::factory::utils::controllers::{ - remove_console_controller, update_mission_control_controllers, + remove_console_controller, }; use crate::fees::get_factory_fee; use crate::rates::increment_ufo_rate; diff --git a/src/tests/specs/console/upgrade/console.upgrade-v0-4-1.spec.ts b/src/tests/specs/console/upgrade/console.upgrade-v0-4-1.spec.ts index 5c0cfb0bf1..cf3a99f0e8 100644 --- a/src/tests/specs/console/upgrade/console.upgrade-v0-4-1.spec.ts +++ b/src/tests/specs/console/upgrade/console.upgrade-v0-4-1.spec.ts @@ -12,11 +12,7 @@ import type { Principal } from '@icp-sdk/core/principal'; import { inject } from 'vitest'; import { CONSOLE_ID } from '../../../constants/console-tests.constants'; import { tick } from '../../../utils/pic-tests.utils'; -import { - CONSOLE_WASM_PATH, - controllersInitArgs, - downloadConsole -} from '../../../utils/setup-tests.utils'; +import { controllersInitArgs, downloadConsole } from '../../../utils/setup-tests.utils'; describe('Console > Upgrade > v0.4.0 -> v0.4.1', () => { let pic: PocketIc; @@ -28,9 +24,11 @@ describe('Console > Upgrade > v0.4.0 -> v0.4.1', () => { const upgrade = async () => { await tick(pic); + const destination = await downloadConsole({ junoVersion: '0.0.69', version: '0.4.1' }); + await pic.upgradeCanister({ canisterId, - wasm: CONSOLE_WASM_PATH, + wasm: destination, sender: controller.getPrincipal() }); }; diff --git a/src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts b/src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts new file mode 100644 index 0000000000..329d617900 --- /dev/null +++ b/src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts @@ -0,0 +1,102 @@ +import { type ConsoleActor, idlFactoryConsole } from '$declarations'; +import { type Actor, PocketIc } from '@dfinity/pic'; +import { toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import type { Principal } from '@icp-sdk/core/principal'; +import { inject } from 'vitest'; +import { CONSOLE_ID } from '../../../constants/console-tests.constants'; +import { tick } from '../../../utils/pic-tests.utils'; +import { + CONSOLE_WASM_PATH, + controllersInitArgs, + downloadConsole +} from '../../../utils/setup-tests.utils'; + +describe('Console > Upgrade > v0.4.3 -> v0.4.4', () => { + let pic: PocketIc; + let actor: Actor; + let canisterId: Principal; + + const controller = Ed25519KeyIdentity.generate(); + + const upgrade = async () => { + await tick(pic); + + await pic.upgradeCanister({ + canisterId, + wasm: CONSOLE_WASM_PATH, + sender: controller.getPrincipal() + }); + }; + + beforeEach(async () => { + pic = await PocketIc.create(inject('PIC_URL')); + + const destination = await downloadConsole({ junoVersion: '0.0.72', version: '0.4.2' }); + + const { actor: c, canisterId: cId } = await pic.setupCanister({ + idlFactory: idlFactoryConsole, + wasm: destination, + arg: controllersInitArgs(controller), + sender: controller.getPrincipal(), + targetCanisterId: CONSOLE_ID + }); + + actor = c; + canisterId = cId; + + actor.setIdentity(controller); + }); + + afterEach(async () => { + await pic?.tearDown(); + }); + + it('should provide fees for ufo', async () => { + await upgrade(); + + const newActor = pic.createActor(idlFactoryConsole, canisterId); + newActor.setIdentity(controller); + + const { get_fee } = newActor; + + const feesWithIcp = [{ Satellite: null }, { Orbiter: null }]; + const feesWithoutIcp = [{ Ufo: null }, { MissionControl: null }]; + + for (const fee of feesWithIcp) { + await expect(get_fee(fee)).resolves.toEqual( + expect.objectContaining({ + fee_icp: toNullable({ e8s: 150_000_000n }), + fee_cycles: { e12s: 3_000_000_000_000n } + }) + ); + } + + for (const fee of feesWithoutIcp) { + await expect(get_fee(fee)).resolves.toEqual( + expect.objectContaining({ + fee_icp: toNullable(), + fee_cycles: { e12s: 3_000_000_000_000n } + }) + ); + } + }); + + it('should provide rates config for ufo', async () => { + await upgrade(); + + const newActor = pic.createActor(idlFactoryConsole, canisterId); + newActor.setIdentity(controller); + + const { get_rate_config } = newActor; + + const rates = [{ Ufo: null }, { MissionControl: null }, { Satellite: null }, { Orbiter: null }]; + + for (const rate of rates) { + await expect(get_rate_config(rate)).resolves.toEqual({ + max_tokens: 100n, + time_per_token_ns: 600_000_000n + }); + } + }); +}); diff --git a/src/tests/utils/observatory-tests.utils.ts b/src/tests/utils/observatory-tests.utils.ts index 15b4b1f612..83f478b269 100644 --- a/src/tests/utils/observatory-tests.utils.ts +++ b/src/tests/utils/observatory-tests.utils.ts @@ -1,4 +1,9 @@ -import type { ObservatoryActor, ObservatoryActor009, ObservatoryDid } from '$declarations'; +import { + type ObservatoryActor, + type ObservatoryActor009, + type ObservatoryDid, + type ObservatoryDid040 +} from '$declarations'; import type { PocketIc } from '@dfinity/pic'; import { assertNonNullish, nonNullish } from '@dfinity/utils'; import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; @@ -61,7 +66,8 @@ export const testDepositedCyclesNotification = async ({ segment: { id: mockMissionControlId, metadata: nonNullish(metadataName) ? [[['name', metadataName]]] : [], - kind + // TODO: cast was added for simplicity reasons when introducing Kind Ufo + kind: kind as ObservatoryDid040.SegmentKind }, user: Ed25519KeyIdentity.generate().getPrincipal() }); From 9ac95ab6e704e3ff610d25be1110bcda48d3dc55 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 12:35:57 +0200 Subject: [PATCH 06/16] test: create segment --- .../factory/console.factory.segments.spec.ts | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/tests/specs/console/factory/console.factory.segments.spec.ts diff --git a/src/tests/specs/console/factory/console.factory.segments.spec.ts b/src/tests/specs/console/factory/console.factory.segments.spec.ts new file mode 100644 index 0000000000..aea766fcc0 --- /dev/null +++ b/src/tests/specs/console/factory/console.factory.segments.spec.ts @@ -0,0 +1,347 @@ +import { type ConsoleActor, type ConsoleDid } from '$declarations'; +import type { Actor, PocketIc } from '@dfinity/pic'; +import { fromNullable, fromNullishNullable, toNullable } from '@dfinity/utils'; +import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; +import type { Principal } from '@icp-sdk/core/principal'; +import { + CONSOLE_ID, + NO_ACCOUNT_ERROR_MSG, + TEST_FEES +} from '../../../constants/console-tests.constants'; +import { CYCLES_LEDGER_ID } from '../../../constants/ledger-tests.contants'; +import { setupConsole } from '../../../utils/console-tests.utils'; +import { approveToken, transferToken } from '../../../utils/ledger-tests.utils'; +import { tick } from '../../../utils/pic-tests.utils'; + +describe('Console > Factory > Segment', () => { + let pic: PocketIc; + let actor: Actor; + let controller: Ed25519KeyIdentity; + + beforeAll(async () => { + const { + pic: p, + actor: c, + controller: cO + } = await setupConsole({ + withApplyRateTokens: true, + withLedger: true, + withSegments: true, + withFee: true + }); + + pic = p; + + controller = cO; + + actor = c; + actor.setIdentity(controller); + }); + + afterAll(async () => { + await pic?.tearDown(); + }); + + const assertMissionControl = async () => { + const { get_account } = actor; + + const account = await get_account(); + + const missionControlId = fromNullishNullable(fromNullable(account)?.mission_control_id); + + expect(missionControlId).not.toBeUndefined(); + }; + + const assertSegments = async ({ + title, + segmentIds + }: { + title: string; + segmentIds: Principal[]; + }) => { + const { list_segments } = actor; + + const segments = await list_segments({ + segment_kind: [ + title === 'UFO' + ? { Ufo: null } + : title === 'Orbiter' + ? { Orbiter: null } + : { Satellite: null } + ], + segment_id: [] + }); + + expect(segments).toHaveLength(segmentIds.length); + + for (const segmentId of segmentIds) { + expect( + segments.find(([_, { segment_id }]) => segment_id.toText() === segmentId.toText()) + ).not.toBeUndefined(); + } + }; + + describe.each([ + { + title: 'Satellite', + args: ({ user }: { user: Ed25519KeyIdentity }): ConsoleDid.CreateSegmentArgs => ({ + Satellite: { + user: user.getPrincipal(), + block_index: toNullable(), + name: toNullable(), + storage: toNullable(), + subnet_id: toNullable() + } + }) + }, + { + title: 'Orbiter', + args: ({ user }: { user: Ed25519KeyIdentity }): ConsoleDid.CreateSegmentArgs => ({ + Orbiter: { + user: user.getPrincipal(), + block_index: toNullable(), + name: toNullable(), + subnet_id: toNullable() + } + }) + }, + { + title: 'UFO', + args: (_: { user: Ed25519KeyIdentity }): ConsoleDid.CreateSegmentArgs => ({ + Ufo: { + name: toNullable(), + subnet_id: toNullable() + } + }) + } + ])('$title', ({ title, args }) => { + let user: Ed25519KeyIdentity; + + beforeEach(() => { + user = Ed25519KeyIdentity.generate(); + actor.setIdentity(user); + }); + + describe('Assertions', () => { + it('should fail with unknown account', async () => { + const { create_segment } = actor; + await expect(create_segment(args({ user }))).rejects.toThrow(NO_ACCOUNT_ERROR_MSG); + }); + }); + + describe('User', () => { + it('should create with user', async () => { + const { get_or_init_account } = actor; + await get_or_init_account(); + + const { create_segment } = actor; + + const id = await create_segment(args({ user })); + + expect(id).not.toBeUndefined(); + + await assertSegments({ + title, + segmentIds: [id] + }); + }); + + it('should fail with without credits and payment', async () => { + const { get_or_init_account, create_segment } = actor; + await get_or_init_account(); + + // First module works out + await create_segment(args({ user })); + + await pic.advanceTime(60_000); + await tick(pic); + + // Second requires payment + await expect(create_segment(args({ user }))).rejects.toThrow('InsufficientAllowance'); + }); + + it('should fail without enough payment', async () => { + const { get_or_init_account, create_segment } = actor; + await get_or_init_account(); + + // First module works out + await create_segment(args({ user })); + + await pic.advanceTime(60_000); + await tick(pic); + + await transferToken({ + pic, + owner: user.getPrincipal(), + ledgerId: CYCLES_LEDGER_ID, + amount: 2n * TEST_FEES.fee_cycles.e12s + }); + + await approveToken({ + pic, + owner: user, + spender: CONSOLE_ID, + ledgerId: CYCLES_LEDGER_ID, + amount: TEST_FEES.fee_cycles.e12s // Fees 100_000_000n are missing + }); + + await tick(pic); + + // Second requires full payment + await expect(create_segment(args({ user }))).rejects.toThrow('InsufficientAllowance'); + }); + + it('should succeed with payment', async () => { + const { get_or_init_account, create_segment } = actor; + await get_or_init_account(); + + // First module works out + const firstId = await create_segment(args({ user })); + + await pic.advanceTime(60_000); + await tick(pic); + + await transferToken({ + pic, + owner: user.getPrincipal(), + ledgerId: CYCLES_LEDGER_ID, + amount: 2n * TEST_FEES.fee_cycles.e12s + }); + + await approveToken({ + pic, + owner: user, + spender: CONSOLE_ID, + ledgerId: CYCLES_LEDGER_ID, + amount: TEST_FEES.fee_cycles.e12s + 100_000_000n + }); + + await tick(pic); + + // Second uses payment + const secondId = await create_segment(args({ user })); + + expect(secondId).not.toBeUndefined(); + + await assertSegments({ + title, + segmentIds: [firstId, secondId] + }); + }); + }); + }); + + describe('Mission Control', () => { + let user: Ed25519KeyIdentity; + + const args: ConsoleDid.CreateSegmentArgs = { MissionControl: { subnet_id: toNullable() } }; + const ufoArgs: ConsoleDid.CreateSegmentArgs = {Ufo: {name: toNullable(), subnet_id: toNullable()}}; + + beforeEach(() => { + user = Ed25519KeyIdentity.generate(); + actor.setIdentity(user); + }); + + describe('Assertions', () => { + it('should fail with unknown account', async () => { + const { create_segment } = actor; + await expect(create_segment(args)).rejects.toThrow(NO_ACCOUNT_ERROR_MSG); + }); + }); + + describe('User', () => { + it('should create with user', async () => { + const { get_or_init_account } = actor; + await get_or_init_account(); + + const { create_segment } = actor; + + const id = await create_segment(args); + + expect(id).not.toBeUndefined(); + + await assertMissionControl(); + }); + + it('should fail with without credits and payment', async () => { + const { get_or_init_account, create_segment } = actor; + await get_or_init_account(); + + // First module works out + await create_segment(ufoArgs); + + await pic.advanceTime(60_000); + await tick(pic); + + // Second requires payment + await expect(create_segment(args)).rejects.toThrow('InsufficientAllowance'); + }); + + it('should fail without enough payment', async () => { + const { get_or_init_account, create_segment } = actor; + await get_or_init_account(); + + // First module works out + await create_segment(ufoArgs); + + await pic.advanceTime(60_000); + await tick(pic); + + await transferToken({ + pic, + owner: user.getPrincipal(), + ledgerId: CYCLES_LEDGER_ID, + amount: 2n * TEST_FEES.fee_cycles.e12s + }); + + await approveToken({ + pic, + owner: user, + spender: CONSOLE_ID, + ledgerId: CYCLES_LEDGER_ID, + amount: TEST_FEES.fee_cycles.e12s // Fees 100_000_000n are missing + }); + + await tick(pic); + + // Second requires full payment + await expect(create_segment(args)).rejects.toThrow('InsufficientAllowance'); + }); + + it('should succeed with payment', async () => { + const { get_or_init_account, create_segment } = actor; + await get_or_init_account(); + + // First module works out + const firstId = await create_segment(ufoArgs); + + await pic.advanceTime(60_000); + await tick(pic); + + await transferToken({ + pic, + owner: user.getPrincipal(), + ledgerId: CYCLES_LEDGER_ID, + amount: 2n * TEST_FEES.fee_cycles.e12s + }); + + await approveToken({ + pic, + owner: user, + spender: CONSOLE_ID, + ledgerId: CYCLES_LEDGER_ID, + amount: TEST_FEES.fee_cycles.e12s + 100_000_000n + }); + + await tick(pic); + + // Second uses payment + const secondId = await create_segment(args); + + expect(secondId).not.toBeUndefined(); + + await assertMissionControl(); + }); + }); + }); +}); From fe52945183ec7ae48683d17a2a5822d08b49c5f9 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 12:40:50 +0200 Subject: [PATCH 07/16] test: fees --- src/tests/utils/console-tests.utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/utils/console-tests.utils.ts b/src/tests/utils/console-tests.utils.ts index 471d3d5fba..8c629efa88 100644 --- a/src/tests/utils/console-tests.utils.ts +++ b/src/tests/utils/console-tests.utils.ts @@ -701,6 +701,7 @@ export const setupConsole = async ({ await set_fee({ Satellite: null }, TEST_FEES); await set_fee({ Orbiter: null }, TEST_FEES); await set_fee({ MissionControl: null }, TEST_FEES); + await set_fee({ Ufo: null }, TEST_FEES); } return { pic, controller, actor, canisterId }; From c4fcb22b64265fe78c286724df6244bd2fc8be32 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 12:52:33 +0200 Subject: [PATCH 08/16] feat: assert controller --- .../factory/console.factory.segments.spec.ts | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/tests/specs/console/factory/console.factory.segments.spec.ts b/src/tests/specs/console/factory/console.factory.segments.spec.ts index aea766fcc0..d8a0a14134 100644 --- a/src/tests/specs/console/factory/console.factory.segments.spec.ts +++ b/src/tests/specs/console/factory/console.factory.segments.spec.ts @@ -10,6 +10,7 @@ import { } from '../../../constants/console-tests.constants'; import { CYCLES_LEDGER_ID } from '../../../constants/ledger-tests.contants'; import { setupConsole } from '../../../utils/console-tests.utils'; +import { canisterStatus } from '../../../utils/ic-management-tests.utils'; import { approveToken, transferToken } from '../../../utils/ledger-tests.utils'; import { tick } from '../../../utils/pic-tests.utils'; @@ -81,6 +82,32 @@ describe('Console > Factory > Segment', () => { } }; + const assertController = async ({ + user, + canisterId, + controllers + }: { + user: Ed25519KeyIdentity; + canisterId: Principal; + controllers: Principal[]; + }) => { + const result = await canisterStatus({ + sender: user, + pic, + canisterId + }); + + const settings = result?.settings; + + expect(settings?.controllers).toHaveLength(controllers.length); + + for (const controller of controllers) { + expect( + settings?.controllers.find((c) => c.toText() === controller.toText()) + ).not.toBeUndefined(); + } + }; + describe.each([ { title: 'Satellite', @@ -146,6 +173,23 @@ describe('Console > Factory > Segment', () => { }); }); + it('should create with expected controllers', async () => { + const { get_or_init_account } = actor; + await get_or_init_account(); + + const { create_segment } = actor; + + const id = await create_segment(args({ user })); + + expect(id).not.toBeUndefined(); + + await assertController({ + user, + canisterId: id, + controllers: [user.getPrincipal()] + }); + }); + it('should fail with without credits and payment', async () => { const { get_or_init_account, create_segment } = actor; await get_or_init_account(); @@ -235,7 +279,9 @@ describe('Console > Factory > Segment', () => { let user: Ed25519KeyIdentity; const args: ConsoleDid.CreateSegmentArgs = { MissionControl: { subnet_id: toNullable() } }; - const ufoArgs: ConsoleDid.CreateSegmentArgs = {Ufo: {name: toNullable(), subnet_id: toNullable()}}; + const ufoArgs: ConsoleDid.CreateSegmentArgs = { + Ufo: { name: toNullable(), subnet_id: toNullable() } + }; beforeEach(() => { user = Ed25519KeyIdentity.generate(); @@ -263,6 +309,23 @@ describe('Console > Factory > Segment', () => { await assertMissionControl(); }); + it('should create with expected controllers', async () => { + const { get_or_init_account } = actor; + await get_or_init_account(); + + const { create_segment } = actor; + + const id = await create_segment(args); + + expect(id).not.toBeUndefined(); + + await assertController({ + user, + canisterId: id, + controllers: [user.getPrincipal(), id] + }); + }); + it('should fail with without credits and payment', async () => { const { get_or_init_account, create_segment } = actor; await get_or_init_account(); From 3da5a29c720c7dafab58961fd66b57b4380c7970 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 12:59:03 +0200 Subject: [PATCH 09/16] chore: fmt --- src/console/src/factory/ufo.rs | 4 +--- .../specs/console/upgrade/console.upgrade-v0-4-4.spec.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/console/src/factory/ufo.rs b/src/console/src/factory/ufo.rs index 828e8986ff..7741af9f39 100644 --- a/src/console/src/factory/ufo.rs +++ b/src/console/src/factory/ufo.rs @@ -3,9 +3,7 @@ use crate::constants::FREEZING_THRESHOLD_ONE_YEAR; use crate::factory::orchestrator::create_segment_with_account; use crate::factory::services::payment::{process_payment_cycles, refund_payment_cycles}; use crate::factory::types::CanisterCreator; -use crate::factory::utils::controllers::{ - remove_console_controller, -}; +use crate::factory::utils::controllers::remove_console_controller; use crate::fees::get_factory_fee; use crate::rates::increment_ufo_rate; use crate::segments::add_segment as add_segment_store; diff --git a/src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts b/src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts index 329d617900..f621a1914f 100644 --- a/src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts +++ b/src/tests/specs/console/upgrade/console.upgrade-v0-4-4.spec.ts @@ -84,7 +84,7 @@ describe('Console > Upgrade > v0.4.3 -> v0.4.4', () => { it('should provide rates config for ufo', async () => { await upgrade(); - + const newActor = pic.createActor(idlFactoryConsole, canisterId); newActor.setIdentity(controller); From 7a77c4f725bcf109a1ab1e73fa7bd35419fdc185 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 13:05:57 +0200 Subject: [PATCH 10/16] chore: fmt --- .../console/factory/console.factory.segments.spec.ts | 4 +++- src/tests/utils/observatory-tests.utils.ts | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/tests/specs/console/factory/console.factory.segments.spec.ts b/src/tests/specs/console/factory/console.factory.segments.spec.ts index d8a0a14134..c0f815e117 100644 --- a/src/tests/specs/console/factory/console.factory.segments.spec.ts +++ b/src/tests/specs/console/factory/console.factory.segments.spec.ts @@ -1,4 +1,4 @@ -import { type ConsoleActor, type ConsoleDid } from '$declarations'; +import type { ConsoleActor, ConsoleDid } from '$declarations'; import type { Actor, PocketIc } from '@dfinity/pic'; import { fromNullable, fromNullishNullable, toNullable } from '@dfinity/utils'; import { Ed25519KeyIdentity } from '@icp-sdk/core/identity'; @@ -152,6 +152,7 @@ describe('Console > Factory > Segment', () => { describe('Assertions', () => { it('should fail with unknown account', async () => { const { create_segment } = actor; + await expect(create_segment(args({ user }))).rejects.toThrow(NO_ACCOUNT_ERROR_MSG); }); }); @@ -291,6 +292,7 @@ describe('Console > Factory > Segment', () => { describe('Assertions', () => { it('should fail with unknown account', async () => { const { create_segment } = actor; + await expect(create_segment(args)).rejects.toThrow(NO_ACCOUNT_ERROR_MSG); }); }); diff --git a/src/tests/utils/observatory-tests.utils.ts b/src/tests/utils/observatory-tests.utils.ts index 83f478b269..9fe4a2d0bf 100644 --- a/src/tests/utils/observatory-tests.utils.ts +++ b/src/tests/utils/observatory-tests.utils.ts @@ -1,8 +1,8 @@ -import { - type ObservatoryActor, - type ObservatoryActor009, - type ObservatoryDid, - type ObservatoryDid040 +import type { + ObservatoryActor, + ObservatoryActor009, + ObservatoryDid, + ObservatoryDid040 } from '$declarations'; import type { PocketIc } from '@dfinity/pic'; import { assertNonNullish, nonNullish } from '@dfinity/utils'; From 4cb91e6bf21687449814578f594128b951f507ea Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 13:29:29 +0200 Subject: [PATCH 11/16] feat(mission-control): attach and monitor ufo --- src/libs/shared/src/types.rs | 1 + src/mission_control/src/api/mod.rs | 1 + src/mission_control/src/api/ufos.rs | 27 ++++++ src/mission_control/src/factory/mod.rs | 3 +- src/mission_control/src/factory/msg.rs | 1 + src/mission_control/src/factory/store.rs | 64 +++++++++++++- src/mission_control/src/factory/ufo.rs | 32 +++++++ src/mission_control/src/impls.rs | 70 +++++++++++++++- src/mission_control/src/lib.rs | 3 + .../src/monitoring/cycles/config.rs | 12 ++- .../src/monitoring/cycles/start.rs | 19 ++++- .../src/monitoring/cycles/stop.rs | 15 +++- .../src/monitoring/store/heap.rs | 84 ++++++++++++++++++- src/mission_control/src/types.rs | 19 ++++- 14 files changed, 338 insertions(+), 13 deletions(-) create mode 100644 src/mission_control/src/api/ufos.rs create mode 100644 src/mission_control/src/factory/ufo.rs diff --git a/src/libs/shared/src/types.rs b/src/libs/shared/src/types.rs index b5ad9983a1..7adfe6540d 100644 --- a/src/libs/shared/src/types.rs +++ b/src/libs/shared/src/types.rs @@ -15,6 +15,7 @@ pub mod state { pub type MissionControlId = SegmentId; pub type SatelliteId = SegmentId; pub type OrbiterId = SegmentId; + pub type UfoId = SegmentId; pub type Metadata = HashMap; diff --git a/src/mission_control/src/api/mod.rs b/src/mission_control/src/api/mod.rs index f8e40c85a7..2005a271dc 100644 --- a/src/mission_control/src/api/mod.rs +++ b/src/mission_control/src/api/mod.rs @@ -4,4 +4,5 @@ mod mgmt; mod monitoring; mod orbiters; mod satellites; +mod ufos; mod wallet; diff --git a/src/mission_control/src/api/ufos.rs b/src/mission_control/src/api/ufos.rs new file mode 100644 index 0000000000..ca42c4d1f0 --- /dev/null +++ b/src/mission_control/src/api/ufos.rs @@ -0,0 +1,27 @@ +use crate::factory::store::{get_ufos, set_ufo_metadata as set_ufo_metadata_store}; +use crate::factory::ufo::{attach_ufo, detach_ufo}; +use crate::guards::caller_is_user_or_admin_controller; +use crate::types::state::{Ufo, Ufos}; +use ic_cdk_macros::{query, update}; +use junobuild_shared::ic::UnwrapOrTrap; +use junobuild_shared::types::state::{Metadata, UfoId}; + +#[query(guard = "caller_is_user_or_admin_controller")] +fn list_ufos() -> Ufos { + get_ufos() +} + +#[update(guard = "caller_is_user_or_admin_controller")] +fn set_ufo(ufo_id: UfoId, name: Option) -> Ufo { + attach_ufo(&ufo_id, &name).unwrap_or_trap() +} + +#[update(guard = "caller_is_user_or_admin_controller")] +fn unset_ufo(ufo_id: UfoId) { + detach_ufo(&ufo_id).unwrap_or_trap() +} + +#[update(guard = "caller_is_user_or_admin_controller")] +fn set_ufo_metadata(ufo_id: UfoId, metadata: Metadata) -> Ufo { + set_ufo_metadata_store(&ufo_id, &metadata).unwrap_or_trap() +} diff --git a/src/mission_control/src/factory/mod.rs b/src/mission_control/src/factory/mod.rs index 27921e5cfc..c92c8809f0 100644 --- a/src/mission_control/src/factory/mod.rs +++ b/src/mission_control/src/factory/mod.rs @@ -1,5 +1,6 @@ -mod canister; +pub mod canister; mod msg; pub mod orbiter; pub mod satellite; pub mod store; +pub mod ufo; diff --git a/src/mission_control/src/factory/msg.rs b/src/mission_control/src/factory/msg.rs index 9f0a405ae4..34f5c9644d 100644 --- a/src/mission_control/src/factory/msg.rs +++ b/src/mission_control/src/factory/msg.rs @@ -1,2 +1,3 @@ pub const ORBITER_NOT_FOUND: &str = "Orbiter not found or not owned by this mission control."; pub const SATELLITE_NOT_FOUND: &str = "Satellite not found or not owned by this mission control."; +pub const UFO_NOT_FOUND: &str = "UFO not found or not owned by this mission control."; diff --git a/src/mission_control/src/factory/store.rs b/src/mission_control/src/factory/store.rs index 03df98202f..7765f0f39c 100644 --- a/src/mission_control/src/factory/store.rs +++ b/src/mission_control/src/factory/store.rs @@ -1,7 +1,7 @@ use crate::memory::manager::STATE; use crate::types::core::Segment; -use crate::types::state::{Orbiter, Orbiters, Satellite, Satellites}; -use junobuild_shared::types::state::{Metadata, OrbiterId, SatelliteId}; +use crate::types::state::{Orbiter, Orbiters, Satellite, Satellites, Ufo, Ufos}; +use junobuild_shared::types::state::{Metadata, OrbiterId, SatelliteId, UfoId}; use std::collections::HashMap; use std::hash::Hash; @@ -79,6 +79,66 @@ pub fn set_orbiter_metadata( }) } +// --------------------------------------------------------- +// UFOs +// --------------------------------------------------------- + +pub fn get_ufos() -> Ufos { + STATE.with(|state| state.borrow().heap.ufos.clone().unwrap_or_default()) +} + +pub fn get_ufo(ufo_id: &UfoId) -> Option { + STATE.with(|state| { + state + .borrow() + .heap + .ufos + .as_ref() + .and_then(|ufos| get_segment_impl(ufo_id, ufos)) + }) +} + +pub fn delete_ufo(ufo_id: &UfoId) -> Option { + STATE.with(|state| { + delete_segment_impl( + ufo_id, + state + .borrow_mut() + .heap + .ufos + .get_or_insert_with(HashMap::new), + ) + }) +} + +pub fn add_ufo(ufo_id: &UfoId, name: &Option) -> Ufo { + STATE.with(|state| { + add_segment_impl( + ufo_id, + &Ufo::from(ufo_id, name), + state + .borrow_mut() + .heap + .ufos + .get_or_insert_with(HashMap::new), + ) + }) +} + +pub fn set_ufo_metadata(ufo_id: &UfoId, metadata: &Metadata) -> Result { + STATE.with(|state| { + set_metadata_impl( + ufo_id, + metadata, + state + .borrow_mut() + .heap + .ufos + .get_or_insert_with(HashMap::new), + ) + }) +} + // --------------------------------------------------------- // Segments // --------------------------------------------------------- diff --git a/src/mission_control/src/factory/ufo.rs b/src/mission_control/src/factory/ufo.rs new file mode 100644 index 0000000000..9402636f78 --- /dev/null +++ b/src/mission_control/src/factory/ufo.rs @@ -0,0 +1,32 @@ +use crate::factory::msg::UFO_NOT_FOUND; +use crate::factory::store::{add_ufo, delete_ufo, get_ufo}; +use crate::types::state::Ufo; +use junobuild_shared::types::state::UfoId; + +pub fn attach_ufo(ufo_id: &UfoId, name: &Option) -> Result { + let ufo = get_ufo(ufo_id); + + match ufo { + Some(_) => Err("UFO already added to mission control.".to_string()), + None => { + // No assertion for Ufo + + let ufo = add_ufo(ufo_id, name); + + Ok(ufo) + } + } +} + +pub fn detach_ufo(ufo_id: &UfoId) -> Result<(), String> { + let ufo = get_ufo(ufo_id); + + match ufo { + None => Err(UFO_NOT_FOUND.to_string()), + Some(_ufo) => { + delete_ufo(ufo_id); + + Ok(()) + } + } +} diff --git a/src/mission_control/src/impls.rs b/src/mission_control/src/impls.rs index 065bd7ff22..11f4a116eb 100644 --- a/src/mission_control/src/impls.rs +++ b/src/mission_control/src/impls.rs @@ -5,7 +5,7 @@ use crate::types::state::CyclesMonitoringStrategy::BelowThreshold; use crate::types::state::{ Config, CyclesMonitoring, CyclesMonitoringStrategy, HeapState, MissionControlSettings, Monitoring, MonitoringHistory, MonitoringHistoryKey, Orbiter, Orbiters, Satellite, Settings, - State, User, + State, Ufo, User, }; use canfund::manager::options::{CyclesThreshold, FundStrategy}; use ic_cdk::api::time; @@ -14,7 +14,7 @@ use ic_stable_structures::Storable; use junobuild_shared::memory::serializers::{ deserialize_from_bytes, serialize_into_bytes, serialize_to_bytes, }; -use junobuild_shared::types::state::{Metadata, OrbiterId, SatelliteId, UserId}; +use junobuild_shared::types::state::{Metadata, OrbiterId, SatelliteId, UfoId, UserId}; use std::borrow::Cow; use std::collections::HashMap; @@ -35,6 +35,7 @@ impl From<&UserId> for HeapState { controllers: HashMap::new(), orbiters: Orbiters::new(), settings: None, + ufos: None, } } } @@ -166,6 +167,59 @@ impl Orbiter { } } +impl Ufo { + pub fn from(ufo_id: &UfoId, name: &Option) -> Self { + let now = time(); + + Ufo { + ufo_id: *ufo_id, + metadata: init_metadata(name), + settings: None, + created_at: now, + updated_at: now, + } + } + + pub fn clone_with_settings(&self, settings: &Settings) -> Self { + let now = time(); + + Ufo { + settings: Some(settings.clone()), + updated_at: now, + ..self.clone() + } + } + + pub fn toggle_cycles_monitoring(&self, enabled: bool) -> Result { + let settings = self + .settings + .clone() + .ok_or_else(|| "Settings not found.".to_string())?; + + let monitoring = settings + .monitoring + .clone() + .ok_or_else(|| "Monitoring configuration not found.".to_string())?; + + let cycles = monitoring + .cycles + .clone() + .ok_or_else(|| "Cycles monitoring configuration not found.".to_string())?; + + let now = time(); + + Ok(Ufo { + settings: Some(Settings { + monitoring: Some(Monitoring { + cycles: Some(CyclesMonitoring { enabled, ..cycles }), + }), + }), + updated_at: now, + ..self.clone() + }) + } +} + impl Settings { pub fn from(strategy: &CyclesMonitoringStrategy) -> Self { Settings { @@ -215,6 +269,18 @@ impl Segment for Orbiter { } } +impl Segment for Ufo { + fn set_metadata(&self, metadata: &Metadata) -> Self { + let now = time(); + + Ufo { + metadata: metadata.clone(), + updated_at: now, + ..self.clone() + } + } +} + impl CyclesMonitoringStrategy { pub fn to_fund_strategy(&self) -> Result { #[allow(unreachable_patterns)] diff --git a/src/mission_control/src/lib.rs b/src/mission_control/src/lib.rs index 308be995cc..a1559908bc 100644 --- a/src/mission_control/src/lib.rs +++ b/src/mission_control/src/lib.rs @@ -26,6 +26,8 @@ use crate::types::state::Orbiter; use crate::types::state::Orbiters; use crate::types::state::Satellite; use crate::types::state::Satellites; +use crate::types::state::Ufo; +use crate::types::state::Ufos; use crate::types::state::User; use candid::Principal; use ic_cdk_macros::export_candid; @@ -39,6 +41,7 @@ use junobuild_shared::types::interface::InitMissionControlArgs; use junobuild_shared::types::interface::SetAccessKey; use junobuild_shared::types::state::Metadata; use junobuild_shared::types::state::SatelliteId; +use junobuild_shared::types::state::UfoId; use junobuild_shared::types::state::UserId; use junobuild_shared::types::state::{AccessKeyId, AccessKeys, OrbiterId}; diff --git a/src/mission_control/src/monitoring/cycles/config.rs b/src/mission_control/src/monitoring/cycles/config.rs index b08d3587b5..70f2083027 100644 --- a/src/mission_control/src/monitoring/cycles/config.rs +++ b/src/mission_control/src/monitoring/cycles/config.rs @@ -6,8 +6,8 @@ use crate::monitoring::cycles::unregister::{ unregister_mission_control_monitoring, unregister_modules_monitoring, }; use crate::monitoring::store::heap::{ - disable_orbiter_monitoring, disable_satellite_monitoring, set_orbiter_strategy, - set_satellite_strategy, + disable_orbiter_monitoring, disable_satellite_monitoring, disable_ufo_monitoring, + set_orbiter_strategy, set_satellite_strategy, set_ufo_strategy, }; use crate::types::interface::{CyclesMonitoringStartConfig, CyclesMonitoringStopConfig}; @@ -26,6 +26,10 @@ pub fn register_and_start_cycles_monitoring( register_modules_monitoring(strategy, set_orbiter_strategy)?; } + if let Some(strategy) = &config.ufos_strategy { + register_modules_monitoring(strategy, set_ufo_strategy)?; + } + start_scheduler(); Ok(()) @@ -42,6 +46,10 @@ pub fn unregister_and_stop_cycles_monitoring( unregister_modules_monitoring(orbiter_ids, disable_orbiter_monitoring)?; } + if let Some(ufo_ids) = &config.ufo_ids { + unregister_modules_monitoring(ufo_ids, disable_ufo_monitoring)?; + } + if let Some(try_mission_control) = config.try_mission_control { if try_mission_control { unregister_mission_control_monitoring()?; diff --git a/src/mission_control/src/monitoring/cycles/start.rs b/src/mission_control/src/monitoring/cycles/start.rs index d6bfa6080f..9a110dc6b4 100644 --- a/src/mission_control/src/monitoring/cycles/start.rs +++ b/src/mission_control/src/monitoring/cycles/start.rs @@ -1,4 +1,4 @@ -use crate::factory::store::{get_orbiters, get_satellites}; +use crate::factory::store::{get_orbiters, get_satellites, get_ufos}; use crate::memory::manager::RUNTIME_STATE; use crate::monitoring::cycles::funding::init_funding_manager; use crate::monitoring::cycles::funding::register_cycles_monitoring; @@ -7,6 +7,7 @@ use crate::monitoring::cycles::scheduler::{ }; use crate::monitoring::store::heap::{ enable_mission_control_monitoring, enable_orbiter_monitoring, enable_satellite_monitoring, + enable_ufo_monitoring, }; use crate::types::core::SettingsMonitoring; use crate::types::runtime::RuntimeState; @@ -39,6 +40,7 @@ pub fn start_cycles_monitoring(enabled_only: bool) -> Result<(), String> { fn register_strategies(enabled_only: bool) -> Result<(), String> { let satellites = get_satellites(); let orbiters = get_orbiters(); + let ufos = get_ufos(); fn map_strategy( segment_id: &SegmentId, @@ -67,6 +69,10 @@ fn register_strategies(enabled_only: bool) -> Result<(), String> { .iter() .flat_map(|(orbiter_id, orbiter)| map_strategy(orbiter_id, &orbiter.settings, enabled_only)) .collect(); + let ufos_strategies: Vec = ufos + .iter() + .flat_map(|(ufo_id, ufo)| map_strategy(ufo_id, &ufo.settings, enabled_only)) + .collect(); if !satellites_strategies.is_empty() { register_cycles_monitoring_with_settings( @@ -90,6 +96,17 @@ fn register_strategies(enabled_only: bool) -> Result<(), String> { )?; } + if !ufos_strategies.is_empty() { + register_cycles_monitoring_with_settings( + &ufos_strategies, + if enabled_only { + None + } else { + Some(enable_ufo_monitoring) + }, + )?; + } + let mission_control_strategy = map_strategy(&id(), &get_settings(), enabled_only); if let Some(mission_control_strategy) = mission_control_strategy { diff --git a/src/mission_control/src/monitoring/cycles/stop.rs b/src/mission_control/src/monitoring/cycles/stop.rs index 847e476376..f7f249d058 100644 --- a/src/mission_control/src/monitoring/cycles/stop.rs +++ b/src/mission_control/src/monitoring/cycles/stop.rs @@ -1,9 +1,11 @@ -use crate::factory::store::{get_orbiters, get_satellites}; +use crate::factory::store::{get_orbiters, get_satellites, get_ufos}; use crate::monitoring::cycles::scheduler::{assert_scheduler_running, stop_scheduler}; use crate::monitoring::cycles::unregister::{ unregister_mission_control_monitoring, unregister_modules_monitoring, }; -use crate::monitoring::store::heap::{disable_orbiter_monitoring, disable_satellite_monitoring}; +use crate::monitoring::store::heap::{ + disable_orbiter_monitoring, disable_satellite_monitoring, disable_ufo_monitoring, +}; use crate::types::core::SettingsMonitoring; use junobuild_shared::types::state::SegmentId; @@ -20,6 +22,7 @@ pub fn stop_cycles_monitoring() -> Result<(), String> { fn unregister_strategies() -> Result<(), String> { let satellites = get_satellites(); let orbiters = get_orbiters(); + let ufos = get_ufos(); fn filter_enabled_strategy(segment_id: &SegmentId, settings: &Option) -> Option where @@ -43,6 +46,10 @@ fn unregister_strategies() -> Result<(), String> { .iter() .filter_map(|(orbiter_id, orbiter)| filter_enabled_strategy(orbiter_id, &orbiter.settings)) .collect(); + let ufo_ids: Vec = ufos + .iter() + .filter_map(|(ufo_id, ufo)| filter_enabled_strategy(ufo_id, &ufo.settings)) + .collect(); if !satellite_ids.is_empty() { unregister_modules_monitoring(&satellite_ids, disable_satellite_monitoring)?; @@ -52,6 +59,10 @@ fn unregister_strategies() -> Result<(), String> { unregister_modules_monitoring(&orbiter_ids, disable_orbiter_monitoring)?; } + if !ufo_ids.is_empty() { + unregister_modules_monitoring(&ufo_ids, disable_ufo_monitoring)?; + } + unregister_mission_control_monitoring()?; Ok(()) diff --git a/src/mission_control/src/monitoring/store/heap.rs b/src/mission_control/src/monitoring/store/heap.rs index 78de6e4474..3dce603fab 100644 --- a/src/mission_control/src/monitoring/store/heap.rs +++ b/src/mission_control/src/monitoring/store/heap.rs @@ -1,8 +1,10 @@ use crate::memory::manager::STATE; use crate::types::state::{ CyclesMonitoringStrategy, HeapState, MissionControlSettings, Orbiters, Satellites, Settings, + Ufos, }; -use junobuild_shared::types::state::{OrbiterId, SatelliteId}; +use junobuild_shared::types::state::{OrbiterId, SatelliteId, UfoId}; +use std::collections::HashMap; pub fn set_mission_control_strategy(strategy: &CyclesMonitoringStrategy) { STATE.with(|state| set_mission_control_strategy_impl(strategy, &mut state.borrow_mut().heap)) @@ -38,6 +40,20 @@ pub fn set_orbiter_strategy( }) } +pub fn set_ufo_strategy(ufo_id: &UfoId, strategy: &CyclesMonitoringStrategy) -> Result<(), String> { + STATE.with(|state| { + set_ufo_setting_impl( + ufo_id, + strategy, + state + .borrow_mut() + .heap + .ufos + .get_or_insert_with(HashMap::new), + ) + }) +} + pub fn enable_satellite_monitoring(satellite_id: &SatelliteId) -> Result<(), String> { STATE.with(|state| { toggle_satellite_monitoring_impl( @@ -54,6 +70,20 @@ pub fn enable_orbiter_monitoring(orbiter_id: &OrbiterId) -> Result<(), String> { }) } +pub fn enable_ufo_monitoring(ufo_id: &UfoId) -> Result<(), String> { + STATE.with(|state| { + toggle_ufo_monitoring_impl( + ufo_id, + true, + state + .borrow_mut() + .heap + .ufos + .get_or_insert_with(HashMap::new), + ) + }) +} + pub fn disable_satellite_monitoring(satellite_id: &SatelliteId) -> Result<(), String> { STATE.with(|state| { toggle_satellite_monitoring_impl( @@ -70,6 +100,20 @@ pub fn disable_orbiter_monitoring(orbiter_id: &OrbiterId) -> Result<(), String> }) } +pub fn disable_ufo_monitoring(ufo_id: &UfoId) -> Result<(), String> { + STATE.with(|state| { + toggle_ufo_monitoring_impl( + ufo_id, + false, + state + .borrow_mut() + .heap + .ufos + .get_or_insert_with(HashMap::new), + ) + }) +} + fn set_mission_control_strategy_impl(strategy: &CyclesMonitoringStrategy, state: &mut HeapState) { state.settings = Some( state @@ -153,6 +197,25 @@ fn set_orbiter_setting_impl( Ok(()) } +fn set_ufo_setting_impl( + ufo_id: &UfoId, + strategy: &CyclesMonitoringStrategy, + ufos: &mut Ufos, +) -> Result<(), String> { + let ufo = ufos.get(ufo_id).ok_or_else(|| { + format!( + "UFO {} not found. Strategy cannot be saved.", + ufo_id.to_text() + ) + })?; + + let update_ufo = ufo.clone_with_settings(&Settings::from(strategy)); + + ufos.insert(*ufo_id, update_ufo); + + Ok(()) +} + fn toggle_orbiter_monitoring_impl( orbiter_id: &OrbiterId, enabled: bool, @@ -171,3 +234,22 @@ fn toggle_orbiter_monitoring_impl( Ok(()) } + +fn toggle_ufo_monitoring_impl( + ufo_id: &UfoId, + enabled: bool, + ufos: &mut Ufos, +) -> Result<(), String> { + let ufo = ufos.get(ufo_id).ok_or_else(|| { + format!( + "UFO {} not found. Monitoring cannot be disabled.", + ufo_id.to_text() + ) + })?; + + let update_ufo = ufo.toggle_cycles_monitoring(enabled)?; + + ufos.insert(*ufo_id, update_ufo); + + Ok(()) +} diff --git a/src/mission_control/src/types.rs b/src/mission_control/src/types.rs index 80b7a30f34..24c5ae0226 100644 --- a/src/mission_control/src/types.rs +++ b/src/mission_control/src/types.rs @@ -4,13 +4,16 @@ pub mod state { use ic_stable_structures::StableBTreeMap; use junobuild_shared::types::memory::Memory; use junobuild_shared::types::monitoring::{CyclesBalance, FundingFailure}; - use junobuild_shared::types::state::{AccessKeys, Metadata, OrbiterId, SegmentId, Timestamp}; + use junobuild_shared::types::state::{ + AccessKeys, Metadata, OrbiterId, SegmentId, Timestamp, UfoId, + }; use junobuild_shared::types::state::{SatelliteId, UserId}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; pub type Satellites = HashMap; pub type Orbiters = HashMap; + pub type Ufos = HashMap; pub type MonitoringHistoryStable = StableBTreeMap; @@ -35,6 +38,7 @@ pub mod state { pub controllers: AccessKeys, pub orbiters: Orbiters, pub settings: Option, + pub ufos: Option, } #[derive(Default, CandidType, Serialize, Deserialize, Clone)] @@ -89,6 +93,15 @@ pub mod state { pub updated_at: Timestamp, } + #[derive(CandidType, Serialize, Deserialize, Clone)] + pub struct Ufo { + pub ufo_id: UfoId, + pub metadata: Metadata, + pub settings: Option, + pub created_at: Timestamp, + pub updated_at: Timestamp, + } + #[derive(Default, CandidType, Serialize, Deserialize, Clone)] pub struct Settings { pub monitoring: Option, @@ -171,7 +184,7 @@ pub mod interface { use candid::CandidType; use junobuild_shared::mgmt::types::cmc::SubnetId; use junobuild_shared::types::interface::InitStorageArgs; - use junobuild_shared::types::state::{OrbiterId, SatelliteId, SegmentId, Timestamp}; + use junobuild_shared::types::state::{OrbiterId, SatelliteId, SegmentId, Timestamp, UfoId}; use serde::{Deserialize, Serialize}; #[derive(CandidType, Serialize, Deserialize, Clone)] @@ -198,6 +211,7 @@ pub mod interface { pub mission_control_strategy: Option, pub satellites_strategy: Option, pub orbiters_strategy: Option, + pub ufos_strategy: Option, } #[derive(CandidType, Serialize, Deserialize, Clone)] @@ -205,6 +219,7 @@ pub mod interface { pub try_mission_control: Option, pub satellite_ids: Option>, pub orbiter_ids: Option>, + pub ufo_ids: Option>, } #[derive(CandidType, Serialize, Deserialize, Clone)] From 8efe5b777ac1bd05f77c7f22ddf76dbf89634233 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 13:30:39 +0200 Subject: [PATCH 12/16] feat: no need to be exposed --- src/mission_control/src/factory/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mission_control/src/factory/mod.rs b/src/mission_control/src/factory/mod.rs index c92c8809f0..04c26f9214 100644 --- a/src/mission_control/src/factory/mod.rs +++ b/src/mission_control/src/factory/mod.rs @@ -1,4 +1,4 @@ -pub mod canister; +mod canister; mod msg; pub mod orbiter; pub mod satellite; From ab51a3c61a387f6a1b14506860db869260153a1c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 14:13:38 +0200 Subject: [PATCH 13/16] feat: did --- .../mission_control/mission_control.did.d.ts | 13 +++++++++++++ .../mission_control.factory.certified.did.js | 17 +++++++++++++++-- .../mission_control.factory.did.js | 17 +++++++++++++++-- src/mission_control/mission_control.did | 13 +++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/declarations/mission_control/mission_control.did.d.ts b/src/declarations/mission_control/mission_control.did.d.ts index 588108d405..85ece06682 100644 --- a/src/declarations/mission_control/mission_control.did.d.ts +++ b/src/declarations/mission_control/mission_control.did.d.ts @@ -52,6 +52,7 @@ export interface CyclesMonitoringStartConfig { orbiters_strategy: [] | [SegmentsMonitoringStrategy]; mission_control_strategy: [] | [CyclesMonitoringStrategy]; satellites_strategy: [] | [SegmentsMonitoringStrategy]; + ufos_strategy: [] | [SegmentsMonitoringStrategy]; } export interface CyclesMonitoringStatus { monitored_ids: Array; @@ -61,6 +62,7 @@ export interface CyclesMonitoringStopConfig { satellite_ids: [] | [Array]; try_mission_control: [] | [boolean]; orbiter_ids: [] | [Array]; + ufo_ids: [] | [Array]; } export type CyclesMonitoringStrategy = { BelowThreshold: CyclesThreshold }; export interface CyclesThreshold { @@ -200,6 +202,13 @@ export type TransferError_1 = | { CreatedInFuture: { ledger_time: bigint } } | { TooOld: null } | { InsufficientFunds: { balance: bigint } }; +export interface Ufo { + updated_at: bigint; + metadata: Array<[string, string]>; + created_at: bigint; + settings: [] | [Settings]; + ufo_id: Principal; +} export interface User { updated_at: bigint; metadata: Array<[string, string]>; @@ -233,6 +242,7 @@ export interface _SERVICE { list_mission_control_controllers: ActorMethod<[], Array<[Principal, AccessKey]>>; list_orbiters: ActorMethod<[], Array<[Principal, Orbiter]>>; list_satellites: ActorMethod<[], Array<[Principal, Satellite]>>; + list_ufos: ActorMethod<[], Array<[Principal, Ufo]>>; set_config: ActorMethod<[[] | [Config]], undefined>; set_metadata: ActorMethod<[Array<[string, string]>], undefined>; set_mission_control_controllers: ActorMethod<[Array, SetAccessKey], undefined>; @@ -248,11 +258,14 @@ export interface _SERVICE { [Array, Array, SetAccessKey], undefined >; + set_ufo: ActorMethod<[Principal, [] | [string]], Ufo>; + set_ufo_metadata: ActorMethod<[Principal, Array<[string, string]>], Ufo>; start_monitoring: ActorMethod<[], undefined>; stop_monitoring: ActorMethod<[], undefined>; top_up: ActorMethod<[Principal, Tokens], undefined>; unset_orbiter: ActorMethod<[Principal], undefined>; unset_satellite: ActorMethod<[Principal], undefined>; + unset_ufo: ActorMethod<[Principal], undefined>; update_and_start_monitoring: ActorMethod<[MonitoringStartConfig], undefined>; update_and_stop_monitoring: ActorMethod<[MonitoringStopConfig], undefined>; } diff --git a/src/declarations/mission_control/mission_control.factory.certified.did.js b/src/declarations/mission_control/mission_control.factory.certified.did.js index 5959ee89fb..ab08d403d3 100644 --- a/src/declarations/mission_control/mission_control.factory.certified.did.js +++ b/src/declarations/mission_control/mission_control.factory.certified.did.js @@ -180,6 +180,13 @@ export const idlFactory = ({ IDL }) => { scope: AccessKeyScope, expires_at: IDL.Opt(IDL.Nat64) }); + const Ufo = IDL.Record({ + updated_at: IDL.Nat64, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + created_at: IDL.Nat64, + settings: IDL.Opt(Settings), + ufo_id: IDL.Principal + }); const SetAccessKey = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(AccessKeyKind), @@ -193,7 +200,8 @@ export const idlFactory = ({ IDL }) => { const CyclesMonitoringStartConfig = IDL.Record({ orbiters_strategy: IDL.Opt(SegmentsMonitoringStrategy), mission_control_strategy: IDL.Opt(CyclesMonitoringStrategy), - satellites_strategy: IDL.Opt(SegmentsMonitoringStrategy) + satellites_strategy: IDL.Opt(SegmentsMonitoringStrategy), + ufos_strategy: IDL.Opt(SegmentsMonitoringStrategy) }); const MonitoringStartConfig = IDL.Record({ cycles_config: IDL.Opt(CyclesMonitoringStartConfig) @@ -201,7 +209,8 @@ export const idlFactory = ({ IDL }) => { const CyclesMonitoringStopConfig = IDL.Record({ satellite_ids: IDL.Opt(IDL.Vec(IDL.Principal)), try_mission_control: IDL.Opt(IDL.Bool), - orbiter_ids: IDL.Opt(IDL.Vec(IDL.Principal)) + orbiter_ids: IDL.Opt(IDL.Vec(IDL.Principal)), + ufo_ids: IDL.Opt(IDL.Vec(IDL.Principal)) }); const MonitoringStopConfig = IDL.Record({ cycles_config: IDL.Opt(CyclesMonitoringStopConfig) @@ -238,6 +247,7 @@ export const idlFactory = ({ IDL }) => { ), list_orbiters: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Orbiter))], []), list_satellites: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Satellite))], []), + list_ufos: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Ufo))], []), set_config: IDL.Func([IDL.Opt(Config)], [], []), set_metadata: IDL.Func([IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))], [], []), set_mission_control_controllers: IDL.Func([IDL.Vec(IDL.Principal), SetAccessKey], [], []), @@ -263,11 +273,14 @@ export const idlFactory = ({ IDL }) => { [], [] ), + set_ufo: IDL.Func([IDL.Principal, IDL.Opt(IDL.Text)], [Ufo], []), + set_ufo_metadata: IDL.Func([IDL.Principal, IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))], [Ufo], []), start_monitoring: IDL.Func([], [], []), stop_monitoring: IDL.Func([], [], []), top_up: IDL.Func([IDL.Principal, Tokens], [], []), unset_orbiter: IDL.Func([IDL.Principal], [], []), unset_satellite: IDL.Func([IDL.Principal], [], []), + unset_ufo: IDL.Func([IDL.Principal], [], []), update_and_start_monitoring: IDL.Func([MonitoringStartConfig], [], []), update_and_stop_monitoring: IDL.Func([MonitoringStopConfig], [], []) }); diff --git a/src/declarations/mission_control/mission_control.factory.did.js b/src/declarations/mission_control/mission_control.factory.did.js index 065e439c53..a2e0a9668e 100644 --- a/src/declarations/mission_control/mission_control.factory.did.js +++ b/src/declarations/mission_control/mission_control.factory.did.js @@ -180,6 +180,13 @@ export const idlFactory = ({ IDL }) => { scope: AccessKeyScope, expires_at: IDL.Opt(IDL.Nat64) }); + const Ufo = IDL.Record({ + updated_at: IDL.Nat64, + metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), + created_at: IDL.Nat64, + settings: IDL.Opt(Settings), + ufo_id: IDL.Principal + }); const SetAccessKey = IDL.Record({ metadata: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)), kind: IDL.Opt(AccessKeyKind), @@ -193,7 +200,8 @@ export const idlFactory = ({ IDL }) => { const CyclesMonitoringStartConfig = IDL.Record({ orbiters_strategy: IDL.Opt(SegmentsMonitoringStrategy), mission_control_strategy: IDL.Opt(CyclesMonitoringStrategy), - satellites_strategy: IDL.Opt(SegmentsMonitoringStrategy) + satellites_strategy: IDL.Opt(SegmentsMonitoringStrategy), + ufos_strategy: IDL.Opt(SegmentsMonitoringStrategy) }); const MonitoringStartConfig = IDL.Record({ cycles_config: IDL.Opt(CyclesMonitoringStartConfig) @@ -201,7 +209,8 @@ export const idlFactory = ({ IDL }) => { const CyclesMonitoringStopConfig = IDL.Record({ satellite_ids: IDL.Opt(IDL.Vec(IDL.Principal)), try_mission_control: IDL.Opt(IDL.Bool), - orbiter_ids: IDL.Opt(IDL.Vec(IDL.Principal)) + orbiter_ids: IDL.Opt(IDL.Vec(IDL.Principal)), + ufo_ids: IDL.Opt(IDL.Vec(IDL.Principal)) }); const MonitoringStopConfig = IDL.Record({ cycles_config: IDL.Opt(CyclesMonitoringStopConfig) @@ -238,6 +247,7 @@ export const idlFactory = ({ IDL }) => { ), list_orbiters: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Orbiter))], ['query']), list_satellites: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Satellite))], ['query']), + list_ufos: IDL.Func([], [IDL.Vec(IDL.Tuple(IDL.Principal, Ufo))], ['query']), set_config: IDL.Func([IDL.Opt(Config)], [], []), set_metadata: IDL.Func([IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))], [], []), set_mission_control_controllers: IDL.Func([IDL.Vec(IDL.Principal), SetAccessKey], [], []), @@ -263,11 +273,14 @@ export const idlFactory = ({ IDL }) => { [], [] ), + set_ufo: IDL.Func([IDL.Principal, IDL.Opt(IDL.Text)], [Ufo], []), + set_ufo_metadata: IDL.Func([IDL.Principal, IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text))], [Ufo], []), start_monitoring: IDL.Func([], [], []), stop_monitoring: IDL.Func([], [], []), top_up: IDL.Func([IDL.Principal, Tokens], [], []), unset_orbiter: IDL.Func([IDL.Principal], [], []), unset_satellite: IDL.Func([IDL.Principal], [], []), + unset_ufo: IDL.Func([IDL.Principal], [], []), update_and_start_monitoring: IDL.Func([MonitoringStartConfig], [], []), update_and_stop_monitoring: IDL.Func([MonitoringStopConfig], [], []) }); diff --git a/src/mission_control/mission_control.did b/src/mission_control/mission_control.did index b2b8767c9a..4f9e0e91cd 100644 --- a/src/mission_control/mission_control.did +++ b/src/mission_control/mission_control.did @@ -32,6 +32,7 @@ type CyclesMonitoringStartConfig = record { orbiters_strategy : opt SegmentsMonitoringStrategy; mission_control_strategy : opt CyclesMonitoringStrategy; satellites_strategy : opt SegmentsMonitoringStrategy; + ufos_strategy : opt SegmentsMonitoringStrategy; }; type CyclesMonitoringStatus = record { monitored_ids : vec principal; @@ -41,6 +42,7 @@ type CyclesMonitoringStopConfig = record { satellite_ids : opt vec principal; try_mission_control : opt bool; orbiter_ids : opt vec principal; + ufo_ids : opt vec principal; }; type CyclesMonitoringStrategy = variant { BelowThreshold : CyclesThreshold }; type CyclesThreshold = record { fund_cycles : nat; min_cycles : nat }; @@ -155,6 +157,13 @@ type TransferError_1 = variant { TooOld; InsufficientFunds : record { balance : nat }; }; +type Ufo = record { + updated_at : nat64; + metadata : vec record { text; text }; + created_at : nat64; + settings : opt Settings; + ufo_id : principal; +}; type User = record { updated_at : nat64; metadata : vec record { text; text }; @@ -189,6 +198,7 @@ service : (InitMissionControlArgs) -> { ) query; list_orbiters : () -> (vec record { principal; Orbiter }) query; list_satellites : () -> (vec record { principal; Satellite }) query; + list_ufos : () -> (vec record { principal; Ufo }) query; set_config : (opt Config) -> (); set_metadata : (vec record { text; text }) -> (); set_mission_control_controllers : (vec principal, SetAccessKey) -> (); @@ -204,11 +214,14 @@ service : (InitMissionControlArgs) -> { vec principal, SetAccessKey, ) -> (); + set_ufo : (principal, opt text) -> (Ufo); + set_ufo_metadata : (principal, vec record { text; text }) -> (Ufo); start_monitoring : () -> (); stop_monitoring : () -> (); top_up : (principal, Tokens) -> (); unset_orbiter : (principal) -> (); unset_satellite : (principal) -> (); + unset_ufo : (principal) -> (); update_and_start_monitoring : (MonitoringStartConfig) -> (); update_and_stop_monitoring : (MonitoringStopConfig) -> (); } From baaf1b81a7d0b9cb629aae14f07ff5c1191252cb Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 14:33:19 +0200 Subject: [PATCH 14/16] test: ufo --- ...mission-control.monitoring.history.spec.ts | 50 +++++- ...n-control.monitoring.notifications.spec.ts | 4 + .../mission-control.monitoring.spec.ts | 148 +++++++++++++++++- ...mission-control.monitoring.upgrade.spec.ts | 16 +- .../mission-control.set-unset.spec.ts | 57 ++++++- .../upgrade/mission-control.upgrade.spec.ts | 5 +- .../utils/mission-control-tests.utils.ts | 14 +- src/tests/utils/monitoring-tests.utils.ts | 19 +++ 8 files changed, 302 insertions(+), 11 deletions(-) diff --git a/src/tests/specs/mission-control/mission-control.monitoring.history.spec.ts b/src/tests/specs/mission-control/mission-control.monitoring.history.spec.ts index c96af20b09..b407e7691b 100644 --- a/src/tests/specs/mission-control/mission-control.monitoring.history.spec.ts +++ b/src/tests/specs/mission-control/mission-control.monitoring.history.spec.ts @@ -23,6 +23,7 @@ describe('Mission Control > History', () => { let missionControlId: Principal; let orbiterId: Principal; let satelliteId: Principal; + let ufoId: Principal; const controller = Ed25519KeyIdentity.generate(); @@ -46,7 +47,11 @@ describe('Mission Control > History', () => { actor.setIdentity(controller); - const { orbiterId: oId, satelliteId: sId } = await setupMissionControlModules({ + const { + orbiterId: oId, + satelliteId: sId, + ufoId: uId + } = await setupMissionControlModules({ pic, controller, missionControlId @@ -54,11 +59,13 @@ describe('Mission Control > History', () => { orbiterId = oId; satelliteId = sId; + ufoId = uId; - const { set_orbiter, set_satellite } = actor; + const { set_orbiter, set_satellite, set_ufo } = actor; await set_orbiter(orbiterId, []); await set_satellite(satelliteId, []); + await set_ufo(ufoId, []); await tick(pic); }); @@ -93,6 +100,10 @@ describe('Mission Control > History', () => { it('should not have monitoring history for orbiter', async () => { await testEmptyHistory(orbiterId); }); + + it('should not have monitoring history for ufo', async () => { + await testEmptyHistory(ufoId); + }); }); describe('with history', () => { @@ -117,6 +128,10 @@ describe('Mission Control > History', () => { ids: [orbiterId], strategy }), + ufos_strategy: toNullable({ + ids: [ufoId], + strategy + }), mission_control_strategy: toNullable(strategy) } ] @@ -145,6 +160,10 @@ describe('Mission Control > History', () => { it('should have monitoring history for orbiter', async () => { await testMonitoringHistory({ segmentId: orbiterId, expectedLength: 1, actor }); }); + + it('should have monitoring history for ufo', async () => { + await testMonitoringHistory({ segmentId: ufoId, expectedLength: 1, actor }); + }); }); describe('collect entries over time', () => { @@ -166,6 +185,10 @@ describe('Mission Control > History', () => { it('should have monitoring history for orbiter', async () => { await testMonitoringHistory({ segmentId: orbiterId, expectedLength: 2, actor }); }); + + it('should have monitoring history for ufo', async () => { + await testMonitoringHistory({ segmentId: ufoId, expectedLength: 2, actor }); + }); }); describe('second round', () => { @@ -188,6 +211,10 @@ describe('Mission Control > History', () => { it('should have monitoring history for orbiter', async () => { await testMonitoringHistory({ segmentId: orbiterId, expectedLength: 3, actor }); }); + + it('should have monitoring history for ufo', async () => { + await testMonitoringHistory({ segmentId: ufoId, expectedLength: 3, actor }); + }); }); }); @@ -204,6 +231,10 @@ describe('Mission Control > History', () => { MissionControlDid.MonitoringHistoryKey, MissionControlDid.MonitoringHistory ][]; + let ufoHistory: [ + MissionControlDid.MonitoringHistoryKey, + MissionControlDid.MonitoringHistory + ][]; const testCleanedHistory = ({ before, @@ -251,6 +282,11 @@ describe('Mission Control > History', () => { expectedLength: 3, actor }); + ufoHistory = await testMonitoringHistory({ + segmentId: ufoId, + expectedLength: 3, + actor + }); const thirtyDays = 1000 * 60 * 60 * 24 * 30; @@ -289,6 +325,16 @@ describe('Mission Control > History', () => { testCleanedHistory({ before: orbiterHistory, after: updatedHistory }); }); + + it('should have monitoring history for ufo', async () => { + const updatedHistory = await testMonitoringHistory({ + segmentId: ufoId, + expectedLength: 3, + actor + }); + + testCleanedHistory({ before: ufoHistory, after: updatedHistory }); + }); }); }); }); diff --git a/src/tests/specs/mission-control/mission-control.monitoring.notifications.spec.ts b/src/tests/specs/mission-control/mission-control.monitoring.notifications.spec.ts index 79ae238d42..a00f455828 100644 --- a/src/tests/specs/mission-control/mission-control.monitoring.notifications.spec.ts +++ b/src/tests/specs/mission-control/mission-control.monitoring.notifications.spec.ts @@ -198,6 +198,7 @@ describe('Mission Control > Notifications', () => { strategy: satelliteStrategy }), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable(missionControlStrategy) } ] @@ -212,6 +213,7 @@ describe('Mission Control > Notifications', () => { { satellite_ids: toNullable([satelliteId]), orbiter_ids: toNullable(), + ufo_ids: toNullable(), try_mission_control: toNullable(true) } ] @@ -347,6 +349,7 @@ describe('Mission Control > Notifications', () => { { satellites_strategy: toNullable(), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable(missionControlStrategy) } ] @@ -361,6 +364,7 @@ describe('Mission Control > Notifications', () => { { satellite_ids: toNullable(), orbiter_ids: toNullable(), + ufo_ids: toNullable(), try_mission_control: toNullable(true) } ] diff --git a/src/tests/specs/mission-control/mission-control.monitoring.spec.ts b/src/tests/specs/mission-control/mission-control.monitoring.spec.ts index 40414056a7..526ca2ed99 100644 --- a/src/tests/specs/mission-control/mission-control.monitoring.spec.ts +++ b/src/tests/specs/mission-control/mission-control.monitoring.spec.ts @@ -18,7 +18,8 @@ import { import { testMissionControlMonitoring, testOrbiterMonitoring, - testSatellitesMonitoring + testSatellitesMonitoring, + testUfoMonitoring } from '../../utils/monitoring-tests.utils'; import { MISSION_CONTROL_WASM_PATH } from '../../utils/setup-tests.utils'; @@ -29,6 +30,7 @@ describe('Mission Control > Monitoring', () => { let missionControlId: Principal; let orbiterId: Principal; let satelliteId: Principal; + let ufoId: Principal; const controller = Ed25519KeyIdentity.generate(); @@ -50,7 +52,11 @@ describe('Mission Control > Monitoring', () => { actor.setIdentity(controller); - const { orbiterId: oId, satelliteId: sId } = await setupMissionControlModules({ + const { + orbiterId: oId, + satelliteId: sId, + ufoId: uId + } = await setupMissionControlModules({ pic, controller, missionControlId @@ -58,11 +64,13 @@ describe('Mission Control > Monitoring', () => { orbiterId = oId; satelliteId = sId; + ufoId = uId; - const { set_orbiter, set_satellite } = actor; + const { set_orbiter, set_satellite, set_ufo } = actor; await set_orbiter(orbiterId, []); await set_satellite(satelliteId, []); + await set_ufo(ufoId, []); }); afterAll(async () => { @@ -204,6 +212,16 @@ describe('Mission Control > Monitoring', () => { ).toBeUndefined(); }); + it('should have no ufos settings', async () => { + const { list_ufos } = actor; + + const results = await list_ufos(); + + expect( + results.find(([_, { settings }]) => nonNullish(fromNullable(settings))) + ).toBeUndefined(); + }); + it('should fail at configuring monitoring if mission control is not already monitored', async () => { const { update_and_start_monitoring } = actor; @@ -215,6 +233,7 @@ describe('Mission Control > Monitoring', () => { strategy }), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable() } ] @@ -233,6 +252,7 @@ describe('Mission Control > Monitoring', () => { { satellites_strategy: toNullable(), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable(strategy) } ] @@ -266,6 +286,7 @@ describe('Mission Control > Monitoring', () => { strategy }), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable() } ] @@ -287,6 +308,7 @@ describe('Mission Control > Monitoring', () => { ids: [orbiterId], strategy }), + ufos_strategy: toNullable(), mission_control_strategy: toNullable() } ] @@ -297,6 +319,28 @@ describe('Mission Control > Monitoring', () => { await testOrbiterMonitoring({ expectedEnabled: true, expectedStrategy: strategy, actor }); }); + it('should config and start monitoring for ufo', async () => { + const { update_and_start_monitoring } = actor; + + const config: MissionControlDid.MonitoringStartConfig = { + cycles_config: [ + { + satellites_strategy: toNullable(), + orbiters_strategy: toNullable(), + ufos_strategy: toNullable({ + ids: [ufoId], + strategy + }), + mission_control_strategy: toNullable() + } + ] + }; + + await update_and_start_monitoring(config); + + await testUfoMonitoring({ expectedEnabled: true, expectedStrategy: strategy, actor }); + }); + it('should fail at configuring monitoring for unknown satellite', async () => { const { update_and_start_monitoring } = actor; @@ -308,6 +352,7 @@ describe('Mission Control > Monitoring', () => { strategy }), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable() } ] @@ -329,6 +374,29 @@ describe('Mission Control > Monitoring', () => { ids: [satelliteId], strategy }), + ufos_strategy: toNullable(), + mission_control_strategy: toNullable() + } + ] + }; + + await expect(update_and_start_monitoring(config)).rejects.toThrow( + `Orbiter ${satelliteId.toText()} not found. Strategy cannot be saved.` + ); + }); + + it('should fail at configuring monitoring for unknown ufo', async () => { + const { update_and_start_monitoring } = actor; + + const config: MissionControlDid.MonitoringStartConfig = { + cycles_config: [ + { + satellites_strategy: toNullable(), + orbiters_strategy: toNullable(), + ufos_strategy: toNullable({ + ids: [satelliteId], + strategy + }), mission_control_strategy: toNullable() } ] @@ -347,6 +415,7 @@ describe('Mission Control > Monitoring', () => { { satellite_ids: toNullable([satelliteIdMock]), orbiter_ids: toNullable(), + ufo_ids: toNullable(), try_mission_control: toNullable() } ] @@ -365,6 +434,26 @@ describe('Mission Control > Monitoring', () => { { satellite_ids: toNullable(), orbiter_ids: toNullable([satelliteId]), + ufo_ids: toNullable(), + try_mission_control: toNullable() + } + ] + }; + + await expect(update_and_stop_monitoring(config)).rejects.toThrow( + `Orbiter ${satelliteId.toText()} not found. Monitoring cannot be disabled.` + ); + }); + + it('should fail at stopping monitoring for unknown ufo', async () => { + const { update_and_stop_monitoring } = actor; + + const config: MissionControlDid.MonitoringStopConfig = { + cycles_config: [ + { + satellite_ids: toNullable(), + orbiter_ids: toNullable(), + ufo_ids: toNullable([satelliteId]), try_mission_control: toNullable() } ] @@ -383,6 +472,7 @@ describe('Mission Control > Monitoring', () => { { satellite_ids: toNullable(), orbiter_ids: toNullable(), + ufo_ids: toNullable(), try_mission_control: toNullable(true) } ] @@ -401,6 +491,7 @@ describe('Mission Control > Monitoring', () => { { satellite_ids: toNullable([satelliteId]), orbiter_ids: toNullable(), + ufo_ids: toNullable(), try_mission_control: toNullable() } ] @@ -420,6 +511,27 @@ describe('Mission Control > Monitoring', () => { { satellite_ids: toNullable(), orbiter_ids: toNullable([orbiterId]), + ufo_ids: toNullable(), + try_mission_control: toNullable() + } + ] + }; + + await update_and_stop_monitoring(config); + + await testSatellitesMonitoring({ expectedEnabled: false, expectedStrategy: strategy, actor }); + await testOrbiterMonitoring({ expectedEnabled: false, expectedStrategy: strategy, actor }); + }); + + it('should stop monitoring for ufo', async () => { + const { update_and_stop_monitoring } = actor; + + const config: MissionControlDid.MonitoringStopConfig = { + cycles_config: [ + { + satellite_ids: toNullable(), + orbiter_ids: toNullable(), + ufo_ids: toNullable([ufoId]), try_mission_control: toNullable() } ] @@ -439,6 +551,7 @@ describe('Mission Control > Monitoring', () => { { satellite_ids: toNullable(), orbiter_ids: toNullable(), + ufo_ids: toNullable(), try_mission_control: toNullable(true) } ] @@ -516,6 +629,7 @@ describe('Mission Control > Monitoring', () => { { satellites_strategy: toNullable(), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable(updateStrategy) } ] @@ -541,6 +655,7 @@ describe('Mission Control > Monitoring', () => { strategy: updateStrategy }), orbiters_strategy: toNullable(), + ufos_strategy: toNullable(), mission_control_strategy: toNullable() } ] @@ -566,6 +681,33 @@ describe('Mission Control > Monitoring', () => { ids: [orbiterId], strategy: updateStrategy }), + ufos_strategy: toNullable(), + mission_control_strategy: toNullable() + } + ] + }; + + await update_and_start_monitoring(config); + + await testOrbiterMonitoring({ + expectedEnabled: true, + expectedStrategy: updateStrategy, + actor + }); + }); + + it('should update config for ufo', async () => { + const { update_and_start_monitoring } = actor; + + const config: MissionControlDid.MonitoringStartConfig = { + cycles_config: [ + { + satellites_strategy: toNullable(), + orbiters_strategy: toNullable(), + ufos_strategy: toNullable({ + ids: [ufoId], + strategy: updateStrategy + }), mission_control_strategy: toNullable() } ] diff --git a/src/tests/specs/mission-control/mission-control.monitoring.upgrade.spec.ts b/src/tests/specs/mission-control/mission-control.monitoring.upgrade.spec.ts index d8897bd62e..4b3a57ea34 100644 --- a/src/tests/specs/mission-control/mission-control.monitoring.upgrade.spec.ts +++ b/src/tests/specs/mission-control/mission-control.monitoring.upgrade.spec.ts @@ -23,6 +23,7 @@ describe.skip('Mission control > Upgrade > Monitoring', () => { let missionControlId: Principal; let orbiterId: Principal; let satelliteId: Principal; + let ufoId: Principal; const controller = Ed25519KeyIdentity.generate(); @@ -51,7 +52,11 @@ describe.skip('Mission control > Upgrade > Monitoring', () => { actor.setIdentity(controller); - const { orbiterId: oId, satelliteId: sId } = await setupMissionControlModules({ + const { + orbiterId: oId, + satelliteId: sId, + ufoId: uId + } = await setupMissionControlModules({ pic, controller, missionControlId @@ -59,11 +64,13 @@ describe.skip('Mission control > Upgrade > Monitoring', () => { orbiterId = oId; satelliteId = sId; + ufoId = uId; - const { set_orbiter, set_satellite } = actor; + const { set_orbiter, set_satellite, set_ufo } = actor; await set_orbiter(orbiterId, []); await set_satellite(satelliteId, []); + await set_ufo(ufoId, []); }); afterEach(async () => { @@ -86,6 +93,10 @@ describe.skip('Mission control > Upgrade > Monitoring', () => { ids: [orbiterId], strategy }), + ufos_strategy: toNullable({ + ids: [ufoId], + strategy + }), mission_control_strategy: toNullable(strategy) } ] @@ -104,6 +115,7 @@ describe.skip('Mission control > Upgrade > Monitoring', () => { { satellite_ids: toNullable(), orbiter_ids: toNullable([orbiterId]), + ufo_ids: toNullable(), try_mission_control: toNullable() } ] diff --git a/src/tests/specs/mission-control/mission-control.set-unset.spec.ts b/src/tests/specs/mission-control/mission-control.set-unset.spec.ts index 5466c3686d..60d0d6afeb 100644 --- a/src/tests/specs/mission-control/mission-control.set-unset.spec.ts +++ b/src/tests/specs/mission-control/mission-control.set-unset.spec.ts @@ -17,6 +17,7 @@ describe('Mission Control > Set / Unset', () => { let orbiterId: Principal; let satelliteId: Principal; + let ufoId: Principal; const controller = Ed25519KeyIdentity.generate(); @@ -38,7 +39,11 @@ describe('Mission Control > Set / Unset', () => { actor.setIdentity(controller); - const { orbiterId: oId, satelliteId: sId } = await setupMissionControlModules({ + const { + orbiterId: oId, + satelliteId: sId, + ufoId: uId + } = await setupMissionControlModules({ pic, controller, missionControlId @@ -46,6 +51,7 @@ describe('Mission Control > Set / Unset', () => { orbiterId = oId; satelliteId = sId; + ufoId = uId; }); afterAll(async () => { @@ -84,6 +90,18 @@ describe('Mission Control > Set / Unset', () => { MISSION_CONTROL_ADMIN_CONTROLLER_ERROR_MSG ); }); + + it('should throw errors on set ufo', async () => { + const { set_ufo } = actor; + + await expect(set_ufo(ufoId, [])).rejects.toThrow(MISSION_CONTROL_ADMIN_CONTROLLER_ERROR_MSG); + }); + + it('should throw errors on unset ufo', async () => { + const { unset_ufo } = actor; + + await expect(unset_ufo(ufoId)).rejects.toThrow(MISSION_CONTROL_ADMIN_CONTROLLER_ERROR_MSG); + }); }; describe('anonymous', () => { @@ -180,5 +198,42 @@ describe('Mission Control > Set / Unset', () => { expect(results).toHaveLength(0); }); + + it('should have no ufos set', async () => { + const { list_ufos } = actor; + + const results = await list_ufos(); + + expect(results).toHaveLength(0); + }); + + it('should set an ufo', async () => { + const { set_ufo, list_ufos } = actor; + + const ufo = await set_ufo(ufoId, ['Hello']); + + expect(ufo.ufo_id.toText()).toEqual(ufoId.toText()); + expect(ufo.created_at).toBeGreaterThan(0n); + expect(ufo.updated_at).toBeGreaterThan(0n); + + const results = await list_ufos(); + + expect(results).toHaveLength(1); + + expect(results[0][0].toText()).toEqual(ufoId.toText()); + expect(results[0][1].ufo_id.toText()).toEqual(ufoId.toText()); + expect(results[0][1].created_at).toBeGreaterThan(0n); + expect(results[0][1].updated_at).toBeGreaterThan(0n); + }); + + it('should unset an ufo', async () => { + const { unset_ufo, list_ufos } = actor; + + await unset_ufo(ufoId); + + const results = await list_ufos(); + + expect(results).toHaveLength(0); + }); }); }); diff --git a/src/tests/specs/mission-control/upgrade/mission-control.upgrade.spec.ts b/src/tests/specs/mission-control/upgrade/mission-control.upgrade.spec.ts index b4b6f501c7..9fc206abd2 100644 --- a/src/tests/specs/mission-control/upgrade/mission-control.upgrade.spec.ts +++ b/src/tests/specs/mission-control/upgrade/mission-control.upgrade.spec.ts @@ -194,7 +194,10 @@ describe('Mission control > Upgrade', () => { ids: [orbiterId], strategy }), - mission_control_strategy: toNullable(strategy) + mission_control_strategy: toNullable(strategy), + // ufos_strategy was technically not part of v0.1.0 but for simplicity reasons + // and to avoid duplicating did files just for this test, we set it to none + ufos_strategy: toNullable() } ] }; diff --git a/src/tests/utils/mission-control-tests.utils.ts b/src/tests/utils/mission-control-tests.utils.ts index 610b52d7ce..628ad1828a 100644 --- a/src/tests/utils/mission-control-tests.utils.ts +++ b/src/tests/utils/mission-control-tests.utils.ts @@ -28,7 +28,7 @@ export const setupMissionControlModules = async ({ pic: PocketIc; controller: Identity; missionControlId: Principal; -}): Promise<{ satelliteId: Principal; orbiterId: Principal }> => { +}): Promise<{ satelliteId: Principal; orbiterId: Principal; ufoId: Principal }> => { const { canisterId: orbiterId } = await pic.setupCanister({ idlFactory: idlFactoryOrbiter, wasm: ORBITER_WASM_PATH, @@ -55,5 +55,15 @@ export const setupMissionControlModules = async ({ sender: controller.getPrincipal() }); - return { satelliteId, orbiterId }; + const ufoId = await pic.createCanister({ + sender: controller.getPrincipal() + }); + + await pic.updateCanisterSettings({ + canisterId: ufoId, + controllers: [controller.getPrincipal(), missionControlId], + sender: controller.getPrincipal() + }); + + return { satelliteId, orbiterId, ufoId }; }; diff --git a/src/tests/utils/monitoring-tests.utils.ts b/src/tests/utils/monitoring-tests.utils.ts index 98a43bdef6..24d2ff6fbb 100644 --- a/src/tests/utils/monitoring-tests.utils.ts +++ b/src/tests/utils/monitoring-tests.utils.ts @@ -83,6 +83,25 @@ export const testOrbiterMonitoring = async ({ testMonitoring({ monitoring, expectedEnabled, expectedStrategy }); }; +export const testUfoMonitoring = async ({ + expectedEnabled, + expectedStrategy, + actor +}: { + expectedEnabled: boolean; + expectedStrategy: MissionControlDid.CyclesMonitoringStrategy; + actor: Actor; +}) => { + const { list_ufos } = actor; + + const [[_, ufo]] = await list_ufos(); + + const settings = fromNullable(ufo.settings); + const monitoring = fromNullable(settings?.monitoring ?? []); + + testMonitoring({ monitoring, expectedEnabled, expectedStrategy }); +}; + export const testMonitoringHistory = async ({ segmentId, expectedLength, From f98c266c0f8d6a70cd10120c8395f4350b23b22c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 14:33:31 +0200 Subject: [PATCH 15/16] feat: placeholder for compile --- .../src/lib/services/factory/factory.create.services.ts | 6 ++++-- .../lib/services/mission-control/monitoring.services.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/lib/services/factory/factory.create.services.ts b/src/frontend/src/lib/services/factory/factory.create.services.ts index 67e382e566..88d4af1138 100644 --- a/src/frontend/src/lib/services/factory/factory.create.services.ts +++ b/src/frontend/src/lib/services/factory/factory.create.services.ts @@ -404,7 +404,8 @@ export const createSatelliteWizard = async ({ strategy: monitoringStrategy, ids: [canisterId] }), - orbiters_strategy: toNullable() + orbiters_strategy: toNullable(), + ufos_strategy: toNullable() }) } }); @@ -536,7 +537,8 @@ export const createOrbiterWizard = async ({ orbiters_strategy: toNullable({ strategy: monitoringStrategy, ids: [canisterId] - }) + }), + ufos_strategy: toNullable() }) } }); diff --git a/src/frontend/src/lib/services/mission-control/monitoring.services.ts b/src/frontend/src/lib/services/mission-control/monitoring.services.ts index da933642e8..d3a0eeb7b3 100644 --- a/src/frontend/src/lib/services/mission-control/monitoring.services.ts +++ b/src/frontend/src/lib/services/mission-control/monitoring.services.ts @@ -283,7 +283,9 @@ const setMonitoringCyclesStrategy = async ({ strategy: moduleStrategy } ] - : [] + : [], + // TODO: support for ufo + ufos_strategy: toNullable() }) } }); @@ -398,7 +400,9 @@ const stopMonitoringCycles = async ({ cycles_config: toNullable({ try_mission_control: toNullable(stopMissionControl), satellite_ids: satellites.length > 0 ? toNullable(satellites) : [], - orbiter_ids: orbiters.length > 0 ? toNullable(orbiters) : [] + orbiter_ids: orbiters.length > 0 ? toNullable(orbiters) : [], + // TODO: support for ufo + ufo_ids: toNullable() }) } }); From f0a5e12fb5bdd77bf97a62ec82bea775f9d5c12c Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Wed, 22 Apr 2026 14:42:38 +0200 Subject: [PATCH 16/16] feat: expected error --- .../specs/mission-control/mission-control.monitoring.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/specs/mission-control/mission-control.monitoring.spec.ts b/src/tests/specs/mission-control/mission-control.monitoring.spec.ts index 526ca2ed99..ad238afc08 100644 --- a/src/tests/specs/mission-control/mission-control.monitoring.spec.ts +++ b/src/tests/specs/mission-control/mission-control.monitoring.spec.ts @@ -403,7 +403,7 @@ describe('Mission Control > Monitoring', () => { }; await expect(update_and_start_monitoring(config)).rejects.toThrow( - `Orbiter ${satelliteId.toText()} not found. Strategy cannot be saved.` + `UFO ${satelliteId.toText()} not found. Strategy cannot be saved.` ); }); @@ -460,7 +460,7 @@ describe('Mission Control > Monitoring', () => { }; await expect(update_and_stop_monitoring(config)).rejects.toThrow( - `Orbiter ${satelliteId.toText()} not found. Monitoring cannot be disabled.` + `UFO ${satelliteId.toText()} not found. Monitoring cannot be disabled.` ); });