canic_core/api/ic/
call.rs

1//! Public IC call façade with optional intent-based concurrency control.
2//!
3//! This module defines the stable, public API used by application code to make
4//! inter-canister calls. It deliberately exposes a *thin* surface:
5//!
6//! - argument encoding
7//! - cycle attachment
8//! - optional intent declaration
9//!
10//! It does NOT:
11//! - perform orchestration itself
12//! - expose intent internals
13//! - leak workflow or storage details
14//!
15//! If an intent is attached to a call, the actual multi-step behavior
16//! (reserve → call → commit/abort) is handled by the workflow layer.
17//!
18//! This separation keeps application code simple while ensuring correctness
19//! under concurrency.
20use crate::{
21    cdk::{
22        candid::CandidType,
23        types::{BoundedString128, Principal},
24    },
25    dto::error::Error,
26    workflow::ic::call::{
27        CallBuilder as WorkflowCallBuilder, CallResult as WorkflowCallResult, CallWorkflow,
28        IntentSpec as WorkflowIntentSpec,
29    },
30};
31use candid::utils::{ArgumentDecoder, ArgumentEncoder};
32use serde::de::DeserializeOwned;
33
34///
35/// Call
36///
37/// Entry point for constructing inter-canister calls.
38///
39/// `Call` itself has no state; it simply selects the wait semantics
40/// (bounded vs unbounded) and produces a `CallBuilder`.
41///
42/// Think of this as the *verb* (“make a call”), not the call itself.
43///
44
45pub struct Call;
46
47impl Call {
48    #[must_use]
49    pub fn bounded_wait(canister_id: impl Into<Principal>, method: &str) -> CallBuilder {
50        CallBuilder {
51            inner: CallWorkflow::bounded_wait(canister_id, method),
52        }
53    }
54
55    #[must_use]
56    pub fn unbounded_wait(canister_id: impl Into<Principal>, method: &str) -> CallBuilder {
57        CallBuilder {
58            inner: CallWorkflow::unbounded_wait(canister_id, method),
59        }
60    }
61}
62
63///
64/// IntentKey
65///
66/// Stable, bounded identifier for a contended resource.
67///
68/// An intent key names *what is being reserved*, not how the reservation
69/// is enforced. Keys are opaque strings with a fixed maximum length
70/// to ensure safe storage and indexing.
71///
72/// Examples:
73/// - "vendor:abc123:inventory"
74/// - "collection:xyz:mint"
75///
76
77pub struct IntentKey(BoundedString128);
78
79impl IntentKey {
80    pub fn try_new(value: impl Into<String>) -> Result<Self, Error> {
81        BoundedString128::try_new(value)
82            .map(Self)
83            .map_err(Error::invalid)
84    }
85
86    #[must_use]
87    pub fn as_str(&self) -> &str {
88        self.0.as_str()
89    }
90
91    #[must_use]
92    pub fn into_inner(self) -> BoundedString128 {
93        self.0
94    }
95}
96
97impl AsRef<str> for IntentKey {
98    fn as_ref(&self) -> &str {
99        self.0.as_str()
100    }
101}
102
103impl From<IntentKey> for BoundedString128 {
104    fn from(key: IntentKey) -> Self {
105        key.0
106    }
107}
108
109///
110/// IntentReservation
111///
112/// Declarative reservation attached to a call.
113///
114/// An intent expresses *preconditions* for executing a call, such as:
115/// - how much of a resource is required (`quantity`)
116/// - how long the reservation may remain pending (`ttl_secs`)
117/// - optional concurrency caps (`max_in_flight`)
118///
119/// Importantly:
120/// - An intent is **single-shot**
121/// - Failed intents are not reused
122/// - Retrying requires creating a new intent
123///
124/// The reservation itself is enforced by the workflow layer.
125///
126
127pub struct IntentReservation {
128    key: IntentKey,
129    quantity: u64,
130    ttl_secs: Option<u64>,
131    max_in_flight: Option<u64>,
132}
133
134impl IntentReservation {
135    #[must_use]
136    pub const fn new(key: IntentKey, quantity: u64) -> Self {
137        Self {
138            key,
139            quantity,
140            ttl_secs: None,
141            max_in_flight: None,
142        }
143    }
144
145    #[must_use]
146    pub const fn with_ttl_secs(mut self, ttl_secs: u64) -> Self {
147        self.ttl_secs = Some(ttl_secs);
148        self
149    }
150
151    #[must_use]
152    pub const fn with_max_in_flight(mut self, max_in_flight: u64) -> Self {
153        self.max_in_flight = Some(max_in_flight);
154        self
155    }
156
157    pub(crate) fn into_spec(self) -> WorkflowIntentSpec {
158        WorkflowIntentSpec::new(
159            self.key.into(),
160            self.quantity,
161            self.ttl_secs,
162            self.max_in_flight,
163        )
164    }
165}
166
167///
168/// CallBuilder (api)
169///
170
171pub struct CallBuilder {
172    inner: WorkflowCallBuilder,
173}
174
175impl CallBuilder {
176    // ---------- arguments ----------
177
178    /// Encode a single argument into Candid bytes (fallible).
179    pub fn with_arg<A>(self, arg: A) -> Result<Self, Error>
180    where
181        A: CandidType,
182    {
183        Ok(Self {
184            inner: self.inner.with_arg(arg).map_err(Error::from)?,
185        })
186    }
187
188    /// Encode multiple arguments into Candid bytes (fallible).
189    pub fn with_args<A>(self, args: A) -> Result<Self, Error>
190    where
191        A: ArgumentEncoder,
192    {
193        Ok(Self {
194            inner: self.inner.with_args(args).map_err(Error::from)?,
195        })
196    }
197
198    /// Use pre-encoded Candid arguments (no validation performed).
199    #[must_use]
200    pub fn with_raw_args(self, args: Vec<u8>) -> Self {
201        Self {
202            inner: self.inner.with_raw_args(args),
203        }
204    }
205
206    // ---------- cycles ----------
207
208    #[must_use]
209    pub fn with_cycles(self, cycles: u128) -> Self {
210        Self {
211            inner: self.inner.with_cycles(cycles),
212        }
213    }
214
215    // ---------- intent ----------
216
217    #[must_use]
218    pub fn with_intent(self, intent: IntentReservation) -> Self {
219        Self {
220            inner: self.inner.with_intent(intent.into_spec()),
221        }
222    }
223
224    // ---------- execution ----------
225
226    pub async fn execute(self) -> Result<CallResult, Error> {
227        Ok(CallResult {
228            inner: self.inner.execute().await.map_err(Error::from)?,
229        })
230    }
231}
232
233///
234/// CallResult
235///
236/// Stable wrapper around an inter-canister call response.
237///
238/// This type exists to:
239/// - decouple API consumers from infra response types
240/// - provide uniform decoding helpers
241/// - allow future extension without breaking callers
242///
243
244pub struct CallResult {
245    inner: WorkflowCallResult,
246}
247
248impl CallResult {
249    pub fn candid<R>(&self) -> Result<R, Error>
250    where
251        R: CandidType + DeserializeOwned,
252    {
253        self.inner.candid().map_err(Error::from)
254    }
255
256    pub fn candid_tuple<R>(&self) -> Result<R, Error>
257    where
258        R: for<'de> ArgumentDecoder<'de>,
259    {
260        self.inner.candid_tuple().map_err(Error::from)
261    }
262}