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/// 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}