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}