Skip to main content

ic_canister_runtime/
lib.rs

1//! Library to abstract the canister runtime so that code making requests to canisters can be reused:
2//! * in production using [`ic_cdk`],
3//! * in unit tests by mocking this trait,
4//! * in integration tests by implementing this trait for `PocketIc`.
5
6#![forbid(unsafe_code)]
7#![forbid(missing_docs)]
8
9use async_trait::async_trait;
10use candid::{utils::ArgumentEncoder, CandidType, Principal};
11use ic_cdk::call::{Call, CallFailed, CandidDecodeFailed};
12use ic_error_types::RejectCode;
13use serde::de::DeserializeOwned;
14pub use stub::StubRuntime;
15use thiserror::Error;
16#[cfg(feature = "wallet")]
17pub use wallet::CyclesWalletRuntime;
18
19mod stub;
20#[cfg(feature = "wallet")]
21mod wallet;
22
23/// Abstract the canister runtime so that code making requests to canisters can be reused:
24/// * in production using [`ic_cdk`],
25/// * in unit tests by mocking this trait,
26/// * in integration tests by implementing this trait for `PocketIc`.
27#[async_trait]
28pub trait Runtime {
29    /// Defines how asynchronous inter-canister update calls are made.
30    async fn update_call<In, Out>(
31        &self,
32        id: Principal,
33        method: &str,
34        args: In,
35        cycles: u128,
36    ) -> Result<Out, IcError>
37    where
38        In: ArgumentEncoder + Send,
39        Out: CandidType + DeserializeOwned;
40
41    /// Defines how asynchronous inter-canister query calls are made.
42    async fn query_call<In, Out>(
43        &self,
44        id: Principal,
45        method: &str,
46        args: In,
47    ) -> Result<Out, IcError>
48    where
49        In: ArgumentEncoder + Send,
50        Out: CandidType + DeserializeOwned;
51}
52
53/// Blanket implementation of [`Runtime`] for references to types that implement [`Runtime`].
54///
55/// # Examples
56///
57/// ```rust
58/// # #[tokio::main]
59/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
60/// use candid::Principal;
61/// use ic_canister_runtime::{IcError, Runtime, StubRuntime};
62///
63/// let runtime = StubRuntime::new()
64///     .add_stub_response(1_u64)
65///     .add_stub_response(2_u64);
66///
67/// async fn call<R: Runtime>(runtime: R) -> u64 {
68///     const PRINCIPAL: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]);
69///     runtime.query_call(PRINCIPAL, "method", ("args",))
70///         .await
71///         .expect("Call failed!")
72/// }
73///
74/// assert_eq!(call(&runtime).await, 1_u64);
75/// assert_eq!(call(runtime).await, 2_u64);
76/// # Ok(())
77/// # }
78/// ```
79#[async_trait]
80impl<R: Runtime + Send + Sync> Runtime for &R {
81    async fn update_call<In, Out>(
82        &self,
83        id: Principal,
84        method: &str,
85        args: In,
86        cycles: u128,
87    ) -> Result<Out, IcError>
88    where
89        In: ArgumentEncoder + Send,
90        Out: CandidType + DeserializeOwned,
91    {
92        (*self).update_call(id, method, args, cycles).await
93    }
94
95    async fn query_call<In, Out>(
96        &self,
97        id: Principal,
98        method: &str,
99        args: In,
100    ) -> Result<Out, IcError>
101    where
102        In: ArgumentEncoder + Send,
103        Out: CandidType + DeserializeOwned,
104    {
105        (*self).query_call(id, method, args).await
106    }
107}
108
109/// Error returned by the Internet Computer when making an inter-canister call.
110#[derive(Error, Clone, Debug, PartialEq, Eq)]
111pub enum IcError {
112    /// The liquid cycle balance is insufficient to perform the call.
113    #[error("Insufficient liquid cycles balance, available: {available}, required: {required}")]
114    InsufficientLiquidCycleBalance {
115        /// The liquid cycle balance available in the canister.
116        available: u128,
117        /// The required cycles to perform the call.
118        required: u128,
119    },
120
121    /// The `ic0.call_perform` operation failed when performing the inter-canister call.
122    #[error("Inter-canister call perform failed")]
123    CallPerformFailed,
124
125    /// The inter-canister call is rejected.
126    #[error("Inter-canister call rejected: {code:?} - {message})")]
127    CallRejected {
128        /// Rejection code as specified [here](https://internetcomputer.org/docs/current/references/ic-interface-spec#reject-codes)
129        code: RejectCode,
130        /// Associated helper message.
131        message: String,
132    },
133
134    /// The response from the inter-canister call could not be decoded as Candid.
135    #[error("The inter-canister call response could not be decoded: {message}")]
136    CandidDecodeFailed {
137        /// The specific Candid error that occurred.
138        message: String,
139    },
140}
141
142impl From<CallFailed> for IcError {
143    fn from(err: CallFailed) -> Self {
144        match err {
145            CallFailed::CallPerformFailed(_) => IcError::CallPerformFailed,
146            CallFailed::CallRejected(e) => {
147                IcError::CallRejected {
148                    // `CallRejected::reject_code()` can only return an error result if there is a
149                    // new error code on ICP that the CDK is not aware of. We map it to `SysFatal`
150                    // since none of the other error codes apply.
151                    // In particular, note that `RejectCode::SysUnknown` is only applicable to
152                    // inter-canister calls that used `ic0.call_with_best_effort_response`.
153                    code: e.reject_code().unwrap_or(RejectCode::SysFatal),
154                    message: e.reject_message().to_string(),
155                }
156            }
157            CallFailed::InsufficientLiquidCycleBalance(e) => {
158                IcError::InsufficientLiquidCycleBalance {
159                    available: e.available,
160                    required: e.required,
161                }
162            }
163        }
164    }
165}
166
167impl From<CandidDecodeFailed> for IcError {
168    fn from(err: CandidDecodeFailed) -> Self {
169        IcError::CandidDecodeFailed {
170            message: err.to_string(),
171        }
172    }
173}
174
175/// Runtime when interacting with a canister running on the Internet Computer.
176///
177/// # Examples
178///
179/// Call the `make_http_post_request` endpoint on the example [`http_canister`].
180/// ```rust
181/// # #[tokio::main]
182/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
183/// use candid::Principal;
184/// use ic_canister_runtime::{IcRuntime, Runtime, StubRuntime};
185///
186/// let runtime = IcRuntime::new();
187/// # let runtime = StubRuntime::new()
188/// #    .add_stub_response(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#);
189/// # let canister_id = Principal::anonymous();
190/// let http_request_result: String = runtime
191///     .update_call(canister_id, "make_http_post_request", (), 0)
192///     .await
193///     .expect("Call to `http_canister` failed");
194///
195/// assert!(http_request_result.contains("Hello, World!"));
196/// assert!(http_request_result.contains("\"X-Id\": \"42\""));
197/// # Ok(())
198/// # }
199/// ```
200///
201/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/
202#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
203pub struct IcRuntime {
204    allow_calls_when_stopping: bool,
205}
206
207impl IcRuntime {
208    /// Create a new instance of [`IcRuntime`].
209    pub fn new() -> Self {
210        Self::default()
211    }
212
213    /// Allow inter-canister calls when the canister is stopping.
214    ///
215    /// <div class="warning">
216    /// Allowing inter-canister calls when the canister making the calls is stopping
217    /// could prevent that canister from being stopped and therefore upgraded.
218    /// This is because the stopping state does not prevent the canister itself from issuing
219    /// new calls (see the specification on <a href="https://docs.internetcomputer.org/references/ic-interface-spec#ic-stop_canister">stop_canister</a>).
220    /// </div>
221    pub fn allow_calls_when_stopping(mut self, allow: bool) -> Self {
222        self.allow_calls_when_stopping = allow;
223        self
224    }
225
226    fn ensure_allowed_to_make_call(&self) -> Result<(), IcError> {
227        if !self.allow_calls_when_stopping {
228            use ic_cdk::api::CanisterStatusCode;
229
230            return match ic_cdk::api::canister_status() {
231                CanisterStatusCode::Running => Ok(()),
232                CanisterStatusCode::Stopping
233                | CanisterStatusCode::Stopped
234                | CanisterStatusCode::Unrecognized(_) => Err(IcError::CallPerformFailed),
235            };
236        }
237        Ok(())
238    }
239}
240
241#[async_trait]
242impl Runtime for IcRuntime {
243    async fn update_call<In, Out>(
244        &self,
245        id: Principal,
246        method: &str,
247        args: In,
248        cycles: u128,
249    ) -> Result<Out, IcError>
250    where
251        In: ArgumentEncoder + Send,
252        Out: CandidType + DeserializeOwned,
253    {
254        self.ensure_allowed_to_make_call()?;
255        Call::unbounded_wait(id, method)
256            .with_args(&args)
257            .with_cycles(cycles)
258            .await
259            .map_err(IcError::from)
260            .and_then(|response| response.candid::<Out>().map_err(IcError::from))
261    }
262
263    async fn query_call<In, Out>(
264        &self,
265        id: Principal,
266        method: &str,
267        args: In,
268    ) -> Result<Out, IcError>
269    where
270        In: ArgumentEncoder + Send,
271        Out: CandidType + DeserializeOwned,
272    {
273        self.ensure_allowed_to_make_call()?;
274        Call::unbounded_wait(id, method)
275            .with_args(&args)
276            .await
277            .map_err(IcError::from)
278            .and_then(|response| response.candid::<Out>().map_err(IcError::from))
279    }
280}