1use crate::{
21 cdk::{
22 candid::{CandidType, encode_one},
23 types::{BoundedString128, Principal},
24 },
25 dto::{
26 auth::{
27 CanicInternalCallEnvelopeV1, CanicInternalCallHeaderV1, InternalInvocationProofRequest,
28 SignedInternalInvocationProofV1,
29 },
30 error::Error,
31 },
32 ids::CanisterRole,
33 ops::{config::ConfigOps, ic::IcOps, runtime::env::EnvOps},
34 workflow::ic::call::{
35 CallBuilder as WorkflowCallBuilder, CallResult as WorkflowCallResult, CallWorkflow,
36 IntentSpec as WorkflowIntentSpec,
37 },
38};
39use candid::{
40 encode_args,
41 utils::{ArgumentDecoder, ArgumentEncoder},
42};
43use serde::de::DeserializeOwned;
44use std::borrow::Cow;
45
46const DEFAULT_INTERNAL_CALL_PROOF_TTL_SECS: u64 = 120;
47
48pub struct Call;
60
61impl Call {
62 #[must_use]
63 pub fn bounded_wait(canister_id: impl Into<Principal>, method: &str) -> CallBuilder<'static> {
64 CallBuilder {
65 inner: CallWorkflow::bounded_wait(canister_id, method),
66 }
67 }
68
69 #[must_use]
70 pub fn unbounded_wait(canister_id: impl Into<Principal>, method: &str) -> CallBuilder<'static> {
71 CallBuilder {
72 inner: CallWorkflow::unbounded_wait(canister_id, method),
73 }
74 }
75}
76
77pub struct CanicCall;
89
90impl CanicCall {
91 #[must_use]
92 pub fn bounded_wait(
93 canister_id: impl Into<Principal>,
94 method: &str,
95 ) -> CanicCallBuilder<'static> {
96 CanicCallBuilder::new(WaitMode::Bounded, canister_id.into(), method)
97 }
98
99 #[must_use]
100 pub fn unbounded_wait(
101 canister_id: impl Into<Principal>,
102 method: &str,
103 ) -> CanicCallBuilder<'static> {
104 CanicCallBuilder::new(WaitMode::Unbounded, canister_id.into(), method)
105 }
106}
107
108pub struct CanicCallBuilder<'a> {
113 wait: WaitMode,
114 canister_id: Principal,
115 method: String,
116 caller_role: Option<CanisterRole>,
117 ttl_secs: Option<u64>,
118 cycles: u128,
119 args: Cow<'a, [u8]>,
120}
121
122impl CanicCallBuilder<'_> {
123 fn new(wait: WaitMode, canister_id: Principal, method: &str) -> Self {
124 Self {
125 wait,
126 canister_id,
127 method: method.to_string(),
128 caller_role: None,
129 ttl_secs: None,
130 cycles: 0,
131 args: Cow::Owned(encode_args(()).expect("empty Candid tuple must encode")),
132 }
133 }
134
135 #[must_use]
136 pub fn with_caller_role(mut self, role: CanisterRole) -> Self {
137 self.caller_role = Some(role);
138 self
139 }
140
141 #[must_use]
142 pub const fn with_proof_ttl_secs(mut self, ttl_secs: u64) -> Self {
143 self.ttl_secs = Some(ttl_secs);
144 self
145 }
146
147 pub fn with_arg<A>(mut self, arg: A) -> Result<Self, Error>
148 where
149 A: CandidType,
150 {
151 self.args = Cow::Owned(encode_one(arg).map_err(|err| Error::invalid(err.to_string()))?);
152 Ok(self)
153 }
154
155 pub fn with_args<A>(mut self, args: A) -> Result<Self, Error>
156 where
157 A: ArgumentEncoder,
158 {
159 self.args = Cow::Owned(encode_args(args).map_err(|err| Error::invalid(err.to_string()))?);
160 Ok(self)
161 }
162
163 #[must_use]
164 pub fn with_raw_args<'b>(self, args: impl Into<Cow<'b, [u8]>>) -> CanicCallBuilder<'b> {
165 CanicCallBuilder {
166 wait: self.wait,
167 canister_id: self.canister_id,
168 method: self.method,
169 caller_role: self.caller_role,
170 ttl_secs: self.ttl_secs,
171 cycles: self.cycles,
172 args: args.into(),
173 }
174 }
175
176 #[must_use]
177 pub const fn with_cycles(mut self, cycles: u128) -> Self {
178 self.cycles = cycles;
179 self
180 }
181
182 pub async fn execute(self) -> Result<CallResult, Error> {
183 let ttl_secs = self.proof_ttl_secs()?;
184 let role = self
185 .caller_role
186 .ok_or_else(|| Error::invalid("CanicCall requires with_caller_role(...)"))?;
187 let proof = crate::api::auth::AuthApi::request_internal_invocation_proof(
188 InternalInvocationProofRequest {
189 subject: IcOps::canister_self(),
190 role,
191 subnet_id: EnvOps::subnet_pid().ok(),
192 audience: self.canister_id,
193 audience_method: self.method.clone(),
194 ttl_secs,
195 metadata: None,
196 },
197 )
198 .await?;
199
200 let envelope = build_internal_call_envelope(
201 self.canister_id,
202 &self.method,
203 proof,
204 self.args.into_owned(),
205 );
206 let call = match self.wait {
207 WaitMode::Bounded => Call::bounded_wait(self.canister_id, &self.method),
208 WaitMode::Unbounded => Call::unbounded_wait(self.canister_id, &self.method),
209 }
210 .with_cycles(self.cycles)
211 .with_arg(envelope)?;
212
213 call.execute().await
214 }
215
216 fn proof_ttl_secs(&self) -> Result<u64, Error> {
217 let requested = self
218 .ttl_secs
219 .unwrap_or(DEFAULT_INTERNAL_CALL_PROOF_TTL_SECS);
220 let max = ConfigOps::role_attestation_config()
221 .map_err(Error::from)?
222 .max_ttl_secs;
223 Ok(requested.min(max))
224 }
225}
226
227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
228enum WaitMode {
229 Bounded,
230 Unbounded,
231}
232
233fn build_internal_call_envelope(
234 target_canister: Principal,
235 target_method: &str,
236 proof: SignedInternalInvocationProofV1,
237 args: Vec<u8>,
238) -> CanicInternalCallEnvelopeV1 {
239 CanicInternalCallEnvelopeV1 {
240 version: 1,
241 header: CanicInternalCallHeaderV1 {
242 target_canister,
243 target_method: target_method.to_string(),
244 },
245 proof,
246 args,
247 }
248}
249
250pub struct IntentKey(BoundedString128);
265
266impl IntentKey {
267 pub fn try_new(value: impl Into<String>) -> Result<Self, Error> {
268 BoundedString128::try_new(value)
269 .map(Self)
270 .map_err(Error::invalid)
271 }
272
273 #[must_use]
274 pub fn as_str(&self) -> &str {
275 self.0.as_str()
276 }
277
278 #[must_use]
279 pub fn into_inner(self) -> BoundedString128 {
280 self.0
281 }
282}
283
284impl AsRef<str> for IntentKey {
285 fn as_ref(&self) -> &str {
286 self.0.as_str()
287 }
288}
289
290impl From<IntentKey> for BoundedString128 {
291 fn from(key: IntentKey) -> Self {
292 key.0
293 }
294}
295
296pub struct IntentReservation {
315 key: IntentKey,
316 quantity: u64,
317 ttl_secs: Option<u64>,
318 max_in_flight: Option<u64>,
319}
320
321impl IntentReservation {
322 #[must_use]
323 pub const fn new(key: IntentKey, quantity: u64) -> Self {
324 Self {
325 key,
326 quantity,
327 ttl_secs: None,
328 max_in_flight: None,
329 }
330 }
331
332 #[must_use]
333 pub const fn with_ttl_secs(mut self, ttl_secs: u64) -> Self {
334 self.ttl_secs = Some(ttl_secs);
335 self
336 }
337
338 #[must_use]
339 pub const fn with_max_in_flight(mut self, max_in_flight: u64) -> Self {
340 self.max_in_flight = Some(max_in_flight);
341 self
342 }
343
344 pub(crate) fn into_spec(self) -> WorkflowIntentSpec {
345 WorkflowIntentSpec::new(
346 self.key.into(),
347 self.quantity,
348 self.ttl_secs,
349 self.max_in_flight,
350 )
351 }
352}
353
354pub struct CallBuilder<'a> {
359 inner: WorkflowCallBuilder<'a>,
360}
361
362impl CallBuilder<'_> {
363 pub fn with_arg<A>(self, arg: A) -> Result<Self, Error>
367 where
368 A: CandidType,
369 {
370 Ok(Self {
371 inner: self.inner.with_arg(arg).map_err(Error::from)?,
372 })
373 }
374
375 pub fn with_args<A>(self, args: A) -> Result<Self, Error>
377 where
378 A: ArgumentEncoder,
379 {
380 Ok(Self {
381 inner: self.inner.with_args(args).map_err(Error::from)?,
382 })
383 }
384
385 #[must_use]
387 pub fn with_raw_args<'b>(self, args: impl Into<Cow<'b, [u8]>>) -> CallBuilder<'b> {
388 CallBuilder {
389 inner: self.inner.with_raw_args(args),
390 }
391 }
392
393 #[must_use]
396 pub fn with_cycles(self, cycles: u128) -> Self {
397 Self {
398 inner: self.inner.with_cycles(cycles),
399 }
400 }
401
402 #[must_use]
405 pub fn with_intent(self, intent: IntentReservation) -> Self {
406 Self {
407 inner: self.inner.with_intent(intent.into_spec()),
408 }
409 }
410
411 pub async fn execute(self) -> Result<CallResult, Error> {
414 Ok(CallResult {
415 inner: self.inner.execute().await.map_err(Error::from)?,
416 })
417 }
418}
419
420pub struct CallResult {
432 inner: WorkflowCallResult,
433}
434
435impl CallResult {
436 pub fn candid<R>(&self) -> Result<R, Error>
437 where
438 R: CandidType + DeserializeOwned,
439 {
440 self.inner.candid().map_err(Error::from)
441 }
442
443 pub fn candid_tuple<R>(&self) -> Result<R, Error>
444 where
445 R: for<'de> ArgumentDecoder<'de>,
446 {
447 self.inner.candid_tuple().map_err(Error::from)
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::dto::auth::{InternalInvocationProofPayloadV1, SignedInternalInvocationProofV1};
455 use candid::decode_args;
456
457 fn p(id: u8) -> Principal {
458 Principal::from_slice(&[id; 29])
459 }
460
461 fn proof() -> SignedInternalInvocationProofV1 {
462 SignedInternalInvocationProofV1 {
463 payload: InternalInvocationProofPayloadV1 {
464 subject: p(1),
465 role: CanisterRole::new("project_hub"),
466 subnet_id: None,
467 audience: p(2),
468 audience_method: "system_add_project_to_user".to_string(),
469 issued_at: 10,
470 expires_at: 20,
471 epoch: 3,
472 },
473 signature: vec![1, 2, 3],
474 key_id: 1,
475 }
476 }
477
478 #[test]
479 fn canic_call_envelope_binds_target_method_and_original_args() {
480 let args = encode_args((7_u64, "project")).expect("args encode");
481 let envelope =
482 build_internal_call_envelope(p(2), "system_add_project_to_user", proof(), args);
483
484 assert_eq!(envelope.version, 1);
485 assert_eq!(envelope.header.target_canister, p(2));
486 assert_eq!(envelope.header.target_method, "system_add_project_to_user");
487 assert_eq!(
488 envelope.proof.payload.audience_method,
489 "system_add_project_to_user"
490 );
491
492 let decoded: (u64, String) = decode_args(&envelope.args).expect("decode original args");
493 assert_eq!(decoded, (7, "project".to_string()));
494 }
495
496 #[test]
497 fn canic_call_builder_records_role_and_raw_args() {
498 let raw = vec![9_u8, 8, 7];
499 let builder = CanicCall::unbounded_wait(p(3), "target")
500 .with_caller_role(CanisterRole::new("project_hub"))
501 .with_proof_ttl_secs(30)
502 .with_cycles(10)
503 .with_raw_args(raw.clone());
504
505 assert_eq!(builder.wait, WaitMode::Unbounded);
506 assert_eq!(builder.canister_id, p(3));
507 assert_eq!(builder.method, "target");
508 assert_eq!(builder.caller_role, Some(CanisterRole::new("project_hub")));
509 assert_eq!(builder.ttl_secs, Some(30));
510 assert_eq!(builder.cycles, 10);
511 assert_eq!(builder.args.as_ref(), raw.as_slice());
512 }
513}