ic_test/icp/
deployer.rs

1//! ## Deployer Trait and DeployBuilder
2//!
3//! The [`Deployer`] trait provides an ergonomic, configurable interface to create or manage
4//! canisters using a builder pattern. It supports creating new canisters, installing/reinstalling/upgrading
5//! WASM modules, and injecting arguments and cycles.
6//!
7//! It’s implemented for types like [`IcpUser`] that fulfill the [`Caller`] trait,
8//! and it relies on a [`Provider`] to handle low-level interaction with the IC test environment.
9//!
10//! The central utility, [`DeployBuilder`], enables step-by-step customization of deployments
11//! and safely returns fully-constructed canister instances.
12
13use candid::Principal;
14
15use ic_cdk::management_canister::{CanisterInstallMode, CanisterSettings};
16use thiserror::Error;
17
18use super::{
19    caller::Caller,
20    provider::{Provider, RejectResponse},
21};
22
23/// Describes potential errors that can occur during the deployment process.
24#[derive(Debug, Error)]
25pub enum DeployError {
26    #[error("failed to candid encode arguments: {}", .0)]
27    ArgumentEncoding(candid::error::Error),
28    #[error("canister rejected: {}, error_code: {}", .0.reject_message, .0.error_code)]
29    Reject(RejectResponse),
30    #[error("failed to candid decode result: {}", .0)]
31    ResultDecoding(candid::error::Error),
32    #[error("canister creation failed: {}", .0)]
33    CreateCanister(String),
34    #[error("canister id is missing")]
35    UnspecifiedCanister,
36}
37
38/// Represents the deployment strategy for a canister.
39pub enum DeployMode {
40    /// Creates and installs a new canister.
41    Create,
42
43    /// Installs a fresh WASM on an existing canister.
44    Install,
45
46    /// Reinstalls WASM (resetting all state).
47    Reinstall,
48
49    /// Upgrades a canister (preserving state).
50    Upgrade,
51}
52
53/// A type capable of deploying canisters with arguments and lifecycle control.
54///
55/// Implementors typically use [`DeployBuilder`] to configure and execute deployment logic.
56pub trait Deployer {
57    type Caller: Caller;
58
59    /// Begins a canister deployment sequence with the given candid-encoded args
60    /// and a constructor function for your strongly-typed client.
61    fn deploy<Canister>(
62        &self,
63        args: Result<Vec<u8>, candid::error::Error>,
64        new: fn(&Self::Caller, Principal) -> Canister,
65    ) -> DeployBuilder<Canister, Self::Caller>;
66}
67
68/// Builder struct for configuring and performing a canister deployment.
69///
70/// Provides an ergonomic way to:
71/// - Set the deployment mode (create, install, upgrade, reinstall)
72/// - Attach initial cycles
73/// - Define WASM module and settings
74/// - Inject candid arguments
75/// - Produce a typed client interface
76pub struct DeployBuilder<Canister, C: Caller> {
77    /// Provider that performs actual deployment (e.g. PocketIc).
78    pub provider: C::Provider,
79    /// The logical caller for interactions post-deployment.
80    pub caller: C,
81    /// Optional canister ID for pre-existing canisters.
82    pub canister_id: Option<Principal>,
83    /// Deployment mode (create, install, etc.).
84    pub mode: DeployMode,
85    /// Canister configuration (controllers, memory allocation, compute allocation, etc.).
86    pub settings: CanisterSettings,
87    /// Initial cycles to add.
88    pub cycles: u128,
89    /// WASM module to install.
90    pub wasm: Vec<u8>,
91    /// Candid-encoded constructor arguments.
92    pub args: Result<Vec<u8>, candid::error::Error>,
93    /// Function to wrap a raw `Principal` in a user-defined canister type.
94    pub new: fn(&C, Principal) -> Canister,
95}
96
97impl<Canister, C: Caller> DeployBuilder<Canister, C> {
98    pub fn with_canister_id(self, canister_id: Principal) -> Self {
99        Self {
100            canister_id: Some(canister_id),
101            ..self
102        }
103    }
104
105    pub fn with_controllers(self, controllers: Vec<Principal>) -> Self {
106        Self {
107            settings: CanisterSettings {
108                controllers: Some(controllers.clone()),
109                ..self.settings
110            },
111            ..self
112        }
113    }
114
115    pub fn with_cycles(self, cycles: u128) -> Self {
116        Self { cycles, ..self }
117    }
118
119    pub fn with_settings(self, settings: CanisterSettings) -> Self {
120        Self { settings, ..self }
121    }
122
123    pub fn with_wasm(self, wasm: Vec<u8>) -> Self {
124        Self { wasm, ..self }
125    }
126
127    pub fn with_install(self) -> Self {
128        Self {
129            mode: DeployMode::Install,
130            ..self
131        }
132    }
133
134    pub fn with_upgrade(self) -> Self {
135        Self {
136            mode: DeployMode::Upgrade,
137            ..self
138        }
139    }
140
141    pub fn with_reinstall(self) -> Self {
142        Self {
143            mode: DeployMode::Reinstall,
144            ..self
145        }
146    }
147
148    /// Execute the deployment, returning either a constructed canister interface or an error.
149    pub async fn maybe_call(self) -> Result<Canister, DeployError> {
150        let args = self.args.map_err(DeployError::ArgumentEncoding)?;
151
152        let canister_id = if let DeployMode::Create = self.mode {
153            self.provider
154                .create_canister(self.settings, self.canister_id)
155                .await
156                .map_err(DeployError::Reject)?
157        } else {
158            match self.canister_id {
159                Some(canister_id) => canister_id,
160                None => {
161                    return Err(DeployError::UnspecifiedCanister);
162                }
163            }
164        };
165
166        self.provider
167            .add_cycles(canister_id, self.cycles)
168            .await
169            .map_err(DeployError::Reject)?;
170
171        let mode = match self.mode {
172            DeployMode::Create | DeployMode::Install => CanisterInstallMode::Install,
173            DeployMode::Reinstall => CanisterInstallMode::Reinstall,
174            DeployMode::Upgrade => CanisterInstallMode::Upgrade(None),
175        };
176
177        self.provider
178            .install_code(mode, canister_id, self.wasm, args)
179            .await
180            .map_err(DeployError::Reject)?;
181
182        Ok((self.new)(&self.caller, canister_id))
183    }
184
185    /// Execute deployment, assuming it should not fail. Panics if deployment fails.
186    pub async fn call(self) -> Canister {
187        self.maybe_call().await.unwrap()
188    }
189}