From c93b5b43176cfb27d9654027d26a56435f917584 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 09:22:25 +0100 Subject: [PATCH 01/18] feat(console): support for raw canister (proof of concept) --- src/console/src/api/factory.rs | 19 +++++- src/console/src/factory/canister.rs | 94 +++++++++++++++++++++++++++++ src/console/src/factory/mod.rs | 1 + src/console/src/rates/services.rs | 4 ++ src/console/src/types.rs | 1 + src/libs/shared/src/impls.rs | 1 + src/libs/shared/src/types.rs | 14 +++++ src/observatory/src/impls.rs | 1 + 8 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 src/console/src/factory/canister.rs diff --git a/src/console/src/api/factory.rs b/src/console/src/api/factory.rs index 3d1379194..0053482e0 100644 --- a/src/console/src/api/factory.rs +++ b/src/console/src/api/factory.rs @@ -6,8 +6,9 @@ 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, }; +use crate::factory::canister::create_canister; #[update] async fn create_mission_control(args: CreateMissionControlArgs) -> Principal { @@ -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::Canister(args) => create_canister(caller, args).await, + }; + + result.unwrap_or_trap() +} diff --git a/src/console/src/factory/canister.rs b/src/console/src/factory/canister.rs new file mode 100644 index 000000000..c6bf8add3 --- /dev/null +++ b/src/console/src/factory/canister.rs @@ -0,0 +1,94 @@ +use candid::{Nat, Principal}; +use junobuild_shared::constants::shared::CREATE_MISSION_CONTROL_CYCLES; +use junobuild_shared::ic::api::id; +use junobuild_shared::mgmt::cmc::cmc_create_canister_install_code; +use junobuild_shared::mgmt::ic::create_canister_install_code; +use junobuild_shared::mgmt::types::cmc::SubnetId; +use junobuild_shared::mgmt::types::ic::CreateCanisterInitSettingsArg; +use junobuild_shared::types::interface::{CreateCanisterArgs}; +use junobuild_shared::types::state::{SegmentKind, UserId}; +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::factory::utils::wasm::mission_control_wasm_arg; +use crate::fees::get_factory_fee; +use crate::rates::{increment_canister_rate}; +use crate::segments::add_segment as add_segment_store; +use crate::types::ledger::Fee; +use crate::types::state::{Segment, SegmentKey, StorableSegmentKind}; + +pub async fn create_canister( + caller: Principal, + args: CreateCanisterArgs, +) -> Result { + let account = get_existing_account(&caller)?; + + let creator: CanisterCreator = CanisterCreator::User((account.owner, None)); + + let fee = get_factory_fee(&SegmentKind::Canister)?.fee_cycles; + + let canister_id = create_segment_with_account( + create_raw_canister, + process_payment_cycles, + refund_payment_cycles, + &increment_canister_rate, + Fee::Cycles(fee), + &account, + creator, + args.into(), + ) + .await?; + + add_segment(&account.owner, &canister_id); + + 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 a raw canister".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 mission_control_id = if let Some(subnet_id) = subnet_id { + cmc_create_canister_install_code( + &create_settings_arg, + &wasm_arg, + CREATE_MISSION_CONTROL_CYCLES, + &subnet_id, + ) + .await + } else { + create_canister_install_code( + &create_settings_arg, + &wasm_arg, + CREATE_MISSION_CONTROL_CYCLES, + ) + .await + }?; + + update_mission_control_controllers(&mission_control_id, &user_id).await?; + + Ok(mission_control_id) +} + + +fn add_segment(user: &UserId, canister_id: &Principal) { + let canister = Segment::new(canister_id, None); + let key = SegmentKey::from(user, canister_id, StorableSegmentKind::Canister); + add_segment_store(&key, &canister) +} \ No newline at end of file diff --git a/src/console/src/factory/mod.rs b/src/console/src/factory/mod.rs index 22157225a..712fb0458 100644 --- a/src/console/src/factory/mod.rs +++ b/src/console/src/factory/mod.rs @@ -6,3 +6,4 @@ pub mod satellite; mod services; mod types; mod utils; +pub mod canister; diff --git a/src/console/src/rates/services.rs b/src/console/src/rates/services.rs index 18413b279..b97d824eb 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_canister_rate() -> Result<(), String> { + increment_rate(&SegmentKind::Canister) +} diff --git a/src/console/src/types.rs b/src/console/src/types.rs index cd6f2cd02..e83e1d7f2 100644 --- a/src/console/src/types.rs +++ b/src/console/src/types.rs @@ -150,6 +150,7 @@ pub mod state { // For historical reasons, MissionControl is not stored in the segments stable tree // but within the Account structure Orbiter, + Canister, } } diff --git a/src/libs/shared/src/impls.rs b/src/libs/shared/src/impls.rs index b944d9fe2..e6d3568c3 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::Canister => write!(f, "Canister"), } } } diff --git a/src/libs/shared/src/types.rs b/src/libs/shared/src/types.rs index 79876f06c..bc9437ec7 100644 --- a/src/libs/shared/src/types.rs +++ b/src/libs/shared/src/types.rs @@ -77,6 +77,7 @@ pub mod state { Satellite, MissionControl, Orbiter, + Canister, } #[derive(CandidType, Serialize, Deserialize, Clone)] @@ -131,6 +132,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), + Canister(CreateCanisterArgs), + } + #[derive(CandidType, Deserialize)] pub struct CreateOrbiterArgs { pub user: UserId, @@ -153,6 +162,11 @@ pub mod interface { pub name: Option, } + #[derive(CandidType, Deserialize)] + pub struct CreateCanisterArgs { + pub subnet_id: 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 2ff485988..70adca92d 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::Canister => "https://console.juno.build/canister".to_string(), } } From 8d939a3b16dd828bd99e40a58e3b1dc3b22436e4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 09:46:35 +0100 Subject: [PATCH 02/18] feat: use new functions --- src/console/src/factory/canister.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/console/src/factory/canister.rs b/src/console/src/factory/canister.rs index c6bf8add3..f44916b01 100644 --- a/src/console/src/factory/canister.rs +++ b/src/console/src/factory/canister.rs @@ -1,8 +1,8 @@ use candid::{Nat, Principal}; use junobuild_shared::constants::shared::CREATE_MISSION_CONTROL_CYCLES; use junobuild_shared::ic::api::id; -use junobuild_shared::mgmt::cmc::cmc_create_canister_install_code; -use junobuild_shared::mgmt::ic::create_canister_install_code; +use junobuild_shared::mgmt::cmc::{cmc_create_canister_install_code, create_canister_with_cmc}; +use junobuild_shared::mgmt::ic::{create_canister_install_code, create_canister_with_ic_mgmt}; use junobuild_shared::mgmt::types::cmc::SubnetId; use junobuild_shared::mgmt::types::ic::CreateCanisterInitSettingsArg; use junobuild_shared::types::interface::{CreateCanisterArgs}; @@ -65,17 +65,15 @@ async fn create_raw_canister( }; let mission_control_id = if let Some(subnet_id) = subnet_id { - cmc_create_canister_install_code( + create_canister_with_cmc( &create_settings_arg, - &wasm_arg, CREATE_MISSION_CONTROL_CYCLES, &subnet_id, ) .await } else { - create_canister_install_code( + create_canister_with_ic_mgmt( &create_settings_arg, - &wasm_arg, CREATE_MISSION_CONTROL_CYCLES, ) .await From ea3c432d04e29f2a5cdf87f671101044f732c8cf Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 09:49:34 +0100 Subject: [PATCH 03/18] feat: finish support for create canister --- src/console/src/factory/canister.rs | 41 +++++++++---------------- src/libs/shared/src/constants/shared.rs | 3 +- src/libs/shared/src/mgmt/settings.rs | 4 +-- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/src/console/src/factory/canister.rs b/src/console/src/factory/canister.rs index f44916b01..ce22651f7 100644 --- a/src/console/src/factory/canister.rs +++ b/src/console/src/factory/canister.rs @@ -1,24 +1,23 @@ -use candid::{Nat, Principal}; -use junobuild_shared::constants::shared::CREATE_MISSION_CONTROL_CYCLES; -use junobuild_shared::ic::api::id; -use junobuild_shared::mgmt::cmc::{cmc_create_canister_install_code, create_canister_with_cmc}; -use junobuild_shared::mgmt::ic::{create_canister_install_code, create_canister_with_ic_mgmt}; -use junobuild_shared::mgmt::types::cmc::SubnetId; -use junobuild_shared::mgmt::types::ic::CreateCanisterInitSettingsArg; -use junobuild_shared::types::interface::{CreateCanisterArgs}; -use junobuild_shared::types::state::{SegmentKind, UserId}; 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::factory::utils::wasm::mission_control_wasm_arg; use crate::fees::get_factory_fee; -use crate::rates::{increment_canister_rate}; +use crate::rates::increment_canister_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_CANISTER_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::CreateCanisterArgs; +use junobuild_shared::types::state::{SegmentKind, UserId}; pub async fn create_canister( caller: Principal, @@ -40,10 +39,10 @@ pub async fn create_canister( creator, args.into(), ) - .await?; + .await?; add_segment(&account.owner, &canister_id); - + Ok(canister_id) } @@ -65,18 +64,9 @@ async fn create_raw_canister( }; let mission_control_id = if let Some(subnet_id) = subnet_id { - create_canister_with_cmc( - &create_settings_arg, - CREATE_MISSION_CONTROL_CYCLES, - &subnet_id, - ) - .await + create_canister_with_cmc(&create_settings_arg, CREATE_CANISTER_CYCLES, &subnet_id).await } else { - create_canister_with_ic_mgmt( - &create_settings_arg, - CREATE_MISSION_CONTROL_CYCLES, - ) - .await + create_canister_with_ic_mgmt(&create_settings_arg, CREATE_CANISTER_CYCLES).await }?; update_mission_control_controllers(&mission_control_id, &user_id).await?; @@ -84,9 +74,8 @@ async fn create_raw_canister( Ok(mission_control_id) } - fn add_segment(user: &UserId, canister_id: &Principal) { let canister = Segment::new(canister_id, None); let key = SegmentKey::from(user, canister_id, StorableSegmentKind::Canister); add_segment_store(&key, &canister) -} \ No newline at end of file +} diff --git a/src/libs/shared/src/constants/shared.rs b/src/libs/shared/src/constants/shared.rs index 1788c0f78..7ac992dd4 100644 --- a/src/libs/shared/src/constants/shared.rs +++ b/src/libs/shared/src/constants/shared.rs @@ -13,12 +13,13 @@ pub const IC_TRANSACTION_FEE_CYCLES: CyclesTokens = CyclesTokens::from_e12s(100_ // Cost for creating a canister have been increased by 400% in November 2024: // - https://forum.dfinity.org/t/evaluating-compute-pricing-in-response-to-increased-demand-on-the-internet-computer-protocol/36565 // - https://nns.ic0.app/proposal/?u=qoctq-giaaa-aaaaa-aaaea-cai&proposal=134032 -pub const CREATE_CANISTER_CYCLES: u128 = 500_000_000_000u128; +pub const IC_CREATE_CANISTER_CYCLES: u128 = 500_000_000_000u128; // Additional cycles allocated for creating different types of canisters to ensure operation beyond the minimum requirement. 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_CANISTER_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/mgmt/settings.rs b/src/libs/shared/src/mgmt/settings.rs index 1a85af880..75fd8c21a 100644 --- a/src/libs/shared/src/mgmt/settings.rs +++ b/src/libs/shared/src/mgmt/settings.rs @@ -1,5 +1,5 @@ use crate::constants::internal::WASM_MEMORY_LIMIT; -use crate::constants::shared::CREATE_CANISTER_CYCLES; +use crate::constants::shared::IC_CREATE_CANISTER_CYCLES; use crate::mgmt::types::ic::CreateCanisterInitSettingsArg; use candid::Nat; use ic_cdk::management_canister::CanisterSettings; @@ -24,5 +24,5 @@ pub fn create_canister_settings( } pub fn create_canister_cycles(cycles: u128) -> u128 { - CREATE_CANISTER_CYCLES + cycles + IC_CREATE_CANISTER_CYCLES + cycles } From 8d01ec1a61401797b7c0c557e4d027a84faab0eb Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 09:49:43 +0100 Subject: [PATCH 04/18] feat: finish support for create canister --- src/console/src/api/factory.rs | 2 +- src/console/src/factory/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/console/src/api/factory.rs b/src/console/src/api/factory.rs index 0053482e0..489ac319c 100644 --- a/src/console/src/api/factory.rs +++ b/src/console/src/api/factory.rs @@ -1,3 +1,4 @@ +use crate::factory::canister::create_canister; 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; @@ -8,7 +9,6 @@ use junobuild_shared::ic::UnwrapOrTrap; use junobuild_shared::types::interface::{ CreateMissionControlArgs, CreateOrbiterArgs, CreateSatelliteArgs, CreateSegmentArgs, }; -use crate::factory::canister::create_canister; #[update] async fn create_mission_control(args: CreateMissionControlArgs) -> Principal { diff --git a/src/console/src/factory/mod.rs b/src/console/src/factory/mod.rs index 712fb0458..182890340 100644 --- a/src/console/src/factory/mod.rs +++ b/src/console/src/factory/mod.rs @@ -1,3 +1,4 @@ +pub mod canister; mod impls; pub mod mission_control; pub mod orbiter; @@ -6,4 +7,3 @@ pub mod satellite; mod services; mod types; mod utils; -pub mod canister; From 982267c813e32ab8d1012969c57055fdf5c27bdc Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 09:51:54 +0100 Subject: [PATCH 05/18] fix: export and impl missing --- src/console/src/factory/impls.rs | 13 ++++++++++++- src/console/src/lib.rs | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/console/src/factory/impls.rs b/src/console/src/factory/impls.rs index ef3a17402..5bd22cf04 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, + CreateCanisterArgs, CreateMissionControlArgs, CreateOrbiterArgs, CreateSatelliteArgs, }; use junobuild_shared::types::state::{ControllerId, UserId}; @@ -63,3 +63,14 @@ impl From for CreateSegmentArgs { } } } + +impl From for CreateSegmentArgs { + fn from(args: CreateCanisterArgs) -> Self { + Self { + // Unlike Satellite and Orbiter, or same as Mission Control, 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/lib.rs b/src/console/src/lib.rs index d4432531b..1eaba9b0d 100644 --- a/src/console/src/lib.rs +++ b/src/console/src/lib.rs @@ -58,6 +58,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, From 9a4369e0327f3702e6ab0ee8cd15a7730ec9b2db Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 09:58:44 +0100 Subject: [PATCH 06/18] feat: generate did --- src/console/console.did | 12 ++++++++++-- src/declarations/console/console.did.d.ts | 17 +++++++++++++++-- .../console/console.factory.certified.did.js | 12 ++++++++++++ src/declarations/console/console.factory.did.js | 12 ++++++++++++ .../console/console.factory.did.mjs | 12 ++++++++++++ .../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, 70 insertions(+), 6 deletions(-) diff --git a/src/console/console.did b/src/console/console.did index 09fe7df16..af852d342 100644 --- a/src/console/console.did +++ b/src/console/console.did @@ -78,6 +78,7 @@ type Controller = record { expires_at : opt nat64; }; type ControllerScope = variant { Write; Admin; Submit }; +type CreateCanisterArgs = record { subnet_id : opt principal }; type CreateMissionControlArgs = record { subnet_id : opt principal }; type CreateOrbiterArgs = record { block_index : opt nat64; @@ -92,6 +93,12 @@ type CreateSatelliteArgs = record { name : opt text; user : principal; }; +type CreateSegmentArgs = variant { + Orbiter : CreateOrbiterArgs; + MissionControl : CreateCanisterArgs; + Canister : CreateCanisterArgs; + Satellite : CreateSatelliteArgs; +}; type CustomDomain = record { updated_at : nat64; created_at : nat64; @@ -299,7 +306,7 @@ type SegmentKey = record { segment_id : principal; segment_kind : StorableSegmentKind; }; -type SegmentKind = variant { Orbiter; MissionControl; Satellite }; +type SegmentKind = variant { Orbiter; MissionControl; Canister; Satellite }; type SegmentsDeploymentOptions = record { orbiter : opt text; mission_control_version : opt text; @@ -340,7 +347,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 { Orbiter; Canister; Satellite }; type StorageConfig = record { iframe : opt StorageConfigIFrame; updated_at : opt nat64; @@ -403,6 +410,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 c3f4c00f5..47b310d5a 100644 --- a/src/declarations/console/console.did.d.ts +++ b/src/declarations/console/console.did.d.ts @@ -104,6 +104,9 @@ export interface Controller { expires_at: [] | [bigint]; } export type ControllerScope = { Write: null } | { Admin: null } | { Submit: null }; +export interface CreateCanisterArgs { + subnet_id: [] | [Principal]; +} export interface CreateMissionControlArgs { subnet_id: [] | [Principal]; } @@ -120,6 +123,11 @@ export interface CreateSatelliteArgs { name: [] | [string]; user: Principal; } +export type CreateSegmentArgs = + | { Orbiter: CreateOrbiterArgs } + | { MissionControl: CreateCanisterArgs } + | { Canister: CreateCanisterArgs } + | { Satellite: CreateSatelliteArgs }; export interface CustomDomain { updated_at: bigint; created_at: bigint; @@ -362,7 +370,11 @@ export interface SegmentKey { segment_id: Principal; segment_kind: StorableSegmentKind; } -export type SegmentKind = { Orbiter: null } | { MissionControl: null } | { Satellite: null }; +export type SegmentKind = + | { Orbiter: null } + | { MissionControl: null } + | { Canister: null } + | { Satellite: null }; export interface SegmentsDeploymentOptions { orbiter: [] | [string]; mission_control_version: [] | [string]; @@ -406,7 +418,7 @@ export interface SignedDelegation { signature: Uint8Array; delegation: Delegation; } -export type StorableSegmentKind = { Orbiter: null } | { Satellite: null }; +export type StorableSegmentKind = { Orbiter: null } | { Canister: null } | { Satellite: null }; export interface StorageConfig { iframe: [] | [StorageConfigIFrame]; updated_at: [] | [bigint]; @@ -475,6 +487,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 cfe6c48a5..328b737f5 100644 --- a/src/declarations/console/console.factory.certified.did.js +++ b/src/declarations/console/console.factory.certified.did.js @@ -123,6 +123,15 @@ export const idlFactory = ({ IDL }) => { name: IDL.Opt(IDL.Text), user: IDL.Principal }); + const CreateCanisterArgs = IDL.Record({ + subnet_id: IDL.Opt(IDL.Principal) + }); + const CreateSegmentArgs = IDL.Variant({ + Orbiter: CreateOrbiterArgs, + MissionControl: CreateCanisterArgs, + Canister: CreateCanisterArgs, + Satellite: CreateSatelliteArgs + }); const DeleteControllersArgs = IDL.Record({ controllers: IDL.Vec(IDL.Principal) }); @@ -220,6 +229,7 @@ export const idlFactory = ({ IDL }) => { const SegmentKind = IDL.Variant({ Orbiter: IDL.Null, MissionControl: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const CyclesTokens = IDL.Record({ e12s: IDL.Nat64 }); @@ -423,6 +433,7 @@ export const idlFactory = ({ IDL }) => { }); const StorableSegmentKind = IDL.Variant({ Orbiter: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const ListSegmentsArgs = IDL.Record({ @@ -501,6 +512,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 d02b73458..0b17e0cc7 100644 --- a/src/declarations/console/console.factory.did.js +++ b/src/declarations/console/console.factory.did.js @@ -123,6 +123,15 @@ export const idlFactory = ({ IDL }) => { name: IDL.Opt(IDL.Text), user: IDL.Principal }); + const CreateCanisterArgs = IDL.Record({ + subnet_id: IDL.Opt(IDL.Principal) + }); + const CreateSegmentArgs = IDL.Variant({ + Orbiter: CreateOrbiterArgs, + MissionControl: CreateCanisterArgs, + Canister: CreateCanisterArgs, + Satellite: CreateSatelliteArgs + }); const DeleteControllersArgs = IDL.Record({ controllers: IDL.Vec(IDL.Principal) }); @@ -220,6 +229,7 @@ export const idlFactory = ({ IDL }) => { const SegmentKind = IDL.Variant({ Orbiter: IDL.Null, MissionControl: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const CyclesTokens = IDL.Record({ e12s: IDL.Nat64 }); @@ -423,6 +433,7 @@ export const idlFactory = ({ IDL }) => { }); const StorableSegmentKind = IDL.Variant({ Orbiter: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const ListSegmentsArgs = IDL.Record({ @@ -501,6 +512,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 d02b73458..0b17e0cc7 100644 --- a/src/declarations/console/console.factory.did.mjs +++ b/src/declarations/console/console.factory.did.mjs @@ -123,6 +123,15 @@ export const idlFactory = ({ IDL }) => { name: IDL.Opt(IDL.Text), user: IDL.Principal }); + const CreateCanisterArgs = IDL.Record({ + subnet_id: IDL.Opt(IDL.Principal) + }); + const CreateSegmentArgs = IDL.Variant({ + Orbiter: CreateOrbiterArgs, + MissionControl: CreateCanisterArgs, + Canister: CreateCanisterArgs, + Satellite: CreateSatelliteArgs + }); const DeleteControllersArgs = IDL.Record({ controllers: IDL.Vec(IDL.Principal) }); @@ -220,6 +229,7 @@ export const idlFactory = ({ IDL }) => { const SegmentKind = IDL.Variant({ Orbiter: IDL.Null, MissionControl: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const CyclesTokens = IDL.Record({ e12s: IDL.Nat64 }); @@ -423,6 +433,7 @@ export const idlFactory = ({ IDL }) => { }); const StorableSegmentKind = IDL.Variant({ Orbiter: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const ListSegmentsArgs = IDL.Record({ @@ -501,6 +512,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 d69e146e9..62f741388 100644 --- a/src/declarations/observatory/observatory.did.d.ts +++ b/src/declarations/observatory/observatory.did.d.ts @@ -117,7 +117,11 @@ export interface Segment { metadata: [] | [Array<[string, string]>]; kind: SegmentKind; } -export type SegmentKind = { Orbiter: null } | { MissionControl: null } | { Satellite: null }; +export type SegmentKind = + | { Orbiter: null } + | { MissionControl: null } + | { Canister: null } + | { Satellite: null }; export interface SetController { metadata: Array<[string, string]>; scope: ControllerScope; diff --git a/src/declarations/observatory/observatory.factory.certified.did.js b/src/declarations/observatory/observatory.factory.certified.did.js index ecb077c60..d88fac45a 100644 --- a/src/declarations/observatory/observatory.factory.certified.did.js +++ b/src/declarations/observatory/observatory.factory.certified.did.js @@ -97,6 +97,7 @@ export const idlFactory = ({ IDL }) => { const SegmentKind = IDL.Variant({ Orbiter: IDL.Null, MissionControl: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const Segment = IDL.Record({ diff --git a/src/declarations/observatory/observatory.factory.did.js b/src/declarations/observatory/observatory.factory.did.js index 8083ab0e5..c36f6894f 100644 --- a/src/declarations/observatory/observatory.factory.did.js +++ b/src/declarations/observatory/observatory.factory.did.js @@ -97,6 +97,7 @@ export const idlFactory = ({ IDL }) => { const SegmentKind = IDL.Variant({ Orbiter: IDL.Null, MissionControl: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const Segment = IDL.Record({ diff --git a/src/declarations/observatory/observatory.factory.did.mjs b/src/declarations/observatory/observatory.factory.did.mjs index 8083ab0e5..c36f6894f 100644 --- a/src/declarations/observatory/observatory.factory.did.mjs +++ b/src/declarations/observatory/observatory.factory.did.mjs @@ -97,6 +97,7 @@ export const idlFactory = ({ IDL }) => { const SegmentKind = IDL.Variant({ Orbiter: IDL.Null, MissionControl: IDL.Null, + Canister: IDL.Null, Satellite: IDL.Null }); const Segment = IDL.Record({ diff --git a/src/observatory/observatory.did b/src/observatory/observatory.did index 947a8317f..1739e46e9 100644 --- a/src/observatory/observatory.did +++ b/src/observatory/observatory.did @@ -76,7 +76,7 @@ type Segment = record { metadata : opt vec record { text; text }; kind : SegmentKind; }; -type SegmentKind = variant { Orbiter; MissionControl; Satellite }; +type SegmentKind = variant { Orbiter; MissionControl; Canister; Satellite }; type SetController = record { metadata : vec record { text; text }; scope : ControllerScope; From 5af1f3697cc8272827cfc204b603999d963f919a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 11:37:47 +0100 Subject: [PATCH 07/18] feat: upgrade state --- src/console/src/constants.rs | 1 + src/console/src/fees/init.rs | 12 ++++++++++-- src/console/src/memory/lifecycle.rs | 5 ++++- src/console/src/rates/init.rs | 7 +++++++ src/console/src/upgrade.rs | 29 +++++++++++++++++++++++++++-- 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/console/src/constants.rs b/src/console/src/constants.rs index ec4b46768..dbb896f43 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 CANISTER_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/fees/init.rs b/src/console/src/fees/init.rs index f5c1d74a9..46bc82db5 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, + CANISTER_CREATION_FEE_CYCLES, MISSION_CONTROL_CREATION_FEE_CYCLES, ORBITER_CREATION_FEE_CYCLES, + ORBITER_CREATION_FEE_ICP, SATELLITE_CREATION_FEE_CYCLES, SATELLITE_CREATION_FEE_ICP, }; 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::Canister, + FactoryFee { + fee_cycles: CANISTER_CREATION_FEE_CYCLES, + fee_icp: None, + updated_at: now, + }, + ), ]) } diff --git a/src/console/src/memory/lifecycle.rs b/src/console/src/memory/lifecycle.rs index 050a94eff..6dda67a0f 100644 --- a/src/console/src/memory/lifecycle.rs +++ b/src/console/src/memory/lifecycle.rs @@ -4,7 +4,7 @@ use crate::fees::init_factory_fees; use crate::memory::manager::{get_memory_upgrades, init_stable_state, STATE}; use crate::rates::init::init_factory_rates; use crate::types::state::{HeapState, ReleasesMetadata, State}; -use crate::upgrade::upgrade_init_factory_fees_and_rates; +use crate::upgrade::{upgrade_init_canister_fees_and_rates, upgrade_init_factory_fees_and_rates}; use ciborium::{from_reader, into_writer}; use ic_cdk_macros::{init, post_upgrade, pre_upgrade}; use junobuild_shared::ic::api::caller; @@ -60,4 +60,7 @@ fn post_upgrade() { // TODO: to be removed, one time upgrade upgrade_init_factory_fees_and_rates(); + + // TODO: to be removed, one time upgrade + upgrade_init_canister_fees_and_rates(); } diff --git a/src/console/src/rates/init.rs b/src/console/src/rates/init.rs index f51bf0364..0ddd76f59 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::Canister, + FactoryRate { + config: DEFAULT_RATE_CONFIG, + tokens: tokens.clone(), + }, + ), ]) } diff --git a/src/console/src/upgrade.rs b/src/console/src/upgrade.rs index 7aac7271f..9ccaebc81 100644 --- a/src/console/src/upgrade.rs +++ b/src/console/src/upgrade.rs @@ -1,7 +1,15 @@ -use crate::fees::init_factory_fees; +use crate::constants::CANISTER_CREATION_FEE_CYCLES; +use crate::fees::{init_factory_fees, set_factory_fee}; use crate::rates::init::init_factory_rates; +use crate::rates::set_factory_rate; use crate::store::mutate_heap_state; -use crate::types::state::HeapState; +use crate::types::interface::FeesArgs; +use crate::types::state::{FactoryFee, FactoryRate, 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_factory_fees_and_rates() { mutate_heap_state(|state| upgrade_init_factory_fees_and_rates_impl(state)) @@ -11,3 +19,20 @@ fn upgrade_init_factory_fees_and_rates_impl(state: &mut HeapState) { state.factory_fees.get_or_insert_with(init_factory_fees); state.factory_rates.get_or_insert_with(init_factory_rates); } + +pub fn upgrade_init_canister_fees_and_rates() { + mutate_heap_state(|state| upgrade_init_factory_fees_and_rates_impl(state)); + + // Init fee + let fee = FeesArgs { + fee_cycles: CANISTER_CREATION_FEE_CYCLES, + fee_icp: None, + }; + + set_factory_fee(&SegmentKind::Canister, &fee) + .unwrap_or_else(|err| print(format!("Error upgrading the Canister fee: {:?}", err))); + + // Init rate + set_factory_rate(&SegmentKind::Canister, &DEFAULT_RATE_CONFIG) + .unwrap_or_else(|err| print(format!("Error upgrading the Canister rate: {:?}", err))); +} From c0aa3deb7fe442fd34d4e71aae3af34a77207ae0 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 11:43:45 +0100 Subject: [PATCH 08/18] chore: no dev --- src/console/src/upgrade.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/console/src/upgrade.rs b/src/console/src/upgrade.rs index 9ccaebc81..e81ad3ca1 100644 --- a/src/console/src/upgrade.rs +++ b/src/console/src/upgrade.rs @@ -4,11 +4,9 @@ use crate::rates::init::init_factory_rates; use crate::rates::set_factory_rate; use crate::store::mutate_heap_state; use crate::types::interface::FeesArgs; -use crate::types::state::{FactoryFee, FactoryRate, HeapState}; -use ic_cdk::api::time; +use crate::types::state::{HeapState}; 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_factory_fees_and_rates() { From 1fa08eb4d58bce2461c64ca1f10da6189efb0081 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 12:28:23 +0100 Subject: [PATCH 09/18] fix: upgrade --- src/console/src/upgrade.rs | 55 +++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/src/console/src/upgrade.rs b/src/console/src/upgrade.rs index e81ad3ca1..a33512623 100644 --- a/src/console/src/upgrade.rs +++ b/src/console/src/upgrade.rs @@ -1,12 +1,12 @@ use crate::constants::CANISTER_CREATION_FEE_CYCLES; -use crate::fees::{init_factory_fees, set_factory_fee}; +use crate::fees::init_factory_fees; use crate::rates::init::init_factory_rates; -use crate::rates::set_factory_rate; use crate::store::mutate_heap_state; -use crate::types::interface::FeesArgs; -use crate::types::state::{HeapState}; +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_factory_fees_and_rates() { @@ -19,18 +19,49 @@ fn upgrade_init_factory_fees_and_rates_impl(state: &mut HeapState) { } pub fn upgrade_init_canister_fees_and_rates() { - mutate_heap_state(|state| upgrade_init_factory_fees_and_rates_impl(state)); + mutate_heap_state(|state| { + upgrade_init_factory_fees_and_rates_impl(state); - // Init fee - let fee = FeesArgs { + upgrade_canister_fees(&mut state.factory_fees) + .unwrap_or_else(|err| print(format!("Error upgrading the Canister fee: {:?}", err))); + + upgrade_canister_rates(&mut state.factory_rates) + .unwrap_or_else(|err| print(format!("Error upgrading the Canister rate: {:?}", err))); + }); +} + +fn upgrade_canister_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: CANISTER_CREATION_FEE_CYCLES, fee_icp: None, + updated_at: time(), + }; + + fees.insert(SegmentKind::Canister, fee); + + Ok(()) +} + +fn upgrade_canister_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(), }; - set_factory_fee(&SegmentKind::Canister, &fee) - .unwrap_or_else(|err| print(format!("Error upgrading the Canister fee: {:?}", err))); + rates.insert(SegmentKind::Canister, rate); - // Init rate - set_factory_rate(&SegmentKind::Canister, &DEFAULT_RATE_CONFIG) - .unwrap_or_else(|err| print(format!("Error upgrading the Canister rate: {:?}", err))); + Ok(()) } From bcef1fd8476b7d3a16a6d557e8412d382e0f84d3 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 12:46:49 +0100 Subject: [PATCH 10/18] feat: create canister --- src/frontend/src/lib/api/console.api.ts | 7 + .../lib/components/icons/IconCanister.svelte | 14 ++ .../launchpad/LaunchpadNewActions.svelte | 13 + .../src/lib/components/modals/Modals.svelte | 5 + .../factory/create/CanisterCreateModal.svelte | 226 ++++++++++++++++++ src/frontend/src/lib/i18n/en.json | 3 + .../factory/factory.create.services.ts | 28 ++- src/frontend/src/lib/types/i18n.d.ts | 5 + src/frontend/src/lib/types/modal.ts | 1 + 9 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/lib/components/icons/IconCanister.svelte create mode 100644 src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte diff --git a/src/frontend/src/lib/api/console.api.ts b/src/frontend/src/lib/api/console.api.ts index 305f1dccb..6d375f1b5 100644 --- a/src/frontend/src/lib/api/console.api.ts +++ b/src/frontend/src/lib/api/console.api.ts @@ -48,6 +48,13 @@ export const getMissionControlFee = async ({ }): Promise => await getFee({ identity, segmentKind: { MissionControl: null } }); +export const getCanisterFee = async ({ + identity + }: { + identity: OptionIdentity; +}): Promise => + await getFee({ identity, segmentKind: { Canister: null } }); + const getFee = async ({ identity, segmentKind diff --git a/src/frontend/src/lib/components/icons/IconCanister.svelte b/src/frontend/src/lib/components/icons/IconCanister.svelte new file mode 100644 index 000000000..e464652d0 --- /dev/null +++ b/src/frontend/src/lib/components/icons/IconCanister.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte b/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte index 05990eb4c..c01770f8b 100644 --- a/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte +++ b/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte @@ -13,12 +13,14 @@ } from '$lib/derived/console/account.mission-control.derived'; import { orbiterLoaded, orbiterStore } from '$lib/derived/orbiter.derived'; import { + initCanisterWizard, initMissionControlWizard, initOrbiterWizard, initSatelliteWizard } from '$lib/services/factory/factory.create.services'; import { i18n } from '$lib/stores/app/i18n.store'; import { testId } from '$lib/utils/test.utils'; + import IconCanister from '$lib/components/icons/IconCanister.svelte'; const createSatellite = async () => { close(); @@ -46,6 +48,15 @@ }); }; + const createCanister = async () => { + close(); + + await initCanisterWizard({ + identity: $authIdentity, + missionControlId: $missionControlId + }); + }; + const close = () => (visible = false); let visible = $state(false); @@ -74,6 +85,8 @@ > {$i18n.satellites.launch} + + {#if analyticsNotEnabled} | undefined = $state(undefined); @@ -50,6 +51,10 @@ {/if} +{#if modal?.type === 'create_canister' && nonNullish(modal.detail)} + +{/if} + {#if modal?.type === 'topup_satellite' && nonNullish(modal.detail)} {/if} diff --git a/src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte b/src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte new file mode 100644 index 000000000..3e0cd65cf --- /dev/null +++ b/src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte @@ -0,0 +1,226 @@ + + + + {#if step === 'ready'} + + +
+

{$i18n.satellites.ready}

+ +
+ {:else if step === 'in_progress'} + + {:else} +

{$i18n.satellites.start}

+ +

+ {$i18n.satellites.description} +

+ + +
+ + {#snippet label()} + {$i18n.satellites.satellite_name} + {/snippet} + + + +
+ + {#snippet label()} + {$i18n.satellites.what_are_you_building} + {/snippet} + +
+ + + +
+
+
+ + + + + +
+ {/if} +
+ + diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index f78738487..891421ab3 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -112,6 +112,9 @@ "custom": "Custom", "launch": "Launch" }, + "canister": { + "create": "Spin up a canister" + }, "canisters": { "top_up": "Top-up", "topping_up": "Topping-up", 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 0a057e9ed..ed1342761 100644 --- a/src/frontend/src/lib/services/factory/factory.create.services.ts +++ b/src/frontend/src/lib/services/factory/factory.create.services.ts @@ -1,5 +1,10 @@ import type { ConsoleDid, MissionControlDid } from '$declarations'; -import { getMissionControlFee, getOrbiterFee, getSatelliteFee } from '$lib/api/console.api'; +import { + getCanisterFee, + getMissionControlFee, + getOrbiterFee, + getSatelliteFee +} from '$lib/api/console.api'; import { updateAndStartMonitoring } from '$lib/api/mission-control.api'; import { missionControlMonitored } from '$lib/derived/mission-control/mission-control-settings.derived'; import { missionControlConfigMonitoring } from '$lib/derived/mission-control/mission-control-user.derived'; @@ -104,6 +109,20 @@ export const initMissionControlWizard = ({ modalType: 'create_mission_control' }); +export const initCanisterWizard = ({ + missionControlId, + identity +}: { + missionControlId: Option; + identity: OptionIdentity; +}): Promise => + initCreateWizard({ + missionControlId, + identity, + feeFn: getCreateCanisterFeeBalance, + modalType: 'create_canister' + }); + const initCreateWizard = async ({ missionControlId, identity, @@ -113,7 +132,7 @@ const initCreateWizard = async ({ missionControlId: Option; identity: OptionIdentity; feeFn: GetFeeBalanceFn; - modalType: 'create_satellite' | 'create_orbiter' | 'create_mission_control'; + modalType: 'create_satellite' | 'create_orbiter' | 'create_mission_control' | 'create_canister'; }) => { if (missionControlId === undefined) { toasts.warn(get(i18n).errors.mission_control_not_loaded); @@ -190,7 +209,7 @@ const initCreateWizardWithoutMissionControl = ({ modalType }: { fee: ConsoleDid.FactoryFee; - modalType: 'create_satellite' | 'create_orbiter' | 'create_mission_control'; + modalType: 'create_satellite' | 'create_orbiter' | 'create_mission_control' | 'create_canister'; }) => { emit>({ message: 'junoModal', @@ -214,6 +233,9 @@ const getCreateOrbiterFeeBalance: GetFeeBalanceFn = async (params): Promise => await getCreateFeeBalance({ ...params, getFee: getMissionControlFee }); +const getCreateCanisterFeeBalance: GetFeeBalanceFn = async (params): Promise => + await getCreateFeeBalance({ ...params, getFee: getCanisterFee }); + const getCreateFeeBalance = async ({ identity, getFee diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index b5cc1a052..29671f8cb 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -116,6 +116,10 @@ interface I18nCore { launch: string; } +interface I18nCanister { + create: string; +} + interface I18nCanisters { top_up: string; topping_up: string; @@ -1073,6 +1077,7 @@ interface I18nSpotlight { interface I18n { lang: Languages; core: I18nCore; + canister: I18nCanister; canisters: I18nCanisters; sign_in: I18nSign_in; sign_in_openid: I18nSign_in_openid; diff --git a/src/frontend/src/lib/types/modal.ts b/src/frontend/src/lib/types/modal.ts index 7ab03df07..1a10c888e 100644 --- a/src/frontend/src/lib/types/modal.ts +++ b/src/frontend/src/lib/types/modal.ts @@ -155,6 +155,7 @@ export interface JunoModal { | 'create_satellite' | 'create_orbiter' | 'create_mission_control' + | 'create_canister' | 'delete_satellite' | 'delete_orbiter' | 'transfer_cycles_satellite' From 00e72d5eb7fa8ec17adbc108528c3ae4a99f9d78 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 12:50:43 +0100 Subject: [PATCH 11/18] feat: canister id schema --- .../modals/factory/create/CanisterCreateModal.svelte | 7 ++++--- src/frontend/src/lib/schemas/canister.schema.ts | 2 ++ src/frontend/src/lib/types/canister.ts | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte b/src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte index 3e0cd65cf..2bffb73f1 100644 --- a/src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte +++ b/src/frontend/src/lib/components/modals/factory/create/CanisterCreateModal.svelte @@ -21,6 +21,7 @@ import type { Option } from '$lib/types/utils'; import { navigateToSatellite } from '$lib/utils/nav.utils'; import { testId } from '$lib/utils/test.utils'; + import type { CanisterId } from '$lib/types/canister'; interface Props { detail: JunoModalDetail; @@ -33,7 +34,7 @@ let insufficientFunds = $state(true); let step: 'init' | 'in_progress' | 'ready' | 'error' = $state('init'); - let satelliteId: SatelliteId | undefined = undefined; + let canisterId = $state(undefined); // Submit @@ -68,13 +69,13 @@ return; } - satelliteId = result.canisterId; + canisterId = result.canisterId; setTimeout(() => (step = 'ready'), 500); }; const navigate = async () => { - await navigateToSatellite(satelliteId); + await navigateToSatellite(canisterId); onclose(); }; diff --git a/src/frontend/src/lib/schemas/canister.schema.ts b/src/frontend/src/lib/schemas/canister.schema.ts index c49896be2..bb7a1b90e 100644 --- a/src/frontend/src/lib/schemas/canister.schema.ts +++ b/src/frontend/src/lib/schemas/canister.schema.ts @@ -1,3 +1,5 @@ +import { PrincipalSchema } from '$lib/schemas/principal.schema'; import { PrincipalTextSchema } from '@dfinity/zod-schemas'; export const CanisterIdTextSchema = PrincipalTextSchema; +export const CanisterIdSchema = PrincipalSchema; diff --git a/src/frontend/src/lib/types/canister.ts b/src/frontend/src/lib/types/canister.ts index af15c2543..465594374 100644 --- a/src/frontend/src/lib/types/canister.ts +++ b/src/frontend/src/lib/types/canister.ts @@ -1,4 +1,4 @@ -import type { CanisterIdTextSchema } from '$lib/schemas/canister.schema'; +import { CanisterIdSchema, type CanisterIdTextSchema } from '$lib/schemas/canister.schema'; import type { ChartsData, TimeOfDayChartData } from '$lib/types/chart'; import type { MonitoringHistory, MonitoringMetadata } from '$lib/types/monitoring'; import type { CertifiedData } from '$lib/types/store'; @@ -83,6 +83,7 @@ export interface CanisterMonitoringData { charts: CanisterMonitoringCharts; } +export type CanisterId = z.infer; export type CanisterIdText = z.infer; export interface Canister { From 2f98e89d10891ea3fbab95a558a87bfc39c36dcf Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 14:04:48 +0100 Subject: [PATCH 12/18] feat: create canister with optional name --- src/console/console.did | 4 +- src/console/src/factory/canister.rs | 8 +- src/declarations/console/console.did.d.ts | 3 +- .../console/console.factory.certified.did.js | 5 +- .../console/console.factory.did.js | 5 +- .../console/console.factory.did.mjs | 5 +- src/frontend/src/lib/api/console.api.ts | 7 +- .../create/FactoryProgressCreate.svelte | 10 +- .../launchpad/LaunchpadNewActions.svelte | 2 +- .../src/lib/components/modals/Modals.svelte | 2 +- .../factory/create/CanisterCreateModal.svelte | 76 +++-------- src/frontend/src/lib/i18n/en.json | 13 +- .../console/console.factory.services.ts | 24 ++++ .../factory/factory.create.services.ts | 126 ++++++++++++++++++ src/frontend/src/lib/types/factory.ts | 3 + src/frontend/src/lib/types/i18n.d.ts | 11 ++ src/frontend/src/lib/utils/nav.utils.ts | 6 + src/libs/shared/src/types.rs | 1 + 18 files changed, 231 insertions(+), 80 deletions(-) diff --git a/src/console/console.did b/src/console/console.did index af852d342..6d0283695 100644 --- a/src/console/console.did +++ b/src/console/console.did @@ -78,7 +78,7 @@ type Controller = record { expires_at : opt nat64; }; type ControllerScope = variant { Write; Admin; Submit }; -type CreateCanisterArgs = record { subnet_id : opt principal }; +type CreateCanisterArgs = record { subnet_id : opt principal; name : opt text }; type CreateMissionControlArgs = record { subnet_id : opt principal }; type CreateOrbiterArgs = record { block_index : opt nat64; @@ -95,7 +95,7 @@ type CreateSatelliteArgs = record { }; type CreateSegmentArgs = variant { Orbiter : CreateOrbiterArgs; - MissionControl : CreateCanisterArgs; + MissionControl : CreateMissionControlArgs; Canister : CreateCanisterArgs; Satellite : CreateSatelliteArgs; }; diff --git a/src/console/src/factory/canister.rs b/src/console/src/factory/canister.rs index ce22651f7..517856c21 100644 --- a/src/console/src/factory/canister.rs +++ b/src/console/src/factory/canister.rs @@ -25,6 +25,7 @@ pub async fn create_canister( ) -> 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::Canister)?.fee_cycles; @@ -41,7 +42,7 @@ pub async fn create_canister( ) .await?; - add_segment(&account.owner, &canister_id); + add_segment(&account.owner, &canister_id, &name); Ok(canister_id) } @@ -74,8 +75,9 @@ async fn create_raw_canister( Ok(mission_control_id) } -fn add_segment(user: &UserId, canister_id: &Principal) { - let canister = Segment::new(canister_id, None); +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::Canister); add_segment_store(&key, &canister) } diff --git a/src/declarations/console/console.did.d.ts b/src/declarations/console/console.did.d.ts index 47b310d5a..eefbbb7ff 100644 --- a/src/declarations/console/console.did.d.ts +++ b/src/declarations/console/console.did.d.ts @@ -106,6 +106,7 @@ export interface Controller { export type ControllerScope = { Write: null } | { Admin: null } | { Submit: null }; export interface CreateCanisterArgs { subnet_id: [] | [Principal]; + name: [] | [string]; } export interface CreateMissionControlArgs { subnet_id: [] | [Principal]; @@ -125,7 +126,7 @@ export interface CreateSatelliteArgs { } export type CreateSegmentArgs = | { Orbiter: CreateOrbiterArgs } - | { MissionControl: CreateCanisterArgs } + | { MissionControl: CreateMissionControlArgs } | { Canister: CreateCanisterArgs } | { Satellite: CreateSatelliteArgs }; export interface CustomDomain { diff --git a/src/declarations/console/console.factory.certified.did.js b/src/declarations/console/console.factory.certified.did.js index 328b737f5..0703ecfdc 100644 --- a/src/declarations/console/console.factory.certified.did.js +++ b/src/declarations/console/console.factory.certified.did.js @@ -124,11 +124,12 @@ export const idlFactory = ({ IDL }) => { user: IDL.Principal }); const CreateCanisterArgs = IDL.Record({ - subnet_id: IDL.Opt(IDL.Principal) + subnet_id: IDL.Opt(IDL.Principal), + name: IDL.Opt(IDL.Text) }); const CreateSegmentArgs = IDL.Variant({ Orbiter: CreateOrbiterArgs, - MissionControl: CreateCanisterArgs, + MissionControl: CreateMissionControlArgs, Canister: CreateCanisterArgs, Satellite: CreateSatelliteArgs }); diff --git a/src/declarations/console/console.factory.did.js b/src/declarations/console/console.factory.did.js index 0b17e0cc7..3049be159 100644 --- a/src/declarations/console/console.factory.did.js +++ b/src/declarations/console/console.factory.did.js @@ -124,11 +124,12 @@ export const idlFactory = ({ IDL }) => { user: IDL.Principal }); const CreateCanisterArgs = IDL.Record({ - subnet_id: IDL.Opt(IDL.Principal) + subnet_id: IDL.Opt(IDL.Principal), + name: IDL.Opt(IDL.Text) }); const CreateSegmentArgs = IDL.Variant({ Orbiter: CreateOrbiterArgs, - MissionControl: CreateCanisterArgs, + MissionControl: CreateMissionControlArgs, Canister: CreateCanisterArgs, Satellite: CreateSatelliteArgs }); diff --git a/src/declarations/console/console.factory.did.mjs b/src/declarations/console/console.factory.did.mjs index 0b17e0cc7..3049be159 100644 --- a/src/declarations/console/console.factory.did.mjs +++ b/src/declarations/console/console.factory.did.mjs @@ -124,11 +124,12 @@ export const idlFactory = ({ IDL }) => { user: IDL.Principal }); const CreateCanisterArgs = IDL.Record({ - subnet_id: IDL.Opt(IDL.Principal) + subnet_id: IDL.Opt(IDL.Principal), + name: IDL.Opt(IDL.Text) }); const CreateSegmentArgs = IDL.Variant({ Orbiter: CreateOrbiterArgs, - MissionControl: CreateCanisterArgs, + MissionControl: CreateMissionControlArgs, Canister: CreateCanisterArgs, Satellite: CreateSatelliteArgs }); diff --git a/src/frontend/src/lib/api/console.api.ts b/src/frontend/src/lib/api/console.api.ts index 6d375f1b5..9b71d5516 100644 --- a/src/frontend/src/lib/api/console.api.ts +++ b/src/frontend/src/lib/api/console.api.ts @@ -49,11 +49,10 @@ export const getMissionControlFee = async ({ await getFee({ identity, segmentKind: { MissionControl: null } }); export const getCanisterFee = async ({ - identity - }: { + identity +}: { identity: OptionIdentity; -}): Promise => - await getFee({ identity, segmentKind: { Canister: null } }); +}): Promise => await getFee({ identity, segmentKind: { Canister: null } }); const getFee = async ({ identity, diff --git a/src/frontend/src/lib/components/factory/create/FactoryProgressCreate.svelte b/src/frontend/src/lib/components/factory/create/FactoryProgressCreate.svelte index 36168e0d2..6cb452b04 100644 --- a/src/frontend/src/lib/components/factory/create/FactoryProgressCreate.svelte +++ b/src/frontend/src/lib/components/factory/create/FactoryProgressCreate.svelte @@ -13,7 +13,7 @@ interface Props { progress: FactoryCreateProgress | undefined; - segment: 'satellite' | 'mission_control' | 'orbiter'; + segment: 'satellite' | 'mission_control' | 'orbiter' | 'canister'; withApprove: boolean; withMonitoring?: boolean; withAttach?: boolean; @@ -61,7 +61,9 @@ ? $i18n.mission_control.initializing : segment === 'orbiter' ? $i18n.analytics.initializing - : $i18n.satellites.initializing + : segment === 'canister' + ? $i18n.canister.initializing + : $i18n.satellites.initializing }, ...(withAttach === true && { attaching: { @@ -72,7 +74,9 @@ ? (attachProgressText ?? $i18n.mission_control.attaching) : segment === 'orbiter' ? $i18n.analytics.attaching - : $i18n.satellites.attaching + : segment === 'canister' + ? $i18n.canister.attaching + : $i18n.satellites.attaching } }), ...(withMonitoring === true && { diff --git a/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte b/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte index c01770f8b..9a7643d3a 100644 --- a/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte +++ b/src/frontend/src/lib/components/launchpad/LaunchpadNewActions.svelte @@ -85,7 +85,7 @@ > {$i18n.satellites.launch} - + {#if analyticsNotEnabled} @@ -102,22 +103,22 @@ {:else if step === 'in_progress'} {:else} -

{$i18n.satellites.start}

+

{$i18n.canister.start}

- {$i18n.satellites.description} + {$i18n.canister.description}

{#snippet label()} - {$i18n.satellites.satellite_name} + {$i18n.canister.canister_name} {/snippet} -
- - {#snippet label()} - {$i18n.satellites.what_are_you_building} - {/snippet} - -
- - - -
-
-
- - {$i18n.satellites.create} + {$i18n.canister.create}
@@ -215,13 +184,4 @@ button { margin-top: var(--padding-2x); } - - .building { - margin: var(--padding-2x) 0 0; - } - - .options { - display: flex; - flex-direction: column; - } diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json index 891421ab3..d573abd36 100644 --- a/src/frontend/src/lib/i18n/en.json +++ b/src/frontend/src/lib/i18n/en.json @@ -113,7 +113,16 @@ "launch": "Launch" }, "canister": { - "create": "Spin up a canister" + "launch": "Spin up a Canister", + "initializing": "Spinning up your Canister...", + "attaching": "Sharing Canister with Mission Control...", + "start": "Spin up a canister", + "description": "A canister is a blank container waiting for your code. Use it to implement your ideas from scratch.", + "create_canister_price": "Starting a new Canister requires {0}. Your current balance is {1}.", + "canister_name": "Canister name", + "enter_name": "Enter a name for your Canister", + "create": "Create Canister", + "ready": "Your Canister is ready!" }, "canisters": { "top_up": "Top-up", @@ -591,6 +600,8 @@ "satellite_metadata_update": "Unexpected error(s) while trying to set the metadata of your Satellite.", "satellite_missing_name": "A name must be provided.", "satellites_not_loaded": "The Satellites data are not yet loaded.", + "create_canister_name_missing": "A name for the Canister must be provided.", + "create_canister_unexpected_error": "Unexpected error(s) while creating the Canister.", "canister_stop": "Unexpected error(s) while trying to stop the module.", "canister_start": "Unexpected error(s) while trying to start the module.", "canister_delete": "Unexpected error(s) while trying to delete the module.", diff --git a/src/frontend/src/lib/services/console/console.factory.services.ts b/src/frontend/src/lib/services/console/console.factory.services.ts index 67f1329f1..653977fe5 100644 --- a/src/frontend/src/lib/services/console/console.factory.services.ts +++ b/src/frontend/src/lib/services/console/console.factory.services.ts @@ -1,5 +1,7 @@ import { getConsoleActor } from '$lib/api/actors/actor.juno.api'; +import type { CanisterId } from '$lib/types/canister'; import type { + CreateCanisterConfig, CreateSatelliteConfig, CreateWithConfig, CreateWithConfigAndName @@ -86,3 +88,25 @@ export const createOrbiterWithConfig = async ({ subnet_id: toNullable(subnetId) }); }; + +export const createCanisterWithConfig = async ({ + identity, + config: { name, subnetId } +}: { + identity: OptionIdentity; + config: CreateCanisterConfig; +}): Promise => { + assertNonNullish(identity); + + const { create_segment } = await getConsoleActor({ + identity + }); + + // TODO: duplicate payload + return create_segment({ + Canister: { + name: toNullable(name), + subnet_id: toNullable(subnetId) + } + }); +}; 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 ed1342761..fec7f90b1 100644 --- a/src/frontend/src/lib/services/factory/factory.create.services.ts +++ b/src/frontend/src/lib/services/factory/factory.create.services.ts @@ -13,6 +13,7 @@ import type { SelectedWallet } from '$lib/schemas/wallet.schema'; import { execute } from '$lib/services/_progress.services'; import { reloadAccount } from '$lib/services/console/account.services'; import { + createCanisterWithConfig as createCanisterWithConsoleAndConfig, createMissionControlWithConfig as createMissionControlWithConsoleAndConfig, createOrbiterWithConfig as createOrbiterWithConsoleAndConfig, createSatelliteWithConfig as createSatelliteWithConsoleAndConfig @@ -41,6 +42,7 @@ import { busy } from '$lib/stores/app/busy.store'; import { i18n } from '$lib/stores/app/i18n.store'; import { toasts } from '$lib/stores/app/toasts.store'; import type { + CreateCanisterConfig, CreateSatelliteConfig, CreateWithConfig, CreateWithConfigAndName @@ -639,6 +641,130 @@ export const createMissionControlWizard = async ({ }); }; +export const createCanisterWizard = async ({ + missionControlId, + onProgress, + subnetId, + canisterName, + monitoringStrategy, + ...rest +}: CreateWizardParams & { + canisterName: string | undefined; +}): Promise => { + if (isEmptyString(canisterName)) { + toasts.error({ + text: get(i18n).errors.create_canister_name_missing + }); + return { success: 'error' }; + } + + const createFn: CreateFn = async ({ identity, selectedWallet: { type: walletType } }) => { + if (walletType === 'mission_control') { + // TODO: + throw new Error('Mission Control wallet not supported'); + } + + return await createWithConsoleFn({ identity }); + }; + + const createConfig: CreateCanisterConfig = { + name: canisterName, + ...(nonNullish(subnetId) && { subnetId: Principal.fromText(subnetId) }) + }; + + const createWithConsoleFn = async ({ identity }: { identity: Identity }): Promise => + await createCanisterWithConsoleAndConfig({ + identity, + config: createConfig + }); + + const buildAttachFn = (): AttachFn | undefined => { + if (isNullish(missionControlId)) { + return undefined; + } + + const attachFn: AttachFn = async ({ + identity, + canisterId, + selectedWallet: { type: walletType } + }) => { + // Mission Control already knowns the newly created module + if (walletType === 'mission_control') { + // TODO: + // 1. Handle error + // 2. Do not show Mission Control wallet in the UI if defined - in the dropdown + throw new Error('Mission Control wallet not supported'); + } + + // Attach the Satellite to the existing Mission Control. + // The controller for the Mission Control to the Satellite has been set by the Console backend. + // await attachSatelliteToMissionControl({ + // missionControlId, + // satelliteId: canisterId, + // identity, + // satelliteName + // }); + + // TODO: + throw new Error('Attach to Mission Control not yet supported'); + }; + + return attachFn; + }; + + const attachFn = buildAttachFn(); + + const buildMonitoringFn = (): MonitoringFn | undefined => { + if (isNullish(monitoringStrategy)) { + return undefined; + } + + return async ({ + identity, + canisterId + }: { + identity: Identity; + canisterId: Principal; + }): Promise => { + assertNonNullish(missionControlId); + + // await updateAndStartMonitoring({ + // identity, + // missionControlId, + // config: { + // cycles_config: toNullable({ + // mission_control_strategy: toNullable(), + // satellites_strategy: toNullable({ + // strategy: monitoringStrategy, + // ids: [canisterId] + // }), + // orbiters_strategy: toNullable() + // }) + // } + // }); + + // TODO: + throw new Error('Start monitoring not yet supported'); + }; + }; + + const monitoringFn = buildMonitoringFn(); + + const reloadFn: ReloadFn = async () => { + await loadSegments({ missionControlId, reload: true, reloadOrbiters: false }); + }; + + return await createWizard({ + ...rest, + onProgress, + createFn, + reloadFn, + attachFn, + monitoringFn, + errorLabel: 'create_canister_unexpected_error' + }); +}; + type CreateFn = (params: { identity: Identity; selectedWallet: SelectedWallet; diff --git a/src/frontend/src/lib/types/factory.ts b/src/frontend/src/lib/types/factory.ts index 83ff56fcd..2d8c98565 100644 --- a/src/frontend/src/lib/types/factory.ts +++ b/src/frontend/src/lib/types/factory.ts @@ -12,3 +12,6 @@ export interface CreateSatelliteConfig extends CreateWithConfig { name: string; kind: 'website' | 'application'; } +export interface CreateCanisterConfig extends CreateWithConfig { + name: string; +} diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts index 29671f8cb..dcc23e3dc 100644 --- a/src/frontend/src/lib/types/i18n.d.ts +++ b/src/frontend/src/lib/types/i18n.d.ts @@ -117,7 +117,16 @@ interface I18nCore { } interface I18nCanister { + launch: string; + initializing: string; + attaching: string; + start: string; + description: string; + create_canister_price: string; + canister_name: string; + enter_name: string; create: string; + ready: string; } interface I18nCanisters { @@ -610,6 +619,8 @@ interface I18nErrors { satellite_metadata_update: string; satellite_missing_name: string; satellites_not_loaded: string; + create_canister_name_missing: string; + create_canister_unexpected_error: string; canister_stop: string; canister_start: string; canister_delete: string; diff --git a/src/frontend/src/lib/utils/nav.utils.ts b/src/frontend/src/lib/utils/nav.utils.ts index 86d3dab7b..0ea695748 100644 --- a/src/frontend/src/lib/utils/nav.utils.ts +++ b/src/frontend/src/lib/utils/nav.utils.ts @@ -20,6 +20,9 @@ export const upgradeDockLink = (satelliteId?: Option): string => export const upgradeChangesLink = (satelliteId: Option): string => `/upgrade-dock/?tab=changes${nonNullish(satelliteId) ? `&s=${satelliteId?.toText() ?? ''}` : ''}`; +export const canisterLink = (canisterId?: Option): string => + `/canister/${nonNullish(canisterId) ? `?c=${canisterId?.toText() ?? ''}` : ''}`; + export const navigateToSatellite = async (satelliteId: Option) => await goto(overviewLink(satelliteId)); @@ -32,6 +35,9 @@ export const navigateToMonitoring = async (satelliteId: Option) => export const navigateToChangesDock = async (satelliteId: Option) => await goto(upgradeChangesLink(satelliteId), { replaceState: true }); +export const navigateToCanister = async (canisterId: Option) => + await goto(canisterLink(canisterId)); + export const back = async ({ pop }: { pop: boolean }) => { if (pop) { history.back(); diff --git a/src/libs/shared/src/types.rs b/src/libs/shared/src/types.rs index bc9437ec7..ad32e915e 100644 --- a/src/libs/shared/src/types.rs +++ b/src/libs/shared/src/types.rs @@ -165,6 +165,7 @@ pub mod interface { #[derive(CandidType, Deserialize)] pub struct CreateCanisterArgs { pub subnet_id: Option, + pub name: Option, } #[derive(CandidType, Deserialize)] From 5826761f855c36e348e65914da39cf1efb23324a Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Tue, 6 Jan 2026 15:01:35 +0100 Subject: [PATCH 13/18] feat: canister overview --- .../canister-segment/CanisterOverview.svelte | 96 +++++++++++++++++++ .../src/lib/components/core/Navmenu.svelte | 49 +++++++--- .../components/guards/CanisterGuard.svelte | 25 +++++ .../lib/components/icons/IconCanister.svelte | 2 +- .../lib/components/launchpad/Launchpad.svelte | 2 + .../launchpad/LaunchpadCanister.svelte | 77 +++++++++++++++ .../launchpad/LaunchpadNewActions.svelte | 2 +- .../launchpad/LaunchpadSegments.svelte | 18 ++++ .../src/lib/components/loaders/Loaders.svelte | 3 +- .../components/loaders/MetadataLoader.svelte | 8 +- .../src/lib/components/modals/Modals.svelte | 2 +- .../factory/create/CanisterCreateModal.svelte | 2 +- .../satellites/SatellitesPicker.svelte | 9 +- .../lib/derived/app/page.derived.svelte.ts | 28 ++++-- .../src/lib/derived/canister.derived.ts | 30 ++++++ .../lib/derived/console/canisters.derived.ts | 20 ++++ .../lib/derived/console/segments.derived.ts | 11 +++ .../src/lib/derived/satellite.derived.ts | 14 ++- src/frontend/src/lib/i18n/en.json | 7 +- src/frontend/src/lib/types/canister.ts | 4 +- src/frontend/src/lib/types/i18n.d.ts | 5 + src/frontend/src/lib/types/segment.ts | 18 +++- src/frontend/src/lib/utils/nav.utils.ts | 23 +++++ .../routes/(split)/canister/+layout.svelte | 33 +++++++ .../src/routes/(split)/canister/+page.svelte | 62 ++++++++++++ .../src/routes/(split)/canister/+page.ts | 5 + .../src/routes/(standalone)/cli/+page.svelte | 3 +- 27 files changed, 516 insertions(+), 42 deletions(-) create mode 100644 src/frontend/src/lib/components/canister-segment/CanisterOverview.svelte create mode 100644 src/frontend/src/lib/components/guards/CanisterGuard.svelte create mode 100644 src/frontend/src/lib/components/launchpad/LaunchpadCanister.svelte create mode 100644 src/frontend/src/lib/derived/canister.derived.ts create mode 100644 src/frontend/src/lib/derived/console/canisters.derived.ts create mode 100644 src/frontend/src/routes/(split)/canister/+layout.svelte create mode 100644 src/frontend/src/routes/(split)/canister/+page.svelte create mode 100644 src/frontend/src/routes/(split)/canister/+page.ts diff --git a/src/frontend/src/lib/components/canister-segment/CanisterOverview.svelte b/src/frontend/src/lib/components/canister-segment/CanisterOverview.svelte new file mode 100644 index 000000000..c11bfec54 --- /dev/null +++ b/src/frontend/src/lib/components/canister-segment/CanisterOverview.svelte @@ -0,0 +1,96 @@ + + + + +
+ {$i18n.satellites.overview} + +
+
+ + + + + +
+ +
+ + {#snippet label()} + {$i18n.canister.id} + {/snippet} + + + + +
+
+
+ + + +
+ {$i18n.monitoring.runtime} + +
+ +
+
+ + + + diff --git a/src/frontend/src/lib/components/core/Navmenu.svelte b/src/frontend/src/lib/components/core/Navmenu.svelte index 40648d301..c636680d7 100644 --- a/src/frontend/src/lib/components/core/Navmenu.svelte +++ b/src/frontend/src/lib/components/core/Navmenu.svelte @@ -1,11 +1,12 @@ @@ -34,13 +42,26 @@ class="link" class:collapsed={$menuCollapsed} class:selected={isRouteSelected({ routeId, path: 'satellite' })} - href={`/satellite${queryParam}`} + href={`/satellite${satelliteQueryParam}`} role="menuitem" > {$i18n.satellites.satellite} + {#if notEmptyString(canisterId)} + + + {$i18n.canister.title} + + {/if} + {#if $isSatelliteRoute}