diff --git a/Cargo.lock b/Cargo.lock index 445f0720c30..58b0a8599ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3797,6 +3797,7 @@ dependencies = [ "ic-cdk 0.13.2", "mockall", "serde", + "thiserror", "tokio", ] @@ -4942,9 +4943,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "908bb38696d7a037a01ebcc68a00634112ac2bbf8ca74e30a2c3d2f4f021302b" dependencies = [ "futures-util", "http 1.1.0", @@ -6149,6 +6150,7 @@ dependencies = [ "candid_parser", "ethers-core", "ethnum", + "evm-rpc-client", "flate2", "futures", "hex", @@ -12673,7 +12675,7 @@ dependencies = [ "http-body-util", "httptest", "hyper 1.3.1", - "hyper-rustls 0.27.2", + "hyper-rustls 0.27.1", "hyper-util", "hyperlocal-next", "ic-agent", diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 7919970d759..442df0dee93 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -653,6 +653,16 @@ http_file( url = "https://download.dfinity.systems/ic/300dc603a92b5f70dae79229793c902f346af3cc/canisters/ic-icrc1-ledger-u256.wasm.gz", ) +# XC artifacts for testing + +# EVM RPC canister + +http_file( + name = "evm_rpc.wasm.gz", + sha256 = "ccce0d8e3210db42ff12b03360c20246855ad8529da0f844faa343bf8b393529", + url = "https://github.com/internet-computer-protocol/evm-rpc-canister/releases/download/release-2024-05-23/evm_rpc.wasm.gz", +) + # Haskell toolchain for spec_compliance tests http_archive( diff --git a/rs/ethereum/cketh/minter/BUILD.bazel b/rs/ethereum/cketh/minter/BUILD.bazel index df3d10366ac..af951b00ae5 100644 --- a/rs/ethereum/cketh/minter/BUILD.bazel +++ b/rs/ethereum/cketh/minter/BUILD.bazel @@ -43,6 +43,7 @@ rust_library( "//packages/icrc-ledger-types:icrc_ledger_types", "//rs/crypto/ecdsa_secp256k1", "//rs/crypto/sha3", + "//rs/ethereum/evm-rpc-client", "//rs/ethereum/types", "//rs/phantom_newtype", "//rs/types/management_canister_types", @@ -173,44 +174,54 @@ rust_binary( ], ) -rust_ic_test_suite( - name = "integration_tests", - srcs = glob(["tests/**/*.rs"]), - data = [ - ":cketh_minter_debug.wasm", - "//rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator_canister.wasm", - "//rs/rosetta-api/icrc1/archive:archive_canister_u256.wasm.gz", - "//rs/rosetta-api/icrc1/index-ng:index_ng_canister_u256.wasm.gz", - "//rs/rosetta-api/icrc1/ledger:ledger_canister_u256.wasm.gz", - ], - env = { - "CARGO_MANIFEST_DIR": "rs/ethereum/cketh/minter", - "CKETH_MINTER_WASM_PATH": "$(rootpath :cketh_minter_debug.wasm)", - "LEDGER_SUITE_ORCHESTRATOR_WASM_PATH": "$(rootpath //rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator_canister.wasm)", - "LEDGER_CANISTER_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/ledger:ledger_canister_u256.wasm.gz)", - "INDEX_CANISTER_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/index-ng:index_ng_canister_u256.wasm.gz)", - "LEDGER_ARCHIVE_NODE_CANISTER_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/archive:archive_canister_u256.wasm.gz)", - }, - proc_macro_deps = [], - deps = [ - ":minter", - "//packages/icrc-ledger-types:icrc_ledger_types", - "//rs/ethereum/cketh/test_utils", - "//rs/ethereum/ledger-suite-orchestrator/test_utils", - "//rs/ethereum/types", - "//rs/state_machine_tests", - "//rs/types/base_types", - "@crate_index//:assert_matches", - "@crate_index//:candid", - "@crate_index//:ethers-core", - "@crate_index//:hex", - "@crate_index//:num-bigint", - "@crate_index//:num-traits", - "@crate_index//:serde", - "@crate_index//:serde_bytes", - "@crate_index//:serde_json", - ], -) +[ + rust_ic_test_suite( + name = "integration_tests" + name_suffix, + srcs = glob(["tests/**/*.rs"]), + data = [ + ":cketh_minter_debug.wasm", + "//rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator_canister.wasm", + "//rs/rosetta-api/icrc1/archive:archive_canister_u256.wasm.gz", + "//rs/rosetta-api/icrc1/index-ng:index_ng_canister_u256.wasm.gz", + "//rs/rosetta-api/icrc1/ledger:ledger_canister_u256.wasm.gz", + ] + extra_data, + env = { + "CARGO_MANIFEST_DIR": "rs/ethereum/cketh/minter", + "CKETH_MINTER_WASM_PATH": "$(rootpath :cketh_minter_debug.wasm)", + "LEDGER_SUITE_ORCHESTRATOR_WASM_PATH": "$(rootpath //rs/ethereum/ledger-suite-orchestrator:ledger_suite_orchestrator_canister.wasm)", + "LEDGER_CANISTER_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/ledger:ledger_canister_u256.wasm.gz)", + "INDEX_CANISTER_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/index-ng:index_ng_canister_u256.wasm.gz)", + "LEDGER_ARCHIVE_NODE_CANISTER_WASM_PATH": "$(rootpath //rs/rosetta-api/icrc1/archive:archive_canister_u256.wasm.gz)", + } | extra_env, + proc_macro_deps = [], + deps = [ + ":minter", + "//packages/icrc-ledger-types:icrc_ledger_types", + "//rs/ethereum/cketh/test_utils", + "//rs/ethereum/ledger-suite-orchestrator/test_utils", + "//rs/ethereum/types", + "//rs/state_machine_tests", + "//rs/types/base_types", + "@crate_index//:assert_matches", + "@crate_index//:candid", + "@crate_index//:ethers-core", + "@crate_index//:hex", + "@crate_index//:num-bigint", + "@crate_index//:num-traits", + "@crate_index//:serde", + "@crate_index//:serde_bytes", + "@crate_index//:serde_json", + ], + ) + for (name_suffix, extra_data, extra_env) in [ + ("", [], {}), + ( + "_evm_rpc", + ["@evm_rpc.wasm.gz//file"], + {"EVM_RPC_CANISTER_WASM_PATH": "$(rootpath @evm_rpc.wasm.gz//file)"}, + ), + ] +] closure_js_library( name = "principal_to_bytes", diff --git a/rs/ethereum/cketh/minter/Cargo.toml b/rs/ethereum/cketh/minter/Cargo.toml index ec56f4aa9ea..c8504bad4f3 100644 --- a/rs/ethereum/cketh/minter/Cargo.toml +++ b/rs/ethereum/cketh/minter/Cargo.toml @@ -15,6 +15,7 @@ path = "bin/principal_to_hex.rs" askama = { workspace = true } candid = { workspace = true } ethnum = { workspace = true } +evm-rpc-client = { path = "../../evm-rpc-client" } futures = { workspace = true } hex = { workspace = true } hex-literal = "0.4.1" diff --git a/rs/ethereum/cketh/minter/src/eth_rpc.rs b/rs/ethereum/cketh/minter/src/eth_rpc.rs index c2d0cc54b2e..3697d664e4e 100644 --- a/rs/ethereum/cketh/minter/src/eth_rpc.rs +++ b/rs/ethereum/cketh/minter/src/eth_rpc.rs @@ -9,6 +9,7 @@ use crate::numeric::{BlockNumber, LogIndex, TransactionCount, Wei, WeiPerGas}; use crate::state::{mutate_state, State}; use candid::{candid_method, CandidType, Principal}; use ethnum; +use evm_rpc_client::types::candid::HttpOutcallError as EvmHttpOutcallError; use ic_canister_log::log; use ic_cdk::api::call::{call_with_payment128, RejectionCode}; use ic_cdk::api::management_canister::http_request::{ @@ -558,6 +559,23 @@ pub enum HttpOutcallError { }, } +impl From for HttpOutcallError { + fn from(value: EvmHttpOutcallError) -> Self { + match value { + EvmHttpOutcallError::IcError { code, message } => Self::IcError { code, message }, + EvmHttpOutcallError::InvalidHttpJsonRpcResponse { + status, + body, + parsing_error, + } => Self::InvalidHttpJsonRpcResponse { + status, + body, + parsing_error, + }, + } + } +} + impl HttpOutcallError { pub fn is_response_too_large(&self) -> bool { match self { diff --git a/rs/ethereum/cketh/minter/src/eth_rpc_client/mod.rs b/rs/ethereum/cketh/minter/src/eth_rpc_client/mod.rs index d7821a27514..181713e11f6 100644 --- a/rs/ethereum/cketh/minter/src/eth_rpc_client/mod.rs +++ b/rs/ethereum/cketh/minter/src/eth_rpc_client/mod.rs @@ -1,19 +1,28 @@ use crate::eth_rpc::{ - self, Block, BlockSpec, FeeHistory, FeeHistoryParams, GetLogsParam, Hash, HttpOutcallError, - HttpOutcallResult, HttpResponsePayload, JsonRpcResult, LogEntry, ResponseSizeEstimate, - SendRawTransactionResult, + self, Block, BlockSpec, BlockTag, FeeHistory, FeeHistoryParams, GetLogsParam, Hash, + HttpOutcallError, HttpOutcallResult, HttpResponsePayload, JsonRpcResult, LogEntry, + ResponseSizeEstimate, SendRawTransactionResult, +}; +use crate::eth_rpc_client::providers::{ + EthereumProvider, RpcNodeProvider, SepoliaProvider, MAINNET_PROVIDERS, SEPOLIA_PROVIDERS, }; -use crate::eth_rpc_client::providers::{RpcNodeProvider, MAINNET_PROVIDERS, SEPOLIA_PROVIDERS}; use crate::eth_rpc_client::requests::GetTransactionCountParams; use crate::eth_rpc_client::responses::TransactionReceipt; use crate::lifecycle::EthereumNetwork; -use crate::logs::{DEBUG, INFO}; -use crate::numeric::TransactionCount; +use crate::logs::{PrintProxySink, DEBUG, INFO, TRACE_HTTP}; +use crate::numeric::{BlockNumber, TransactionCount, Wei}; use crate::state::State; +use evm_rpc_client::{ + types::candid::{ + Block as EvmBlock, BlockTag as EvmBlockTag, MultiRpcResult as EvmMultiRpcResult, + RpcError as EvmRpcError, RpcResult as EvmRpcResult, + }, + EvmRpcClient, IcRuntime, +}; use ic_canister_log::log; use serde::{de::DeserializeOwned, Serialize}; use std::collections::BTreeMap; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; mod providers; pub mod requests; @@ -22,22 +31,35 @@ pub mod responses; #[cfg(test)] mod tests; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug)] pub struct EthRpcClient { + evm_rpc_client: Option>, chain: EthereumNetwork, } impl EthRpcClient { const fn new(chain: EthereumNetwork) -> Self { - Self { chain } + Self { + evm_rpc_client: None, + chain, + } } - pub const fn from_state(state: &State) -> Self { - if state.evm_rpc_id.is_some() { - //TODO XC-131: use EVM-RPC canister to retrieve last finalized block - unimplemented!(); + pub fn from_state(state: &State) -> Self { + let mut client = Self::new(state.ethereum_network()); + if let Some(evm_rpc_id) = state.evm_rpc_id { + let providers = match client.chain { + EthereumNetwork::Mainnet => EthereumProvider::evm_rpc_node_providers(), + EthereumNetwork::Sepolia => SepoliaProvider::evm_rpc_node_providers(), + }; + client.evm_rpc_client = Some( + EvmRpcClient::builder_for_ic(TRACE_HTTP) + .with_providers(providers) + .with_evm_canister_id(evm_rpc_id) + .build(), + ); } - Self::new(state.ethereum_network()) + client } fn providers(&self) -> &[RpcNodeProvider] { @@ -143,6 +165,18 @@ impl EthRpcClient { ) -> Result> { use crate::eth_rpc::GetBlockByNumberParams; + if let Some(evm_rpc_client) = &self.evm_rpc_client { + let result = evm_rpc_client + .eth_get_block_by_number(match block { + BlockSpec::Number(n) => EvmBlockTag::Number(n.into()), + BlockSpec::Tag(BlockTag::Latest) => EvmBlockTag::Latest, + BlockSpec::Tag(BlockTag::Safe) => EvmBlockTag::Safe, + BlockSpec::Tag(BlockTag::Finalized) => EvmBlockTag::Finalized, + }) + .await; + return ReducedResult::from(result).into(); + } + let expected_block_size = match self.chain { EthereumNetwork::Sepolia => 12 * 1024, EthereumNetwork::Mainnet => 24 * 1024, @@ -221,36 +255,90 @@ pub struct MultiCallResults { errors: BTreeMap, } +impl Default for MultiCallResults { + fn default() -> Self { + Self::new() + } +} + impl MultiCallResults { + pub fn new() -> Self { + Self { + ok_results: BTreeMap::new(), + errors: BTreeMap::new(), + } + } + + fn map Result, O: Fn(E) -> SingleCallError>( + self, + f: &F, + map_err: &O, + ) -> MultiCallResults { + let mut errors = self.errors; + let ok_results = self + .ok_results + .into_iter() + .filter_map(|(provider, v)| match f(v) { + Ok(value) => Some((provider, value)), + Err(e) => { + errors.insert(provider, map_err(e)); + None + } + }) + .collect(); + MultiCallResults { ok_results, errors } + } + + fn insert_once(&mut self, provider: RpcNodeProvider, result: Result) { + match result { + Ok(value) => { + assert!(!self.errors.contains_key(&provider)); + assert!(self.ok_results.insert(provider, value).is_none()); + } + Err(error) => { + assert!(!self.ok_results.contains_key(&provider)); + assert!(self.errors.insert(provider, error).is_none()); + } + } + } + fn from_non_empty_iter< I: IntoIterator>)>, >( iter: I, ) -> Self { - let results = BTreeMap::from_iter(iter); + let mut results = MultiCallResults::new(); + for (provider, result) in iter { + let result: Result = match result { + Ok(JsonRpcResult::Result(value)) => Ok(value), + Ok(JsonRpcResult::Error { code, message }) => { + Err(SingleCallError::JsonRpcError { code, message }) + } + Err(error) => Err(SingleCallError::HttpOutcallError(error)), + }; + results.insert_once(provider, result); + } if results.is_empty() { panic!("BUG: MultiCallResults cannot be empty!") } - let mut ok_results = BTreeMap::new(); - let mut errors = BTreeMap::new(); - for (provider, result) in results.into_iter() { - match result { - Ok(JsonRpcResult::Result(value)) => { - assert!(ok_results.insert(provider, value).is_none()); - } - Ok(JsonRpcResult::Error { code, message }) => { - assert!(errors - .insert(provider, SingleCallError::JsonRpcError { code, message }) - .is_none()); - } - Err(error) => { - assert!(errors - .insert(provider, SingleCallError::HttpOutcallError(error)) - .is_none()); - } - } + results + } + + fn from_iter)>>( + iter: I, + ) -> Self { + let mut results = MultiCallResults::new(); + for (provider, result) in iter { + results.insert_once(provider, result); + } + if results.is_empty() { + panic!("BUG: MultiCallResults cannot be empty!") } - Self { ok_results, errors } + results + } + + pub fn is_empty(&self) -> bool { + self.ok_results.is_empty() && self.errors.is_empty() } } @@ -285,12 +373,10 @@ impl MultiCallResults { .expect("BUG: expect errors should be non-empty"); for (provider, error) in errors_iter { if first_error != error { - return MultiCallError::InconsistentResults(MultiCallResults::from_non_empty_iter( - vec![ - (first_provider, first_error.into()), - (provider, error.into()), - ], - )); + return MultiCallError::InconsistentResults(MultiCallResults::from_iter(vec![ + (first_provider, Err(first_error)), + (provider, Err(error)), + ])); } } match first_error { @@ -300,6 +386,9 @@ impl MultiCallResults { SingleCallError::JsonRpcError { code, message } => { MultiCallError::ConsistentJsonRpcError { code, message } } + SingleCallError::EvmRpcError(error) => { + MultiCallError::ConsistentEvmRpcCanisterError(error) + } } } } @@ -308,26 +397,143 @@ impl MultiCallResults { pub enum SingleCallError { HttpOutcallError(HttpOutcallError), JsonRpcError { code: i64, message: String }, -} - -impl From for HttpOutcallResult> { - fn from(value: SingleCallError) -> Self { - match value { - SingleCallError::HttpOutcallError(error) => Err(error), - SingleCallError::JsonRpcError { code, message } => { - Ok(JsonRpcResult::Error { code, message }) - } - } - } + EvmRpcError(String), } #[derive(Debug, PartialEq, Eq)] pub enum MultiCallError { ConsistentHttpOutcallError(HttpOutcallError), ConsistentJsonRpcError { code: i64, message: String }, + ConsistentEvmRpcCanisterError(String), InconsistentResults(MultiCallResults), } +#[derive(Debug, PartialEq, Eq)] +pub struct ReducedResult { + result: Result>, +} + +impl ReducedResult { + /// Transform a `ReducedResult` into a `ReducedResult` by applying a mapping function `F`. + /// The mapping function is also applied to the elements contained in the error `MultiCallError::InconsistentResults`, + /// which depending on the mapping function could lead to the mapped results no longer being inconsistent. + /// The final result in that case is given by applying the reduction function `R` to the mapped results. + pub fn map_reduce< + U, + E: Display, + F: Fn(T) -> Result, + R: FnOnce(MultiCallResults) -> Result>, + >( + self, + faillible_op: &F, + reduction: R, + ) -> ReducedResult { + let result = match self.result { + Ok(t) => faillible_op(t) + .map_err(|e| MultiCallError::::ConsistentEvmRpcCanisterError(e.to_string())), + Err(MultiCallError::ConsistentHttpOutcallError(e)) => { + Err(MultiCallError::::ConsistentHttpOutcallError(e)) + } + Err(MultiCallError::ConsistentJsonRpcError { code, message }) => { + Err(MultiCallError::::ConsistentJsonRpcError { code, message }) + } + Err(MultiCallError::ConsistentEvmRpcCanisterError(e)) => { + Err(MultiCallError::::ConsistentEvmRpcCanisterError(e)) + } + Err(MultiCallError::InconsistentResults(results)) => { + reduction(results.map(faillible_op, &|e| { + SingleCallError::EvmRpcError(e.to_string()) + })) + } + }; + ReducedResult { result } + } + + fn from_internal(value: EvmMultiRpcResult) -> Self { + fn into_single_call_result(result: EvmRpcResult) -> Result { + match result { + Ok(t) => Ok(t), + Err(e) => match e { + EvmRpcError::ProviderError(e) => { + Err(SingleCallError::EvmRpcError(e.to_string())) + } + EvmRpcError::HttpOutcallError(e) => { + Err(SingleCallError::HttpOutcallError(e.into())) + } + EvmRpcError::JsonRpcError(e) => Err(SingleCallError::JsonRpcError { + code: e.code, + message: e.message, + }), + EvmRpcError::ValidationError(e) => { + Err(SingleCallError::EvmRpcError(e.to_string())) + } + }, + } + } + + let result = match value { + EvmMultiRpcResult::Consistent(result) => match result { + Ok(t) => Ok(t), + Err(e) => match e { + EvmRpcError::ProviderError(e) => { + Err(MultiCallError::ConsistentEvmRpcCanisterError(e.to_string())) + } + EvmRpcError::HttpOutcallError(e) => { + Err(MultiCallError::ConsistentHttpOutcallError(e.into())) + } + EvmRpcError::JsonRpcError(e) => Err(MultiCallError::ConsistentJsonRpcError { + code: e.code, + message: e.message, + }), + EvmRpcError::ValidationError(e) => { + Err(MultiCallError::ConsistentEvmRpcCanisterError(e.to_string())) + } + }, + }, + EvmMultiRpcResult::Inconsistent(results) => { + let mut multi_results = MultiCallResults::new(); + results.into_iter().for_each(|(provider, result)| { + multi_results.insert_once( + RpcNodeProvider::EvmRpc(provider), + into_single_call_result(result), + ); + }); + Err(MultiCallError::InconsistentResults(multi_results)) + } + }; + Self { result } + } +} + +impl From>> for ReducedResult { + fn from(result: Result>) -> Self { + Self { result } + } +} + +impl From> for Result> { + fn from(value: ReducedResult) -> Self { + value.result + } +} + +impl From> for ReducedResult { + fn from(value: EvmMultiRpcResult) -> Self { + ReducedResult::from_internal(value).map_reduce( + &|block: EvmBlock| { + Ok::(Block { + number: BlockNumber::try_from(block.number)?, + base_fee_per_gas: Wei::try_from(block.base_fee_per_gas)?, + }) + }, + MultiCallResults::reduce_with_equality, + ) + } +} + +// TODO XC-131: add proptest to ensure HttpOutcallError are kept, so that the halving +// of the log scrapping happens correctly + impl MultiCallError { pub fn has_http_outcall_error_matching bool>( &self, @@ -342,9 +548,12 @@ impl MultiCallError { .values() .any(|single_call_error| match single_call_error { SingleCallError::HttpOutcallError(error) => predicate(error), - SingleCallError::JsonRpcError { .. } => false, + SingleCallError::JsonRpcError { .. } | SingleCallError::EvmRpcError(_) => { + false + } }) } + MultiCallError::ConsistentEvmRpcCanisterError(_) => false, } } } @@ -360,10 +569,10 @@ impl MultiCallResults { .collect(); if !inconsistent_results.is_empty() { inconsistent_results.push((base_node_provider, base_result)); - let error = MultiCallError::InconsistentResults(MultiCallResults::from_non_empty_iter( + let error = MultiCallError::InconsistentResults(MultiCallResults::from_iter( inconsistent_results .into_iter() - .map(|(provider, result)| (provider, Ok(JsonRpcResult::Result(result)))), + .map(|(provider, result)| (provider, Ok(result))), )); log!( INFO, @@ -399,16 +608,13 @@ impl MultiCallResults { .last_key_value() .expect("BUG: results_with_same_key is non-empty"); if &result != other_result { - let error = MultiCallError::InconsistentResults( - MultiCallResults::from_non_empty_iter( + let error = + MultiCallError::InconsistentResults(MultiCallResults::from_iter( votes_for_same_key .into_iter() .chain(std::iter::once((provider, result))) - .map(|(provider, result)| { - (provider, Ok(JsonRpcResult::Result(result))) - }), - ), - ); + .map(|(provider, result)| (provider, Ok(result))), + )); log!( INFO, "[reduce_with_strict_majority_by_key]: inconsistent results {error:?}" @@ -445,16 +651,13 @@ impl MultiCallResults { .expect("BUG: tally should be non-empty") .1) } else { - let error = - MultiCallError::InconsistentResults(MultiCallResults::from_non_empty_iter( - first - .1 - .into_iter() - .chain(second.1) - .map(|(provider, result)| { - (provider, Ok(JsonRpcResult::Result(result))) - }), - )); + let error = MultiCallError::InconsistentResults(MultiCallResults::from_iter( + first + .1 + .into_iter() + .chain(second.1) + .map(|(provider, result)| (provider, Ok(result))), + )); log!( INFO, "[reduce_with_strict_majority_by_key]: no strict majority {error:?}" diff --git a/rs/ethereum/cketh/minter/src/eth_rpc_client/providers.rs b/rs/ethereum/cketh/minter/src/eth_rpc_client/providers.rs index c999ff70f05..3765036d3a9 100644 --- a/rs/ethereum/cketh/minter/src/eth_rpc_client/providers.rs +++ b/rs/ethereum/cketh/minter/src/eth_rpc_client/providers.rs @@ -1,3 +1,8 @@ +use evm_rpc_client::types::candid::{ + EthSepoliaService as EvmEthSepoliaService, RpcService as EvmRpcService, + RpcServices as EvmRpcServices, +}; + pub(crate) const MAINNET_PROVIDERS: [RpcNodeProvider; 3] = [ RpcNodeProvider::Ethereum(EthereumProvider::Ankr), RpcNodeProvider::Ethereum(EthereumProvider::PublicNode), @@ -9,17 +14,25 @@ pub(crate) const SEPOLIA_PROVIDERS: [RpcNodeProvider; 2] = [ RpcNodeProvider::Sepolia(SepoliaProvider::PublicNode), ]; +const EVM_RPC_SEPOLIA_PROVIDERS: [EvmEthSepoliaService; 2] = + [EvmEthSepoliaService::Ankr, EvmEthSepoliaService::PublicNode]; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] pub(crate) enum RpcNodeProvider { Ethereum(EthereumProvider), Sepolia(SepoliaProvider), + EvmRpc(EvmRpcService), } impl RpcNodeProvider { + //TODO XC-27: remove this method pub(crate) fn url(&self) -> &str { match self { Self::Ethereum(provider) => provider.ethereum_mainnet_endpoint_url(), Self::Sepolia(provider) => provider.ethereum_sepolia_endpoint_url(), + RpcNodeProvider::EvmRpc(_) => { + panic!("BUG: should not need URL of provider from EVM RPC canister") + } } } } @@ -42,6 +55,24 @@ impl EthereumProvider { EthereumProvider::LlamaNodes => "https://eth.llamarpc.com", } } + + // TODO XC-131: Replace using Custom providers with EthMainnetService, + // when LlamaNodes is supported as a provider. + pub(crate) fn evm_rpc_node_providers() -> EvmRpcServices { + use evm_rpc_client::types::candid::RpcApi as EvmRpcApi; + + let services = MAINNET_PROVIDERS + .iter() + .map(|provider| EvmRpcApi { + url: provider.url().to_string(), + headers: None, + }) + .collect(); + EvmRpcServices::Custom { + chain_id: 1, + services, + } + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] @@ -59,4 +90,8 @@ impl SepoliaProvider { SepoliaProvider::PublicNode => "https://ethereum-sepolia-rpc.publicnode.com", } } + + pub(crate) fn evm_rpc_node_providers() -> EvmRpcServices { + EvmRpcServices::EthSepolia(Some(EVM_RPC_SEPOLIA_PROVIDERS.to_vec())) + } } diff --git a/rs/ethereum/cketh/minter/src/eth_rpc_client/tests.rs b/rs/ethereum/cketh/minter/src/eth_rpc_client/tests.rs index 235d11d5f24..8874a07c9bb 100644 --- a/rs/ethereum/cketh/minter/src/eth_rpc_client/tests.rs +++ b/rs/ethereum/cketh/minter/src/eth_rpc_client/tests.rs @@ -659,3 +659,234 @@ mod eth_get_transaction_count { assert_eq!(count, TransactionCount::from(0x3d8_u32)); } } + +mod evm_rpc_conversion { + use crate::eth_rpc_client::providers::RpcNodeProvider; + use crate::eth_rpc_client::{Block, MultiCallError}; + use crate::eth_rpc_client::{MultiCallResults, ReducedResult}; + use crate::numeric::{BlockNumber, Wei}; + use assert_matches::assert_matches; + use candid::Nat; + use evm_rpc_client::types::candid::{ + Block as EvmBlock, EthMainnetService as EvmEthMainnetService, + MultiRpcResult as EvmMultiRpcResult, RpcService as EvmRpcService, + }; + use num_bigint::BigUint; + + #[test] + fn should_map_consistent_result() { + let block = evm_rpc_block(); + let evm_result = EvmMultiRpcResult::Consistent(Ok(block.clone())); + + let reduced_block: Result<_, _> = ReducedResult::from(evm_result).into(); + + assert_eq!( + reduced_block, + Ok(Block { + number: BlockNumber::try_from(block.number).unwrap(), + base_fee_per_gas: Wei::try_from(block.base_fee_per_gas).unwrap(), + }) + ); + } + + #[test] + fn should_map_inconsistent_results() { + let block = evm_rpc_block(); + let next_block = EvmBlock { + number: block.number.clone() + 1_u8, + ..evm_rpc_block() + }; + + let evm_result = EvmMultiRpcResult::Inconsistent(vec![ + ( + EvmRpcService::EthMainnet(EvmEthMainnetService::Alchemy), + Ok(block.clone()), + ), + ( + EvmRpcService::EthMainnet(EvmEthMainnetService::Ankr), + Ok(next_block.clone()), + ), + ]); + + let reduced_block: Result<_, _> = ReducedResult::from(evm_result).into(); + + assert_eq!( + reduced_block, + Err(MultiCallError::InconsistentResults( + MultiCallResults::from_iter(vec![ + ( + RpcNodeProvider::EvmRpc(EvmRpcService::EthMainnet( + EvmEthMainnetService::Alchemy + )), + Ok(Block { + number: BlockNumber::try_from(block.number).unwrap(), + base_fee_per_gas: Wei::try_from(block.base_fee_per_gas).unwrap(), + }), + ), + ( + RpcNodeProvider::EvmRpc(EvmRpcService::EthMainnet( + EvmEthMainnetService::Ankr + )), + Ok(Block { + number: BlockNumber::try_from(next_block.number).unwrap(), + base_fee_per_gas: Wei::try_from(next_block.base_fee_per_gas).unwrap(), + }), + ), + ]) + )) + ); + } + + #[test] + fn should_be_consistent_when_evm_block_is_not() { + let block = evm_rpc_block(); + let inconsistent_block = EvmBlock { + miner: "other".to_string(), + ..evm_rpc_block() + }; + assert_ne!(block, inconsistent_block); + let evm_result = EvmMultiRpcResult::Inconsistent(vec![ + ( + EvmRpcService::EthMainnet(EvmEthMainnetService::Alchemy), + Ok(block.clone()), + ), + ( + EvmRpcService::EthMainnet(EvmEthMainnetService::Ankr), + Ok(inconsistent_block), + ), + ]); + + let reduced_block: Result<_, _> = ReducedResult::from(evm_result).into(); + + assert_eq!( + reduced_block, + Ok(Block { + number: BlockNumber::try_from(block.number).unwrap(), + base_fee_per_gas: Wei::try_from(block.base_fee_per_gas).unwrap(), + }) + ); + } + + #[test] + fn should_fail_on_invalid_u256_nat() { + const U256_MAX: &[u8; 64] = + b"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"; + let u256_max_plus_one: Nat = + Nat(BigUint::parse_bytes(U256_MAX, 16).expect("Failed to parse u256 max")) + + Nat::from(1_u8); + + for invalid_block in vec![ + EvmBlock { + number: u256_max_plus_one.clone(), + ..evm_rpc_block() + }, + EvmBlock { + base_fee_per_gas: u256_max_plus_one.clone(), + ..evm_rpc_block() + }, + ] { + let evm_result = EvmMultiRpcResult::Consistent(Ok(invalid_block)); + let reduced_block: Result<_, _> = ReducedResult::from(evm_result).into(); + + assert_matches!( + reduced_block, + Err(MultiCallError::ConsistentEvmRpcCanisterError(s)) if s.contains("Nat does not fit in a U256") + ); + } + } + + fn evm_rpc_block() -> EvmBlock { + EvmBlock { + base_fee_per_gas: 8_876_901_983_u64.into(), + number: 20_061_336_u32.into(), + difficulty: 0_u8.into(), + extra_data: "0xd883010d0e846765746888676f312e32312e36856c696e7578".to_string(), + gas_limit: 30_000_000_u32.into(), + gas_used: 2_858_256_u32.into(), + hash: "0x3a68e81a96d436f421b7cae6a66f78f6aef075340edaec5c7c1db0919c0f909b".to_string(), + logs_bloom: "0x006000060010410010180000940006000000200040006108008801008022000900a005820000001100000300000d058962202900084080a0000031080022800000480c08100000006800000a20002028841080209044003041000940802448100002002a820085000000008400200d40204c10110810040403000210020004000a20208028104110a48429100033080e000040050501004800850042405230204230800000a0202282019080040040090a858000014014800440000208000008081804124002800030002040080610c000050002502000100005000a08002000001020500100804612440042300c0080040812000a1208420108200000000045".to_string(), + miner: "0xd2732e3e4c264ab330af53f661f6da91cbbb594a".to_string(), + mix_hash: "0x472d18a0b90d7007028dded03d7ef9923c2a7fc60f7e276bc6928fa9aeb6cbe8".to_string(), + nonce: 0_u8.into(), + parent_hash: "0xc0debe594704702ec9c2e5a56595ccbc285305108286a6a19aa33f8b3755da65".to_string(), + receipts_root: "0x54179d043f2fe97f122a01366cd6ad18868501253282575fb00cada3fecf8fe1".to_string(), + sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347".to_string(), + size: 17_484_u32.into(), + state_root: "0x1e25cbd8eb25aadda3da160fd9b3fd46dfae61d7df1097d7990ca420e5c7c608".to_string(), + timestamp: 1_718_021_363_u32.into(), + total_difficulty: 58_750_003_716_598_352_816_469_u128.into(), + transactions: vec![ + "0x5f17526ee5ab415ed44aa3788f0e8154230faa50f8b6d547a95858a8a90f259e", + "0x1d0d559a2e113a4a4b738c97536c48e4a047a491614ddefe77c6e0f25b9e3a42", + "0xd8c4f005fd4c7832205f6eda9bfde5bc5f9e0a1002b325d02348889f39e21850", + "0xee14faac7f1d05a71ce69b11116a2ed8bf7a020a7b81a6a7a82096fdea7823a5", + "0x63725de23700e115a48cb969a9e26eea56a65a971d63a21cc9cc660aa0cf4204", + "0x77cbe1a9c3aef1ee9f345de7c189e9631e5458b194ba91ab2d9dc6d625e7eb68", + "0x0e3403dcc6dea9dec03203ed9b1b89c66fd606abe3ac8bb33ed440283e5444cb", + "0x91935e9885348f1ec4d673532c4f6709a28298f804b8054dea406407b00566af", + "0x728b9eab683e4a59e75ebe03f1f9cdf081c04bc57f505cd8fc157a282e299c08", + "0xb00dfcae52ef97f4f80965603f7c3a4c7f8c58e3e12caf6b636a522d0fbfef86", + "0x604737ccc8f69cd4c1cd4c1e8f62655272d0a6db98923e907a5b0404d1822df4", + "0x079ffeb1040d2490e248eb85047422bf3519c5fb5e3632ec3f245217c540a4b1", + "0xd0c5a03b82d2b7cb62be59cb691cf5f6b0940b433360545e23e55044741f51dd", + "0xe5707c1a13739613acec865053b88a03d7417004dec6086b544d92f4d9235880", + "0x8f8541fa86b636d26b620c15741095e2920c27545b4b42efde1e15a702f99a00", + "0x763b7f0bde974974d96e2ae1c9bee1bea8841acebf7c188477b198c93022f797", + "0x9e518c8ced080b6d25836b506a5424ff98ca1933637e7586dd9464c48930880a", + "0x08467c33ab74e9a379d63cbb1a03097c7cde7f85a166e60804c855cfd8bdcb96", + "0x38928c665e5c62509066deaffcc94d497928b26bfef33d570a92b09af3a6cbbd", + "0x2c616b1f2aa52a5f481d8aa5ebe0991f1f03d5c676f1b92cd03496ce753a5ae2", + "0x3a4cf1999fe714e2be26f12a05270d85bb2033ee787727b94e5a7a3494e45f59", + "0x8b3fc42aa0de7d0a181829229bc6ec8a5dd6c5d096945c0a2d149dd48a38e94a", + "0xf1a3521cb1c73ae3bf5af18e25fdff023adabeea83503f73ca8721ce6ea27bfa", + "0xff3265ddf367f97b50f95e4295bd101914fced55677315dee6c7618e31c721b6", + "0xe6cc4470987f866cbddfe8e47a069a803fbda1b71055c49e96e78bdbe0cf1462", + "0xccb8d52db4861b571240d71a58ba6cf8ea8e06567b82d68d517d383753cd8c65", + "0x7c620a3c26299632c513f3940aae5dc971d1bedc93f669482e760cf4a86e25ee", + "0xc2b265b37be476a291c87f10913960fe7ac790796248fb07e39fa40502e9fc03", + "0x78083d9907ab4136e7df0cc266e4a6cddc4cf9e62948d5ab6bf81821ed26f45e", + "0xf3776413512018e401b49b4661ecfd3f6daabe4aa52b3ae158ef8f10be424ca1", + "0x53bc3267ef9f8f5a2d7be33f111391cbee7f13390de9bd531f5f216eef13582d", + "0x6fc125dda0b34acd12f72fc5980fa5250ed1cfa985e60f5535123e7bfe82baca", + "0xf9ace1b33ed117617cdae76a79a8fa758a8f3817c3aaf245a94953f565001d8a", + "0xb186f79d1d6218ce61715f579ae2bde0582dede16d0ef9cf1cd85735f05445ea", + "0x75e69b143d0fb26e4113c2dd0c2f702b2e917b5c23d81aaf587243525ef56e5a", + "0xe6595bcb2ae614d890d38d153057826c3ad08063332276fa1b16e5f13b06e7a2", + "0xd473fc760fb6cd8c81b7fe26e1bb6114d95957be22a713e1aac2cc63c2a3f0a3", + "0x132d23074d8442c60337018bba749e0479f49b3d99a449934031289de6bd4587", + "0xcead5cec4d5a30b28af721d8efbf77f05261daf76f60bc36298dbdc2793af703", + "0x8b5b553313660e25a9a357c050576122e6d697314b1044f19f076df0d33f9823", + "0xd73e844cd930c7463023fcc2eab8e40de90a5476f1c69d9466f506ec0a1c6953", + "0x70bf1aed5af719155b036b0d761b86610e22855f60279982d1ca83c2c1493861", + "0x5c2f23360e5247942d0b5150745cb4d8692de92e0fcb3cdfedff0341ff1f3a8e", + "0x1c2eaceb326006f77142e3ffacc660d17b5b1ccf0ef2d22026149b9973d03752", + "0x27f087175f96f9169e5e5320dffc920bab0181958df8385a143ac1ce9b7703a5", + "0x672608a35f4fa4bb65955138521a887a892b0cd35d09f0359d00fdfa5cf427fd", + "0x3b8942ca076f4e4e3e6222b577da88d888c79768d337bef14de6d75ba2540d11", + "0x7e1614b107c5a7adc7478198b2d99d3dee48e443f1f475524479aee0a4c9e402", + "0x5f9c5284a47ed5a6f6e672d48fea29966b3d91d63487ab47bc8f5514f231e687", + "0x3715bb37c438c4e95fab950f573d184770faf8019018d2b47d6220003f0b35d0", + "0x33137040d80df84243b63833eea5b34a505a2ca8fb1a34318b74cecf5f4aa7c8", + "0x470940a47746125aae7513cb22bdac628865ee3df34e99bd0ecd48ff23b47f41", + "0x875c9fda2e0ccffde973385ee72d106f1fea12fda8d250f55a85007e13422e40", + "0xd3a08793b023ff2eb5c3e1d9b90254172a796095972d8dc2714cc094f6fc6c19", + "0x135366e9141a1b871e73941f00c2e321b4ab51c99d58b95f1b201f30c3f7d0d2", + "0xc93ec0af7511a39dfe389fb37d21144914c99ddc8d259e47146e8b45d288e8f8", + "0x6ba2a677ff759be8e76f42e1b5d009b5a39f186fa417f00679105059b4cc725c", + "0x8657b391f8575ab4f7323a5e24e3ca53df61cb433cf88cbef40000c05badedc7", + "0x6e14d76d37b4dab55b5e49276b207b0e4f117ef8103728f8dadc487996e30c34", + "0xac4489a73246f8f81503e11254003158893785ae4a603eedddec8b23945d3630", + "0x50b5e07019621c041d061df0dc447674d719391862238c25181fd45f4bea441c", + "0x424431243694085158cdcf5ed1666b88421fb3c7fde538acf36f8ea8316d827b", + "0xf1d5e8256194f29e7da773ea8ef9e60ba7c5ceb7fb9ab9966d2c7b53d4c347ff", + "0x25f85c5fcda53d733bf0dafe552984b0e17e5202fe9225a9a1bf94b50575e5d8", + "0xe2499f7bbc8acdc3f273ac29f7757071844b739d2a84ab19440a9b1a3cbe901d", + "0x25525be1316671638e2b6146f3e3259be8dee11cf8a24cb64b0feb2ad7f1ebf9", + "0x0518268fb4b06a1285997efb841615a74d113571332ac7c935d2a303ca1d6f23", + "0x1510c9bf4678ec3e67d05c908ba6d2762c4a815476638cc1d281d65a7dab6745" + ].into_iter().map(|s| s.to_string()).collect(), + transactions_root: Some("0xdee0b25a965ff236e4d2e89f56de233759d71ad3e3e150ceb4cf5bb1f0ecf5c0".to_string()), + uncles: vec![], + } + } +} diff --git a/rs/ethereum/cketh/minter/src/logs.rs b/rs/ethereum/cketh/minter/src/logs.rs index d0cd82474ce..0e774463797 100644 --- a/rs/ethereum/cketh/minter/src/logs.rs +++ b/rs/ethereum/cketh/minter/src/logs.rs @@ -15,6 +15,7 @@ pub const INFO: PrintProxySink = PrintProxySink("INFO", &INFO_BUF); pub const DEBUG: PrintProxySink = PrintProxySink("DEBUG", &DEBUG_BUF); pub const TRACE_HTTP: PrintProxySink = PrintProxySink("TRACE_HTTP", &TRACE_HTTP_BUF); +#[derive(Debug)] pub struct PrintProxySink(&'static str, &'static GlobalBuffer); impl Sink for PrintProxySink { diff --git a/rs/ethereum/cketh/minter/tests/cketh.rs b/rs/ethereum/cketh/minter/tests/cketh.rs index efa1256c15b..61f21202178 100644 --- a/rs/ethereum/cketh/minter/tests/cketh.rs +++ b/rs/ethereum/cketh/minter/tests/cketh.rs @@ -41,7 +41,7 @@ use std::str::FromStr; #[test] fn should_deposit_and_withdraw() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let minter: Principal = cketh.minter_id.into(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); @@ -101,50 +101,50 @@ fn should_deposit_and_withdraw() { assert_eq!(cketh.balance_of(caller), Nat::from(0_u8)); cketh.assert_has_unique_events_in_order(&vec![ - EventPayload::AcceptedEthWithdrawalRequest { - withdrawal_amount: withdrawal_amount.clone(), - destination: destination.clone(), - ledger_burn_index: withdrawal_id.clone(), - from: caller, - from_subaccount: None, - created_at: Some(time), - }, - EventPayload::CreatedTransaction { - withdrawal_id: withdrawal_id.clone(), - transaction: UnsignedTransaction { - chain_id: Nat::from(1_u8), - nonce: Nat::from(0_u8), - max_priority_fee_per_gas, - max_fee_per_gas: max_fee_per_gas.clone(), - gas_limit: gas_limit.clone(), - destination, - value: withdrawal_amount - max_fee_per_gas * gas_limit, - data: Default::default(), - access_list: vec![], + EventPayload::AcceptedEthWithdrawalRequest { + withdrawal_amount: withdrawal_amount.clone(), + destination: destination.clone(), + ledger_burn_index: withdrawal_id.clone(), + from: caller, + from_subaccount: None, + created_at: Some(time), + }, + EventPayload::CreatedTransaction { + withdrawal_id: withdrawal_id.clone(), + transaction: UnsignedTransaction { + chain_id: Nat::from(1_u8), + nonce: Nat::from(0_u8), + max_priority_fee_per_gas, + max_fee_per_gas: max_fee_per_gas.clone(), + gas_limit: gas_limit.clone(), + destination, + value: withdrawal_amount - max_fee_per_gas * gas_limit, + data: Default::default(), + access_list: vec![], + }, + }, + EventPayload::SignedTransaction { + withdrawal_id: withdrawal_id.clone(), + raw_transaction: "0x02f87301808459682f008507af2c9f6282520894221e931fbfcb9bd54ddd26ce6f5e29e98add01c0880160cf1e9917a0e680c001a0b27af25a08e87836a778ac2858fdfcff1f6f3a0d43313782c81d05ca34b80271a078026b399a32d3d7abab625388a3c57f651c66a182eb7f8b1a58d9aef7547256".to_string(), }, - }, - EventPayload::SignedTransaction { - withdrawal_id: withdrawal_id.clone(), - raw_transaction: "0x02f87301808459682f008507af2c9f6282520894221e931fbfcb9bd54ddd26ce6f5e29e98add01c0880160cf1e9917a0e680c001a0b27af25a08e87836a778ac2858fdfcff1f6f3a0d43313782c81d05ca34b80271a078026b399a32d3d7abab625388a3c57f651c66a182eb7f8b1a58d9aef7547256".to_string(), - }, - EventPayload::FinalizedTransaction { - withdrawal_id, - transaction_receipt: TransactionReceipt { - block_hash: DEFAULT_BLOCK_HASH.to_string(), - block_number: Nat::from(DEFAULT_BLOCK_NUMBER), - effective_gas_price: Nat::from(4277923390u64), - gas_used: Nat::from(21_000_u32), - status: TransactionStatus::Success, - transaction_hash: - "0x2cf1763e8ee3990103a31a5709b17b83f167738abb400844e67f608a98b0bdb5".to_string(), + EventPayload::FinalizedTransaction { + withdrawal_id, + transaction_receipt: TransactionReceipt { + block_hash: DEFAULT_BLOCK_HASH.to_string(), + block_number: Nat::from(DEFAULT_BLOCK_NUMBER), + effective_gas_price: Nat::from(4277923390u64), + gas_used: Nat::from(21_000_u32), + status: TransactionStatus::Success, + transaction_hash: + "0x2cf1763e8ee3990103a31a5709b17b83f167738abb400844e67f608a98b0bdb5".to_string(), + }, }, - }, - ]); + ]); } #[test] fn should_retrieve_cache_transaction_price() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); let destination = DEFAULT_WITHDRAWAL_DESTINATION_ADDRESS.to_string(); @@ -187,7 +187,7 @@ fn should_retrieve_cache_transaction_price() { #[test] fn should_block_deposit_from_blocked_address() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let from_address_blocked: Address = "0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1" .parse() .unwrap(); @@ -221,7 +221,7 @@ fn should_not_mint_when_logs_inconsistent() { }; assert_ne!(ankr_logs, public_node_logs); - CkEthSetup::default() + CkEthSetup::default_with_maybe_evm_rpc() .deposit(deposit_params.with_mock_eth_get_logs(move |mock| { mock.respond_with(JsonRpcProvider::Ankr, ankr_logs.clone()) .respond_with(JsonRpcProvider::PublicNode, public_node_logs.clone()) @@ -232,7 +232,7 @@ fn should_not_mint_when_logs_inconsistent() { #[test] fn should_block_withdrawal_to_blocked_address() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); let blocked_address = "0x01e2919679362dFBC9ee1644Ba9C6da6D6245BB1".to_string(); @@ -250,7 +250,7 @@ fn should_block_withdrawal_to_blocked_address() { #[test] fn should_fail_to_withdraw_without_approval() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); cketh @@ -268,7 +268,7 @@ fn should_fail_to_withdraw_without_approval() { #[test] fn should_fail_to_withdraw_when_insufficient_funds() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let deposit_amount = CKETH_MINIMUM_WITHDRAWAL_AMOUNT + CKETH_TRANSFER_FEE; let amount_after_approval = CKETH_MINIMUM_WITHDRAWAL_AMOUNT; @@ -294,7 +294,7 @@ fn should_fail_to_withdraw_when_insufficient_funds() { #[test] fn should_fail_to_withdraw_too_small_amount() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); cketh .deposit(DepositParams::default()) @@ -313,7 +313,7 @@ fn should_fail_to_withdraw_too_small_amount() { #[test] fn should_not_finalize_transaction_when_receipts_do_not_match() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); @@ -338,7 +338,7 @@ fn should_not_finalize_transaction_when_receipts_do_not_match() { #[test] fn should_not_send_eth_transaction_when_fee_history_inconsistent() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); @@ -379,7 +379,7 @@ fn should_not_send_eth_transaction_when_fee_history_inconsistent() { #[test] fn should_reimburse() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let minter: Principal = cketh.minter_id.into(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); @@ -542,7 +542,7 @@ fn should_reimburse() { #[test] fn should_resubmit_transaction_as_is_when_price_still_actual() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); let (expected_tx, expected_sig) = default_signed_eip_1559_transaction(); @@ -566,7 +566,7 @@ fn should_resubmit_transaction_as_is_when_price_still_actual() { #[test] fn should_resubmit_new_transaction_when_price_increased() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); let (expected_tx, expected_sig) = default_signed_eip_1559_transaction(); @@ -641,7 +641,7 @@ fn should_resubmit_new_transaction_when_price_increased() { #[test] fn should_not_overlap_when_scrapping_logs() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); cketh.env.advance_time(SCRAPPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) @@ -690,7 +690,8 @@ fn should_not_overlap_when_scrapping_logs() { #[test] fn should_retry_from_same_block_when_scrapping_fails() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); + let prev_events_len = cketh.get_all_events().len(); cketh.env.advance_time(SCRAPPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) @@ -715,6 +716,8 @@ fn should_retry_from_same_block_when_scrapping_fails() { let cketh = cketh .check_audit_logs_and_upgrade(Default::default()) + .check_events() + .skip(prev_events_len) .assert_has_unique_events_in_order(&vec![EventPayload::SyncedToBlock { block_number: LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(), }]); @@ -744,7 +747,7 @@ fn should_retry_from_same_block_when_scrapping_fails() { #[test] fn should_scrap_one_block_when_at_boundary_with_last_finalized_block() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); cketh.env.advance_time(SCRAPPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) @@ -766,7 +769,8 @@ fn should_scrap_one_block_when_at_boundary_with_last_finalized_block() { #[test] fn should_panic_when_last_finalized_block_in_the_past() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); + let prev_events_len = cketh.get_all_events().len(); cketh.env.advance_time(SCRAPPING_ETH_LOGS_INTERVAL); MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) @@ -776,6 +780,8 @@ fn should_panic_when_last_finalized_block_in_the_past() { let cketh = cketh .check_audit_logs_and_upgrade(Default::default()) + .check_events() + .skip(prev_events_len) .assert_has_unique_events_in_order(&vec![EventPayload::SyncedToBlock { block_number: LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL.into(), }]); @@ -811,7 +817,7 @@ fn should_panic_when_last_finalized_block_in_the_past() { fn should_skip_scrapping_when_last_seen_block_newer_than_current_height() { let safe_block_number = LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 100; let finalized_block_number = safe_block_number - 32; - let cketh = CkEthSetup::default().check_audit_logs_and_upgrade(UpgradeArg { + let cketh = CkEthSetup::default_with_maybe_evm_rpc().check_audit_logs_and_upgrade(UpgradeArg { ethereum_block_height: Some(CandidBlockTag::Safe), ..Default::default() }); @@ -862,7 +868,7 @@ fn should_skip_scrapping_when_last_seen_block_newer_than_current_height() { #[test] fn should_half_range_of_scrapped_logs_when_response_over_two_mega_bytes() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let deposit = DepositParams::default().eth_log_entry(); // around 600 bytes per log // we need at least 3334 logs to reach the 2MB limit @@ -918,7 +924,7 @@ fn should_half_range_of_scrapped_logs_when_response_over_two_mega_bytes() { #[test] fn should_skip_single_block_containing_too_many_events() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let deposit = DepositParams::default().eth_log_entry(); // around 600 bytes per log // we need at least 3334 logs to reach the 2MB limit @@ -1005,7 +1011,7 @@ fn should_skip_single_block_containing_too_many_events() { #[allow(deprecated)] #[test] fn should_retrieve_minter_info() { - let cketh = CkEthSetup::default(); + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); let caller: Principal = cketh.caller.into(); let withdrawal_amount = Nat::from(CKETH_WITHDRAWAL_AMOUNT); let destination = DEFAULT_WITHDRAWAL_DESTINATION_ADDRESS.to_string(); @@ -1074,3 +1080,18 @@ fn should_retrieve_minter_info() { fn format_ethereum_address_to_eip_55(address: &str) -> String { Address::from_str(address).unwrap().to_string() } + +/// Tests with the EVM RPC canister +mod cketh_evm_rpc { + use super::*; + + #[test] + fn should_retrieve_block_number() { + let cketh = CkEthSetup::default_with_maybe_evm_rpc(); + + MockJsonRpcProviders::when(JsonRpcMethod::EthGetBlockByNumber) + .respond_for_all_with(block_response(LAST_SCRAPED_BLOCK_NUMBER_AT_INSTALL + 3)) + .build() + .expect_rpc_calls(&cketh); + } +} diff --git a/rs/ethereum/cketh/test_utils/src/ckerc20.rs b/rs/ethereum/cketh/test_utils/src/ckerc20.rs index 97038a37fc1..3fe9a99405d 100644 --- a/rs/ethereum/cketh/test_utils/src/ckerc20.rs +++ b/rs/ethereum/cketh/test_utils/src/ckerc20.rs @@ -80,7 +80,7 @@ impl CkErc20Setup { } pub fn new_without_ckerc20_active(env: Arc) -> Self { - let cketh = CkEthSetup::new(env.clone()); + let cketh = CkEthSetup::maybe_evm_rpc(env.clone()); let orchestrator = LedgerSuiteOrchestrator::new( env.clone(), LedgerSuiteOrchestratorInitArg { diff --git a/rs/ethereum/cketh/test_utils/src/events.rs b/rs/ethereum/cketh/test_utils/src/events.rs index 7a451280025..c37eb8684c3 100644 --- a/rs/ethereum/cketh/test_utils/src/events.rs +++ b/rs/ethereum/cketh/test_utils/src/events.rs @@ -15,6 +15,14 @@ impl> MinterEventAssert { } impl MinterEventAssert { + pub fn skip(self, count: usize) -> Self { + let events = self.events.into_iter().skip(count).collect(); + Self { + setup: self.setup, + events, + } + } + pub fn assert_has_unique_events_in_order(self, expected_events: &[EventPayload]) -> T { let mut found_event_indexes = BTreeMap::new(); for (index_expected_event, expected_event) in expected_events.iter().enumerate() { diff --git a/rs/ethereum/cketh/test_utils/src/lib.rs b/rs/ethereum/cketh/test_utils/src/lib.rs index 12463b44f57..ca4dfc436c9 100644 --- a/rs/ethereum/cketh/test_utils/src/lib.rs +++ b/rs/ethereum/cketh/test_utils/src/lib.rs @@ -4,7 +4,7 @@ use crate::flow::{ }; use crate::mock::JsonRpcMethod; use assert_matches::assert_matches; -use candid::{Decode, Encode, Nat, Principal}; +use candid::{CandidType, Decode, Deserialize, Encode, Nat, Principal}; use ic_canisters_http_types::{HttpRequest, HttpResponse}; use ic_cketh_minter::endpoints::events::{Event, EventPayload, GetEventsResult}; use ic_cketh_minter::endpoints::{ @@ -89,6 +89,7 @@ pub struct CkEthSetup { pub caller: PrincipalId, pub ledger_id: CanisterId, pub minter_id: CanisterId, + pub evm_rpc_id: Option, } impl Default for CkEthSetup { @@ -132,6 +133,7 @@ impl CkEthSetup { caller, ledger_id, minter_id, + evm_rpc_id: None, }; assert_eq!( @@ -141,6 +143,32 @@ impl CkEthSetup { cketh } + fn new_with_evm_rpc(env: Arc) -> Self { + // install ckETH first to keep the same canisterID and minter Ethereum address. + let mut cketh = Self::new(env); + let evm_rpc_id = cketh.env.create_canister(None); + install_evm_rpc(&cketh.env, evm_rpc_id); + cketh.upgrade_minter(UpgradeArg { + evm_rpc_id: Some(evm_rpc_id.into()), + ..Default::default() + }); + cketh.evm_rpc_id = Some(evm_rpc_id); + cketh.env.tick(); + cketh.env.tick(); //to load tECDSA key and minter's address + cketh + } + + pub fn default_with_maybe_evm_rpc() -> Self { + Self::maybe_evm_rpc(Arc::new(new_state_machine())) + } + + pub fn maybe_evm_rpc(env: Arc) -> Self { + match std::env::var("EVM_RPC_CANISTER_WASM_PATH") { + Ok(_) => CkEthSetup::new_with_evm_rpc(env), + Err(_) => CkEthSetup::new(env), + } + } + pub fn deposit(self, params: DepositParams) -> DepositFlow { DepositFlow { setup: self, @@ -395,6 +423,10 @@ impl CkEthSetup { serde_json::from_slice(&response.body).expect("failed to parse ckbtc minter log") } + pub fn check_events(self) -> MinterEventAssert { + MinterEventAssert::from_fetching_all_events(self) + } + pub fn assert_has_unique_events_in_order(self, expected_events: &[EventPayload]) -> Self { MinterEventAssert::from_fetching_all_events(self) .assert_has_unique_events_in_order(expected_events) @@ -583,6 +615,14 @@ fn minter_wasm() -> Vec { ) } +fn evm_rpc_wasm() -> Vec { + load_wasm( + std::env::var("CARGO_MANIFEST_DIR").unwrap(), + "evm_rpc_canister", + &[], + ) +} + fn install_minter(env: &StateMachine, ledger_id: CanisterId, minter_id: CanisterId) -> CanisterId { let args = MinterInitArgs { ecdsa_key_name: "master_ecdsa_public_key".parse().unwrap(), @@ -600,6 +640,20 @@ fn install_minter(env: &StateMachine, ledger_id: CanisterId, minter_id: Canister minter_id } +fn install_evm_rpc(env: &StateMachine, evm_rpc_id: CanisterId) { + let args = EvmRpcInitArgs { + nodes_in_subnet: 28, + }; + env.install_existing_canister(evm_rpc_id, evm_rpc_wasm(), Encode!(&args).unwrap()) + .unwrap(); +} + +#[derive(Clone, Debug, CandidType, Deserialize)] +pub struct EvmRpcInitArgs { + #[serde(rename = "nodesInSubnet")] + pub nodes_in_subnet: u32, +} + fn assert_reply(result: WasmResult) -> Vec { match result { WasmResult::Reply(bytes) => bytes, diff --git a/rs/ethereum/cketh/test_utils/src/mock.rs b/rs/ethereum/cketh/test_utils/src/mock.rs index 08f368da340..51acd123f37 100644 --- a/rs/ethereum/cketh/test_utils/src/mock.rs +++ b/rs/ethereum/cketh/test_utils/src/mock.rs @@ -1,6 +1,5 @@ use crate::{assert_reply, CkEthSetup, MAX_TICKS}; use candid::{Decode, Encode}; -use ic_base_types::CanisterId; use ic_cdk::api::management_canister::http_request::{ HttpResponse as OutCallHttpResponse, TransformArgs, }; @@ -213,7 +212,7 @@ impl StubOnce { } } - fn expect_rpc_call(self, env: &StateMachine, canister_id_cleanup_response: CanisterId) { + fn expect_rpc_call(self, env: &StateMachine) { let (id, context) = self.matcher.find_rpc_call(env).unwrap_or_else(|| { panic!( "no request found matching the stub {:?}. Current requests {}", @@ -267,6 +266,7 @@ impl StubOnce { }, context: clean_up_context.to_vec(), }; + let canister_id_cleanup_response = context.request.sender; let clean_up_response = Decode!( &assert_reply( env.execute_ingress( @@ -336,7 +336,7 @@ impl MockJsonRpcProviders { pub fn expect_rpc_calls>(self, cketh: T) { let cketh = cketh.as_ref(); for stub in self.stubs { - stub.expect_rpc_call(&cketh.env, cketh.minter_id); + stub.expect_rpc_call(&cketh.env); } } diff --git a/rs/ethereum/cketh/test_utils/src/response.rs b/rs/ethereum/cketh/test_utils/src/response.rs index e1cf164499f..d4fceeb5ce8 100644 --- a/rs/ethereum/cketh/test_utils/src/response.rs +++ b/rs/ethereum/cketh/test_utils/src/response.rs @@ -128,9 +128,31 @@ pub fn send_raw_transaction_response() -> ethers_core::types::TxHash { } pub fn block_response(block_number: u64) -> ethers_core::types::Block { + use ethers_core::types::{H256, H64}; + + let mut hash = [0_u8; 32]; + hex::decode_to_slice(&DEFAULT_BLOCK_HASH[2..], &mut hash).unwrap(); + + let mut log_blooms = [0_u8; 256]; + hex::decode_to_slice(&"0x93ab55f727ed7f7f7ffa47b6e520df221dce71f165d0f470a71d051cfd36ab0ad4015725f16938bb3798fb4fc58fd3d95e23a8689ba06ae3ce16ffba95afbedcfece0dcdce2f1e7f6eb6573a92c5a8feadec65bd2655296a5ff07ecee9ae5b2abfddd7b2877ed3f5ac7bf4d95061bd5d6f8e37fb87995ea0904d58d6d8cbb86f9fef7af0364e834154dbb74a2ff7c6355a43ac1d73d9bcf9f5a9ed756492fffdfbffd1e7ffdf7f274e36fbc4e9e3bdf9e56fdad089dd582fd7e5fc733fcc63753762f4f7c49dbffeb5a196ae6a3fddd749f1f26effedd1df23ad5d23b9d2fc2f19ffa5513504a53155d477f1f155b966ddadfa195b4c6bdafb9df97ff065debf"[2..], &mut log_blooms).unwrap(); + + let mut miner = [0_u8; 20]; + hex::decode_to_slice( + &"0x1f9090aae28b8a3dceadf281b0f12828e676c326"[2..], + &mut miner, + ) + .unwrap(); + ethers_core::types::Block:: { + hash: Some(hash.into()), number: Some(block_number.into()), base_fee_per_gas: Some(0x3e4f64de7_u64.into()), + nonce: Some(H64::zero()), + logs_bloom: Some(log_blooms.into()), + total_difficulty: Some(0xc70d815d562d3cfa955_u128.into()), + mix_hash: Some(H256::zero()), + author: Some(miner.into()), + size: Some(0x19eea_u64.into()), ..Default::default() } } diff --git a/rs/ethereum/evm-rpc-client/BUILD.bazel b/rs/ethereum/evm-rpc-client/BUILD.bazel index 2aa58d9ffe6..aca3f66eb7a 100644 --- a/rs/ethereum/evm-rpc-client/BUILD.bazel +++ b/rs/ethereum/evm-rpc-client/BUILD.bazel @@ -3,7 +3,7 @@ load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") package(default_visibility = ["//visibility:public"]) rust_library( - name = "evm_rpc_client", + name = "evm-rpc-client", srcs = glob( ["src/**/*.rs"], ), @@ -15,14 +15,15 @@ rust_library( "@crate_index//:ic-canister-log", "@crate_index//:ic-cdk", "@crate_index//:serde", + "@crate_index//:thiserror", ], ) rust_test( name = "unit_tests", - crate = ":evm_rpc_client", + crate = ":evm-rpc-client", deps = [ - ":evm_rpc_client", + ":evm-rpc-client", "@crate_index//:mockall", "@crate_index//:tokio", ], diff --git a/rs/ethereum/evm-rpc-client/Cargo.toml b/rs/ethereum/evm-rpc-client/Cargo.toml index 4a2891096fa..6613bbee0a2 100644 --- a/rs/ethereum/evm-rpc-client/Cargo.toml +++ b/rs/ethereum/evm-rpc-client/Cargo.toml @@ -12,6 +12,7 @@ candid = { workspace = true } ic-canister-log = "0.2.0" ic-cdk = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] mockall = { workspace = true } diff --git a/rs/ethereum/evm-rpc-client/src/lib.rs b/rs/ethereum/evm-rpc-client/src/lib.rs index 3d72b25e48d..986f8ca4708 100644 --- a/rs/ethereum/evm-rpc-client/src/lib.rs +++ b/rs/ethereum/evm-rpc-client/src/lib.rs @@ -7,6 +7,7 @@ use crate::types::candid::{ Block, BlockTag, MultiRpcResult, ProviderError, RpcConfig, RpcError, RpcServices, }; use async_trait::async_trait; +use candid::utils::ArgumentEncoder; use candid::{CandidType, Principal}; use ic_canister_log::{log, Sink}; use ic_cdk::api::call::RejectionCode; @@ -23,10 +24,11 @@ pub trait Runtime { cycles: u128, ) -> Result where - In: CandidType + Send + 'static, + In: ArgumentEncoder + Send + 'static, Out: CandidType + DeserializeOwned + 'static; } +#[derive(Clone, Debug, PartialEq, Eq)] pub struct EvmRpcClient { runtime: R, logger: L, @@ -36,6 +38,12 @@ pub struct EvmRpcClient { max_num_retries: u32, } +impl EvmRpcClient { + pub fn builder_for_ic(logger: L) -> EvmRpcClientBuilder { + EvmRpcClientBuilder::new(IcRuntime {}, logger) + } +} + impl EvmRpcClient { pub fn builder(runtime: R, logger: L) -> EvmRpcClientBuilder { EvmRpcClientBuilder::new(runtime, logger) @@ -147,11 +155,30 @@ impl EvmRpcClientBuilder { } } + pub fn with_runtime( + self, + runtime: OtherRuntime, + ) -> EvmRpcClientBuilder { + EvmRpcClientBuilder { + runtime, + logger: self.logger, + providers: self.providers, + evm_canister_id: self.evm_canister_id, + min_attached_cycles: self.min_attached_cycles, + max_num_retries: self.max_num_retries, + } + } + pub fn with_providers(mut self, providers: RpcServices) -> Self { self.providers = providers; self } + pub fn with_evm_canister_id(mut self, evm_canister_id: Principal) -> Self { + self.evm_canister_id = evm_canister_id; + self + } + pub fn with_min_attached_cycles(mut self, min_attached_cycles: u128) -> Self { self.min_attached_cycles = min_attached_cycles; self @@ -173,3 +200,25 @@ impl EvmRpcClientBuilder { } } } + +#[derive(Clone, Debug, Copy, Eq, PartialEq)] +pub struct IcRuntime {} + +#[async_trait] +impl Runtime for IcRuntime { + async fn call( + &self, + id: Principal, + method: &str, + args: In, + cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send + 'static, + Out: CandidType + DeserializeOwned + 'static, + { + ic_cdk::api::call::call_with_payment128(id, method, args, cycles) + .await + .map(|(res,)| res) + } +} diff --git a/rs/ethereum/evm-rpc-client/src/tests.rs b/rs/ethereum/evm-rpc-client/src/tests.rs index 876a568efb3..e7e132b6207 100644 --- a/rs/ethereum/evm-rpc-client/src/tests.rs +++ b/rs/ethereum/evm-rpc-client/src/tests.rs @@ -232,6 +232,7 @@ fn printable_logger() -> MockLogger { mod mock { use crate::Runtime; use async_trait::async_trait; + use candid::utils::ArgumentEncoder; use candid::{CandidType, Principal}; use ic_canister_log::{LogEntry, Sink}; use ic_cdk::api::call::RejectionCode; @@ -251,7 +252,7 @@ mod mock { cycles: u128, ) -> Result where - In: CandidType + Send + 'static, + In: ArgumentEncoder + Send + 'static, Out: CandidType + DeserializeOwned + 'static; } } diff --git a/rs/ethereum/evm-rpc-client/src/types.rs b/rs/ethereum/evm-rpc-client/src/types.rs index be06703bb1f..3027eba9be7 100644 --- a/rs/ethereum/evm-rpc-client/src/types.rs +++ b/rs/ethereum/evm-rpc-client/src/types.rs @@ -1,8 +1,10 @@ pub mod candid { use candid::{CandidType, Deserialize, Nat}; use ic_cdk::api::call::RejectionCode; + use ic_cdk::api::management_canister::http_request::HttpHeader; use serde::Serialize; use std::iter; + use thiserror::Error; #[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize, Default)] pub enum BlockTag { @@ -74,11 +76,15 @@ pub mod candid { } } - #[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize)] + #[derive(Clone, Error, Debug, PartialEq, Eq, CandidType, Deserialize)] pub enum RpcError { + #[error("Provider error: {0}")] ProviderError(ProviderError), + #[error("HTTP outcall error: {0}")] HttpOutcallError(HttpOutcallError), + #[error("JSON-RPC error: {0}")] JsonRpcError(JsonRpcError), + #[error("Validation error: {0}")] ValidationError(ValidationError), } @@ -88,17 +94,22 @@ pub mod candid { } } - #[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize)] + #[derive(Clone, Error, Debug, PartialEq, Eq, CandidType, Deserialize)] pub enum ProviderError { + #[error("No permission to call this provider")] NoPermission, + #[error("Not enough cycles, expected {expected}, received {received}")] TooFewCycles { expected: u128, received: u128 }, + #[error("Provider not found")] ProviderNotFound, + #[error("Missing required provider")] MissingRequiredProvider, } - #[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize)] + #[derive(Clone, Error, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize)] pub enum HttpOutcallError { /// Error from the IC system API. + #[error("IC error (code: {code:?}): {message}")] IcError { code: RejectionCode, message: String, @@ -106,6 +117,7 @@ pub mod candid { /// Response is not a valid JSON-RPC response, /// which means that the response was not successful (status other than 2xx) /// or that the response body could not be deserialized into a JSON-RPC response. + #[error("Invalid HTTP JSON-RPC response: status {status}, body: {body}, parsing error: {parsing_error:?}")] InvalidHttpJsonRpcResponse { status: u16, body: String, @@ -115,31 +127,49 @@ pub mod candid { } #[derive( - Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Serialize, Deserialize, + Clone, Error, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Serialize, Deserialize, )] + #[error("JSON-RPC error (code: {code}): {message}")] pub struct JsonRpcError { pub code: i64, pub message: String, } - #[derive(Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize)] + #[derive(Clone, Error, Debug, PartialEq, Eq, PartialOrd, Ord, CandidType, Deserialize)] pub enum ValidationError { + #[error("Custom: {0}")] Custom(String), + #[error("Invalid hex: {0}")] InvalidHex(String), + #[error("Invalid URL: {0}")] UrlParseError(String), + #[error("Host not allowed: {0}")] HostNotAllowed(String), + #[error("Credential path not allowed")] CredentialPathNotAllowed, + #[error("Credential header not allowed")] CredentialHeaderNotAllowed, } - #[derive(Clone, Debug, CandidType, Deserialize)] + #[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)] pub enum RpcServices { + Custom { + #[serde(rename = "chainId")] + chain_id: u64, + services: Vec, + }, EthMainnet(Option>), EthSepolia(Option>), } + #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, CandidType)] + pub struct RpcApi { + pub url: String, + pub headers: Option>, + } + #[derive( - Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, CandidType, + Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, CandidType, )] pub enum RpcService { EthMainnet(EthMainnetService),