ic-platform 0.6.14

Controlled Candid and IC runtime dependency boundary
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
// Imported from ic-cdk 0.20.3; Apache-2.0. See ../NOTICE.md.
//! Raw inter-canister calls with the imported CDK 0.20.3 callback state machine.
//!
//! Callers supply explicit Wire frames or separately encoded external Candid.
//! The registry executor remains shared; this module performs no codec work.

use super::api::{cost_call, msg_arg_data, msg_reject_code, msg_reject_msg};
use super::{api::trap, futures::is_recovering_from_trap};
use crate::Principal;
use ic_cdk_executor::{MethodHandle, TaskHandle};
use std::borrow::Cow;
use std::future::IntoFuture;
use std::mem;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use std::task::{Context, Poll, Waker};
use thiserror::Error;

pub use crate::cdk::reject_code::RejectCode;

/// Consumer-selected response waiting; this does not configure retries.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WaitPolicy {
    /// Request a best-effort response with the supplied timeout in seconds.
    /// Zero remains bounded; the IC applies its own maximum.
    Bounded {
        /// Requested timeout, forwarded unchanged to the IC.
        timeout_seconds: u32,
    },
    /// Wait without a best-effort response timeout.
    Unbounded,
}

/// Raw inter-canister call builder.
///
/// Bounded waits default to 300 seconds. Constructors start with empty transport
/// bytes; supply an explicit Wire request or external Candid arguments. Callers
/// retain timeout, cycle and retry policy. Callback traps retain the imported
/// task-cancellation behavior of the shared executor.
#[derive(Debug, Clone)]
pub struct Call<'m, 'a> {
    canister_id: Principal,
    method: &'m str,
    cycles: u128,
    timeout_seconds: Option<u32>,
    encoded_args: Cow<'a, [u8]>,
}

// Constructors
impl<'m> Call<'m, '_> {
    /// Constructs a [`Call`] which will **boundedly** wait for response.
    ///
    /// # Note
    ///
    /// The bounded waiting is set with a default 300-second timeout.
    /// It aligns with the `MAX_CALL_TIMEOUT` constant in the current IC implementation.
    /// The timeout can be changed using the [`with_wait_policy`][Self::with_wait_policy] method.
    ///
    /// To unboundedly wait for response, use the [`Call::unbounded_wait`] constructor instead.
    #[must_use]
    pub fn bounded_wait(canister_id: Principal, method: &'m str) -> Self {
        Self {
            canister_id,
            method,
            cycles: 0,
            // Default to 300-second timeout.
            timeout_seconds: Some(300),
            // Raw transport starts empty; supply an explicit Wire or external Candid payload.
            encoded_args: Cow::Borrowed(&[]),
        }
    }

    /// Constructs a [`Call`] which will **unboundedly** wait for response.
    ///
    /// To boundedly wait for response, use the  [`Call::bounded_wait`] constructor instead.
    #[must_use]
    pub fn unbounded_wait(canister_id: Principal, method: &'m str) -> Self {
        Self {
            canister_id,
            method,
            cycles: 0,
            timeout_seconds: None,
            // Raw transport starts empty; supply an explicit Wire or external Candid payload.
            encoded_args: Cow::Borrowed(&[]),
        }
    }
}

// Configuration
impl<'a> Call<'_, 'a> {
    /// Sets the arguments for the call as raw bytes.
    ///
    /// # Note
    ///
    /// This method just borrows the bytes, so it is useful when making multiple calls with the same argument data.
    ///
    /// The `Call` object will be tied to the lifetime of the argument bytes,
    /// which may prevent storing the call in collections or returning it from functions
    /// if the arguments don't live long enough.
    ///
    /// For cases where you need to transfer ownership of the arguments bytes consider using [`Self::take_raw_args`] instead.
    #[must_use]
    pub fn with_raw_args(self, raw_args: &'a [u8]) -> Self {
        Self {
            encoded_args: Cow::Borrowed(raw_args),
            ..self
        }
    }

    /// Sets the arguments for the call as raw bytes and consumes the bytes.
    ///
    /// # Note
    ///
    /// This method takes ownership of the arguments bytes, so it is useful
    /// when you want to store the call in collections or return a `Call` from functions.
    ///
    /// For cases where you want to make multiple calls with the same argument data,
    /// consider using [`Self::with_raw_args`] instead to avoid unnecessary cloning.
    #[must_use]
    pub fn take_raw_args(self, raw_args: Vec<u8>) -> Self {
        Self {
            encoded_args: Cow::Owned(raw_args),
            ..self
        }
    }

    /// Sets the cycles payment for the call.
    ///
    /// # Note
    ///
    /// The behavior of this method when invoked multiple times is as follows:
    /// - Overrides any previously set cycle value
    /// - Last invocation determines the final cycles amount
    /// - Does not accumulate cycles across multiple invocations
    #[must_use]
    pub fn with_cycles(mut self, cycles: u128) -> Self {
        self.cycles = cycles;
        self
    }

    /// Selects bounded or unbounded response waiting explicitly.
    ///
    /// Replaces the previous wait policy without changing payload, cycles or retry
    /// behavior. Values are forwarded unchanged to the IC; zero is bounded, not
    /// unbounded. The IC applies its own maximum timeout.
    #[must_use]
    pub fn with_wait_policy(mut self, policy: WaitPolicy) -> Self {
        self.timeout_seconds = match policy {
            WaitPolicy::Bounded { timeout_seconds } => Some(timeout_seconds),
            WaitPolicy::Unbounded => None,
        };
        self
    }

    /// Returns the amount of cycles a canister needs to be above the freezing threshold in order to
    /// successfully perform this call. Takes into account the attached cycles ([`with_cycles`](Self::with_cycles))
    /// as well as
    /// - the method name byte length
    /// - the payload length
    /// - the cost of transmitting the request
    /// - the cost for the reservation of response transmission (may be partially refunded)
    /// - the cost for the reservation of callback execution (may be partially refunded).
    #[must_use]
    pub fn get_cost(&self) -> u128 {
        self.cycles.saturating_add(cost_call(
            self.method.len() as u64,
            self.encoded_args.len() as u64,
        ))
    }
}

/// Response of a successful call.
#[derive(Debug)]
pub struct Response(Vec<u8>);

impl Response {
    /// Gets the raw bytes of the response.
    pub fn into_bytes(self) -> Vec<u8> {
        self.0
    }
}

impl PartialEq<&[u8]> for Response {
    fn eq(&self, other: &&[u8]) -> bool {
        self.0 == *other
    }
}

impl PartialEq<Vec<u8>> for Response {
    fn eq(&self, other: &Vec<u8>) -> bool {
        self.0 == *other
    }
}

impl PartialEq for Response {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

impl std::ops::Deref for Response {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl AsRef<[u8]> for Response {
    fn as_ref(&self) -> &[u8] {
        &self.0
    }
}

impl std::borrow::Borrow<[u8]> for Response {
    fn borrow(&self) -> &[u8] {
        &self.0
    }
}

// Errors ---------------------------------------------------------------------

/// The error type when awaiting a `CallFuture`.
///
/// Only transport failures belong here; codecs retain their own typed errors.
#[derive(Error, Debug, Clone)]
pub enum CallFailed {
    /// The liquid cycle balance is insufficient to perform the call.
    #[error(transparent)]
    InsufficientLiquidCycleBalance(#[from] InsufficientLiquidCycleBalance),

    /// The `ic0.call_perform` operation failed.
    #[error(transparent)]
    CallPerformFailed(#[from] CallPerformFailed),

    /// The inter-canister call is rejected.
    #[error(transparent)]
    CallRejected(#[from] CallRejected),
}

/// The error type of [`Call::oneway`].
///
/// This encapsulates all possible errors that can occur when sending a oneway call.
/// Therefore, it includes the [`InsufficientLiquidCycleBalance`] and [`CallPerformFailed`] variants.
#[derive(Error, Debug, Clone)]
pub enum OnewayError {
    /// The liquid cycle balance is insufficient to perform the call.
    #[error(transparent)]
    InsufficientLiquidCycleBalance(#[from] InsufficientLiquidCycleBalance),
    /// The `ic0.call_perform` operation failed.
    #[error(transparent)]
    CallPerformFailed(#[from] CallPerformFailed),
}

/// Represents an error that occurs when the liquid cycle balance is insufficient to perform the call.
///
/// The liquid cycle balance is determined by [`canister_liquid_cycle_balance`](super::api::canister_liquid_cycle_balance).
/// The cost of the call is determined by [`Call::get_cost`].
///
/// The call won't be performed if the former is less than the latter.
#[derive(Error, Debug, Clone)]
#[error("insufficient liquid cycles balance, available: {available}, required: {required}")]
pub struct InsufficientLiquidCycleBalance {
    /// The liquid cycle balance available in the canister.
    pub available: u128,
    /// The required cycles to perform the call.
    pub required: u128,
}

/// Represents an error that occurs when the `ic0.call_perform` operation fails.
///
/// This error type indicates that the underlying `ic0.call_perform` operation
/// returned a non-zero code, signaling a failure.
#[derive(Error, Debug, Clone)]
#[error("call perform failed")]
pub struct CallPerformFailed;

/// Represents an error that occurs when an inter-canister call is rejected.
///
/// The [`reject_code`][`Self::reject_code`] and [`reject_message`][`Self::reject_message`]
/// are exposed to provide details of the rejection.
///
/// This is wrapped by the [`CallFailed::CallRejected`] variant.
#[derive(Error, Debug, Clone)]
#[error("call rejected: {raw_reject_code} - {reject_message}")]
pub struct CallRejected {
    /// All fields are private so we will be able to change the implementation without breaking the API.
    /// Once we have `ic0.msg_error_code` system API, we will only store the `error_code` in this struct.
    /// It will still be possible to get the [`RejectCode`] using the public getter,
    /// because every `error_code` can map to a [`RejectCode`].
    raw_reject_code: u32,
    reject_message: String,
}

/// The error type for when an unrecognized reject code is encountered.
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[error("unrecognized reject code: {0}")]
pub struct UnrecognizedRejectCode(u32);

impl CallRejected {
    /// Constructs a [`CallRejected`] instance with the reject code and message.
    ///
    /// # Note
    ///
    /// This constructor is primarily intended for testing scenarios where you need to simulate
    /// rejected inter-canister calls. In production code, instances of this error are typically
    /// created by the system when actual rejections occur during inter-canister communication.
    /// Use this constructor with caution outside of test environments.
    pub fn with_rejection(raw_reject_code: u32, reject_message: String) -> Self {
        Self {
            raw_reject_code,
            reject_message,
        }
    }

    /// Gets the [`RejectCode`].
    ///
    /// The value is converted from `ic0.msg_reject_code`.
    ///
    /// # Errors
    ///
    /// If the raw reject code is not recognized, this method will return an `UnrecognizedRejectCode` error.
    /// This can happen if the IC produces a new reject code that hasn't been included in [`RejectCode`].
    /// Please check if your `ic-error-types` dependency is up-to-date.
    /// If the latest version of `ic-error-types` doesn't include the new reject code, please report it to the `ic-cdk` maintainers.
    pub fn reject_code(&self) -> Result<RejectCode, UnrecognizedRejectCode> {
        RejectCode::try_from(u64::from(self.raw_reject_code))
            .map_err(|_| UnrecognizedRejectCode(self.raw_reject_code))
    }

    /// Gets the raw numeric [`RejectCode`] value.
    ///
    /// This is a "never-fail" version of [`reject_code`](Self::reject_code) that returns the raw numeric value.
    pub fn raw_reject_code(&self) -> u32 {
        self.raw_reject_code
    }

    /// Retrieves the reject message associated with the call.
    ///
    /// This message is obtained from `ic0.msg_reject_msg`.
    pub fn reject_message(&self) -> &str {
        &self.reject_message
    }
}

// Errors END -----------------------------------------------------------------

impl<'m, 'a> IntoFuture for Call<'m, 'a> {
    type Output = Result<Response, CallFailed>;
    type IntoFuture = CallFuture<'m, 'a>;

    fn into_future(self) -> Self::IntoFuture {
        CallFuture {
            state: Arc::new(RwLock::new(CallFutureState::Prepared { call: self })),
        }
    }
}

// Execution
impl Call<'_, '_> {
    /// Sends the call and ignores the reply.
    pub fn oneway(&self) -> Result<(), OnewayError> {
        self.check_liquid_cycle_balance_sufficient()?;
        match self.perform(None) {
            0 => Ok(()),
            _ => Err(CallPerformFailed.into()),
        }
    }

    /// Checks if the liquid cycle balance is sufficient to perform the call.
    fn check_liquid_cycle_balance_sufficient(&self) -> Result<(), InsufficientLiquidCycleBalance> {
        let required = self.get_cost();
        let available = super::api::canister_liquid_cycle_balance();
        if available >= required {
            Ok(())
        } else {
            Err(InsufficientLiquidCycleBalance {
                available,
                required,
            })
        }
    }

    /// Performs the call.
    ///
    /// This is an internal helper function only for [`Self::oneway`] and [`CallFuture::poll`].
    ///
    /// # Arguments
    ///
    /// - `state_ptr`: An optional pointer to the internal state of the `CallFuture`.
    ///   - If `Some`, the call will be prepared for asynchronous execution:
    ///     - `ic0.call_new` will be invoked with [`callback`] and state pointer.
    ///     - `ic0.call_on_cleanup` will be invoked with [`cleanup`].
    ///   - If `None`, the call will be prepared for oneway execution:
    ///     - `ic0.call_new` will be invoked with invalid callback functions.
    ///     - `ic0.call_on_cleanup` won't be invoked.
    ///
    /// # Returns
    ///
    /// The return value of `ic0.call_perform`.
    fn perform(&self, state_opt: Option<Arc<RwLock<CallFutureState<'_, '_>>>>) -> u32 {
        let callee = self.canister_id.as_slice();
        let method = self.method;
        let arg = match &self.encoded_args {
            Cow::Owned(vec) => vec,
            Cow::Borrowed(r) => *r,
        };
        let state_ptr_opt = state_opt.map(Arc::<RwLock<CallFutureState<'_, '_>>>::into_raw);
        match state_ptr_opt {
            Some(state_ptr) => {
                // asynchronous execution
                //
                // # SAFETY:
                // - `callback` is intended as an entrypoint and therefore can be called as both reply and reject fn
                //      for ic0.call_new.
                // - `cleanup` is intended as an entrypoint and therefore can be called as cleanup fn for ic0.call_on_cleanup.
                // - `state_ptr` is a pointer created via Arc::<RwLock<CallFutureState>>::into_raw, and can therefore be passed as the userdata for
                //      `callback` and `cleanup`.
                // - if-and-only-if ic0.call_perform returns 0, exactly one(‡) of `callback` or `cleanup`
                //      receive ownership of `state_ptr`
                // - both functions deallocate `state_ptr`, and this enclosing function deallocates `state_ptr` if ic0.call_perform
                //      returns !=0, and therefore `state_ptr`'s ownership can be passed to FFI without leaking memory.
                //
                // ‡ The flow from outside the WASM runtime is that the callback runs, it traps, state is rolled back,
                //   and the cleanup callback runs afterwards. Inside the runtime, there is no difference between
                //   'state is rolled back to before the callback was called' and 'the callback was never called'.
                //   So from the code's perspective, exactly one function is called.
                unsafe {
                    ic0::call_new(
                        callee,
                        method,
                        callback,
                        state_ptr as usize,
                        callback,
                        state_ptr as usize,
                    );
                    ic0::call_on_cleanup(cleanup, state_ptr as usize);
                }
            }

            None => {
                ic0::call_new_oneway(callee, method);
                // There is no `call_on_cleanup` invocation because:
                // - the callback does not exist, and so cannot trap to require cleanup
                // - under the current behavior of the IC, this produces an error,
                //   which would unconditionally call the cleanup callback
            }
        }
        if !arg.is_empty() {
            ic0::call_data_append(arg);
        }
        if self.cycles > 0 {
            ic0::call_cycles_add128(self.cycles);
        }
        if let Some(timeout_seconds) = self.timeout_seconds {
            ic0::call_with_best_effort_response(timeout_seconds);
        }
        let res = ic0::call_perform();
        if res != 0
            && let Some(state_ptr) = state_ptr_opt
        {
            // SAFETY:
            // - `state_ptr_opt` is `Some` if-and-only-if ic0.call_new was called with ownership of `state`
            // - by returning !=0, ic0.call_new relinquishes ownership of `state_ptr`; it will never be passed
            //      to any functions
            // therefore, there is an outstanding handle to `state`, which it is safe to deallocate
            unsafe {
                Arc::from_raw(state_ptr);
            }
        }
        res
    }
}

// # Internal =================================================================

/// Internal state for the Future when sending a call.
#[derive(Debug, Default)]
enum CallFutureState<'m, 'a> {
    /// The future has been constructed, and the call has not yet been performed.
    /// Needed because futures are supposed to do nothing unless polled.
    /// Polling will attempt to fire off the request. Success returns `Pending` and transitions to `Executing`,
    /// failure returns `Ready` and transitions to `PostComplete.`
    Prepared { call: Call<'m, 'a> },
    /// The call has been performed and the message is in flight. Neither callback has been called. Polling will return `Pending`.
    /// This state will transition to `Trapped` if the future is canceled because of a trap in another future.
    Executing {
        waker: Waker,
        method: MethodHandle,
        task: Option<TaskHandle>,
    },
    /// `callback` has been called, so the call has been completed. This completion state has not yet been read by the user.
    /// Polling will return `Ready` and transition to `PostComplete`.
    Complete {
        result: Result<Response, CallFailed>,
    },
    /// The completion state of `Complete` has been returned from `poll` as `Poll::Ready`. Polling again will trap.
    #[default]
    PostComplete,
    /// The future (*not* the state) was canceled because of a trap in another future during `Executing`. Polling will trap.
    Trapped,
}

/// Represents a future that resolves to the result of an inter-canister call.
///
/// This type is returned by [`IntoFuture::into_future`] when called on a [`Call`].
/// The [`Call`] type implements the [`IntoFuture`] trait, allowing it to be converted
/// into a `CallFuture`. The future can be awaited to retrieve the result of the call.
#[derive(Debug)]
pub struct CallFuture<'m, 'a> {
    state: Arc<RwLock<CallFutureState<'m, 'a>>>,
}

impl std::future::Future for CallFuture<'_, '_> {
    type Output = Result<Response, CallFailed>;

    fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
        let self_ref = Pin::into_inner(self);
        let mut state = self_ref.state.write().unwrap();
        match mem::take(&mut *state) {
            CallFutureState::Prepared { call } => {
                if let Err(e) = call.check_liquid_cycle_balance_sufficient() {
                    *state = CallFutureState::PostComplete;
                    Poll::Ready(Err(e.into()))
                } else {
                    match call.perform(Some(self_ref.state.clone())) {
                        0 => {
                            // call_perform returns 0 means the call was successfully enqueued.
                            *state = CallFutureState::Executing {
                                waker: context.waker().clone(),
                                method: ic_cdk_executor::extend_current_method_context(),
                                task: TaskHandle::current(),
                            };
                            Poll::Pending
                        }
                        _ => {
                            *state = CallFutureState::PostComplete;
                            Poll::Ready(Err(CallPerformFailed.into()))
                        }
                    }
                }
            }
            CallFutureState::Executing { method, task, .. } => {
                *state = CallFutureState::Executing {
                    waker: context.waker().clone(),
                    method,
                    task,
                };
                Poll::Pending
            }
            CallFutureState::Complete { result } => {
                *state = CallFutureState::PostComplete;
                Poll::Ready(result)
            }
            CallFutureState::Trapped => trap("Call already trapped"),
            CallFutureState::PostComplete => trap("CallFuture polled after completing"),
        }
    }
}

impl Drop for CallFuture<'_, '_> {
    fn drop(&mut self) {
        // If this future is dropped while is_recovering_from_trap is true,
        // then it has been canceled due to a trap in another future.
        if is_recovering_from_trap() {
            *self.state.write().unwrap() = CallFutureState::Trapped;
        }
    }
}

/// The reply/reject callback for `ic0.call_new`.
///
/// It dereferences the future from a raw pointer, assigns the result and calls the waker.
/// We cannot use a closure here because we pass raw pointers to the System and back.
///
/// # Safety
///
/// This function must only be passed to the IC with a pointer from `Arc::<RwLock<CallFutureState>>::into_raw` as userdata.
unsafe extern "C" fn callback(env: usize) {
    let state_ptr = env as *const RwLock<CallFutureState<'_, '_>>;
    // SAFETY: This function is only ever called by the IC, and we only ever pass an Arc as userdata.
    let state = unsafe { Arc::from_raw(state_ptr) };
    let completed_state = CallFutureState::Complete {
        result: match msg_reject_code() {
            0 => Ok(Response(msg_arg_data())),
            code => {
                // The conversion is safe because the code is not 0.
                Err(CallFailed::CallRejected(CallRejected {
                    raw_reject_code: code,
                    reject_message: msg_reject_msg(),
                }))
            }
        },
    };
    let (waker, method) = match mem::replace(&mut *state.write().unwrap(), completed_state) {
        CallFutureState::Executing { waker, method, .. } => (waker, method),
        // This future has already been cancelled and waking it will do nothing.
        // All that's left is to explicitly trap in case this is the last call being multiplexed,
        // to replace an automatic trap from not replying.
        CallFutureState::Trapped => trap("Call already trapped"),
        _ => {
            unreachable!(
                "CallFutureState for in-flight calls should only be Executing or Trapped (callback)"
            )
        }
    };
    ic_cdk_executor::in_callback_executor_context_for(method, || {
        waker.wake();
    });
}

/// The cleanup callback for `ic0.call_on_cleanup`.
///
/// This function is called when [`callback`] was just called with the same parameter, and trapped.
/// We can't guarantee internal consistency at this point, but we can at least e.g. drop mutex guards.
/// Waker is a very opaque API, so the best we can do is set a global flag and proceed normally.
///
/// # Safety
///
/// This function must only be passed to the IC with a pointer from `Arc::<RwLock<CallFutureState>>::into_raw` as userdata.
unsafe extern "C" fn cleanup(env: usize) {
    let state_ptr = env as *const RwLock<CallFutureState<'_, '_>>;
    // SAFETY: This function is only ever called by the IC, and we only ever pass a Arc as userdata.
    let state = unsafe { Arc::from_raw(state_ptr) };
    // We set the call result, even though it won't be read on the
    // default executor, because we can't guarantee it was called on
    // our executor. However, we are not allowed to inspect
    // reject_code() inside of a cleanup callback, so always set the
    // result to a reject.
    //
    // Borrowing does not trap - the rollback from the
    // previous trap ensures that the RwLock can be borrowed again.
    let err_state = CallFutureState::Complete {
        result: Err(CallFailed::CallRejected(CallRejected {
            raw_reject_code: RejectCode::CanisterReject as u32,
            reject_message: "cleanup".into(),
        })),
    };
    let (method, task) = match mem::replace(&mut *state.write().unwrap(), err_state) {
        CallFutureState::Executing { method, task, .. } => (method, task),
        CallFutureState::Trapped => {
            // The future has already been canceled and dropped. There is nothing
            // more to clean up except for the CallFutureState.
            return;
        }
        _ => {
            unreachable!(
                "CallFutureState for in-flight calls should only be Executing or Trapped (cleanup)"
            )
        }
    };
    ic_cdk_executor::in_trap_recovery_context_for(method, || {
        ic_cdk_executor::cancel_all_tasks_attached_to_current_method();
        if let Some(task) = task {
            ic_cdk_executor::cancel_task(&task);
        }
    });
}

// # Internal END =============================================================

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wait_policy_replaces_mode_without_changing_payload_or_payment() {
        for initial in [
            Call::bounded_wait(Principal::anonymous(), "probe"),
            Call::unbounded_wait(Principal::anonymous(), "probe"),
        ] {
            let initial = initial.take_raw_args(vec![1, 2]).with_cycles(123);
            for timeout_seconds in [0, 1, 300, u32::MAX] {
                let call = initial
                    .clone()
                    .with_wait_policy(WaitPolicy::Bounded { timeout_seconds });
                assert_eq!(call.timeout_seconds, Some(timeout_seconds));
                assert_eq!(call.encoded_args.as_ref(), [1, 2]);
                assert_eq!(call.cycles, 123);
                assert_eq!(call.method, "probe");
                assert_eq!(call.canister_id, Principal::anonymous());
                let call = call.with_wait_policy(WaitPolicy::Unbounded);
                assert_eq!(call.timeout_seconds, None);
            }
        }
        assert_eq!(
            Call::bounded_wait(Principal::anonymous(), "probe").timeout_seconds,
            Some(300)
        );
        assert_eq!(
            Call::unbounded_wait(Principal::anonymous(), "probe").timeout_seconds,
            None
        );
    }

    #[test]
    fn raw_constructors_do_not_choose_a_codec() {
        for call in [
            Call::bounded_wait(Principal::anonymous(), "probe"),
            Call::unbounded_wait(Principal::anonymous(), "probe"),
        ] {
            assert!(call.encoded_args.is_empty());
            assert_eq!(call.with_raw_args(b"raw").encoded_args.as_ref(), b"raw");
        }
    }

    #[cfg(feature = "candid")]
    #[test]
    fn external_unit_arguments_are_explicit() {
        let args = crate::candid::contract::encode_args(()).unwrap();
        let call =
            Call::unbounded_wait(Principal::management_canister(), "raw_rand").with_raw_args(&args);
        assert_eq!(call.encoded_args.as_ref(), candid::encode_args(()).unwrap());
    }
}