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    #[must_use]
179    pub fn with_arg<A>(self, arg: A) -> Self
180    where
181        A: CandidType,
182    {
183        Self {
184            inner: self.inner.with_arg(arg),
185        }
186    }
187
188    #[must_use]
189    pub fn with_args<A>(self, args: A) -> Self
190    where
191        A: ArgumentEncoder,
192    {
193        Self {
194            inner: self.inner.with_args(args),
195        }
196    }
197
198    pub fn try_with_arg<A>(self, arg: A) -> Result<Self, Error>
199    where
200        A: CandidType,
201    {
202        Ok(Self {
203            inner: self.inner.try_with_arg(arg).map_err(Error::from)?,
204        })
205    }
206
207    pub fn try_with_args<A>(self, args: A) -> Result<Self, Error>
208    where
209        A: ArgumentEncoder,
210    {
211        Ok(Self {
212            inner: self.inner.try_with_args(args).map_err(Error::from)?,
213        })
214    }
215
216    // ---------- cycles ----------
217
218    #[must_use]
219    pub fn with_cycles(self, cycles: u128) -> Self {
220        Self {
221            inner: self.inner.with_cycles(cycles),
222        }
223    }
224
225    // ---------- intent ----------
226
227    #[must_use]
228    pub fn with_intent(self, intent: IntentReservation) -> Self {
229        Self {
230            inner: self.inner.with_intent(intent.into_spec()),
231        }
232    }
233
234    // ---------- execution ----------
235
236    pub async fn execute(self) -> Result<CallResult, Error> {
237        Ok(CallResult {
238            inner: self.inner.execute().await.map_err(Error::from)?,
239        })
240    }
241}
242
243///
244/// CallResult
245///
246/// Stable wrapper around an inter-canister call response.
247///
248/// This type exists to:
249/// - decouple API consumers from infra response types
250/// - provide uniform decoding helpers
251/// - allow future extension without breaking callers
252///
253
254pub struct CallResult {
255    inner: WorkflowCallResult,
256}
257
258impl CallResult {
259    pub fn candid<R>(&self) -> Result<R, Error>
260    where
261        R: CandidType + DeserializeOwned,
262    {
263        self.inner.candid().map_err(Error::from)
264    }
265
266    pub fn candid_tuple<R>(&self) -> Result<R, Error>
267    where
268        R: for<'de> ArgumentDecoder<'de>,
269    {
270        self.inner.candid_tuple().map_err(Error::from)
271    }
272}