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 Retained,
40 Store,
41 Workload,
42}
43
44impl RootOwnedCanisterLifecycle {
45 #[must_use]
46 pub const fn label(self) -> &'static str {
47 match self {
48 Self::Claimed => "claimed",
49 Self::Idle => "idle",
50 Self::Retained => "retained",
51 Self::Store => "store",
52 Self::Workload => "workload",
53 }
54 }
55}
56
57#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
60#[serde(rename_all = "snake_case")]
61pub enum CanisterRuntimeStatus {
62 Running,
63 Stopped,
64 Stopping,
65}
66
67#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
70pub struct FleetObservation {
71 pub additional_controlled_cycles: BTreeMap<String, u128>,
72 pub canisters: BTreeMap<String, Option<LiveCanister>>,
73 pub estate_funding_domains: BTreeMap<String, EstateFundingDomainObservation>,
74 #[serde(with = "u128_text")]
75 pub ledger_fee_cycles: u128,
76 #[serde(with = "u128_text")]
77 pub operator_cycles: u128,
78 pub protocol_ready: BTreeMap<String, bool>,
79}
80
81#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83#[serde(deny_unknown_fields)]
84pub struct EstateFundingDomainObservation {
85 #[serde(with = "option_u128_text")]
86 pub balance_cycles: Option<u128>,
87 pub cycles_ledger: String,
88 pub pool: Option<EstatePoolInventoryObservation>,
89 pub root_principal: Option<String>,
90}
91
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
94#[serde(deny_unknown_fields)]
95pub struct EstatePoolInventoryObservation {
96 pub assets: Vec<EstatePoolAssetObservation>,
97 pub maximum_size: u32,
98 pub minimum_size: u32,
99 pub pending_creation: Option<EstatePoolPendingCreationObservation>,
100 #[serde(with = "u128_text")]
101 pub readiness_floor_cycles: u128,
102 #[serde(with = "u128_text")]
103 pub creation_execution_margin_cycles: u128,
104}
105
106#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
108#[serde(deny_unknown_fields)]
109pub struct EstatePoolAssetObservation {
110 pub creation_receipt: Option<EstatePoolCreationReceiptObservation>,
111 #[serde(with = "u128_text")]
112 pub cycles: u128,
113 pub lifecycle: EstatePoolAssetLifecycle,
114 pub origin: EstatePoolAssetOrigin,
115 pub principal: String,
116}
117
118#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
120#[serde(deny_unknown_fields)]
121pub struct EstatePoolCreationReceiptObservation {
122 pub block_index: u64,
123 pub operation_id: String,
124 pub cycles_ledger: String,
125 #[serde(with = "u128_text")]
126 pub ledger_amount_cycles: u128,
127 #[serde(with = "u128_text")]
128 pub ledger_fee_cycles: u128,
129 #[serde(with = "u128_text")]
130 pub readiness_floor_cycles: u128,
131 #[serde(with = "u128_text")]
132 pub creation_execution_margin_cycles: u128,
133 #[serde(with = "u128_text")]
134 pub management_creation_fee_cycles: u128,
135 #[serde(with = "option_u128_text")]
136 pub first_observed_cycles: Option<u128>,
137}
138
139#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub enum EstatePoolAssetOrigin {
143 InfrastructureStore,
144 Created,
145 Imported,
146 Recycled,
147}
148
149#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151#[serde(deny_unknown_fields)]
152pub struct PoolFundingAuthority {
153 pub root: String,
154 pub lifecycle: EstatePoolAssetLifecycle,
155}
156
157#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
159#[serde(rename_all = "snake_case")]
160pub enum EstatePoolAssetLifecycle {
161 Claimed,
162 Failed,
163 HandingOff,
164 PendingReset,
165 Ready,
166 Recycling,
167 Workload,
168}
169
170#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172#[serde(deny_unknown_fields)]
173pub struct EstatePoolPendingCreationObservation {
174 pub attempt_count: u32,
175 #[serde(with = "option_u128_text")]
176 pub available_cycles: Option<u128>,
177 #[serde(with = "u128_text")]
178 pub creation_amount_cycles: u128,
179 pub created_principal: Option<String>,
180 pub diagnostic: Option<EstatePoolCreationDiagnostic>,
181 pub last_attempt_at_ns: Option<u64>,
182 pub operation_id: String,
183 #[serde(with = "option_u128_text")]
184 pub required_cycles: Option<u128>,
185 pub retry_at_ns: Option<u64>,
186 #[serde(with = "option_u128_text")]
187 pub shortfall_cycles: Option<u128>,
188 pub uncertain_result: bool,
189}
190
191#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
193#[serde(rename_all = "snake_case")]
194pub enum EstatePoolCreationDiagnostic {
195 FundingRequired,
196 LedgerCreationFailed,
197 LedgerRejected,
198 UnresolvedAfterLedgerWindow,
199}
200
201#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
204#[serde(rename_all = "snake_case")]
205pub enum DesiredPresence {
206 Absent,
207 Present,
208}
209
210#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
213#[serde(deny_unknown_fields)]
214pub struct DrainAuthority {
215 pub candid: String,
216 pub method: String,
217 pub destination: String,
218 pub maximum_execution_burn_cycles: String,
219}
220
221#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
224#[serde(deny_unknown_fields)]
225pub struct DesiredCanister {
226 #[serde(default)]
227 pub canic_init: Option<DesiredCanisterInit>,
228 #[serde(default)]
229 pub controller_canisters: Vec<String>,
230 pub controllers: Vec<String>,
231 pub drain: Option<DrainAuthority>,
232 pub initial_cycles: String,
233 pub init_arg: Option<String>,
234 pub init_candid: Option<String>,
235 pub kind: DesiredCanisterKind,
236 pub minimum_cycles: String,
237 pub name: String,
238 pub parent: Option<String>,
239 pub presence: DesiredPresence,
240 pub principal: Option<String>,
241 #[serde(skip)]
242 pub(crate) protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
243 pub replace: bool,
244 pub subnet: String,
245 pub wasm: Option<String>,
246}
247
248#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
250#[serde(rename_all = "snake_case")]
251pub enum DesiredCanisterKind {
252 Auxiliary,
253 Component,
254 Coordinator,
255 Pool,
256 Root,
257 Store,
258}
259
260#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
262#[serde(rename_all = "snake_case", tag = "role")]
263pub enum DesiredCanisterInit {
264 Coordinator,
265 Root { root: String },
266 Store { root: String },
267}
268
269#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
276#[serde(deny_unknown_fields)]
277pub(crate) struct DesiredProtocolStep {
278 pub canister: String,
279 pub candid: String,
280 pub command_args: String,
281 pub command_method: String,
282 pub expected_status: String,
283 pub maximum_execution_burn_cycles: String,
284 pub name: String,
285 pub status_args: String,
286 pub status_method: String,
287}
288
289#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
292#[serde(deny_unknown_fields)]
293pub struct DesiredFleet {
294 #[serde(default)]
295 pub bootstrap: Option<DesiredFleetBootstrap>,
296 pub canisters: Vec<DesiredCanister>,
297 pub cycles_ledger: String,
298 pub environment: String,
299 pub fleet: String,
300 pub ledger_fee_cycles: String,
301 pub management_creation_fee_cycles: String,
302 pub material_cycle_threshold: String,
303 pub maximum_observation_burn_cycles: String,
304 pub maximum_stalled_observations: u32,
305 pub maximum_update_burn_cycles: String,
306 pub operator: String,
307 #[serde(default)]
308 pub protocol: Option<DesiredFleetProtocol>,
309 #[serde(skip)]
310 pub(crate) protocol_steps: Vec<DesiredProtocolStep>,
311 pub schema_version: u16,
312 pub treasury: String,
314}
315
316#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
318#[serde(deny_unknown_fields)]
319pub struct DesiredFleetBootstrap {
320 pub admission: canic_core::ids::FleetAdmissionPolicyTemplate,
321 pub app: canic_core::ids::AppId,
322 pub canonical_network_id: canic_core::ids::CanonicalNetworkId,
323 pub component_deployment_configuration:
324 canic_core::control_plane_support::config::ComponentDeploymentConfiguration,
325 pub coordinator: String,
326 pub coordinator_subnet: canic_core::ids::SubnetId,
327 pub fleet_id: canic_core::ids::FleetId,
328 pub fresh_estate: bool,
329 pub release_build_id: canic_core::ids::ReleaseBuildId,
330 pub root_funding: Option<canic_core::ids::FleetCoordinatorRootFundingPolicy>,
331 pub roots: Vec<DesiredFleetBootstrapRoot>,
332}
333
334#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
336#[serde(deny_unknown_fields)]
337pub struct DesiredFleetBootstrapRoot {
338 pub canister_pool_imports: Vec<String>,
339 pub component_admissions: Vec<canic_core::ids::ComponentSpecAdmission>,
340 pub component_topology_digest: canic_core::ids::ComponentTopologyDigest,
341 pub funding: canic_core::ids::FleetSubnetRootFundingAuthority,
342 pub limits: canic_core::ids::FleetSubnetRootLimits,
343 pub placement_subnet: canic_core::ids::SubnetId,
344 pub root: String,
345 pub store: String,
346}
347
348#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
350#[serde(deny_unknown_fields)]
351pub struct DesiredFleetProtocol {
352 pub app_config: String,
354 pub component_group_placements: Vec<DesiredComponentGroupPlacement>,
356 pub coordinator_candid: String,
358 pub root_candid: String,
360 pub store_candid: String,
362}
363
364#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
366#[serde(deny_unknown_fields)]
367pub struct DesiredComponentGroupPlacement {
368 pub deployment: String,
369 pub ordinal: u32,
370 pub root: String,
371}
372
373#[derive(Clone, Debug, Default, Eq, PartialEq)]
375pub struct DesiredFleetArtifacts {
376 pub continuation: Option<FleetEnsureContinuationAuthority>,
377 pub drain_candid_sha256_by_canister: BTreeMap<String, String>,
378 pub init_arg_sha256_by_canister: BTreeMap<String, String>,
379 pub init_candid_sha256_by_canister: BTreeMap<String, String>,
380 pub wasm_sha256_by_canister: BTreeMap<String, String>,
381 pub(crate) protocol_by_step: BTreeMap<String, ProtocolArtifactDigests>,
382}
383
384#[derive(Clone, Debug, Default, Eq, PartialEq)]
386pub(crate) struct ProtocolArtifactDigests {
387 pub candid_sha256: String,
388 pub command_args_sha256: String,
389 pub expected_status_sha256: String,
390 pub status_args_sha256: String,
391}
392
393#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
396pub struct CanisterCyclePolicy {
397 #[serde(with = "u128_text")]
398 pub initial_cycles: u128,
399 #[serde(with = "u128_text")]
400 pub minimum_cycles: u128,
401}
402
403#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
405#[serde(deny_unknown_fields)]
406pub struct ReinstallHistoryWitness {
407 pub prior_module_sha256: String,
409 pub authority: RootManagementBinding,
410 pub candid: String,
411 pub candid_sha256: String,
412}
413
414#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
416#[serde(rename_all = "snake_case", tag = "kind")]
417pub enum EnsureAction {
418 SealAuthority {
420 candid: String,
421 candid_sha256: String,
422 authority_kind: DesiredCanisterKind,
423 name: String,
424 principal: String,
425 },
426 Create {
427 controller_canisters: Vec<String>,
428 controllers: Vec<String>,
429 created_at_time: u64,
430 ledger: String,
431 name: String,
432 #[serde(with = "u128_text")]
433 requested_initial_cycles: u128,
434 subnet: String,
435 },
436 Delete {
437 #[serde(with = "u128_text")]
438 maximum_remaining_cycles: u128,
439 name: String,
440 principal: String,
441 },
442 Fund {
443 #[serde(deserialize_with = "serialization::required_option")]
445 pool_funding: Option<PoolFundingAuthority>,
446 #[serde(with = "u128_text")]
447 amount: u128,
448 created_at_time: u64,
449 #[serde(with = "u128_text")]
450 expected_post_cycles: u128,
451 #[serde(with = "u128_text")]
452 funding_deficit_cycles: u128,
453 #[serde(with = "u128_text")]
454 funding_margin_cycles: u128,
455 ledger: String,
456 name: String,
457 principal: String,
458 },
459 FundEstate {
460 #[serde(with = "u128_text")]
461 amount: u128,
462 created_at_time: u64,
463 #[serde(with = "u128_text")]
464 expected_post_cycles: u128,
465 ledger: String,
466 #[serde(with = "u128_text")]
467 ledger_fee_cycles: u128,
468 name: String,
469 principal: String,
470 },
471 Install {
472 canic_init: Option<DesiredCanisterInit>,
473 #[serde(deserialize_with = "serialization::required_option")]
474 reinstall_witness: Option<Box<ReinstallHistoryWitness>>,
475 init_arg: Option<String>,
476 init_arg_sha256: Option<String>,
477 init_candid: Option<String>,
478 init_candid_sha256: Option<String>,
479 mode: InstallMode,
480 name: String,
481 principal: String,
482 wasm: String,
483 wasm_sha256: String,
484 },
485 FleetProtocol {
486 action: Box<CurrentFleetProtocolAction>,
487 candid: String,
488 candid_sha256: String,
489 #[serde(with = "u128_text")]
490 maximum_execution_burn_cycles: u128,
491 name: String,
492 principal: String,
493 },
494 Protocol {
495 candid: String,
496 candid_sha256: String,
497 command_args: String,
498 command_args_sha256: String,
499 command_method: String,
500 expected_status: String,
501 expected_status_sha256: String,
502 #[serde(with = "u128_text")]
503 maximum_execution_burn_cycles: u128,
504 name: String,
505 principal: String,
506 status_args: String,
507 status_args_sha256: String,
508 status_method: String,
509 },
510 SetControllers {
511 controller_canisters: Vec<String>,
512 controllers: Vec<String>,
513 name: String,
514 principal: String,
515 },
516 Start {
517 name: String,
518 principal: String,
519 },
520 Stop {
521 name: String,
522 principal: String,
523 },
524 Transfer {
525 #[serde(with = "u128_text")]
526 amount: u128,
527 candid: String,
528 candid_sha256: String,
529 destination: String,
530 #[serde(with = "u128_text")]
531 maximum_execution_burn_cycles: u128,
532 method: String,
533 name: String,
534 principal: String,
535 },
536}
537
538impl EnsureAction {
539 #[must_use]
540 pub fn name(&self) -> &str {
541 match self {
542 Self::SealAuthority { name, .. }
543 | Self::Create { name, .. }
544 | Self::Delete { name, .. }
545 | Self::FleetProtocol { name, .. }
546 | Self::Fund { name, .. }
547 | Self::FundEstate { name, .. }
548 | Self::Install { name, .. }
549 | Self::Protocol { name, .. }
550 | Self::SetControllers { name, .. }
551 | Self::Start { name, .. }
552 | Self::Stop { name, .. }
553 | Self::Transfer { name, .. } => name,
554 }
555 }
556}
557
558#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
560#[serde(rename_all = "snake_case", tag = "kind")]
561#[expect(
562 clippy::large_enum_variant,
563 reason = "each exact typed action is boxed by EnsureAction before durable retention"
564)]
565pub enum CurrentFleetProtocolAction {
566 ObservePoolReadiness {
568 minimum_ready: u32,
569 readiness_floor: canic_core::cdk::types::Cycles,
570 },
571 MaintainPoolReadiness {
572 maximum_updates: u32,
573 minimum_ready: u32,
574 readiness_floor: canic_core::cdk::types::Cycles,
575 },
576 ReconcilePoolAsset {
577 request: canic_core::dto::pool::PoolCanisterRequest,
578 minimum_cycles: canic_core::cdk::types::Cycles,
579 },
580 ActivateRegistry {
581 expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
582 expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
583 request: canic_core::dto::fleet_registry::FleetRegistryActivationRequest,
584 },
585 ActivateRegistryMirror {
586 expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistryMirrorActivationResponse,
587 request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
588 },
589 AdoptStore {
590 request: canic_core::dto::fleet_subnet_root::FleetSubnetWasmStoreAdoptionRequest,
591 },
592 BootstrapStore {
593 expected: canic_core::dto::root_store::RootStoreBootstrapResponse,
594 request: canic_core::dto::root_store::RootStoreBootstrapRequest,
595 },
596 JoinRoot {
597 expected_registry: canic_core::dto::fleet_registry::FleetRegistry,
598 expected_version: canic_core::dto::fleet_registry::FleetRegistryVersion,
599 request: canic_core::dto::fleet_registry::FleetSubnetRootJoinRequest,
600 },
601 PrepareStoreChunkSet {
602 request: canic_control_plane::dto::template::TemplateChunkSetPrepareInput,
603 },
604 PrepareComponentRegistry {
605 expected: canic_core::dto::component_registry::RootComponentRegistryStatusResponse,
606 request: canic_core::dto::component_registry::RootComponentRegistryPreparationRequest,
607 },
608 ProvisionComponents {
609 plan_hash: [u8; 32],
610 request: canic_core::dto::component_provisioning::FleetComponentProvisioningPrepareRequest,
611 },
612 PublishStoreChunk {
613 request: canic_control_plane::dto::template::TemplateChunkInput,
614 },
615 StageStoreManifest {
616 request: canic_control_plane::dto::template::TemplateManifestInput,
617 },
618 SynchronizeRegistry {
619 expected: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncResponse,
620 request: canic_core::dto::fleet_registry::FleetSubnetRootRegistrySyncRequest,
621 },
622}
623
624impl CurrentFleetProtocolAction {
625 #[must_use]
627 pub const fn target_kind(&self) -> DesiredCanisterKind {
628 match self {
629 Self::ActivateRegistry { .. }
630 | Self::JoinRoot { .. }
631 | Self::ProvisionComponents { .. } => DesiredCanisterKind::Coordinator,
632 Self::ActivateRegistryMirror { .. }
633 | Self::ReconcilePoolAsset { .. }
634 | Self::MaintainPoolReadiness { .. }
635 | Self::ObservePoolReadiness { .. }
636 | Self::AdoptStore { .. }
637 | Self::BootstrapStore { .. }
638 | Self::PrepareComponentRegistry { .. }
639 | Self::SynchronizeRegistry { .. } => DesiredCanisterKind::Root,
640 Self::PrepareStoreChunkSet { .. }
641 | Self::PublishStoreChunk { .. }
642 | Self::StageStoreManifest { .. } => DesiredCanisterKind::Store,
643 }
644 }
645
646 #[must_use]
648 pub const fn operation_id(&self) -> Option<[u8; 32]> {
649 match self {
650 Self::ActivateRegistry { .. }
651 | Self::ReconcilePoolAsset { .. }
652 | Self::MaintainPoolReadiness { .. }
653 | Self::ObservePoolReadiness { .. }
654 | Self::JoinRoot { .. }
655 | Self::PrepareComponentRegistry { .. }
656 | Self::PrepareStoreChunkSet { .. }
657 | Self::PublishStoreChunk { .. }
658 | Self::StageStoreManifest { .. } => None,
659 Self::ActivateRegistryMirror { request, .. }
660 | Self::SynchronizeRegistry { request, .. } => Some(request.operation_id),
661 Self::AdoptStore { request } => Some(request.operation_id),
662 Self::BootstrapStore { request, .. } => Some(request.operation_id),
663 Self::ProvisionComponents { request, .. } => Some(request.operation_id),
664 }
665 }
666}
667
668#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
671#[serde(rename_all = "snake_case")]
672pub enum InstallMode {
673 Install,
674 Reinstall,
675}
676
677#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
680#[serde(deny_unknown_fields)]
681pub struct CanisterPlan {
682 pub actions: Vec<EnsureAction>,
683 pub disposition: CanisterDisposition,
684 pub name: String,
685 #[serde(with = "u128_text")]
686 pub observed_cycles: u128,
687 #[serde(deserialize_with = "serialization::required_option")]
688 pub principal: Option<String>,
689}
690
691#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
694#[serde(rename_all = "snake_case")]
695pub enum CanisterDisposition {
696 Create,
697 Delete,
698 Replace,
699 Reinstall,
700 Reuse,
701}
702
703#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
706pub struct CycleConservation {
707 pub estate_funding_domains: Vec<EstateFundingDomainPlan>,
708 #[serde(with = "u128_text")]
709 pub expected_post_operation_cycles: u128,
710 #[serde(with = "u128_text")]
711 pub maximum_execution_burn_cycles: u128,
712 #[serde(with = "u128_text")]
713 pub maximum_new_funding_cycles: u128,
714 #[serde(with = "u128_text")]
715 pub maximum_operator_debit_cycles: u128,
716 #[serde(with = "u128_text")]
717 pub maximum_unavoidable_fee_cycles: u128,
718 #[serde(with = "u128_text")]
719 pub observed_controlled_cycles: u128,
720 #[serde(with = "u128_text")]
721 pub retained_in_reused_canisters_cycles: u128,
722 #[serde(with = "u128_text")]
723 pub scheduled_transfer_cycles: u128,
724}
725
726#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
728#[serde(deny_unknown_fields)]
729pub struct EstateFundingDomainPlan {
730 pub allocated_workloads: u32,
731 #[serde(with = "option_u128_text")]
732 pub available_cycles: Option<u128>,
733 pub available_pool_slots: u32,
734 #[serde(with = "u128_text")]
735 pub creation_amount_cycles: u128,
736 pub cycles_ledger: String,
737 #[serde(with = "u128_text")]
738 pub creation_execution_margin_cycles: u128,
739 #[serde(with = "u128_text")]
740 pub readiness_floor_cycles: u128,
741 pub eligible_ready_pool_assets: u32,
742 pub initial_pool_assets: Vec<String>,
743 #[serde(with = "u128_text")]
744 pub ledger_fee_cycles: u128,
745 #[serde(with = "u128_text")]
746 pub management_creation_fee_cycles: u128,
747 #[serde(with = "u128_text")]
748 pub maximum_creation_debit_cycles: u128,
749 #[serde(with = "u128_text")]
750 pub maximum_creation_fee_cycles: u128,
751 #[serde(with = "u128_text")]
752 pub maximum_funding_cycles: u128,
753 pub occupied_pool_assets: u32,
754 pub pending_creation_count: u32,
755 pub pending_creation: Option<EstatePoolPendingCreationObservation>,
756 pub pool_maximum_size: u32,
757 pub planned_initial_workloads: u32,
758 pub required_creation_count: u32,
759 pub root: String,
760 pub root_principal: Option<String>,
761 #[serde(with = "u128_text")]
762 pub shortfall_cycles: u128,
763}
764
765#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
767#[serde(deny_unknown_fields)]
768pub struct EstateFundingRequiredRecord {
769 pub attempt_count: Option<u32>,
770 #[serde(with = "u128_text")]
771 pub available_cycles: u128,
772 #[serde(with = "u128_text")]
773 pub creation_amount_cycles: u128,
774 pub cycles_ledger: String,
775 #[serde(with = "u128_text")]
776 pub creation_execution_margin_cycles: u128,
777 #[serde(with = "u128_text")]
778 pub readiness_floor_cycles: u128,
779 #[serde(with = "u128_text")]
780 pub ledger_fee_cycles: u128,
781 #[serde(with = "u128_text")]
782 pub management_creation_fee_cycles: u128,
783 #[serde(with = "u128_text")]
784 pub maximum_creation_debit_cycles: u128,
785 #[serde(with = "u128_text")]
786 pub maximum_creation_fee_cycles: u128,
787 pub operation_id: String,
788 pub pending_creation_operation_id: Option<String>,
789 pub plan_sha256: String,
790 pub required_creation_count: u32,
791 pub root: String,
792 pub root_principal: String,
793 pub last_attempt_at_ns: Option<u64>,
794 pub retry_at_ns: Option<u64>,
795 #[serde(with = "u128_text")]
796 pub shortfall_cycles: u128,
797}
798
799#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
801pub struct ActualCycleConservation {
802 #[serde(with = "u128_text")]
803 pub estate_funding_cycles: u128,
804 #[serde(with = "u128_text")]
805 pub exact_estate_creation_fee_cycles: u128,
806 #[serde(with = "u128_text")]
807 pub exact_unavoidable_fee_cycles: u128,
808 #[serde(with = "u128_text")]
809 pub final_controlled_cycles: u128,
810 #[serde(with = "u128_text")]
811 pub measured_execution_burn_cycles: u128,
812 #[serde(with = "u128_text")]
813 pub observed_starting_cycles: u128,
814 #[serde(with = "u128_text")]
815 pub operator_debit_cycles: u128,
816 #[serde(with = "u128_text")]
817 pub received_new_funding_cycles: u128,
818}
819
820pub(crate) const fn creation_observation_burn(observation: u128, update: u128) -> Option<u128> {
823 observation.checked_add(update)
824}
825
826pub(crate) const fn create_balance_is_terminal(
830 actual_cycles: Option<u128>,
831 requested_initial_cycles: u128,
832 maximum_observation_burn_cycles: u128,
833) -> bool {
834 let Some(actual_cycles) = actual_cycles else {
835 return false;
836 };
837 if actual_cycles > requested_initial_cycles {
838 return false;
839 }
840 requested_initial_cycles - actual_cycles <= maximum_observation_burn_cycles
841}
842
843#[derive(Clone, Debug, Eq, PartialEq)]
849pub struct ReviewedDesiredFleetRecord {
850 desired: DesiredFleet,
851}
852
853impl ReviewedDesiredFleetRecord {
854 pub(crate) fn capture(desired: &DesiredFleet) -> Self {
855 Self {
856 desired: desired.clone(),
857 }
858 }
859
860 #[must_use]
862 pub const fn desired(&self) -> &DesiredFleet {
863 &self.desired
864 }
865
866 #[must_use]
868 pub fn into_desired(self) -> DesiredFleet {
869 self.desired
870 }
871}
872
873#[derive(Deserialize, Serialize)]
874struct ReviewedDesiredFleetRecordProjection {
875 desired: DesiredFleet,
876 protocol_steps: Vec<DesiredProtocolStep>,
877}
878
879impl Serialize for ReviewedDesiredFleetRecord {
880 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
881 where
882 S: serde::Serializer,
883 {
884 ReviewedDesiredFleetRecordProjection {
885 desired: self.desired.clone(),
886 protocol_steps: self.desired.protocol_steps.clone(),
887 }
888 .serialize(serializer)
889 }
890}
891
892impl<'de> Deserialize<'de> for ReviewedDesiredFleetRecord {
893 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
894 where
895 D: serde::Deserializer<'de>,
896 {
897 let mut projection = ReviewedDesiredFleetRecordProjection::deserialize(deserializer)?;
898 projection.desired.protocol_steps = projection.protocol_steps;
899 Ok(Self {
900 desired: projection.desired,
901 })
902 }
903}
904
905#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
907#[serde(deny_unknown_fields)]
908pub struct FleetEnsurePlan {
909 #[serde(deserialize_with = "serialization::required_option")]
911 pub recovery_review: Option<Box<FleetRecoveryReview>>,
912 #[serde(deserialize_with = "serialization::required_option")]
914 pub reinstall: Option<Box<FleetReinstallRecord>>,
915 #[serde(deserialize_with = "serialization::required_option")]
916 pub continuation: Option<FleetEnsureContinuationAuthority>,
917 pub canisters: Vec<CanisterPlan>,
918 pub conservation: CycleConservation,
919 pub desired_sha256: String,
920 pub environment: String,
921 pub fleet: String,
922 pub operation_id: String,
923 pub plan_sha256: String,
924 pub planned_at_time: u64,
925 pub protocol_actions: Vec<EnsureAction>,
926 #[serde(deserialize_with = "serialization::required_option")]
927 pub root_start_authority: Option<Box<RetainedRootStartAuthorityRecord>>,
928 pub root_reinstall_bindings: Vec<RootManagementBinding>,
930 #[serde(deserialize_with = "serialization::required_option")]
931 pub reviewed_desired: Option<Box<ReviewedDesiredFleetRecord>>,
932 pub schema_version: u16,
933 pub scope: FleetEnsurePlanScope,
934 #[serde(deserialize_with = "serialization::required_option")]
936 pub terminal_inventory_operation_id: Option<String>,
937}
938
939#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
941#[serde(rename_all = "snake_case")]
942pub enum FleetEnsurePlanScope {
943 #[default]
945 Full,
946 ReinstallPreparation,
948 RootReinstallPrerequisite,
950 RootStartPrerequisite,
952}
953
954impl FleetEnsurePlanScope {
955 #[must_use]
957 pub const fn as_str(self) -> &'static str {
958 match self {
959 Self::Full => "full",
960 Self::ReinstallPreparation => "reinstall_preparation",
961 Self::RootReinstallPrerequisite => "root_reinstall_prerequisite",
962 Self::RootStartPrerequisite => "root_start_prerequisite",
963 }
964 }
965}
966
967#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
969#[serde(deny_unknown_fields)]
970pub struct FleetReinstallRecord {
971 #[serde(deserialize_with = "serialization::required_option")]
972 pub activation_reset: Option<Box<FleetActivationResetRecord>>,
973 pub operation_id: String,
974 pub source_operation_id: String,
975 pub authorities: Vec<RootManagementBinding>,
976 pub assets: Vec<FleetReinstallAssetRecord>,
978}
979
980#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
984#[serde(deny_unknown_fields)]
985pub struct FleetActivationSourceRecord {
986 pub infrastructure: Vec<RootManagementBinding>,
987 pub operator: String,
988 pub cycles_ledger: String,
989 #[serde(with = "u128_text")]
990 pub initial_controlled_cycles: u128,
991 #[serde(with = "u128_text")]
992 pub maximum_execution_burn_cycles: u128,
993 #[serde(with = "u128_text_map")]
994 pub initial_estate_funding_cycles_by_root: BTreeMap<String, u128>,
995 pub operation_id: String,
996 pub plan_sha256: String,
997 pub plan_document_sha256: String,
998 pub journal_document_sha256: String,
999 pub state_document_sha256: String,
1000 pub provisioning: EnsureAction,
1001 pub registry_preparations: Vec<EnsureAction>,
1002 pub stores: Vec<FleetProtocolReadRecord>,
1003}
1004
1005#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1007#[serde(deny_unknown_fields)]
1008pub struct FleetActivationResetRecord {
1009 #[serde(deserialize_with = "serialization::required_option")]
1010 pub preparation: Option<ActivationPreparationEvidenceRecord>,
1011 pub source: FleetActivationSourceRecord,
1012 pub roots: Vec<RootActivationResetRecord>,
1013}
1014
1015#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1017#[serde(deny_unknown_fields)]
1018pub struct ActivationPreparationEvidenceRecord {
1019 pub plan_document_sha256: String,
1020 pub journal_document_sha256: String,
1021}
1022
1023#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1025#[serde(deny_unknown_fields)]
1026pub struct ActivationResetAdoptionRecord {
1027 pub source_plan_sha256: String,
1028 pub source_journal_sha256: String,
1029 pub source_state_sha256: String,
1030 pub replacement_plan_sha256: String,
1031 pub replacement_journal_sha256: String,
1032 pub complete: bool,
1033}
1034
1035#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1038#[serde(deny_unknown_fields)]
1039pub struct FleetProtocolReadRecord {
1040 pub candid: String,
1041 pub candid_sha256: String,
1042 pub principal: String,
1043}
1044
1045#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1048#[serde(deny_unknown_fields)]
1049pub struct RootActivationResetRecord {
1050 pub root: String,
1051 pub activation_operation_id: [u8; 32],
1052 pub inventory_hash: [u8; 32],
1053 pub provisioning_receipt_hash: [u8; 32],
1054 pub component_count: u32,
1055 pub managed_descendants: u32,
1056}
1057
1058#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1060#[serde(deny_unknown_fields)]
1061pub struct FleetReinstallAssetRecord {
1062 pub controllers: Vec<String>,
1063 #[serde(deserialize_with = "serialization::required_option")]
1064 pub module_sha256: Option<String>,
1065 pub principal: String,
1066 pub root: String,
1067 pub subnet: String,
1068}
1069
1070#[derive(Clone, Debug, Eq, PartialEq)]
1072pub struct RootManagementCanisterObservation {
1073 pub live: LiveCanister,
1074 pub name: String,
1075 pub subnet: String,
1076}
1077
1078#[derive(Clone, Debug, Eq, PartialEq)]
1080pub struct RootManagementObservation {
1081 pub operator_cycles: u128,
1082 pub roots: BTreeMap<String, RootManagementCanisterObservation>,
1083}
1084
1085#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1087#[serde(deny_unknown_fields)]
1088pub struct RootManagementBinding {
1089 pub controllers: Vec<String>,
1090 pub name: String,
1091 pub module_sha256: String,
1092 pub principal: String,
1093 pub subnet: String,
1094}
1095
1096#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1101#[serde(deny_unknown_fields)]
1102pub struct RetainedRootStartAuthorityRecord {
1103 pub authority_sha256: String,
1104 pub environment: String,
1105 pub fleet: String,
1106 pub fleet_id: canic_core::ids::FleetId,
1107 pub roots: Vec<RootManagementBinding>,
1108 pub schema_version: u16,
1109}
1110
1111impl RetainedRootStartAuthorityRecord {
1112 pub(crate) fn seal(&mut self) {
1113 self.authority_sha256 = self.expected_sha256();
1114 }
1115
1116 pub(crate) fn has_valid_digest(&self) -> bool {
1117 self.authority_sha256 == self.expected_sha256()
1118 }
1119
1120 fn expected_sha256(&self) -> String {
1121 let mut canonical = self.clone();
1122 canonical.authority_sha256.clear();
1123 let bytes = serde_json::to_vec(&canonical)
1124 .expect("retained Root-start authority is JSON serializable");
1125 let mut framed = b"canic:fleet-ensure:root-start-authority:v1".to_vec();
1126 framed.extend_from_slice(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
1127 framed.extend_from_slice(&bytes);
1128 canic_core::cdk::utils::hash::sha256_hex(&framed)
1129 }
1130}
1131
1132#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1135#[serde(rename_all = "snake_case")]
1136pub enum EffectState {
1137 Applied,
1138 Intent,
1139 Issued,
1140}
1141
1142#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1144#[serde(rename_all = "snake_case")]
1145pub enum FleetEnsureCompletion {
1146 Converged,
1147 Prepared,
1149 InProgress,
1150 ReplanRequired,
1151}
1152
1153#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1156#[serde(deny_unknown_fields)]
1157pub struct EffectRecord {
1158 pub maintenance_attempts: u32,
1160 pub action_sha256: String,
1161 #[serde(deserialize_with = "serialization::required_option")]
1162 pub created_principal: Option<String>,
1163 #[serde(with = "option_u128_text")]
1164 pub destination_post_cycles: Option<u128>,
1165 #[serde(with = "option_u128_text")]
1166 pub destination_pre_cycles: Option<u128>,
1167 #[serde(with = "option_u128_text")]
1168 pub post_cycles: Option<u128>,
1169 #[serde(with = "option_u128_text")]
1170 pub pre_cycles: Option<u128>,
1171 #[serde(deserialize_with = "serialization::required_option")]
1172 pub pre_canister_version: Option<u64>,
1173 #[serde(deserialize_with = "serialization::required_option")]
1174 pub progress_identity: Option<String>,
1175 #[serde(deserialize_with = "serialization::required_option")]
1176 pub receipt: Option<String>,
1177 pub state: EffectState,
1178}
1179
1180#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1182pub struct RetirementTransferBalances {
1183 pub destination_after: u128,
1184 pub destination_before: u128,
1185 pub maximum_execution_burn: u128,
1186 pub source_after: u128,
1187 pub source_before: u128,
1188 pub transfer_amount: u128,
1189}
1190
1191#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1193pub enum RetirementTransferReconciliation {
1194 Conserved {
1195 destination_credit: u128,
1196 source_debit: u128,
1197 },
1198 Pending,
1199}
1200
1201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1203pub enum RetirementTransferInvariantError {
1204 ArithmeticOverflow,
1205 BalanceMovedInInvalidDirection,
1206 Unbalanced {
1207 destination_credit: u128,
1208 maximum_source_debit: u128,
1209 source_debit: u128,
1210 },
1211}
1212
1213pub fn reconcile_retirement_transfer(
1215 balances: RetirementTransferBalances,
1216) -> Result<RetirementTransferReconciliation, RetirementTransferInvariantError> {
1217 let source_debit = balances
1218 .source_before
1219 .checked_sub(balances.source_after)
1220 .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
1221 let destination_credit = balances
1222 .destination_after
1223 .checked_sub(balances.destination_before)
1224 .ok_or(RetirementTransferInvariantError::BalanceMovedInInvalidDirection)?;
1225 let maximum_source_debit = balances
1226 .transfer_amount
1227 .checked_add(balances.maximum_execution_burn)
1228 .ok_or(RetirementTransferInvariantError::ArithmeticOverflow)?;
1229 if source_debit == 0 && destination_credit == 0 {
1230 return Ok(RetirementTransferReconciliation::Pending);
1231 }
1232 if source_debit >= balances.transfer_amount
1233 && source_debit <= maximum_source_debit
1234 && destination_credit == balances.transfer_amount
1235 {
1236 return Ok(RetirementTransferReconciliation::Conserved {
1237 destination_credit,
1238 source_debit,
1239 });
1240 }
1241 Err(RetirementTransferInvariantError::Unbalanced {
1242 destination_credit,
1243 maximum_source_debit,
1244 source_debit,
1245 })
1246}
1247
1248#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1251#[serde(deny_unknown_fields)]
1252pub struct FleetEnsureStateRecord {
1253 #[serde(deserialize_with = "serialization::required_option")]
1254 pub active_registry: Option<canic_core::dto::fleet_registry::FleetRegistry>,
1255 pub completed_reinstall_action_sha256: BTreeMap<String, String>,
1257 #[serde(deserialize_with = "serialization::required_option")]
1259 pub completed_reinstall_operation_id: Option<String>,
1260 pub completed_reinstalls: BTreeMap<String, u64>,
1263 pub fleet: String,
1264 pub pending_principals: BTreeMap<String, String>,
1265 pub principals: BTreeMap<String, String>,
1266 pub retained_cycles_by_principal: BTreeMap<String, u128>,
1267 pub schema_version: u16,
1268 pub topology: BTreeMap<String, FleetEnsureTopologyRecord>,
1269}
1270
1271#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1273#[serde(deny_unknown_fields)]
1274pub struct FleetEnsureTopologyRecord {
1275 pub kind: DesiredCanisterKind,
1276 #[serde(deserialize_with = "serialization::required_option")]
1277 pub module_hash: Option<String>,
1278 #[serde(deserialize_with = "serialization::required_option")]
1279 pub parent: Option<String>,
1280 #[serde(deserialize_with = "serialization::required_option")]
1281 pub protocol_binding: Option<crate::protocol_binding::RegistryProtocolBinding>,
1282 #[serde(deserialize_with = "serialization::required_option")]
1283 pub role: Option<String>,
1284}
1285
1286#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1289#[serde(deny_unknown_fields)]
1290pub struct FleetEnsureJournalRecord {
1291 pub funding_reviews: Vec<EstateFundingReviewRecord>,
1292 pub successor_phases: Vec<FleetEnsureSuccessorPhaseRecord>,
1293 pub completion: FleetEnsureCompletion,
1294 #[serde(deserialize_with = "serialization::required_option")]
1295 pub estate_funding_required: Option<EstateFundingRequiredRecord>,
1296 pub effects: Vec<EffectRecord>,
1297 pub fleet: String,
1298 #[serde(with = "u128_text")]
1299 pub initial_controlled_cycles: u128,
1300 #[serde(with = "u128_text_map")]
1301 pub initial_estate_funding_cycles_by_root: BTreeMap<String, u128>,
1302 #[serde(with = "u128_text")]
1303 pub initial_operator_cycles: u128,
1304 pub operation_id: String,
1305 pub plan_sha256: String,
1306 pub schema_version: u16,
1307 pub stalled_observations: u32,
1308}
1309
1310#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1312#[serde(deny_unknown_fields)]
1313pub struct FleetEnsureContinuationAuthority {
1314 pub app_config_sha256: String,
1315 pub application_artifact_union_sha256: String,
1316 pub coordinator_candid_sha256: String,
1317 pub maximum_successor_actions: u32,
1318 pub root_candid_sha256: String,
1319 pub store_candid_sha256: String,
1320}
1321
1322#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1324#[serde(deny_unknown_fields)]
1325pub struct FleetEnsureSuccessorPhaseRecord {
1326 #[serde(with = "u128_text")]
1327 pub execution_burn_before_phase: u128,
1328 pub plan_sha256: String,
1329 #[serde(skip)]
1331 pub plan: Option<Box<FleetEnsurePlan>>,
1332}
1333
1334#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
1336#[serde(rename_all = "snake_case")]
1337pub enum FleetEnsureSuccessorReviewReason {
1338 AdditionalEffect,
1339 BudgetExceeded,
1340 ChangedInputs,
1341 PhaseBound,
1342 ProtocolAuthority,
1343}
1344
1345#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1348pub struct FleetEnsureReport {
1349 pub funding_review: Option<EstateFundingReviewRecord>,
1350 pub actual_conservation: Option<ActualCycleConservation>,
1351 pub effects_applied: u32,
1352 pub plan: FleetEnsurePlan,
1353 pub terminal: bool,
1354}
1355
1356#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1359#[serde(deny_unknown_fields)]
1360pub struct EstateFundingReviewRecord {
1361 pub action: EnsureAction,
1362 #[serde(deserialize_with = "serialization::required_option")]
1363 pub effect: Option<EffectRecord>,
1364 pub pause: EstateFundingRequiredRecord,
1365 pub review_sha256: String,
1366}
1367
1368mod u128_text {
1369 use serde::{Deserialize, Deserializer, Serializer};
1370
1371 pub fn serialize<S>(value: &u128, serializer: S) -> Result<S::Ok, S::Error>
1372 where
1373 S: Serializer,
1374 {
1375 serializer.serialize_str(&value.to_string())
1376 }
1377
1378 pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
1379 where
1380 D: Deserializer<'de>,
1381 {
1382 String::deserialize(deserializer)?
1383 .parse()
1384 .map_err(serde::de::Error::custom)
1385 }
1386}
1387
1388mod option_u128_text {
1389 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1390
1391 #[expect(
1392 clippy::ref_option,
1393 reason = "serde(with) requires the serializer to accept the field by reference"
1394 )]
1395 pub fn serialize<S>(value: &Option<u128>, serializer: S) -> Result<S::Ok, S::Error>
1396 where
1397 S: Serializer,
1398 {
1399 value.map(|cycles| cycles.to_string()).serialize(serializer)
1400 }
1401
1402 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<u128>, D::Error>
1403 where
1404 D: Deserializer<'de>,
1405 {
1406 Option::<String>::deserialize(deserializer)?
1407 .map(|value| value.parse().map_err(serde::de::Error::custom))
1408 .transpose()
1409 }
1410}
1411
1412mod u128_text_map {
1413 use std::collections::BTreeMap;
1414
1415 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1416
1417 pub fn serialize<S>(values: &BTreeMap<String, u128>, serializer: S) -> Result<S::Ok, S::Error>
1418 where
1419 S: Serializer,
1420 {
1421 values
1422 .iter()
1423 .map(|(key, value)| (key, value.to_string()))
1424 .collect::<BTreeMap<_, _>>()
1425 .serialize(serializer)
1426 }
1427
1428 pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<String, u128>, D::Error>
1429 where
1430 D: Deserializer<'de>,
1431 {
1432 BTreeMap::<String, String>::deserialize(deserializer)?
1433 .into_iter()
1434 .map(|(key, value)| {
1435 value
1436 .parse()
1437 .map(|value| (key, value))
1438 .map_err(serde::de::Error::custom)
1439 })
1440 .collect()
1441 }
1442}
1443
1444#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1446#[serde(deny_unknown_fields)]
1447pub struct FleetRecoveryReview {
1448 #[serde(with = "u128_text")]
1449 pub base_execution_burn_cycles: u128,
1450 #[serde(with = "u128_text")]
1451 pub continuation_reserve_cycles: u128,
1452 #[serde(with = "u128_text")]
1453 pub whole_continuation_ceiling_cycles: u128,
1454 pub known_pool_funding: Vec<PoolRecoveryFunding>,
1455 pub discovery: RecoveryDiscovery,
1457}
1458
1459#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
1461#[serde(rename_all = "snake_case")]
1462pub enum RecoveryDiscovery {
1463 PendingCurrentProtocol,
1464}
1465
1466#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1468#[serde(deny_unknown_fields)]
1469pub struct PoolRecoveryFunding {
1470 pub principal: String,
1471 pub root: String,
1472 #[serde(with = "u128_text")]
1473 pub amount_cycles: u128,
1474 #[serde(with = "u128_text")]
1475 pub ledger_fee_cycles: u128,
1476 #[serde(with = "u128_text")]
1477 pub funding_deficit_cycles: u128,
1478 #[serde(with = "u128_text")]
1479 pub funding_margin_cycles: u128,
1480 #[serde(with = "u128_text")]
1481 pub expected_post_cycles: u128,
1482}
1483
1484#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1486pub struct FleetSuccessorReview {
1487 pub actions: Vec<FleetReviewAction>,
1488 #[serde(with = "u128_text")]
1489 pub maximum_additional_debit_cycles: u128,
1490 pub next_review_command: String,
1491}
1492
1493#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1495pub struct FleetReviewAction {
1496 pub name: String,
1497 pub principal: Option<String>,
1498 pub kind: String,
1499}
1500
1501impl std::fmt::Display for FleetSuccessorReview {
1502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1503 write!(f, "remaining actions: ")?;
1504 for (index, action) in self.actions.iter().enumerate() {
1505 if index > 0 {
1506 write!(f, ", ")?;
1507 }
1508 write!(
1509 f,
1510 "{} {} ({})",
1511 action.kind,
1512 action.name,
1513 action.principal.as_deref().unwrap_or("not yet created")
1514 )?;
1515 }
1516 write!(
1517 f,
1518 "; maximum additional debit including fees: {} cycles; next review: {}",
1519 self.maximum_additional_debit_cycles, self.next_review_command
1520 )
1521 }
1522}