Skip to main content

bity_ic_subcanister_manager/
lib.rs

1//! A library for managing sub-canisters on the Internet Computer.
2//!
3//! This library provides functionality to create, manage, and update sub-canisters
4//! on the Internet Computer. It handles the lifecycle of canisters including
5//! creation, installation, updates, and state management.
6//!
7//! # Features
8//!
9//! - Create and manage sub-canisters
10//! - Handle canister lifecycle (create, install, update, stop)
11//! - Manage canister controllers and permissions
12//! - Handle cycles allocation and management
13//!
14//! # Example
15//!
16//! ```rust
17//! use bity_ic_subcanister_manager::{SubCanisterManager, CanisterState};
18//!
19//! // Create a new sub-canister manager
20//! let manager = SubCanisterManager::new(
21//!     master_canister_id,
22//!     HashMap::new(),
23//!     vec![],
24//!     vec![],
25//!     1_000_000_000, // initial cycles
26//!     100_000_000,   // reserved cycles
27//!     false,         // test mode
28//!     "commit_hash".to_string(),
29//!     wasm_module,
30//! );
31//! ```
32//!
33//! # License
34//!
35//! This project is licensed under the MIT License.
36
37use bity_ic_utils::retry_async::retry_async;
38use candid::{CandidType, Encode, Nat, Principal};
39use canfund::{
40    manager::{options::FundManagerOptions, RegisterOpts},
41    operations::fetch::FetchCyclesBalanceFromCanisterStatus,
42    FundManager,
43};
44use ic_cdk::management_canister::create_canister_with_extra_cycles;
45use ic_cdk::management_canister::{
46    canister_status, install_code, start_canister, stop_canister, CanisterIdRecord,
47    CanisterInstallMode, CanisterSettings, CreateCanisterArgs, InstallCodeArgs, LogVisibility,
48};
49use serde::{Deserialize, Serialize};
50use std::sync::Arc;
51use std::{any::Any, collections::HashMap, fmt::Debug};
52
53/// Error types for storage operations
54#[derive(Debug)]
55pub enum NewStorageError {
56    /// Error when creating a new canister
57    CreateCanisterError(String),
58    /// Error when installing code on a canister
59    InstallCodeError(String),
60    /// Error when serializing initialization arguments
61    FailedToSerializeInitArgs(String),
62}
63
64/// Error types for canister operations
65#[derive(Debug)]
66pub enum NewCanisterError {
67    /// Error when creating a new canister
68    CreateCanisterError(String),
69    /// Error when installing code on a canister
70    InstallCodeError(String),
71    /// Error when serializing initialization arguments
72    FailedToSerializeInitArgs(String),
73}
74
75/// Error types for canister operations
76#[derive(Serialize, Deserialize, Clone)]
77pub enum CanisterError {
78    /// Error when controllers cannot be found
79    CantFindControllers(String),
80}
81
82/// Represents the current state of a canister
83#[derive(CandidType, Serialize, Deserialize, Clone, PartialEq, Debug)]
84pub enum CanisterState {
85    /// Canister has been created but not yet installed
86    Created,
87    /// Canister is installed and running
88    Installed,
89    /// Canister has been stopped
90    Stopped,
91}
92
93/// Trait that must be implemented by canister types
94pub trait Canister {
95    /// Type of parameters used for canister initialization
96    type ParamType: CandidType + Serialize + Clone + Send;
97
98    /// Creates a new canister instance
99    fn new(canister_id: Principal, state: CanisterState, canister_param: Self::ParamType) -> Self;
100
101    /// Returns the canister's parameters
102    fn canister_param(&self) -> Self::ParamType;
103
104    /// Returns the canister's ID
105    fn canister_id(&self) -> Principal;
106
107    /// Returns the current state of the canister
108    fn state(&self) -> CanisterState;
109
110    /// Returns the canister as an Any type for type erasure
111    fn as_any(&self) -> &dyn Any;
112
113    /// Retrieves the controllers of the canister
114    fn get_canister_controllers(
115        &self,
116    ) -> impl std::future::Future<Output = Result<Vec<Principal>, CanisterError>> + Send
117    where
118        Self: Sync + Send,
119    {
120        async {
121            match retry_async(
122                async || {
123                    canister_status(&CanisterIdRecord {
124                        canister_id: self.canister_id(),
125                    })
126                    .await
127                },
128                3,
129            )
130            .await
131            {
132                Ok(res) => Ok(res.settings.controllers),
133                Err(e) => Err(CanisterError::CantFindControllers(format!("{e:?}"))),
134            }
135        }
136    }
137}
138
139/// Manager for handling sub-canisters
140#[derive(Serialize, Deserialize)]
141pub struct SubCanisterManager<T>
142where
143    T: Canister + Clone + Send,
144{
145    /// ID of the master canister
146    pub master_canister_id: Principal,
147    /// Map of sub-canisters
148    pub sub_canisters: HashMap<Principal, Box<T>>,
149    /// List of controllers
150    pub controllers: Vec<Principal>,
151    /// List of authorized principals
152    pub authorized_principal: Vec<Principal>,
153    /// Initial cycles for new canisters
154    pub initial_cycles: u128,
155    /// Reserved cycles for canisters
156    pub reserved_cycles: u128,
157    /// Whether the manager is in test mode
158    pub test_mode: bool,
159    /// Commit hash of the current version
160    pub commit_hash: String,
161    /// WASM module for canister installation
162    pub wasm: Vec<u8>,
163    /// Fund manager
164    #[serde(skip)]
165    pub fund_manager: FundManager,
166    /// Funding config
167    #[serde(skip)]
168    pub funding_config: FundManagerOptions,
169}
170
171impl<T> SubCanisterManager<T>
172where
173    T: Canister + Clone + Send,
174{
175    pub fn new(
176        master_canister_id: Principal,
177        sub_canisters: HashMap<Principal, Box<T>>,
178        mut controllers: Vec<Principal>,
179        mut authorized_principal: Vec<Principal>,
180        initial_cycles: u128,
181        reserved_cycles: u128,
182        test_mode: bool,
183        commit_hash: String,
184        wasm: Vec<u8>,
185        funding_config: FundManagerOptions,
186    ) -> Self {
187        controllers.push(master_canister_id);
188        authorized_principal.push(master_canister_id);
189
190        Self {
191            master_canister_id,
192            sub_canisters,
193            controllers,
194            authorized_principal,
195            initial_cycles,
196            reserved_cycles,
197            test_mode,
198            commit_hash,
199            wasm,
200            fund_manager: FundManager::new(),
201            funding_config: funding_config,
202        }
203    }
204
205    pub async fn create_canister(
206        &mut self,
207        init_args: <T as Canister>::ParamType,
208    ) -> Result<Box<T>, NewCanisterError> {
209        let mut canister_id = Principal::anonymous();
210
211        for (_canister_id, canister) in self.sub_canisters.iter() {
212            if canister.state() == CanisterState::Created {
213                canister_id = *_canister_id;
214                break;
215            }
216        }
217
218        if canister_id == Principal::anonymous() {
219            let settings = CanisterSettings {
220                controllers: Some(self.controllers.clone()),
221                compute_allocation: None,
222                memory_allocation: None,
223                freezing_threshold: None,
224                reserved_cycles_limit: Some(Nat::from(self.reserved_cycles)),
225                log_visibility: Some(LogVisibility::Public),
226                wasm_memory_limit: None,
227                wasm_memory_threshold: None,
228                environment_variables: None,
229            };
230
231            canister_id = match retry_async(
232                async || {
233                    create_canister_with_extra_cycles(
234                        &CreateCanisterArgs {
235                            settings: Some(settings.clone()),
236                        },
237                        self.initial_cycles,
238                    )
239                    .await
240                },
241                3,
242            )
243            .await
244            {
245                Ok(canister) => canister.canister_id,
246                Err(e) => {
247                    return Err(NewCanisterError::CreateCanisterError(format!("{e:?}")));
248                }
249            };
250
251            add_canisters_to_fund_manager(
252                &mut self.fund_manager,
253                self.funding_config.clone(),
254                vec![canister_id],
255            );
256
257            self.sub_canisters.insert(
258                canister_id,
259                Box::new(T::new(
260                    canister_id,
261                    CanisterState::Created,
262                    init_args.clone(),
263                )),
264            );
265        }
266
267        let encoded_init_args = match Encode!(&init_args) {
268            Ok(encoded_init_args) => encoded_init_args,
269            Err(e) => {
270                return Err(NewCanisterError::FailedToSerializeInitArgs(format!("{e}")));
271            }
272        };
273
274        let install_args = InstallCodeArgs {
275            mode: CanisterInstallMode::Install,
276            canister_id,
277            wasm_module: self.wasm.clone(),
278            arg: encoded_init_args.clone(),
279        };
280
281        match install_code(&install_args).await {
282            Ok(_) => {}
283            Err(e) => {
284                return Err(NewCanisterError::InstallCodeError(format!("{:?}", e)));
285            }
286        }
287
288        let canister = Box::new(T::new(
289            canister_id,
290            CanisterState::Installed,
291            init_args.clone(),
292        ));
293
294        self.sub_canisters.insert(canister_id, canister);
295
296        Ok(self
297            .sub_canisters
298            .get(&canister_id)
299            .expect("Canister was just inserted")
300            .clone())
301    }
302
303    pub async fn update_canisters(
304        &mut self,
305        update_args: <T as Canister>::ParamType,
306    ) -> Result<(), Vec<String>> {
307        let init_args = match Encode!(&update_args.clone()) {
308            Ok(encoded_init_args) => encoded_init_args,
309            Err(e) => {
310                return Err(vec![format!(
311                    "ERROR : failed to create init args with error - {e}"
312                )]);
313            }
314        };
315
316        let mut canister_upgrade_errors = vec![];
317
318        for (canister_id, _canister) in self.sub_canisters.clone().iter() {
319            match retry_async(
320                async || {
321                    stop_canister(&CanisterIdRecord {
322                        canister_id: *canister_id,
323                    })
324                    .await
325                },
326                3,
327            )
328            .await
329            {
330                Ok(_) => {
331                    self.sub_canisters.insert(
332                        *canister_id,
333                        Box::new(T::new(
334                            *canister_id,
335                            CanisterState::Stopped,
336                            update_args.clone(),
337                        )),
338                    );
339                }
340                Err(e) => {
341                    canister_upgrade_errors.push(format!(
342                            "ERROR: storage upgrade :: storage with principal : {} failed to stop with error {:?}",
343                            *canister_id, e
344                        ));
345                    continue;
346                }
347            }
348
349            let result = {
350                let init_args = init_args.clone();
351                let wasm_module = self.wasm.clone();
352
353                let install_args = InstallCodeArgs {
354                    mode: CanisterInstallMode::Upgrade(None),
355                    canister_id: *canister_id,
356                    wasm_module,
357                    arg: init_args,
358                };
359                retry_async(|| install_code(&install_args), 3).await
360            };
361
362            match result {
363                Ok(_) => {
364                    match retry_async(
365                        async || {
366                            start_canister(&CanisterIdRecord {
367                                canister_id: *canister_id,
368                            })
369                            .await
370                        },
371                        3,
372                    )
373                    .await
374                    {
375                        Ok(_) => {
376                            self.sub_canisters.insert(
377                                *canister_id,
378                                Box::new(T::new(
379                                    *canister_id,
380                                    CanisterState::Installed,
381                                    update_args.clone(),
382                                )),
383                            );
384                        }
385                        Err(e) => {
386                            canister_upgrade_errors.push(format!(
387                                    "ERROR: storage upgrade :: storage with principal : {} failed to start with error {:?}",
388                                    *canister_id, e
389                                ));
390                        }
391                    }
392                }
393                Err(e) => {
394                    canister_upgrade_errors.push(format!(
395                            "ERROR: storage upgrade :: storage with principal : {} failed to install upgrade {:?}",
396                            *canister_id, e
397                        ));
398                }
399            }
400        }
401
402        if !canister_upgrade_errors.is_empty() {
403            Err(canister_upgrade_errors)
404        } else {
405            Ok(())
406        }
407    }
408
409    pub fn list_canisters(&self) -> Vec<Box<impl Canister>> {
410        self.sub_canisters.values().cloned().collect()
411    }
412
413    pub fn list_canisters_ids(&self) -> Vec<Principal> {
414        self.sub_canisters.clone().into_keys().collect()
415    }
416}
417
418impl<T> Clone for SubCanisterManager<T>
419where
420    T: Canister + Clone + Send,
421{
422    fn clone(&self) -> Self {
423        let mut fund_manager = FundManager::new();
424
425        add_canisters_to_fund_manager(
426            &mut fund_manager,
427            self.funding_config.clone(),
428            self.sub_canisters.clone().into_keys().collect(),
429        );
430
431        Self {
432            master_canister_id: self.master_canister_id,
433            sub_canisters: self.sub_canisters.clone(),
434            controllers: self.controllers.clone(),
435            authorized_principal: self.authorized_principal.clone(),
436            initial_cycles: self.initial_cycles,
437            reserved_cycles: self.reserved_cycles,
438            test_mode: self.test_mode,
439            commit_hash: self.commit_hash.clone(),
440            wasm: self.wasm.clone(),
441            fund_manager: fund_manager,
442            funding_config: self.funding_config.clone(),
443        }
444    }
445}
446
447pub fn add_canisters_to_fund_manager(
448    fund_manager: &mut FundManager,
449    funding_config: FundManagerOptions,
450    canister_id_lst: Vec<Principal>,
451) {
452    fund_manager.stop();
453
454    fund_manager.with_options(funding_config);
455
456    for canister_id in canister_id_lst {
457        fund_manager.register(
458            canister_id,
459            RegisterOpts::new()
460                .with_cycles_fetcher(Arc::new(FetchCyclesBalanceFromCanisterStatus::new())),
461        );
462    }
463
464    fund_manager.start();
465}