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