Skip to main content

canic_core/dto/
pool.rs

1//! Passive boundary contracts for the Fleet Subnet Root prepaid Canister pool.
2
3use crate::{
4    cdk::types::Cycles,
5    ids::{ComponentInstanceId, FleetSubnetCanisterPoolConfig},
6};
7use candid::{CandidType, Principal};
8use serde::{Deserialize, Serialize};
9
10/// Identifies the durable Component allocation that has claimed one empty Canister.
11#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct CanisterPoolClaim {
13    pub component: Option<ComponentInstanceId>,
14    pub operation_id: [u8; 32],
15}
16
17/// How one prepaid empty Canister entered the root-owned inventory.
18#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
19pub enum CanisterPoolAssetOrigin {
20    Created,
21    Imported,
22    Recycled,
23}
24
25/// Current durable state of one prepaid empty-Canister asset.
26#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
27pub enum CanisterPoolAssetStatus {
28    PendingReset,
29    Ready,
30    Claimed { claim: CanisterPoolClaim },
31    HandingOff { recipient: Principal },
32    Failed { reason: String },
33}
34
35/// Controller-visible inventory row for one prepaid empty Canister.
36#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
37pub struct CanisterPoolAsset {
38    pub canister_id: Principal,
39    pub cycles: Cycles,
40    pub origin: CanisterPoolAssetOrigin,
41    pub status: CanisterPoolAssetStatus,
42    pub added_at_ns: u64,
43    pub updated_at_ns: u64,
44}
45
46/// Durable refill creation whose paid effect is incomplete or awaiting commit.
47#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
48pub struct CanisterPoolCreation {
49    pub operation_id: [u8; 32],
50    pub canister_cycles: Cycles,
51    pub canister_id: Option<Principal>,
52    pub prepared_at_ns: u64,
53}
54
55/// Durable transfer of one paid asset to replacement authority during root draining.
56#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57pub struct CanisterPoolHandoff {
58    pub canister_id: Principal,
59    pub recipient: Principal,
60    pub prepared_at_ns: u64,
61}
62
63/// Bounded controller query for one canonical inventory page.
64#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
65pub struct CanisterPoolStatusRequest {
66    pub start_after: Option<Principal>,
67    pub limit: u16,
68}
69
70/// Exact configured policy and current pool inventory.
71#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72pub struct CanisterPoolResponse {
73    pub config: FleetSubnetCanisterPoolConfig,
74    pub tracked: u32,
75    pub surplus: u32,
76    pub ready: u32,
77    pub pending_reset: u32,
78    pub claimed: u32,
79    pub handing_off: u32,
80    pub failed: u32,
81    pub completed_handoffs: u64,
82    pub pending_creation: Option<CanisterPoolCreation>,
83    pub pending_handoff: Option<CanisterPoolHandoff>,
84    pub entries: Vec<CanisterPoolAsset>,
85    pub next_start_after: Option<Principal>,
86}
87
88/// Controller command for explicit pool maintenance.
89#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
90pub enum PoolAdminCommand {
91    Maintain,
92    Import {
93        canister_id: Principal,
94    },
95    RetryReset {
96        canister_id: Principal,
97    },
98    Handoff {
99        canister_id: Principal,
100        recipient: Principal,
101    },
102}
103
104/// Result of one explicit pool maintenance command.
105#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq)]
106pub enum PoolAdminResponse {
107    Maintained,
108    MaintenancePaused {
109        reason: String,
110    },
111    Created {
112        canister_id: Principal,
113    },
114    Imported {
115        canister_id: Principal,
116    },
117    ResetQueued {
118        canister_id: Principal,
119    },
120    ResetReady {
121        canister_id: Principal,
122    },
123    HandedOff {
124        canister_id: Principal,
125        recipient: Principal,
126    },
127    ResetFailed {
128        canister_id: Principal,
129        reason: String,
130    },
131}
132
133// -----------------------------------------------------------------------------
134// Tests
135// -----------------------------------------------------------------------------
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn pool_status_and_admin_contracts_round_trip_through_candid() {
143        let canister_id = Principal::from_slice(&[7; 29]);
144        let response = CanisterPoolResponse {
145            config: FleetSubnetCanisterPoolConfig {
146                minimum_size: 3,
147                maximum_size: 10,
148                canister_cycles: Cycles::new(5_000_000_000_000),
149            },
150            tracked: 1,
151            surplus: 0,
152            ready: 0,
153            pending_reset: 0,
154            claimed: 0,
155            handing_off: 0,
156            failed: 1,
157            completed_handoffs: 0,
158            pending_creation: None,
159            pending_handoff: None,
160            entries: vec![CanisterPoolAsset {
161                canister_id,
162                cycles: Cycles::new(4_000_000_000_000),
163                origin: CanisterPoolAssetOrigin::Recycled,
164                status: CanisterPoolAssetStatus::Failed {
165                    reason: "below configured cycles".to_string(),
166                },
167                added_at_ns: 10,
168                updated_at_ns: 11,
169            }],
170            next_start_after: None,
171        };
172        let bytes = candid::encode_one(&response).expect("encode pool status");
173        assert_eq!(
174            candid::decode_one::<CanisterPoolResponse>(&bytes).expect("decode pool status"),
175            response,
176        );
177
178        let command = PoolAdminCommand::Import { canister_id };
179        let bytes = candid::encode_one(&command).expect("encode pool command");
180        assert_eq!(
181            candid::decode_one::<PoolAdminCommand>(&bytes).expect("decode pool command"),
182            command,
183        );
184    }
185}