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;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WaitPolicy {
Bounded {
timeout_seconds: u32,
},
Unbounded,
}
#[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]>,
}
impl<'m> Call<'m, '_> {
#[must_use]
pub fn bounded_wait(canister_id: Principal, method: &'m str) -> Self {
Self {
canister_id,
method,
cycles: 0,
timeout_seconds: Some(300),
encoded_args: Cow::Borrowed(&[]),
}
}
#[must_use]
pub fn unbounded_wait(canister_id: Principal, method: &'m str) -> Self {
Self {
canister_id,
method,
cycles: 0,
timeout_seconds: None,
encoded_args: Cow::Borrowed(&[]),
}
}
}
impl<'a> Call<'_, 'a> {
#[must_use]
pub fn with_raw_args(self, raw_args: &'a [u8]) -> Self {
Self {
encoded_args: Cow::Borrowed(raw_args),
..self
}
}
#[must_use]
pub fn take_raw_args(self, raw_args: Vec<u8>) -> Self {
Self {
encoded_args: Cow::Owned(raw_args),
..self
}
}
#[must_use]
pub fn with_cycles(mut self, cycles: u128) -> Self {
self.cycles = cycles;
self
}
#[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
}
#[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,
))
}
}
#[derive(Debug)]
pub struct Response(Vec<u8>);
impl 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
}
}
#[derive(Error, Debug, Clone)]
pub enum CallFailed {
#[error(transparent)]
InsufficientLiquidCycleBalance(#[from] InsufficientLiquidCycleBalance),
#[error(transparent)]
CallPerformFailed(#[from] CallPerformFailed),
#[error(transparent)]
CallRejected(#[from] CallRejected),
}
#[derive(Error, Debug, Clone)]
pub enum OnewayError {
#[error(transparent)]
InsufficientLiquidCycleBalance(#[from] InsufficientLiquidCycleBalance),
#[error(transparent)]
CallPerformFailed(#[from] CallPerformFailed),
}
#[derive(Error, Debug, Clone)]
#[error("insufficient liquid cycles balance, available: {available}, required: {required}")]
pub struct InsufficientLiquidCycleBalance {
pub available: u128,
pub required: u128,
}
#[derive(Error, Debug, Clone)]
#[error("call perform failed")]
pub struct CallPerformFailed;
#[derive(Error, Debug, Clone)]
#[error("call rejected: {raw_reject_code} - {reject_message}")]
pub struct CallRejected {
raw_reject_code: u32,
reject_message: String,
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[error("unrecognized reject code: {0}")]
pub struct UnrecognizedRejectCode(u32);
impl CallRejected {
pub fn with_rejection(raw_reject_code: u32, reject_message: String) -> Self {
Self {
raw_reject_code,
reject_message,
}
}
pub fn reject_code(&self) -> Result<RejectCode, UnrecognizedRejectCode> {
RejectCode::try_from(u64::from(self.raw_reject_code))
.map_err(|_| UnrecognizedRejectCode(self.raw_reject_code))
}
pub fn raw_reject_code(&self) -> u32 {
self.raw_reject_code
}
pub fn reject_message(&self) -> &str {
&self.reject_message
}
}
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 })),
}
}
}
impl Call<'_, '_> {
pub fn oneway(&self) -> Result<(), OnewayError> {
self.check_liquid_cycle_balance_sufficient()?;
match self.perform(None) {
0 => Ok(()),
_ => Err(CallPerformFailed.into()),
}
}
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,
})
}
}
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) => {
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);
}
}
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
{
unsafe {
Arc::from_raw(state_ptr);
}
}
res
}
}
#[derive(Debug, Default)]
enum CallFutureState<'m, 'a> {
Prepared { call: Call<'m, 'a> },
Executing {
waker: Waker,
method: MethodHandle,
task: Option<TaskHandle>,
},
Complete {
result: Result<Response, CallFailed>,
},
#[default]
PostComplete,
Trapped,
}
#[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 => {
*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 is_recovering_from_trap() {
*self.state.write().unwrap() = CallFutureState::Trapped;
}
}
}
unsafe extern "C" fn callback(env: usize) {
let state_ptr = env as *const RwLock<CallFutureState<'_, '_>>;
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 => {
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),
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();
});
}
unsafe extern "C" fn cleanup(env: usize) {
let state_ptr = env as *const RwLock<CallFutureState<'_, '_>>;
let state = unsafe { Arc::from_raw(state_ptr) };
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 => {
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);
}
});
}
#[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());
}
}