diff --git a/Cargo.lock b/Cargo.lock index 0c2f307..46181be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,9 +176,9 @@ dependencies = [ [[package]] name = "candid" -version = "0.10.14" +version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d90f5a1426d0489283a0bd5da9ed406fb3e69597e0d823dcb88a1965bb58d2" +checksum = "3ea81e16df186fae1979175058f05dfbfac6e2fdf3b161edcbdc440ef09232cf" dependencies = [ "anyhow", "binread", @@ -199,9 +199,9 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.6.6" +version = "0.10.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de398570c386726e7a59d9887b68763c481477f9a043fb998a2e09d428df1a9" +checksum = "2e6d499625531c41f474e55160a40313b33d002262ddaae40cade71bcc3bc75a" dependencies = [ "lazy_static", "proc-macro2", @@ -221,6 +221,7 @@ dependencies = [ "http", "ic-cdk", "ic-error-types", + "ic-management-canister-types", "itertools", "maplit", "num-traits", @@ -371,6 +372,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.104", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -768,35 +804,42 @@ dependencies = [ [[package]] name = "ic-cdk" -version = "0.17.2" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a7344f41493cbf591f13ae9f90181076f808a83af799815c3074b19c693d2e" +checksum = "4efb278f5d3ef033b3eed7f01f1096eaf67701896aa5ef69f5eddf5a84833dc0" dependencies = [ "candid", "ic-cdk-executor", "ic-cdk-macros", + "ic-error-types", + "ic-management-canister-types", "ic0", "serde", "serde_bytes", + "slotmap", + "thiserror 2.0.12", ] [[package]] name = "ic-cdk-executor" -version = "0.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903057edd3d4ff4b3fe44a64eaee1ceb73f579ba29e3ded372b63d291d7c16c2" +checksum = "99f4ee8930fd2e491177e2eb7fff53ee1c407c13b9582bdc7d6920cf83109a2d" +dependencies = [ + "ic0", + "slotmap", +] [[package]] name = "ic-cdk-macros" -version = "0.17.2" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cbaa50fa36d3e0616114becf81faa95a099e0d60948ed6978f30f1c77399fd" +checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" dependencies = [ "candid", + "darling", "proc-macro2", "quote", - "serde", - "serde_tokenstream", "syn 2.0.104", ] @@ -825,9 +868,9 @@ dependencies = [ [[package]] name = "ic-management-canister-types" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98554c2d8a30c00b6bfda18062fdcef21215cad07a52d8b8b1eb3130e51bfe71" +checksum = "ea7e5b8a0f7c3b320d9450ac950547db4f24a31601b5d398f9680b64427455d2" dependencies = [ "candid", "serde", @@ -863,9 +906,9 @@ dependencies = [ [[package]] name = "ic0" -version = "0.23.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de254dd67bbd58073e23dc1c8553ba12fa1dc610a19de94ad2bbcd0460c067f" +checksum = "1499d08fd5be8f790d477e1865d63bab6a8d748300e141270c4296e6d5fdd6bc" [[package]] name = "ic_principal" @@ -966,6 +1009,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1908,18 +1957,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "serde_tokenstream" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64060d864397305347a78851c51588fd283767e7e7589829e8121d65512340f1" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.104", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1982,6 +2019,15 @@ dependencies = [ "erased-serde", ] +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -2017,6 +2063,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "strum" version = "0.26.3" diff --git a/Cargo.toml b/Cargo.toml index 8f33dc5..8169b21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,9 @@ ciborium = "0.2.2" futures-channel = "0.3.31" futures-util = "0.3.31" http = "1.3.1" -ic-cdk = "0.17.2" +ic-cdk = "0.18.7" ic-error-types = "0.2" -ic-management-canister-types = "0.3.1" +ic-management-canister-types = "0.3.3" ic-test-utilities-load-wasm = { git = "https://github.com/dfinity/ic", tag = "release-2025-01-23_03-04-base" } itertools = "0.14.0" maplit = "1.0.2" diff --git a/canhttp/Cargo.toml b/canhttp/Cargo.toml index 28ad6e0..8af0723 100644 --- a/canhttp/Cargo.toml +++ b/canhttp/Cargo.toml @@ -25,6 +25,7 @@ futures-util = { workspace = true } http = { workspace = true, optional = true } ic-cdk = { workspace = true } ic-error-types = { workspace = true } +ic-management-canister-types = { workspace = true } num-traits = { workspace = true, optional = true } pin-project = { workspace = true } serde = { workspace = true, optional = true } diff --git a/canhttp/src/client/mod.rs b/canhttp/src/client/mod.rs index b41a8ce..392d400 100644 --- a/canhttp/src/client/mod.rs +++ b/canhttp/src/client/mod.rs @@ -1,21 +1,22 @@ #[cfg(test)] mod tests; -use crate::convert::ConvertError; -use crate::ConvertServiceBuilder; -use ic_cdk::api::call::RejectionCode; -use ic_cdk::api::management_canister::http_request::{ - CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse, TransformContext, -}; +use crate::{convert::ConvertError, ConvertServiceBuilder}; +use ic_cdk::call::Error as IcCdkError; use ic_error_types::RejectCode; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; +use ic_management_canister_types::{ + HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse, TransformContext, +}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; use thiserror::Error; use tower::{BoxError, Service, ServiceBuilder}; -/// Thin wrapper around [`ic_cdk::api::management_canister::http_request::http_request`] -/// that implements the [`tower::Service`] trait. Its functionality can be extended by composing so-called +/// Thin wrapper around [`ic_cdk::management_canister::http_request`] that implements the +/// [`tower::Service`] trait. Its functionality can be extended by composing so-called /// [tower middlewares](https://docs.rs/tower/latest/tower/#usage). /// /// Middlewares from this crate: @@ -34,7 +35,7 @@ impl Client { .service(Client) } - /// Creates a new client where error type is erased. + /// Creates a new client where the error type is erased. pub fn new_with_box_error() -> ConvertError { Self::new_with_error::() } @@ -42,15 +43,29 @@ impl Client { /// Error returned by the Internet Computer when making an HTTPs outcall. #[derive(Error, Clone, Debug, PartialEq, Eq)] -#[error("Error from ICP: (code {code:?}, message {message})")] -pub struct IcError { - /// Rejection code as specified [here](https://internetcomputer.org/docs/current/references/ic-interface-spec#reject-codes) - pub code: RejectCode, - /// Associated helper message. - pub message: String, +pub enum IcError { + /// The inter-canister call is rejected. + /// + /// Note that [`ic_cdk::call::Error::CallPerformFailed`] errors are also mapped to this variant + /// with an [`ic_error_types::RejectCode::SysFatal`] error code. + #[error("Error from ICP: (code {code:?}, message {message})")] + CallRejected { + /// Rejection code as specified [here](https://internetcomputer.org/docs/current/references/ic-interface-spec#reject-codes) + code: RejectCode, + /// Associated helper message. + message: String, + }, + /// The liquid cycle balance is insufficient to perform the call. + #[error("Insufficient liquid cycles balance, available: {available}, required: {required}")] + InsufficientLiquidCycleBalance { + /// The liquid cycle balance available in the canister. + available: u128, + /// The required cycles to perform the call. + required: u128, + }, } -impl Service for Client { +impl Service for Client { type Response = IcHttpResponse; type Error = IcError; type Future = Pin>>>; @@ -59,51 +74,52 @@ impl Service for Client { Poll::Ready(Ok(())) } - fn call( - &mut self, - IcHttpRequestWithCycles { request, cycles }: IcHttpRequestWithCycles, - ) -> Self::Future { - use ic_cdk::api::call::RejectionCode as IcCdkRejectionCode; - fn convert_reject_code(code: IcCdkRejectionCode) -> RejectCode { - match code { - IcCdkRejectionCode::SysFatal => RejectCode::SysFatal, - IcCdkRejectionCode::SysTransient => RejectCode::SysTransient, - IcCdkRejectionCode::DestinationInvalid => RejectCode::DestinationInvalid, - IcCdkRejectionCode::CanisterReject => RejectCode::CanisterReject, - IcCdkRejectionCode::CanisterError => RejectCode::CanisterError, - IcCdkRejectionCode::Unknown => { - // This can only happen if there is a new error code on ICP that the CDK is not aware of. - // We map it to SysFatal since none of the other error codes apply. - // In particular, note that RejectCode::SysUnknown is only applicable to inter-canister calls that used ic0.call_with_best_effort_response. - RejectCode::SysFatal + fn call(&mut self, request: IcHttpRequest) -> Self::Future { + fn convert_error(error: IcCdkError) -> IcError { + match error { + IcCdkError::CallRejected(e) => { + IcError::CallRejected { + // `CallRejected::reject_code()` can only return an error result if there is a + // new error code on ICP that the CDK is not aware of. We map it to `SysFatal` + // since none of the other error codes apply. + // In particular, note that `RejectCode::SysUnknown` is only applicable to + // inter-canister calls that used `ic0.call_with_best_effort_response`. + code: e.reject_code().unwrap_or(RejectCode::SysFatal), + message: e.reject_message().to_string(), + } + } + IcCdkError::CallPerformFailed(e) => { + IcError::CallRejected { + // This error indicates that the `ic0.call_perform` system API returned a non-zero code. + // The only possible non-zero value (2) has the same semantics as `RejectCode::SysFatal`. + // See the IC specifications here: + // https://internetcomputer.org/docs/references/ic-interface-spec#system-api-call + code: RejectCode::SysFatal, + message: e.to_string(), + } + } + IcCdkError::InsufficientLiquidCycleBalance(e) => { + IcError::InsufficientLiquidCycleBalance { + available: e.available, + required: e.required, + } + } + IcCdkError::CandidDecodeFailed(e) => { + // This can only happen if there is a bug in the CDK in the implementation + // of `ic_cdk::management_canister::http_request`. + panic!("Candid decode failed while performing HTTP outcall: {e}"); } - RejectionCode::NoError => unreachable!("ic_cdk::api::management_canister::http_request::http_request should never produce a RejectionCode::NoError error") } } Box::pin(async move { - match ic_cdk::api::management_canister::http_request::http_request(request, cycles) + ic_cdk::management_canister::http_request(&request) .await - { - Ok((response,)) => Ok(response), - Err((code, message)) => Err(IcError { - code: convert_reject_code(code), - message, - }), - } + .map_err(convert_error) }) } } -/// [`IcHttpRequest`] specifying how many cycles should be attached for the HTTPs outcall. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct IcHttpRequestWithCycles { - /// Request to be made. - pub request: IcHttpRequest, - /// Number of cycles to attach. - pub cycles: u128, -} - /// Add support for max response bytes. pub trait MaxResponseBytesRequestExtension: Sized { /// Set the max response bytes. @@ -133,16 +149,6 @@ impl MaxResponseBytesRequestExtension for IcHttpRequest { } } -impl MaxResponseBytesRequestExtension for IcHttpRequestWithCycles { - fn set_max_response_bytes(&mut self, value: u64) { - self.request.set_max_response_bytes(value); - } - - fn get_max_response_bytes(&self) -> Option { - self.request.get_max_response_bytes() - } -} - /// Add support for transform context to specify how the response will be canonicalized by the replica /// to maximize chances of consensus. /// @@ -172,16 +178,6 @@ impl TransformContextRequestExtension for IcHttpRequest { } } -impl TransformContextRequestExtension for IcHttpRequestWithCycles { - fn set_transform_context(&mut self, value: TransformContext) { - self.request.set_transform_context(value); - } - - fn get_transform_context(&self) -> Option<&TransformContext> { - self.request.get_transform_context() - } -} - /// Characterize errors that are specific to HTTPs outcalls. pub trait HttpsOutcallError { /// Determines whether the error indicates that the response was larger than the specified @@ -193,8 +189,13 @@ pub trait HttpsOutcallError { impl HttpsOutcallError for IcError { fn is_response_too_large(&self) -> bool { - self.code == RejectCode::SysFatal - && (self.message.contains("size limit") || self.message.contains("length limit")) + match self { + IcError::CallRejected { code, message } => { + code == &RejectCode::SysFatal + && (message.contains("size limit") || message.contains("length limit")) + } + IcError::InsufficientLiquidCycleBalance { .. } => false, + } } } diff --git a/canhttp/src/client/tests.rs b/canhttp/src/client/tests.rs index 792641d..31ca844 100644 --- a/canhttp/src/client/tests.rs +++ b/canhttp/src/client/tests.rs @@ -1,5 +1,4 @@ -use crate::retry::DoubleMaxResponseBytes; -use crate::{Client, HttpsOutcallError, IcError}; +use crate::{retry::DoubleMaxResponseBytes, Client, HttpsOutcallError, IcError}; use tower::{ServiceBuilder, ServiceExt}; // Some middlewares like tower::retry need the underlying service to be cloneable. diff --git a/canhttp/src/cycles/mod.rs b/canhttp/src/cycles/mod.rs index 6b4b71c..5892d52 100644 --- a/canhttp/src/cycles/mod.rs +++ b/canhttp/src/cycles/mod.rs @@ -1,7 +1,7 @@ //! Middleware to handle cycles accounting. //! //! Issuing HTTPs outcalls requires cycles, and this layer takes care of the following: -//! 1. Estimate the number of cycles required. +//! 1. Calculate the number of cycles required. //! 2. Decide how the canister should charge for those cycles. //! 3. Do the actual charging. //! @@ -15,7 +15,7 @@ //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { //! let mut service = ServiceBuilder::new() -//! .cycles_accounting(34, ChargeMyself::default()) +//! .cycles_accounting(ChargeMyself::default()) //! .service(Client::new_with_box_error()); //! //! let _ = service.ready().await.unwrap(); @@ -32,7 +32,7 @@ //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { //! let mut service = ServiceBuilder::new() -//! .cycles_accounting(34, ChargeCaller::new(|_request, cost| cost + 1_000_000)) +//! .cycles_accounting(ChargeCaller::new(|_request, cost| cost + 1_000_000)) //! .service(Client::new_with_box_error()); //! //! let _ = service.ready().await.unwrap(); @@ -40,13 +40,12 @@ //! # Ok(()) //! # } //! ``` -#[cfg(test)] -mod tests; -use crate::client::IcHttpRequestWithCycles; -use crate::convert::{Convert, ConvertRequestLayer}; -use crate::ConvertServiceBuilder; -use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument; +use crate::{ + convert::{ConvertRequestLayer, Filter}, + ConvertServiceBuilder, +}; +use ic_management_canister_types::HttpRequestArgs; use std::convert::Infallible; use thiserror::Error; use tower::ServiceBuilder; @@ -60,7 +59,7 @@ pub trait CyclesChargingPolicy { /// Charge cycles and return the charged amount. fn charge_cycles( &self, - request: &CanisterHttpRequestArgument, + request: &HttpRequestArgs, request_cycles_cost: u128, ) -> Result; } @@ -74,7 +73,7 @@ impl CyclesChargingPolicy for ChargeMyself { fn charge_cycles( &self, - _request: &CanisterHttpRequestArgument, + _request: &HttpRequestArgs, _request_cycles_cost: u128, ) -> Result { // no-op, @@ -90,7 +89,7 @@ pub struct ChargeCaller { impl ChargeCaller where - F: Fn(&CanisterHttpRequestArgument, u128) -> u128, + F: Fn(&HttpRequestArgs, u128) -> u128, { /// Create a new instance of [`ChargeCaller`]. pub fn new(cycles_to_charge: F) -> Self { @@ -100,25 +99,25 @@ where impl CyclesChargingPolicy for ChargeCaller where - F: Fn(&CanisterHttpRequestArgument, u128) -> u128, + F: Fn(&HttpRequestArgs, u128) -> u128, { type Error = ChargeCallerError; fn charge_cycles( &self, - request: &CanisterHttpRequestArgument, + request: &HttpRequestArgs, request_cycles_cost: u128, ) -> Result { let cycles_to_charge = (self.cycles_to_charge)(request, request_cycles_cost); if cycles_to_charge > 0 { - let cycles_available = ic_cdk::api::call::msg_cycles_available128(); + let cycles_available = ic_cdk::api::msg_cycles_available(); if cycles_available < cycles_to_charge { return Err(ChargeCallerError::InsufficientCyclesError { expected: cycles_to_charge, received: cycles_available, }); } - let cycles_received = ic_cdk::api::call::msg_cycles_accept128(cycles_to_charge); + let cycles_received = ic_cdk::api::msg_cycles_accept(cycles_to_charge); assert_eq!( cycles_received, cycles_to_charge, "Expected to receive {cycles_to_charge}, but got {cycles_received}" @@ -128,79 +127,6 @@ where } } -/// Estimate the exact minimum cycles amount required to send an HTTPs outcall as specified -/// [here](https://internetcomputer.org/docs/current/developer-docs/gas-cost#https-outcalls). -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct CyclesCostEstimator { - num_nodes_in_subnet: u32, -} - -impl CyclesCostEstimator { - /// Maximum value for `max_response_bytes` which is 2MB, - /// see the [IC specification](https://internetcomputer.org/docs/current/references/ic-interface-spec#ic-http_request). - pub const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; - - /// Create a new estimator for a subnet having the given number of nodes. - pub const fn new(num_nodes_in_subnet: u32) -> Self { - CyclesCostEstimator { - num_nodes_in_subnet, - } - } - - /// Compute the number of cycles required to send the given request via HTTPs outcall. - /// - /// An HTTP outcall entails calling the `http_request` method on the management canister interface, - /// which requires that cycles to pay for the call must be explicitly attached with the call - /// ([IC specification](https://internetcomputer.org/docs/current/references/ic-interface-spec#ic-http_request)). - /// The required amount of cycles to attach is specified - /// [here](https://internetcomputer.org/docs/current/developer-docs/gas-cost#https-outcalls). - pub fn cost_of_http_request(&self, request: &CanisterHttpRequestArgument) -> u128 { - let payload_body_bytes = request - .body - .as_ref() - .map(|body| body.len()) - .unwrap_or_default(); - let extra_payload_bytes = request.url.len() - + request - .headers - .iter() - .map(|header| header.name.len() + header.value.len()) - .sum::() - + request.transform.as_ref().map_or(0, |transform| { - transform.function.0.method.len() + transform.context.len() - }); - let max_response_bytes = request - .max_response_bytes - .unwrap_or(Self::DEFAULT_MAX_RESPONSE_BYTES); - let request_bytes = (payload_body_bytes + extra_payload_bytes) as u128; - self.base_fee() - + self.request_fee(request_bytes) - + self.response_fee(max_response_bytes as u128) - } - - fn base_fee(&self) -> u128 { - 3_000_000_u128 - .saturating_add(60_000_u128.saturating_mul(self.num_nodes_as_u128())) - .saturating_mul(self.num_nodes_as_u128()) - } - - fn request_fee(&self, bytes: u128) -> u128 { - 400_u128 - .saturating_mul(self.num_nodes_as_u128()) - .saturating_mul(bytes) - } - - fn response_fee(&self, bytes: u128) -> u128 { - 800_u128 - .saturating_mul(self.num_nodes_as_u128()) - .saturating_mul(bytes) - } - - fn num_nodes_as_u128(&self) -> u128 { - self.num_nodes_in_subnet as u128 - } -} - /// Error returned by the [`CyclesAccounting`] middleware. #[derive(Error, Clone, Debug, PartialEq, Eq)] pub enum ChargeCallerError { @@ -215,41 +141,30 @@ pub enum ChargeCallerError { } /// A middleware to handle cycles accounting, i.e. verify if sufficiently many cycles are available in a request. -/// How cycles are estimated is given by `CyclesEstimator` +/// The cost of sending the request is calculated by [`ic_cdk::api::cost_http_request`]. #[derive(Clone, Debug)] pub struct CyclesAccounting { - cycles_cost_estimator: CyclesCostEstimator, charging_policy: ChargingPolicy, } impl CyclesAccounting { - /// Create a new middleware given the cycles estimator. - pub fn new(num_nodes_in_subnet: u32, charging_policy: ChargingPolicy) -> Self { - Self { - cycles_cost_estimator: CyclesCostEstimator::new(num_nodes_in_subnet), - charging_policy, - } + /// Create a new middleware given the charging policy. + pub fn new(charging_policy: ChargingPolicy) -> Self { + Self { charging_policy } } } -impl Convert for CyclesAccounting +impl Filter for CyclesAccounting where ChargingPolicy: CyclesChargingPolicy, { - type Output = IcHttpRequestWithCycles; type Error = ChargingPolicy::Error; - fn try_convert( - &mut self, - request: CanisterHttpRequestArgument, - ) -> Result { - let cycles_to_attach = self.cycles_cost_estimator.cost_of_http_request(&request); + fn filter(&mut self, request: HttpRequestArgs) -> Result { + let cycles_to_attach = ic_cdk::management_canister::cost_http_request(&request); self.charging_policy .charge_cycles(&request, cycles_to_attach)?; - Ok(IcHttpRequestWithCycles { - request, - cycles: cycles_to_attach, - }) + Ok(request) } } @@ -261,7 +176,6 @@ pub trait CyclesAccountingServiceBuilder { /// See the [module docs](crate::cycles) for examples. fn cycles_accounting( self, - num_nodes_in_subnet: u32, charging: C, ) -> ServiceBuilder>, L>>; } @@ -269,9 +183,8 @@ pub trait CyclesAccountingServiceBuilder { impl CyclesAccountingServiceBuilder for ServiceBuilder { fn cycles_accounting( self, - num_nodes_in_subnet: u32, charging: C, ) -> ServiceBuilder>, L>> { - self.convert_request(CyclesAccounting::new(num_nodes_in_subnet, charging)) + self.convert_request(CyclesAccounting::new(charging)) } } diff --git a/canhttp/src/cycles/tests.rs b/canhttp/src/cycles/tests.rs deleted file mode 100644 index ad3f8fe..0000000 --- a/canhttp/src/cycles/tests.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::cycles::CyclesCostEstimator; -use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument; - -#[test] -fn test_http_request_fee_components() { - // Assert the calculation matches the cost table at - // https://internetcomputer.org/docs/current/developer-docs/gas-cost#cycles-price-breakdown - let estimator = CyclesCostEstimator::new(13); - assert_eq!(estimator.base_fee(), 49_140_000); - assert_eq!(estimator.request_fee(1), 5_200); - assert_eq!(estimator.response_fee(1), 10_400); - - let estimator = CyclesCostEstimator::new(34); - assert_eq!(estimator.base_fee(), 171_360_000); - assert_eq!(estimator.request_fee(1), 13_600); - assert_eq!(estimator.response_fee(1), 27_200); -} - -#[test] -fn test_candid_rpc_cost() { - const OVERHEAD_BYTES: u32 = 356; - - let estimator = CyclesCostEstimator::new(13); - assert_eq!( - [ - estimator.cost_of_http_request(&request(0, OVERHEAD_BYTES, 0)), - estimator.cost_of_http_request(&request(123, OVERHEAD_BYTES, 123)), - estimator.cost_of_http_request(&request(123, OVERHEAD_BYTES, 4567890)), - estimator.cost_of_http_request(&request(890, OVERHEAD_BYTES, 4567890)), - ], - [50991200, 52910000, 47557686800, 47561675200] - ); - - let estimator = CyclesCostEstimator::new(34); - assert_eq!( - [ - estimator.cost_of_http_request(&request(0, OVERHEAD_BYTES, 0)), - estimator.cost_of_http_request(&request(123, OVERHEAD_BYTES, 123)), - estimator.cost_of_http_request(&request(123, OVERHEAD_BYTES, 4567890)), - estimator.cost_of_http_request(&request(890, OVERHEAD_BYTES, 4567890)), - ], - [176201600, 181220000, 124424482400, 124434913600] - ); -} - -fn request( - payload_body_bytes: u32, - extra_payload_bytes: u32, - max_response_bytes: u64, -) -> CanisterHttpRequestArgument { - let body = Some(vec![42_u8; payload_body_bytes as usize]); - let max_response_bytes = Some(max_response_bytes); - CanisterHttpRequestArgument { - url: "a".repeat(extra_payload_bytes as usize), - max_response_bytes, - method: Default::default(), - headers: vec![], - body, - transform: None, - } -} diff --git a/canhttp/src/http/mod.rs b/canhttp/src/http/mod.rs index 8baceca..277acb5 100644 --- a/canhttp/src/http/mod.rs +++ b/canhttp/src/http/mod.rs @@ -29,8 +29,8 @@ //! # Examples //! //! ```rust -//! use canhttp::{http::{HttpConversionLayer }, IcError, MaxResponseBytesRequestExtension}; -//! use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse}; +//! use canhttp::{http::{HttpConversionLayer }, MaxResponseBytesRequestExtension}; +//! use ic_management_canister_types::{HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse}; //! use tower::{Service, ServiceBuilder, ServiceExt, BoxError}; //! //! async fn always_200_ok(request: IcHttpRequest) -> Result { @@ -59,8 +59,8 @@ //! # } //! ``` //! -//! [`IcHttpRequest`]: ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument -//! [`IcHttpResponse`]: ic_cdk::api::management_canister::http_request::HttpResponse +//! [`IcHttpRequest`]: ic_management_canister_types::HttpRequestArgs +//! [`IcHttpResponse`]: ic_management_canister_types::HttpRequestResult #[cfg(test)] mod tests; diff --git a/canhttp/src/http/request.rs b/canhttp/src/http/request.rs index 6823546..927977e 100644 --- a/canhttp/src/http/request.rs +++ b/canhttp/src/http/request.rs @@ -1,8 +1,8 @@ use crate::convert::Convert; use crate::{MaxResponseBytesRequestExtension, TransformContextRequestExtension}; -use ic_cdk::api::management_canister::http_request::{ - CanisterHttpRequestArgument as IcHttpRequest, HttpHeader as IcHttpHeader, - HttpMethod as IcHttpMethod, TransformContext, +use ic_management_canister_types::{ + HttpHeader as IcHttpHeader, HttpMethod as IcHttpMethod, HttpRequestArgs as IcHttpRequest, + TransformContext, }; use thiserror::Error; diff --git a/canhttp/src/http/response.rs b/canhttp/src/http/response.rs index ae49145..c29a885 100644 --- a/canhttp/src/http/response.rs +++ b/canhttp/src/http/response.rs @@ -1,6 +1,5 @@ use crate::convert::{Convert, Filter}; -use http::Response; -use ic_cdk::api::management_canister::http_request::HttpResponse as IcHttpResponse; +use ic_management_canister_types::HttpRequestResult as IcHttpResponse; use thiserror::Error; /// HTTP response with a body made of bytes. @@ -41,7 +40,7 @@ impl Convert for HttpResponseConverter { fn try_convert(&mut self, response: IcHttpResponse) -> Result { use http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; - use ic_cdk::api::management_canister::http_request::HttpHeader as IcHttpHeader; + use ic_management_canister_types::HttpHeader as IcHttpHeader; use num_traits::ToPrimitive; let status = response @@ -94,7 +93,7 @@ pub struct FilterNonSuccessfulHttpResponse; impl Filter> for FilterNonSuccessfulHttpResponse { type Error = FilterNonSuccessfulHttpResponseError; - fn filter(&mut self, response: Response) -> Result, Self::Error> { + fn filter(&mut self, response: http::Response) -> Result, Self::Error> { if !response.status().is_success() { return Err(FilterNonSuccessfulHttpResponseError::UnsuccessfulResponse( response, diff --git a/canhttp/src/http/tests.rs b/canhttp/src/http/tests.rs index 466c02b..1135ec0 100644 --- a/canhttp/src/http/tests.rs +++ b/canhttp/src/http/tests.rs @@ -1,19 +1,20 @@ -use crate::http::request::HttpRequestConversionError; -use crate::http::response::{HttpResponse, HttpResponseConversionError}; -use crate::http::{HttpConversionLayer, HttpRequestConverter, HttpResponseConverter}; use crate::{ + http::{ + request::HttpRequestConversionError, + response::{HttpResponse, HttpResponseConversionError}, + HttpConversionLayer, HttpRequestConverter, HttpResponseConverter, + }, ConvertServiceBuilder, IcError, MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; use assert_matches::assert_matches; use candid::{Decode, Encode, Principal}; use http::StatusCode; -use ic_cdk::api::management_canister::http_request::{ - CanisterHttpRequestArgument as IcHttpRequest, HttpHeader as IcHttpHeader, - HttpMethod as IcHttpMethod, HttpResponse as IcHttpResponse, -}; -use ic_cdk::api::management_canister::http_request::{TransformContext, TransformFunc}; use ic_error_types::RejectCode; +use ic_management_canister_types::{ + HttpHeader as IcHttpHeader, HttpMethod as IcHttpMethod, HttpRequestArgs as IcHttpRequest, + HttpRequestResult as IcHttpResponse, TransformContext, TransformFunc, +}; use std::error::Error; use std::fmt::Debug; use tower::{BoxError, Service, ServiceBuilder, ServiceExt}; @@ -149,9 +150,10 @@ async fn should_fail_to_convert_http_response() { .service_fn(always_error); let error = expect_error::<_, IcError>(service.ready().await.unwrap().call(invalid_response).await); + assert_eq!( error, - IcError { + IcError::CallRejected { code: RejectCode::SysUnknown, message: "always error".to_string(), } @@ -226,7 +228,7 @@ async fn echo_response(response: IcHttpResponse) -> Result Result { - Err(BoxError::from(IcError { + Err(BoxError::from(IcError::CallRejected { code: RejectCode::SysUnknown, message: "always error".to_string(), })) diff --git a/canhttp/src/lib.rs b/canhttp/src/lib.rs index 1e816ce..8641b26 100644 --- a/canhttp/src/lib.rs +++ b/canhttp/src/lib.rs @@ -6,7 +6,7 @@ #![forbid(missing_docs)] pub use client::{ - Client, HttpsOutcallError, IcError, IcHttpRequestWithCycles, MaxResponseBytesRequestExtension, + Client, HttpsOutcallError, IcError, MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; pub use convert::ConvertServiceBuilder; diff --git a/canhttp/src/observability/mod.rs b/canhttp/src/observability/mod.rs index 43bca8f..981cdbb 100644 --- a/canhttp/src/observability/mod.rs +++ b/canhttp/src/observability/mod.rs @@ -16,7 +16,7 @@ //! //! ```rust //! use canhttp::{IcError, observability::ObservabilityLayer}; -//! use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse}; +//! use ic_management_canister_types::{HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse}; //! use tower::{Service, ServiceBuilder, ServiceExt}; //! use std::cell::RefCell; //! @@ -76,7 +76,7 @@ //! The previous example can be refined by extracting request data (such as the request URL) to observe the responses/errors: //! ```rust //! use canhttp::{IcError, observability::ObservabilityLayer}; -//! use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse}; +//! use ic_management_canister_types::{HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse}; //! use maplit::btreemap; //! use tower::{Service, ServiceBuilder, ServiceExt}; //! use std::cell::RefCell; diff --git a/canhttp/src/retry/mod.rs b/canhttp/src/retry/mod.rs index 22b3e9d..8df2068 100644 --- a/canhttp/src/retry/mod.rs +++ b/canhttp/src/retry/mod.rs @@ -20,21 +20,25 @@ const HTTP_MAX_SIZE: u64 = 2_000_000; /// # Examples /// /// ```rust -/// use tower::{Service, ServiceBuilder, ServiceExt}; -/// use canhttp::{Client, http::HttpRequest, HttpsOutcallError, IcError, MaxResponseBytesRequestExtension, retry::DoubleMaxResponseBytes}; +/// use canhttp::{ +/// Client, http::HttpRequest, HttpsOutcallError, IcError, MaxResponseBytesRequestExtension, +/// retry::DoubleMaxResponseBytes +/// }; /// use ic_error_types::RejectCode; +/// use tower::{Service, ServiceBuilder, ServiceExt}; /// /// fn response_is_too_large_error() -> IcError { -/// let error = IcError { -/// code: RejectCode::SysFatal, -/// message: "Http body exceeds size limit".to_string(), -/// }; +/// let error = IcError::CallRejected { +/// code: RejectCode::SysFatal, +/// message: "Http body exceeds size limit".to_string(), +/// }; /// assert!(error.is_response_too_large()); /// error /// } /// /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { +/// use assert_matches::assert_matches; /// let mut service = ServiceBuilder::new() /// .retry(DoubleMaxResponseBytes) /// .service_fn(|request: HttpRequest| async move { @@ -52,7 +56,7 @@ const HTTP_MAX_SIZE: u64 = 2_000_000; /// // This will effectively do 4 calls with the following max_response_bytes values: 0, 2048, 4096, 8192. /// let response = service.ready().await?.call(request).await; /// -/// assert_eq!(response, Ok(())); +/// assert_matches!(response, Ok(())); /// # Ok(()) /// # } /// ``` diff --git a/canhttp/src/retry/tests.rs b/canhttp/src/retry/tests.rs index 620f70f..e83833c 100644 --- a/canhttp/src/retry/tests.rs +++ b/canhttp/src/retry/tests.rs @@ -1,12 +1,14 @@ -use crate::http::HttpRequest; -use crate::retry::DoubleMaxResponseBytes; -use crate::{HttpsOutcallError, IcError, MaxResponseBytesRequestExtension}; +use crate::{ + client::IcError, http::HttpRequest, retry::DoubleMaxResponseBytes, HttpsOutcallError, + MaxResponseBytesRequestExtension, +}; use assert_matches::assert_matches; use ic_error_types::RejectCode; -use std::future; -use std::sync::mpsc; -use std::sync::mpsc::Sender; -use std::task::{Context, Poll}; +use std::{ + future, + sync::mpsc::{self, Sender}, + task::{Context, Poll}, +}; use tower::{Service, ServiceBuilder, ServiceExt}; #[tokio::test] @@ -173,7 +175,7 @@ where } fn response_is_too_large_error() -> IcError { - let error = IcError { + let error = IcError::CallRejected { code: RejectCode::SysFatal, message: "Http body exceeds size limit".to_string(), }; diff --git a/examples/http_canister/src/main.rs b/examples/http_canister/src/main.rs index 22482a5..08d5378 100644 --- a/examples/http_canister/src/main.rs +++ b/examples/http_canister/src/main.rs @@ -1,8 +1,10 @@ //! Example of a canister using `canhttp` to issue HTTP requests. -use canhttp::cycles::{ChargeMyself, CyclesAccountingServiceBuilder}; -use canhttp::http::HttpConversionLayer; -use canhttp::observability::ObservabilityLayer; -use canhttp::{Client, MaxResponseBytesRequestExtension}; +use canhttp::{ + cycles::{ChargeMyself, CyclesAccountingServiceBuilder}, + http::HttpConversionLayer, + observability::ObservabilityLayer, + Client, MaxResponseBytesRequestExtension, +}; use ic_cdk::update; use tower::{BoxError, Service, ServiceBuilder, ServiceExt}; @@ -45,7 +47,7 @@ fn http_client( // Only deal with types from the http crate. .layer(HttpConversionLayer) // Use cycles from the canister to pay for HTTPs outcalls - .cycles_accounting(34, ChargeMyself::default()) + .cycles_accounting(ChargeMyself::default()) // The actual client .service(Client::new_with_box_error()) }