Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/console/console.did
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) -> ();
Expand Down
19 changes: 18 additions & 1 deletion src/console/src/api/factory.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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()
}
1 change: 1 addition & 0 deletions src/console/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 12 additions & 1 deletion src/console/src/factory/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -63,3 +63,14 @@ impl From<CreateMissionControlArgs> for CreateSegmentArgs {
}
}
}

impl From<CreateUfoArgs> 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,
}
}
}
1 change: 1 addition & 0 deletions src/console/src/factory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ mod orchestrator;
pub mod satellite;
mod services;
mod types;
pub mod ufo;
mod utils;
82 changes: 82 additions & 0 deletions src/console/src/factory/ufo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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::remove_console_controller;
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<Principal, String> {
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<SubnetId>,
) -> Result<Principal, String> {
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 = [id()].into_iter().chain(controllers.clone()).collect();

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
}?;

remove_console_controller(&ufo_id, &controllers).await?;

Ok(ufo_id)
}

fn add_segment(user: &UserId, canister_id: &Principal, name: &Option<String>) {
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)
}
10 changes: 9 additions & 1 deletion src/console/src/fees/init.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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,
},
),
])
}
1 change: 1 addition & 0 deletions src/console/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/console/src/memory/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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();
}
1 change: 1 addition & 0 deletions src/console/src/memory/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod lifecycle;
pub mod manager;
mod upgrade;
54 changes: 54 additions & 0 deletions src/console/src/memory/upgrade.rs
Original file line number Diff line number Diff line change
@@ -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};
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<FactoryFees>) -> 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<FactoryRates>) -> 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(())
}
7 changes: 7 additions & 0 deletions src/console/src/rates/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,12 @@ pub fn init_factory_rates() -> FactoryRates {
tokens: tokens.clone(),
},
),
(
SegmentKind::Ufo,
FactoryRate {
config: DEFAULT_RATE_CONFIG,
tokens: tokens.clone(),
},
),
])
}
4 changes: 4 additions & 0 deletions src/console/src/rates/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion src/console/src/segments/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
};

Expand Down
1 change: 1 addition & 0 deletions src/console/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 16 additions & 2 deletions src/declarations/console/console.did.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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>;
Expand Down
Loading
Loading