#[cfg(test)]
mod tests;
use crate::{convert::ConvertError, ConvertServiceBuilder};
use ic_cdk::call::Error as IcCdkError;
use ic_cdk_management_canister::{
HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse, TransformContext,
};
use ic_error_types::RejectCode;
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use thiserror::Error;
use tower::{BoxError, Service, ServiceBuilder};
use tower_layer::Layer;
#[derive(Clone, Debug)]
pub struct Client;
impl Client {
pub fn new_with_error<CustomError: From<IcError>>() -> ConvertError<Client, CustomError> {
ServiceBuilder::new()
.convert_error::<CustomError>()
.service(Client)
}
pub fn new_with_box_error() -> ConvertError<Client, BoxError> {
Self::new_with_error::<BoxError>()
}
}
#[derive(Error, Clone, Debug, PartialEq, Eq)]
pub enum IcError {
#[error("Error from ICP: (code {code:?}, message {message})")]
CallRejected {
code: RejectCode,
message: String,
},
#[error("Insufficient liquid cycles balance, available: {available}, required: {required}")]
InsufficientLiquidCycleBalance {
available: u128,
required: u128,
},
}
impl Service<IcHttpRequest> for Client {
type Response = IcHttpResponse;
type Error = IcError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, request: IcHttpRequest) -> Self::Future {
fn convert_error(error: IcCdkError) -> IcError {
match error {
IcCdkError::CallRejected(e) => {
IcError::CallRejected {
code: e.reject_code().unwrap_or(RejectCode::SysFatal),
message: e.reject_message().to_string(),
}
}
IcCdkError::CallPerformFailed(e) => {
IcError::CallRejected {
code: RejectCode::SysFatal,
message: e.to_string(),
}
}
IcCdkError::InsufficientLiquidCycleBalance(e) => {
IcError::InsufficientLiquidCycleBalance {
available: e.available,
required: e.required,
}
}
IcCdkError::CandidDecodeFailed(e) => {
panic!("Candid decode failed while performing HTTP outcall: {e}");
}
}
}
Box::pin(async move {
ic_cdk_management_canister::http_request(&request)
.await
.map_err(convert_error)
})
}
}
pub trait MaxResponseBytesRequestExtension: Sized {
fn set_max_response_bytes(&mut self, value: u64);
fn get_max_response_bytes(&self) -> Option<u64>;
fn max_response_bytes(mut self, value: u64) -> Self {
self.set_max_response_bytes(value);
self
}
}
impl MaxResponseBytesRequestExtension for IcHttpRequest {
fn set_max_response_bytes(&mut self, value: u64) {
self.max_response_bytes = Some(value);
}
fn get_max_response_bytes(&self) -> Option<u64> {
self.max_response_bytes
}
}
pub trait TransformContextRequestExtension: Sized {
fn set_transform_context(&mut self, value: TransformContext);
fn get_transform_context(&self) -> Option<&TransformContext>;
fn transform_context(mut self, value: TransformContext) -> Self {
self.set_transform_context(value);
self
}
}
impl TransformContextRequestExtension for IcHttpRequest {
fn set_transform_context(&mut self, value: TransformContext) {
self.transform = Some(value);
}
fn get_transform_context(&self) -> Option<&TransformContext> {
self.transform.as_ref()
}
}
pub trait IsReplicatedRequestExtension: Sized {
fn set_is_replicated(&mut self, value: bool);
fn get_is_replicated(&self) -> Option<bool>;
fn replicated(mut self, value: bool) -> Self {
self.set_is_replicated(value);
self
}
}
impl IsReplicatedRequestExtension for IcHttpRequest {
fn set_is_replicated(&mut self, value: bool) {
self.is_replicated = Some(value);
}
fn get_is_replicated(&self) -> Option<bool> {
self.is_replicated
}
}
pub trait HttpsOutcallError {
fn is_response_too_large(&self) -> bool;
}
impl HttpsOutcallError for IcError {
fn is_response_too_large(&self) -> bool {
match self {
IcError::CallRejected { code, message } => {
code == &RejectCode::SysFatal
&& (message.contains("size limit") || message.contains("length limit"))
}
IcError::InsufficientLiquidCycleBalance { .. } => false,
}
}
}
impl HttpsOutcallError for BoxError {
fn is_response_too_large(&self) -> bool {
if let Some(ic_error) = self.downcast_ref::<IcError>() {
return ic_error.is_response_too_large();
}
false
}
}
#[derive(Clone, Debug, Default)]
pub struct CanisterReadyLayer;
impl<S> Layer<S> for CanisterReadyLayer {
type Service = CanisterReadyService<S>;
fn layer(&self, inner: S) -> Self::Service {
Self::Service { inner }
}
}
pub struct CanisterReadyService<S> {
inner: S,
}
#[derive(Error, Clone, Debug, Eq, PartialEq)]
pub enum CanisterReadyError {
#[error("Canister is not running and has status {0}")]
CanisterNotRunning(u32),
}
impl<S, Req> Service<Req> for CanisterReadyService<S>
where
S: Service<Req>,
CanisterReadyError: Into<S::Error>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
use ic_cdk::api::CanisterStatusCode;
match ic_cdk::api::canister_status() {
CanisterStatusCode::Running => self.inner.poll_ready(cx),
status => Poll::Ready(Err(CanisterReadyError::CanisterNotRunning(u32::from(
status,
))
.into())),
}
}
fn call(&mut self, req: Req) -> Self::Future {
self.inner.call(req)
}
}