Skip to main content

ic_agent_canister_runtime/
lib.rs

1//! Library that implements the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime)
2//! crate's Runtime trait using [`ic-agent`](https://crates.io/crates/ic-agent).
3//! This can be useful when, e.g., contacting a canister via ingress messages instead of via another
4//! canister.
5
6use async_trait::async_trait;
7use candid::{decode_one, encode_args, utils::ArgumentEncoder, CandidType, Principal};
8use ic_agent::{Agent, AgentError};
9use ic_canister_runtime::{IcError, Runtime};
10use ic_error_types::RejectCode;
11use serde::de::DeserializeOwned;
12
13/// Runtime for interacting with a canister through an [`ic_agent::Agent`].
14/// This can be useful when, e.g., contacting a canister via ingress messages instead of via another
15/// canister.
16///
17///
18/// # Examples
19///
20/// Call the `make_http_post_request` endpoint on the example [`http_canister`].
21/// ```rust, no_run
22/// # #[allow(deref_nullptr)]
23/// # #[tokio::main]
24/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
25/// use ic_agent::agent::Agent;
26/// use ic_agent_canister_runtime::AgentRuntime;
27/// use ic_canister_runtime::Runtime;
28/// # use candid::Principal;
29///
30/// let agent = Agent::builder().build().expect("Failed to initialize agent");
31/// let runtime = AgentRuntime::new(&agent);
32/// # let canister_id = Principal::anonymous();
33/// let http_request_result: String = runtime
34///     .update_call(canister_id, "make_http_post_request", (), 0)
35///     .await
36///     .expect("Call to `http_canister` failed");
37///
38/// assert!(http_request_result.contains("Hello, World!"));
39/// assert!(http_request_result.contains("\"X-Id\": \"42\""));
40/// # Ok(())
41/// # }
42/// ```
43///
44/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/
45#[derive(Clone, Debug)]
46pub struct AgentRuntime<'a> {
47    agent: &'a Agent,
48}
49
50impl<'a> AgentRuntime<'a> {
51    /// Create a new [`AgentRuntime`] with the given [`Agent`].
52    pub fn new(agent: &'a Agent) -> Self {
53        Self { agent }
54    }
55}
56
57#[async_trait]
58impl Runtime for AgentRuntime<'_> {
59    async fn update_call<In, Out>(
60        &self,
61        id: Principal,
62        method: &str,
63        args: In,
64        _cycles: u128,
65    ) -> Result<Out, IcError>
66    where
67        In: ArgumentEncoder + Send,
68        Out: CandidType + DeserializeOwned,
69    {
70        self.agent
71            .update(&id, method)
72            .with_arg(encode_args(args).unwrap_or_else(panic_when_encode_fails))
73            .call_and_wait()
74            .await
75            .map_err(convert_agent_error)
76            .and_then(decode_agent_response)
77    }
78
79    async fn query_call<In, Out>(
80        &self,
81        id: Principal,
82        method: &str,
83        args: In,
84    ) -> Result<Out, IcError>
85    where
86        In: ArgumentEncoder + Send,
87        Out: CandidType + DeserializeOwned,
88    {
89        self.agent
90            .query(&id, method)
91            .with_arg(encode_args(args).unwrap_or_else(panic_when_encode_fails))
92            .call()
93            .await
94            .map_err(convert_agent_error)
95            .and_then(decode_agent_response)
96    }
97}
98
99fn decode_agent_response<Out>(result: Vec<u8>) -> Result<Out, IcError>
100where
101    Out: CandidType + DeserializeOwned,
102{
103    decode_one::<Out>(&result).map_err(|e| IcError::CandidDecodeFailed {
104        message: e.to_string(),
105    })
106}
107
108fn convert_agent_error(e: AgentError) -> IcError {
109    if let AgentError::CertifiedReject { ref reject, .. } = e {
110        if let Ok(code) = RejectCode::try_from(reject.reject_code as u64) {
111            return IcError::CallRejected {
112                code,
113                message: reject.reject_message.clone(),
114            };
115        }
116    }
117    IcError::CallRejected {
118        code: RejectCode::SysFatal,
119        message: e.to_string(),
120    }
121}
122
123fn panic_when_encode_fails(err: candid::error::Error) -> Vec<u8> {
124    panic!("failed to encode args: {err}")
125}