From 7816262e2ca07c905d5242c1a41a436dfb234b68 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 3 Oct 2025 07:48:52 +0200 Subject: [PATCH 1/6] XC-502: Update `ic-cdk` --- Cargo.lock | 57 +++++++--- Cargo.toml | 5 +- e2e/motoko/main.mo | 2 +- e2e/rust/src/main.rs | 56 +++++----- evm_rpc_client/src/fixtures/mod.rs | 8 +- evm_rpc_client/src/lib.rs | 3 +- evm_rpc_client/src/request/mod.rs | 6 +- evm_rpc_client/src/runtime/mod.rs | 51 +++------ evm_rpc_types/src/result/mod.rs | 13 +++ src/http.rs | 161 ++++++++++++++--------------- src/main.rs | 40 +++---- src/metrics.rs | 4 +- src/rpc_client/eth_rpc/mod.rs | 20 ++-- src/rpc_client/mod.rs | 16 +-- tests/mock_http_runtime/mod.rs | 50 ++++----- tests/setup/mod.rs | 20 ++-- tests/tests.rs | 88 ---------------- 17 files changed, 261 insertions(+), 339 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d5f8990..a80c740c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,8 +856,7 @@ dependencies = [ [[package]] name = "canhttp" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30b89e93857ec22d9b5f11b1647cdf9bb28c328e1d5ae871ecba2d94e38f8fb" +source = "git+https://github.com/dfinity/canhttp/?rev=0b935a2ba6defb0265bb94bbfb5c5675975531a3#0b935a2ba6defb0265bb94bbfb5c5675975531a3" dependencies = [ "assert_matches", "ciborium", @@ -866,6 +865,7 @@ dependencies = [ "http", "ic-cdk", "ic-error-types", + "ic-management-canister-types", "num-traits", "pin-project", "serde", @@ -885,7 +885,7 @@ dependencies = [ "candid", "canlog_derive", "ic-canister-log", - "ic0 1.0.1", + "ic0", "regex", "serde", "serde_json", @@ -1358,7 +1358,7 @@ dependencies = [ "evm_rpc_types", "ic-cdk", "ic-cdk-bindgen", - "ic-cdk-macros", + "ic-cdk-macros 0.17.2", "ic-certified-map", "serde", "serde_bytes", @@ -1560,7 +1560,6 @@ dependencies = [ "http", "ic-canister-log", "ic-cdk", - "ic-cdk-macros", "ic-crypto-test-utils-reproducible-rng", "ic-error-types", "ic-ethereum-types", @@ -2079,16 +2078,20 @@ 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", - "ic0 0.23.0", + "ic-cdk-macros 0.18.7", + "ic-error-types", + "ic-management-canister-types", + "ic0", "serde", "serde_bytes", + "slotmap", + "thiserror 2.0.16", ] [[package]] @@ -2102,9 +2105,13 @@ dependencies = [ [[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" @@ -2120,6 +2127,19 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "ic-cdk-macros" +version = "0.18.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb14c5d691cc9d72bb95459b4761e3a4b3444b85a63d17555d5ddd782969a1e" +dependencies = [ + "candid", + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "ic-certification" version = "3.0.3" @@ -2266,12 +2286,6 @@ dependencies = [ "thiserror 2.0.16", ] -[[package]] -name = "ic0" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de254dd67bbd58073e23dc1c8553ba12fa1dc610a19de94ad2bbcd0460c067f" - [[package]] name = "ic0" version = "1.0.1" @@ -4173,6 +4187,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" diff --git a/Cargo.toml b/Cargo.toml index 54c608a0..9da5356a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,6 @@ ic-metrics-encoder = { workspace = true } ic-stable-structures = { workspace = true } ic-canister-log = { workspace = true } ic-cdk = { workspace = true } -ic-cdk-macros = { workspace = true } ic-management-canister-types = { workspace = true } maplit = { workspace = true } minicbor = { workspace = true } @@ -76,7 +75,7 @@ alloy-rpc-types = "1.0.23" assert_matches = "1.5.0" async-trait = "0.1.88" candid = { version = "0.10.13" } -canhttp = { version = "0.2.0", features = ["json", "multi"] } +canhttp = { git = "https://github.com/dfinity/canhttp/", features = ["json", "multi"], rev = "0b935a2ba6defb0265bb94bbfb5c5675975531a3" } canlog = { version = "0.2.0", features = ["derive"] } candid_parser = { version = "0.1.4" } derive_more = { version = "2.0.1", features = ["from", "into"] } @@ -86,7 +85,7 @@ getrandom = { version = "0.2", features = ["custom"] } hex = "0.4.3" http = "1.3.1" ic-canister-log = "0.2.0" -ic-cdk = "0.17.2" +ic-cdk = "0.18.7" ic-cdk-bindgen = "0.1" ic-cdk-macros = "0.17.2" ic-certified-map = "0.4" diff --git a/e2e/motoko/main.mo b/e2e/motoko/main.mo index caa184b3..5672e8e5 100644 --- a/e2e/motoko/main.mo +++ b/e2e/motoko/main.mo @@ -14,7 +14,7 @@ shared ({ caller = installer }) persistent actor class Main() { // (`subnet name`, `nodes in subnet`, `expected cycles for JSON-RPC call`) type SubnetTarget = (Text, Nat32, Nat); - transient let fiduciarySubnet : SubnetTarget = ("fiduciary", 34, 540_545_600); + transient let fiduciarySubnet : SubnetTarget = ("fiduciary", 34, 400_299_200); transient let testTargets = [ // (`canister module`, `canister type`, `subnet`) diff --git a/e2e/rust/src/main.rs b/e2e/rust/src/main.rs index 2ad2cb1a..608e5f21 100644 --- a/e2e/rust/src/main.rs +++ b/e2e/rust/src/main.rs @@ -1,12 +1,10 @@ -use std::str::FromStr; - use candid::{candid_method, Principal}; -use ic_cdk_macros::update; - use evm_rpc_types::{ Block, BlockTag, ConsensusStrategy, EthMainnetService, Hex32, MultiRpcResult, ProviderError, RpcConfig, RpcError, RpcResult, RpcService, RpcServices, }; +use ic_cdk::{call::Call, update}; +use std::str::FromStr; fn main() {} @@ -21,7 +19,7 @@ const CANISTER_ID: Option<&str> = None; #[update] #[candid_method(update)] pub async fn test() { - assert!(ic_cdk::api::is_controller(&ic_cdk::caller())); + assert!(ic_cdk::api::is_controller(&ic_cdk::api::msg_caller())); let canister_id = Principal::from_str(CANISTER_ID.unwrap()) .expect("Error parsing canister ID environment variable"); @@ -34,18 +32,22 @@ pub async fn test() { ); // Get cycles cost - let (cycles_result,): (Result,) = - ic_cdk::api::call::call(canister_id, "requestCost", params.clone()) - .await - .unwrap(); + let cycles_result = Call::unbounded_wait(canister_id, "requestCost") + .with_args(¶ms) + .await + .unwrap() + .candid::>() + .unwrap(); let cycles = cycles_result .unwrap_or_else(|e| ic_cdk::trap(&format!("error in `request_cost`: {:?}", e))); // Call without sending cycles - let (result_without_cycles,): (Result,) = - ic_cdk::api::call::call(canister_id, "request", params.clone()) - .await - .unwrap(); + let result_without_cycles = Call::unbounded_wait(canister_id, "request") + .with_args(¶ms) + .await + .unwrap() + .candid::>() + .unwrap(); match result_without_cycles { Ok(s) => ic_cdk::trap(&format!("response from `request` without cycles: {:?}", s)), Err(RpcError::ProviderError(ProviderError::TooFewCycles { expected, .. })) => { @@ -55,10 +57,13 @@ pub async fn test() { } // Call with expected number of cycles - let (result,): (Result,) = - ic_cdk::api::call::call_with_payment128(canister_id, "request", params, cycles) - .await - .unwrap(); + let result: Result = Call::unbounded_wait(canister_id, "request") + .with_args(¶ms) + .with_cycles(cycles) + .await + .unwrap() + .candid::>() + .unwrap(); match result { Ok(response) => { // Check response structure around gas price @@ -72,10 +77,8 @@ pub async fn test() { } // Call a Candid-RPC method - let (results,): (MultiRpcResult,) = ic_cdk::api::call::call_with_payment128( - canister_id, - "eth_getBlockByNumber", - ( + let results = Call::unbounded_wait(canister_id, "eth_getBlockByNumber") + .with_args(&( RpcServices::EthMainnet(Some(vec![ // EthMainnetService::Ankr, // Need API key EthMainnetService::BlockPi, @@ -90,11 +93,12 @@ pub async fn test() { ..Default::default() }), BlockTag::Number(19709434_u32.into()), - ), - 10000000000, - ) - .await - .unwrap(); + )) + .with_cycles(10000000000) + .await + .unwrap() + .candid::>() + .unwrap(); match results { MultiRpcResult::Consistent(result) => match result { Ok(block) => { diff --git a/evm_rpc_client/src/fixtures/mod.rs b/evm_rpc_client/src/fixtures/mod.rs index 6144ccc2..f6da5e27 100644 --- a/evm_rpc_client/src/fixtures/mod.rs +++ b/evm_rpc_client/src/fixtures/mod.rs @@ -5,7 +5,7 @@ use crate::{ClientBuilder, Runtime}; use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Decode, Encode, Principal}; -use ic_error_types::RejectCode; +use ic_cdk::call::Error; use serde::de::DeserializeOwned; use std::collections::BTreeMap; @@ -82,7 +82,7 @@ impl StubRuntime { self } - fn call(&self, method: &str) -> Result + fn call(&self, method: &str) -> Result where Out: CandidType + DeserializeOwned, { @@ -109,7 +109,7 @@ impl Runtime for StubRuntime { method: &str, _args: In, _cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -122,7 +122,7 @@ impl Runtime for StubRuntime { _id: Principal, method: &str, _args: In, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 9487ca30..9030fdce 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -127,7 +127,6 @@ use evm_rpc_types::{ BlockTag, CallArgs, ConsensusStrategy, FeeHistoryArgs, GetLogsArgs, GetTransactionCountArgs, Hex, Hex32, RpcConfig, RpcServices, }; -use ic_error_types::RejectCode; #[cfg(feature = "alloy")] pub use request::alloy::AlloyResponseConverter; pub use request::CandidResponseConverter; @@ -783,7 +782,7 @@ impl EvmRpcClient { async fn try_execute_request( &self, request: Request, - ) -> Result + ) -> Result where Config: CandidType + Send, Params: CandidType + Send, diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index dc3e5086..dab932e8 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -7,7 +7,6 @@ use evm_rpc_types::{ BlockTag, CallArgs, FeeHistoryArgs, GetLogsArgs, GetLogsRpcConfig, GetTransactionCountArgs, Hex, Hex20, Hex32, MultiRpcResult, Nat256, RpcConfig, RpcServices, }; -use ic_error_types::RejectCode; use serde::de::DeserializeOwned; use std::fmt::{Debug, Formatter}; use strum::EnumIter; @@ -507,8 +506,9 @@ impl } /// Constructs the [`Request`] and sends it using the [`EvmRpcClient`]. This method returns - /// either the request response or any error that occurs while sending the request. - pub async fn try_send(self) -> Result + /// either the request response or any [`ic_cdk::call::Error`] that occurs while sending the + /// request. + pub async fn try_send(self) -> Result where Config: CandidType + Send, Params: CandidType + Send, diff --git a/evm_rpc_client/src/runtime/mod.rs b/evm_rpc_client/src/runtime/mod.rs index 2cd89d29..4ae0ee44 100644 --- a/evm_rpc_client/src/runtime/mod.rs +++ b/evm_rpc_client/src/runtime/mod.rs @@ -1,8 +1,7 @@ use async_trait::async_trait; use candid::utils::ArgumentEncoder; use candid::{CandidType, Principal}; -use ic_cdk::api::call::RejectionCode as IcCdkRejectionCode; -use ic_error_types::RejectCode; +use ic_cdk::call::{Call, Error}; use serde::de::DeserializeOwned; /// Abstract the canister runtime so that the client code can be reused: @@ -18,7 +17,7 @@ pub trait Runtime { method: &str, args: In, cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned; @@ -29,7 +28,7 @@ pub trait Runtime { id: Principal, method: &str, args: In, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned; @@ -47,50 +46,28 @@ impl Runtime for IcRuntime { method: &str, args: In, cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, { - ic_cdk::api::call::call_with_payment128(id, method, args, cycles) + Call::unbounded_wait(id, method) + .with_args(&args) + .with_cycles(cycles) .await - .map(|(res,)| res) - .map_err(|(code, message)| (convert_reject_code(code), message)) + .map_err(Error::from) + .and_then(|response| response.candid::().map_err(Error::from)) } - async fn query_call( - &self, - id: Principal, - method: &str, - args: In, - ) -> Result + async fn query_call(&self, id: Principal, method: &str, args: In) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, { - ic_cdk::api::call::call(id, method, args) + Call::unbounded_wait(id, method) + .with_args(&args) .await - .map(|(res,)| res) - .map_err(|(code, message)| (convert_reject_code(code), message)) - } -} - -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 - } - IcCdkRejectionCode::NoError => { - unreachable!("inter-canister calls should never produce a RejectionCode::NoError error") - } + .map_err(Error::from) + .and_then(|response| response.candid::().map_err(Error::from)) } } diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index 4c3481bd..24fa9b12 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -233,3 +233,16 @@ impl From for LegacyRejectionCode { } } } + +impl From for LegacyRejectionCode { + fn from(value: u32) -> Self { + match value { + 1 => LegacyRejectionCode::SysFatal, + 2 => LegacyRejectionCode::SysTransient, + 3 => LegacyRejectionCode::DestinationInvalid, + 4 => LegacyRejectionCode::CanisterReject, + 5 => LegacyRejectionCode::CanisterError, + _ => LegacyRejectionCode::Unknown, + } + } +} diff --git a/src/http.rs b/src/http.rs index 1b061956..f032b666 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,15 +1,15 @@ -use crate::constants::COLLATERAL_CYCLES_PER_NODE; -use crate::logs::Priority; -use crate::memory::{get_num_subnet_nodes, is_demo_active, next_request_id}; use crate::{ add_metric_entry, - constants::CONTENT_TYPE_VALUE, - memory::get_override_provider, + constants::{COLLATERAL_CYCLES_PER_NODE, CONTENT_TYPE_VALUE}, + logs::Priority, + memory::{get_num_subnet_nodes, get_override_provider, is_demo_active, next_request_id}, types::{MetricRpcHost, MetricRpcMethod, ResolvedRpcService}, util::canonicalize_json, }; +use canhttp::cycles::ChargeCallerError; use canhttp::{ convert::ConvertRequestLayer, + cycles::{ChargeCaller, CyclesAccounting}, http::{ json::{ ConsistentResponseIdFilterError, CreateJsonRpcIdFilter, HttpJsonRpcRequest, @@ -22,30 +22,29 @@ use canhttp::{ }, observability::ObservabilityLayer, retry::DoubleMaxResponseBytes, - ConvertServiceBuilder, CyclesAccounting, CyclesAccountingError, CyclesChargingPolicy, - HttpsOutcallError, IcError, MaxResponseBytesRequestExtension, TransformContextRequestExtension, + ConvertServiceBuilder, HttpsOutcallError, MaxResponseBytesRequestExtension, + TransformContextRequestExtension, }; use canlog::log; use evm_rpc_types::{ HttpOutcallError, LegacyRejectionCode, ProviderError, RpcError, RpcResult, ValidationError, }; -use http::header::CONTENT_TYPE; -use http::HeaderValue; -use ic_cdk::api::management_canister::http_request::{ - CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse, TransformArgs, - TransformContext, +use http::{header::CONTENT_TYPE, HeaderValue}; +use ic_cdk::call::Error as IcError; +use ic_management_canister_types::{ + HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse, TransformArgs, + TransformContext, TransformFunc, }; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::cmp::PartialEq; +use serde::{de::DeserializeOwned, Serialize}; use std::fmt::Debug; use thiserror::Error; -use tower::layer::util::{Identity, Stack}; -use tower::retry::RetryLayer; -use tower::util::MapRequestLayer; -use tower::{Service, ServiceBuilder}; -use tower_http::set_header::SetRequestHeaderLayer; -use tower_http::ServiceBuilderExt; +use tower::{ + layer::util::{Identity, Stack}, + retry::RetryLayer, + util::MapRequestLayer, + Service, ServiceBuilder, +}; +use tower_http::{set_header::SetRequestHeaderLayer, ServiceBuilderExt}; pub fn json_rpc_request_arg( service: ResolvedRpcService, @@ -61,10 +60,13 @@ pub fn json_rpc_request_arg( service .post(&get_override_provider())? .max_response_bytes(max_response_bytes) - .transform_context(TransformContext::from_name( - "__transform_json_rpc".to_string(), - vec![], - )) + .transform_context(TransformContext { + function: TransformFunc(candid::Func { + method: "__transform_json_rpc".to_string(), + principal: ic_cdk::api::canister_self(), + }), + context: vec![], + }) .body(body) .map_err(|e| { RpcError::ValidationError(ValidationError::Custom(format!("Invalid request: {e}"))) @@ -142,7 +144,7 @@ where }) .on_error( |req_data: MetricData, error: &HttpClientError| match error { - HttpClientError::IcError(IcError { code, message }) => { + HttpClientError::IcError(error) => { if error.is_response_too_large() { add_metric_entry!( err_max_response_size_exceeded, @@ -150,20 +152,32 @@ where 1 ); } else { - add_metric_entry!( - err_http_outcall, - (req_data.method, req_data.host, LegacyRejectionCode::from(*code)), - 1 - ); log!( Priority::TraceHttp, - "IC Error for request with id `{}` with code `{}` and message `{}`", + "IC error for request with id `{}`: {}", req_data.request_id, - code, - message, + error ); + match error { + IcError::CallRejected(error) => { + add_metric_entry!( + err_http_outcall, + (req_data.method, req_data.host, LegacyRejectionCode::from(error.raw_reject_code())), + 1 + ); + } + IcError::CallPerformFailed(_) => { + add_metric_entry!( + err_http_outcall, + (req_data.method, req_data.host, LegacyRejectionCode::SysTransient), + 1 + ); + } + IcError::InsufficientLiquidCycleBalance(_) | IcError::CandidDecodeFailed(_) => {} + } } } + HttpClientError::UnsuccessfulHttpResponse( FilterNonSuccessfulHttpResponseError::UnsuccessfulResponse(response), ) => { @@ -215,10 +229,7 @@ where .convert_response(JsonResponseConverter::new()) .convert_response(FilterNonSuccessfulHttpResponse) .convert_response(HttpResponseConverter) - .convert_request(CyclesAccounting::new( - get_num_subnet_nodes(), - ChargingPolicyWithCollateral::default(), - )) + .convert_request(CyclesAccounting::new(charging_policy_with_collateral())) .service(canhttp::Client::new_with_error::()) } @@ -263,7 +274,7 @@ pub enum HttpClientError { #[error("unknown error (most likely sign of a bug): {0}")] NotHandledError(String), #[error("cycles accounting error: {0}")] - CyclesAccountingError(CyclesAccountingError), + CyclesAccountingError(ChargeCallerError), #[error("HTTP response was not successful: {0}")] UnsuccessfulHttpResponse(FilterNonSuccessfulHttpResponseError>), #[error("Error converting response to JSON: {0}")] @@ -297,8 +308,8 @@ impl From for HttpClientError { } } -impl From for HttpClientError { - fn from(value: CyclesAccountingError) -> Self { +impl From for HttpClientError { + fn from(value: ChargeCallerError) -> Self { HttpClientError::CyclesAccountingError(value) } } @@ -324,17 +335,29 @@ impl From for HttpClientError { impl From for RpcError { fn from(error: HttpClientError) -> Self { match error { - HttpClientError::IcError(IcError { code, message }) => { + HttpClientError::IcError(IcError::CallRejected(e)) => { RpcError::HttpOutcallError(HttpOutcallError::IcError { - code: LegacyRejectionCode::from(code), - message, + code: LegacyRejectionCode::from(e.raw_reject_code()), + message: e.reject_message().to_string(), }) } + HttpClientError::IcError(IcError::CallPerformFailed(e)) => { + RpcError::HttpOutcallError(HttpOutcallError::IcError { + code: LegacyRejectionCode::SysTransient, + message: e.to_string(), + }) + } + HttpClientError::IcError(IcError::CandidDecodeFailed(e)) => { + panic!("{}", e.to_string()) + } + HttpClientError::IcError(IcError::InsufficientLiquidCycleBalance(e)) => { + panic!("{}", e.to_string()) + } HttpClientError::NotHandledError(e) => { RpcError::ValidationError(ValidationError::Custom(e)) } HttpClientError::CyclesAccountingError( - CyclesAccountingError::InsufficientCyclesError { expected, received }, + ChargeCallerError::InsufficientCyclesError { expected, received }, ) => RpcError::ProviderError(ProviderError::TooFewCycles { expected, received }), HttpClientError::InvalidJsonResponse( JsonResponseConversionError::InvalidJsonResponse { @@ -380,44 +403,18 @@ struct MetricData { request_id: Id, } -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct ChargingPolicyWithCollateral { - charge_user: bool, - collateral_cycles: u128, -} - -impl ChargingPolicyWithCollateral { - pub fn new( - num_nodes_in_subnet: u32, - charge_user: bool, - collateral_cycles_per_node: u128, - ) -> Self { - let collateral_cycles = - collateral_cycles_per_node.saturating_mul(num_nodes_in_subnet as u128); - Self { - charge_user, - collateral_cycles, - } - } -} - -impl Default for ChargingPolicyWithCollateral { - fn default() -> Self { - Self::new( - get_num_subnet_nodes(), - !is_demo_active(), - COLLATERAL_CYCLES_PER_NODE, - ) - } -} - -impl CyclesChargingPolicy for ChargingPolicyWithCollateral { - fn cycles_to_charge(&self, _request: &IcHttpRequest, attached_cycles: u128) -> u128 { - if self.charge_user { - return attached_cycles.saturating_add(self.collateral_cycles); +pub fn charging_policy_with_collateral( +) -> ChargeCaller u128 + Clone> { + let charge_caller = if is_demo_active() { + |_request: &IcHttpRequest, _request_cost| 0 + } else { + |_request: &IcHttpRequest, request_cost| { + let collateral_cycles = + COLLATERAL_CYCLES_PER_NODE.saturating_mul(get_num_subnet_nodes() as u128); + request_cost + collateral_cycles } - 0 - } + }; + ChargeCaller::new(charge_caller) } pub fn transform_http_request(args: TransformArgs) -> IcHttpResponse { diff --git a/src/main.rs b/src/main.rs index 6dc66668..40e4cea4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,11 @@ use candid::candid_method; -use canhttp::{multi::Timestamp, CyclesChargingPolicy, CyclesCostEstimator}; +use canhttp::{cycles::CyclesChargingPolicy, multi::Timestamp}; use canlog::{Log, Sort}; use evm_rpc::{ candid_rpc::CandidRpcClient, http::{ - json_rpc_request, json_rpc_request_arg, service_request_builder, transform_http_request, - ChargingPolicyWithCollateral, + charging_policy_with_collateral, json_rpc_request, json_rpc_request_arg, + service_request_builder, transform_http_request, }, logs::{Priority, INFO}, memory::{ @@ -19,23 +19,17 @@ use evm_rpc::{ }; use evm_rpc_types::{Hex32, HttpOutcallError, MultiRpcResult, RpcConfig, RpcResult, RpcServices}; use ic_canister_log::log; -use ic_cdk::{ - api::{ - is_controller, - management_canister::http_request::{ - CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse, - TransformArgs, - }, - }, - query, update, -}; +use ic_cdk::{api::is_controller, query, update}; use ic_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; +use ic_management_canister_types::{ + HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse, TransformArgs, +}; use ic_metrics_encoder::MetricsEncoder; use std::str::FromStr; use tower::Service; pub fn require_api_key_principal_or_controller() -> Result<(), String> { - let caller = ic_cdk::caller(); + let caller = ic_cdk::api::msg_caller(); if is_api_key_principal(&caller) || is_controller(&caller) { Ok(()) } else { @@ -153,8 +147,8 @@ pub async fn multi_request( } } -#[update] -#[candid_method] +#[update(name = "request")] +#[candid_method(rename = "request")] async fn request( service: evm_rpc_types::RpcService, json_rpc_payload: String, @@ -205,12 +199,12 @@ async fn request_cost( .expect("Error: invalid request") .into_body(); - let cycles_to_attach = { - let estimator = CyclesCostEstimator::new(get_num_subnet_nodes()); - estimator.cost_of_http_request(&request) - }; - let estimator = ChargingPolicyWithCollateral::default(); - Ok(estimator.cycles_to_charge(&request, cycles_to_attach)) + let request_cost_with_collateral = charging_policy_with_collateral().cycles_to_charge( + &request, + ic_cdk::management_canister::cost_http_request(&request), + ); + + Ok(request_cost_with_collateral) } } @@ -280,7 +274,7 @@ async fn update_api_keys(api_keys: Vec<(ProviderId, Option)>) { log!( INFO, "[{}] Updating API keys for providers: {}", - ic_cdk::caller(), + ic_cdk::api::msg_caller(), api_keys .iter() .map(|(id, _)| id.to_string()) diff --git a/src/metrics.rs b/src/metrics.rs index f5fe2f48..6ad66c64 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -60,7 +60,7 @@ pub fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> st w.gauge_vec("cycle_balance", "Cycle balance of this canister")? .value( &[("canister", "evmrpc")], - ic_cdk::api::canister_balance128().metric_value(), + ic_cdk::api::canister_cycle_balance().metric_value(), )?; w.encode_gauge( "evmrpc_canister_version", @@ -69,7 +69,7 @@ pub fn encode_metrics(w: &mut ic_metrics_encoder::MetricsEncoder>) -> st )?; w.encode_gauge( "stable_memory_bytes", - ic_cdk::api::stable::stable_size() as f64 * WASM_PAGE_SIZE_IN_BYTES, + ic_cdk::stable::stable_size() as f64 * WASM_PAGE_SIZE_IN_BYTES, "Size of the stable memory allocated by this canister.", )?; diff --git a/src/rpc_client/eth_rpc/mod.rs b/src/rpc_client/eth_rpc/mod.rs index 6113d07d..caf8babf 100644 --- a/src/rpc_client/eth_rpc/mod.rs +++ b/src/rpc_client/eth_rpc/mod.rs @@ -1,17 +1,17 @@ //! This module contains definitions for communicating witEthereum API using the [JSON RPC](https://ethereum.org/en/developers/docs/apis/json-rpc/) //! interface. -use crate::rpc_client::eth_rpc_error::{sanitize_send_raw_transaction_result, Parser}; -use crate::rpc_client::json::responses::{Block, FeeHistory, LogEntry, TransactionReceipt}; -use crate::rpc_client::numeric::{TransactionCount, Wei}; -use candid::candid_method; +use crate::rpc_client::{ + eth_rpc_error::{sanitize_send_raw_transaction_result, Parser}, + json::responses::{Block, FeeHistory, LogEntry, TransactionReceipt}, + numeric::{TransactionCount, Wei}, +}; use canhttp::http::json::JsonRpcResponse; -use ic_cdk::api::management_canister::http_request::{HttpResponse, TransformArgs}; -use ic_cdk_macros::query; +use ic_cdk::query; +use ic_management_canister_types::{HttpRequestResult, TransformArgs}; use minicbor::{Decode, Encode}; use serde::{de::DeserializeOwned, Serialize}; -use std::fmt; -use std::fmt::Debug; +use std::{fmt, fmt::Debug}; #[cfg(test)] mod tests; @@ -95,8 +95,8 @@ impl ResponseTransform { } #[query] -#[candid_method(query)] -fn cleanup_response(mut args: TransformArgs) -> HttpResponse { +#[allow(unused_mut)] // Clearing the response header requires `args` to be `mut` +fn cleanup_response(mut args: TransformArgs) -> HttpRequestResult { args.response.headers.clear(); let status_ok = args.response.status >= 200u16 && args.response.status < 300u16; if status_ok && !args.context.is_empty() { diff --git a/src/rpc_client/mod.rs b/src/rpc_client/mod.rs index 6a807599..dcce4b31 100644 --- a/src/rpc_client/mod.rs +++ b/src/rpc_client/mod.rs @@ -1,4 +1,3 @@ -use crate::types::RpcMethod; use crate::{ http::http_client, memory::{get_override_provider, rank_providers, record_ok_result}, @@ -8,7 +7,7 @@ use crate::{ json::responses::RawJson, numeric::TransactionCount, }, - types::MetricRpcMethod, + types::{MetricRpcMethod, RpcMethod}, }; use canhttp::{ http::json::JsonRpcRequest, @@ -18,7 +17,7 @@ use canhttp::{ use evm_rpc_types::{ ConsensusStrategy, JsonRpcError, ProviderError, RpcConfig, RpcError, RpcService, RpcServices, }; -use ic_cdk::api::management_canister::http_request::TransformContext; +use ic_management_canister_types::{TransformContext, TransformFunc}; use json::{ requests::{ BlockSpec, EthCallParams, FeeHistoryParams, GetBlockByNumberParams, GetLogsParam, @@ -297,10 +296,13 @@ impl EthRpcClient { .map(|builder| { builder .max_response_bytes(effective_size_estimate) - .transform_context(TransformContext::from_name( - "cleanup_response".to_owned(), - transform_op.clone(), - )) + .transform_context(TransformContext { + function: TransformFunc(candid::Func { + method: "cleanup_response".to_string(), + principal: ic_cdk::api::canister_self(), + }), + context: transform_op.clone(), + }) .body(JsonRpcRequest::new(method.clone().name(), params.clone())) .expect("BUG: invalid request") }); diff --git a/tests/mock_http_runtime/mod.rs b/tests/mock_http_runtime/mod.rs index a44831b3..5e7eac00 100644 --- a/tests/mock_http_runtime/mod.rs +++ b/tests/mock_http_runtime/mod.rs @@ -2,9 +2,10 @@ pub mod mock; use crate::MAX_TICKS; use async_trait::async_trait; -use candid::{decode_args, utils::ArgumentEncoder, CandidType, Principal}; +use candid::{decode_one, utils::ArgumentEncoder, CandidType, Principal}; use evm_rpc::constants::DEFAULT_MAX_RESPONSE_BYTES; use evm_rpc_client::Runtime; +use ic_cdk::call::{CallFailed, CallRejected, Error}; use ic_error_types::RejectCode; use mock::MockHttpOutcalls; use pocket_ic::{ @@ -34,7 +35,7 @@ impl Runtime for MockHttpRuntime { method: &str, args: In, _cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -58,7 +59,7 @@ impl Runtime for MockHttpRuntime { id: Principal, method: &str, args: In, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -120,37 +121,36 @@ fn check_response_size( response } -fn parse_reject_response(response: RejectResponse) -> (RejectCode, String) { - use pocket_ic::RejectCode as PocketIcRejectCode; - let rejection_code = match response.reject_code { - PocketIcRejectCode::SysFatal => RejectCode::SysFatal, - PocketIcRejectCode::SysTransient => RejectCode::SysTransient, - PocketIcRejectCode::DestinationInvalid => RejectCode::DestinationInvalid, - PocketIcRejectCode::CanisterReject => RejectCode::CanisterReject, - PocketIcRejectCode::CanisterError => RejectCode::CanisterError, - PocketIcRejectCode::SysUnknown => RejectCode::SysUnknown, - }; - (rejection_code, response.reject_message) +fn parse_reject_response(response: RejectResponse) -> Error { + CallFailed::CallRejected(CallRejected::with_rejection( + response.reject_code as u32, + response.reject_message, + )) + .into() } pub fn encode_args(args: In) -> Vec { candid::encode_args(args).expect("Failed to encode arguments.") } -pub fn decode_call_response(bytes: Vec) -> Result +pub fn decode_call_response(bytes: Vec) -> Result where Out: CandidType + DeserializeOwned, { - decode_args(&bytes).map(|(res,)| res).map_err(|e| { - ( - RejectCode::CanisterError, - format!( - "failed to decode canister response as {}: {}", - std::any::type_name::(), - e - ), - ) - }) + decode_one(&bytes) + .map_err(|e| { + // This would normally map to a `ic_cdk::call::CandidDecodeFailed`, but there is no + // way to instantiate this error from outside. + CallFailed::CallRejected(CallRejected::with_rejection( + RejectCode::CanisterError as u32, + format!( + "failed to decode canister response as {}: {}", + std::any::type_name::(), + e + ), + )) + }) + .map_err(Error::from) } async fn tick_until_http_requests(env: &PocketIc) -> Vec { diff --git a/tests/setup/mod.rs b/tests/setup/mod.rs index 2e7069c7..59f6c723 100644 --- a/tests/setup/mod.rs +++ b/tests/setup/mod.rs @@ -11,8 +11,8 @@ use evm_rpc::{ }; use evm_rpc_client::{AlloyResponseConverter, ClientBuilder, EvmRpcClient, Runtime}; use evm_rpc_types::{InstallArgs, Provider, RpcResult, RpcService}; -use ic_cdk::api::management_canister::main::CanisterId; use ic_http_types::{HttpRequest, HttpResponse}; +use ic_management_canister_types::CanisterId; use ic_management_canister_types::CanisterSettings; use ic_metrics_assert::{MetricsAssert, PocketIcAsyncHttpQuery}; use ic_test_utilities_load_wasm::load_wasm; @@ -239,12 +239,11 @@ impl EvmRpcSetup { input: Vec, caller: Principal, ) -> R { - let candid = &assert_reply( + decode_reply( self.env .query_call(self.canister_id, caller, method, input) .await, - ); - Decode!(candid, R).expect("error while decoding Candid response from query call") + ) } async fn call_update( @@ -253,12 +252,11 @@ impl EvmRpcSetup { input: Vec, caller: Principal, ) -> R { - let candid = &assert_reply( + decode_reply( self.env .update_call(self.canister_id, caller, method, input) .await, - ); - Decode!(candid, R).expect("error while decoding Candid response from query call") + ) } } @@ -276,6 +274,10 @@ fn evm_rpc_wasm() -> Vec { load_wasm(std::env::var("CARGO_MANIFEST_DIR").unwrap(), "evm_rpc", &[]) } -fn assert_reply(result: Result, RejectResponse>) -> Vec { - result.unwrap_or_else(|e| panic!("Expected a successful reply, got error {e}")) +fn decode_reply(result: Result, RejectResponse>) -> R { + Decode!( + &result.unwrap_or_else(|e| panic!("Expected a successful reply, got error {e}")), + R + ) + .unwrap_or_else(|e| panic!("Error while decoding Candid response: {e}")) } diff --git a/tests/tests.rs b/tests/tests.rs index 67878010..459eb264 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1015,71 +1015,6 @@ async fn candid_rpc_should_err_without_cycles() { } } -#[tokio::test] -async fn candid_rpc_should_err_with_insufficient_cycles() { - let setup = EvmRpcSetup::with_args(InstallArgs { - demo: Some(true), - nodes_in_subnet: Some(33), - ..Default::default() - }) - .await - .mock_api_keys() - .await; - - let mut result = setup - .client(MockHttpOutcalls::NEVER) - .with_rpc_sources(RpcServices::EthMainnet(None)) - .build() - .get_transaction_receipt(b256!( - "0xdd5d4b18923d7aae953c7996d791118102e889bea37b48a651157a4890e4746f" - )) - .send() - .await - .expect_inconsistent(); - let regex = regex::Regex::new( - "http_request request sent with [0-9_]+ cycles, but [0-9_]+ cycles are required.", - ) - .unwrap(); - assert_matches!( - result.pop().unwrap(), - ( - RpcService::EthMainnet(EthMainnetService::PublicNode), - Err(RpcError::HttpOutcallError(HttpOutcallError::IcError { - code: LegacyRejectionCode::CanisterReject, - message - })) - ) if regex.is_match(&message) - ); - - // Same request should succeed after upgrade to the expected node count - setup - .upgrade_canister(InstallArgs { - nodes_in_subnet: Some(34), - ..Default::default() - }) - .await; - - let mocks = MockHttpOutcallsBuilder::new() - .given(get_transaction_receipt_request().with_id(0_u64)) - .respond_with(get_transaction_receipt_response().with_id(0_u64)) - .given(get_transaction_receipt_request().with_id(1_u64)) - .respond_with(get_transaction_receipt_response().with_id(1_u64)) - .given(get_transaction_receipt_request().with_id(2_u64)) - .respond_with(get_transaction_receipt_response().with_id(2_u64)); - let result = setup - .client(mocks) - .with_rpc_sources(RpcServices::EthMainnet(None)) - .build() - .get_transaction_receipt(b256!( - "0xdd5d4b18923d7aae953c7996d791118102e889bea37b48a651157a4890e4746f" - )) - .send() - .await - .expect_consistent() - .unwrap(); - assert_matches!(result, Some(alloy_rpc_types::TransactionReceipt { .. })); -} - #[tokio::test] async fn candid_rpc_should_err_when_service_unavailable() { let setup = EvmRpcSetup::new().await.mock_api_keys().await; @@ -2593,29 +2528,6 @@ fn get_transaction_count_response() -> JsonRpcResponse { JsonRpcResponse::from(json!({ "jsonrpc" : "2.0", "id" : 0, "result" : "0x1" })) } -fn get_transaction_receipt_response() -> JsonRpcResponse { - JsonRpcResponse::from(json!({ - "jsonrpc": "2.0", - "id": 0, - "result": { - "blockHash": "0x5115c07eb1f20a9d6410db0916ed3df626cfdab161d3904f45c8c8b65c90d0be", - "blockNumber": "0x11a85ab", - "contractAddress": null, - "cumulativeGasUsed": "0xf02aed", - "effectiveGasPrice": "0x63c00ee76", - "from": "0x0aa8ebb6ad5a8e499e550ae2c461197624c6e667", - "gasUsed": "0x7d89", - "logs": [], - "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "status": "0x1", - "to": "0x356cfd6e6d0000400000003900b415f80669009e", - "transactionHash": "0xdd5d4b18923d7aae953c7996d791118102e889bea37b48a651157a4890e4746f", - "transactionIndex": "0xd9", - "type": "0x2" - } - })) -} - fn send_raw_transaction_response() -> JsonRpcResponse { JsonRpcResponse::from(json!({ "id": 0, "jsonrpc": "2.0", "result": MOCK_TRANSACTION_HASH })) } From 2a5f7b73ce91faf5cce08726f03b979fe166efff Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 7 Oct 2025 10:58:03 +0200 Subject: [PATCH 2/6] XC-502: Update `canhttp` to latest commit --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/http.rs | 36 +++++++++--------------------------- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a80c740c..d022b87c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,7 +856,7 @@ dependencies = [ [[package]] name = "canhttp" version = "0.2.1" -source = "git+https://github.com/dfinity/canhttp/?rev=0b935a2ba6defb0265bb94bbfb5c5675975531a3#0b935a2ba6defb0265bb94bbfb5c5675975531a3" +source = "git+https://github.com/dfinity/canhttp/?rev=724e87719d2f5d43dc463ed8d881c0fc0f3a4541#724e87719d2f5d43dc463ed8d881c0fc0f3a4541" dependencies = [ "assert_matches", "ciborium", diff --git a/Cargo.toml b/Cargo.toml index 9da5356a..27a907c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ alloy-rpc-types = "1.0.23" assert_matches = "1.5.0" async-trait = "0.1.88" candid = { version = "0.10.13" } -canhttp = { git = "https://github.com/dfinity/canhttp/", features = ["json", "multi"], rev = "0b935a2ba6defb0265bb94bbfb5c5675975531a3" } +canhttp = { git = "https://github.com/dfinity/canhttp/", features = ["json", "multi"], rev = "724e87719d2f5d43dc463ed8d881c0fc0f3a4541" } canlog = { version = "0.2.0", features = ["derive"] } candid_parser = { version = "0.1.4" } derive_more = { version = "2.0.1", features = ["from", "into"] } diff --git a/src/http.rs b/src/http.rs index f032b666..f72b60a5 100644 --- a/src/http.rs +++ b/src/http.rs @@ -6,10 +6,9 @@ use crate::{ types::{MetricRpcHost, MetricRpcMethod, ResolvedRpcService}, util::canonicalize_json, }; -use canhttp::cycles::ChargeCallerError; use canhttp::{ convert::ConvertRequestLayer, - cycles::{ChargeCaller, CyclesAccounting}, + cycles::{ChargeCaller, ChargeCallerError, CyclesAccounting}, http::{ json::{ ConsistentResponseIdFilterError, CreateJsonRpcIdFilter, HttpJsonRpcRequest, @@ -22,7 +21,7 @@ use canhttp::{ }, observability::ObservabilityLayer, retry::DoubleMaxResponseBytes, - ConvertServiceBuilder, HttpsOutcallError, MaxResponseBytesRequestExtension, + ConvertServiceBuilder, HttpsOutcallError, IcError, MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; use canlog::log; @@ -30,7 +29,6 @@ use evm_rpc_types::{ HttpOutcallError, LegacyRejectionCode, ProviderError, RpcError, RpcResult, ValidationError, }; use http::{header::CONTENT_TYPE, HeaderValue}; -use ic_cdk::call::Error as IcError; use ic_management_canister_types::{ HttpRequestArgs as IcHttpRequest, HttpRequestResult as IcHttpResponse, TransformArgs, TransformContext, TransformFunc, @@ -159,21 +157,14 @@ where error ); match error { - IcError::CallRejected(error) => { + IcError::CallRejected {code, ..} => { add_metric_entry!( err_http_outcall, - (req_data.method, req_data.host, LegacyRejectionCode::from(error.raw_reject_code())), + (req_data.method, req_data.host, LegacyRejectionCode::from(*code)), 1 ); } - IcError::CallPerformFailed(_) => { - add_metric_entry!( - err_http_outcall, - (req_data.method, req_data.host, LegacyRejectionCode::SysTransient), - 1 - ); - } - IcError::InsufficientLiquidCycleBalance(_) | IcError::CandidDecodeFailed(_) => {} + IcError::InsufficientLiquidCycleBalance {..} => {} } } } @@ -335,22 +326,13 @@ impl From for HttpClientError { impl From for RpcError { fn from(error: HttpClientError) -> Self { match error { - HttpClientError::IcError(IcError::CallRejected(e)) => { + HttpClientError::IcError(IcError::CallRejected { code, message }) => { RpcError::HttpOutcallError(HttpOutcallError::IcError { - code: LegacyRejectionCode::from(e.raw_reject_code()), - message: e.reject_message().to_string(), + code: LegacyRejectionCode::from(code), + message, }) } - HttpClientError::IcError(IcError::CallPerformFailed(e)) => { - RpcError::HttpOutcallError(HttpOutcallError::IcError { - code: LegacyRejectionCode::SysTransient, - message: e.to_string(), - }) - } - HttpClientError::IcError(IcError::CandidDecodeFailed(e)) => { - panic!("{}", e.to_string()) - } - HttpClientError::IcError(IcError::InsufficientLiquidCycleBalance(e)) => { + e @ HttpClientError::IcError { .. } => { panic!("{}", e.to_string()) } HttpClientError::NotHandledError(e) => { From 3e62ad37eef1ce9655a1ff47a41037d7b6c49bd6 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 8 Oct 2025 11:59:28 +0200 Subject: [PATCH 3/6] XC-502: Remove `#[allow(unused_mut)]` --- src/rpc_client/eth_rpc/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rpc_client/eth_rpc/mod.rs b/src/rpc_client/eth_rpc/mod.rs index caf8babf..ae86aacd 100644 --- a/src/rpc_client/eth_rpc/mod.rs +++ b/src/rpc_client/eth_rpc/mod.rs @@ -95,8 +95,8 @@ impl ResponseTransform { } #[query] -#[allow(unused_mut)] // Clearing the response header requires `args` to be `mut` -fn cleanup_response(mut args: TransformArgs) -> HttpRequestResult { +fn cleanup_response(args: TransformArgs) -> HttpRequestResult { + let mut args = args; args.response.headers.clear(); let status_ok = args.response.status >= 200u16 && args.response.status < 300u16; if status_ok && !args.context.is_empty() { From 66936559b874184b362e11922cc7c8fb83817488 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 8 Oct 2025 12:45:14 +0200 Subject: [PATCH 4/6] XC-502: Use custom `IcError` instead of `ic_cdk::call::Error` --- Cargo.lock | 1 + evm_rpc_client/Cargo.toml | 1 + evm_rpc_client/src/fixtures/mod.rs | 9 ++- evm_rpc_client/src/lib.rs | 4 +- evm_rpc_client/src/request/mod.rs | 6 +- evm_rpc_client/src/runtime/mod.rs | 94 ++++++++++++++++++++++++++---- tests/mock_http_runtime/mod.rs | 33 +++++------ 7 files changed, 107 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d022b87c..5c93004d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1603,6 +1603,7 @@ dependencies = [ "serde", "serde_json", "strum 0.27.2", + "thiserror 2.0.16", "tokio", ] diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index f91ce96b..d0a16640 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -21,6 +21,7 @@ ic-error-types = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } strum = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] alloy-dyn-abi = { workspace = true } diff --git a/evm_rpc_client/src/fixtures/mod.rs b/evm_rpc_client/src/fixtures/mod.rs index f6da5e27..a23f4bc4 100644 --- a/evm_rpc_client/src/fixtures/mod.rs +++ b/evm_rpc_client/src/fixtures/mod.rs @@ -2,10 +2,9 @@ //! //! Types and methods for this module are only available for non-canister architecture (non `wasm32`). -use crate::{ClientBuilder, Runtime}; +use crate::{ClientBuilder, IcError, Runtime}; use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Decode, Encode, Principal}; -use ic_cdk::call::Error; use serde::de::DeserializeOwned; use std::collections::BTreeMap; @@ -82,7 +81,7 @@ impl StubRuntime { self } - fn call(&self, method: &str) -> Result + fn call(&self, method: &str) -> Result where Out: CandidType + DeserializeOwned, { @@ -109,7 +108,7 @@ impl Runtime for StubRuntime { method: &str, _args: In, _cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -122,7 +121,7 @@ impl Runtime for StubRuntime { _id: Principal, method: &str, _args: In, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 9030fdce..62366947 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -138,7 +138,7 @@ use request::{ GetTransactionReceiptRequestBuilder, JsonRequest, JsonRequestBuilder, Request, RequestBuilder, SendRawTransactionRequest, SendRawTransactionRequestBuilder, }; -pub use runtime::{IcRuntime, Runtime}; +pub use runtime::{IcError, IcRuntime, Runtime}; use serde::de::DeserializeOwned; use std::sync::Arc; @@ -782,7 +782,7 @@ impl EvmRpcClient { async fn try_execute_request( &self, request: Request, - ) -> Result + ) -> Result where Config: CandidType + Send, Params: CandidType + Send, diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index dab932e8..c69c0bbe 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "alloy")] pub(crate) mod alloy; +use crate::runtime::IcError; use crate::{EvmRpcClient, Runtime}; use candid::CandidType; use evm_rpc_types::{ @@ -506,9 +507,8 @@ impl } /// Constructs the [`Request`] and sends it using the [`EvmRpcClient`]. This method returns - /// either the request response or any [`ic_cdk::call::Error`] that occurs while sending the - /// request. - pub async fn try_send(self) -> Result + /// either the request response or any error that occurs while sending the request. + pub async fn try_send(self) -> Result where Config: CandidType + Send, Params: CandidType + Send, diff --git a/evm_rpc_client/src/runtime/mod.rs b/evm_rpc_client/src/runtime/mod.rs index 4ae0ee44..f28e6ffa 100644 --- a/evm_rpc_client/src/runtime/mod.rs +++ b/evm_rpc_client/src/runtime/mod.rs @@ -1,8 +1,9 @@ use async_trait::async_trait; -use candid::utils::ArgumentEncoder; -use candid::{CandidType, Principal}; -use ic_cdk::call::{Call, Error}; +use candid::{utils::ArgumentEncoder, CandidType, Principal}; +use ic_cdk::call::{Call, CallFailed}; +use ic_error_types::RejectCode; use serde::de::DeserializeOwned; +use thiserror::Error; /// Abstract the canister runtime so that the client code can be reused: /// * in production using `ic_cdk`, @@ -17,7 +18,7 @@ pub trait Runtime { method: &str, args: In, cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned; @@ -28,7 +29,7 @@ pub trait Runtime { id: Principal, method: &str, args: In, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned; @@ -46,7 +47,7 @@ impl Runtime for IcRuntime { method: &str, args: In, cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -55,11 +56,20 @@ impl Runtime for IcRuntime { .with_args(&args) .with_cycles(cycles) .await - .map_err(Error::from) - .and_then(|response| response.candid::().map_err(Error::from)) + .map_err(IcError::from) + .map(|response| { + response + .candid::() + .unwrap_or_else(|e| panic!("Failed to decode result: {e}")) + }) } - async fn query_call(&self, id: Principal, method: &str, args: In) -> Result + async fn query_call( + &self, + id: Principal, + method: &str, + args: In, + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -67,7 +77,69 @@ impl Runtime for IcRuntime { Call::unbounded_wait(id, method) .with_args(&args) .await - .map_err(Error::from) - .and_then(|response| response.candid::().map_err(Error::from)) + .map_err(IcError::from) + .map(|response| { + response + .candid::() + .unwrap_or_else(|e| panic!("Failed to decode result: {e}")) + }) + } +} + +/// Error returned by the Internet Computer when making an inter-canister call. +#[derive(Error, Clone, Debug, PartialEq, Eq)] +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 From for IcError { + fn from(err: CallFailed) -> Self { + match err { + CallFailed::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(), + } + } + CallFailed::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(), + } + } + CallFailed::InsufficientLiquidCycleBalance(e) => { + IcError::InsufficientLiquidCycleBalance { + available: e.available, + required: e.required, + } + } + } } } diff --git a/tests/mock_http_runtime/mod.rs b/tests/mock_http_runtime/mod.rs index 5e7eac00..9babd0e5 100644 --- a/tests/mock_http_runtime/mod.rs +++ b/tests/mock_http_runtime/mod.rs @@ -4,8 +4,8 @@ use crate::MAX_TICKS; use async_trait::async_trait; use candid::{decode_one, utils::ArgumentEncoder, CandidType, Principal}; use evm_rpc::constants::DEFAULT_MAX_RESPONSE_BYTES; -use evm_rpc_client::Runtime; -use ic_cdk::call::{CallFailed, CallRejected, Error}; +use evm_rpc_client::{IcError, Runtime}; +use ic_cdk::call::{CallFailed, CallRejected}; use ic_error_types::RejectCode; use mock::MockHttpOutcalls; use pocket_ic::{ @@ -35,7 +35,7 @@ impl Runtime for MockHttpRuntime { method: &str, args: In, _cycles: u128, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -59,7 +59,7 @@ impl Runtime for MockHttpRuntime { id: Principal, method: &str, args: In, - ) -> Result + ) -> Result where In: ArgumentEncoder + Send, Out: CandidType + DeserializeOwned, @@ -121,7 +121,7 @@ fn check_response_size( response } -fn parse_reject_response(response: RejectResponse) -> Error { +fn parse_reject_response(response: RejectResponse) -> IcError { CallFailed::CallRejected(CallRejected::with_rejection( response.reject_code as u32, response.reject_message, @@ -133,24 +133,17 @@ pub fn encode_args(args: In) -> Vec { candid::encode_args(args).expect("Failed to encode arguments.") } -pub fn decode_call_response(bytes: Vec) -> Result +pub fn decode_call_response(bytes: Vec) -> Result where Out: CandidType + DeserializeOwned, { - decode_one(&bytes) - .map_err(|e| { - // This would normally map to a `ic_cdk::call::CandidDecodeFailed`, but there is no - // way to instantiate this error from outside. - CallFailed::CallRejected(CallRejected::with_rejection( - RejectCode::CanisterError as u32, - format!( - "failed to decode canister response as {}: {}", - std::any::type_name::(), - e - ), - )) - }) - .map_err(Error::from) + decode_one(&bytes).map_err(|e| { + panic!( + "failed to decode canister response as {}: {}", + std::any::type_name::(), + e + ) + }) } async fn tick_until_http_requests(env: &PocketIc) -> Vec { From 81c05106c9f1f6a6781b34a1e423c30207489fa7 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 20 Oct 2025 09:29:54 +0200 Subject: [PATCH 5/6] XC-502: Update `canhttp` --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a86069be..609e638e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,8 +855,9 @@ dependencies = [ [[package]] name = "canhttp" -version = "0.2.1" -source = "git+https://github.com/dfinity/canhttp/?rev=724e87719d2f5d43dc463ed8d881c0fc0f3a4541#724e87719d2f5d43dc463ed8d881c0fc0f3a4541" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a904573d46c342d81fc9d07e9f5dad0af53ece2831cb9651bd5fe8dee673ea" dependencies = [ "assert_matches", "ciborium", diff --git a/Cargo.toml b/Cargo.toml index 9096effb..d091ae0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ alloy-rpc-types = "1.0.23" assert_matches = "1.5.0" async-trait = "0.1.88" candid = { version = "0.10.13" } -canhttp = { git = "https://github.com/dfinity/canhttp/", features = ["json", "multi"], rev = "724e87719d2f5d43dc463ed8d881c0fc0f3a4541" } +canhttp = { version = "0.3.0", features = ["json", "multi"] } canlog = { version = "0.2.0", features = ["derive"] } candid_parser = { version = "0.1.4" } derive_more = { version = "2.0.1", features = ["from", "into"] } From b6bfc2f8414c3f7844710fa0eacb35e5c067619f Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 20 Oct 2025 14:30:24 +0200 Subject: [PATCH 6/6] XC-502: Increase `IcError` granularity --- evm_rpc_client/src/runtime/mod.rs | 64 +++++++++++++++---------------- src/http.rs | 2 +- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/evm_rpc_client/src/runtime/mod.rs b/evm_rpc_client/src/runtime/mod.rs index f28e6ffa..7b83dfb3 100644 --- a/evm_rpc_client/src/runtime/mod.rs +++ b/evm_rpc_client/src/runtime/mod.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use candid::{utils::ArgumentEncoder, CandidType, Principal}; -use ic_cdk::call::{Call, CallFailed}; +use ic_cdk::call::{Call, CallFailed, CandidDecodeFailed}; use ic_error_types::RejectCode; use serde::de::DeserializeOwned; use thiserror::Error; @@ -57,11 +57,7 @@ impl Runtime for IcRuntime { .with_cycles(cycles) .await .map_err(IcError::from) - .map(|response| { - response - .candid::() - .unwrap_or_else(|e| panic!("Failed to decode result: {e}")) - }) + .and_then(|response| response.candid::().map_err(IcError::from)) } async fn query_call( @@ -78,28 +74,13 @@ impl Runtime for IcRuntime { .with_args(&args) .await .map_err(IcError::from) - .map(|response| { - response - .candid::() - .unwrap_or_else(|e| panic!("Failed to decode result: {e}")) - }) + .and_then(|response| response.candid::().map_err(IcError::from)) } } /// Error returned by the Internet Computer when making an inter-canister call. #[derive(Error, Clone, Debug, PartialEq, Eq)] 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 { @@ -108,11 +89,32 @@ pub enum IcError { /// The required cycles to perform the call. required: u128, }, + + /// The `ic0.call_perform` operation failed when performing the inter-canister call. + #[error("Inter-canister call perform failed")] + CallPerformFailed, + + /// The inter-canister call is rejected. + #[error("Inter-canister call rejected: {code:?} - {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 response from the inter-canister call could not be decoded as Candid. + #[error("The inter-canister call response could not be decoded: {message}")] + CandidDecodeFailed { + /// The specific Candid error that occurred. + message: String, + }, } impl From for IcError { fn from(err: CallFailed) -> Self { match err { + CallFailed::CallPerformFailed(_) => IcError::CallPerformFailed, CallFailed::CallRejected(e) => { IcError::CallRejected { // `CallRejected::reject_code()` can only return an error result if there is a @@ -124,16 +126,6 @@ impl From for IcError { message: e.reject_message().to_string(), } } - CallFailed::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(), - } - } CallFailed::InsufficientLiquidCycleBalance(e) => { IcError::InsufficientLiquidCycleBalance { available: e.available, @@ -143,3 +135,11 @@ impl From for IcError { } } } + +impl From for IcError { + fn from(err: CandidDecodeFailed) -> Self { + IcError::CandidDecodeFailed { + message: err.to_string(), + } + } +} diff --git a/src/http.rs b/src/http.rs index f72b60a5..1449bee8 100644 --- a/src/http.rs +++ b/src/http.rs @@ -332,7 +332,7 @@ impl From for RpcError { message, }) } - e @ HttpClientError::IcError { .. } => { + e @ HttpClientError::IcError(IcError::InsufficientLiquidCycleBalance { .. }) => { panic!("{}", e.to_string()) } HttpClientError::NotHandledError(e) => {