bity_ic_canister_client/lib.rs
1//! Client utilities for Internet Computer canister interactions.
2//!
3//! This module provides low-level utilities for making cross-canister calls (C2C)
4//! with support for different serialization formats and cycle payments.
5//!
6//! # Features
7//! - Cross-canister calls with custom serialization/deserialization
8//! - Support for cycle payments in C2C calls
9//! - Raw C2C call functionality with detailed error handling
10//! - Integration with tracing for debugging and monitoring
11//!
12//! # Examples
13//! ```
14//! use bity_ic_canister_client::make_c2c_call;
15//! use candid::{encode_one, decode_one};
16//!
17//! async fn transfer(
18//! canister_id: Principal,
19//! args: &TransferArgs,
20//! ) -> CallResult<TransferResponse> {
21//! make_c2c_call(
22//! canister_id,
23//! "transfer",
24//! args,
25//! encode_one,
26//! |r| decode_one(r),
27//! )
28//! .await
29//! }
30//! ```
31
32pub use anyhow::{Context, Result};
33use candid::Principal;
34use ic_cdk::call::CallFailed;
35use std::fmt::Debug;
36
37pub mod canister_client_macros;
38/// Makes a cross-canister call with custom serialization and deserialization.
39///
40/// This function handles the complete flow of a cross-canister call, including:
41/// - Serialization of arguments
42/// - Making the actual call
43/// - Deserialization of the response
44///
45/// # Type Parameters
46/// * `A` - The type of the arguments
47/// * `R` - The type of the response
48/// * `S` - The type of the serializer function
49/// * `D` - The type of the deserializer function
50/// * `SError` - The error type of the serializer
51/// * `DError` - The error type of the deserializer
52///
53/// # Arguments
54/// * `canister_id` - The ID of the target canister
55/// * `method_name` - The name of the method to call
56/// * `args` - The arguments to pass to the method
57/// * `serializer` - Function to serialize the arguments
58/// * `deserializer` - Function to deserialize the response
59///
60/// # Returns
61/// A `CallResult` containing either the deserialized response or an error.
62///
63/// # Example
64/// ```
65/// use bity_ic_canister_client::make_c2c_call;
66/// use candid::{encode_one, decode_one};
67///
68/// async fn example(canister_id: Principal, args: &MyArgs) -> CallResult<MyResponse> {
69/// make_c2c_call(
70/// canister_id,
71/// "my_method",
72/// args,
73/// encode_one,
74/// |r| decode_one(r),
75/// )
76/// .await
77/// }
78/// ```
79pub async fn make_c2c_call<A, R, S, D, SError: Debug, DError: Debug>(
80 canister_id: Principal,
81 method_name: &str,
82 args: A,
83 serializer: S,
84 deserializer: D,
85) -> Result<R>
86where
87 S: Fn(A) -> Result<Vec<u8>, SError>,
88 D: Fn(&[u8]) -> Result<R, DError>,
89{
90 let payload_bytes =
91 serializer(args).map_err(|e| anyhow::anyhow!("Serialization error: {:?}", e))?;
92
93 let response_bytes = make_c2c_call_raw(canister_id, method_name, &payload_bytes, 0, None)
94 .await
95 .context("Cross-canister call failed")?;
96
97 deserializer(&response_bytes).map_err(|e| anyhow::anyhow!("Deserialization error: {:?}", e))
98}
99
100/// Makes a cross-canister call with cycle payment and custom serialization.
101///
102/// This function is similar to `make_c2c_call` but includes cycle payment support.
103/// It allows specifying the number of cycles to be transferred with the call.
104///
105/// # Type Parameters
106/// * `A` - The type of the arguments
107/// * `R` - The type of the response
108/// * `S` - The type of the serializer function
109/// * `D` - The type of the deserializer function
110/// * `SError` - The error type of the serializer
111/// * `DError` - The error type of the deserializer
112///
113/// # Arguments
114/// * `canister_id` - The ID of the target canister
115/// * `method_name` - The name of the method to call
116/// * `args` - The arguments to pass to the method
117/// * `serializer` - Function to serialize the arguments
118/// * `deserializer` - Function to deserialize the response
119/// * `cycles` - The number of cycles to transfer with the call
120///
121/// # Returns
122/// A `CallResult` containing either the deserialized response or an error.
123///
124/// # Example
125/// ```
126/// use bity_ic_canister_client::make_c2c_call_with_payment;
127/// use candid::{encode_one, decode_one};
128///
129/// async fn example(canister_id: Principal, args: &MyArgs, cycles: u128) -> CallResult<MyResponse> {
130/// make_c2c_call_with_payment(
131/// canister_id,
132/// "my_method",
133/// args,
134/// encode_one,
135/// |r| decode_one(r),
136/// cycles,
137/// )
138/// .await
139/// }
140/// ```
141pub async fn make_c2c_call_with_payment<A, R, S, D, SError: Debug, DError: Debug>(
142 canister_id: Principal,
143 method_name: &str,
144 args: A,
145 serializer: S,
146 deserializer: D,
147 cycles: u128,
148) -> Result<R>
149where
150 S: Fn(A) -> Result<Vec<u8>, SError>,
151 D: Fn(&[u8]) -> Result<R, DError>,
152{
153 let payload_bytes =
154 serializer(args).map_err(|e| anyhow::anyhow!("Serialization error: {:?}", e))?;
155
156 let response_bytes = make_c2c_call_raw(canister_id, method_name, &payload_bytes, cycles, None)
157 .await
158 .context("Cross-canister call with payment failed")?;
159
160 deserializer(&response_bytes).map_err(|e| anyhow::anyhow!("Deserialization error: {:?}", e))
161}
162
163/// Makes a raw cross-canister call with byte-level control.
164///
165/// This is the lowest-level function for making cross-canister calls. It handles
166/// the actual call to the Internet Computer and includes tracing for debugging.
167///
168/// # Arguments
169/// * `canister_id` - The ID of the target canister
170/// * `method_name` - The name of the method to call
171/// * `payload_bytes` - The raw bytes to send as the payload
172/// * `cycles` - The number of cycles to transfer with the call
173///
174/// # Returns
175/// A `CallResult` containing either the raw response bytes or an error.
176///
177/// # Example
178/// ```
179/// use bity_ic_canister_client::make_c2c_call_raw;
180///
181/// async fn example(canister_id: Principal, payload: &[u8]) -> CallResult<Vec<u8>> {
182/// make_c2c_call_raw(canister_id, "my_method", payload, 0).await
183/// }
184/// ```
185
186pub async fn make_c2c_call_raw(
187 canister_id: Principal,
188 method_name: &str,
189 payload_bytes: &[u8],
190 cycles: u128,
191 timeout_seconds: Option<u32>,
192) -> Result<Vec<u8>, CallFailed> {
193 let call = if let Some(timeout_seconds) = timeout_seconds {
194 ic_cdk::call::Call::bounded_wait(canister_id, method_name).change_timeout(timeout_seconds)
195 } else {
196 ic_cdk::call::Call::unbounded_wait(canister_id, method_name)
197 };
198
199 let response = call.with_raw_args(payload_bytes).with_cycles(cycles).await;
200
201 match response {
202 Ok(response_bytes) => {
203 tracing::trace!(method_name, %canister_id, "Completed c2c call successfully");
204 Ok(response_bytes.into_bytes())
205 }
206 Err(error) => Err(error),
207 }
208}