Skip to main content

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}