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/// Error returned by the Internet Computer when making an inter-canister call.
54#[derive(Error, Clone, Debug, PartialEq, Eq)]
55pub enum IcError {
56 /// The liquid cycle balance is insufficient to perform the call.
57 #[error("Insufficient liquid cycles balance, available: {available}, required: {required}")]
58 InsufficientLiquidCycleBalance {
59 /// The liquid cycle balance available in the canister.
60 available: u128,
61 /// The required cycles to perform the call.
62 required: u128,
63 },
64
65 /// The `ic0.call_perform` operation failed when performing the inter-canister call.
66 #[error("Inter-canister call perform failed")]
67 CallPerformFailed,
68
69 /// The inter-canister call is rejected.
70 #[error("Inter-canister call rejected: {code:?} - {message})")]
71 CallRejected {
72 /// Rejection code as specified [here](https://internetcomputer.org/docs/current/references/ic-interface-spec#reject-codes)
73 code: RejectCode,
74 /// Associated helper message.
75 message: String,
76 },
77
78 /// The response from the inter-canister call could not be decoded as Candid.
79 #[error("The inter-canister call response could not be decoded: {message}")]
80 CandidDecodeFailed {
81 /// The specific Candid error that occurred.
82 message: String,
83 },
84}
85
86impl From<CallFailed> for IcError {
87 fn from(err: CallFailed) -> Self {
88 match err {
89 CallFailed::CallPerformFailed(_) => IcError::CallPerformFailed,
90 CallFailed::CallRejected(e) => {
91 IcError::CallRejected {
92 // `CallRejected::reject_code()` can only return an error result if there is a
93 // new error code on ICP that the CDK is not aware of. We map it to `SysFatal`
94 // since none of the other error codes apply.
95 // In particular, note that `RejectCode::SysUnknown` is only applicable to
96 // inter-canister calls that used `ic0.call_with_best_effort_response`.
97 code: e.reject_code().unwrap_or(RejectCode::SysFatal),
98 message: e.reject_message().to_string(),
99 }
100 }
101 CallFailed::InsufficientLiquidCycleBalance(e) => {
102 IcError::InsufficientLiquidCycleBalance {
103 available: e.available,
104 required: e.required,
105 }
106 }
107 }
108 }
109}
110
111impl From<CandidDecodeFailed> for IcError {
112 fn from(err: CandidDecodeFailed) -> Self {
113 IcError::CandidDecodeFailed {
114 message: err.to_string(),
115 }
116 }
117}
118
119/// Runtime when interacting with a canister running on the Internet Computer.
120///
121/// # Examples
122///
123/// Call the `make_http_post_request` endpoint on the example [`http_canister`].
124/// ```rust
125/// # #[tokio::main]
126/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
127/// use candid::Principal;
128/// use ic_canister_runtime::{IcRuntime, Runtime, StubRuntime};
129///
130/// let runtime = IcRuntime::new();
131/// # let runtime = StubRuntime::new()
132/// # .add_stub_response(r#"{"data": "Hello, World!", "headers": {"X-Id": "42"}}"#);
133/// # let canister_id = Principal::anonymous();
134/// let http_request_result: String = runtime
135/// .update_call(canister_id, "make_http_post_request", (), 0)
136/// .await
137/// .expect("Call to `http_canister` failed");
138///
139/// assert!(http_request_result.contains("Hello, World!"));
140/// assert!(http_request_result.contains("\"X-Id\": \"42\""));
141/// # Ok(())
142/// # }
143/// ```
144///
145/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/
146#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
147pub struct IcRuntime {
148 _private: (),
149}
150
151impl IcRuntime {
152 /// Create a new instance of [`IcRuntime`].
153 pub fn new() -> Self {
154 Self::default()
155 }
156}
157
158#[async_trait]
159impl Runtime for IcRuntime {
160 async fn update_call<In, Out>(
161 &self,
162 id: Principal,
163 method: &str,
164 args: In,
165 cycles: u128,
166 ) -> Result<Out, IcError>
167 where
168 In: ArgumentEncoder + Send,
169 Out: CandidType + DeserializeOwned,
170 {
171 Call::unbounded_wait(id, method)
172 .with_args(&args)
173 .with_cycles(cycles)
174 .await
175 .map_err(IcError::from)
176 .and_then(|response| response.candid::<Out>().map_err(IcError::from))
177 }
178
179 async fn query_call<In, Out>(
180 &self,
181 id: Principal,
182 method: &str,
183 args: In,
184 ) -> Result<Out, IcError>
185 where
186 In: ArgumentEncoder + Send,
187 Out: CandidType + DeserializeOwned,
188 {
189 Call::unbounded_wait(id, method)
190 .with_args(&args)
191 .await
192 .map_err(IcError::from)
193 .and_then(|response| response.candid::<Out>().map_err(IcError::from))
194 }
195}