Skip to main content

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