1mod serialization;
8
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11
12pub const FLEET_ENSURE_SCHEMA_VERSION: u16 = 1;
13pub const MAX_FLEET_ENSURE_CANISTERS: usize = 4_096;
14pub const MAX_FLEET_ENSURE_PROTOCOL_STEPS: usize = 4_096;
15
16#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
19pub struct LiveCanister {
20 pub canister_version: Option<u64>,
21 pub controllers: Vec<String>,
22 #[serde(with = "u128_text")]
23 pub cycles: u128,
24 pub module_sha256: Option<String>,
25 pub principal: String,
26 pub reinstall_required: bool,
27 pub root_owned_lifecycle: Option<RootOwnedCanisterLifecycle>,
28 pub status: CanisterRuntimeStatus,
29}
30
31#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
34#[serde(rename_all = "snake_case")]
35pub enum RootOwnedCanisterLifecycle {
36 Claimed,
37 Idle,
38 Reconciling,
40 Retained,
42 Store,
43 Workload,
44}
45
46impl RootOwnedCanisterLifecycle {
47 #[must_use]
48 pub const fn label(self) -> &'static str {
49 match self {
50 Self::Claimed => "claimed",
51 Self::Idle => "idle",
52 Self::Reconciling => "reconciling",
53 Self::Retained => "retained",
54 Self::Store => "store",
55 Self::Workload => "workload",
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
63#[serde(rename_all = "snake_case")]
64pub enum CanisterRuntimeStatus {
65 Running,
66 Stopped,
67 Stopping,
68}
69
70#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
73pub struct FleetObservation {
74 pub additional_controlled_cycles: BTreeMap<String, u128>,
75 pub canisters: BTreeMap<String, Option<LiveCanister>>,
76 pub estate_funding_domains: BTreeMap<String, EstateFundingDomainObservation>,
77 #[serde(with = "u128_text")]
78 pub ledger_fee_cycles: u128,
79 #[serde(with = "u128_text")]
80 pub operator_cycles: u128,
81 pub protocol_ready: BTreeMap<String, bool>,
82}
83
84#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
86#[serde(deny_unknown_fields)]
87pub struct EstateFundingDomainObservation {
88 #[serde(with = "option_u128_text")]
89 pub balance_cycles: Option<u128>,
90 pub cycles_ledger: String,
91 pub pool: Option<EstatePoolInventoryObservation>,
92 pub root_principal: Option<String>,
93}
94
95#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
97#[serde(deny_unknown_fields)]
98pub struct EstatePoolInventoryObservation {
99 pub assets: Vec<EstatePoolAssetObservation>,
100 pub maximum_size: u32,
101 pub minimum_size: u32,
102 pub pending_creation: Option<EstatePoolPendingCreationObservation>,
103 #[serde(with = "u128_text")]
104 pub readiness_floor_cycles: u128,
105 #[serde(with = "u128_text")]
106 pub creation_execution_margin_cycles: u128,
107}
108
109#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
111#[serde(deny_unknown_fields)]
112pub struct EstatePoolAssetObservation {
113 pub creation_receipt: Option<EstatePoolCreationReceiptObservation>,
114 #[serde(with = "u128_text")]
115 pub cycles: u128,
116 pub lifecycle: EstatePoolAssetLifecycle,
117 pub origin: EstatePoolAssetOrigin,
118 pub principal: String,
119}
120
121#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
123#[serde(deny_unknown_fields)]
124pub struct EstatePoolCreationReceiptObservation {
125 pub block_index: u64,
126 pub operation_id: String,
127 pub cycles_ledger: String,
128 #[serde(with = "u128_text")]
129 pub ledger_amount_cycles: u128,
130 #[serde(with = "u128_text")]
131 pub ledger_fee_cycles: u128,
132 #[serde(with = "u128_text")]
133 pub readiness_floor_cycles: u128,
134 #[serde(with = "u128_text")]
135 pub creation_execution_margin_cycles: u128,
136 #[serde(with = "u128_text")]
137 pub management_creation_fee_cycles: u128,
138 #[serde(with = "option_u128_text")]
139 pub first_observed_cycles: Option<u128>,
140}
141
142#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
144#[serde(rename_all = "snake_case")]
145pub enum EstatePoolAssetOrigin {
146 InfrastructureStore,
147 Created,
148 Imported,
149 Recycled,
150}
151
152#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
154#[serde(deny_unknown_fields)]
155pub struct PoolFundingAuthority {
156 pub root: String,
157 pub lifecycle: EstatePoolAssetLifecycle,
158}
159
160#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
162#[serde(rename_all = "snake_case")]
163pub enum EstatePoolAssetLifecycle {
164 Claimed,
165 Failed,
166 HandingOff,
167 PendingReset,
168 Ready,
169 Recycling,
170 Workload,
171}
172
173#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
175#[serde(deny_unknown_fields)]
176pub struct EstatePoolPendingCreationObservation {
177 pub attempt_count: u32,
178 #[serde(with = "option_u128_text")]
179 pub available_cycles: Option<u128>,
180 #[serde(with = "u128_text")]
181 pub creation_amount_cycles: u128,
182 pub created_principal: Option<String>,
183 pub diagnostic: Option<EstatePoolCreationDiagnostic>,
184 pub last_attempt_at_ns: Option<u64>,
185 pub operation_id: String,
186 #[serde(with = "option_u128_text")]
187 pub required_cycles: Option<u128>,
188 pub retry_at_ns: Option<u64>,
189 #[serde(with = "option_u128_text")]
190 pub shortfall_cycles: Option<u128>,
191 pub uncertain_result: bool,
192}
193
194#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
196#[serde(rename_all = "snake_case")]
197pub enum EstatePoolCreationDiagnostic {
198 FundingRequired,
199 LedgerCreationFailed,
200 LedgerRejected,
201 UnresolvedAfterLedgerWindow,
202}
203
204#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
207#[serde(rename_all = "snake_case")]
208pub enum DesiredPresence {
209 Absent,
210 Present,
211}
212
213#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
216#[serde(deny_unknown_fields)]
217pub struct DrainAuthority {
218 pub candid: String,
219 pub method: String,
220 pub destination: String,
221 pub maximum_execution_burn_cycles: String,
222}
223
224#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
227#[serde(deny_unknown_fields)]
228pub struct DesiredCanister {
229 #[serde(default)]
230 pub canic_init: Option<DesiredCanisterInit>,
231 #[serde(default)]
232 pub controller_canisters: Vec<String>,
233 pub controllers: Vec<String>,
234 pub drain: Option<DrainAuthority>,
235 pub initial_cycles: String,
236 pub init_arg: Option<String>,
237 pub init_candid: Option<String>,
238 pub kind: DesiredCanisterKind,
239 pub minimum_cycles: String,
240 pub name: String,
241 pub parent: Option<String>,
242 pub presence: DesiredPresence,
243 pub principal: Option<String>,
244 #[serde(skip)]
245 pub(crate) protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
246 pub replace: bool,
247 pub subnet: String,
248 pub wasm: Option<String>,
249}
250
251#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
253#[serde(rename_all = "snake_case")]
254pub enum DesiredCanisterKind {
255 Auxiliary,
256 Component,
257 Coordinator,
258 Pool,
259 Root,
260 Store,
261}
262
263#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
265#[serde(rename_all = "snake_case", tag = "role")]
266pub enum DesiredCanisterInit {
267 Coordinator,
268 Root { root: String },
269 Store { root: String },
270}
271
272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
279#[serde(deny_unknown_fields)]
280pub(crate) struct DesiredProtocolStep {
281 pub canister: String,
282 pub candid: String,
283 pub command_args: String,
284 pub command_method: String,
285 pub expected_status: String,
286 pub maximum_execution_burn_cycles: String,
287 pub name: String,
288 pub status_args: String,
289 pub status_method: String,
290}
291
292#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
295#[serde(deny_unknown_fields)]
296pub struct DesiredFleet {
297 #[serde(default)]
298 pub bootstrap: Option<DesiredFleetBootstrap>,
299 pub canisters: Vec<DesiredCanister>,
300 pub cycles_ledger: String,
301 pub environment: String,
302 pub fleet: String,
303 pub ledger_fee_cycles: String,
304 pub management_creation_fee_cycles: String,
305 pub material_cycle_threshold: String,
306 pub maximum_observation_burn_cycles: String,
307 pub maximum_stalled_observations: u32,
309 pub maximum_update_burn_cycles: String,
310 pub operator: String,
311 #[serde(default)]
312 pub protocol: Option<DesiredFleetProtocol>,
313 #[serde(skip)]
314 pub(crate) protocol_steps: Vec<DesiredProtocolStep>,
315 pub schema_version: u16,
316 pub treasury: String,
318}
319
320#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
322#[serde(deny_unknown_fields)]
323pub struct DesiredFleetBootstrap {
324 #[serde(default)]
326 pub admission_identity_origin: Option<String>,
327 pub admission: canic_core::ids::FleetAdmissionPolicyTemplate,
328 pub app: canic_core::ids::AppId,
329 pub canonical_network_id: canic_core::ids::CanonicalNetworkId,
330 pub component_deployment_configuration:
331 canic_core::control_plane_support::config::ComponentDeploymentConfiguration,
332 pub coordinator: String,
333 pub coordinator_subnet: canic_core::ids::SubnetId,
334 pub fleet_id: canic_core::ids::FleetId,
335 pub fresh_estate: bool,
336 pub release_build_id: canic_core::ids::ReleaseBuildId,
337 pub root_funding: Option<canic_core::ids::FleetCoordinatorRootFundingPolicy>,
338 pub roots: Vec<DesiredFleetBootstrapRoot>,
339}
340
341#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
343#[serde(deny_unknown_fields)]
344pub struct DesiredFleetBootstrapRoot {
345 pub canister_pool_imports: Vec<String>,
346 pub component_admissions: Vec<canic_core::ids::ComponentSpecAdmission>,
347 pub component_topology_digest: canic_core::ids::ComponentTopologyDigest,
348 pub funding: canic_core::ids::FleetSubnetRootFundingAuthority,
349 pub limits: canic_core::ids::FleetSubnetRootLimits,
350 pub placement_subnet: canic_core::ids::SubnetId,
351 pub root: String,
352 pub store: String,
353}
354
355#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
357#[serde(deny_unknown_fields)]
358pub struct DesiredFleetProtocol {
359 pub app_config: String,
361 pub component_group_placements: Vec<DesiredComponentGroupPlacement>,
363 pub coordinator_candid: String,
365 pub root_candid: String,
367 pub store_candid: String,
369}
370
371#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
373#[serde(deny_unknown_fields)]
374pub struct DesiredComponentGroupPlacement {
375 pub deployment: String,
376 pub ordinal: u32,
377 pub root: String,
378}
379
380#[derive(Clone, Debug, Default, Eq, PartialEq)]
382pub struct DesiredFleetArtifacts {
383 pub continuation: Option<FleetEnsureContinuationAuthority>,
384 pub drain_candid_sha256_by_canister: BTreeMap<String, String>,
385 pub init_arg_sha256_by_canister: BTreeMap<String, String>,
386 pub init_candid_sha256_by_canister: BTreeMap<String, String>,
387 pub wasm_sha256_by_canister: BTreeMap<String, String>,
388 pub startup_funding_by_root: BTreeMap<String, StartupFundingRequirement>,
389 pub(crate) protocol_by_step: BTreeMap<String, ProtocolArtifactDigests>,
390}
391
392#[derive(Clone, Debug, Eq, PartialEq)]
394pub struct StartupFundingRequirement {
395 pub maximum_continuation_steps: u32,
397 pub minimum_native_cycles: u128,
398 pub unfunded_role: Option<StartupRoleShortfall>,
399}
400
401#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
403#[serde(deny_unknown_fields)]
404pub struct StartupRoleShortfall {
405 pub role: canic_core::ids::CanisterRole,
406 #[serde(with = "u128_text")]
407 pub cycles: u128,
408}
409
410#[derive(Clone, Debug, Default, Eq, PartialEq)]
412pub(crate) struct ProtocolArtifactDigests {
413 pub candid_sha256: String,
414 pub command_args_sha256: String,
415 pub expected_status_sha256: String,
416 pub status_args_sha256: String,
417}
418
419#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
422pub struct CanisterCyclePolicy {
423 #[serde(with = "u128_text")]
424 pub initial_cycles: u128,
425 #[serde(with = "u128_text")]
426 pub minimum_cycles: u128,
427}
428
429#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
431#[serde(deny_unknown_fields)]
432pub struct ReinstallHistoryWitness {
433 pub prior_module_sha256: String,
435 pub authority: RootManagementBinding,
436 pub candid: String,
437 pub candid_sha256: String,
438 #[serde(deserialize_with = "serialization::required_option")]
440 pub replacement: Option<Box<ReinstallRootWitnessRecord>>,
441}
442
443#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
445#[serde(deny_unknown_fields)]
446pub struct ReinstallRootWitnessRecord {
447 pub authority: RootManagementBinding,
448 pub candid: String,
449 pub candid_sha256: String,
450}
451
452#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
454#[serde(rename_all = "snake_case", tag = "kind")]
455pub enum EnsureAction {
456 SealAuthority {
458 candid: String,
459 candid_sha256: String,
460 authority_kind: DesiredCanisterKind,
461 name: String,
462 principal: String,
463 },
464 Create {
465 controller_canisters: Vec<String>,
466 controllers: Vec<String>,
467 created_at_time: u64,
468 ledger: String,
469 name: String,
470 #[serde(with = "u128_text")]
471 requested_initial_cycles: u128,
472 subnet: String,
473 },
474 Delete {
475 #[serde(with = "u128_text")]
476 maximum_remaining_cycles: u128,
477 name: String,
478 principal: String,
479 },
480 Fund {
481 #[serde(deserialize_with = "serialization::required_option")]
483 pool_funding: Option<PoolFundingAuthority>,
484 #[serde(with = "u128_text")]
485 amount: u128,
486 created_at_time: u64,
487 #[serde(with = "u128_text")]
488 expected_post_cycles: u128,
489 #[serde(with = "u128_text")]
490 funding_deficit_cycles: u128,
491 #[serde(with = "u128_text")]
492 funding_margin_cycles: u128,
493 ledger: String,
494 name: String,
495 principal: String,
496 },
497 FundEstate {
498 #[serde(with = "u128_text")]
499 amount: u128,
500 created_at_time: u64,
501 #[serde(with = "u128_text")]
502 expected_post_cycles: u128,
503 ledger: String,
504 #[serde(with = "u128_text")]
505 ledger_fee_cycles: u128,
506 name: String,
507 principal: String,
508 },
509 Install {
510 canic_init: Option<DesiredCanisterInit>,
511 #[serde(deserialize_with = "serialization::required_option")]
512 reinstall_witness: Option<Box<ReinstallHistoryWitness>>,
513 init_arg: Option<String>,
514 init_arg_sha256: Option<String>,
515 init_candid: Option<String>,
516 init_candid_sha256: Option<String>,
517 mode: InstallMode,
518 name: String,
519 principal: String,
520 wasm: String,
521 wasm_sha256: String,
522 },
523 FleetProtocol {
524 action: Box<CurrentFleetProtocolAction>,
525 candid: String,
526 candid_sha256: String,
527 #[serde(with = "u128_text")]
528 maximum_execution_burn_cycles: u128,
529 name: String,
530 principal: String,
531 },
532 Protocol {
533 candid: String,
534 candid_sha256: String,
535 command_args: String,
536 command_args_sha256: String,
537 command_method: String,
538 expected_status: String,
539 expected_status_sha256: String,
540 #[serde(with = "u128_text")]
541 maximum_execution_burn_cycles: u128,
542 name: String,
543 principal: String,
544 status_args: String,
545 status_args_sha256: String,
546 status_method: String,
547 },
548 SetControllers {
549 controller_canisters: Vec<String>,
550 controllers: Vec<String>,
551 name: String,
552 principal: String,
553 },
554 Start {
555 name: String,
556 principal: String,
557 },
558 Stop {
559 name: String,
560 principal: String,
561 },
562 Transfer {
563 #[serde(with = "u128_text")]
564 amount: u128,
565 candid: String,
566 candid_sha256: String,
567 destination: String,
568 #[serde(with = "u128_text")]
569 maximum_execution_burn_cycles: u128,
570 method: String,
571 name: String,
572 principal: String,
573 },
574}
575
576impl EnsureAction {
577 pub(crate) fn fixture_publication_attempt_limit(&self) -> Option<u32> {
579 if let Self::FleetProtocol { action, .. } = self {
580 match action.as_ref() {
581 CurrentFleetProtocolAction::PrepareStoreFixture {
582 maximum_attempts, ..
583 }
584 | CurrentFleetProtocolAction::PublishStoreFixtureChunk {
585 maximum_attempts, ..
586 } => {
587 return Some(*maximum_attempts);
588 }
589 _ => {}
590 }
591 }
592 None
593 }
594
595 #[must_use]
596 pub fn name(&self) -> &str {
597 match self {
598 Self::SealAuthority { name, .. }
599 | Self::Create { name, .. }
600 | Self::Delete { name, .. }
601 | Self::FleetProtocol { name, .. }
602 | Self::Fund { name, .. }
603 | Self::FundEstate { name, .. }
604 | Self::Install { name, .. }
605 | Self::Protocol { name, .. }
606 | Self::SetControllers { name, .. }
607 | Self::Start { name, .. }
608 | Self::Stop { name, .. }
609 | Self::Transfer { name, .. } => name,
610 }
611 }
612}
613
614#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
616#[serde(rename_all = "snake_case", tag = "kind")]
617#[expect(
618 clippy::large_enum_variant,
619 reason = "each exact typed action is boxed by EnsureAction before durable retention"
620)]
621pub enum CurrentFleetProtocolAction {
622 ObservePoolReadiness {
624 minimum_ready: u32,
625 readiness_floor: canic_core::cdk::types::Cycles,
626 },
627 MaintainPoolReadiness {
628 maximum_updates: u32,
629 minimum_ready: u32,
630 readiness_floor: canic_core::cdk::types::Cycles,
631 },
632 ReconcilePoolAsset {
633 request: canic_core::dto::pool::PoolCanisterRequest,
634 minimum_cycles: canic_core::cdk::types::Cycles,
635 },
636 ActivateRegistry {
637 expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
638 expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
639 request: canic_core::dto::fleet_registry::FleetRegistryActivationRequest,
640 },
641 ActivateRegistryMirror {
642 expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistryMirrorActivationResponse,
643 request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
644 },
645 AdoptStore {
646 request: canic_core::dto::fleet_subnet_root::FleetSubnetWasmStoreAdoptionRequest,
647 },
648 BootstrapStore {
649 expected: canic_core::dto::root_store::RootStoreBootstrapResponse,
650 request: canic_core::dto::root_store::RootStoreBootstrapRequest,
651 },
652 JoinRoot {
653 expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
654 expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
655 request: canic_core::dto::fleet_registry::FleetSubnetRootJoinRequest,
656 },
657 PrepareStoreFixture {
658 maximum_attempts: u32,
660 request: canic_core::dto::root_store::RootStoreFixturePrepareRequest,
661 source: canic_core::dto::root_store::RootStoreFixture,
662 store: candid::Principal,
663 },
664 PublishStoreFixtureChunk {
665 maximum_attempts: u32,
667 request: canic_core::dto::fixture_provisioning::FixtureChunkUpload,
668 expected: canic_core::dto::fixture_provisioning::FixtureSourceStatus,
669 source_bytes: u64,
670 },
671 PrepareStoreChunkSet {
672 request: canic_control_plane::dto::template::TemplateChunkSetPrepareInput,
673 },
674 PrepareComponentRegistry {
675 expected: canic_core::dto::component_registry::RootComponentRegistryStatusResponse,
676 request: canic_core::dto::component_registry::RootComponentRegistryPreparationRequest,
677 },
678 ProvisionComponents {
679 plan_hash: [u8; 32],
680 request: canic_core::dto::component_provisioning::FleetComponentProvisioningPrepareRequest,
681 },
682 PublishStoreChunk {
683 request: canic_control_plane::dto::template::TemplateChunkInput,
684 },
685 StageStoreManifest {
686 request: canic_control_plane::dto::template::TemplateManifestInput,
687 },
688 SynchronizeRegistry {
689 expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncResponse,
690 request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
691 },
692}
693
694impl CurrentFleetProtocolAction {
695 pub(crate) fn publication_bytes(&self) -> Option<&[u8]> {
697 match self {
698 Self::PublishStoreChunk { request } => Some(&request.bytes),
699 Self::PublishStoreFixtureChunk { request, .. } => Some(&request.bytes),
700 _ => None,
701 }
702 }
703
704 pub(crate) const fn publication_bytes_mut(&mut self) -> Option<&mut Vec<u8>> {
705 match self {
706 Self::PublishStoreChunk { request } => Some(&mut request.bytes),
707 Self::PublishStoreFixtureChunk { request, .. } => Some(&mut request.bytes),
708 _ => None,
709 }
710 }
711
712 #[must_use]
714 pub const fn target_kind(&self) -> DesiredCanisterKind {
715 match self {
716 Self::ActivateRegistry { .. }
717 | Self::JoinRoot { .. }
718 | Self::ProvisionComponents { .. } => DesiredCanisterKind::Coordinator,
719 Self::ActivateRegistryMirror { .. }
720 | Self::ReconcilePoolAsset { .. }
721 | Self::MaintainPoolReadiness { .. }
722 | Self::ObservePoolReadiness { .. }
723 | Self::AdoptStore { .. }
724 | Self::BootstrapStore { .. }
725 | Self::PrepareStoreFixture { .. }
726 | Self::PrepareComponentRegistry { .. }
727 | Self::SynchronizeRegistry { .. } => DesiredCanisterKind::Root,
728 Self::PrepareStoreChunkSet { .. }
729 | Self::PublishStoreChunk { .. }
730 | Self::PublishStoreFixtureChunk { .. }
731 | Self::StageStoreManifest { .. } => DesiredCanisterKind::Store,
732 }
733 }
734
735 #[must_use]
737 pub const fn operation_id(&self) -> Option<[u8; 32]> {
738 match self {
739 Self::ActivateRegistry { .. }
740 | Self::ReconcilePoolAsset { .. }
741 | Self::MaintainPoolReadiness { .. }
742 | Self::ObservePoolReadiness { .. }
743 | Self::JoinRoot { .. }
744 | Self::PrepareComponentRegistry { .. }
745 | Self::PrepareStoreFixture { .. }
746 | Self::PrepareStoreChunkSet { .. }
747 | Self::PublishStoreChunk { .. }
748 | Self::PublishStoreFixtureChunk { .. }
749 | Self::StageStoreManifest { .. } => None,
750 Self::ActivateRegistryMirror { request, .. }
751 | Self::SynchronizeRegistry { request, .. } => Some(request.operation_id),
752 Self::AdoptStore { request } => Some(request.operation_id),
753 Self::BootstrapStore { request, .. } => Some(request.operation_id),
754 Self::ProvisionComponents { request, .. } => Some(request.operation_id),
755 }
756 }
757}
758
759#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
762#[serde(rename_all = "snake_case")]
763pub enum InstallMode {
764 Install,
765 Reinstall,
766}
767
768#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
771#[serde(deny_unknown_fields)]
772pub struct CanisterPlan {
773 pub actions: Vec<EnsureAction>,
774 pub disposition: CanisterDisposition,
775 pub name: String,
776 #[serde(with = "u128_text")]
777 pub observed_cycles: u128,
778 #[serde(deserialize_with = "serialization::required_option")]
779 pub principal: Option<String>,
780}
781
782#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
785#[serde(rename_all = "snake_case")]
786pub enum CanisterDisposition {
787 Create,
788 Delete,
789 Replace,
790 Reinstall,
791 Reuse,
792}
793
794#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
797pub struct CycleConservation {
798 pub estate_funding_domains: Vec<EstateFundingDomainPlan>,
799 #[serde(with = "u128_text")]
800 pub expected_post_operation_cycles: u128,
801 #[serde(with = "u128_text")]
802 pub maximum_execution_burn_cycles: u128,
803 #[serde(with = "u128_text")]
804 pub maximum_new_funding_cycles: u128,
805 #[serde(with = "u128_text")]
806 pub maximum_operator_debit_cycles: u128,
807 #[serde(with = "u128_text")]
808 pub maximum_unavoidable_fee_cycles: u128,
809 #[serde(with = "u128_text")]
810 pub observed_controlled_cycles: u128,
811 #[serde(with = "u128_text")]
812 pub retained_in_reused_canisters_cycles: u128,
813 #[serde(with = "u128_text")]
814 pub scheduled_transfer_cycles: u128,
815}
816
817#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
819#[serde(deny_unknown_fields)]
820pub struct EstateFundingDomainPlan {
821 pub allocated_workloads: u32,
822 #[serde(with = "option_u128_text")]
823 pub available_cycles: Option<u128>,
824 pub available_pool_slots: u32,
825 #[serde(with = "u128_text")]
826 pub creation_amount_cycles: u128,
827 pub cycles_ledger: String,
828 #[serde(with = "u128_text")]
829 pub creation_execution_margin_cycles: u128,
830 #[serde(with = "u128_text")]
831 pub readiness_floor_cycles: u128,
832 pub eligible_ready_pool_assets: u32,
833 pub initial_pool_assets: Vec<String>,
834 #[serde(with = "u128_text")]
835 pub ledger_fee_cycles: u128,
836 #[serde(with = "u128_text")]
837 pub management_creation_fee_cycles: u128,
838 #[serde(with = "u128_text")]
839 pub maximum_creation_debit_cycles: u128,
840 #[serde(with = "u128_text")]
841 pub maximum_creation_fee_cycles: u128,
842 #[serde(with = "u128_text")]
843 pub maximum_funding_cycles: u128,
844 pub occupied_pool_assets: u32,
845 pub pending_creation_count: u32,
846 pub pending_creation: Option<EstatePoolPendingCreationObservation>,
847 pub pool_maximum_size: u32,
848 pub planned_initial_workloads: u32,
849 pub required_creation_count: u32,
850 pub root: String,
851 pub root_principal: Option<String>,
852 #[serde(with = "u128_text")]
853 pub shortfall_cycles: u128,
854}
855
856#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
858#[serde(deny_unknown_fields)]
859pub struct EstateFundingRequiredRecord {
860 pub attempt_count: Option<u32>,
861 #[serde(with = "u128_text")]
862 pub available_cycles: u128,
863 #[serde(with = "u128_text")]
864 pub creation_amount_cycles: u128,
865 pub cycles_ledger: String,
866 #[serde(with = "u128_text")]
867 pub creation_execution_margin_cycles: u128,
868 #[serde(with = "u128_text")]
869 pub readiness_floor_cycles: u128,
870 #[serde(with = "u128_text")]
871 pub ledger_fee_cycles: u128,
872 #[serde(with = "u128_text")]
873 pub management_creation_fee_cycles: u128,
874 #[serde(with = "u128_text")]
875 pub maximum_creation_debit_cycles: u128,
876 #[serde(with = "u128_text")]
877 pub maximum_creation_fee_cycles: u128,
878 pub operation_id: String,
879 pub pending_creation_operation_id: Option<String>,
880 pub plan_sha256: String,
881 pub required_creation_count: u32,
882 pub root: String,
883 pub root_principal: String,
884 pub last_attempt_at_ns: Option<u64>,
885 pub retry_at_ns: Option<u64>,
886 #[serde(with = "u128_text")]
887 pub shortfall_cycles: u128,
888}
889
890#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
892pub struct ActualCycleConservation {
893 #[serde(with = "u128_text")]
894 pub estate_funding_cycles: u128,
895 #[serde(with = "u128_text")]
896 pub exact_estate_creation_fee_cycles: u128,
897 #[serde(with = "u128_text")]
898 pub exact_unavoidable_fee_cycles: u128,
899 #[serde(with = "u128_text")]
900 pub final_controlled_cycles: u128,
901 #[serde(with = "u128_text")]
902 pub measured_execution_burn_cycles: u128,
903 #[serde(with = "u128_text")]
904 pub observed_starting_cycles: u128,
905 #[serde(with = "u128_text")]
908 pub observed_settlement_credit_cycles: u128,
909 #[serde(with = "u128_text")]
910 pub operator_debit_cycles: u128,
911 #[serde(with = "u128_text")]
912 pub received_new_funding_cycles: u128,
913}
914
915pub(crate) const fn creation_observation_burn(observation: u128, update: u128) -> Option<u128> {
918 observation.checked_add(update)
919}
920
921pub(crate) const fn create_balance_is_terminal(
925 actual_cycles: Option<u128>,
926 requested_initial_cycles: u128,
927 maximum_observation_burn_cycles: u128,
928) -> bool {
929 let Some(actual_cycles) = actual_cycles else {
930 return false;
931 };
932 if actual_cycles > requested_initial_cycles {
933 return false;
934 }
935 requested_initial_cycles - actual_cycles <= maximum_observation_burn_cycles
936}
937
938#[derive(Clone, Debug, Eq, PartialEq)]
944pub struct ReviewedDesiredFleetRecord {
945 desired: DesiredFleet,
946}
947
948impl ReviewedDesiredFleetRecord {
949 pub(crate) fn capture(desired: &DesiredFleet) -> Self {
950 Self {
951 desired: desired.clone(),
952 }
953 }
954
955 #[must_use]
957 pub const fn desired(&self) -> &DesiredFleet {
958 &self.desired
959 }
960
961 #[must_use]
963 pub fn into_desired(self) -> DesiredFleet {
964 self.desired
965 }
966}
967
968#[derive(Deserialize, Serialize)]
969struct ReviewedDesiredFleetRecordProjection {
970 desired: DesiredFleet,
971 protocol_steps: Vec<DesiredProtocolStep>,
972}
973
974impl Serialize for ReviewedDesiredFleetRecord {
975 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
976 where
977 S: serde::Serializer,
978 {
979 ReviewedDesiredFleetRecordProjection {
980 desired: self.desired.clone(),
981 protocol_steps: self.desired.protocol_steps.clone(),
982 }
983 .serialize(serializer)
984 }
985}
986
987impl<'de> Deserialize<'de> for ReviewedDesiredFleetRecord {
988 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
989 where
990 D: serde::Deserializer<'de>,
991 {
992 let mut projection = ReviewedDesiredFleetRecordProjection::deserialize(deserializer)?;
993 projection.desired.protocol_steps = projection.protocol_steps;
994 Ok(Self {
995 desired: projection.desired,
996 })
997 }
998}
999
1000#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1002#[serde(deny_unknown_fields)]
1003pub struct FleetEnsurePlan {
1004 #[serde(deserialize_with = "serialization::required_option")]
1006 pub recovery_review: Option<Box<FleetRecoveryReview>>,
1007 #[serde(deserialize_with = "serialization::required_option")]
1009 pub reinstall: Option<Box<FleetReinstallRecord>>,
1010 #[serde(deserialize_with = "serialization::required_option")]
1011 pub continuation: Option<FleetEnsureContinuationAuthority>,
1012 pub canisters: Vec<CanisterPlan>,
1013 pub conservation: CycleConservation,
1014 pub desired_sha256: String,
1015 pub environment: String,
1016 pub fleet: String,
1017 pub operation_id: String,
1018 pub plan_sha256: String,
1019 pub planned_at_time: u64,
1020 pub protocol_actions: Vec<EnsureAction>,
1021 #[serde(deserialize_with = "serialization::required_option")]
1022 pub root_start_authority: Option<Box<RetainedRootStartAuthorityRecord>>,
1023 pub root_reinstall_bindings: Vec<RootManagementBinding>,
1025 #[serde(deserialize_with = "serialization::required_option")]
1026 pub reviewed_desired: Option<Box<ReviewedDesiredFleetRecord>>,
1027 pub schema_version: u16,
1028 pub scope: FleetEnsurePlanScope,
1029 #[serde(deserialize_with = "serialization::required_option")]
1031 pub terminal_inventory_operation_id: Option<String>,
1032}
1033
1034#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
1036#[serde(rename_all = "snake_case")]
1037pub enum FleetEnsurePlanScope {
1038 #[default]
1040 Full,
1041 ReinstallPreparation,
1043 RootReinstallPrerequisite,
1045 RootStartPrerequisite,
1047}
1048
1049impl FleetEnsurePlanScope {
1050 #[must_use]
1052 pub const fn as_str(self) -> &'static str {
1053 match self {
1054 Self::Full => "full",
1055 Self::ReinstallPreparation => "reinstall_preparation",
1056 Self::RootReinstallPrerequisite => "root_reinstall_prerequisite",
1057 Self::RootStartPrerequisite => "root_start_prerequisite",
1058 }
1059 }
1060}
1061
1062#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1064#[serde(deny_unknown_fields)]
1065pub struct FleetReinstallRecord {
1066 #[serde(deserialize_with = "serialization::required_option")]
1068 pub target_artifacts_sha256: Option<String>,
1069 #[serde(deserialize_with = "serialization::required_option")]
1071 pub source: Option<Box<FleetReinstallSourceRecord>>,
1072 #[serde(deserialize_with = "serialization::required_option")]
1073 pub activation_reset: Option<Box<FleetActivationResetRecord>>,
1074 pub operation_id: String,
1075 pub source_operation_id: String,
1076 pub authorities: Vec<RootManagementBinding>,
1077 pub assets: Vec<FleetReinstallAssetRecord>,
1079}
1080
1081#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1083#[serde(deny_unknown_fields)]
1084pub struct FleetReinstallSourceRecord {
1085 pub reviewed_desired: ReviewedDesiredFleetRecord,
1086 pub wasm_sha256_by_canister: BTreeMap<String, String>,
1087 pub candid_sha256_by_path: BTreeMap<String, String>,
1088}
1089
1090#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1094#[serde(deny_unknown_fields)]
1095pub struct FleetActivationSourceRecord {
1096 pub infrastructure: Vec<RootManagementBinding>,
1097 pub operator: String,
1098 pub cycles_ledger: String,
1099 #[serde(with = "u128_text")]
1100 pub initial_controlled_cycles: u128,
1101 #[serde(with = "u128_text")]
1102 pub maximum_execution_burn_cycles: u128,
1103 #[serde(with = "u128_text_map")]
1104 pub initial_estate_funding_cycles_by_root: BTreeMap<String, u128>,
1105 pub operation_id: String,
1106 pub plan_sha256: String,
1107 pub plan_document_sha256: String,
1108 pub journal_document_sha256: String,
1109 pub state_document_sha256: String,
1110 pub provisioning: EnsureAction,
1111 pub registry_preparations: Vec<EnsureAction>,
1112 pub stores: Vec<FleetProtocolReadRecord>,
1113}
1114
1115#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1117#[serde(deny_unknown_fields)]
1118pub struct FleetActivationResetRecord {
1119 #[serde(deserialize_with = "serialization::required_option")]
1120 pub preparation: Option<ActivationPreparationEvidenceRecord>,
1121 pub source: FleetActivationSourceRecord,
1122 pub roots: Vec<RootActivationResetRecord>,
1123}
1124
1125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1127#[serde(deny_unknown_fields)]
1128pub struct ActivationPreparationEvidenceRecord {
1129 pub plan_document_sha256: String,
1130 pub journal_document_sha256: String,
1131}
1132
1133#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1135#[serde(deny_unknown_fields)]
1136pub struct ActivationResetAdoptionRecord {
1137 pub source_plan_sha256: String,
1138 pub source_journal_sha256: String,
1139 pub source_state_sha256: String,
1140 pub replacement_plan_sha256: String,
1141 pub replacement_journal_sha256: String,
1142 pub complete: bool,
1143}
1144
1145#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1148#[serde(deny_unknown_fields)]
1149pub struct FleetProtocolReadRecord {
1150 pub candid: String,
1151 pub candid_sha256: String,
1152 pub principal: String,
1153}
1154
1155#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1158#[serde(deny_unknown_fields)]
1159pub struct RootActivationResetRecord {
1160 pub root: String,
1161 pub activation_operation_id: [u8; 32],
1162 pub inventory_hash: [u8; 32],
1163 pub provisioning_receipt_hash: [u8; 32],
1164 pub component_count: u32,
1165 pub managed_descendants: u32,
1166}
1167
1168#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1170#[serde(deny_unknown_fields)]
1171pub struct FleetReinstallAssetRecord {
1172 pub controllers: Vec<String>,
1173 #[serde(deserialize_with = "serialization::required_option")]
1174 pub module_sha256: Option<String>,
1175 pub principal: String,
1176 pub root: String,
1177 pub subnet: String,
1178}
1179
1180#[derive(Clone, Debug, Eq, PartialEq)]
1182pub struct RootManagementCanisterObservation {
1183 pub live: LiveCanister,
1184 pub name: String,
1185 pub subnet: String,
1186}
1187
1188#[derive(Clone, Debug, Eq, PartialEq)]
1190pub struct RootManagementObservation {
1191 pub operator_cycles: u128,
1192 pub roots: BTreeMap<String, RootManagementCanisterObservation>,
1193}
1194
1195#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1197#[serde(deny_unknown_fields)]
1198pub struct RootManagementBinding {
1199 pub controllers: Vec<String>,
1200 pub name: String,
1201 pub module_sha256: String,
1202 pub principal: String,
1203 pub subnet: String,
1204}
1205
1206#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1211#[serde(deny_unknown_fields)]
1212pub struct RetainedRootStartAuthorityRecord {
1213 pub authority_sha256: String,
1214 pub environment: String,
1215 pub fleet: String,
1216 pub fleet_id: canic_core::ids::FleetId,
1217 pub roots: Vec<RootManagementBinding>,
1218 pub schema_version: u16,
1219}
1220
1221impl RetainedRootStartAuthorityRecord {
1222 pub(crate) fn seal(&mut self) {
1223 self.authority_sha256 = self.expected_sha256();
1224 }
1225
1226 pub(crate) fn has_valid_digest(&self) -> bool {
1227 self.authority_sha256 == self.expected_sha256()
1228 }
1229
1230 fn expected_sha256(&self) -> String {
1231 let mut canonical = self.clone();
1232 canonical.authority_sha256.clear();
1233 let bytes = serde_json::to_vec(&canonical)
1234 .expect("retained Root-start authority is JSON serializable");
1235 let mut framed = b"canic:fleet-ensure:root-start-authority:v1".to_vec();
1236 framed.extend_from_slice(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
1237 framed.extend_from_slice(&bytes);
1238 canic_core::cdk::utils::hash::sha256_hex(&framed)
1239 }
1240}
1241
1242#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1245#[serde(rename_all = "snake_case")]
1246pub enum EffectState {
1247 Applied,
1248 Intent,
1249 Issued,
1250}
1251
1252#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1254#[serde(rename_all = "snake_case")]
1255pub enum FleetEnsureCompletion {
1256 Converged,
1257 Prepared,
1259 InProgress,
1260 ReplanRequired,
1261}
1262
1263#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1266#[serde(deny_unknown_fields)]
1267pub struct EffectRecord {
1268 pub maintenance_attempts: u32,
1270 pub publication_attempts: u32,
1272 pub action_sha256: String,
1273 #[serde(deserialize_with = "serialization::required_option")]
1274 pub created_principal: Option<String>,
1275 #[serde(with = "option_u128_text")]
1276 pub destination_post_cycles: Option<u128>,
1277 #[serde(with = "option_u128_text")]
1278 pub destination_pre_cycles: Option<u128>,
1279 #[serde(with = "option_u128_text")]
1280 pub post_cycles: Option<u128>,
1281 #[serde(with = "option_u128_text")]
1282 pub pre_cycles: Option<u128>,
1283 #[serde(deserialize_with = "serialization::required_option")]
1284 pub pre_canister_version: Option<u64>,
1285 #[serde(deserialize_with = "serialization::required_option")]
1286 pub progress_identity: Option<String>,
1287 #[serde(deserialize_with = "serialization::required_option")]
1288 pub receipt: Option<String>,
1289 pub state: EffectState,
1290}
1291
1292#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1294pub struct RetirementTransferBalances {
1295 pub destination_after: u128,
1296 pub destination_before: u128,
1297 pub maximum_execution_burn: u128,
1298 pub source_after: u128,
1299 pub source_before: u128,
1300 pub transfer_amount: u128,
1301}
1302
1303#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1305pub enum RetirementTransferReconciliation {
1306 Conserved {
1307 destination_credit: u128,
1308 source_debit: u128,
1309 },
1310 Pending,
1311}
1312
1313#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1315pub enum RetirementTransferInvariantError {
1316 ArithmeticOverflow,
1317 BalanceMovedInInvalidDirection,
1318 Unbalanced {
1319 destination_credit: u128,
1320 maximum_source_debit: u128,
1321 source_debit: u128,
1322 },
1323}
1324
1325pub fn reconcile_retirement_transfer(
1327 balances: RetirementTransferBalances,
1328) -> Result<RetirementTransferReconciliation, RetirementTransferInvariantError> {
1329 let source_debit = balances
1330 .source_before
1331 .checked_sub(balances.source_after)
1332 .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
1333 let destination_credit = balances
1334 .destination_after
1335 .checked_sub(balances.destination_before)
1336 .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
1337 let maximum_source_debit = balances
1338 .transfer_amount
1339 .checked_add(balances.maximum_execution_burn)
1340 .ok_or(RetirementTransferInvariantError::ArithmeticOverflow)?;
1341 if source_debit == 0 && destination_credit == 0 {
1342 return Ok(RetirementTransferReconciliation::Pending);
1343 }
1344 if source_debit >= balances.transfer_amount
1345 && source_debit <= maximum_source_debit
1346 && destination_credit == balances.transfer_amount
1347 {
1348 return Ok(RetirementTransferReconciliation::Conserved {
1349 destination_credit,
1350 source_debit,
1351 });
1352 }
1353 Err(RetirementTransferInvariantError::Unbalanced {
1354 destination_credit,
1355 maximum_source_debit,
1356 source_debit,
1357 })
1358}
1359
1360#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1363#[serde(deny_unknown_fields)]
1364pub struct FleetEnsureStateRecord {
1365 #[serde(deserialize_with = "serialization::required_option")]
1366 pub active_registry: Option<canic_core::dto::fleet_registry::FleetRegistry>,
1367 pub completed_reinstall_action_sha256: BTreeMap<String, String>,
1369 #[serde(deserialize_with = "serialization::required_option")]
1371 pub completed_reinstall_operation_id: Option<String>,
1372 pub completed_reinstalls: BTreeMap<String, u64>,
1375 pub fleet: String,
1376 pub pending_principals: BTreeMap<String, String>,
1377 pub principals: BTreeMap<String, String>,
1378 pub retained_cycles_by_principal: BTreeMap<String, u128>,
1379 pub schema_version: u16,
1380 pub topology: BTreeMap<String, FleetEnsureTopologyRecord>,
1381}
1382
1383#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1385#[serde(deny_unknown_fields)]
1386pub struct FleetEnsureTopologyRecord {
1387 pub kind: DesiredCanisterKind,
1388 #[serde(deserialize_with = "serialization::required_option")]
1389 pub module_hash: Option<String>,
1390 #[serde(deserialize_with = "serialization::required_option")]
1391 pub parent: Option<String>,
1392 #[serde(deserialize_with = "serialization::required_option")]
1393 pub protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
1394 #[serde(deserialize_with = "serialization::required_option")]
1395 pub role: Option<String>,
1396}
1397
1398#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1401#[serde(deny_unknown_fields)]
1402pub struct FleetEnsureJournalRecord {
1403 pub funding_reviews: Vec<FundingReviewRecord>,
1404 pub successor_phases: Vec<FleetEnsureSuccessorPhaseRecord>,
1405 pub completion: FleetEnsureCompletion,
1406 #[serde(deserialize_with = "serialization::required_option")]
1407 pub estate_funding_required: Option<EstateFundingRequiredRecord>,
1408 pub effects: Vec<EffectRecord>,
1409 pub fleet: String,
1410 #[serde(with = "u128_text")]
1411 pub initial_controlled_cycles: u128,
1412 #[serde(with = "u128_text_map")]
1413 pub initial_estate_funding_cycles_by_root: BTreeMap<String, u128>,
1414 #[serde(with = "u128_text")]
1415 pub initial_operator_cycles: u128,
1416 pub operation_id: String,
1417 pub plan_sha256: String,
1418 pub schema_version: u16,
1419 pub stalled_observations: u32,
1420}
1421
1422#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1424#[serde(deny_unknown_fields)]
1425pub struct FleetEnsureContinuationAuthority {
1426 pub fixture_publication_retry_attempts: u32,
1428 pub app_config_sha256: String,
1429 pub application_artifact_union_sha256: String,
1430 pub coordinator_candid_sha256: String,
1431 pub maximum_successor_actions: u32,
1432 pub root_candid_sha256: String,
1433 pub store_candid_sha256: String,
1434}
1435
1436#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1438#[serde(deny_unknown_fields)]
1439pub struct FleetEnsureSuccessorPhaseRecord {
1440 #[serde(with = "u128_text")]
1441 pub execution_burn_before_phase: u128,
1442 pub plan_sha256: String,
1443 #[serde(skip)]
1445 pub plan: Option<Box<FleetEnsurePlan>>,
1446}
1447
1448#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1450#[serde(rename_all = "snake_case")]
1451pub enum FleetEnsureSuccessorReviewReason {
1452 AdditionalEffect,
1453 BudgetExceeded,
1454 ChangedInputs,
1455 PhaseBound,
1456 ProtocolAuthority,
1457}
1458
1459#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1462pub struct FleetEnsureReport {
1463 pub funding_review: Option<FundingReviewRecord>,
1464 pub actual_conservation: Option<ActualCycleConservation>,
1465 pub effects_applied: u32,
1466 pub plan: FleetEnsurePlan,
1467 pub terminal: bool,
1468}
1469
1470#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1473#[serde(deny_unknown_fields)]
1474pub struct FundingReviewRecord {
1475 pub action: EnsureAction,
1476 #[serde(deserialize_with = "serialization::required_option")]
1477 pub effect: Option<EffectRecord>,
1478 pub pause: FundingPauseRecord,
1479 pub review_sha256: String,
1480}
1481
1482#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1484#[serde(
1485 tag = "kind",
1486 content = "evidence",
1487 rename_all = "snake_case",
1488 deny_unknown_fields
1489)]
1490pub enum FundingPauseRecord {
1491 Estate(EstateFundingRequiredRecord),
1492 Native(NativeFundingRequiredRecord),
1493}
1494
1495impl FundingPauseRecord {
1496 #[must_use]
1498 pub fn root(&self) -> &str {
1499 match self {
1500 Self::Estate(p) => &p.root,
1501 Self::Native(p) => &p.root,
1502 }
1503 }
1504
1505 #[must_use]
1507 pub fn root_principal(&self) -> &str {
1508 match self {
1509 Self::Estate(p) => &p.root_principal,
1510 Self::Native(p) => &p.root_principal,
1511 }
1512 }
1513
1514 #[must_use]
1516 pub fn cycles_ledger(&self) -> &str {
1517 match self {
1518 Self::Estate(p) => &p.cycles_ledger,
1519 Self::Native(p) => &p.cycles_ledger,
1520 }
1521 }
1522
1523 #[must_use]
1525 pub const fn available_cycles(&self) -> u128 {
1526 match self {
1527 Self::Estate(p) => p.available_cycles,
1528 Self::Native(p) => p.available_cycles,
1529 }
1530 }
1531
1532 #[must_use]
1534 pub const fn shortfall_cycles(&self) -> u128 {
1535 match self {
1536 Self::Estate(p) => p.shortfall_cycles,
1537 Self::Native(p) => p.shortfall_cycles,
1538 }
1539 }
1540
1541 #[must_use]
1543 pub const fn ledger_fee_cycles(&self) -> u128 {
1544 match self {
1545 Self::Estate(p) => p.ledger_fee_cycles,
1546 Self::Native(p) => p.ledger_fee_cycles,
1547 }
1548 }
1549}
1550
1551#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1553#[serde(deny_unknown_fields)]
1554pub struct NativeFundingRequiredRecord {
1555 #[serde(with = "u128_text")]
1556 pub available_cycles: u128,
1557 pub cycles_ledger: String,
1558 #[serde(with = "u128_text")]
1559 pub funding_margin_cycles: u128,
1560 #[serde(with = "u128_text")]
1561 pub ledger_fee_cycles: u128,
1562 #[serde(with = "u128_text")]
1563 pub minimum_cycles: u128,
1564 pub operation_id: String,
1565 pub plan_sha256: String,
1566 pub provisioning_action_sha256: String,
1567 pub root: String,
1568 pub root_principal: String,
1569 #[serde(with = "u128_text")]
1570 pub shortfall_cycles: u128,
1571}
1572
1573#[derive(Clone, Debug, Eq, PartialEq)]
1575pub struct NativeFundingObservation {
1576 pub cycles_ledger: String,
1577 pub ledger_fee_cycles: u128,
1578 pub live: LiveCanister,
1579 pub operator_cycles: u128,
1580}
1581
1582mod u128_text {
1583 use serde::{Deserialize, Deserializer, Serializer};
1584
1585 pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
1586 where
1587 S: Serializer,
1588 {
1589 serializer.serialize_str(&value.to_string())
1590 }
1591
1592 pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
1593 where
1594 D: Deserializer<'de>,
1595 {
1596 String::deserialize(deserializer)?
1597 .parse()
1598 .map_err(serde::de::Error::custom)
1599 }
1600}
1601
1602mod option_u128_text {
1603 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1604
1605 #[expect(
1606 clippy::ref_option,
1607 reason = "serde(with) requires the serializer to accept the field by reference"
1608 )]
1609 pub fn serialize<S>(value: &Option<u128>, serializer: S) -> Result<S::Ok, S::Error>
1610 where
1611 S: Serializer,
1612 {
1613 value.map(|cycles| cycles.to_string()).serialize(serializer)
1614 }
1615
1616 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
1617 where
1618 D: Deserializer<'de>,
1619 {
1620 Option::<String>::deserialize(deserializer)?
1621 .map(|value| value.parse().map_err(serde::de::Error::custom))
1622 .transpose()
1623 }
1624}
1625
1626mod u128_text_map {
1627 use std::collections::BTreeMap;
1628
1629 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1630
1631 pub fn serialize<S>(values: &BTreeMap<String, u128>, serializer: S) -> Result<S::Ok, S::Error>
1632 where
1633 S: Serializer,
1634 {
1635 values
1636 .iter()
1637 .map(|(key, value)| (key, value.to_string()))
1638 .collect::<BTreeMap<_, _>>()
1639 .serialize(serializer)
1640 }
1641
1642 pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<String, u128>, D::Error>
1643 where
1644 D: Deserializer<'de>,
1645 {
1646 BTreeMap::<String, String>::deserialize(deserializer)?
1647 .into_iter()
1648 .map(|(key, value)| {
1649 value
1650 .parse()
1651 .map(|value| (key, value))
1652 .map_err(serde::de::Error::custom)
1653 })
1654 .collect()
1655 }
1656}
1657
1658#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1660#[serde(deny_unknown_fields)]
1661pub struct FleetRecoveryReview {
1662 #[serde(with = "u128_text")]
1663 pub base_execution_burn_cycles: u128,
1664 #[serde(with = "u128_text")]
1665 pub continuation_reserve_cycles: u128,
1666 #[serde(with = "u128_text")]
1667 pub whole_continuation_ceiling_cycles: u128,
1668 pub maximum_successor_actions: u32,
1669 pub fixture_publication_retry_attempts: u32,
1670 #[serde(with = "u128_text")]
1671 pub per_step_burn_cycles: u128,
1672 pub startup_funding: Vec<RootStartupFundingForecast>,
1673 pub known_pool_funding: Vec<PoolRecoveryFunding>,
1674 pub discovery: RecoveryDiscovery,
1676}
1677
1678#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1682#[serde(deny_unknown_fields)]
1683pub struct RootStartupFundingForecast {
1684 pub root: String,
1685 #[serde(with = "u128_text")]
1686 pub startup_minimum_cycles: u128,
1687 pub maximum_continuation_steps: u32,
1688 #[serde(with = "u128_text")]
1689 pub continuation_allowance_cycles: u128,
1690 #[serde(with = "u128_text")]
1691 pub configured_minimum_cycles: u128,
1692 #[serde(with = "u128_text")]
1693 pub required_native_cycles: u128,
1694 pub reuse_assumption: StartupFundingReuseAssumption,
1695 #[serde(deserialize_with = "serialization::required_option")]
1696 pub unfunded_role: Option<StartupRoleShortfall>,
1697}
1698
1699#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1701#[serde(rename_all = "snake_case")]
1702pub enum StartupFundingReuseAssumption {
1703 FreshChildrenAndFullPublication,
1704}
1705
1706#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1708#[serde(rename_all = "snake_case")]
1709pub enum RecoveryDiscovery {
1710 PendingCurrentProtocol,
1711}
1712
1713#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1715#[serde(deny_unknown_fields)]
1716pub struct PoolRecoveryFunding {
1717 pub principal: String,
1718 pub root: String,
1719 #[serde(with = "u128_text")]
1720 pub amount_cycles: u128,
1721 #[serde(with = "u128_text")]
1722 pub ledger_fee_cycles: u128,
1723 #[serde(with = "u128_text")]
1724 pub funding_deficit_cycles: u128,
1725 #[serde(with = "u128_text")]
1726 pub funding_margin_cycles: u128,
1727 #[serde(with = "u128_text")]
1728 pub expected_post_cycles: u128,
1729}
1730
1731#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1733pub struct FleetSuccessorReview {
1734 pub actions: Vec<FleetReviewAction>,
1735 #[serde(with = "u128_text")]
1736 pub maximum_additional_debit_cycles: u128,
1737 pub next_review_command: String,
1738}
1739
1740#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1742pub struct FleetReviewAction {
1743 pub name: String,
1744 pub principal: Option<String>,
1745 pub kind: String,
1746}
1747
1748impl std::fmt::Display for FleetSuccessorReview {
1749 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1750 write!(f, "remaining actions: ")?;
1751 for (index, action) in self.actions.iter().enumerate() {
1752 if index > 0 {
1753 write!(f, ", ")?;
1754 }
1755 write!(
1756 f,
1757 "{} {} ({})",
1758 action.kind,
1759 action.name,
1760 action.principal.as_deref().unwrap_or("not yet created")
1761 )?;
1762 }
1763 write!(
1764 f,
1765 "; maximum additional debit including fees: {} cycles; next review: {}",
1766 self.maximum_additional_debit_cycles, self.next_review_command
1767 )
1768 }
1769}