1use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10pub const FLEET_ENSURE_SCHEMA_VERSION: u16 = 1;
11pub const MAX_FLEET_ENSURE_CANISTERS: usize = 4_096;
12pub const MAX_FLEET_ENSURE_PROTOCOL_STEPS: usize = 4_096;
13
14#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
17pub struct LiveCanister {
18 pub canister_version: Option<u64>,
19 pub controllers: Vec<String>,
20 #[serde(with = "u128_text")]
21 pub cycles: u128,
22 pub module_sha256: Option<String>,
23 pub principal: String,
24 pub reinstall_required: bool,
25 pub root_owned_lifecycle: Option<RootOwnedCanisterLifecycle>,
26 pub status: CanisterRuntimeStatus,
27}
28
29#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
32#[serde(rename_all = "snake_case")]
33pub enum RootOwnedCanisterLifecycle {
34 Claimed,
35 Idle,
36 Retained,
38 Store,
39 Workload,
40}
41
42#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
45#[serde(rename_all = "snake_case")]
46pub enum CanisterRuntimeStatus {
47 Running,
48 Stopped,
49 Stopping,
50}
51
52#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
55pub struct FleetObservation {
56 pub additional_controlled_cycles: BTreeMap<String, u128>,
57 pub canisters: BTreeMap<String, Option<LiveCanister>>,
58 #[serde(with = "u128_text")]
59 pub ledger_fee_cycles: u128,
60 #[serde(with = "u128_text")]
61 pub operator_cycles: u128,
62 pub protocol_ready: BTreeMap<String, bool>,
63}
64
65#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
68#[serde(rename_all = "snake_case")]
69pub enum DesiredPresence {
70 Absent,
71 Present,
72}
73
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
77#[serde(deny_unknown_fields)]
78pub struct DrainAuthority {
79 pub candid: String,
80 pub method: String,
81 pub destination: String,
82 pub maximum_execution_burn_cycles: String,
83}
84
85#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
88#[serde(deny_unknown_fields)]
89pub struct DesiredCanister {
90 #[serde(default)]
91 pub canic_init: Option<DesiredCanisterInit>,
92 #[serde(default)]
93 pub controller_canisters: Vec<String>,
94 pub controllers: Vec<String>,
95 pub drain: Option<DrainAuthority>,
96 pub initial_cycles: String,
97 pub init_arg: Option<String>,
98 pub init_candid: Option<String>,
99 pub kind: DesiredCanisterKind,
100 pub minimum_cycles: String,
101 pub name: String,
102 pub parent: Option<String>,
103 pub presence: DesiredPresence,
104 pub principal: Option<String>,
105 #[serde(skip)]
106 pub(crate) protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
107 pub replace: bool,
108 pub subnet: String,
109 pub wasm: Option<String>,
110}
111
112#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
114#[serde(rename_all = "snake_case")]
115pub enum DesiredCanisterKind {
116 Auxiliary,
117 Component,
118 Coordinator,
119 Pool,
120 Root,
121 Store,
122}
123
124#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
126#[serde(rename_all = "snake_case", tag = "role")]
127pub enum DesiredCanisterInit {
128 Coordinator,
129 Root { root: String },
130 Store { root: String },
131}
132
133#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
140#[serde(deny_unknown_fields)]
141pub(crate) struct DesiredProtocolStep {
142 pub canister: String,
143 pub candid: String,
144 pub command_args: String,
145 pub command_method: String,
146 pub expected_status: String,
147 pub maximum_execution_burn_cycles: String,
148 pub name: String,
149 pub status_args: String,
150 pub status_method: String,
151}
152
153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156#[serde(deny_unknown_fields)]
157pub struct DesiredFleet {
158 #[serde(default)]
159 pub bootstrap: Option<DesiredFleetBootstrap>,
160 pub canisters: Vec<DesiredCanister>,
161 pub cycles_ledger: String,
162 pub environment: String,
163 pub fleet: String,
164 pub ledger_fee_cycles: String,
165 pub management_creation_fee_cycles: String,
166 pub material_cycle_threshold: String,
167 pub maximum_observation_burn_cycles: String,
168 pub maximum_stalled_observations: u32,
169 pub maximum_update_burn_cycles: String,
170 pub operator: String,
171 #[serde(default)]
172 pub protocol: Option<DesiredFleetProtocol>,
173 #[serde(skip)]
174 pub(crate) protocol_steps: Vec<DesiredProtocolStep>,
175 pub schema_version: u16,
176 pub treasury: String,
178}
179
180#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
182#[serde(deny_unknown_fields)]
183pub struct DesiredFleetBootstrap {
184 pub admission: canic_core::ids::FleetAdmissionPolicyTemplate,
185 pub app: canic_core::ids::AppId,
186 pub canonical_network_id: canic_core::ids::CanonicalNetworkId,
187 pub component_deployment_configuration:
188 canic_core::control_plane_support::config::ComponentDeploymentConfiguration,
189 pub coordinator: String,
190 pub coordinator_subnet: canic_core::ids::SubnetId,
191 pub fleet_id: canic_core::ids::FleetId,
192 pub fresh_estate: bool,
193 pub release_build_id: canic_core::ids::ReleaseBuildId,
194 pub root_funding: Option<canic_core::ids::FleetCoordinatorRootFundingPolicy>,
195 pub roots: Vec<DesiredFleetBootstrapRoot>,
196}
197
198#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
200#[serde(deny_unknown_fields)]
201pub struct DesiredFleetBootstrapRoot {
202 pub canister_pool_imports: Vec<String>,
203 pub component_admissions: Vec<canic_core::ids::ComponentSpecAdmission>,
204 pub component_topology_digest: canic_core::ids::ComponentTopologyDigest,
205 pub funding: canic_core::ids::FleetSubnetRootFundingAuthority,
206 pub limits: canic_core::ids::FleetSubnetRootLimits,
207 pub placement_subnet: canic_core::ids::SubnetId,
208 pub root: String,
209 pub store: String,
210}
211
212#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
214#[serde(deny_unknown_fields)]
215pub struct DesiredFleetProtocol {
216 pub app_config: String,
218 pub component_group_placements: Vec<DesiredComponentGroupPlacement>,
220 pub coordinator_candid: String,
222 pub root_candid: String,
224 pub store_candid: String,
226}
227
228#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(deny_unknown_fields)]
231pub struct DesiredComponentGroupPlacement {
232 pub deployment: String,
233 pub ordinal: u32,
234 pub root: String,
235}
236
237#[derive(Clone, Debug, Default, Eq, PartialEq)]
239pub struct DesiredFleetArtifacts {
240 pub drain_candid_sha256_by_canister: BTreeMap<String, String>,
241 pub init_arg_sha256_by_canister: BTreeMap<String, String>,
242 pub init_candid_sha256_by_canister: BTreeMap<String, String>,
243 pub wasm_sha256_by_canister: BTreeMap<String, String>,
244 pub(crate) protocol_by_step: BTreeMap<String, ProtocolArtifactDigests>,
245}
246
247#[derive(Clone, Debug, Default, Eq, PartialEq)]
249pub(crate) struct ProtocolArtifactDigests {
250 pub candid_sha256: String,
251 pub command_args_sha256: String,
252 pub expected_status_sha256: String,
253 pub status_args_sha256: String,
254}
255
256#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
259pub struct CanisterCyclePolicy {
260 #[serde(with = "u128_text")]
261 pub initial_cycles: u128,
262 #[serde(with = "u128_text")]
263 pub minimum_cycles: u128,
264}
265
266#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
269#[serde(rename_all = "snake_case", tag = "kind")]
270pub enum EnsureAction {
271 Create {
272 controller_canisters: Vec<String>,
273 controllers: Vec<String>,
274 created_at_time: u64,
275 ledger: String,
276 name: String,
277 #[serde(with = "u128_text")]
278 requested_initial_cycles: u128,
279 subnet: String,
280 },
281 Delete {
282 #[serde(with = "u128_text")]
283 maximum_remaining_cycles: u128,
284 name: String,
285 principal: String,
286 },
287 Fund {
288 #[serde(with = "u128_text")]
289 amount: u128,
290 created_at_time: u64,
291 #[serde(default, with = "u128_text", skip_serializing_if = "is_zero_u128")]
292 expected_post_cycles: u128,
293 #[serde(default, with = "u128_text", skip_serializing_if = "is_zero_u128")]
294 funding_deficit_cycles: u128,
295 #[serde(default, with = "u128_text", skip_serializing_if = "is_zero_u128")]
296 funding_margin_cycles: u128,
297 ledger: String,
298 name: String,
299 principal: String,
300 },
301 Install {
302 canic_init: Option<DesiredCanisterInit>,
303 init_arg: Option<String>,
304 init_arg_sha256: Option<String>,
305 init_candid: Option<String>,
306 init_candid_sha256: Option<String>,
307 mode: InstallMode,
308 name: String,
309 principal: String,
310 wasm: String,
311 wasm_sha256: String,
312 },
313 FleetProtocol {
314 action: Box<CurrentFleetProtocolAction>,
315 candid: String,
316 candid_sha256: String,
317 #[serde(with = "u128_text")]
318 maximum_execution_burn_cycles: u128,
319 name: String,
320 principal: String,
321 },
322 Protocol {
323 candid: String,
324 candid_sha256: String,
325 command_args: String,
326 command_args_sha256: String,
327 command_method: String,
328 expected_status: String,
329 expected_status_sha256: String,
330 #[serde(with = "u128_text")]
331 maximum_execution_burn_cycles: u128,
332 name: String,
333 principal: String,
334 status_args: String,
335 status_args_sha256: String,
336 status_method: String,
337 },
338 SetControllers {
339 controller_canisters: Vec<String>,
340 controllers: Vec<String>,
341 name: String,
342 principal: String,
343 },
344 Start {
345 name: String,
346 principal: String,
347 },
348 Stop {
349 name: String,
350 principal: String,
351 },
352 Transfer {
353 #[serde(with = "u128_text")]
354 amount: u128,
355 candid: String,
356 candid_sha256: String,
357 destination: String,
358 #[serde(with = "u128_text")]
359 maximum_execution_burn_cycles: u128,
360 method: String,
361 name: String,
362 principal: String,
363 },
364}
365
366impl EnsureAction {
367 #[must_use]
368 pub fn name(&self) -> &str {
369 match self {
370 Self::Create { name, .. }
371 | Self::Delete { name, .. }
372 | Self::FleetProtocol { name, .. }
373 | Self::Fund { name, .. }
374 | Self::Install { name, .. }
375 | Self::Protocol { name, .. }
376 | Self::SetControllers { name, .. }
377 | Self::Start { name, .. }
378 | Self::Stop { name, .. }
379 | Self::Transfer { name, .. } => name,
380 }
381 }
382}
383
384#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
386#[serde(rename_all = "snake_case", tag = "kind")]
387#[expect(
388 clippy::large_enum_variant,
389 reason = "each exact typed action is boxed by EnsureAction before durable retention"
390)]
391pub enum CurrentFleetProtocolAction {
392 ActivateRegistry {
393 expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
394 expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
395 request: canic_core::dto::fleet_registry::FleetRegistryActivationRequest,
396 },
397 ActivateRegistryMirror {
398 expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistryMirrorActivationResponse,
399 request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
400 },
401 AdoptStore {
402 request: canic_core::dto::fleet_subnet_root::FleetSubnetWasmStoreAdoptionRequest,
403 },
404 BootstrapStore {
405 expected: canic_core::dto::root_store::RootStoreBootstrapResponse,
406 request: canic_core::dto::root_store::RootStoreBootstrapRequest,
407 },
408 JoinRoot {
409 expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
410 expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
411 request: canic_core::dto::fleet_registry::FleetSubnetRootJoinRequest,
412 },
413 PrepareStoreChunkSet {
414 request: canic_control_plane::dto::template::TemplateChunkSetPrepareInput,
415 },
416 PrepareComponentRegistry {
417 expected: canic_core::dto::component_registry::RootComponentRegistryStatusResponse,
418 request: canic_core::dto::component_registry::RootComponentRegistryPreparationRequest,
419 },
420 ProvisionComponents {
421 plan_hash: [u8; 32],
422 request: canic_core::dto::component_provisioning::FleetComponentProvisioningPrepareRequest,
423 },
424 RecoverPoolLedger {
425 request: canic_core::dto::pool::PoolLedgerRecoveryRequest,
426 },
427 PublishStoreChunk {
428 request: canic_control_plane::dto::template::TemplateChunkInput,
429 },
430 StageStoreManifest {
431 request: canic_control_plane::dto::template::TemplateManifestInput,
432 },
433 SynchronizeRegistry {
434 expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncResponse,
435 request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
436 },
437}
438
439impl CurrentFleetProtocolAction {
440 #[must_use]
442 pub const fn target_kind(&self) -> DesiredCanisterKind {
443 match self {
444 Self::ActivateRegistry { .. }
445 | Self::JoinRoot { .. }
446 | Self::ProvisionComponents { .. } => DesiredCanisterKind::Coordinator,
447 Self::ActivateRegistryMirror { .. }
448 | Self::AdoptStore { .. }
449 | Self::BootstrapStore { .. }
450 | Self::PrepareComponentRegistry { .. }
451 | Self::RecoverPoolLedger { .. }
452 | Self::SynchronizeRegistry { .. } => DesiredCanisterKind::Root,
453 Self::PrepareStoreChunkSet { .. }
454 | Self::PublishStoreChunk { .. }
455 | Self::StageStoreManifest { .. } => DesiredCanisterKind::Store,
456 }
457 }
458
459 #[must_use]
461 pub const fn operation_id(&self) -> Option<[u8; 32]> {
462 match self {
463 Self::ActivateRegistry { .. }
464 | Self::JoinRoot { .. }
465 | Self::PrepareComponentRegistry { .. }
466 | Self::PrepareStoreChunkSet { .. }
467 | Self::PublishStoreChunk { .. }
468 | Self::StageStoreManifest { .. } => None,
469 Self::ActivateRegistryMirror { request, .. }
470 | Self::SynchronizeRegistry { request, .. } => Some(request.operation_id),
471 Self::AdoptStore { request } => Some(request.operation_id),
472 Self::BootstrapStore { request, .. } => Some(request.operation_id),
473 Self::ProvisionComponents { request, .. } => Some(request.operation_id),
474 Self::RecoverPoolLedger { request } => Some(request.operation_id),
475 }
476 }
477}
478
479#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(rename_all = "snake_case")]
483pub enum InstallMode {
484 Install,
485 Reinstall,
486}
487
488#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
491pub struct CanisterPlan {
492 pub actions: Vec<EnsureAction>,
493 pub disposition: CanisterDisposition,
494 pub name: String,
495 #[serde(with = "u128_text")]
496 pub observed_cycles: u128,
497 pub principal: Option<String>,
498}
499
500#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
503#[serde(rename_all = "snake_case")]
504pub enum CanisterDisposition {
505 Create,
506 Delete,
507 Replace,
508 Reinstall,
509 Reuse,
510}
511
512#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
515pub struct CycleConservation {
516 #[serde(with = "u128_text")]
517 pub expected_post_operation_cycles: u128,
518 #[serde(with = "u128_text")]
519 pub maximum_execution_burn_cycles: u128,
520 #[serde(with = "u128_text")]
521 pub maximum_new_funding_cycles: u128,
522 #[serde(with = "u128_text")]
523 pub maximum_operator_debit_cycles: u128,
524 #[serde(with = "u128_text")]
525 pub maximum_unavoidable_fee_cycles: u128,
526 #[serde(with = "u128_text")]
527 pub observed_controlled_cycles: u128,
528 #[serde(with = "u128_text")]
529 pub retained_in_reused_canisters_cycles: u128,
530 #[serde(with = "u128_text")]
531 pub scheduled_transfer_cycles: u128,
532}
533
534#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
536pub struct ActualCycleConservation {
537 #[serde(with = "u128_text")]
538 pub exact_unavoidable_fee_cycles: u128,
539 #[serde(with = "u128_text")]
540 pub final_controlled_cycles: u128,
541 #[serde(with = "u128_text")]
542 pub measured_execution_burn_cycles: u128,
543 #[serde(with = "u128_text")]
544 pub observed_starting_cycles: u128,
545 #[serde(with = "u128_text")]
546 pub operator_debit_cycles: u128,
547 #[serde(with = "u128_text")]
548 pub received_new_funding_cycles: u128,
549}
550
551#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct ReviewedDesiredFleetRecord {
558 desired: DesiredFleet,
559}
560
561impl ReviewedDesiredFleetRecord {
562 pub(crate) fn capture(desired: &DesiredFleet) -> Self {
563 Self {
564 desired: desired.clone(),
565 }
566 }
567
568 #[must_use]
570 pub const fn desired(&self) -> &DesiredFleet {
571 &self.desired
572 }
573
574 #[must_use]
576 pub fn into_desired(self) -> DesiredFleet {
577 self.desired
578 }
579}
580
581#[derive(Deserialize, Serialize)]
582struct ReviewedDesiredFleetRecordProjection {
583 desired: DesiredFleet,
584 protocol_steps: Vec<DesiredProtocolStep>,
585}
586
587impl Serialize for ReviewedDesiredFleetRecord {
588 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
589 where
590 S: serde::Serializer,
591 {
592 ReviewedDesiredFleetRecordProjection {
593 desired: self.desired.clone(),
594 protocol_steps: self.desired.protocol_steps.clone(),
595 }
596 .serialize(serializer)
597 }
598}
599
600impl<'de> Deserialize<'de> for ReviewedDesiredFleetRecord {
601 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
602 where
603 D: serde::Deserializer<'de>,
604 {
605 let mut projection = ReviewedDesiredFleetRecordProjection::deserialize(deserializer)?;
606 projection.desired.protocol_steps = projection.protocol_steps;
607 Ok(Self {
608 desired: projection.desired,
609 })
610 }
611}
612
613#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
615pub struct FleetEnsurePlan {
616 pub canisters: Vec<CanisterPlan>,
617 pub conservation: CycleConservation,
618 pub desired_sha256: String,
619 pub environment: String,
620 pub fleet: String,
621 pub operation_id: String,
622 pub plan_sha256: String,
623 pub planned_at_time: u64,
624 pub protocol_actions: Vec<EnsureAction>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub reviewed_desired: Option<Box<ReviewedDesiredFleetRecord>>,
627 pub schema_version: u16,
628}
629
630#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
633#[serde(rename_all = "snake_case")]
634pub enum EffectState {
635 Applied,
636 Intent,
637 Issued,
638}
639
640#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
642#[serde(rename_all = "snake_case")]
643pub enum FleetEnsureCompletion {
644 Converged,
645 InProgress,
646 ReplanRequired,
647}
648
649#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
652pub struct EffectRecord {
653 pub action_sha256: String,
654 pub created_principal: Option<String>,
655 #[serde(with = "option_u128_text")]
656 pub destination_post_cycles: Option<u128>,
657 #[serde(with = "option_u128_text")]
658 pub destination_pre_cycles: Option<u128>,
659 #[serde(with = "option_u128_text")]
660 pub post_cycles: Option<u128>,
661 #[serde(with = "option_u128_text")]
662 pub pre_cycles: Option<u128>,
663 #[serde(default)]
664 pub pre_canister_version: Option<u64>,
665 pub progress_identity: Option<String>,
666 pub receipt: Option<String>,
667 pub state: EffectState,
668}
669
670#[derive(Clone, Copy, Debug, Eq, PartialEq)]
672pub struct RetirementTransferBalances {
673 pub destination_after: u128,
674 pub destination_before: u128,
675 pub maximum_execution_burn: u128,
676 pub source_after: u128,
677 pub source_before: u128,
678 pub transfer_amount: u128,
679}
680
681#[derive(Clone, Copy, Debug, Eq, PartialEq)]
683pub enum RetirementTransferReconciliation {
684 Conserved {
685 destination_credit: u128,
686 source_debit: u128,
687 },
688 Pending,
689}
690
691#[derive(Clone, Copy, Debug, Eq, PartialEq)]
693pub enum RetirementTransferInvariantError {
694 ArithmeticOverflow,
695 BalanceMovedInInvalidDirection,
696 Unbalanced {
697 destination_credit: u128,
698 maximum_source_debit: u128,
699 source_debit: u128,
700 },
701}
702
703pub fn reconcile_retirement_transfer(
705 balances: RetirementTransferBalances,
706) -> Result<RetirementTransferReconciliation, RetirementTransferInvariantError> {
707 let source_debit = balances
708 .source_before
709 .checked_sub(balances.source_after)
710 .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
711 let destination_credit = balances
712 .destination_after
713 .checked_sub(balances.destination_before)
714 .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
715 let maximum_source_debit = balances
716 .transfer_amount
717 .checked_add(balances.maximum_execution_burn)
718 .ok_or(RetirementTransferInvariantError::ArithmeticOverflow)?;
719 if source_debit == 0 && destination_credit == 0 {
720 return Ok(RetirementTransferReconciliation::Pending);
721 }
722 if source_debit >= balances.transfer_amount
723 && source_debit <= maximum_source_debit
724 && destination_credit == balances.transfer_amount
725 {
726 return Ok(RetirementTransferReconciliation::Conserved {
727 destination_credit,
728 source_debit,
729 });
730 }
731 Err(RetirementTransferInvariantError::Unbalanced {
732 destination_credit,
733 maximum_source_debit,
734 source_debit,
735 })
736}
737
738#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
741pub struct FleetEnsureStateRecord {
742 pub active_registry: Option<canic_core::dto::fleet_registry::FleetRegistry>,
743 #[serde(default)]
744 pub completed_reinstalls: BTreeMap<String, u64>,
745 pub fleet: String,
746 pub pending_principals: BTreeMap<String, String>,
747 pub principals: BTreeMap<String, String>,
748 #[serde(default)]
749 pub retained_cycles_by_principal: BTreeMap<String, u128>,
750 pub schema_version: u16,
751 pub topology: BTreeMap<String, FleetEnsureTopologyRecord>,
752}
753
754#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
756pub struct FleetEnsureTopologyRecord {
757 pub kind: DesiredCanisterKind,
758 pub module_hash: Option<String>,
759 pub parent: Option<String>,
760 pub protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
761 pub role: Option<String>,
762}
763
764#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
767pub struct FleetEnsureJournalRecord {
768 pub completion: FleetEnsureCompletion,
769 pub effects: Vec<EffectRecord>,
770 pub fleet: String,
771 #[serde(with = "u128_text")]
772 pub initial_controlled_cycles: u128,
773 #[serde(with = "u128_text")]
774 pub initial_operator_cycles: u128,
775 pub operation_id: String,
776 pub plan_sha256: String,
777 pub schema_version: u16,
778 pub stalled_observations: u32,
779}
780
781#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
784pub struct FleetEnsureReport {
785 pub actual_conservation: Option<ActualCycleConservation>,
786 pub effects_applied: u32,
787 pub plan: FleetEnsurePlan,
788 pub terminal: bool,
789}
790
791const fn is_zero_u128(value: &u128) -> bool {
792 *value == 0
793}
794
795mod u128_text {
796 use serde::{Deserialize, Deserializer, Serializer};
797
798 pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
799 where
800 S: Serializer,
801 {
802 serializer.serialize_str(&value.to_string())
803 }
804
805 pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
806 where
807 D: Deserializer<'de>,
808 {
809 String::deserialize(deserializer)?
810 .parse()
811 .map_err(serde::de::Error::custom)
812 }
813}
814
815mod option_u128_text {
816 use serde::{Deserialize, Deserializer, Serialize, Serializer};
817
818 #[expect(
819 clippy::ref_option,
820 reason = "serde(with) requires the serializer to accept the field by reference"
821 )]
822 pub fn serialize<S>(value: &Option<u128>, serializer: S) -> Result<S::Ok, S::Error>
823 where
824 S: Serializer,
825 {
826 value.map(|cycles| cycles.to_string()).serialize(serializer)
827 }
828
829 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
830 where
831 D: Deserializer<'de>,
832 {
833 Option::<String>::deserialize(deserializer)?
834 .map(|value| value.parse().map_err(serde::de::Error::custom))
835 .transpose()
836 }
837}