From 4d2c5a5dde0f22a2a5a3c81ffbfe6d55947192ed Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 6 Aug 2025 15:33:40 +0200 Subject: [PATCH 01/57] XC-412: Initialize new empty `evm_rpc_client` crate --- Cargo.lock | 25 ++++++++----------------- Cargo.toml | 2 +- evm_rpc_client/CHANGELOG.md | 0 evm_rpc_client/Cargo.toml | 15 +++++++++++++++ evm_rpc_client/LICENSE | 1 + evm_rpc_client/NOTICE | 1 + evm_rpc_client/README.md | 3 +++ evm_rpc_client/src/lib.rs | 0 8 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 evm_rpc_client/CHANGELOG.md create mode 100644 evm_rpc_client/Cargo.toml create mode 120000 evm_rpc_client/LICENSE create mode 120000 evm_rpc_client/NOTICE create mode 100644 evm_rpc_client/README.md create mode 100644 evm_rpc_client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 22d51834..0d47c2f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,27 +316,23 @@ dependencies = [ [[package]] name = "canhttp" -version = "0.1.1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30b89e93857ec22d9b5f11b1647cdf9bb28c328e1d5ae871ecba2d94e38f8fb" dependencies = [ "assert_matches", - "candid", "ciborium", "futures-channel", "futures-util", "http", "ic-cdk", "ic-error-types", - "itertools 0.14.0", - "maplit", "num-traits", "pin-project", - "proptest", "serde", "serde_json", "sha2", - "strum 0.27.1", "thiserror 2.0.12", - "tokio", "tower", "tower-layer", ] @@ -959,6 +955,10 @@ dependencies = [ "zeroize", ] +[[package]] +name = "evm_rpc_client" +version = "1.4.0" + [[package]] name = "evm_rpc_types" version = "1.4.0" @@ -1707,15 +1707,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -1763,7 +1754,7 @@ dependencies = [ "ascii-canvas", "bit-set 0.5.3", "ena", - "itertools 0.11.0", + "itertools", "lalrpop-util", "petgraph", "pico-args", diff --git a/Cargo.toml b/Cargo.toml index c786c4fa..a69b43c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,4 +103,4 @@ thiserror = "2.0.12" url = "2.5" [workspace] -members = ["e2e/rust", "evm_rpc_types"] +members = ["e2e/rust", "evm_rpc_types", "evm_rpc_client"] diff --git a/evm_rpc_client/CHANGELOG.md b/evm_rpc_client/CHANGELOG.md new file mode 100644 index 00000000..e69de29b diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml new file mode 100644 index 00000000..965198fd --- /dev/null +++ b/evm_rpc_client/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "evm_rpc_client" +version = "1.4.0" +description = "Rust client for interacting with the EVM RPC canister" +license = "Apache-2.0" +readme = "README.md" +authors = ["DFINITY Foundation"] +edition = "2021" +include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"] +repository = "https://github.com/dfinity/evm-rpc-client" +documentation = "https://docs.rs/evm_rpc_client" + +[dependencies] + +[dev-dependencies] \ No newline at end of file diff --git a/evm_rpc_client/LICENSE b/evm_rpc_client/LICENSE new file mode 120000 index 00000000..ea5b6064 --- /dev/null +++ b/evm_rpc_client/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/evm_rpc_client/NOTICE b/evm_rpc_client/NOTICE new file mode 120000 index 00000000..7e1b82f6 --- /dev/null +++ b/evm_rpc_client/NOTICE @@ -0,0 +1 @@ +../NOTICE \ No newline at end of file diff --git a/evm_rpc_client/README.md b/evm_rpc_client/README.md new file mode 100644 index 00000000..577d4cce --- /dev/null +++ b/evm_rpc_client/README.md @@ -0,0 +1,3 @@ +# EVM RPC Client + +This crate defines a client for interacting with the EVM RPC canister. \ No newline at end of file diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs new file mode 100644 index 00000000..e69de29b From d10ee6a8ddcd6b55877ff341c869d99bcdc70401 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 6 Aug 2025 16:08:33 +0200 Subject: [PATCH 02/57] XC-412: Add skeleton for client --- Cargo.lock | 19 +++ Cargo.toml | 1 + evm_rpc_client/Cargo.toml | 6 + evm_rpc_client/src/lib.rs | 256 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 282 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 0d47c2f9..d71025c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -958,6 +969,14 @@ dependencies = [ [[package]] name = "evm_rpc_client" version = "1.4.0" +dependencies = [ + "async-trait", + "candid", + "evm_rpc_types", + "ic-cdk", + "ic-error-types", + "serde", +] [[package]] name = "evm_rpc_types" diff --git a/Cargo.toml b/Cargo.toml index a69b43c4..953997b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ rand = "0.8" [workspace.dependencies] assert_matches = "1.5.0" +async-trait = "0.1.88" candid = { version = "0.10.13" } canlog = { version = "0.1.1", features = ["derive"] } candid_parser = { version = "0.1.4" } diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 965198fd..7dd1bdc1 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -11,5 +11,11 @@ repository = "https://github.com/dfinity/evm-rpc-client" documentation = "https://docs.rs/evm_rpc_client" [dependencies] +async-trait = { workspace = true } +candid = { workspace = true } +evm_rpc_types = { path = "../evm_rpc_types" } +ic-cdk = { workspace = true } +ic-error-types = { workspace = true } +serde = { workspace = true } [dev-dependencies] \ No newline at end of file diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index e69de29b..a7475bdb 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -0,0 +1,256 @@ +use async_trait::async_trait; +use candid::utils::ArgumentEncoder; +use candid::{CandidType, Principal}; +use evm_rpc_types::{ConsensusStrategy, RpcConfig, RpcServices}; +use ic_cdk::api::call::RejectionCode as IcCdkRejectionCode; +use ic_error_types::RejectCode; +use serde::de::DeserializeOwned; +use std::sync::Arc; + +/// The principal identifying the productive EVM RPC canister under NNS control. +/// +/// ```rust +/// use candid::Principal; +/// use evm_rpc_client::EVM_RPC_CANISTER; +/// +/// assert_eq!(EVM_RPC_CANISTER, Principal::from_text("7hfb6-caaaa-aaaar-qadga-cai").unwrap()) +/// ``` +pub const EVM_RPC_CANISTER: Principal = Principal::from_slice(&[0, 0, 0, 0, 2, 48, 0, 204, 1, 1]); + +/// Abstract the canister runtime so that the client code can be reused: +/// * in production using `ic_cdk`, +/// * in unit tests by mocking this trait, +/// * in integration tests by implementing this trait for `PocketIc`. +#[async_trait] +pub trait Runtime { + /// Defines how asynchronous inter-canister update calls are made. + async fn update_call( + &self, + id: Principal, + method: &str, + args: In, + cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned; + + /// Defines how asynchronous inter-canister query calls are made. + async fn query_call( + &self, + id: Principal, + method: &str, + args: In, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned; +} + +/// Client to interact with the EVM RPC canister. +#[derive(Debug)] +pub struct EvmRpcClient { + config: Arc>, +} + +impl Clone for EvmRpcClient { + fn clone(&self) -> Self { + Self { + config: self.config.clone(), + } + } +} + +impl EvmRpcClient { + /// Creates a [`ClientBuilder`] to configure a [`EvmRpcClient`]. + pub fn builder(runtime: R, evm_rpc_canister: Principal) -> ClientBuilder { + ClientBuilder::new(runtime, evm_rpc_canister) + } + + /// Returns a reference to the client's runtime. + pub fn runtime(&self) -> &R { + &self.config.runtime + } +} + +impl EvmRpcClient { + /// Creates a [`ClientBuilder`] to configure a [`EvmRpcClient`] targeting [`EVM_RPC_CANISTER`] + /// running on the Internet Computer. + pub fn builder_for_ic() -> ClientBuilder { + ClientBuilder::new(IcRuntime, EVM_RPC_CANISTER) + } +} + +/// Client to interact with the EVM RPC canister. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct ClientConfig { + runtime: R, + evm_rpc_canister: Principal, + rpc_config: Option, + rpc_services: RpcServices, +} + +/// A [`ClientBuilder`] to create a [`EvmRpcClient`] with custom configuration. +#[must_use] +pub struct ClientBuilder { + config: ClientConfig, +} + +impl ClientBuilder { + fn new(runtime: R, evm_rpc_canister: Principal) -> Self { + Self { + config: ClientConfig { + runtime, + evm_rpc_canister, + rpc_config: None, + rpc_services: RpcServices::EthMainnet(None), + }, + } + } + + /// Modify the existing runtime by applying a transformation function. + /// + /// The transformation does not necessarily produce a runtime of the same type. + pub fn with_runtime S>(self, other_runtime: F) -> ClientBuilder { + ClientBuilder { + config: ClientConfig { + runtime: other_runtime(self.config.runtime), + evm_rpc_canister: self.config.evm_rpc_canister, + rpc_config: self.config.rpc_config, + rpc_services: self.config.rpc_services, + }, + } + } + + /// Mutates the builder to use the given [`RpcServices`]. + pub fn with_rpc_sources(mut self, rpc_services: RpcServices) -> Self { + self.config.rpc_services = rpc_services; + self + } + + /// Mutates the builder to use the given [`RpcConfig`]. + pub fn with_rpc_config(mut self, rpc_config: RpcConfig) -> Self { + self.config.rpc_config = Some(rpc_config); + self + } + + /// Mutates the builder to use the given [`ConsensusStrategy`] in the [`RpcConfig`]. + pub fn with_consensus_strategy(mut self, consensus_strategy: ConsensusStrategy) -> Self { + self.config.rpc_config = Some(RpcConfig { + response_consensus: Some(consensus_strategy), + ..self.config.rpc_config.unwrap_or_default() + }); + self + } + + /// Mutates the builder to use the given `response_size_estimate` in the [`RpcConfig`]. + pub fn with_response_size_estimate(mut self, response_size_estimate: u64) -> Self { + self.config.rpc_config = Some(RpcConfig { + response_size_estimate: Some(response_size_estimate), + ..self.config.rpc_config.unwrap_or_default() + }); + self + } + + /// Creates a [`EvmRpcClient`] from the configuration specified in the [`ClientBuilder`]. + pub fn build(self) -> EvmRpcClient { + EvmRpcClient { + config: Arc::new(self.config), + } + } +} + +impl EvmRpcClient { + /// Call `getProviders` on the EVM RPC canister. + pub async fn get_providers(&self) -> Vec { + self.config + .runtime + .query_call(self.config.evm_rpc_canister, "getProviders", ()) + .await + .unwrap() + } + + /// Call `getServiceProviderMap` on the EVM RPC canister. + // TODO XC-412: Create type alias in `evm_rpc_types` for `ProviderId` i.e. `u64` + pub async fn get_service_provider_map(&self) -> Vec<(evm_rpc_types::RpcService, u64)> { + self.config + .runtime + .query_call(self.config.evm_rpc_canister, "getServiceProviderMap", ()) + .await + .unwrap() + } + + /// Call `updateApiKeys` on the EVM RPC canister. + // TODO XC-412: Create type alias in `evm_rpc_types` for `ProviderId` i.e. `u64` + pub async fn update_api_keys(&self, api_keys: &[(u64, Option)]) { + self.config + .runtime + .update_call( + self.config.evm_rpc_canister, + "updateApiKeys", + (api_keys.to_vec(),), + 0, + ) + .await + .unwrap() + } +} + +/// Runtime when interacting with a canister running on the Internet Computer. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct IcRuntime; + +#[async_trait] +impl Runtime for IcRuntime { + async fn update_call( + &self, + id: Principal, + method: &str, + args: In, + cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + ic_cdk::api::call::call_with_payment128(id, method, args, cycles) + .await + .map(|(res,)| res) + .map_err(|(code, message)| (convert_reject_code(code), message)) + } + + 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) + .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") + } + } +} From 4b71bd1f74f24f87a257646ad3ae086af7d12d74 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 6 Aug 2025 18:15:57 +0200 Subject: [PATCH 03/57] XC-412: Add `eth_getLogs` to new client --- Cargo.lock | 1104 ++++++++++++++++++++++++++- Cargo.toml | 2 + evm_rpc_client/Cargo.toml | 4 +- evm_rpc_client/src/lib.rs | 41 + evm_rpc_client/src/request/mod.rs | 351 +++++++++ evm_rpc_types/Cargo.toml | 8 +- evm_rpc_types/src/response/mod.rs | 21 + evm_rpc_types/src/result/mod.rs | 28 +- evm_rpc_types/src/rpc_client/mod.rs | 6 +- 9 files changed, 1544 insertions(+), 21 deletions(-) create mode 100644 evm_rpc_client/src/request/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d71025c7..8cb6fc8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,352 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloy-consensus" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6093bc69509849435a2d68237a2e9fea79d27390c8e62f1e4012c460aabad8" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "c-kzg", + "derive_more 2.0.1", + "either", + "k256", + "once_cell", + "rand 0.8.5", + "secp256k1", + "serde", + "serde_with", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1cfed4fefd13b5620cb81cdb6ba397866ff0de514c1b24806e6e79cdff5570" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b82752a889170df67bbb36d42ca63c531eb16274f0d7299ae2a680facba17bd" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4769c6ffddca380b0070d71c8b7f30bed375543fe76bb2f74ec0acf4b7cd16" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-eips" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5937e2d544e9b71000942d875cbc57965b32859a666ea543cc57aae5a06d602d" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "c-kzg", + "derive_more 2.0.1", + "either", + "serde", + "sha2", +] + +[[package]] +name = "alloy-json-abi" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459f98c6843f208856f338bfb25e65325467f7aff35dfeb0484d0a76e059134b" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793df1e3457573877fbde8872e4906638fde565ee2d3bd16d04aad17d43dbf0e" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cfebde8c581a5d37b678d0a48a32decb51efd7a63a08ce2517ddec26db705c8" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more 2.0.1", + "foldhash", + "hashbrown 0.15.3", + "indexmap 2.9.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.1", + "ruint", + "rustc-hash", + "serde", + "sha3", + "tiny-keccak", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +dependencies = [ + "alloy-rlp-derive", + "arrayvec 0.7.6", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "alloy-rpc-types" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d47b637369245d2dafef84b223b1ff5ea59e6cd3a98d2d3516e32788a0b216df" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-engine" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f9cbf5f781b9ee39cfdddea078fdef6015424f4c8282ef0e5416d15ca352c4" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more 2.0.1", + "jsonwebtoken", + "rand 0.8.5", + "serde", + "strum 0.27.1", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46586ec3c278639fc0e129f0eb73dbfa3d57f683c44b2ff5e066fab7ba63fa1f" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.12", +] + +[[package]] +name = "alloy-serde" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e1722bc30feef87cc0fa824e43c9013f9639cc6c037be7be28a31361c788be2" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedac07a10d4c2027817a43cc1f038313fc53c7ac866f7363239971fd01f9f18" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f9a598f010f048d8b8226492b6401104f5a5c1273c2869b72af29b48bb4ba9" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.9.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.101", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f494adf9d60e49aa6ce26dfd42c7417aa6d4343cf2ae621f20e4d92a5ad07d85" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.101", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52db32fbd35a9c0c0e538b58b81ebbae08a51be029e7ad60e08b60481c2ec6c3" +dependencies = [ + "serde", + "winnow", +] + +[[package]] +name = "alloy-sol-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a285b46e3e0c177887028278f04cc8262b76fd3b8e0e20e93cea0a58c35f5ac5" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-trie" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bada1fc392a33665de0dc50d401a3701b62583c655e3522a323490a5da016962" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "arrayvec 0.7.6", + "derive_more 2.0.1", + "nybbles", + "serde", + "smallvec", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f916ff6d52f219c44a9684aea764ce2c7e1d53bd4a724c9b127863aeacc30bb" +dependencies = [ + "alloy-primitives", + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -38,6 +384,130 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -49,6 +519,9 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] [[package]] name = "ascii-canvas" @@ -208,6 +681,22 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.9.1" @@ -235,6 +724,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd49896f12ac9b6dcd7a5998466b9b58263a695a3dd1ecc1aaca2e12a90b080" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -262,6 +763,21 @@ dependencies = [ "serde", ] +[[package]] +name = "c-kzg" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318cfa722931cb5fe0838b98d3ce5621e75f6a6408abc21721d80de9223f2e4" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + [[package]] name = "camino" version = "1.1.9" @@ -392,7 +908,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.26", "serde", "serde_json", ] @@ -424,7 +940,11 @@ version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ + "android-tzdata", + "iana-time-zone", "num-traits", + "serde", + "windows-link", ] [[package]] @@ -537,6 +1057,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -647,6 +1182,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -687,6 +1234,16 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.101", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", ] [[package]] @@ -733,6 +1290,12 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.19" @@ -760,9 +1323,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest", + "digest 0.10.7", "elliptic-curve", "rfc6979", + "serdect", "signature", "spki", ] @@ -772,6 +1336,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "elliptic-curve" @@ -781,13 +1348,14 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", "pkcs8", "rand_core 0.6.4", "sec1", + "serdect", "subtle", "zeroize", ] @@ -970,18 +1538,22 @@ dependencies = [ name = "evm_rpc_client" version = "1.4.0" dependencies = [ + "alloy-rpc-types", "async-trait", "candid", "evm_rpc_types", "ic-cdk", "ic-error-types", "serde", + "strum 0.27.1", ] [[package]] name = "evm_rpc_types" version = "1.4.0" dependencies = [ + "alloy-primitives", + "alloy-rpc-types", "candid", "canlog", "hex", @@ -1001,6 +1573,28 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec 0.7.6", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec 0.7.6", + "auto_impl", + "bytes", +] + [[package]] name = "ff" version = "0.13.1" @@ -1045,6 +1639,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1166,6 +1766,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "group" version = "0.13.0" @@ -1189,7 +1795,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -1212,11 +1818,21 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "foldhash", + "serde", +] [[package]] name = "heck" @@ -1224,6 +1840,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1233,13 +1855,22 @@ dependencies = [ "serde", ] +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec 0.7.6", +] + [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -1340,6 +1971,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ic-canister-log" version = "0.2.0" @@ -1692,6 +2347,17 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -1699,7 +2365,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.3", + "serde", ] [[package]] @@ -1717,6 +2384,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.11.0" @@ -1726,6 +2402,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1742,6 +2427,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.13.4" @@ -1752,6 +2452,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", + "serdect", "sha2", ] @@ -1759,9 +2460,19 @@ dependencies = [ name = "keccak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" dependencies = [ - "cpufeatures", + "digest 0.10.7", + "sha3-asm", ] [[package]] @@ -1773,7 +2484,7 @@ dependencies = [ "ascii-canvas", "bit-set 0.5.3", "ena", - "itertools", + "itertools 0.11.0", "lalrpop-util", "petgraph", "pico-args", @@ -1813,6 +2524,12 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.3" @@ -1889,6 +2606,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "maplit" version = "1.0.2" @@ -2035,6 +2763,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -2058,6 +2797,20 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "nybbles" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675b3a54e5b12af997abc8b6638b0aee51a28caedab70d4967e0d5db3a3f1d06" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + [[package]] name = "object" version = "0.36.7" @@ -2167,12 +2920,33 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -2180,7 +2954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.9.0", ] [[package]] @@ -2255,7 +3029,7 @@ dependencies = [ "ic-management-canister-types", "ic-transport-types", "reqwest", - "schemars", + "schemars 0.8.22", "serde", "serde_bytes", "serde_cbor", @@ -2337,6 +3111,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -2466,6 +3262,7 @@ dependencies = [ "libc", "rand_chacha 0.3.1", "rand_core 0.6.4", + "serde", ] [[package]] @@ -2476,6 +3273,7 @@ checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", + "serde", ] [[package]] @@ -2514,6 +3312,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.3", + "serde", ] [[package]] @@ -2545,6 +3344,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "regex" version = "1.11.1" @@ -2685,6 +3504,39 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ruint" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecb38f82477f20c5c3d62ef52d7c4e536e38ea9b73fb570a20c5cae0e14bcf6" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.5", + "rand 0.9.1", + "rlp", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2703,6 +3555,24 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.26", +] + [[package]] name = "rustix" version = "1.0.7" @@ -2850,6 +3720,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars_derive" version = "0.8.22" @@ -2878,10 +3772,32 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect", "subtle", "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.5", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -2905,6 +3821,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.26" @@ -2914,6 +3839,15 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.219" @@ -3011,6 +3945,48 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "schemars 0.9.0", + "schemars 1.0.4", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3019,7 +3995,7 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -3028,10 +4004,20 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] +[[package]] +name = "sha3-asm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +dependencies = [ + "cc", + "cfg-if", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3062,10 +4048,22 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -3095,6 +4093,9 @@ name = "smallvec" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -3231,6 +4232,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-solidity" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a985ff4ffd7373e10e0fb048110fb11a162e5a4c47f92ddb8787a6f766b769" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -3346,6 +4359,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "time" version = "0.3.41" @@ -3487,7 +4509,7 @@ version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ - "indexmap", + "indexmap 2.9.0", "toml_datetime", "winnow", ] @@ -3639,6 +4661,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uint" version = "0.9.5" @@ -3918,6 +4946,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "windows-link" version = "0.1.1" @@ -3931,7 +4994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result", - "windows-strings", + "windows-strings 0.3.1", "windows-targets 0.53.0", ] @@ -3953,6 +5016,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 953997b8..1f7a436d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,8 @@ proptest = { workspace = true } rand = "0.8" [workspace.dependencies] +alloy-primitives = "1.3.0" +alloy-rpc-types = "1.0.23" assert_matches = "1.5.0" async-trait = "0.1.88" candid = { version = "0.10.13" } diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 7dd1bdc1..2b55b58f 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -11,11 +11,13 @@ repository = "https://github.com/dfinity/evm-rpc-client" documentation = "https://docs.rs/evm_rpc_client" [dependencies] +alloy-rpc-types = { workspace = true } async-trait = { workspace = true } candid = { workspace = true } -evm_rpc_types = { path = "../evm_rpc_types" } +evm_rpc_types = { path = "../evm_rpc_types", features = ["alloy"] } ic-cdk = { workspace = true } ic-error-types = { workspace = true } serde = { workspace = true } +strum = { workspace = true } [dev-dependencies] \ No newline at end of file diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index a7475bdb..7738ca20 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -1,3 +1,8 @@ +//! TODO XC-412: Add documentation and examples + +mod request; + +use crate::request::Request; use async_trait::async_trait; use candid::utils::ArgumentEncoder; use candid::{CandidType, Principal}; @@ -194,6 +199,42 @@ impl EvmRpcClient { .await .unwrap() } + + async fn execute_request( + &self, + request: Request, + ) -> Output + where + Config: CandidType + Send, + Params: CandidType + Send, + CandidOutput: Into + CandidType + DeserializeOwned, + { + let rpc_method = request.endpoint.rpc_method(); + self.try_execute_request(request) + .await + .unwrap_or_else(|e| panic!("Client error: failed to call `{}`: {e:?}", rpc_method)) + } + + async fn try_execute_request( + &self, + request: Request, + ) -> Result + where + Config: CandidType + Send, + Params: CandidType + Send, + CandidOutput: Into + CandidType + DeserializeOwned, + { + self.config + .runtime + .update_call::<(RpcServices, Option, Params), CandidOutput>( + self.config.evm_rpc_canister, + request.endpoint.rpc_method(), + (request.rpc_services, request.rpc_config, request.params), + request.cycles, + ) + .await + .map(Into::into) + } } /// Runtime when interacting with a canister running on the Internet Computer. diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs new file mode 100644 index 00000000..cd63cc9e --- /dev/null +++ b/evm_rpc_client/src/request/mod.rs @@ -0,0 +1,351 @@ +use crate::{EvmRpcClient, Runtime}; +use candid::CandidType; +use evm_rpc_types::{ + BlockTag, GetLogsArgs, GetLogsRpcConfig, Hex20, Hex32, MultiRpcResult, RpcConfig, RpcResult, + RpcServices, +}; +use ic_error_types::RejectCode; +use serde::de::DeserializeOwned; +use std::fmt::{Debug, Formatter}; +use strum::EnumIter; + +pub type GetLogsRequestBuilder = RequestBuilder< + R, + GetLogsRpcConfig, + GetLogsArgs, + MultiRpcResult>, + MultiRpcResult>, +>; + +impl GetLogsRequestBuilder { + /// Change the `from_block` parameter for an `eth_getLogs` request. + pub fn with_from_block(mut self, from_block: BlockTag) -> Self { + self.request.params.from_block = Some(from_block); + self + } + + /// Change the `to_block` parameter for an `eth_getLogs` request. + pub fn with_to_block(mut self, to_block: BlockTag) -> Self { + self.request.params.to_block = Some(to_block); + self + } + + /// Change the `addresses` parameter for an `eth_getLogs` request. + pub fn with_addresses(mut self, addresses: Vec) -> Self { + self.request.params.addresses = addresses; + self + } + + /// Change the `topics` parameter for an `eth_getLogs` request. + pub fn with_topics(mut self, topics: Vec>) -> Self { + self.request.params.topics = Some(topics); + self + } +} + +/// Ethereum RPC endpoint supported by the EVM RPC canister. +pub trait EvmRpcRequest { + /// Type of RPC config for that request. + type Config; + /// The type of parameters taken by this endpoint. + type Params; + /// The Candid type returned when executing this request which is then converted to [`Self::Output`]. + type CandidOutput; + /// The type returned by this endpoint. + type Output; + + /// The name of the endpoint on the EVM RPC canister. + fn endpoint(&self) -> EvmRpcEndpoint; + + /// Return the request parameters. + fn params(self) -> Self::Params; +} + +/// Endpoint on the EVM RPC canister triggering a call to EVM providers. +#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, EnumIter)] +pub enum EvmRpcEndpoint { + /// `eth_getLogs` endpoint. + GetLogs, +} + +impl EvmRpcEndpoint { + /// Method name on the EVM RPC canister + pub fn rpc_method(&self) -> &'static str { + match &self { + Self::GetLogs => "eth_getLogs", + } + } +} + +/// A builder to construct a [`Request`]. +/// +/// To construct a [`RequestBuilder`], refer to the [`EvmRpcClient`] documentation. +#[must_use = "RequestBuilder does nothing until you 'send' it"] +pub struct RequestBuilder { + client: EvmRpcClient, + request: Request, +} + +impl Clone + for RequestBuilder +{ + fn clone(&self) -> Self { + Self { + client: self.client.clone(), + request: self.request.clone(), + } + } +} + +impl Debug + for RequestBuilder +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let RequestBuilder { client, request } = &self; + f.debug_struct("RequestBuilder") + .field("client", client) + .field("request", request) + .finish() + } +} + +impl + RequestBuilder +{ + pub(super) fn new( + client: EvmRpcClient, + rpc_request: RpcRequest, + cycles: u128, + ) -> Self + where + RpcRequest: EvmRpcRequest< + Config = Config, + Params = Params, + CandidOutput = CandidOutput, + Output = Output, + >, + Config: From, + { + let endpoint = rpc_request.endpoint(); + let params = rpc_request.params(); + let request = Request { + endpoint, + rpc_services: client.config.rpc_services.clone(), + rpc_config: client.config.rpc_config.clone().map(Config::from), + params, + cycles, + _candid_marker: Default::default(), + _output_marker: Default::default(), + }; + RequestBuilder:: { client, request } + } + + /// Query the cycles cost for that request + pub fn request_cost(self) -> RequestCostBuilder { + RequestCostBuilder { + client: self.client, + request: RequestCost { + endpoint: self.request.endpoint, + rpc_services: self.request.rpc_services, + rpc_config: self.request.rpc_config, + params: self.request.params, + cycles: 0, + _candid_marker: Default::default(), + _output_marker: Default::default(), + }, + } + } + + /// Change the amount of cycles to send for that request. + pub fn with_cycles(mut self, cycles: u128) -> Self { + *self.request.cycles_mut() = cycles; + self + } + + /// Change the parameters to send for that request. + pub fn with_params(mut self, params: impl Into) -> Self { + *self.request.params_mut() = params.into(); + self + } + + /// Modify current parameters to send for that request. + pub fn modify_params(mut self, mutator: F) -> Self + where + F: FnOnce(&mut Params), + { + mutator(self.request.params_mut()); + self + } + + /// Change the RPC configuration to use for that request. + pub fn with_rpc_config(mut self, rpc_config: impl Into) -> Self { + *self.request.rpc_config_mut() = Some(rpc_config.into()); + self + } +} + +impl + RequestBuilder +{ + /// Constructs the [`Request`] and sends it using the [`EvmRpcClient`] returning the response. + /// + /// # Panics + /// + /// If the request was not successful. + pub async fn send(self) -> Output + where + Config: CandidType + Send, + Params: CandidType + Send, + CandidOutput: Into + CandidType + DeserializeOwned, + { + self.client + .execute_request::(self.request) + .await + } + + /// 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 + where + Config: CandidType + Send, + Params: CandidType + Send, + CandidOutput: Into + CandidType + DeserializeOwned, + { + self.client + .try_execute_request::(self.request) + .await + } +} + +impl + RequestBuilder +{ + /// Change the max block range error for `eth_getLogs` request. + pub fn with_max_block_range(mut self, max_block_range: u32) -> Self { + let config = self.request.rpc_config_mut().get_or_insert_default(); + config.max_block_range = Some(max_block_range); + self + } +} + +/// A request which can be executed with `EvmRpcClient::execute_request` or `EvmRpcClient::execute_query_request`. +pub struct Request { + pub(super) endpoint: EvmRpcEndpoint, + pub(super) rpc_services: RpcServices, + pub(super) rpc_config: Option, + pub(super) params: Params, + pub(super) cycles: u128, + pub(super) _candid_marker: std::marker::PhantomData, + pub(super) _output_marker: std::marker::PhantomData, +} + +impl Debug + for Request +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let Request { + endpoint, + rpc_services, + rpc_config, + params, + cycles, + _candid_marker, + _output_marker, + } = &self; + f.debug_struct("Request") + .field("endpoint", endpoint) + .field("rpc_services", rpc_services) + .field("rpc_config", rpc_config) + .field("params", params) + .field("cycles", cycles) + .field("_candid_marker", _candid_marker) + .field("_output_marker", _output_marker) + .finish() + } +} + +impl PartialEq + for Request +{ + fn eq( + &self, + Request { + endpoint, + rpc_services, + rpc_config, + params, + cycles, + _candid_marker, + _output_marker, + }: &Self, + ) -> bool { + &self.endpoint == endpoint + && &self.rpc_services == rpc_services + && &self.rpc_config == rpc_config + && &self.params == params + && &self.cycles == cycles + && &self._candid_marker == _candid_marker + && &self._output_marker == _output_marker + } +} + +impl Clone + for Request +{ + fn clone(&self) -> Self { + Self { + endpoint: self.endpoint.clone(), + rpc_services: self.rpc_services.clone(), + rpc_config: self.rpc_config.clone(), + params: self.params.clone(), + cycles: self.cycles, + _candid_marker: self._candid_marker, + _output_marker: self._output_marker, + } + } +} + +impl Request { + /// Get a mutable reference to the cycles. + #[inline] + pub fn cycles_mut(&mut self) -> &mut u128 { + &mut self.cycles + } + + /// Get a mutable reference to the RPC configuration. + #[inline] + pub fn rpc_config_mut(&mut self) -> &mut Option { + &mut self.rpc_config + } + + /// Get a mutable reference to the request parameters. + #[inline] + pub fn params_mut(&mut self) -> &mut Params { + &mut self.params + } +} + +pub type RequestCost = Request, RpcResult>; + +#[must_use = "RequestCostBuilder does nothing until you 'send' it"] +pub struct RequestCostBuilder { + client: EvmRpcClient, + request: RequestCost, +} + +impl RequestCostBuilder { + /// Constructs the [`Request`] and send it using the [`EvmRpcClient`]. + pub async fn send(self) -> RpcResult + where + Config: CandidType + Send, + Params: CandidType + Send, + { + self.client.execute_cycles_cost_request(self.request).await + } +} + +fn set_default(default_value: Option, value: &mut Option) { + if default_value.is_some() && value.is_none() { + *value = Some(default_value.unwrap()) + } +} diff --git a/evm_rpc_types/Cargo.toml b/evm_rpc_types/Cargo.toml index 2cff3325..703efa91 100644 --- a/evm_rpc_types/Cargo.toml +++ b/evm_rpc_types/Cargo.toml @@ -11,6 +11,8 @@ repository = "https://github.com/dfinity/evm-rpc-canister" documentation = "https://docs.rs/evm_rpc_types" [dependencies] +alloy-primitives = { workspace = true, optional = true } +alloy-rpc-types = { workspace = true, optional = true } candid = { workspace = true } canlog = { workspace = true } hex = { workspace = true } @@ -23,4 +25,8 @@ thiserror = { workspace = true } url = { workspace = true } [dev-dependencies] -proptest = { workspace = true } \ No newline at end of file +proptest = { workspace = true } + +[features] +default = ["alloy"] +alloy = ["dep:alloy-primitives", "dep:alloy-rpc-types"] \ No newline at end of file diff --git a/evm_rpc_types/src/response/mod.rs b/evm_rpc_types/src/response/mod.rs index a9fb2991..f19adad6 100644 --- a/evm_rpc_types/src/response/mod.rs +++ b/evm_rpc_types/src/response/mod.rs @@ -68,6 +68,27 @@ pub struct LogEntry { pub removed: bool, } +#[cfg(feature = "alloy")] +impl TryFrom for alloy_rpc_types::Log { + type Error = String; + + fn try_from(entry: LogEntry) -> Result { + Ok(Self { + inner: alloy_primitives::Log { + address: entry.address, + data: alloy_primitives::LogData::new(entry.topics, entry.data).ok_or()?, + }, + block_hash: entry.block_hash, + block_number: entry.block_number, + block_timestamp: entry.block_timestamp, + transaction_hash: entry.transaction_hash, + transaction_index: entry.transaction_index, + log_index: entry.log_index, + removed: entry.removed, + }) + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CandidType)] pub struct TransactionReceipt { /// The hash of the block containing the transaction. diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index d42b1e51..26cf8c1a 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests; -use crate::RpcService; +use crate::{LogEntry, RpcService}; use candid::{CandidType, Deserialize}; use ic_error_types::RejectCode; use std::fmt::Debug; @@ -16,6 +16,8 @@ pub enum MultiRpcResult { } impl MultiRpcResult { + /// Maps a [`MultiRpcResult`] containing values of type `T` to a [`MultiRpcResult`] containing + /// values of type `R` by an infallible map. pub fn map(self, mut f: impl FnMut(T) -> R) -> MultiRpcResult { match self { MultiRpcResult::Consistent(result) => MultiRpcResult::Consistent(result.map(f)), @@ -35,6 +37,23 @@ impl MultiRpcResult { ), } } + + /// Maps a [`MultiRpcResult`] containing values of type `T` to a [`MultiRpcResult`] containing + /// values of type `R` by a fallible map. + pub fn and_then(self, f: F) -> MultiRpcResult + where + F: FnMut(T) -> RpcResult + Clone, + { + match self { + MultiRpcResult::Consistent(result) => MultiRpcResult::Consistent(result.and_then(f)), + MultiRpcResult::Inconsistent(results) => MultiRpcResult::Inconsistent( + results + .into_iter() + .map(|(source, result)| (source, result.and_then(f.clone()))) + .collect(), + ), + } + } } impl MultiRpcResult { @@ -184,3 +203,10 @@ impl From for LegacyRejectionCode { } } } + +#[cfg(feature = "alloy")] +impl From>> for MultiRpcResult> { + fn from(result: MultiRpcResult>) -> Self { + result.map(|logs| logs.into_iter().map(alloy_rpc_types::Log::from).collect()) + } +} diff --git a/evm_rpc_types/src/rpc_client/mod.rs b/evm_rpc_types/src/rpc_client/mod.rs index 6179638b..9ef34475 100644 --- a/evm_rpc_types/src/rpc_client/mod.rs +++ b/evm_rpc_types/src/rpc_client/mod.rs @@ -186,7 +186,7 @@ impl L2MainnetService { #[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, CandidType)] pub enum RpcService { - Provider(u64), + Provider(ProviderId), Custom(RpcApi), EthMainnet(EthMainnetService), EthSepolia(EthSepoliaService), @@ -209,10 +209,12 @@ impl Debug for RpcService { } } +pub type ProviderId = u64; + #[derive(Debug, Clone, PartialEq, Eq, CandidType, Deserialize, Serialize)] pub struct Provider { #[serde(rename = "providerId")] - pub provider_id: u64, + pub provider_id: ProviderId, #[serde(rename = "chainId")] pub chain_id: u64, pub access: RpcAccess, From 07c871279ee28b4d392a08060ed931d0a51e070d Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 6 Aug 2025 18:17:18 +0200 Subject: [PATCH 04/57] XC-412: Add empty changelog --- evm_rpc_client/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/evm_rpc_client/CHANGELOG.md b/evm_rpc_client/CHANGELOG.md index e69de29b..7b307443 100644 --- a/evm_rpc_client/CHANGELOG.md +++ b/evm_rpc_client/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] \ No newline at end of file From d5d34132232f7b9a9da9f983bc48896a414cdafd Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 7 Aug 2025 10:55:28 +0200 Subject: [PATCH 05/57] XC-412: Add more type conversions to `alloy` --- evm_rpc_types/src/lib.rs | 30 ++++++++++++++++++++++++++++++ evm_rpc_types/src/response/mod.rs | 30 ++++++++++++++++++++---------- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/evm_rpc_types/src/lib.rs b/evm_rpc_types/src/lib.rs index c9b0319d..17bda182 100644 --- a/evm_rpc_types/src/lib.rs +++ b/evm_rpc_types/src/lib.rs @@ -47,6 +47,15 @@ impl Debug for Nat256 { } } +impl TryFrom for u64 { + type Error = RpcError; + + fn try_from(value: Nat256) -> Result { + u64::try_from(value.0 .0) + .map_err(|e| RpcError::ValidationError(ValidationError::Custom(format!("{:?}", e)))) + } +} + impl Nat256 { pub const ZERO: Nat256 = Nat256(Nat(BigUint::ZERO)); @@ -203,6 +212,27 @@ impl_hex_string!(Hex32([u8; 32])); impl_hex_string!(Hex256([u8; 256])); impl_hex_string!(Hex(Vec)); +#[cfg(feature = "alloy")] +impl From for alloy_primitives::Address { + fn from(value: Hex20) -> Self { + Self::from(value.0) + } +} + +#[cfg(feature = "alloy")] +impl From for alloy_primitives::B256 { + fn from(value: Hex32) -> Self { + Self::from(value.0) + } +} + +#[cfg(feature = "alloy")] +impl From for alloy_primitives::Bytes { + fn from(value: Hex) -> Self { + Self::from_iter(value.0) + } +} + impl Hex20 { pub fn as_array(&self) -> &[u8; 20] { &self.0 diff --git a/evm_rpc_types/src/response/mod.rs b/evm_rpc_types/src/response/mod.rs index f19adad6..0545fe26 100644 --- a/evm_rpc_types/src/response/mod.rs +++ b/evm_rpc_types/src/response/mod.rs @@ -1,4 +1,4 @@ -use crate::{Hex, Hex20, Hex256, Hex32, HexByte, Nat256}; +use crate::{Hex, Hex20, Hex256, Hex32, HexByte, Nat256, RpcError, ValidationError}; use candid::CandidType; use serde::{Deserialize, Serialize}; @@ -70,20 +70,30 @@ pub struct LogEntry { #[cfg(feature = "alloy")] impl TryFrom for alloy_rpc_types::Log { - type Error = String; + type Error = RpcError; fn try_from(entry: LogEntry) -> Result { Ok(Self { inner: alloy_primitives::Log { - address: entry.address, - data: alloy_primitives::LogData::new(entry.topics, entry.data).ok_or()?, + address: alloy_primitives::Address::from(entry.address), + data: alloy_primitives::LogData::new( + entry + .topics + .into_iter() + .map(|topic| alloy_primitives::B256::from(topic)) + .collect(), + alloy_primitives::Bytes::from(entry.data), + ) + .ok_or(RpcError::ValidationError(ValidationError::Custom( + "Invalid log data".to_string(), + )))?, }, - block_hash: entry.block_hash, - block_number: entry.block_number, - block_timestamp: entry.block_timestamp, - transaction_hash: entry.transaction_hash, - transaction_index: entry.transaction_index, - log_index: entry.log_index, + block_hash: entry.block_hash.map(alloy_primitives::BlockHash::from), + block_number: entry.block_number.map(u64::try_from).transpose()?, + block_timestamp: None, + transaction_hash: entry.transaction_hash.map(alloy_primitives::TxHash::from), + transaction_index: entry.transaction_index.map(u64::try_from).transpose()?, + log_index: entry.log_index.map(u64::try_from).transpose()?, removed: entry.removed, }) } From 99d82b29458d5a6ae73af8f8939f6caf1f66672c Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 7 Aug 2025 11:00:21 +0200 Subject: [PATCH 06/57] XC-412: Use `try_from` instead of `from` --- evm_rpc_types/src/response/mod.rs | 2 +- evm_rpc_types/src/result/mod.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/evm_rpc_types/src/response/mod.rs b/evm_rpc_types/src/response/mod.rs index 0545fe26..968d0ee9 100644 --- a/evm_rpc_types/src/response/mod.rs +++ b/evm_rpc_types/src/response/mod.rs @@ -80,7 +80,7 @@ impl TryFrom for alloy_rpc_types::Log { entry .topics .into_iter() - .map(|topic| alloy_primitives::B256::from(topic)) + .map(alloy_primitives::B256::from) .collect(), alloy_primitives::Bytes::from(entry.data), ) diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index 26cf8c1a..b2f9a82d 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -207,6 +207,10 @@ impl From for LegacyRejectionCode { #[cfg(feature = "alloy")] impl From>> for MultiRpcResult> { fn from(result: MultiRpcResult>) -> Self { - result.map(|logs| logs.into_iter().map(alloy_rpc_types::Log::from).collect()) + result.and_then(|logs| { + logs.into_iter() + .map(alloy_rpc_types::Log::try_from) + .collect() + }) } } From 34c70b999c5e2e9181cda538dd41c568e00b9ab4 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 7 Aug 2025 11:15:59 +0200 Subject: [PATCH 07/57] XC-412: Add `get_logs` to client --- evm_rpc_client/src/lib.rs | 17 +++++++- evm_rpc_client/src/request/mod.rs | 68 +++++++++++------------------ evm_rpc_types/src/rpc_client/mod.rs | 10 +++++ 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 7738ca20..ff08c271 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -2,13 +2,14 @@ mod request; -use crate::request::Request; +use crate::request::{Request, RequestBuilder}; use async_trait::async_trait; use candid::utils::ArgumentEncoder; use candid::{CandidType, Principal}; -use evm_rpc_types::{ConsensusStrategy, RpcConfig, RpcServices}; +use evm_rpc_types::{ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; use ic_cdk::api::call::RejectionCode as IcCdkRejectionCode; use ic_error_types::RejectCode; +pub use request::{GetLogsRequest, GetLogsRequestBuilder}; use serde::de::DeserializeOwned; use std::sync::Arc; @@ -165,6 +166,18 @@ impl ClientBuilder { } } +impl EvmRpcClient { + /// Call `get_ethLogs` on the EVM RPC canister. + /// TODO XC-412: Add docs and examples + pub fn get_logs(&self, params: impl Into) -> GetLogsRequestBuilder { + RequestBuilder::new( + self.clone(), + GetLogsRequest::new(params.into()), + 10_000_000_000, + ) + } +} + impl EvmRpcClient { /// Call `getProviders` on the EVM RPC canister. pub async fn get_providers(&self) -> Vec { diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index cd63cc9e..47c36a1f 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -1,14 +1,37 @@ use crate::{EvmRpcClient, Runtime}; use candid::CandidType; use evm_rpc_types::{ - BlockTag, GetLogsArgs, GetLogsRpcConfig, Hex20, Hex32, MultiRpcResult, RpcConfig, RpcResult, - RpcServices, + BlockTag, GetLogsArgs, GetLogsRpcConfig, Hex20, Hex32, MultiRpcResult, RpcConfig, RpcServices, }; use ic_error_types::RejectCode; use serde::de::DeserializeOwned; use std::fmt::{Debug, Formatter}; use strum::EnumIter; +#[derive(Debug, Clone)] +pub struct GetLogsRequest(GetLogsArgs); + +impl GetLogsRequest { + pub fn new(params: GetLogsArgs) -> Self { + Self(params) + } +} + +impl EvmRpcRequest for GetLogsRequest { + type Config = GetLogsRpcConfig; + type Params = GetLogsArgs; + type CandidOutput = MultiRpcResult>; + type Output = MultiRpcResult>; + + fn endpoint(&self) -> EvmRpcEndpoint { + EvmRpcEndpoint::GetLogs + } + + fn params(self) -> Self::Params { + self.0 + } +} + pub type GetLogsRequestBuilder = RequestBuilder< R, GetLogsRpcConfig, @@ -140,22 +163,6 @@ impl RequestBuilder:: { client, request } } - /// Query the cycles cost for that request - pub fn request_cost(self) -> RequestCostBuilder { - RequestCostBuilder { - client: self.client, - request: RequestCost { - endpoint: self.request.endpoint, - rpc_services: self.request.rpc_services, - rpc_config: self.request.rpc_config, - params: self.request.params, - cycles: 0, - _candid_marker: Default::default(), - _output_marker: Default::default(), - }, - } - } - /// Change the amount of cycles to send for that request. pub fn with_cycles(mut self, cycles: u128) -> Self { *self.request.cycles_mut() = cycles; @@ -324,28 +331,3 @@ impl Request = Request, RpcResult>; - -#[must_use = "RequestCostBuilder does nothing until you 'send' it"] -pub struct RequestCostBuilder { - client: EvmRpcClient, - request: RequestCost, -} - -impl RequestCostBuilder { - /// Constructs the [`Request`] and send it using the [`EvmRpcClient`]. - pub async fn send(self) -> RpcResult - where - Config: CandidType + Send, - Params: CandidType + Send, - { - self.client.execute_cycles_cost_request(self.request).await - } -} - -fn set_default(default_value: Option, value: &mut Option) { - if default_value.is_some() && value.is_none() { - *value = Some(default_value.unwrap()) - } -} diff --git a/evm_rpc_types/src/rpc_client/mod.rs b/evm_rpc_types/src/rpc_client/mod.rs index 9ef34475..a35ceeef 100644 --- a/evm_rpc_types/src/rpc_client/mod.rs +++ b/evm_rpc_types/src/rpc_client/mod.rs @@ -37,6 +37,16 @@ impl From for RpcConfig { } } +impl From for GetLogsRpcConfig { + fn from(config: RpcConfig) -> Self { + Self { + response_size_estimate: config.response_size_estimate, + response_consensus: config.response_consensus, + max_block_range: None, + } + } +} + impl GetLogsRpcConfig { pub fn max_block_range_or_default(&self) -> u32 { const DEFAULT_ETH_GET_LOGS_MAX_BLOCK_RANGE: u32 = 500; From fb4a545d04b5b53645cb64bc2406295e156c8acc Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 7 Aug 2025 11:16:47 +0200 Subject: [PATCH 08/57] XC-412: Don't expose private types --- evm_rpc_client/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index ff08c271..63ca72fa 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -9,7 +9,7 @@ use candid::{CandidType, Principal}; use evm_rpc_types::{ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; use ic_cdk::api::call::RejectionCode as IcCdkRejectionCode; use ic_error_types::RejectCode; -pub use request::{GetLogsRequest, GetLogsRequestBuilder}; +use request::{GetLogsRequest, GetLogsRequestBuilder}; use serde::de::DeserializeOwned; use std::sync::Arc; From 041a47bdaa5849e746438bcd7d4f54f5ca810b36 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 7 Aug 2025 14:23:25 +0200 Subject: [PATCH 09/57] XC-412: Remove new `ProviderId` type --- evm_rpc_types/src/rpc_client/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/evm_rpc_types/src/rpc_client/mod.rs b/evm_rpc_types/src/rpc_client/mod.rs index a35ceeef..747a879a 100644 --- a/evm_rpc_types/src/rpc_client/mod.rs +++ b/evm_rpc_types/src/rpc_client/mod.rs @@ -196,7 +196,7 @@ impl L2MainnetService { #[derive(Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize, CandidType)] pub enum RpcService { - Provider(ProviderId), + Provider(u64), Custom(RpcApi), EthMainnet(EthMainnetService), EthSepolia(EthSepoliaService), @@ -219,12 +219,10 @@ impl Debug for RpcService { } } -pub type ProviderId = u64; - #[derive(Debug, Clone, PartialEq, Eq, CandidType, Deserialize, Serialize)] pub struct Provider { #[serde(rename = "providerId")] - pub provider_id: ProviderId, + pub provider_id: u64, #[serde(rename = "chainId")] pub chain_id: u64, pub access: RpcAccess, From 33e50dfeb48e52947002cd537d1e97f32cd7b24e Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 7 Aug 2025 14:31:02 +0200 Subject: [PATCH 10/57] XC-412: Refactor `and_then` --- evm_rpc_types/src/result/mod.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index b2f9a82d..07ab8ed9 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -40,16 +40,21 @@ impl MultiRpcResult { /// Maps a [`MultiRpcResult`] containing values of type `T` to a [`MultiRpcResult`] containing /// values of type `R` by a fallible map. - pub fn and_then(self, f: F) -> MultiRpcResult - where - F: FnMut(T) -> RpcResult + Clone, - { + pub fn and_then(self, f: impl FnMut(T) -> RpcResult) -> MultiRpcResult { match self { MultiRpcResult::Consistent(result) => MultiRpcResult::Consistent(result.and_then(f)), MultiRpcResult::Inconsistent(results) => MultiRpcResult::Inconsistent( results .into_iter() - .map(|(source, result)| (source, result.and_then(f.clone()))) + .map(|(service, result)| { + ( + service, + match result { + Ok(ok) => f(ok), + Err(err) => Err(err), + }, + ) + }) .collect(), ), } From 4619b97ac820235ebe10037656ad5ac659a65eb7 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 8 Aug 2025 17:55:55 +0200 Subject: [PATCH 11/57] XC-412: Make `f` mutable --- evm_rpc_types/src/result/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index 07ab8ed9..f499c5a3 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -40,7 +40,7 @@ impl MultiRpcResult { /// Maps a [`MultiRpcResult`] containing values of type `T` to a [`MultiRpcResult`] containing /// values of type `R` by a fallible map. - pub fn and_then(self, f: impl FnMut(T) -> RpcResult) -> MultiRpcResult { + pub fn and_then(self, mut f: impl FnMut(T) -> RpcResult) -> MultiRpcResult { match self { MultiRpcResult::Consistent(result) => MultiRpcResult::Consistent(result.and_then(f)), MultiRpcResult::Inconsistent(results) => MultiRpcResult::Inconsistent( From e8e4eb249b15ba73f6629d7bcb77c7e819772e67 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 8 Aug 2025 17:53:08 +0200 Subject: [PATCH 12/57] XC-412: Basic `PocketIcRuntime` --- Cargo.lock | 8 + Cargo.toml | 10 +- evm_rpc_client/Cargo.toml | 9 +- evm_rpc_client/src/lib.rs | 79 +----- evm_rpc_client/src/runtime/mod.rs | 83 ++++++ evm_rpc_client/src/runtime/pocket_ic/mock.rs | 257 ++++++++++++++++++ evm_rpc_client/src/runtime/pocket_ic/mod.rs | 196 ++++++++++++++ tests/tests.rs | 263 +++++++++++++++---- 8 files changed, 779 insertions(+), 126 deletions(-) create mode 100644 evm_rpc_client/src/runtime/mod.rs create mode 100644 evm_rpc_client/src/runtime/pocket_ic/mock.rs create mode 100644 evm_rpc_client/src/runtime/pocket_ic/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8cb6fc8a..04d1dfbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1494,6 +1494,8 @@ dependencies = [ name = "evm_rpc" version = "2.4.0" dependencies = [ + "alloy-primitives", + "alloy-rpc-types", "assert_matches", "candid", "candid_parser", @@ -1502,6 +1504,7 @@ dependencies = [ "derive_more 2.0.1", "ethers-core", "ethnum", + "evm_rpc_client", "evm_rpc_types", "getrandom 0.2.16", "hex", @@ -1528,6 +1531,7 @@ dependencies = [ "sha2", "thiserror 2.0.12", "thousands", + "tokio", "tower", "tower-http", "url", @@ -1541,11 +1545,15 @@ dependencies = [ "alloy-rpc-types", "async-trait", "candid", + "canhttp", "evm_rpc_types", "ic-cdk", "ic-error-types", + "pocket-ic", "serde", + "serde_json", "strum 0.27.1", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1f7a436d..663ebfa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ inherits = "release" [dependencies] candid = { workspace = true } canlog = { workspace = true } -canhttp = { version = "0.2.0", features = ["json", "multi"] } +canhttp = { workspace = true } derive_more = { workspace = true } ethnum = { workspace = true } evm_rpc_types = { path = "evm_rpc_types" } @@ -50,15 +50,19 @@ zeroize = { version = "1.8", features = ["zeroize_derive"] } regex = "1.11" [dev-dependencies] +alloy-primitives = { workspace = true } +alloy-rpc-types = { workspace = true } assert_matches = "1.5" candid_parser = { workspace = true } +evm_rpc_client = { path = "evm_rpc_client", features = ["pocket-ic"] } ic-crypto-test-utils-reproducible-rng = { git = "https://github.com/dfinity/ic", rev = "release-2024-09-26_01-31-base" } ic-management-canister-types = { workspace = true } ic-test-utilities-load-wasm = { git = "https://github.com/dfinity/ic", rev = "release-2024-09-26_01-31-base" } maplit = "1" -pocket-ic = "9.0.0" +pocket-ic = { workspace = true } proptest = { workspace = true } rand = "0.8" +tokio = "1.44.1" [workspace.dependencies] alloy-primitives = "1.3.0" @@ -66,6 +70,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"] } canlog = { version = "0.1.1", features = ["derive"] } candid_parser = { version = "0.1.4" } ciborium = "0.2.2" @@ -92,6 +97,7 @@ minicbor = { version = "1.0.0", features = ["alloc", "derive"] } num-bigint = "0.4.6" num-traits = "0.2.19" pin-project = "1.1.10" +pocket-ic = "9.0.0" proptest = "1.6.0" serde = "1.0" serde_json = "1.0" diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 2b55b58f..2c800e34 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -14,10 +14,17 @@ documentation = "https://docs.rs/evm_rpc_client" alloy-rpc-types = { workspace = true } async-trait = { workspace = true } candid = { workspace = true } +canhttp = { workspace = true, optional = true } evm_rpc_types = { path = "../evm_rpc_types", features = ["alloy"] } ic-cdk = { workspace = true } ic-error-types = { workspace = true } +pocket-ic = { workspace = true, optional = true } serde = { workspace = true } +serde_json = { workspace = true, optional = true } strum = { workspace = true } +url = { workspace = true } -[dev-dependencies] \ No newline at end of file +[dev-dependencies] + +[features] +pocket-ic = ["dep:canhttp", "dep:pocket-ic", "dep:serde_json"] \ No newline at end of file diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 63ca72fa..33bc9412 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -1,15 +1,20 @@ //! TODO XC-412: Add documentation and examples mod request; +mod runtime; use crate::request::{Request, RequestBuilder}; -use async_trait::async_trait; -use candid::utils::ArgumentEncoder; use candid::{CandidType, Principal}; use evm_rpc_types::{ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; use ic_cdk::api::call::RejectionCode as IcCdkRejectionCode; use ic_error_types::RejectCode; use request::{GetLogsRequest, GetLogsRequestBuilder}; +#[cfg(feature = "pocket-ic")] +pub use runtime::{ + forever, once, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, + MockOutcallRepeat, PocketIcRuntime, RepeatExt, +}; +pub use runtime::{IcRuntime, Runtime}; use serde::de::DeserializeOwned; use std::sync::Arc; @@ -23,36 +28,6 @@ use std::sync::Arc; /// ``` pub const EVM_RPC_CANISTER: Principal = Principal::from_slice(&[0, 0, 0, 0, 2, 48, 0, 204, 1, 1]); -/// Abstract the canister runtime so that the client code can be reused: -/// * in production using `ic_cdk`, -/// * in unit tests by mocking this trait, -/// * in integration tests by implementing this trait for `PocketIc`. -#[async_trait] -pub trait Runtime { - /// Defines how asynchronous inter-canister update calls are made. - async fn update_call( - &self, - id: Principal, - method: &str, - args: In, - cycles: u128, - ) -> Result - where - In: ArgumentEncoder + Send, - Out: CandidType + DeserializeOwned; - - /// Defines how asynchronous inter-canister query calls are made. - async fn query_call( - &self, - id: Principal, - method: &str, - args: In, - ) -> Result - where - In: ArgumentEncoder + Send, - Out: CandidType + DeserializeOwned; -} - /// Client to interact with the EVM RPC canister. #[derive(Debug)] pub struct EvmRpcClient { @@ -250,46 +225,6 @@ impl EvmRpcClient { } } -/// Runtime when interacting with a canister running on the Internet Computer. -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub struct IcRuntime; - -#[async_trait] -impl Runtime for IcRuntime { - async fn update_call( - &self, - id: Principal, - method: &str, - args: In, - cycles: u128, - ) -> Result - where - In: ArgumentEncoder + Send, - Out: CandidType + DeserializeOwned, - { - ic_cdk::api::call::call_with_payment128(id, method, args, cycles) - .await - .map(|(res,)| res) - .map_err(|(code, message)| (convert_reject_code(code), message)) - } - - 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) - .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, diff --git a/evm_rpc_client/src/runtime/mod.rs b/evm_rpc_client/src/runtime/mod.rs new file mode 100644 index 00000000..fc398524 --- /dev/null +++ b/evm_rpc_client/src/runtime/mod.rs @@ -0,0 +1,83 @@ +#[cfg(feature = "pocket-ic")] +mod pocket_ic; + +use async_trait::async_trait; +use candid::utils::ArgumentEncoder; +use candid::{CandidType, Principal}; +use ic_error_types::RejectCode; +#[cfg(feature = "pocket-ic")] +pub use pocket_ic::{ + forever, once, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, + MockOutcallRepeat, PocketIcRuntime, RepeatExt, +}; +use serde::de::DeserializeOwned; + +/// Abstract the canister runtime so that the client code can be reused: +/// * in production using `ic_cdk`, +/// * in unit tests by mocking this trait, +/// * in integration tests by implementing this trait for `PocketIc`. +#[async_trait] +pub trait Runtime { + /// Defines how asynchronous inter-canister update calls are made. + async fn update_call( + &self, + id: Principal, + method: &str, + args: In, + cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned; + + /// Defines how asynchronous inter-canister query calls are made. + async fn query_call( + &self, + id: Principal, + method: &str, + args: In, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned; +} + +/// Runtime when interacting with a canister running on the Internet Computer. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct IcRuntime; + +#[async_trait] +impl Runtime for IcRuntime { + async fn update_call( + &self, + id: Principal, + method: &str, + args: In, + cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + ic_cdk::api::call::call_with_payment128(id, method, args, cycles) + .await + .map(|(res,)| res) + .map_err(|(code, message)| (crate::convert_reject_code(code), message)) + } + + 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) + .await + .map(|(res,)| res) + .map_err(|(code, message)| (crate::convert_reject_code(code), message)) + } +} diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs new file mode 100644 index 00000000..8ff422dd --- /dev/null +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -0,0 +1,257 @@ +use canhttp::http::json::JsonRpcRequest; +use ic_cdk::api::call::RejectionCode; +use pocket_ic::common::rest::{ + CanisterHttpHeader, CanisterHttpMethod, CanisterHttpReject, CanisterHttpReply, + CanisterHttpRequest, CanisterHttpResponse, +}; +use serde_json::Value; +use std::collections::{BTreeSet, VecDeque}; +use std::str::FromStr; +use url::{Host, Url}; + +#[derive(Clone, Default)] +pub struct MockOutcallQueue(VecDeque>); + +trait CloneableMockOutcallIterator: Iterator + Send { + fn clone_box(&self) -> Box; +} + +impl CloneableMockOutcallIterator for T +where + T: Iterator + Clone + Send + 'static, +{ + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Clone for Box { + fn clone(&self) -> Box { + self.clone_box() + } +} + +impl MockOutcallQueue { + pub fn push(&mut self, outcall: impl Into, repeat: MockOutcallRepeat) { + self.0.push_back(match repeat { + MockOutcallRepeat::Once => Box::new(std::iter::once(outcall.into())), + MockOutcallRepeat::Times(n) => Box::new(std::iter::repeat(outcall.into()).take(n)), + MockOutcallRepeat::Forever => Box::new(std::iter::repeat(outcall.into())), + }) + } +} + +impl Iterator for MockOutcallQueue { + type Item = MockOutcall; + + fn next(&mut self) -> Option { + while let Some(iter) = self.0.front_mut() { + match iter.next() { + Some(item) => return Some(item), + None => { + self.0.pop_front(); + } + } + } + None + } +} + +#[derive(Clone, Default)] +pub enum MockOutcallRepeat { + #[default] + Once, + Times(usize), + Forever, +} + +pub fn once() -> MockOutcallRepeat { + MockOutcallRepeat::Once +} + +pub fn forever() -> MockOutcallRepeat { + MockOutcallRepeat::Forever +} + +pub trait RepeatExt { + fn times(self) -> MockOutcallRepeat; +} + +impl RepeatExt for usize { + fn times(self) -> MockOutcallRepeat { + assert!(self > 1, "Repeat count must be greater than 1"); + MockOutcallRepeat::Times(self) + } +} + +pub struct MockOutcallBody(pub Vec); + +impl From<&Value> for MockOutcallBody { + fn from(value: &Value) -> Self { + value.to_string().into() + } +} + +impl From for MockOutcallBody { + fn from(value: Value) -> Self { + Self::from(serde_json::to_vec(&value).unwrap()) + } +} + +impl From for MockOutcallBody { + fn from(string: String) -> Self { + string.as_bytes().to_vec().into() + } +} + +impl<'a> From<&'a str> for MockOutcallBody { + fn from(string: &'a str) -> Self { + string.to_string().into() + } +} + +impl From> for MockOutcallBody { + fn from(bytes: Vec) -> Self { + MockOutcallBody(bytes) + } +} + +#[derive(Clone, Debug)] +pub struct MockOutcallBuilder(MockOutcall); + +impl MockOutcallBuilder { + pub fn new(status: u16, bodies: impl IntoIterator>) -> Self { + Self(MockOutcall { + method: None, + url: None, + host: None, + request_headers: None, + request_body: None, + max_response_bytes: None, + responses: bodies + .into_iter() + .map(|body| { + CanisterHttpResponse::CanisterHttpReply(CanisterHttpReply { + status, + headers: vec![], + body: body.into().0, + }) + }) + .collect(), + }) + } + + pub fn new_error(code: RejectionCode, num_providers: usize, message: impl ToString) -> Self { + Self(MockOutcall { + method: None, + url: None, + host: None, + request_headers: None, + request_body: None, + max_response_bytes: None, + responses: vec![ + CanisterHttpResponse::CanisterHttpReject(CanisterHttpReject { + reject_code: code as u64, + message: message.to_string(), + }); + num_providers + ], + }) + } + + pub fn with_method(mut self, method: CanisterHttpMethod) -> Self { + self.0.method = Some(method); + self + } + + pub fn with_url(mut self, url: impl ToString) -> Self { + self.0.url = Some(url.to_string()); + self + } + + pub fn with_host(mut self, host: &str) -> Self { + self.0.host = Some(Host::parse(host).expect("BUG: invalid host for a URL")); + self + } + + pub fn with_request_headers(mut self, headers: Vec<(impl ToString, impl ToString)>) -> Self { + self.0.request_headers = Some( + headers + .into_iter() + .map(|(name, value)| CanisterHttpHeader { + name: name.to_string(), + value: value.to_string(), + }) + .collect(), + ); + self + } + + pub fn with_raw_request_body(self, body: &str) -> Self { + self.with_request_body(serde_json::from_str(body).unwrap()) + } + + pub fn with_request_body(mut self, body: Value) -> Self { + self.0.request_body = Some(serde_json::from_value(body).unwrap()); + self + } + + pub fn with_max_response_bytes(mut self, max_response_bytes: u64) -> Self { + self.0.max_response_bytes = Some(max_response_bytes); + self + } + + pub fn build(self) -> MockOutcall { + self.0 + } +} + +impl From for MockOutcall { + fn from(builder: MockOutcallBuilder) -> Self { + builder.build() + } +} + +#[derive(Clone, Debug)] +pub struct MockOutcall { + pub method: Option, + pub url: Option, + pub host: Option, + pub request_headers: Option>, + pub request_body: Option>, + pub max_response_bytes: Option, + pub responses: Vec, +} + +impl MockOutcall { + pub fn assert_matches(&self, request: &CanisterHttpRequest) { + let req_url = Url::from_str(&request.url).expect("BUG: invalid URL"); + if let Some(ref url) = self.url { + let mock_url = Url::from_str(url).unwrap(); + assert_eq!(mock_url, req_url); + } + if let Some(ref host) = self.host { + assert_eq!( + host, + &req_url.host().expect("BUG: missing host in URL").to_owned() + ); + } + if let Some(ref method) = self.method { + assert_eq!(method, &request.http_method); + } + if let Some(ref headers) = self.request_headers { + assert_eq!( + headers.iter().collect::>(), + request.headers.iter().collect::>() + ); + } + if let Some(ref expected_body) = self.request_body { + let actual_body: JsonRpcRequest = serde_json::from_slice(&request.body) + .expect("BUG: failed to parse JSON request body"); + assert_eq!(expected_body, &actual_body); + } + if let Some(max_response_bytes) = self.max_response_bytes { + assert_eq!(Some(max_response_bytes), request.max_response_bytes); + } + } +} diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs new file mode 100644 index 00000000..3435ceff --- /dev/null +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -0,0 +1,196 @@ +mod mock; + +use crate::{ClientBuilder, Runtime}; +use async_trait::async_trait; +use candid::{decode_args, utils::ArgumentEncoder, CandidType, Principal}; +use ic_error_types::RejectCode; +pub use mock::{ + forever, once, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, + MockOutcallRepeat, RepeatExt, +}; +use pocket_ic::common::rest::{ + CanisterHttpReject, CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse, +}; +use pocket_ic::nonblocking::PocketIc; +use pocket_ic::RejectResponse; +use serde::de::DeserializeOwned; +use std::sync::Mutex; +use std::time::Duration; + +const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; +const MAX_TICKS: usize = 10; + +pub struct PocketIcRuntime<'a> { + pub env: &'a PocketIc, + pub caller: Principal, + // This field is in a `RefCell` so we can use interior mutability to pop the next element from + // the queue (i.e., perform a mutable operation) within the `Runtime::update_call` method which + // takes an immutable reference of `self`. + pub mocks: Mutex, + pub controller: Principal, +} + +impl Clone for PocketIcRuntime<'_> { + fn clone(&self) -> Self { + Self { + env: self.env, + caller: self.caller.clone(), + mocks: Mutex::new(self.mocks.lock().unwrap().clone()), + controller: self.controller.clone(), + } + } +} + +#[async_trait] +impl Runtime for PocketIcRuntime<'_> { + async fn update_call( + &self, + id: Principal, + method: &str, + args: In, + _cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + // Forward the call through the wallet canister to attach cycles + let message_id = self + .env + .submit_call(id, self.caller, method, encode_args(args)) + .await + .unwrap(); + self.execute_mock().await; + self.env + .await_call(message_id) + .await + .map(decode_call_response) + .map_err(parse_reject_response)? + } + + async fn query_call( + &self, + id: Principal, + method: &str, + args: In, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + self.env + .query_call(id, self.caller, method, encode_args(args)) + .await + .map(decode_call_response) + .map_err(parse_reject_response)? + } +} + +impl PocketIcRuntime<'_> { + async fn execute_mock(&self) { + let maybe_mock = { + let mut mocks = self.mocks.lock().unwrap(); + mocks.next() + }; + if let Some(mock) = maybe_mock { + if !self.try_mock_http_inner(mock).await { + panic!("no pending HTTP request") + } + } + } + + async fn try_mock_http_inner(&self, mock: MockOutcall) -> bool { + let http_requests = tick_until_http_request(self.env).await; + let request = match http_requests.first() { + Some(request) => request, + None => return false, + }; + mock.assert_matches(request); + + for response in mock.responses.clone() { + let mock_response = MockCanisterHttpResponse { + subnet_id: request.subnet_id, + request_id: request.request_id, + response: check_response_size(request, response), + additional_responses: vec![], + }; + self.env.mock_canister_http_response(mock_response).await; + } + + true + } +} + +fn check_response_size( + request: &CanisterHttpRequest, + response: CanisterHttpResponse, +) -> CanisterHttpResponse { + if let CanisterHttpResponse::CanisterHttpReply(reply) = &response { + let max_response_bytes = request + .max_response_bytes + .unwrap_or(DEFAULT_MAX_RESPONSE_BYTES); + if reply.body.len() as u64 > max_response_bytes { + // Approximate replica behavior since headers are not accounted for. + return CanisterHttpResponse::CanisterHttpReject(CanisterHttpReject { + reject_code: RejectCode::SysFatal as u64, + message: format!("Http body exceeds size limit of {max_response_bytes} bytes.",), + }); + } + } + 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) +} + +pub fn encode_args(args: In) -> Vec { + candid::encode_args(args).expect("Failed to encode arguments.") +} + +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 + ), + ) + }) +} + +async fn tick_until_http_request(env: &PocketIc) -> Vec { + let mut requests = Vec::new(); + for _ in 0..MAX_TICKS { + requests = env.get_canister_http().await; + if !requests.is_empty() { + break; + } + env.tick().await; + env.advance_time(Duration::from_nanos(1)).await; + } + requests +} + +impl ClientBuilder> { + pub fn mock(self, outcall: impl Into, repeat: MockOutcallRepeat) -> Self { + self.with_runtime(|r| { + r.mocks.lock().unwrap().push(outcall, repeat); + r + }) + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 87a88330..7aa4462a 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,6 +1,7 @@ mod mock; use crate::mock::MockJsonRequestBody; +use alloy_primitives::{address, b256, bytes}; use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat, Principal}; use canlog::{Log, LogEntry}; @@ -11,6 +12,9 @@ use evm_rpc::{ providers::PROVIDERS, types::{Metrics, ProviderId, RpcAccess, RpcMethod}, }; +use evm_rpc_client::{ + ClientBuilder, EvmRpcClient, MockOutcallQueue, MockOutcallRepeat, PocketIcRuntime, +}; use evm_rpc_types::{ BlockTag, ConsensusStrategy, EthMainnetService, EthSepoliaService, GetLogsRpcConfig, Hex, Hex20, Hex32, HttpOutcallError, InstallArgs, JsonRpcError, LegacyRejectionCode, MultiRpcResult, @@ -29,10 +33,10 @@ use pocket_ic::common::rest::{ CanisterHttpMethod, CanisterHttpReject, CanisterHttpResponse, MockCanisterHttpResponse, RawMessageId, }; -use pocket_ic::{ErrorCode, PocketIc, PocketIcBuilder, RejectResponse}; +use pocket_ic::{nonblocking, ErrorCode, PocketIc, PocketIcBuilder, RejectResponse}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::json; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::{marker::PhantomData, mem, str::FromStr, time::Duration}; const DEFAULT_CALLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]); @@ -76,6 +80,149 @@ fn assert_reply(result: Result, RejectResponse>) -> Vec { result.unwrap_or_else(|e| panic!("Expected a successful reply, got error {e}")) } +#[derive(Clone)] +pub struct EvmRpcNonblockingSetup { + pub env: Arc, + pub caller: Principal, + pub controller: Principal, + pub canister_id: CanisterId, +} + +impl EvmRpcNonblockingSetup { + pub async fn new() -> Self { + Self::with_args(InstallArgs { + demo: Some(true), + ..Default::default() + }) + .await + } + + pub async fn with_args(args: InstallArgs) -> Self { + // The `with_fiduciary_subnet` setup below requires that `nodes_in_subnet` + // setting (part of InstallArgs) to be set appropriately. Otherwise + // http outcall will fail due to insufficient cycles, even when `demo` is + // enabled (which is the default above). + // + // As of writing, the default value of `nodes_in_subnet` is 34, which is + // also the node count in fiduciary subnet. + let pocket_ic = PocketIcBuilder::new() + .with_fiduciary_subnet() + .build_async() + .await; + let env = Arc::new(pocket_ic); + + let controller = DEFAULT_CONTROLLER_TEST_ID; + let canister_id = env + .create_canister_with_settings( + None, + Some(CanisterSettings { + controllers: Some(vec![controller]), + ..CanisterSettings::default() + }), + ) + .await; + env.add_cycles(canister_id, INITIAL_CYCLES).await; + env.install_canister( + canister_id, + evm_rpc_wasm(), + Encode!(&args).unwrap(), + Some(controller), + ) + .await; + + let caller = DEFAULT_CALLER_TEST_ID; + + Self { + env, + caller, + controller, + canister_id, + } + } + + pub async fn upgrade_canister(&self, args: InstallArgs) { + for _ in 0..100 { + self.env.tick().await; + // Avoid `CanisterInstallCodeRateLimited` error + self.env.advance_time(Duration::from_secs(600)).await; + self.env.tick().await; + match self + .env + .upgrade_canister( + self.canister_id, + evm_rpc_wasm(), + Encode!(&args).unwrap(), + Some(self.controller), + ) + .await + { + Ok(_) => return, + Err(e) if e.error_code == ErrorCode::CanisterInstallCodeRateLimited => continue, + Err(e) => panic!("Error while upgrading canister: {e:?}"), + } + } + panic!("Failed to upgrade canister after many trials!") + } + + /// Shorthand for deriving an `EvmRpcSetup` with the caller as the canister controller. + pub fn as_controller(mut self) -> Self { + self.caller = self.controller; + self + } + + /// Shorthand for deriving an `EvmRpcSetup` with an arbitrary caller. + pub fn as_caller>(mut self, id: T) -> Self { + self.caller = id.into(); + self + } + + pub fn client(&self) -> ClientBuilder { + EvmRpcClient::builder(self.new_pocket_ic_runtime(), self.canister_id) + } + + fn new_pocket_ic_runtime(&self) -> PocketIcRuntime { + PocketIcRuntime { + env: &self.env, + caller: self.caller, + mocks: Mutex::::default(), + controller: self.controller, + } + } + + pub async fn update_api_keys(&self, api_keys: &[(ProviderId, Option)]) { + self.env + .update_call( + self.canister_id, + self.controller, + "updateApiKeys", + Encode!(&api_keys).expect("Failed to encode arguments."), + ) + .await + .expect("BUG: Failed to call updateApiKeys"); + } + + pub async fn mock_api_keys(self) -> Self { + self.clone() + .as_controller() + .update_api_keys( + &PROVIDERS + .iter() + .filter_map(|provider| { + Some(( + provider.provider_id, + match provider.access { + RpcAccess::Authenticated { .. } => Some(MOCK_API_KEY.to_string()), + RpcAccess::Unauthenticated { .. } => None?, + }, + )) + }) + .collect::>(), + ) + .await; + self + } +} + #[derive(Clone)] pub struct EvmRpcSetup { pub env: Arc, @@ -665,63 +812,74 @@ fn should_decode_transaction_receipt() { ); } -#[test] -fn eth_get_logs_should_succeed() { +#[tokio::test] +async fn eth_get_logs_should_succeed() { fn mock_responses() -> [serde_json::Value; 3] { - json_rpc_sequential_id( - json!({"id":0,"jsonrpc":"2.0","result":[{"address":"0xdac17f958d2ee523a2206206994597c13d831ec7","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43","0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2"],"data":"0x000000000000000000000000000000000000000000000000000000003b9c6433","blockNumber":"0x11dc77e","transactionHash":"0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678","transactionIndex":"0x65","blockHash":"0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629","logIndex":"0xe8","removed":false}]}), - ) + json_rpc_sequential_id(json!({ + "id" : 0, + "jsonrpc" : "2.0", + "result" : [ + { + "address" : "0xdac17f958d2ee523a2206206994597c13d831ec7", + "topics" : [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43", + "0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2" + ], + "data" : "0x000000000000000000000000000000000000000000000000000000003b9c6433", + "blockNumber" : "0x11dc77e", + "transactionHash" : "0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678", + "transactionIndex" : "0x65", + "blockHash" : "0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629", + "logIndex" : "0xe8", + "removed" : false + } + ] + })) } - fn expected_logs() -> Vec { - vec![evm_rpc_types::LogEntry { - address: "0xdac17f958d2ee523a2206206994597c13d831ec7" - .parse() - .unwrap(), - topics: vec![ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43", - "0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2", - ] - .into_iter() - .map(|hex| hex.parse().unwrap()) - .collect(), - data: "0x000000000000000000000000000000000000000000000000000000003b9c6433" - .parse() - .unwrap(), + fn expected_logs() -> Vec { + vec![alloy_rpc_types::Log { + inner: alloy_primitives::Log::new( + address!("0xdac17f958d2ee523a2206206994597c13d831ec7"), + vec![ + b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + b256!("0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43"), + b256!("0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2"), + ], + bytes!("0x000000000000000000000000000000000000000000000000000000003b9c6433"), + ) + .unwrap(), block_number: Some(0x11dc77e_u32.into()), - transaction_hash: Some( + transaction_hash: Some(b256!( "0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678" - .parse() - .unwrap(), - ), + )), transaction_index: Some(0x65_u32.into()), - block_hash: Some( + block_hash: Some(b256!( "0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629" - .parse() - .unwrap(), - ), + )), log_index: Some(0xe8_u32.into()), removed: false, + block_timestamp: None, }] } - let setup = EvmRpcSetup::new().mock_api_keys(); + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; let mut offset = 0_u64; for source in RPC_SERVICES { for (config, from_block, to_block) in [ // default block range ( - None, + GetLogsRpcConfig::default(), Some(BlockTag::Number(0_u8.into())), Some(BlockTag::Number(500_u16.into())), ), // large block range ( - Some(GetLogsRpcConfig { + GetLogsRpcConfig { max_block_range: Some(1_000), ..Default::default() - }), + }, Some(BlockTag::Number(0_u8.into())), Some(BlockTag::Number(501_u16.into())), ), @@ -729,23 +887,26 @@ fn eth_get_logs_should_succeed() { let mut responses: [serde_json::Value; 3] = mock_responses(); add_offset_json_rpc_id(responses.as_mut_slice(), offset); - let response = setup - .eth_get_logs( - source.clone(), - config, - evm_rpc_types::GetLogsArgs { - addresses: vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7" - .parse() - .unwrap()], - from_block, - to_block, - topics: None, - }, + let client = setup + .client() + .with_rpc_sources(source.clone()) + .mock( + evm_rpc_client::MockOutcallBuilder::new(200, responses.clone()), + MockOutcallRepeat::Once, ) - .mock_http_once(MockOutcallBuilder::new(200, responses[0].clone())) - .mock_http_once(MockOutcallBuilder::new(200, responses[1].clone())) - .mock_http_once(MockOutcallBuilder::new(200, responses[2].clone())) - .wait() + .build(); + let response = client + .get_logs(evm_rpc_types::GetLogsArgs { + addresses: vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7" + .parse() + .unwrap()], + from_block, + to_block, + topics: None, + }) + .with_rpc_config(config) + .send() + .await .expect_consistent() .unwrap(); offset += 3; From 74e00f1eb6e2c9db4049db861c8d60494098e19f Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 8 Aug 2025 18:08:40 +0200 Subject: [PATCH 13/57] XC-412: Clippy --- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 2 +- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 8ff422dd..5715aa79 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -35,7 +35,7 @@ impl MockOutcallQueue { pub fn push(&mut self, outcall: impl Into, repeat: MockOutcallRepeat) { self.0.push_back(match repeat { MockOutcallRepeat::Once => Box::new(std::iter::once(outcall.into())), - MockOutcallRepeat::Times(n) => Box::new(std::iter::repeat(outcall.into()).take(n)), + MockOutcallRepeat::Times(n) => Box::new(std::iter::repeat_n(outcall.into(), n)), MockOutcallRepeat::Forever => Box::new(std::iter::repeat(outcall.into())), }) } diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index 3435ceff..9650e3c0 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -34,9 +34,9 @@ impl Clone for PocketIcRuntime<'_> { fn clone(&self) -> Self { Self { env: self.env, - caller: self.caller.clone(), + caller: self.caller, mocks: Mutex::new(self.mocks.lock().unwrap().clone()), - controller: self.controller.clone(), + controller: self.controller, } } } From 266373a56c4f3b0a71bfa4f3586bc11fdc585dea Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 19 Aug 2025 14:53:48 +0200 Subject: [PATCH 14/57] XC-412: Add some type conversions --- evm_rpc_client/src/request/mod.rs | 21 +++++---- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 11 +++-- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 4 +- evm_rpc_types/src/lib.rs | 25 ++++++++++- evm_rpc_types/src/request/mod.rs | 45 +++++++++++++++++++- tests/tests.rs | 28 +++++------- 6 files changed, 101 insertions(+), 33 deletions(-) diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index 47c36a1f..fc828953 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -42,26 +42,31 @@ pub type GetLogsRequestBuilder = RequestBuilder< impl GetLogsRequestBuilder { /// Change the `from_block` parameter for an `eth_getLogs` request. - pub fn with_from_block(mut self, from_block: BlockTag) -> Self { - self.request.params.from_block = Some(from_block); + pub fn with_from_block(mut self, from_block: impl Into) -> Self { + self.request.params.from_block = Some(from_block.into()); self } /// Change the `to_block` parameter for an `eth_getLogs` request. - pub fn with_to_block(mut self, to_block: BlockTag) -> Self { - self.request.params.to_block = Some(to_block); + pub fn with_to_block(mut self, to_block: impl Into) -> Self { + self.request.params.to_block = Some(to_block.into()); self } /// Change the `addresses` parameter for an `eth_getLogs` request. - pub fn with_addresses(mut self, addresses: Vec) -> Self { - self.request.params.addresses = addresses; + pub fn with_addresses(mut self, addresses: Vec>) -> Self { + self.request.params.addresses = addresses.into_iter().map(Into::into).collect(); self } /// Change the `topics` parameter for an `eth_getLogs` request. - pub fn with_topics(mut self, topics: Vec>) -> Self { - self.request.params.topics = Some(topics); + pub fn with_topics(mut self, topics: Vec>>) -> Self { + self.request.params.topics = Some( + topics + .into_iter() + .map(|array| array.into_iter().map(Into::into).collect()) + .collect(), + ); self } } diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 5715aa79..f7c0affe 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -6,6 +6,7 @@ use pocket_ic::common::rest::{ }; use serde_json::Value; use std::collections::{BTreeSet, VecDeque}; +use std::iter; use std::str::FromStr; use url::{Host, Url}; @@ -120,7 +121,7 @@ impl From> for MockOutcallBody { pub struct MockOutcallBuilder(MockOutcall); impl MockOutcallBuilder { - pub fn new(status: u16, bodies: impl IntoIterator>) -> Self { + pub fn new(responses: impl IntoIterator)>) -> Self { Self(MockOutcall { method: None, url: None, @@ -128,9 +129,9 @@ impl MockOutcallBuilder { request_headers: None, request_body: None, max_response_bytes: None, - responses: bodies + responses: responses .into_iter() - .map(|body| { + .map(|(status, body)| { CanisterHttpResponse::CanisterHttpReply(CanisterHttpReply { status, headers: vec![], @@ -141,6 +142,10 @@ impl MockOutcallBuilder { }) } + pub fn new_success(bodies: impl IntoIterator>) -> Self { + MockOutcallBuilder::new(iter::zip(iter::repeat(16), bodies)) + } + pub fn new_error(code: RejectionCode, num_providers: usize, message: impl ToString) -> Self { Self(MockOutcall { method: None, diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index 9650e3c0..074c7623 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -23,9 +23,9 @@ const MAX_TICKS: usize = 10; pub struct PocketIcRuntime<'a> { pub env: &'a PocketIc, pub caller: Principal, - // This field is in a `RefCell` so we can use interior mutability to pop the next element from + // This field is in a `Mutex` so we can use interior mutability to pop the next element from // the queue (i.e., perform a mutable operation) within the `Runtime::update_call` method which - // takes an immutable reference of `self`. + // takes an immutable reference to `self`. pub mocks: Mutex, pub controller: Principal, } diff --git a/evm_rpc_types/src/lib.rs b/evm_rpc_types/src/lib.rs index 17bda182..e8814c55 100644 --- a/evm_rpc_types/src/lib.rs +++ b/evm_rpc_types/src/lib.rs @@ -215,13 +215,27 @@ impl_hex_string!(Hex(Vec)); #[cfg(feature = "alloy")] impl From for alloy_primitives::Address { fn from(value: Hex20) -> Self { - Self::from(value.0) + Self::from(<[u8; 20]>::from(value)) + } +} + +#[cfg(feature = "alloy")] +impl From for Hex20 { + fn from(value: alloy_primitives::Address) -> Self { + Self::from(value.into_array()) } } #[cfg(feature = "alloy")] impl From for alloy_primitives::B256 { fn from(value: Hex32) -> Self { + Self::from(<[u8; 32]>::from(value)) + } +} + +#[cfg(feature = "alloy")] +impl From for Hex32 { + fn from(value: alloy_primitives::B256) -> Self { Self::from(value.0) } } @@ -229,7 +243,14 @@ impl From for alloy_primitives::B256 { #[cfg(feature = "alloy")] impl From for alloy_primitives::Bytes { fn from(value: Hex) -> Self { - Self::from_iter(value.0) + Self::from_iter(Vec::::from(value)) + } +} + +#[cfg(feature = "alloy")] +impl From for Hex { + fn from(value: alloy_primitives::Bytes) -> Self { + Hex(value.to_vec()) } } diff --git a/evm_rpc_types/src/request/mod.rs b/evm_rpc_types/src/request/mod.rs index f654a0f0..b2ba3821 100644 --- a/evm_rpc_types/src/request/mod.rs +++ b/evm_rpc_types/src/request/mod.rs @@ -1,4 +1,4 @@ -use crate::{Hex, Hex20, Hex32, HexByte, Nat256}; +use crate::{Hex, Hex20, Hex32, HexByte, Nat256, RpcError}; use candid::CandidType; use serde::Deserialize; @@ -13,6 +13,37 @@ pub enum BlockTag { Number(Nat256), } +#[cfg(feature = "alloy")] +impl From for BlockTag { + fn from(tag: alloy_rpc_types::BlockNumberOrTag) -> Self { + use alloy_rpc_types::BlockNumberOrTag; + match tag { + BlockNumberOrTag::Latest => Self::Latest, + BlockNumberOrTag::Finalized => Self::Finalized, + BlockNumberOrTag::Safe => Self::Safe, + BlockNumberOrTag::Earliest => Self::Earliest, + BlockNumberOrTag::Pending => Self::Pending, + BlockNumberOrTag::Number(n) => Self::Number(n.into()), + } + } +} + +#[cfg(feature = "alloy")] +impl TryFrom for alloy_rpc_types::BlockNumberOrTag { + type Error = RpcError; + + fn try_from(tag: BlockTag) -> Result { + Ok(match tag { + BlockTag::Latest => Self::Latest, + BlockTag::Finalized => Self::Finalized, + BlockTag::Safe => Self::Safe, + BlockTag::Earliest => Self::Earliest, + BlockTag::Pending => Self::Pending, + BlockTag::Number(n) => Self::Number(u64::try_from(n)?), + }) + } +} + #[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize)] pub struct FeeHistoryArgs { /// Number of blocks in the requested range. @@ -52,6 +83,18 @@ pub struct GetLogsArgs { pub topics: Option>>, } +#[cfg(feature = "alloy")] +impl From> for GetLogsArgs { + fn from(addresses: Vec) -> Self { + Self { + from_block: None, + to_block: None, + addresses: addresses.into_iter().map(Hex20::from).collect(), + topics: None, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, CandidType, Deserialize)] pub struct GetTransactionCountArgs { pub address: Hex20, diff --git a/tests/tests.rs b/tests/tests.rs index 7aa4462a..982c6163 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -2,6 +2,7 @@ mod mock; use crate::mock::MockJsonRequestBody; use alloy_primitives::{address, b256, bytes}; +use alloy_rpc_types::BlockNumberOrTag; use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat, Principal}; use canlog::{Log, LogEntry}; @@ -12,9 +13,7 @@ use evm_rpc::{ providers::PROVIDERS, types::{Metrics, ProviderId, RpcAccess, RpcMethod}, }; -use evm_rpc_client::{ - ClientBuilder, EvmRpcClient, MockOutcallQueue, MockOutcallRepeat, PocketIcRuntime, -}; +use evm_rpc_client::{once, ClientBuilder, EvmRpcClient, MockOutcallQueue, PocketIcRuntime}; use evm_rpc_types::{ BlockTag, ConsensusStrategy, EthMainnetService, EthSepoliaService, GetLogsRpcConfig, Hex, Hex20, Hex32, HttpOutcallError, InstallArgs, JsonRpcError, LegacyRejectionCode, MultiRpcResult, @@ -871,8 +870,8 @@ async fn eth_get_logs_should_succeed() { // default block range ( GetLogsRpcConfig::default(), - Some(BlockTag::Number(0_u8.into())), - Some(BlockTag::Number(500_u16.into())), + BlockNumberOrTag::Number(0_u8.into()), + BlockNumberOrTag::Number(500_u16.into()), ), // large block range ( @@ -880,8 +879,8 @@ async fn eth_get_logs_should_succeed() { max_block_range: Some(1_000), ..Default::default() }, - Some(BlockTag::Number(0_u8.into())), - Some(BlockTag::Number(501_u16.into())), + BlockNumberOrTag::Number(0_u8.into()), + BlockNumberOrTag::Number(501_u16.into()), ), ] { let mut responses: [serde_json::Value; 3] = mock_responses(); @@ -891,19 +890,14 @@ async fn eth_get_logs_should_succeed() { .client() .with_rpc_sources(source.clone()) .mock( - evm_rpc_client::MockOutcallBuilder::new(200, responses.clone()), - MockOutcallRepeat::Once, + evm_rpc_client::MockOutcallBuilder::new_success(responses.clone()), + once(), ) .build(); let response = client - .get_logs(evm_rpc_types::GetLogsArgs { - addresses: vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7" - .parse() - .unwrap()], - from_block, - to_block, - topics: None, - }) + .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) + .with_from_block(from_block) + .with_to_block(to_block) .with_rpc_config(config) .send() .await From c1e031ea32e1c421b7a1b9ff146da867505e7af3 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 19 Aug 2025 17:03:18 +0200 Subject: [PATCH 15/57] XC-412: Fix mock iteration --- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 16 ++++---- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 43 ++++++++------------ tests/tests.rs | 6 +-- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index f7c0affe..7a9ba499 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -5,9 +5,11 @@ use pocket_ic::common::rest::{ CanisterHttpRequest, CanisterHttpResponse, }; use serde_json::Value; -use std::collections::{BTreeSet, VecDeque}; -use std::iter; -use std::str::FromStr; +use std::{ + collections::{BTreeSet, VecDeque}, + iter, + str::FromStr, +}; use url::{Host, Url}; #[derive(Clone, Default)] @@ -35,9 +37,9 @@ impl Clone for Box { impl MockOutcallQueue { pub fn push(&mut self, outcall: impl Into, repeat: MockOutcallRepeat) { self.0.push_back(match repeat { - MockOutcallRepeat::Once => Box::new(std::iter::once(outcall.into())), - MockOutcallRepeat::Times(n) => Box::new(std::iter::repeat_n(outcall.into(), n)), - MockOutcallRepeat::Forever => Box::new(std::iter::repeat(outcall.into())), + MockOutcallRepeat::Once => Box::new(iter::once(outcall.into())), + MockOutcallRepeat::Times(n) => Box::new(iter::repeat_n(outcall.into(), n)), + MockOutcallRepeat::Forever => Box::new(iter::repeat(outcall.into())), }) } } @@ -143,7 +145,7 @@ impl MockOutcallBuilder { } pub fn new_success(bodies: impl IntoIterator>) -> Self { - MockOutcallBuilder::new(iter::zip(iter::repeat(16), bodies)) + MockOutcallBuilder::new(iter::zip(iter::repeat(200), bodies)) } pub fn new_error(code: RejectionCode, num_providers: usize, message: impl ToString) -> Self { diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index 074c7623..4b87ba97 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -14,6 +14,7 @@ use pocket_ic::common::rest::{ use pocket_ic::nonblocking::PocketIc; use pocket_ic::RejectResponse; use serde::de::DeserializeOwned; +use std::iter; use std::sync::Mutex; use std::time::Duration; @@ -88,36 +89,28 @@ impl Runtime for PocketIcRuntime<'_> { impl PocketIcRuntime<'_> { async fn execute_mock(&self) { - let maybe_mock = { + if let Some(mock) = { let mut mocks = self.mocks.lock().unwrap(); mocks.next() - }; - if let Some(mock) = maybe_mock { - if !self.try_mock_http_inner(mock).await { - panic!("no pending HTTP request") + } { + let mut responses = mock.responses.clone().into_iter(); + let requests = tick_until_http_request(self.env).await; + + for (request, response) in iter::zip(requests, responses.by_ref()) { + mock.assert_matches(&request); + let mock_response = MockCanisterHttpResponse { + subnet_id: request.subnet_id, + request_id: request.request_id, + response: check_response_size(&request, response), + additional_responses: vec![], + }; + self.env.mock_canister_http_response(mock_response).await; } - } - } - - async fn try_mock_http_inner(&self, mock: MockOutcall) -> bool { - let http_requests = tick_until_http_request(self.env).await; - let request = match http_requests.first() { - Some(request) => request, - None => return false, - }; - mock.assert_matches(request); - for response in mock.responses.clone() { - let mock_response = MockCanisterHttpResponse { - subnet_id: request.subnet_id, - request_id: request.request_id, - response: check_response_size(request, response), - additional_responses: vec![], - }; - self.env.mock_canister_http_response(mock_response).await; + if responses.next().is_some() { + panic!("no pending HTTP request") + } } - - true } } diff --git a/tests/tests.rs b/tests/tests.rs index 982c6163..04a51f16 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -849,15 +849,15 @@ async fn eth_get_logs_should_succeed() { bytes!("0x000000000000000000000000000000000000000000000000000000003b9c6433"), ) .unwrap(), - block_number: Some(0x11dc77e_u32.into()), + block_number: Some(0x11dc77e_u64), transaction_hash: Some(b256!( "0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678" )), - transaction_index: Some(0x65_u32.into()), + transaction_index: Some(0x65_u64), block_hash: Some(b256!( "0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629" )), - log_index: Some(0xe8_u32.into()), + log_index: Some(0xe8_u64), removed: false, block_timestamp: None, }] From 5e8aeedcced2b8b0968786d4f88a166b3058078e Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 20 Aug 2025 07:40:50 +0200 Subject: [PATCH 16/57] XC-412: Fix repository link Co-authored-by: gregorydemay <112856886+gregorydemay@users.noreply.github.com> --- evm_rpc_client/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 2b55b58f..91b0d482 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -7,7 +7,7 @@ readme = "README.md" authors = ["DFINITY Foundation"] edition = "2021" include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"] -repository = "https://github.com/dfinity/evm-rpc-client" +repository = "https://github.com/dfinity/evm-rpc-canister" documentation = "https://docs.rs/evm_rpc_client" [dependencies] From fff88e08e261754b7d491bdbc9e920c787747e6b Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 20 Aug 2025 12:12:14 +0200 Subject: [PATCH 17/57] XC-412: Require docs --- evm_rpc_client/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 63ca72fa..29da5b78 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -1,4 +1,8 @@ -//! TODO XC-412: Add documentation and examples +//! TODO XC-412: Add top-level documentation +//! TODO XC-412: Add examples (needs dummy runtime) + +#![forbid(unsafe_code)] +#![forbid(missing_docs)] mod request; From a54740962988213f1d3b67581aa27e3645ca6f03 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 20 Aug 2025 15:14:19 +0200 Subject: [PATCH 18/57] XC-412: Add rustdoc and examples --- Cargo.lock | 1 + evm_rpc_client/Cargo.toml | 3 +- evm_rpc_client/src/fixtures/mod.rs | 132 +++++++++++++++++++ evm_rpc_client/src/lib.rs | 199 +++++++++++++++++++++-------- evm_rpc_client/src/request/mod.rs | 4 +- 5 files changed, 284 insertions(+), 55 deletions(-) create mode 100644 evm_rpc_client/src/fixtures/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8cb6fc8a..ef4b2bf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1546,6 +1546,7 @@ dependencies = [ "ic-error-types", "serde", "strum 0.27.1", + "tokio", ] [[package]] diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 91b0d482..90542fc4 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -20,4 +20,5 @@ ic-error-types = { workspace = true } serde = { workspace = true } strum = { workspace = true } -[dev-dependencies] \ No newline at end of file +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } \ No newline at end of file diff --git a/evm_rpc_client/src/fixtures/mod.rs b/evm_rpc_client/src/fixtures/mod.rs new file mode 100644 index 00000000..612624d1 --- /dev/null +++ b/evm_rpc_client/src/fixtures/mod.rs @@ -0,0 +1,132 @@ +//! Simple types to create basic unit tests for the [`crate::EvmRpcClient`]. +//! +//! Types and methods for this module are only available for non-canister architecture (non `wasm32`). + +use crate::{ClientBuilder, Runtime}; +use async_trait::async_trait; +use candid::{utils::ArgumentEncoder, CandidType, Decode, Encode, Principal}; +use ic_error_types::RejectCode; +use serde::de::DeserializeOwned; +use std::collections::BTreeMap; + +impl ClientBuilder { + /// Set the runtime to a [`StubRuntime`]. + pub fn with_stub_responses(self) -> ClientBuilder { + self.with_runtime(|_runtime| StubRuntime::default()) + } + + /// Change the runtime to return the given stub response for all calls. + pub fn with_default_stub_response( + self, + stub_response: Out, + ) -> ClientBuilder { + self.with_stub_responses() + .with_default_response(stub_response) + } +} + +impl ClientBuilder { + /// Change the runtime to return the given stub response for all calls. + pub fn with_default_response( + self, + stub_response: Out, + ) -> ClientBuilder { + self.with_runtime(|runtime| runtime.with_default_response(stub_response)) + } + + /// Change the runtime to return the given stub response for calls to the given method. + pub fn with_response_for_method( + self, + method_name: &str, + stub_response: Out, + ) -> ClientBuilder { + self.with_runtime(|runtime| runtime.with_response_for_method(method_name, stub_response)) + } +} + +/// An implementation of [`Runtime`] that always returns the same candid-encoded response +/// for a given method. +/// +/// Implement your own [`Runtime`] in case a more refined approach is needed. +pub struct StubRuntime { + default_call_result: Option>, + method_to_call_result_map: BTreeMap>, +} + +impl StubRuntime { + /// Create a new [`StubRuntime`] with the given default stub response. + pub fn new() -> Self { + Self { + default_call_result: None, + method_to_call_result_map: BTreeMap::new(), + } + } + + /// Create a new [`StubRuntime`] with the given default stub response. + pub fn with_default_response(mut self, stub_response: Out) -> Self { + let result = Encode!(&stub_response).expect("Failed to encode Candid stub response"); + self.default_call_result = Some(result); + self + } + + /// Modify a [`StubRuntime`] to return the given response for the given method + pub fn with_response_for_method( + mut self, + method: &str, + stub_response: Out, + ) -> Self { + self.method_to_call_result_map.insert( + method.to_string(), + Encode!(&stub_response).expect("Failed to encode Candid stub response"), + ); + self + } + + fn call(&self, method: &str) -> Result + where + Out: CandidType + DeserializeOwned, + { + let bytes = self + .method_to_call_result_map + .get(method) + .or(self.default_call_result.as_ref()) + .unwrap_or_else(|| panic!("No available call response value for method `{method}`")); + Ok(Decode!(bytes, Out).expect("Failed to decode Candid stub response")) + } +} + +impl Default for StubRuntime { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl Runtime for StubRuntime { + async fn update_call( + &self, + _id: Principal, + method: &str, + _args: In, + _cycles: u128, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + self.call(method) + } + + async fn query_call( + &self, + _id: Principal, + method: &str, + _args: In, + ) -> Result + where + In: ArgumentEncoder + Send, + Out: CandidType + DeserializeOwned, + { + self.call(method) + } +} diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 29da5b78..15be20d5 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -1,9 +1,120 @@ -//! TODO XC-412: Add top-level documentation -//! TODO XC-412: Add examples (needs dummy runtime) +//! Client to interact with the EVM RPC canister +//! +//! # Examples +//! +//! ## Configuring the client +//! +//! By default, any RPC endpoint supported by the EVM RPC canister will call 3 providers and require +//! equality between their results. It is possible to customize the client so that another strategy, +//! such as 2-out-of-3 in the example below, is used for all following calls. +//! +//! ```rust +//! use evm_rpc_client::EvmRpcClient; +//! use evm_rpc_types::{ConsensusStrategy, RpcConfig, RpcServices}; +//! +//! let client = EvmRpcClient::builder_for_ic() +//! .with_rpc_sources(RpcServices::EthMainnet(None)) +//! .with_consensus_strategy(ConsensusStrategy::Threshold { +//! total: Some(3), +//! min: 2, +//! }) +//! .build(); +//! ``` +//! +//! ## Specifying the amount of cycles to send +//! +//! Every call made to the EVM RPC canister that triggers HTTPs outcalls (e.g., `eth_getLogs`) +//! needs to attach some cycles to pay for the call. +//! By default, the client will attach some amount of cycles that should be sufficient for most cases. +//! +//! If this is not the case, the amount of cycles to be sent can be overridden. It's advisable to +//! actually send *more* cycles than required, since *unused cycles will be refunded*. +//! +//! ```rust +//! use evm_rpc_client::EvmRpcClient; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # use evm_rpc_types::{GetLogsArgs, MultiRpcResult, RpcError}; +//! let client = EvmRpcClient::builder_for_ic() +//! # // TODO XC-412: Return meaningful response +//! # .with_default_stub_response(MultiRpcResult::Consistent::>(Ok(vec![]))) +//! .build(); +//! +//! // TODO XC-412: Fetch with meaningful parameters +//! let request = client.get_logs(GetLogsArgs { +//! from_block: None, +//! to_block: None, +//! addresses: vec![], +//! topics: None, +//! }); +//! +//! let logs = request +//! .with_cycles(10_000_000_000) +//! .send() +//! .await +//! .expect_consistent(); +//! +//! assert_eq!(logs, Ok(vec![])); +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Overriding client configuration for a specific call +//! +//! Besides changing the amount of cycles for a particular call as described above, +//! it is sometimes desirable to have a custom configuration for a specific +//! call that is different from the one used by the client for all the other calls. +//! +//! For example, maybe for most calls, a 2 out-of 3 strategy is good enough, but for `eth_getSlot` +//! your application requires a higher threshold and more robustness with a 3-out-of-5 : +//! +//! ```rust +//! use evm_rpc_client::EvmRpcClient; +//! use evm_rpc_types::{ConsensusStrategy, GetLogsRpcConfig , RpcServices}; +//! +//! # #[tokio::main] +//! # async fn main() -> Result<(), Box> { +//! # use evm_rpc_types::{GetLogsArgs, MultiRpcResult}; +//! let client = EvmRpcClient::builder_for_ic() +//! # // TODO XC-412: Return meaningful response +//! # .with_default_stub_response(MultiRpcResult::>::Consistent(Ok(vec![]))) +//! .with_rpc_sources(RpcServices::EthMainnet(None)) +//! .with_consensus_strategy(ConsensusStrategy::Threshold { +//! total: Some(3), +//! min: 2, +//! }) +//! .build(); +//! +//! let logs = client +//! // TODO XC-412: Fetch with meaningful parameters +//! .get_logs(GetLogsArgs { +//! from_block: None, +//! to_block: None, +//! addresses: vec![], +//! topics: None, +//! }) +//! .with_rpc_config(GetLogsRpcConfig { +//! response_consensus: Some(ConsensusStrategy::Threshold { +//! total: Some(5), +//! min: 3, +//! }), +//! ..Default::default() +//! }) +//! .send() +//! .await +//! .expect_consistent(); +//! +//! assert_eq!(logs, Ok(vec![])); +//! # Ok(()) +//! # } +//! ``` #![forbid(unsafe_code)] #![forbid(missing_docs)] +#[cfg(not(target_arch = "wasm32"))] +pub mod fixtures; mod request; use crate::request::{Request, RequestBuilder}; @@ -71,18 +182,6 @@ impl Clone for EvmRpcClient { } } -impl EvmRpcClient { - /// Creates a [`ClientBuilder`] to configure a [`EvmRpcClient`]. - pub fn builder(runtime: R, evm_rpc_canister: Principal) -> ClientBuilder { - ClientBuilder::new(runtime, evm_rpc_canister) - } - - /// Returns a reference to the client's runtime. - pub fn runtime(&self) -> &R { - &self.config.runtime - } -} - impl EvmRpcClient { /// Creates a [`ClientBuilder`] to configure a [`EvmRpcClient`] targeting [`EVM_RPC_CANISTER`] /// running on the Internet Computer. @@ -171,8 +270,39 @@ impl ClientBuilder { } impl EvmRpcClient { - /// Call `get_ethLogs` on the EVM RPC canister. - /// TODO XC-412: Add docs and examples + /// Call `eth_getLogs` on the EVM RPC canister. + /// + /// # Examples + /// + /// ```rust + /// use evm_rpc_client::EvmRpcClient; + /// + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// # use evm_rpc_types::{GetLogsArgs, MultiRpcResult, RpcError}; + /// let client = EvmRpcClient::builder_for_ic() + /// # // TODO XC-412: Return meaningful response + /// # .with_default_stub_response(MultiRpcResult::Consistent::>(Ok(vec![]))) + /// .build(); + /// + /// // TODO XC-412: Fetch with meaningful parameters + /// let request = client.get_logs(GetLogsArgs { + /// from_block: None, + /// to_block: None, + /// addresses: vec![], + /// topics: None, + /// }); + /// + /// let logs = request + /// .with_cycles(10_000_000_000) + /// .send() + /// .await + /// .expect_consistent(); + /// + /// assert_eq!(logs, Ok(vec![])); + /// # Ok(()) + /// # } + /// ``` pub fn get_logs(&self, params: impl Into) -> GetLogsRequestBuilder { RequestBuilder::new( self.clone(), @@ -183,40 +313,6 @@ impl EvmRpcClient { } impl EvmRpcClient { - /// Call `getProviders` on the EVM RPC canister. - pub async fn get_providers(&self) -> Vec { - self.config - .runtime - .query_call(self.config.evm_rpc_canister, "getProviders", ()) - .await - .unwrap() - } - - /// Call `getServiceProviderMap` on the EVM RPC canister. - // TODO XC-412: Create type alias in `evm_rpc_types` for `ProviderId` i.e. `u64` - pub async fn get_service_provider_map(&self) -> Vec<(evm_rpc_types::RpcService, u64)> { - self.config - .runtime - .query_call(self.config.evm_rpc_canister, "getServiceProviderMap", ()) - .await - .unwrap() - } - - /// Call `updateApiKeys` on the EVM RPC canister. - // TODO XC-412: Create type alias in `evm_rpc_types` for `ProviderId` i.e. `u64` - pub async fn update_api_keys(&self, api_keys: &[(u64, Option)]) { - self.config - .runtime - .update_call( - self.config.evm_rpc_canister, - "updateApiKeys", - (api_keys.to_vec(),), - 0, - ) - .await - .unwrap() - } - async fn execute_request( &self, request: Request, @@ -304,7 +400,8 @@ fn convert_reject_code(code: IcCdkRejectionCode) -> RejectCode { 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. + // 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 => { diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index 47c36a1f..aed767fa 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -1,8 +1,6 @@ use crate::{EvmRpcClient, Runtime}; use candid::CandidType; -use evm_rpc_types::{ - BlockTag, GetLogsArgs, GetLogsRpcConfig, Hex20, Hex32, MultiRpcResult, RpcConfig, RpcServices, -}; +use evm_rpc_types::{BlockTag, GetLogsArgs, GetLogsRpcConfig, Hex20, Hex32, MultiRpcResult, RpcConfig, RpcServices}; use ic_error_types::RejectCode; use serde::de::DeserializeOwned; use std::fmt::{Debug, Formatter}; From 5e5c55a981174ab6659ed654c5e69dd5f120c3af Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 20 Aug 2025 15:35:26 +0200 Subject: [PATCH 19/57] XC-412: Move type conversions to separate files --- Cargo.lock | 1 + evm_rpc_client/Cargo.toml | 1 + evm_rpc_client/src/lib.rs | 31 ++++++---------------- evm_rpc_types/src/alloy.rs | 37 +++++++++++++++++++++++++++ evm_rpc_types/src/lib.rs | 23 ++--------------- evm_rpc_types/src/request/alloy.rs | 41 ++++++++++++++++++++++++++++++ evm_rpc_types/src/request/mod.rs | 3 +++ 7 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 evm_rpc_types/src/alloy.rs create mode 100644 evm_rpc_types/src/request/alloy.rs diff --git a/Cargo.lock b/Cargo.lock index ef4b2bf5..f0d0efb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1538,6 +1538,7 @@ dependencies = [ name = "evm_rpc_client" version = "1.4.0" dependencies = [ + "alloy-primitives", "alloy-rpc-types", "async-trait", "candid", diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 90542fc4..2fc713e3 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/dfinity/evm-rpc-canister" documentation = "https://docs.rs/evm_rpc_client" [dependencies] +alloy-primitives = { workspace = true } alloy-rpc-types = { workspace = true } async-trait = { workspace = true } candid = { workspace = true } diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 15be20d5..2d971a2e 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -31,6 +31,7 @@ //! actually send *more* cycles than required, since *unused cycles will be refunded*. //! //! ```rust +//! use alloy_primitives::address; //! use evm_rpc_client::EvmRpcClient; //! //! # #[tokio::main] @@ -41,13 +42,7 @@ //! # .with_default_stub_response(MultiRpcResult::Consistent::>(Ok(vec![]))) //! .build(); //! -//! // TODO XC-412: Fetch with meaningful parameters -//! let request = client.get_logs(GetLogsArgs { -//! from_block: None, -//! to_block: None, -//! addresses: vec![], -//! topics: None, -//! }); +//! let request = client.get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]); //! //! let logs = request //! .with_cycles(10_000_000_000) @@ -70,12 +65,13 @@ //! your application requires a higher threshold and more robustness with a 3-out-of-5 : //! //! ```rust +//! use alloy_primitives::address; //! use evm_rpc_client::EvmRpcClient; //! use evm_rpc_types::{ConsensusStrategy, GetLogsRpcConfig , RpcServices}; //! //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { -//! # use evm_rpc_types::{GetLogsArgs, MultiRpcResult}; +//! use evm_rpc_types::{GetLogsArgs, MultiRpcResult}; //! let client = EvmRpcClient::builder_for_ic() //! # // TODO XC-412: Return meaningful response //! # .with_default_stub_response(MultiRpcResult::>::Consistent(Ok(vec![]))) @@ -87,13 +83,7 @@ //! .build(); //! //! let logs = client -//! // TODO XC-412: Fetch with meaningful parameters -//! .get_logs(GetLogsArgs { -//! from_block: None, -//! to_block: None, -//! addresses: vec![], -//! topics: None, -//! }) +//! .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) //! .with_rpc_config(GetLogsRpcConfig { //! response_consensus: Some(ConsensusStrategy::Threshold { //! total: Some(5), @@ -275,23 +265,18 @@ impl EvmRpcClient { /// # Examples /// /// ```rust + /// use alloy_primitives::address; /// use evm_rpc_client::EvmRpcClient; /// /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// # use evm_rpc_types::{GetLogsArgs, MultiRpcResult, RpcError}; + /// # use evm_rpc_types::MultiRpcResult; /// let client = EvmRpcClient::builder_for_ic() /// # // TODO XC-412: Return meaningful response /// # .with_default_stub_response(MultiRpcResult::Consistent::>(Ok(vec![]))) /// .build(); /// - /// // TODO XC-412: Fetch with meaningful parameters - /// let request = client.get_logs(GetLogsArgs { - /// from_block: None, - /// to_block: None, - /// addresses: vec![], - /// topics: None, - /// }); + /// let request = client.get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]); /// /// let logs = request /// .with_cycles(10_000_000_000) diff --git a/evm_rpc_types/src/alloy.rs b/evm_rpc_types/src/alloy.rs new file mode 100644 index 00000000..f6e09a1f --- /dev/null +++ b/evm_rpc_types/src/alloy.rs @@ -0,0 +1,37 @@ +use crate::{Hex, Hex20, Hex32}; + +impl From for alloy_primitives::Address { + fn from(value: Hex20) -> Self { + Self::from(<[u8; 20]>::from(value)) + } +} + +impl From for Hex20 { + fn from(value: alloy_primitives::Address) -> Self { + Self::from(value.into_array()) + } +} + +impl From for alloy_primitives::B256 { + fn from(value: Hex32) -> Self { + Self::from(<[u8; 32]>::from(value)) + } +} + +impl From for Hex32 { + fn from(value: alloy_primitives::B256) -> Self { + Self::from(value.0) + } +} + +impl From for alloy_primitives::Bytes { + fn from(value: Hex) -> Self { + Self::from_iter(Vec::::from(value)) + } +} + +impl From for Hex { + fn from(value: alloy_primitives::Bytes) -> Self { + Hex(value.to_vec()) + } +} diff --git a/evm_rpc_types/src/lib.rs b/evm_rpc_types/src/lib.rs index 17bda182..bc318993 100644 --- a/evm_rpc_types/src/lib.rs +++ b/evm_rpc_types/src/lib.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod tests; +#[cfg(feature = "alloy")] +mod alloy; mod lifecycle; mod request; mod response; @@ -212,27 +214,6 @@ impl_hex_string!(Hex32([u8; 32])); impl_hex_string!(Hex256([u8; 256])); impl_hex_string!(Hex(Vec)); -#[cfg(feature = "alloy")] -impl From for alloy_primitives::Address { - fn from(value: Hex20) -> Self { - Self::from(value.0) - } -} - -#[cfg(feature = "alloy")] -impl From for alloy_primitives::B256 { - fn from(value: Hex32) -> Self { - Self::from(value.0) - } -} - -#[cfg(feature = "alloy")] -impl From for alloy_primitives::Bytes { - fn from(value: Hex) -> Self { - Self::from_iter(value.0) - } -} - impl Hex20 { pub fn as_array(&self) -> &[u8; 20] { &self.0 diff --git a/evm_rpc_types/src/request/alloy.rs b/evm_rpc_types/src/request/alloy.rs new file mode 100644 index 00000000..a1ba1882 --- /dev/null +++ b/evm_rpc_types/src/request/alloy.rs @@ -0,0 +1,41 @@ +use crate::{BlockTag, GetLogsArgs, Hex20, RpcError}; + +impl From for BlockTag { + fn from(tag: alloy_rpc_types::BlockNumberOrTag) -> Self { + use alloy_rpc_types::BlockNumberOrTag; + match tag { + BlockNumberOrTag::Latest => Self::Latest, + BlockNumberOrTag::Finalized => Self::Finalized, + BlockNumberOrTag::Safe => Self::Safe, + BlockNumberOrTag::Earliest => Self::Earliest, + BlockNumberOrTag::Pending => Self::Pending, + BlockNumberOrTag::Number(n) => Self::Number(n.into()), + } + } +} + +impl TryFrom for alloy_rpc_types::BlockNumberOrTag { + type Error = RpcError; + + fn try_from(tag: BlockTag) -> Result { + Ok(match tag { + BlockTag::Latest => Self::Latest, + BlockTag::Finalized => Self::Finalized, + BlockTag::Safe => Self::Safe, + BlockTag::Earliest => Self::Earliest, + BlockTag::Pending => Self::Pending, + BlockTag::Number(n) => Self::Number(u64::try_from(n)?), + }) + } +} + +impl> From> for GetLogsArgs { + fn from(addresses: Vec) -> Self { + Self { + from_block: None, + to_block: None, + addresses: addresses.into_iter().map(Into::into).collect(), + topics: None, + } + } +} diff --git a/evm_rpc_types/src/request/mod.rs b/evm_rpc_types/src/request/mod.rs index f654a0f0..9432cc0b 100644 --- a/evm_rpc_types/src/request/mod.rs +++ b/evm_rpc_types/src/request/mod.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "alloy")] +mod alloy; + use crate::{Hex, Hex20, Hex32, HexByte, Nat256}; use candid::CandidType; use serde::Deserialize; From 4a73233a41b57ee2ce04d37f7c237fb9bdfc1854 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 20 Aug 2025 16:32:52 +0200 Subject: [PATCH 20/57] XC-412: Flesh out examples --- Cargo.toml | 2 +- evm_rpc_client/src/lib.rs | 152 +++++++++++++++++++++++++---- evm_rpc_types/src/request/alloy.rs | 4 +- 3 files changed, 137 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1f7a436d..acf8435e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ zeroize = { version = "1.8", features = ["zeroize_derive"] } regex = "1.11" [dev-dependencies] -assert_matches = "1.5" +assert_matches = { workspace = true } candid_parser = { workspace = true } ic-crypto-test-utils-reproducible-rng = { git = "https://github.com/dfinity/ic", rev = "release-2024-09-26_01-31-base" } ic-management-canister-types = { workspace = true } diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 2d971a2e..88068b7a 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -31,26 +31,65 @@ //! actually send *more* cycles than required, since *unused cycles will be refunded*. //! //! ```rust -//! use alloy_primitives::address; +//! # // TODO XC-412: Use simpler example e.g. `eth_getBalance` +//! use alloy_primitives::{address, b256, bytes}; //! use evm_rpc_client::EvmRpcClient; //! +//! # use evm_rpc_types::{Hex, Hex20, Hex32, MultiRpcResult}; +//! # use std::str::FromStr; //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { -//! # use evm_rpc_types::{GetLogsArgs, MultiRpcResult, RpcError}; //! let client = EvmRpcClient::builder_for_ic() -//! # // TODO XC-412: Return meaningful response -//! # .with_default_stub_response(MultiRpcResult::Consistent::>(Ok(vec![]))) +//! # .with_default_stub_response(MultiRpcResult::Consistent(Ok(vec![ +//! # evm_rpc_types::LogEntry { +//! # address: Hex20::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), +//! # topics: vec![ +//! # Hex32::from_str("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef").unwrap(), +//! # Hex32::from_str("0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90").unwrap(), +//! # Hex32::from_str("0x0000000000000000000000000000000aa232009084bd71a5797d089aa4edfad4").unwrap(), +//! # ], +//! # data: Hex::from_str("0x00000000000000000000000000000000000000000000000000000000cd566ae8").unwrap(), +//! # block_number: Some(0x161bd70_u64.into()), +//! # transaction_hash: Some(Hex32::from_str("0xfe5bc88d0818b66a67b0619b1b4d81bfe38029e3799c7f0eb86b33ca7dc4c811").unwrap()), +//! # transaction_index: Some(0x0_u64.into()), +//! # block_hash: Some(Hex32::from_str("0x0bbd9b12140e674cdd55e63539a25df8280a70cee3676c94d8e05fa5f868a914").unwrap()), +//! # log_index: Some(0x0_u64.into()), +//! # removed: false, +//! # } +//! # ]))) //! .build(); //! //! let request = client.get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]); //! -//! let logs = request +//! let result = request //! .with_cycles(10_000_000_000) //! .send() //! .await //! .expect_consistent(); //! -//! assert_eq!(logs, Ok(vec![])); +//! assert!(result.is_ok()); +//! assert_eq!(result.unwrap().first(), Some( +//! &alloy_rpc_types::Log { +//! inner: alloy_primitives::Log { +//! address: address!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), +//! data: alloy_primitives::LogData::new( +//! vec![ +//! b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), +//! b256!("0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90"), +//! b256!("0x0000000000000000000000000000000aa232009084bd71a5797d089aa4edfad4"), +//! ], +//! bytes!("0x00000000000000000000000000000000000000000000000000000000cd566ae8"), +//! ).unwrap(), +//! }, +//! block_hash: Some(b256!("0x0bbd9b12140e674cdd55e63539a25df8280a70cee3676c94d8e05fa5f868a914")), +//! block_number: Some(0x161bd70_u64), +//! block_timestamp: None, +//! transaction_hash: Some(b256!("0xfe5bc88d0818b66a67b0619b1b4d81bfe38029e3799c7f0eb86b33ca7dc4c811")), +//! transaction_index: Some(0x0_u64), +//! log_index: Some(0x0_u64), +//! removed: false, +//! }, +//! )); //! # Ok(()) //! # } //! ``` @@ -65,16 +104,33 @@ //! your application requires a higher threshold and more robustness with a 3-out-of-5 : //! //! ```rust -//! use alloy_primitives::address; +//! # // TODO XC-412: Use simpler example e.g. `eth_getBalance` +//! use alloy_primitives::{address, b256, bytes}; //! use evm_rpc_client::EvmRpcClient; //! use evm_rpc_types::{ConsensusStrategy, GetLogsRpcConfig , RpcServices}; //! +//! # use evm_rpc_types::{Hex, Hex20, Hex32, MultiRpcResult}; +//! # use std::str::FromStr; //! # #[tokio::main] //! # async fn main() -> Result<(), Box> { -//! use evm_rpc_types::{GetLogsArgs, MultiRpcResult}; //! let client = EvmRpcClient::builder_for_ic() -//! # // TODO XC-412: Return meaningful response -//! # .with_default_stub_response(MultiRpcResult::>::Consistent(Ok(vec![]))) +//! # .with_default_stub_response(MultiRpcResult::Consistent(Ok(vec![ +//! # evm_rpc_types::LogEntry { +//! # address: Hex20::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), +//! # topics: vec![ +//! # Hex32::from_str("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef").unwrap(), +//! # Hex32::from_str("0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90").unwrap(), +//! # Hex32::from_str("0x0000000000000000000000000000000aa232009084bd71a5797d089aa4edfad4").unwrap(), +//! # ], +//! # data: Hex::from_str("0x00000000000000000000000000000000000000000000000000000000cd566ae8").unwrap(), +//! # block_number: Some(0x161bd70_u64.into()), +//! # transaction_hash: Some(Hex32::from_str("0xfe5bc88d0818b66a67b0619b1b4d81bfe38029e3799c7f0eb86b33ca7dc4c811").unwrap()), +//! # transaction_index: Some(0x0_u64.into()), +//! # block_hash: Some(Hex32::from_str("0x0bbd9b12140e674cdd55e63539a25df8280a70cee3676c94d8e05fa5f868a914").unwrap()), +//! # log_index: Some(0x0_u64.into()), +//! # removed: false, +//! # } +//! # ]))) //! .with_rpc_sources(RpcServices::EthMainnet(None)) //! .with_consensus_strategy(ConsensusStrategy::Threshold { //! total: Some(3), @@ -82,7 +138,7 @@ //! }) //! .build(); //! -//! let logs = client +//! let result = client //! .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) //! .with_rpc_config(GetLogsRpcConfig { //! response_consensus: Some(ConsensusStrategy::Threshold { @@ -95,7 +151,29 @@ //! .await //! .expect_consistent(); //! -//! assert_eq!(logs, Ok(vec![])); +//! assert!(result.is_ok()); +//! assert_eq!(result.unwrap().first(), Some( +//! &alloy_rpc_types::Log { +//! inner: alloy_primitives::Log { +//! address: address!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), +//! data: alloy_primitives::LogData::new( +//! vec![ +//! b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), +//! b256!("0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90"), +//! b256!("0x0000000000000000000000000000000aa232009084bd71a5797d089aa4edfad4"), +//! ], +//! bytes!("0x00000000000000000000000000000000000000000000000000000000cd566ae8"), +//! ).unwrap(), +//! }, +//! block_hash: Some(b256!("0x0bbd9b12140e674cdd55e63539a25df8280a70cee3676c94d8e05fa5f868a914")), +//! block_number: Some(0x161bd70_u64), +//! block_timestamp: None, +//! transaction_hash: Some(b256!("0xfe5bc88d0818b66a67b0619b1b4d81bfe38029e3799c7f0eb86b33ca7dc4c811")), +//! transaction_index: Some(0x0_u64), +//! log_index: Some(0x0_u64), +//! removed: false, +//! }, +//! )); //! # Ok(()) //! # } //! ``` @@ -265,26 +343,64 @@ impl EvmRpcClient { /// # Examples /// /// ```rust - /// use alloy_primitives::address; + /// use alloy_primitives::{address, b256, bytes}; /// use evm_rpc_client::EvmRpcClient; /// + /// # use evm_rpc_types::{Hex, Hex20, Hex32, MultiRpcResult}; + /// # use std::str::FromStr; /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// # use evm_rpc_types::MultiRpcResult; /// let client = EvmRpcClient::builder_for_ic() - /// # // TODO XC-412: Return meaningful response - /// # .with_default_stub_response(MultiRpcResult::Consistent::>(Ok(vec![]))) + /// # .with_default_stub_response(MultiRpcResult::Consistent(Ok(vec![ + /// # evm_rpc_types::LogEntry { + /// # address: Hex20::from_str("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap(), + /// # topics: vec![ + /// # Hex32::from_str("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef").unwrap(), + /// # Hex32::from_str("0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90").unwrap(), + /// # Hex32::from_str("0x0000000000000000000000000000000aa232009084bd71a5797d089aa4edfad4").unwrap(), + /// # ], + /// # data: Hex::from_str("0x00000000000000000000000000000000000000000000000000000000cd566ae8").unwrap(), + /// # block_number: Some(0x161bd70_u64.into()), + /// # transaction_hash: Some(Hex32::from_str("0xfe5bc88d0818b66a67b0619b1b4d81bfe38029e3799c7f0eb86b33ca7dc4c811").unwrap()), + /// # transaction_index: Some(0x0_u64.into()), + /// # block_hash: Some(Hex32::from_str("0x0bbd9b12140e674cdd55e63539a25df8280a70cee3676c94d8e05fa5f868a914").unwrap()), + /// # log_index: Some(0x0_u64.into()), + /// # removed: false, + /// # } + /// # ]))) /// .build(); /// /// let request = client.get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]); /// - /// let logs = request + /// let result = request /// .with_cycles(10_000_000_000) /// .send() /// .await /// .expect_consistent(); /// - /// assert_eq!(logs, Ok(vec![])); + /// assert!(result.is_ok()); + /// assert_eq!(result.unwrap().first(), Some( + /// &alloy_rpc_types::Log { + /// inner: alloy_primitives::Log { + /// address: address!("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"), + /// data: alloy_primitives::LogData::new( + /// vec![ + /// b256!("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + /// b256!("0x000000000000000000000000000000000004444c5dc75cb358380d2e3de08a90"), + /// b256!("0x0000000000000000000000000000000aa232009084bd71a5797d089aa4edfad4"), + /// ], + /// bytes!("0x00000000000000000000000000000000000000000000000000000000cd566ae8"), + /// ).unwrap(), + /// }, + /// block_hash: Some(b256!("0x0bbd9b12140e674cdd55e63539a25df8280a70cee3676c94d8e05fa5f868a914")), + /// block_number: Some(0x161bd70_u64), + /// block_timestamp: None, + /// transaction_hash: Some(b256!("0xfe5bc88d0818b66a67b0619b1b4d81bfe38029e3799c7f0eb86b33ca7dc4c811")), + /// transaction_index: Some(0x0_u64), + /// log_index: Some(0x0_u64), + /// removed: false, + /// }, + /// )); /// # Ok(()) /// # } /// ``` diff --git a/evm_rpc_types/src/request/alloy.rs b/evm_rpc_types/src/request/alloy.rs index a1ba1882..2af22c6c 100644 --- a/evm_rpc_types/src/request/alloy.rs +++ b/evm_rpc_types/src/request/alloy.rs @@ -29,8 +29,8 @@ impl TryFrom for alloy_rpc_types::BlockNumberOrTag { } } -impl> From> for GetLogsArgs { - fn from(addresses: Vec) -> Self { +impl, S: Into> From for GetLogsArgs { + fn from(addresses: T) -> Self { Self { from_block: None, to_block: None, From 99d031cf76fd80a67a3c463b51f7ecc16ed85fc3 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 20 Aug 2025 16:35:44 +0200 Subject: [PATCH 21/57] XC-412: Move more type conversions to separate files --- evm_rpc_types/src/response/alloy.rs | 31 ++++++++++++++++++++++++++ evm_rpc_types/src/response/mod.rs | 34 +++-------------------------- evm_rpc_types/src/result/alloy.rs | 11 ++++++++++ evm_rpc_types/src/result/mod.rs | 13 ++--------- 4 files changed, 47 insertions(+), 42 deletions(-) create mode 100644 evm_rpc_types/src/response/alloy.rs create mode 100644 evm_rpc_types/src/result/alloy.rs diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs new file mode 100644 index 00000000..7f59008b --- /dev/null +++ b/evm_rpc_types/src/response/alloy.rs @@ -0,0 +1,31 @@ +use crate::{LogEntry, RpcError, ValidationError}; + +impl TryFrom for alloy_rpc_types::Log { + type Error = RpcError; + + fn try_from(entry: LogEntry) -> Result { + Ok(Self { + inner: alloy_primitives::Log { + address: alloy_primitives::Address::from(entry.address), + data: alloy_primitives::LogData::new( + entry + .topics + .into_iter() + .map(alloy_primitives::B256::from) + .collect(), + alloy_primitives::Bytes::from(entry.data), + ) + .ok_or(RpcError::ValidationError(ValidationError::Custom( + "Invalid log data".to_string(), + )))?, + }, + block_hash: entry.block_hash.map(alloy_primitives::BlockHash::from), + block_number: entry.block_number.map(u64::try_from).transpose()?, + block_timestamp: None, + transaction_hash: entry.transaction_hash.map(alloy_primitives::TxHash::from), + transaction_index: entry.transaction_index.map(u64::try_from).transpose()?, + log_index: entry.log_index.map(u64::try_from).transpose()?, + removed: entry.removed, + }) + } +} diff --git a/evm_rpc_types/src/response/mod.rs b/evm_rpc_types/src/response/mod.rs index 968d0ee9..4c605b29 100644 --- a/evm_rpc_types/src/response/mod.rs +++ b/evm_rpc_types/src/response/mod.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "alloy")] +mod alloy; + use crate::{Hex, Hex20, Hex256, Hex32, HexByte, Nat256, RpcError, ValidationError}; use candid::CandidType; use serde::{Deserialize, Serialize}; @@ -68,37 +71,6 @@ pub struct LogEntry { pub removed: bool, } -#[cfg(feature = "alloy")] -impl TryFrom for alloy_rpc_types::Log { - type Error = RpcError; - - fn try_from(entry: LogEntry) -> Result { - Ok(Self { - inner: alloy_primitives::Log { - address: alloy_primitives::Address::from(entry.address), - data: alloy_primitives::LogData::new( - entry - .topics - .into_iter() - .map(alloy_primitives::B256::from) - .collect(), - alloy_primitives::Bytes::from(entry.data), - ) - .ok_or(RpcError::ValidationError(ValidationError::Custom( - "Invalid log data".to_string(), - )))?, - }, - block_hash: entry.block_hash.map(alloy_primitives::BlockHash::from), - block_number: entry.block_number.map(u64::try_from).transpose()?, - block_timestamp: None, - transaction_hash: entry.transaction_hash.map(alloy_primitives::TxHash::from), - transaction_index: entry.transaction_index.map(u64::try_from).transpose()?, - log_index: entry.log_index.map(u64::try_from).transpose()?, - removed: entry.removed, - }) - } -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CandidType)] pub struct TransactionReceipt { /// The hash of the block containing the transaction. diff --git a/evm_rpc_types/src/result/alloy.rs b/evm_rpc_types/src/result/alloy.rs new file mode 100644 index 00000000..33655c29 --- /dev/null +++ b/evm_rpc_types/src/result/alloy.rs @@ -0,0 +1,11 @@ +use crate::{LogEntry, MultiRpcResult}; + +impl From>> for MultiRpcResult> { + fn from(result: MultiRpcResult>) -> Self { + result.and_then(|logs| { + logs.into_iter() + .map(alloy_rpc_types::Log::try_from) + .collect() + }) + } +} diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index f499c5a3..ed5659df 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -1,5 +1,7 @@ #[cfg(test)] mod tests; +#[cfg(feature = "alloy")] +mod alloy; use crate::{LogEntry, RpcService}; use candid::{CandidType, Deserialize}; @@ -208,14 +210,3 @@ impl From for LegacyRejectionCode { } } } - -#[cfg(feature = "alloy")] -impl From>> for MultiRpcResult> { - fn from(result: MultiRpcResult>) -> Self { - result.and_then(|logs| { - logs.into_iter() - .map(alloy_rpc_types::Log::try_from) - .collect() - }) - } -} From ab47bb817d2556d0b5280f205c564edec9c37dbe Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 20 Aug 2025 16:40:59 +0200 Subject: [PATCH 22/57] XC-412: Add TODO for conversion from `alloy_rpc_types::Filter` to `GetLogArgs` --- evm_rpc_types/src/request/alloy.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evm_rpc_types/src/request/alloy.rs b/evm_rpc_types/src/request/alloy.rs index 2af22c6c..c6a0bab4 100644 --- a/evm_rpc_types/src/request/alloy.rs +++ b/evm_rpc_types/src/request/alloy.rs @@ -39,3 +39,5 @@ impl, S: Into> From for GetLogsArgs { } } } + +// TODO XC-412: impl From for GetLogsArgs From 59801f27b652bba4b36f4dcae0fb466ba57ed7f7 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 09:16:41 +0200 Subject: [PATCH 23/57] XC-412: Add some alloy conversion unit tests --- evm_rpc_types/src/response/mod.rs | 2 +- evm_rpc_types/src/result/mod.rs | 3 +- evm_rpc_types/src/tests.rs | 60 ++++++++++++++++++++++--------- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/evm_rpc_types/src/response/mod.rs b/evm_rpc_types/src/response/mod.rs index 4c605b29..180953b8 100644 --- a/evm_rpc_types/src/response/mod.rs +++ b/evm_rpc_types/src/response/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "alloy")] mod alloy; -use crate::{Hex, Hex20, Hex256, Hex32, HexByte, Nat256, RpcError, ValidationError}; +use crate::{Hex, Hex20, Hex256, Hex32, HexByte, Nat256}; use candid::CandidType; use serde::{Deserialize, Serialize}; diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index ed5659df..7b8107e1 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -1,9 +1,10 @@ #[cfg(test)] mod tests; + #[cfg(feature = "alloy")] mod alloy; -use crate::{LogEntry, RpcService}; +use crate::RpcService; use candid::{CandidType, Deserialize}; use ic_error_types::RejectCode; use std::fmt::Debug; diff --git a/evm_rpc_types/src/tests.rs b/evm_rpc_types/src/tests.rs index 890cfa6e..1bccb8d8 100644 --- a/evm_rpc_types/src/tests.rs +++ b/evm_rpc_types/src/tests.rs @@ -1,8 +1,15 @@ +use crate::{Hex, Hex20, Hex256, Hex32, HexByte, Nat256}; +use candid::{CandidType, Decode, Encode, Nat}; +use num_bigint::BigUint; +use proptest::{ + prelude::{any, Strategy, TestCaseError}, + prop_assert, prop_assert_eq, proptest, +}; +use serde::de::DeserializeOwned; +use std::{ops::RangeInclusive, str::FromStr}; + mod nat256 { - use crate::Nat256; - use candid::{Decode, Encode, Nat}; - use num_bigint::BigUint; - use proptest::{arbitrary::any, prelude::Strategy, proptest}; + use super::*; proptest! { #[test] @@ -68,13 +75,7 @@ mod nat256 { } mod hex_string { - use crate::{Hex, Hex20, Hex256, Hex32, HexByte}; - use candid::{CandidType, Decode, Encode}; - use proptest::prelude::{Strategy, TestCaseError}; - use proptest::{prop_assert, prop_assert_eq, proptest}; - use serde::de::DeserializeOwned; - use std::ops::RangeInclusive; - use std::str::FromStr; + use super::*; proptest! { #[test] @@ -179,12 +180,37 @@ mod hex_string { ); Ok(()) } +} + +#[cfg(feature = "alloy")] +mod alloy_conversion_tests { + use super::*; + use alloy_primitives::{Address, Bytes, B256}; + + proptest! { + #[test] + fn should_convert_to_and_from_alloy(hex20 in arb_hex20(), hex32 in arb_hex32(), hex in arb_hex()) { + prop_assert_eq!(hex20.clone(), Hex20::from(Address::from(hex20))); + prop_assert_eq!(hex32.clone(), Hex32::from(B256::from(hex32))); + prop_assert_eq!(hex.clone(), Hex::from(Bytes::from(hex))); + } + } + + fn arb_hex20() -> impl Strategy { + arb_var_len_hex_string(20..=20_usize).prop_map(|s| Hex20::from_str(s.as_str()).unwrap()) + } - fn arb_var_len_hex_string( - num_bytes_range: RangeInclusive, - ) -> impl Strategy { - num_bytes_range.prop_flat_map(|num_bytes| { - proptest::string::string_regex(&format!("0x[0-9a-fA-F]{{{}}}", 2 * num_bytes)).unwrap() - }) + fn arb_hex32() -> impl Strategy { + arb_var_len_hex_string(32..=32_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) } + + fn arb_hex() -> impl Strategy { + arb_var_len_hex_string(0..=100_usize).prop_map(|s| Hex::from_str(s.as_str()).unwrap()) + } +} + +fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { + num_bytes_range.prop_flat_map(|num_bytes| { + proptest::string::string_regex(&format!("0x[0-9a-fA-F]{{{}}}", 2 * num_bytes)).unwrap() + }) } From ccb0f5d52b69cb4ff24e9a9165d839c62a3beb29 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 10:51:28 +0200 Subject: [PATCH 24/57] XC-412: Add unit tests for `and_then` method --- evm_rpc_types/src/result/mod.rs | 26 ++++- evm_rpc_types/src/result/tests.rs | 159 ++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 2 deletions(-) diff --git a/evm_rpc_types/src/result/mod.rs b/evm_rpc_types/src/result/mod.rs index 7b8107e1..4c3ec7d8 100644 --- a/evm_rpc_types/src/result/mod.rs +++ b/evm_rpc_types/src/result/mod.rs @@ -21,7 +21,7 @@ pub enum MultiRpcResult { impl MultiRpcResult { /// Maps a [`MultiRpcResult`] containing values of type `T` to a [`MultiRpcResult`] containing /// values of type `R` by an infallible map. - pub fn map(self, mut f: impl FnMut(T) -> R) -> MultiRpcResult { + pub fn map(self, mut f: impl FnMut(T) -> R) -> MultiRpcResult { match self { MultiRpcResult::Consistent(result) => MultiRpcResult::Consistent(result.map(f)), MultiRpcResult::Inconsistent(results) => MultiRpcResult::Inconsistent( @@ -43,7 +43,10 @@ impl MultiRpcResult { /// Maps a [`MultiRpcResult`] containing values of type `T` to a [`MultiRpcResult`] containing /// values of type `R` by a fallible map. - pub fn and_then(self, mut f: impl FnMut(T) -> RpcResult) -> MultiRpcResult { + pub fn and_then( + self, + mut f: impl FnMut(T) -> RpcResult, + ) -> MultiRpcResult { match self { MultiRpcResult::Consistent(result) => MultiRpcResult::Consistent(result.and_then(f)), MultiRpcResult::Inconsistent(results) => MultiRpcResult::Inconsistent( @@ -64,6 +67,25 @@ impl MultiRpcResult { } } +impl MultiRpcResult { + /// Collapses an [`Inconsistent`](MultiRpcResult::Inconsistent) into + /// [`Consistent`](MultiRpcResult::Consistent) if all results match. + /// Otherwise, returns the value unchanged. + pub fn collapse(self) -> MultiRpcResult { + match self { + MultiRpcResult::Consistent(r) => MultiRpcResult::Consistent(r), + MultiRpcResult::Inconsistent(v) => { + if let Some((_, first)) = v.first() { + if v.iter().all(|(_, result)| result == first) { + return MultiRpcResult::Consistent(first.clone()); + } + } + MultiRpcResult::Inconsistent(v) + } + } + } +} + impl MultiRpcResult { pub fn expect_consistent(self) -> RpcResult { match self { diff --git a/evm_rpc_types/src/result/tests.rs b/evm_rpc_types/src/result/tests.rs index 0f38bd43..5b44c6ff 100644 --- a/evm_rpc_types/src/result/tests.rs +++ b/evm_rpc_types/src/result/tests.rs @@ -57,4 +57,163 @@ fn test_multi_rpc_result_map() { ) ]) ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(2)), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(3)) + ]) + .map(|n| n / 2), + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(1)), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(1)), + ]) + ); +} + +#[test] +fn test_multi_rpc_result_and_then() { + let err = RpcError::ProviderError(ProviderError::ProviderNotFound); + assert_eq!( + MultiRpcResult::Consistent(Ok(5)).and_then(|n| Ok(n + 1)), + MultiRpcResult::Consistent(Ok(6)) + ); + assert_eq!( + MultiRpcResult::Consistent(Err(err.clone())).and_then(|()| unreachable!()), + MultiRpcResult::Consistent::<()>(Err(err.clone())) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![( + RpcService::EthMainnet(EthMainnetService::Ankr), + Ok(5) + )]) + .and_then(|n| Ok(n + 1)), + MultiRpcResult::Inconsistent(vec![( + RpcService::EthMainnet(EthMainnetService::Ankr), + Ok(6) + )]) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(5)), + ( + RpcService::EthMainnet(EthMainnetService::Cloudflare), + Ok(10) + ) + ]) + .and_then(|n| Ok(n + 1)), + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(6)), + ( + RpcService::EthMainnet(EthMainnetService::Cloudflare), + Ok(11) + ) + ]) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(5)), + ( + RpcService::EthMainnet(EthMainnetService::PublicNode), + Err(err.clone()) + ) + ]) + .and_then(|n| Ok(n + 1)), + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(6)), + ( + RpcService::EthMainnet(EthMainnetService::PublicNode), + Err(err.clone()) + ) + ]) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(1)), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(2)) + ]) + .and_then(|n| if n % 2 == 0 { Ok(n) } else { Err(err.clone()) }), + MultiRpcResult::Inconsistent(vec![ + ( + RpcService::EthMainnet(EthMainnetService::Ankr), + Err(err.clone()) + ), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(2)), + ]) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(1)), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(3)) + ]) + .and_then(|n| if n % 2 == 0 { Ok(n) } else { Err(err.clone()) }), + MultiRpcResult::Inconsistent(vec![ + ( + RpcService::EthMainnet(EthMainnetService::Ankr), + Err(err.clone()) + ), + ( + RpcService::EthMainnet(EthMainnetService::Llama), + Err(err.clone()) + ) + ]) + ); +} + +#[test] +fn test_multi_rpc_result_collapse() { + let err = RpcError::ProviderError(ProviderError::ProviderNotFound); + assert_eq!( + MultiRpcResult::Consistent(Ok(5)).collapse(), + MultiRpcResult::Consistent(Ok(5)) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(2)), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(3)) + ]) + .collapse(), + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(2)), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(3)) + ]) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + ( + RpcService::EthMainnet(EthMainnetService::Ankr), + Err(err.clone()) + ), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(2)) + ]) + .collapse(), + MultiRpcResult::Inconsistent(vec![ + ( + RpcService::EthMainnet(EthMainnetService::Ankr), + Err(err.clone()) + ), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(2)) + ]) + ); + assert_eq!( + MultiRpcResult::Inconsistent(vec![ + (RpcService::EthMainnet(EthMainnetService::Ankr), Ok(2)), + (RpcService::EthMainnet(EthMainnetService::Llama), Ok(2)) + ]) + .collapse(), + MultiRpcResult::Consistent(Ok(2)) + ); + assert_eq!( + MultiRpcResult::Inconsistent::<()>(vec![ + ( + RpcService::EthMainnet(EthMainnetService::Ankr), + Err(err.clone()) + ), + ( + RpcService::EthMainnet(EthMainnetService::Llama), + Err(err.clone()) + ) + ]) + .collapse(), + MultiRpcResult::Consistent::<()>(Err(err.clone())) + ); } From b1529272503cbf2aa0ed2abbd317e2ff7607dec4 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 11:04:13 +0200 Subject: [PATCH 25/57] XC-412: Formatting --- evm_rpc_client/src/request/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index aed767fa..47c36a1f 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -1,6 +1,8 @@ use crate::{EvmRpcClient, Runtime}; use candid::CandidType; -use evm_rpc_types::{BlockTag, GetLogsArgs, GetLogsRpcConfig, Hex20, Hex32, MultiRpcResult, RpcConfig, RpcServices}; +use evm_rpc_types::{ + BlockTag, GetLogsArgs, GetLogsRpcConfig, Hex20, Hex32, MultiRpcResult, RpcConfig, RpcServices, +}; use ic_error_types::RejectCode; use serde::de::DeserializeOwned; use std::fmt::{Debug, Formatter}; From 5de395d7c45cc4bf28b0443cf76a1847ad952983 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 12:12:27 +0200 Subject: [PATCH 26/57] XC-412: Add more unit tests --- Cargo.lock | 1 + evm_rpc_types/Cargo.toml | 1 + evm_rpc_types/src/response/mod.rs | 3 + evm_rpc_types/src/response/test.rs | 91 ++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 evm_rpc_types/src/response/test.rs diff --git a/Cargo.lock b/Cargo.lock index a86f5e38..4985af4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1563,6 +1563,7 @@ dependencies = [ "num-bigint", "proptest", "serde", + "serde_json", "strum 0.27.2", "thiserror 2.0.16", "url", diff --git a/evm_rpc_types/Cargo.toml b/evm_rpc_types/Cargo.toml index e2bf2063..879e96b9 100644 --- a/evm_rpc_types/Cargo.toml +++ b/evm_rpc_types/Cargo.toml @@ -26,6 +26,7 @@ url = { workspace = true } [dev-dependencies] proptest = { workspace = true } +serde_json = { workspace = true } [features] default = ["alloy"] diff --git a/evm_rpc_types/src/response/mod.rs b/evm_rpc_types/src/response/mod.rs index 180953b8..b184fbf2 100644 --- a/evm_rpc_types/src/response/mod.rs +++ b/evm_rpc_types/src/response/mod.rs @@ -1,3 +1,6 @@ +#[cfg(test)] +mod test; + #[cfg(feature = "alloy")] mod alloy; diff --git a/evm_rpc_types/src/response/test.rs b/evm_rpc_types/src/response/test.rs new file mode 100644 index 00000000..a1674237 --- /dev/null +++ b/evm_rpc_types/src/response/test.rs @@ -0,0 +1,91 @@ +use crate::{Hex, Hex20, Hex32}; +use proptest::prelude::Strategy; +use proptest::proptest; +use std::ops::RangeInclusive; + +#[cfg(feature = "alloy")] +mod alloy_conversion_tests { + use super::*; + use crate::{LogEntry, Nat256}; + use num_bigint::BigUint; + use proptest::arbitrary::any; + use proptest::option; + use serde_json::Value; + use std::str::FromStr; + + proptest! { + #[test] + fn should_convert_from_alloy(entry in arb_log_entry()) { + // Convert a number serialized as a hexadecimal string into an array of u32 digits + fn hex_to_u32_digits(serialized: &mut Value, field: &str) { + if let Some(Value::String(hex)) = serialized.get(field) { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + let digits = BigUint::parse_bytes(hex.as_bytes(), 16).unwrap().to_u32_digits(); + serialized[field] = digits.into(); + } + } + + let serialized = serde_json::to_value(&entry).unwrap(); + + let mut alloy_serialized = serde_json::to_value(&alloy_rpc_types::Log::try_from(entry.clone()).unwrap()).unwrap(); + hex_to_u32_digits(&mut alloy_serialized, "transactionIndex"); + hex_to_u32_digits(&mut alloy_serialized, "logIndex"); + hex_to_u32_digits(&mut alloy_serialized, "blockNumber"); + + assert_eq!(serialized, alloy_serialized); + } + } + + fn arb_log_entry() -> impl Strategy { + ( + arb_hex20(), + arb_hex(), + option::of(any::().prop_map(Nat256::from)), + option::of(arb_hex32()), + option::of(any::().prop_map(Nat256::from)), + option::of(arb_hex32()), + option::of(any::().prop_map(Nat256::from)), + any::(), + ) + .prop_map( + |( + address, + data, + block_number, + transaction_hash, + transaction_index, + block_hash, + log_index, + removed, + )| LogEntry { + address, + topics: vec![], + data, + block_number, + transaction_hash, + transaction_index, + block_hash, + log_index, + removed, + }, + ) + } + + fn arb_hex20() -> impl Strategy { + arb_var_len_hex_string(20..=20_usize).prop_map(|s| Hex20::from_str(s.as_str()).unwrap()) + } + + fn arb_hex32() -> impl Strategy { + arb_var_len_hex_string(32..=32_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) + } + + fn arb_hex() -> impl Strategy { + arb_var_len_hex_string(0..=100_usize).prop_map(|s| Hex::from_str(s.as_str()).unwrap()) + } +} + +fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { + num_bytes_range.prop_flat_map(|num_bytes| { + proptest::string::string_regex(&format!("0x[0-9a-fA-F]{{{}}}", 2 * num_bytes)).unwrap() + }) +} From 93874e6f6fe893706309a44ec6e93d7a0c95b8a6 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 13:08:58 +0200 Subject: [PATCH 27/57] XC-412: Add `#[allow(missing_docs)]` to `pocket_ic` --- evm_rpc_client/src/runtime/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/evm_rpc_client/src/runtime/mod.rs b/evm_rpc_client/src/runtime/mod.rs index fc398524..8a3513ba 100644 --- a/evm_rpc_client/src/runtime/mod.rs +++ b/evm_rpc_client/src/runtime/mod.rs @@ -1,4 +1,5 @@ #[cfg(feature = "pocket-ic")] +#[allow(missing_docs)] mod pocket_ic; use async_trait::async_trait; From 9802e56bb6383d4d73bf4acfe329f73e8a855846 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 13:32:24 +0200 Subject: [PATCH 28/57] XC-412: change `forbit` to `deny` --- evm_rpc_client/src/lib.rs | 10 +++++++++- evm_rpc_client/src/runtime/mod.rs | 1 - evm_rpc_client/src/runtime/pocket_ic/mock.rs | 7 +++++-- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 15 ++++++++++++--- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index d94f7f09..2320329f 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -179,7 +179,7 @@ //! ``` #![forbid(unsafe_code)] -#![forbid(missing_docs)] +#![deny(missing_docs)] #[cfg(not(target_arch = "wasm32"))] pub mod fixtures; @@ -200,6 +200,7 @@ pub use runtime::{ pub use runtime::{IcRuntime, Runtime}; use serde::de::DeserializeOwned; use std::sync::Arc; +use ic_cdk::api::management_canister::main::CanisterId; /// The principal identifying the productive EVM RPC canister under NNS control. /// @@ -217,6 +218,13 @@ pub struct EvmRpcClient { config: Arc>, } +impl EvmRpcClient { + /// Creates a [`ClientBuilder`] to configure a [`EvmRpcClient`]. + pub fn builder(runtime: R, evm_rpc_canister: Principal) -> ClientBuilder { + ClientBuilder::new(runtime, evm_rpc_canister) + } +} + impl Clone for EvmRpcClient { fn clone(&self) -> Self { Self { diff --git a/evm_rpc_client/src/runtime/mod.rs b/evm_rpc_client/src/runtime/mod.rs index 8a3513ba..fc398524 100644 --- a/evm_rpc_client/src/runtime/mod.rs +++ b/evm_rpc_client/src/runtime/mod.rs @@ -1,5 +1,4 @@ #[cfg(feature = "pocket-ic")] -#[allow(missing_docs)] mod pocket_ic; use async_trait::async_trait; diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 7a9ba499..282ed6b1 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -80,10 +80,13 @@ pub trait RepeatExt { fn times(self) -> MockOutcallRepeat; } -impl RepeatExt for usize { +impl RepeatExt for T +where + T: Into, +{ fn times(self) -> MockOutcallRepeat { assert!(self > 1, "Repeat count must be greater than 1"); - MockOutcallRepeat::Times(self) + MockOutcallRepeat::Times(self.into()) } } diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index 4b87ba97..11c2f6c7 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -1,3 +1,4 @@ +#[allow(missing_docs)] mod mock; use crate::{ClientBuilder, Runtime}; @@ -21,13 +22,20 @@ use std::time::Duration; const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; const MAX_TICKS: usize = 10; +/// Runtime used in tests with PocketIC. pub struct PocketIcRuntime<'a> { + /// Main entry point for interacting with PocketIC. pub env: &'a PocketIc, + /// Default caller [`Principal`] when making inter-canister calls. pub caller: Principal, - // This field is in a `Mutex` so we can use interior mutability to pop the next element from - // the queue (i.e., perform a mutable operation) within the `Runtime::update_call` method which - // takes an immutable reference to `self`. + /// Queue that holds the mocked HTTP outcall requests and responses. + /// + /// This field is in a [`Mutex`] so we can use interior mutability to pop the next element from + /// the queue (i.e., perform a mutable operation) within the [`Runtime::update_call`] method which + /// takes an immutable reference to `self`. Furthermore, this has to be thread-safe to be used + /// in multithreaded [`tokio`] tests. pub mocks: Mutex, + /// Default controller [`Principal`] when making inter-canister calls. pub controller: Principal, } @@ -180,6 +188,7 @@ async fn tick_until_http_request(env: &PocketIc) -> Vec { } impl ClientBuilder> { + /// Add a mocked outcall to the queue. pub fn mock(self, outcall: impl Into, repeat: MockOutcallRepeat) -> Self { self.with_runtime(|r| { r.mocks.lock().unwrap().push(outcall, repeat); From 4e62c83a4808df34f1db18ecacc179db49eb1a68 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 13:53:26 +0200 Subject: [PATCH 29/57] XC-412: Formatting --- evm_rpc_client/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 2320329f..f562f26c 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -200,7 +200,6 @@ pub use runtime::{ pub use runtime::{IcRuntime, Runtime}; use serde::de::DeserializeOwned; use std::sync::Arc; -use ic_cdk::api::management_canister::main::CanisterId; /// The principal identifying the productive EVM RPC canister under NNS control. /// From 52e8b63859955398181fef2c89cd90097f3c686b Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 14:02:30 +0200 Subject: [PATCH 30/57] XC-412: Fix `RepeatExt` implementation --- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 282ed6b1..7a9ba499 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -80,13 +80,10 @@ pub trait RepeatExt { fn times(self) -> MockOutcallRepeat; } -impl RepeatExt for T -where - T: Into, -{ +impl RepeatExt for usize { fn times(self) -> MockOutcallRepeat { assert!(self > 1, "Repeat count must be greater than 1"); - MockOutcallRepeat::Times(self.into()) + MockOutcallRepeat::Times(self) } } From f3752e8a85385291fe72faea8af7466d88b4ef99 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 21 Aug 2025 14:10:14 +0200 Subject: [PATCH 31/57] XC-412: Fix rustdoc --- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index 11c2f6c7..c4f874de 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -33,7 +33,7 @@ pub struct PocketIcRuntime<'a> { /// This field is in a [`Mutex`] so we can use interior mutability to pop the next element from /// the queue (i.e., perform a mutable operation) within the [`Runtime::update_call`] method which /// takes an immutable reference to `self`. Furthermore, this has to be thread-safe to be used - /// in multithreaded [`tokio`] tests. + /// in multithreaded tests. pub mocks: Mutex, /// Default controller [`Principal`] when making inter-canister calls. pub controller: Principal, From 3bc9ab2e764c9dd080a11c414fd11d4fc8f40c58 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 22 Aug 2025 08:15:19 +0200 Subject: [PATCH 32/57] Add correct TODO link --- evm_rpc_client/src/lib.rs | 8 + evm_rpc_client/src/runtime/pocket_ic/mock.rs | 7 +- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 8 +- tests/tests.rs | 204 ++++++++----------- 4 files changed, 110 insertions(+), 117 deletions(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index f562f26c..4355bb46 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -255,6 +255,14 @@ pub struct ClientBuilder { config: ClientConfig, } +impl Clone for ClientBuilder { + fn clone(&self) -> Self { + ClientBuilder { + config: self.config.clone(), + } + } +} + impl ClientBuilder { fn new(runtime: R, evm_rpc_canister: Principal) -> Self { Self { diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 7a9ba499..c81949a2 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -1,4 +1,4 @@ -use canhttp::http::json::JsonRpcRequest; +use canhttp::http::json::{Id, JsonRpcRequest}; use ic_cdk::api::call::RejectionCode; use pocket_ic::common::rest::{ CanisterHttpHeader, CanisterHttpMethod, CanisterHttpReject, CanisterHttpReply, @@ -208,6 +208,11 @@ impl MockOutcallBuilder { self } + pub fn with_request_id(mut self, id: Id) -> Self { + self.0.request_body = self.0.request_body.map(|body| body.with_id(id)); + self + } + pub fn build(self) -> MockOutcall { self.0 } diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index c4f874de..3813ae21 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -188,11 +188,17 @@ async fn tick_until_http_request(env: &PocketIc) -> Vec { } impl ClientBuilder> { - /// Add a mocked outcall to the queue. pub fn mock(self, outcall: impl Into, repeat: MockOutcallRepeat) -> Self { self.with_runtime(|r| { r.mocks.lock().unwrap().push(outcall, repeat); r }) } + + pub fn mock_sequence(mut self, outcalls: impl IntoIterator>) -> Self { + for outcall in outcalls.into_iter() { + self = self.mock(outcall, once()); + } + self + } } diff --git a/tests/tests.rs b/tests/tests.rs index 04a51f16..cea2d651 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -5,6 +5,7 @@ use alloy_primitives::{address, b256, bytes}; use alloy_rpc_types::BlockNumberOrTag; use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat, Principal}; +use canhttp::http::json::Id; use canlog::{Log, LogEntry}; use evm_rpc::constants::DEFAULT_MAX_RESPONSE_BYTES; use evm_rpc::logs::Priority; @@ -36,7 +37,7 @@ use pocket_ic::{nonblocking, ErrorCode, PocketIc, PocketIcBuilder, RejectRespons use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::json; use std::sync::{Arc, Mutex}; -use std::{marker::PhantomData, mem, str::FromStr, time::Duration}; +use std::{marker::PhantomData, str::FromStr, time::Duration}; const DEFAULT_CALLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]); const DEFAULT_CONTROLLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x02]); @@ -910,9 +911,9 @@ async fn eth_get_logs_should_succeed() { } } -#[test] -fn eth_get_logs_should_fail_when_block_range_too_large() { - let setup = EvmRpcSetup::new().mock_api_keys(); +#[tokio::test] +async fn eth_get_logs_should_fail_when_block_range_too_large() { + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; let error_msg_regex = regex::Regex::new("Requested [0-9_]+ blocks; limited to [0-9_]+").unwrap(); @@ -920,34 +921,29 @@ fn eth_get_logs_should_fail_when_block_range_too_large() { for (config, from_block, to_block) in [ // default block range ( - None, - Some(BlockTag::Number(0_u8.into())), - Some(BlockTag::Number(501_u16.into())), + GetLogsRpcConfig::default(), + BlockTag::Number(0_u8.into()), + BlockTag::Number(501_u16.into()), ), // large block range ( - Some(GetLogsRpcConfig { + GetLogsRpcConfig { max_block_range: Some(1_000), ..Default::default() - }), - Some(BlockTag::Number(0_u8.into())), - Some(BlockTag::Number(1001_u16.into())), + }, + BlockTag::Number(0_u8.into()), + BlockTag::Number(1001_u16.into()), ), ] { - let response = setup - .eth_get_logs( - source.clone(), - config, - evm_rpc_types::GetLogsArgs { - addresses: vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7" - .parse() - .unwrap()], - from_block, - to_block, - topics: None, - }, - ) - .wait() + let client = setup.client().with_rpc_sources(source.clone()).build(); + + let response = client + .get_logs(vec![address!("0xdAC17F958D2ee523a2206206994597C13D831ec7")]) + .with_from_block(from_block) + .with_to_block(to_block) + .with_rpc_config(config) + .send() + .await .expect_consistent() .unwrap_err(); @@ -1991,32 +1987,26 @@ fn candid_rpc_should_recognize_rate_limit() { ); } -#[test] -fn should_use_custom_response_size_estimate() { - let setup = EvmRpcSetup::new().mock_api_keys(); +#[tokio::test] +async fn should_use_custom_response_size_estimate() { + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; let max_response_bytes = 1234; let expected_response = r#"{"id":0,"jsonrpc":"2.0","result":[{"address":"0xdac17f958d2ee523a2206206994597c13d831ec7","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43","0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2"],"data":"0x000000000000000000000000000000000000000000000000000000003b9c6433","blockNumber":"0x11dc77e","transactionHash":"0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678","transactionIndex":"0x65","blockHash":"0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629","logIndex":"0xe8","removed":false}]}"#; - let response = setup - .eth_get_logs( - RpcServices::EthMainnet(Some(vec![EthMainnetService::Cloudflare])), - Some(evm_rpc_types::GetLogsRpcConfig { - response_size_estimate: Some(max_response_bytes), - ..Default::default() - }), - evm_rpc_types::GetLogsArgs { - addresses: vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7" - .parse() - .unwrap()], - from_block: None, - to_block: None, - topics: None, - }, - ) - .mock_http_once( - MockOutcallBuilder::new(200, expected_response) - .with_max_response_bytes(max_response_bytes), + let client = setup + .client() + .mock( + evm_rpc_client::MockOutcallBuilder::new_success(expected_response), + once(), ) - .wait() + .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ + EthMainnetService::Cloudflare, + ]))) + .with_response_size_estimate(max_response_bytes) + .build(); + let response = client + .get_logs(vec![address!("0xdAC17F958D2ee523a2206206994597C13D831ec7")]) + .send() + .await .expect_consistent(); assert_matches!(response, Ok(_)); } @@ -2402,43 +2392,37 @@ fn should_retrieve_logs() { .contains("Updating API keys")); } -#[test] -fn should_retry_when_response_too_large() { - let setup = EvmRpcSetup::new().mock_api_keys(); +#[tokio::test] +async fn should_retry_when_response_too_large() { + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + + let rpc_services = RpcServices::EthMainnet(Some(vec![EthMainnetService::Cloudflare])); + // around 600 bytes per log // we need at least 3334 logs to reach the 2MB limit - let large_amount_of_logs: [serde_json::Value; 12] = - json_rpc_sequential_id(multi_logs_for_single_transaction(3_500)); - let mut mocks = MockOutcallBuilder::new_array(200, large_amount_of_logs); - let response = setup - .eth_get_logs( - RpcServices::EthMainnet(Some(vec![EthMainnetService::Cloudflare])), - Some(evm_rpc_types::GetLogsRpcConfig { - response_size_estimate: Some(1), - ..Default::default() - }), - evm_rpc_types::GetLogsArgs { - addresses: vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7" - .parse() - .unwrap()], - from_block: None, - to_block: None, - topics: None, - }, - ) - .mock_http_once(mem::take(&mut mocks[0]).with_max_response_bytes(1)) - .mock_http_once(mem::take(&mut mocks[1]).with_max_response_bytes(1024 << 1)) - .mock_http_once(mem::take(&mut mocks[2]).with_max_response_bytes(1024 << 2)) - .mock_http_once(mem::take(&mut mocks[3]).with_max_response_bytes(1024 << 3)) - .mock_http_once(mem::take(&mut mocks[4]).with_max_response_bytes(1024 << 4)) - .mock_http_once(mem::take(&mut mocks[5]).with_max_response_bytes(1024 << 5)) - .mock_http_once(mem::take(&mut mocks[6]).with_max_response_bytes(1024 << 6)) - .mock_http_once(mem::take(&mut mocks[7]).with_max_response_bytes(1024 << 7)) - .mock_http_once(mem::take(&mut mocks[8]).with_max_response_bytes(1024 << 8)) - .mock_http_once(mem::take(&mut mocks[9]).with_max_response_bytes(1024 << 9)) - .mock_http_once(mem::take(&mut mocks[10]).with_max_response_bytes(1024 << 10)) - .mock_http_once(mem::take(&mut mocks[11]).with_max_response_bytes(2_000_000)) - .wait() + let mocks = std::iter::once(1_u64) + .chain((1..=10).map(|i| 1024_u64 << i)) + .chain(2_000_000) + .enumerate() + .map(|(id, max_response_bytes)| { + evm_rpc_client::MockOutcallBuilder::new_success(multi_logs_for_single_transaction( + 3_500, + )) + .with_max_response_bytes(max_response_bytes) + .with_request_id(Id::Number(id)) + }); + + let client = setup + .client() + .with_rpc_sources(rpc_services.clone()) + .with_response_size_estimate(1) + .mock_sequence(mocks) + .build(); + + let response = client + .get_logs(vec![address!("0xdAC17F958D2ee523a2206206994597C13D831ec7")]) + .send() + .await .expect_consistent(); assert_matches!( @@ -2447,38 +2431,28 @@ fn should_retry_when_response_too_large() { if code == LegacyRejectionCode::SysFatal && message.contains("body exceeds size limit") ); - let mut large_amount_of_logs: [serde_json::Value; 11] = - json_rpc_sequential_id(multi_logs_for_single_transaction(1_000)); - add_offset_json_rpc_id(large_amount_of_logs.as_mut_slice(), 12); - let mut mocks = MockOutcallBuilder::new_array(200, large_amount_of_logs); - let response = setup - .eth_get_logs( - RpcServices::EthMainnet(Some(vec![EthMainnetService::Cloudflare])), - Some(evm_rpc_types::GetLogsRpcConfig { - response_size_estimate: Some(1), - ..Default::default() - }), - evm_rpc_types::GetLogsArgs { - addresses: vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7" - .parse() - .unwrap()], - from_block: None, - to_block: None, - topics: None, - }, - ) - .mock_http_once(mem::take(&mut mocks[0]).with_max_response_bytes(1)) - .mock_http_once(mem::take(&mut mocks[1]).with_max_response_bytes(1024 << 1)) - .mock_http_once(mem::take(&mut mocks[2]).with_max_response_bytes(1024 << 2)) - .mock_http_once(mem::take(&mut mocks[3]).with_max_response_bytes(1024 << 3)) - .mock_http_once(mem::take(&mut mocks[4]).with_max_response_bytes(1024 << 4)) - .mock_http_once(mem::take(&mut mocks[5]).with_max_response_bytes(1024 << 5)) - .mock_http_once(mem::take(&mut mocks[6]).with_max_response_bytes(1024 << 6)) - .mock_http_once(mem::take(&mut mocks[7]).with_max_response_bytes(1024 << 7)) - .mock_http_once(mem::take(&mut mocks[8]).with_max_response_bytes(1024 << 8)) - .mock_http_once(mem::take(&mut mocks[9]).with_max_response_bytes(1024 << 9)) - .mock_http_once(mem::take(&mut mocks[10]).with_max_response_bytes(1024 << 10)) - .wait() + let mocks = std::iter::once(1_u64) + .chain((1..=10).map(|i| 1024_u64 << i)) + .enumerate() + .map(|(id, max_response_bytes)| { + evm_rpc_client::MockOutcallBuilder::new_success(multi_logs_for_single_transaction( + 1_000, + )) + .with_max_response_bytes(max_response_bytes) + .with_request_id(Id::Number(id + 12)) + }); + + let client = setup + .client() + .with_rpc_sources(rpc_services.clone()) + .with_response_size_estimate(1) + .mock_sequence(mocks) + .build(); + + let response = client + .get_logs(vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7"]) + .send() + .await .expect_consistent(); assert_matches!( From 9d1c8b10203ac16bd931f67b7f6be497e4bf8383 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 22 Aug 2025 11:18:25 +0200 Subject: [PATCH 33/57] XC-412: Refactor remaining `eth_getLogs` tests --- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 14 ++++++++++-- tests/tests.rs | 25 ++++++--------------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index 3813ae21..4b78d0d6 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -188,6 +188,7 @@ async fn tick_until_http_request(env: &PocketIc) -> Vec { } impl ClientBuilder> { + /// Add a mock outcall to the queue. pub fn mock(self, outcall: impl Into, repeat: MockOutcallRepeat) -> Self { self.with_runtime(|r| { r.mocks.lock().unwrap().push(outcall, repeat); @@ -195,9 +196,18 @@ impl ClientBuilder> { }) } - pub fn mock_sequence(mut self, outcalls: impl IntoIterator>) -> Self { + /// Add a mock outcall to the queue, executed once. + pub fn mock_once(self, outcall: impl Into) -> Self { + self.mock(outcall.into(), once()) + } + + /// Add a seuqence of mock outcalls to the queue, each executed once. + pub fn mock_sequence( + mut self, + outcalls: impl IntoIterator>, + ) -> Self { for outcall in outcalls.into_iter() { - self = self.mock(outcall, once()); + self = self.mock_once(outcall); } self } diff --git a/tests/tests.rs b/tests/tests.rs index cea2d651..978ad2c6 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -14,7 +14,7 @@ use evm_rpc::{ providers::PROVIDERS, types::{Metrics, ProviderId, RpcAccess, RpcMethod}, }; -use evm_rpc_client::{once, ClientBuilder, EvmRpcClient, MockOutcallQueue, PocketIcRuntime}; +use evm_rpc_client::{ClientBuilder, EvmRpcClient, MockOutcallQueue, PocketIcRuntime}; use evm_rpc_types::{ BlockTag, ConsensusStrategy, EthMainnetService, EthSepoliaService, GetLogsRpcConfig, Hex, Hex20, Hex32, HttpOutcallError, InstallArgs, JsonRpcError, LegacyRejectionCode, MultiRpcResult, @@ -381,15 +381,6 @@ impl EvmRpcSetup { ) } - pub fn eth_get_logs( - &self, - source: RpcServices, - config: Option, - args: evm_rpc_types::GetLogsArgs, - ) -> CallFlow>> { - self.call_update("eth_getLogs", Encode!(&source, &config, &args).unwrap()) - } - pub fn eth_get_block_by_number( &self, source: RpcServices, @@ -890,10 +881,9 @@ async fn eth_get_logs_should_succeed() { let client = setup .client() .with_rpc_sources(source.clone()) - .mock( - evm_rpc_client::MockOutcallBuilder::new_success(responses.clone()), - once(), - ) + .mock_once(evm_rpc_client::MockOutcallBuilder::new_success( + responses.clone(), + )) .build(); let response = client .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) @@ -1994,10 +1984,9 @@ async fn should_use_custom_response_size_estimate() { let expected_response = r#"{"id":0,"jsonrpc":"2.0","result":[{"address":"0xdac17f958d2ee523a2206206994597c13d831ec7","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43","0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2"],"data":"0x000000000000000000000000000000000000000000000000000000003b9c6433","blockNumber":"0x11dc77e","transactionHash":"0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678","transactionIndex":"0x65","blockHash":"0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629","logIndex":"0xe8","removed":false}]}"#; let client = setup .client() - .mock( - evm_rpc_client::MockOutcallBuilder::new_success(expected_response), - once(), - ) + .mock_once(evm_rpc_client::MockOutcallBuilder::new_success( + expected_response, + )) .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ EthMainnetService::Cloudflare, ]))) From 7f4dbd5ad02f25aadb163ae1fa9f91c22341ddfb Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 22 Aug 2025 11:28:10 +0200 Subject: [PATCH 34/57] XC-412: Clippy --- tests/tests.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 978ad2c6..6e7fca75 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1984,9 +1984,9 @@ async fn should_use_custom_response_size_estimate() { let expected_response = r#"{"id":0,"jsonrpc":"2.0","result":[{"address":"0xdac17f958d2ee523a2206206994597c13d831ec7","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43","0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2"],"data":"0x000000000000000000000000000000000000000000000000000000003b9c6433","blockNumber":"0x11dc77e","transactionHash":"0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678","transactionIndex":"0x65","blockHash":"0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629","logIndex":"0xe8","removed":false}]}"#; let client = setup .client() - .mock_once(evm_rpc_client::MockOutcallBuilder::new_success( + .mock_once(evm_rpc_client::MockOutcallBuilder::new_success([ expected_response, - )) + ])) .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ EthMainnetService::Cloudflare, ]))) @@ -2391,14 +2391,14 @@ async fn should_retry_when_response_too_large() { // we need at least 3334 logs to reach the 2MB limit let mocks = std::iter::once(1_u64) .chain((1..=10).map(|i| 1024_u64 << i)) - .chain(2_000_000) + .chain(std::iter::once(2_000_000_u64)) .enumerate() .map(|(id, max_response_bytes)| { - evm_rpc_client::MockOutcallBuilder::new_success(multi_logs_for_single_transaction( + evm_rpc_client::MockOutcallBuilder::new_success([multi_logs_for_single_transaction( 3_500, - )) + )]) .with_max_response_bytes(max_response_bytes) - .with_request_id(Id::Number(id)) + .with_request_id(Id::from(id as u64)) }); let client = setup @@ -2424,11 +2424,11 @@ async fn should_retry_when_response_too_large() { .chain((1..=10).map(|i| 1024_u64 << i)) .enumerate() .map(|(id, max_response_bytes)| { - evm_rpc_client::MockOutcallBuilder::new_success(multi_logs_for_single_transaction( + evm_rpc_client::MockOutcallBuilder::new_success([multi_logs_for_single_transaction( 1_000, - )) + )]) .with_max_response_bytes(max_response_bytes) - .with_request_id(Id::Number(id + 12)) + .with_request_id(Id::from(id as u64 + 12)) }); let client = setup @@ -2439,7 +2439,7 @@ async fn should_retry_when_response_too_large() { .build(); let response = client - .get_logs(vec!["0xdAC17F958D2ee523a2206206994597C13D831ec7"]) + .get_logs(vec![address!("0xdAC17F958D2ee523a2206206994597C13D831ec7")]) .send() .await .expect_consistent(); From 472a0e6c912b669ea3f3e50e60c937a6773dd811 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 26 Aug 2025 09:55:09 +0200 Subject: [PATCH 35/57] XC-412: Fix mocking logic --- Cargo.lock | 1 + Cargo.toml | 1 + evm_rpc_client/Cargo.toml | 1 + evm_rpc_client/src/lib.rs | 4 +- evm_rpc_client/src/runtime/mod.rs | 4 +- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 116 +++++++++---------- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 59 ++++++---- src/http.rs | 9 +- tests/tests.rs | 59 +++++----- 9 files changed, 130 insertions(+), 124 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a51528f..bcad1a94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1546,6 +1546,7 @@ dependencies = [ "async-trait", "candid", "canhttp", + "dyn-clone", "evm_rpc_types", "ic-cdk", "ic-error-types", diff --git a/Cargo.toml b/Cargo.toml index 5f4d42b6..358ddf91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ canlog = { version = "0.2.0", features = ["derive"] } candid_parser = { version = "0.1.4" } ciborium = "0.2.2" derive_more = { version = "2.0.1", features = ["from", "into"] } +dyn-clone = "1.0.20" ethnum = { version = "1.5.0", features = ["serde"] } futures = "0.3.31" futures-channel = "0.3.31" diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 0e651609..bd581b6d 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -16,6 +16,7 @@ alloy-rpc-types = { workspace = true } async-trait = { workspace = true } candid = { workspace = true } canhttp = { workspace = true, optional = true } +dyn-clone = { workspace = true } evm_rpc_types = { path = "../evm_rpc_types", features = ["alloy"] } ic-cdk = { workspace = true } ic-error-types = { workspace = true } diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index 4355bb46..472df429 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -194,8 +194,8 @@ use ic_error_types::RejectCode; use request::{GetLogsRequest, GetLogsRequestBuilder}; #[cfg(feature = "pocket-ic")] pub use runtime::{ - forever, once, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, - MockOutcallRepeat, PocketIcRuntime, RepeatExt, + forever, once, times, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, + MockOutcallRepeat, PocketIcRuntime, }; pub use runtime::{IcRuntime, Runtime}; use serde::de::DeserializeOwned; diff --git a/evm_rpc_client/src/runtime/mod.rs b/evm_rpc_client/src/runtime/mod.rs index fc398524..9991fc4a 100644 --- a/evm_rpc_client/src/runtime/mod.rs +++ b/evm_rpc_client/src/runtime/mod.rs @@ -7,8 +7,8 @@ use candid::{CandidType, Principal}; use ic_error_types::RejectCode; #[cfg(feature = "pocket-ic")] pub use pocket_ic::{ - forever, once, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, - MockOutcallRepeat, PocketIcRuntime, RepeatExt, + forever, once, times, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, + MockOutcallRepeat, PocketIcRuntime, }; use serde::de::DeserializeOwned; diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index c81949a2..8b34df54 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -1,38 +1,26 @@ -use canhttp::http::json::{Id, JsonRpcRequest}; +use canhttp::http::json::JsonRpcRequest; +use dyn_clone::DynClone; use ic_cdk::api::call::RejectionCode; use pocket_ic::common::rest::{ CanisterHttpHeader, CanisterHttpMethod, CanisterHttpReject, CanisterHttpReply, CanisterHttpRequest, CanisterHttpResponse, }; use serde_json::Value; +use std::fmt::Debug; use std::{ collections::{BTreeSet, VecDeque}, - iter, + fmt, iter, str::FromStr, }; use url::{Host, Url}; #[derive(Clone, Default)] -pub struct MockOutcallQueue(VecDeque>); +pub struct MockOutcallQueue(VecDeque>); -trait CloneableMockOutcallIterator: Iterator + Send { - fn clone_box(&self) -> Box; -} +trait MockOutcallIterator: Iterator + Send + DynClone {} +dyn_clone::clone_trait_object!(MockOutcallIterator); -impl CloneableMockOutcallIterator for T -where - T: Iterator + Clone + Send + 'static, -{ - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.clone_box() - } -} +impl MockOutcallIterator for T where T: Iterator + Clone + Send + 'static {} impl MockOutcallQueue { pub fn push(&mut self, outcall: impl Into, repeat: MockOutcallRepeat) { @@ -72,19 +60,12 @@ pub fn once() -> MockOutcallRepeat { MockOutcallRepeat::Once } -pub fn forever() -> MockOutcallRepeat { - MockOutcallRepeat::Forever -} - -pub trait RepeatExt { - fn times(self) -> MockOutcallRepeat; +pub fn times(n: usize) -> MockOutcallRepeat { + MockOutcallRepeat::Times(n) } -impl RepeatExt for usize { - fn times(self) -> MockOutcallRepeat { - assert!(self > 1, "Repeat count must be greater than 1"); - MockOutcallRepeat::Times(self) - } +pub fn forever() -> MockOutcallRepeat { + MockOutcallRepeat::Forever } pub struct MockOutcallBody(pub Vec); @@ -123,7 +104,7 @@ impl From> for MockOutcallBody { pub struct MockOutcallBuilder(MockOutcall); impl MockOutcallBuilder { - pub fn new(responses: impl IntoIterator)>) -> Self { + pub fn new(responses: impl IntoIterator) -> Self { Self(MockOutcall { method: None, url: None, @@ -131,39 +112,34 @@ impl MockOutcallBuilder { request_headers: None, request_body: None, max_response_bytes: None, - responses: responses + responses: responses.into_iter().map(Into::into).collect(), + }) + } + + pub fn new_success(bodies: impl IntoIterator>) -> Self { + MockOutcallBuilder::new( + bodies .into_iter() - .map(|(status, body)| { + .map(|body| { CanisterHttpResponse::CanisterHttpReply(CanisterHttpReply { - status, + status: 200, headers: vec![], body: body.into().0, }) - }) - .collect(), - }) + }), + ) } - pub fn new_success(bodies: impl IntoIterator>) -> Self { - MockOutcallBuilder::new(iter::zip(iter::repeat(200), bodies)) - } - - pub fn new_error(code: RejectionCode, num_providers: usize, message: impl ToString) -> Self { - Self(MockOutcall { - method: None, - url: None, - host: None, - request_headers: None, - request_body: None, - max_response_bytes: None, - responses: vec![ - CanisterHttpResponse::CanisterHttpReject(CanisterHttpReject { + pub fn new_reject(code: RejectionCode, num_providers: usize, message: impl ToString) -> Self { + MockOutcallBuilder::new(vec![ + CanisterHttpResponse::CanisterHttpReject( + CanisterHttpReject { reject_code: code as u64, message: message.to_string(), - }); - num_providers - ], - }) + } + ); + num_providers + ]) } pub fn with_method(mut self, method: CanisterHttpMethod) -> Self { @@ -208,11 +184,6 @@ impl MockOutcallBuilder { self } - pub fn with_request_id(mut self, id: Id) -> Self { - self.0.request_body = self.0.request_body.map(|body| body.with_id(id)); - self - } - pub fn build(self) -> MockOutcall { self.0 } @@ -224,7 +195,7 @@ impl From for MockOutcall { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct MockOutcall { pub method: Option, pub url: Option, @@ -235,6 +206,27 @@ pub struct MockOutcall { pub responses: Vec, } +impl Debug for MockOutcall { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MockOutcall") + .field("method", &self.method) + .field("url", &self.url) + .field("host", &self.host) + .field("request_headers", &self.request_headers) + .field("request_body", &self.request_body) + .field("max_response_bytes", &self.max_response_bytes) + .field( + "responses", + &self + .responses + .iter() + .map(|_| "") + .collect::>(), + ) + .finish() + } +} + impl MockOutcall { pub fn assert_matches(&self, request: &CanisterHttpRequest) { let req_url = Url::from_str(&request.url).expect("BUG: invalid URL"); diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs index 4b78d0d6..01bf87b6 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mod.rs @@ -6,8 +6,8 @@ use async_trait::async_trait; use candid::{decode_args, utils::ArgumentEncoder, CandidType, Principal}; use ic_error_types::RejectCode; pub use mock::{ - forever, once, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, - MockOutcallRepeat, RepeatExt, + forever, once, times, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, + MockOutcallRepeat, }; use pocket_ic::common::rest::{ CanisterHttpReject, CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse, @@ -69,7 +69,7 @@ impl Runtime for PocketIcRuntime<'_> { .submit_call(id, self.caller, method, encode_args(args)) .await .unwrap(); - self.execute_mock().await; + self.execute_mocks().await; self.env .await_call(message_id) .await @@ -96,27 +96,38 @@ impl Runtime for PocketIcRuntime<'_> { } impl PocketIcRuntime<'_> { - async fn execute_mock(&self) { - if let Some(mock) = { - let mut mocks = self.mocks.lock().unwrap(); - mocks.next() - } { - let mut responses = mock.responses.clone().into_iter(); - let requests = tick_until_http_request(self.env).await; - - for (request, response) in iter::zip(requests, responses.by_ref()) { - mock.assert_matches(&request); - let mock_response = MockCanisterHttpResponse { - subnet_id: request.subnet_id, - request_id: request.request_id, - response: check_response_size(&request, response), - additional_responses: vec![], - }; - self.env.mock_canister_http_response(mock_response).await; + // Loops, polling for pending canister HTTP requests and answering them with queued mocks. + // Each batch of requests consumes one mock, with requests validated and responded to via + // `PocketIc::mock_canister_http_response`. Panics if a mock has leftover responses. + async fn execute_mocks(&self) { + loop { + let pending_requests = tick_until_http_requests(self.env).await; + if pending_requests.is_empty() { + return; } - if responses.next().is_some() { - panic!("no pending HTTP request") + if let Some(mock) = { + let mut mocks = self.mocks.lock().unwrap(); + mocks.next() + } { + let mut mocked_responses = mock.responses.clone().into_iter(); + + for (request, response) in iter::zip(pending_requests, mocked_responses.by_ref()) { + mock.assert_matches(&request); + let mock_response = MockCanisterHttpResponse { + subnet_id: request.subnet_id, + request_id: request.request_id, + response: check_response_size(&request, response), + additional_responses: vec![], + }; + self.env + .mock_canister_http_response(mock_response) + .await; + } + + if mocked_responses.next().is_some() { + panic!("Some mocked responses were not consumed") + } } } } @@ -174,7 +185,7 @@ where }) } -async fn tick_until_http_request(env: &PocketIc) -> Vec { +async fn tick_until_http_requests(env: &PocketIc) -> Vec { let mut requests = Vec::new(); for _ in 0..MAX_TICKS { requests = env.get_canister_http().await; @@ -201,7 +212,7 @@ impl ClientBuilder> { self.mock(outcall.into(), once()) } - /// Add a seuqence of mock outcalls to the queue, each executed once. + /// Add a sequence of mock outcalls to the queue, each executed once. pub fn mock_sequence( mut self, outcalls: impl IntoIterator>, diff --git a/src/http.rs b/src/http.rs index 1328574e..190cd544 100644 --- a/src/http.rs +++ b/src/http.rs @@ -135,12 +135,19 @@ where }) .on_error( |req_data: MetricData, error: &HttpClientError| match error { - HttpClientError::IcError(IcError { code, message: _ }) => { + HttpClientError::IcError(IcError { code, message }) => { 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 `{}`", + req_data.request_id, + code, + message, + ); } HttpClientError::UnsuccessfulHttpResponse( FilterNonSuccessfulHttpResponseError::UnsuccessfulResponse(response), diff --git a/tests/tests.rs b/tests/tests.rs index 6e7fca75..eb5b2c1c 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -5,7 +5,6 @@ use alloy_primitives::{address, b256, bytes}; use alloy_rpc_types::BlockNumberOrTag; use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat, Principal}; -use canhttp::http::json::Id; use canlog::{Log, LogEntry}; use evm_rpc::constants::DEFAULT_MAX_RESPONSE_BYTES; use evm_rpc::logs::Priority; @@ -37,7 +36,7 @@ use pocket_ic::{nonblocking, ErrorCode, PocketIc, PocketIcBuilder, RejectRespons use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::json; use std::sync::{Arc, Mutex}; -use std::{marker::PhantomData, str::FromStr, time::Duration}; +use std::{iter, marker::PhantomData, str::FromStr, time::Duration}; const DEFAULT_CALLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x01]); const DEFAULT_CONTROLLER_TEST_ID: Principal = Principal::from_slice(&[0x9d, 0xf7, 0x02]); @@ -878,14 +877,13 @@ async fn eth_get_logs_should_succeed() { let mut responses: [serde_json::Value; 3] = mock_responses(); add_offset_json_rpc_id(responses.as_mut_slice(), offset); - let client = setup + let response = setup .client() .with_rpc_sources(source.clone()) .mock_once(evm_rpc_client::MockOutcallBuilder::new_success( responses.clone(), )) - .build(); - let response = client + .build() .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) .with_from_block(from_block) .with_to_block(to_block) @@ -2389,26 +2387,24 @@ async fn should_retry_when_response_too_large() { // around 600 bytes per log // we need at least 3334 logs to reach the 2MB limit - let mocks = std::iter::once(1_u64) + let response_bodies = json_rpc_sequential_id::<12>(multi_logs_for_single_transaction(3_500)); + let max_response_bytes = iter::once(1_u64) .chain((1..=10).map(|i| 1024_u64 << i)) - .chain(std::iter::once(2_000_000_u64)) - .enumerate() - .map(|(id, max_response_bytes)| { - evm_rpc_client::MockOutcallBuilder::new_success([multi_logs_for_single_transaction( - 3_500, - )]) - .with_max_response_bytes(max_response_bytes) - .with_request_id(Id::from(id as u64)) - }); + .chain(iter::once(2_000_000_u64)); - let client = setup + let mocks = iter::zip(response_bodies, max_response_bytes).map( + |(response_body, max_response_bytes)| { + evm_rpc_client::MockOutcallBuilder::new_success([response_body]) + .with_max_response_bytes(max_response_bytes) + }, + ); + + let response = setup .client() .with_rpc_sources(rpc_services.clone()) .with_response_size_estimate(1) .mock_sequence(mocks) - .build(); - - let response = client + .build() .get_logs(vec![address!("0xdAC17F958D2ee523a2206206994597C13D831ec7")]) .send() .await @@ -2420,25 +2416,22 @@ async fn should_retry_when_response_too_large() { if code == LegacyRejectionCode::SysFatal && message.contains("body exceeds size limit") ); - let mocks = std::iter::once(1_u64) - .chain((1..=10).map(|i| 1024_u64 << i)) - .enumerate() - .map(|(id, max_response_bytes)| { - evm_rpc_client::MockOutcallBuilder::new_success([multi_logs_for_single_transaction( - 1_000, - )]) - .with_max_response_bytes(max_response_bytes) - .with_request_id(Id::from(id as u64 + 12)) - }); + let mut response_bodies = json_rpc_sequential_id::<11>(multi_logs_for_single_transaction(1_000)); + add_offset_json_rpc_id(response_bodies.as_mut_slice(), 12); + let max_response_bytes = iter::once(1_u64).chain((1..=10).map(|i| 1024_u64 << i)); + let mocks = iter::zip(max_response_bytes, response_bodies).map( + |(max_response_bytes, response_body)| { + evm_rpc_client::MockOutcallBuilder::new_success([response_body]) + .with_max_response_bytes(max_response_bytes) + }, + ); - let client = setup + let response = setup .client() .with_rpc_sources(rpc_services.clone()) .with_response_size_estimate(1) .mock_sequence(mocks) - .build(); - - let response = client + .build() .get_logs(vec![address!("0xdAC17F958D2ee523a2206206994597C13D831ec7")]) .send() .await From b19309f7ddc54a59fed1bd5cb4d020b46bb29cf3 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 26 Aug 2025 10:15:43 +0200 Subject: [PATCH 36/57] XC-412: Revert merge mistake --- evm_rpc_client/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index a68663dd..a3cd400a 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -66,7 +66,6 @@ //! .await //! .expect_consistent(); //! -//! assert!(result.is_ok()); //! assert_eq!(result.unwrap().first(), Some( //! &alloy_rpc_types::Log { //! inner: alloy_primitives::Log { @@ -150,7 +149,6 @@ //! .await //! .expect_consistent(); //! -//! assert!(result.is_ok()); //! assert_eq!(result.unwrap().first(), Some( //! &alloy_rpc_types::Log { //! inner: alloy_primitives::Log { @@ -365,7 +363,6 @@ impl EvmRpcClient { /// .await /// .expect_consistent(); /// - /// assert!(result.is_ok()); /// assert_eq!(result.unwrap().first(), Some( /// &alloy_rpc_types::Log { /// inner: alloy_primitives::Log { From bd4fae61e5d13f1ec5e550342ec0db3bbf71b254 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 26 Aug 2025 10:21:04 +0200 Subject: [PATCH 37/57] XC-412: Remove debugging code --- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 23 +------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 8f249bfa..9773371b 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -191,7 +191,7 @@ impl From for MockOutcall { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct MockOutcall { pub method: Option, pub url: Option, @@ -202,27 +202,6 @@ pub struct MockOutcall { pub responses: Vec, } -impl Debug for MockOutcall { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("MockOutcall") - .field("method", &self.method) - .field("url", &self.url) - .field("host", &self.host) - .field("request_headers", &self.request_headers) - .field("request_body", &self.request_body) - .field("max_response_bytes", &self.max_response_bytes) - .field( - "responses", - &self - .responses - .iter() - .map(|_| "") - .collect::>(), - ) - .finish() - } -} - impl MockOutcall { pub fn assert_matches(&self, request: &CanisterHttpRequest) { let req_url = Url::from_str(&request.url).expect("BUG: invalid URL"); From 4735b5377bc9018daeb8fc4ca30d46b954d989ca Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 26 Aug 2025 10:22:39 +0200 Subject: [PATCH 38/57] XC-412: Clippy --- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 9773371b..b953fb26 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -9,7 +9,7 @@ use serde_json::Value; use std::fmt::Debug; use std::{ collections::{BTreeSet, VecDeque}, - fmt, iter, + iter, str::FromStr, }; use url::{Host, Url}; From d5e5b248e296d82370c1cfeccf058f140a9b3582 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 26 Aug 2025 16:26:09 +0200 Subject: [PATCH 39/57] XC-412: Add support for `eth_getBlockByNumber` method to client --- Cargo.lock | 23 +-- Cargo.toml | 2 + evm_rpc_client/Cargo.toml | 1 + evm_rpc_client/src/lib.rs | 69 +++++++- evm_rpc_client/src/request/mod.rs | 36 ++++ evm_rpc_client/src/runtime/pocket_ic/mock.rs | 11 ++ evm_rpc_types/Cargo.toml | 7 +- evm_rpc_types/src/alloy.rs | 40 ++++- evm_rpc_types/src/response/alloy.rs | 65 ++++++- evm_rpc_types/src/response/test.rs | 7 +- evm_rpc_types/src/result/alloy.rs | 8 +- tests/tests.rs | 177 ++++++++++++------- 12 files changed, 362 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86fb50bf..4d35a95c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35f021a55afd68ff2364ccfddaa364fc9a38a72200cdc74fcfb8dc3231d38f2c" +checksum = "88bf8bac84bad62b19c047ceb71af26052155b924800d1f2eb7da99022799878" dependencies = [ "alloy-eips", "alloy-primitives", @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7473a19f02b25f8e1e8c69d35f02c07245694d11bd91bfe00e9190ac106b3838" +checksum = "ccab489a99806d6586118b4712506e56c2888cdf4798f29b401511bc3af13475" dependencies = [ "alloy-eip2124", "alloy-eip2930", @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d540d962ddbc3e95153bafe56ccefeb16dfbffa52c5f7bdd66cd29ec8f52259" +checksum = "5d090a0ca79509160da70edddf7f7144a1eab1f45c2c9bf53695b6971b6ff890" dependencies = [ "alloy-consensus", "alloy-eips", @@ -249,9 +249,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30be84f45d4f687b00efaba1e6290cbf53ccc8f6b8fbb54e4c2f9d2a0474ce95" +checksum = "93cb27da33b618fe729ab6b053275e754fbb31782caa83dfd11219d7e2e8ebf1" dependencies = [ "alloy-primitives", "serde", @@ -346,9 +346,9 @@ dependencies = [ [[package]] name = "alloy-tx-macros" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e29436068f836727d4e7c819ae6bf6f9c9e19a32e96fc23e814709a277f23a" +checksum = "20401796a1205414da724f63cc4e7713d674576ee923469f07c29ccc6d382ba2" dependencies = [ "alloy-primitives", "darling", @@ -1493,6 +1493,7 @@ dependencies = [ name = "evm_rpc" version = "2.4.0" dependencies = [ + "alloy-consensus", "alloy-primitives", "alloy-rpc-types", "assert_matches", @@ -1541,6 +1542,7 @@ dependencies = [ name = "evm_rpc_client" version = "1.4.0" dependencies = [ + "alloy-consensus", "alloy-primitives", "alloy-rpc-types", "async-trait", @@ -1562,6 +1564,7 @@ dependencies = [ name = "evm_rpc_types" version = "2.0.0" dependencies = [ + "alloy-consensus", "alloy-primitives", "alloy-rpc-types", "candid", diff --git a/Cargo.toml b/Cargo.toml index 358ddf91..152b1dfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ zeroize = { version = "1.8", features = ["zeroize_derive"] } regex = "1.11" [dev-dependencies] +alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } alloy-rpc-types = { workspace = true } assert_matches = { workspace = true } @@ -65,6 +66,7 @@ rand = "0.8" tokio = "1.44.1" [workspace.dependencies] +alloy-consensus = "1.0.26" alloy-primitives = "1.3.0" alloy-rpc-types = "1.0.23" assert_matches = "1.5.0" diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index bd581b6d..524b18d6 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/dfinity/evm-rpc-canister" documentation = "https://docs.rs/evm_rpc_client" [dependencies] +alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } alloy-rpc-types = { workspace = true } async-trait = { workspace = true } diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index a3cd400a..84686611 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -183,9 +183,11 @@ pub mod fixtures; mod request; mod runtime; -use crate::request::{Request, RequestBuilder}; +use crate::request::{ + GetBlockByNumberRequest, GetBlockByNumberRequestBuilder, Request, RequestBuilder, +}; use candid::{CandidType, Principal}; -use evm_rpc_types::{ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; +use evm_rpc_types::{BlockTag, ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; use ic_error_types::RejectCode; use request::{GetLogsRequest, GetLogsRequestBuilder}; #[cfg(feature = "pocket-ic")] @@ -324,6 +326,69 @@ impl ClientBuilder { } impl EvmRpcClient { + /// Call `eth_getBlockByNumber` on the EVM RPC canister. + /// + /// # Examples + /// + /// ```rust + /// use alloy_primitives::{address, b256, bytes}; + /// use alloy_rpc_types::BlockNumberOrTag; + /// use evm_rpc_client::EvmRpcClient; + /// + /// # use evm_rpc_types::{Block, Hex, Hex20, Hex32, MultiRpcResult, Nat256}; + /// # use std::str::FromStr; + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// use evm_rpc_types::Hex256; + /// let client = EvmRpcClient::builder_for_ic() + /// # .with_default_stub_response(MultiRpcResult::Consistent(Ok(Block { + /// # base_fee_per_gas: None, + /// # number: Nat256::ZERO, + /// # difficulty: Some(Nat256::ZERO), + /// # extra_data: Hex::from(vec![]), + /// # gas_limit: Nat256::ZERO, + /// # gas_used: Nat256::ZERO, + /// # hash: Hex32::from(b256!("0x47302c2ebfb29611c74f917a380f3cf45c9dfe9de3554e18bff9a9ca7c8454e2")), + /// # logs_bloom: Hex256::from([0; 256]), + /// # miner: Hex20::from([0; 20]), + /// # mix_hash: Hex32::from([0; 32]), + /// # nonce: Nat256::ZERO, + /// # parent_hash: Hex32::from([0; 32]), + /// # receipts_root: Hex32::from([0; 32]), + /// # sha3_uncles: Hex32::from([0; 32]), + /// # size: Nat256::ZERO, + /// # state_root: Hex32::from([0; 32]), + /// # timestamp: Nat256::ZERO, + /// # total_difficulty: Some(Nat256::ZERO), + /// # transactions: vec![], + /// # transactions_root: Some(Hex32::from([0; 32])), + /// # uncles: vec![], + /// # }))) + /// .build(); + /// + /// let result = client + /// .get_block_by_number(BlockNumberOrTag::Number(23225439)) + /// .with_cycles(10_000_000_000) + /// .send() + /// .await + /// .expect_consistent() + /// .unwrap(); + /// + /// assert_eq!(result.hash(), b256!("0x47302c2ebfb29611c74f917a380f3cf45c9dfe9de3554e18bff9a9ca7c8454e2")); + /// # Ok(()) + /// # } + /// ``` + pub fn get_block_by_number( + &self, + params: impl Into, + ) -> GetBlockByNumberRequestBuilder { + RequestBuilder::new( + self.clone(), + GetBlockByNumberRequest::new(params.into()), + 10_000_000_000, + ) + } + /// Call `eth_getLogs` on the EVM RPC canister. /// /// # Examples diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index fc828953..f35ebece 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -8,6 +8,39 @@ use serde::de::DeserializeOwned; use std::fmt::{Debug, Formatter}; use strum::EnumIter; + +#[derive(Debug, Clone)] +pub struct GetBlockByNumberRequest(BlockTag); + +impl GetBlockByNumberRequest { + pub fn new(params: BlockTag) -> Self { + Self(params) + } +} + +impl EvmRpcRequest for GetBlockByNumberRequest { + type Config = RpcConfig; + type Params = BlockTag; + type CandidOutput = MultiRpcResult; + type Output = MultiRpcResult; + + fn endpoint(&self) -> EvmRpcEndpoint { + EvmRpcEndpoint::GetBlockByNumber + } + + fn params(self) -> Self::Params { + self.0 + } +} + +pub type GetBlockByNumberRequestBuilder = RequestBuilder< + R, + RpcConfig, + BlockTag, + MultiRpcResult, + MultiRpcResult, +>; + #[derive(Debug, Clone)] pub struct GetLogsRequest(GetLogsArgs); @@ -92,6 +125,8 @@ pub trait EvmRpcRequest { /// Endpoint on the EVM RPC canister triggering a call to EVM providers. #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, EnumIter)] pub enum EvmRpcEndpoint { + /// `eth_getBlockByNumber` endpoint. + GetBlockByNumber, /// `eth_getLogs` endpoint. GetLogs, } @@ -100,6 +135,7 @@ impl EvmRpcEndpoint { /// Method name on the EVM RPC canister pub fn rpc_method(&self) -> &'static str { match &self { + Self::GetBlockByNumber => "eth_getBlockByNumber", Self::GetLogs => "eth_getLogs", } } diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index b953fb26..0c37b6f3 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -180,6 +180,17 @@ impl MockOutcallBuilder { self } + pub fn with_sequential_response_ids(mut self, first_id: u64) -> Self{ + for (i, response) in self.0.responses.iter_mut().enumerate() { + if let CanisterHttpResponse::CanisterHttpReply(reply) = response { + let mut body: Value = serde_json::from_slice(&reply.body).expect("Invalid response body"); + *body.get_mut("id").expect("Missing request ID") = Value::Number((first_id + i as u64).into()); + reply.body = serde_json::to_vec(&body).expect("Invalid response body"); + } + } + self + } + pub fn build(self) -> MockOutcall { self.0 } diff --git a/evm_rpc_types/Cargo.toml b/evm_rpc_types/Cargo.toml index 879e96b9..ce1583e7 100644 --- a/evm_rpc_types/Cargo.toml +++ b/evm_rpc_types/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/dfinity/evm-rpc-canister" documentation = "https://docs.rs/evm_rpc_types" [dependencies] +alloy-consensus = { workspace = true, optional = true } alloy-primitives = { workspace = true, optional = true } alloy-rpc-types = { workspace = true, optional = true } candid = { workspace = true } @@ -30,4 +31,8 @@ serde_json = { workspace = true } [features] default = ["alloy"] -alloy = ["dep:alloy-primitives", "dep:alloy-rpc-types"] \ No newline at end of file +alloy = [ + "dep:alloy-consensus", + "dep:alloy-primitives", + "dep:alloy-rpc-types" +] \ No newline at end of file diff --git a/evm_rpc_types/src/alloy.rs b/evm_rpc_types/src/alloy.rs index f6e09a1f..2e2fa7d7 100644 --- a/evm_rpc_types/src/alloy.rs +++ b/evm_rpc_types/src/alloy.rs @@ -1,4 +1,4 @@ -use crate::{Hex, Hex20, Hex32}; +use crate::{Hex, Hex20, Hex256, Hex32, Nat256, RpcError}; impl From for alloy_primitives::Address { fn from(value: Hex20) -> Self { @@ -35,3 +35,41 @@ impl From for Hex { Hex(value.to_vec()) } } + +impl From for alloy_primitives::U256 { + fn from(value: Nat256) -> Self { + alloy_primitives::U256::from_be_bytes(value.into_be_bytes()) + } +} + +impl From for Nat256 { + fn from(value: alloy_primitives::U256) -> Self { + Nat256::from_be_bytes(value.to_be_bytes()) + } +} + +impl From for alloy_primitives::Bloom { + fn from(value: Hex256) -> Self { + alloy_primitives::Bloom::from(value.0) + } +} + +impl From for Hex256 { + fn from(value: alloy_primitives::Bloom) -> Self { + Hex256::from(value.into_array()) + } +} + +impl TryFrom for alloy_primitives::B64 { + type Error = RpcError; + + fn try_from(value: Nat256) -> Result { + Ok(alloy_primitives::B64::from(u64::try_from(value)?)) + } +} + +impl From for Nat256 { + fn from(value: alloy_primitives::B64) -> Self { + Nat256::from(u64::from(value)) + } +} diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index 7f59008b..7c0f15bc 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -1,4 +1,5 @@ -use crate::{LogEntry, RpcError, ValidationError}; +use crate::{Block, LogEntry, RpcError, ValidationError}; +use alloy_rpc_types::BlockTransactions; impl TryFrom for alloy_rpc_types::Log { type Error = RpcError; @@ -29,3 +30,65 @@ impl TryFrom for alloy_rpc_types::Log { }) } } + +impl TryFrom for alloy_rpc_types::Block { + type Error = RpcError; + + fn try_from(value: Block) -> Result { + Ok(Self { + header: alloy_rpc_types::Header { + hash: alloy_primitives::BlockHash::from(value.hash), + inner: alloy_consensus::Header { + parent_hash: alloy_primitives::BlockHash::from(value.parent_hash), + ommers_hash: alloy_primitives::BlockHash::from(value.sha3_uncles), + beneficiary: alloy_primitives::Address::from(value.miner), + state_root: alloy_primitives::B256::from(value.state_root), + transactions_root: alloy_primitives::B256::from( + value.transactions_root.ok_or(RpcError::ValidationError( + ValidationError::Custom( + "Block does not have a transactions root field".to_string(), + ), + ))?, + ), + receipts_root: alloy_primitives::B256::from(value.receipts_root), + logs_bloom: alloy_primitives::Bloom::from(value.logs_bloom), + difficulty: alloy_primitives::U256::from(u64::try_from( + value.difficulty.ok_or(RpcError::ValidationError( + ValidationError::Custom( + "Block does not have a difficulty field".to_string(), + ), + ))?, + )?), + number: alloy_primitives::BlockNumber::from(u64::try_from(value.number)?), + gas_limit: alloy_primitives::BlockNumber::from(u64::try_from(value.gas_limit)?), + gas_used: alloy_primitives::BlockNumber::from(u64::try_from(value.gas_used)?), + timestamp: alloy_primitives::BlockNumber::from(u64::try_from(value.timestamp)?), + extra_data: alloy_primitives::Bytes::from(value.extra_data), + mix_hash: alloy_primitives::B256::from(value.mix_hash), + nonce: alloy_primitives::B64::try_from(value.nonce)?, + base_fee_per_gas: value.base_fee_per_gas.map(u64::try_from).transpose()?, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + }, + total_difficulty: value.total_difficulty.map(|value| value.into()), + size: Some(value.size.into()), + }, + uncles: value + .uncles + .into_iter() + .map(alloy_primitives::B256::from) + .collect(), + transactions: BlockTransactions::Hashes( + value + .transactions + .into_iter() + .map(alloy_primitives::B256::from) + .collect(), + ), + withdrawals: None, + }) + } +} diff --git a/evm_rpc_types/src/response/test.rs b/evm_rpc_types/src/response/test.rs index d76ffe0d..7194e2f2 100644 --- a/evm_rpc_types/src/response/test.rs +++ b/evm_rpc_types/src/response/test.rs @@ -15,7 +15,7 @@ mod alloy_conversion_tests { proptest! { #[test] - fn should_convert_from_alloy(entry in arb_log_entry()) { + fn should_convert_log_to_alloy(entry in arb_log_entry()) { // Convert a number serialized as a hexadecimal string into an array of u32 digits. // This is needed to compare a serialized `alloy_rpc_types::Log` with an // `evm_rpc_types::LogEntry` since `transactionIndex`, `logIndex` and `blockNumber` get @@ -39,6 +39,11 @@ mod alloy_conversion_tests { } } + #[test] + fn should_convert_block_to_alloy() { + todo!() + } + fn arb_log_entry() -> impl Strategy { ( arb_hex20(), diff --git a/evm_rpc_types/src/result/alloy.rs b/evm_rpc_types/src/result/alloy.rs index 33655c29..115781db 100644 --- a/evm_rpc_types/src/result/alloy.rs +++ b/evm_rpc_types/src/result/alloy.rs @@ -1,4 +1,4 @@ -use crate::{LogEntry, MultiRpcResult}; +use crate::{Block, LogEntry, MultiRpcResult}; impl From>> for MultiRpcResult> { fn from(result: MultiRpcResult>) -> Self { @@ -9,3 +9,9 @@ impl From>> for MultiRpcResult> for MultiRpcResult { + fn from(result: MultiRpcResult) -> Self { + result.and_then(alloy_rpc_types::Block::try_from) + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 99c8dc4d..bd68b931 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,8 +1,8 @@ mod mock; use crate::mock::MockJsonRequestBody; -use alloy_primitives::{address, b256, bytes}; -use alloy_rpc_types::BlockNumberOrTag; +use alloy_primitives::{address, b256, bloom, bytes}; +use alloy_rpc_types::{BlockNumberOrTag, BlockTransactions}; use assert_matches::assert_matches; use candid::{CandidType, Decode, Encode, Nat, Principal}; use canlog::{Log, LogEntry}; @@ -804,29 +804,27 @@ fn should_decode_transaction_receipt() { #[tokio::test] async fn eth_get_logs_should_succeed() { - fn mock_responses() -> [serde_json::Value; 3] { - json_rpc_sequential_id(json!({ - "id" : 0, - "jsonrpc" : "2.0", - "result" : [ - { - "address" : "0xdac17f958d2ee523a2206206994597c13d831ec7", - "topics" : [ - "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", - "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43", - "0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2" - ], - "data" : "0x000000000000000000000000000000000000000000000000000000003b9c6433", - "blockNumber" : "0x11dc77e", - "transactionHash" : "0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678", - "transactionIndex" : "0x65", - "blockHash" : "0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629", - "logIndex" : "0xe8", - "removed" : false - } - ] - })) - } + let response = json!({ + "id" : 0, + "jsonrpc" : "2.0", + "result" : [ + { + "address" : "0xdac17f958d2ee523a2206206994597c13d831ec7", + "topics" : [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43", + "0x00000000000000000000000078cccfb3d517cd4ed6d045e263e134712288ace2" + ], + "data" : "0x000000000000000000000000000000000000000000000000000000003b9c6433", + "blockNumber" : "0x11dc77e", + "transactionHash" : "0xf3ed91a03ddf964281ac7a24351573efd535b80fc460a5c2ad2b9d23153ec678", + "transactionIndex" : "0x65", + "blockHash" : "0xd5c72ad752b2f0144a878594faf8bd9f570f2f72af8e7f0940d3545a6388f629", + "logIndex" : "0xe8", + "removed" : false + } + ] + }); fn expected_logs() -> Vec { vec![alloy_rpc_types::Log { @@ -874,15 +872,16 @@ async fn eth_get_logs_should_succeed() { BlockNumberOrTag::Number(501_u16.into()), ), ] { - let mut responses: [serde_json::Value; 3] = mock_responses(); - add_offset_json_rpc_id(responses.as_mut_slice(), offset); - let response = setup .client() .with_rpc_sources(source.clone()) - .mock_once(evm_rpc_client::MockOutcallBuilder::new_success( - responses.clone(), - )) + .mock_once( + evm_rpc_client::MockOutcallBuilder::new_success(iter::repeat_n( + response.clone(), + 3, + )) + .with_sequential_response_ids(offset), + ) .build() .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) .with_from_block(from_block) @@ -892,6 +891,7 @@ async fn eth_get_logs_should_succeed() { .await .expect_consistent() .unwrap(); + offset += 3; assert_eq!(response, expected_logs()); @@ -943,47 +943,90 @@ async fn eth_get_logs_should_fail_when_block_range_too_large() { } } -#[test] -fn eth_get_block_by_number_should_succeed() { - let [response_0, response_1, response_2] = json_rpc_sequential_id( - json!({"jsonrpc":"2.0","result":{"baseFeePerGas":"0xd7232aa34","difficulty":"0x0","extraData":"0x546974616e2028746974616e6275696c6465722e78797a29","gasLimit":"0x1c9c380","gasUsed":"0xa768c4","hash":"0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae","logsBloom":"0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b","miner":"0x388c818ca8b9251b393131c08a736a67ccb19297","mixHash":"0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f","nonce":"0x0000000000000000","number":"0x11db01d","parentHash":"0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae","receiptsRoot":"0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0xcd35","stateRoot":"0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6","timestamp":"0x656f96f3","totalDifficulty":"0xc70d815d562d3cfa955","withdrawalsRoot":"0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48"},"id":0}), - ); +#[tokio::test] +async fn eth_get_block_by_number_should_succeed() { + let response = json!({ + "jsonrpc": "2.0", + "result": { + "baseFeePerGas": "0xd7232aa34", + "difficulty": "0x0", + "extraData": "0x546974616e2028746974616e6275696c6465722e78797a29", + "gasLimit": "0x1c9c380", + "gasUsed": "0xa768c4", + "hash": "0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae", + "logsBloom": "0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b", + "miner": "0x388c818ca8b9251b393131c08a736a67ccb19297", + "mixHash": "0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f", + "nonce": "0x0000000000000000", + "number": "0x11db01d", + "parentHash": "0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae", + "receiptsRoot": "0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0xcd35", + "stateRoot": "0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6", + "timestamp": "0x656f96f3", + "withdrawalsRoot": "0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48", + "transactionsRoot": "0x93a1ad3d067009259b508cc95fde63b5efd7e9d8b55754314c173fdde8c0826a", + }, + "id": 0 + }); + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + let mut offset = 0; + for source in RPC_SERVICES { - let setup = EvmRpcSetup::new().mock_api_keys(); let response = setup - .eth_get_block_by_number(source.clone(), None, evm_rpc_types::BlockTag::Latest) - .mock_http_once(MockOutcallBuilder::new(200, response_0.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_1.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_2.clone())) - .wait() + .client() + .with_rpc_sources(source.clone()) + .mock_once( + evm_rpc_client::MockOutcallBuilder::new_success(iter::repeat_n( + response.clone(), + 3, + )) + .with_sequential_response_ids(offset), + ) + .build() + .get_block_by_number(BlockNumberOrTag::Latest) + .send() + .await .expect_consistent() .unwrap(); - assert_eq!( - response, - evm_rpc_types::Block { - base_fee_per_gas: Some(57_750_497_844_u64.into()), - difficulty: Some(Nat256::ZERO), - extra_data: "0x546974616e2028746974616e6275696c6465722e78797a29".parse().unwrap(), - gas_limit: 0x1c9c380_u32.into(), - gas_used: 0xa768c4_u32.into(), - hash: "0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae".parse().unwrap(), - logs_bloom: "0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b".parse().unwrap(), - miner: "0x388c818ca8b9251b393131c08a736a67ccb19297".parse().unwrap(), - mix_hash: "0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f".parse().unwrap(), - nonce: Nat256::ZERO, - number: 18_722_845_u32.into(), - parent_hash: "0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae".parse().unwrap(), - receipts_root: "0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929".parse().unwrap(), - sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347".parse().unwrap(), - size: 0xcd35_u32.into(), - state_root: "0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6".parse().unwrap(), - timestamp: 0x656f96f3_u32.into(), + + assert_eq!(response, alloy_rpc_types::Block { + header: alloy_rpc_types::Header { + hash: b256!("0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae"), + inner: alloy_consensus::Header { + parent_hash: b256!("0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae"), + ommers_hash: b256!("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"), + beneficiary: address!("0x388c818ca8b9251b393131c08a736a67ccb19297"), + state_root: b256!("0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6"), + transactions_root: b256!("0x93a1ad3d067009259b508cc95fde63b5efd7e9d8b55754314c173fdde8c0826a"), + receipts_root: b256!("0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929"), + logs_bloom: bloom!("0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b"), + difficulty: alloy_primitives::U256::ZERO, + number: 18_722_845_u64, + gas_limit: 0x1c9c380_u64, + gas_used: 0xa768c4_u64, + timestamp: 0x656f96f3_u64, + extra_data: bytes!("0x546974616e2028746974616e6275696c6465722e78797a29"), + mix_hash: b256!("0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f"), + nonce: alloy_primitives::B64::ZERO, + base_fee_per_gas: Some(57_750_497_844_u64), + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + }, total_difficulty: None, - transactions: vec![], - transactions_root: None, - uncles: vec![], - } - ); + size: Some(alloy_primitives::U256::from(0xcd35_u64)), + }, + uncles: vec![], + transactions: BlockTransactions::Hashes(vec![]), + withdrawals: None, + }); + + offset += 3; } } From b33867ef6b724fe0ecb88755f5adf4ecee6248d7 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 26 Aug 2025 16:35:56 +0200 Subject: [PATCH 40/57] XC-412: Formatting --- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs index 0c37b6f3..c1df483a 100644 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ b/evm_rpc_client/src/runtime/pocket_ic/mock.rs @@ -180,11 +180,13 @@ impl MockOutcallBuilder { self } - pub fn with_sequential_response_ids(mut self, first_id: u64) -> Self{ + pub fn with_sequential_response_ids(mut self, first_id: u64) -> Self { for (i, response) in self.0.responses.iter_mut().enumerate() { if let CanisterHttpResponse::CanisterHttpReply(reply) = response { - let mut body: Value = serde_json::from_slice(&reply.body).expect("Invalid response body"); - *body.get_mut("id").expect("Missing request ID") = Value::Number((first_id + i as u64).into()); + let mut body: Value = + serde_json::from_slice(&reply.body).expect("Invalid response body"); + *body.get_mut("id").expect("Missing request ID") = + Value::Number((first_id + i as u64).into()); reply.body = serde_json::to_vec(&body).expect("Invalid response body"); } } From 9470eba31e5495eb4051cc9777b0526a533ba8ea Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Tue, 26 Aug 2025 16:43:37 +0200 Subject: [PATCH 41/57] XC-412: Formatting --- evm_rpc_client/src/request/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/evm_rpc_client/src/request/mod.rs b/evm_rpc_client/src/request/mod.rs index f35ebece..47f23edf 100644 --- a/evm_rpc_client/src/request/mod.rs +++ b/evm_rpc_client/src/request/mod.rs @@ -8,7 +8,6 @@ use serde::de::DeserializeOwned; use std::fmt::{Debug, Formatter}; use strum::EnumIter; - #[derive(Debug, Clone)] pub struct GetBlockByNumberRequest(BlockTag); From f810211c3e15d5631b95001986e223067af5483c Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 27 Aug 2025 13:20:32 +0200 Subject: [PATCH 42/57] XC-412: Add unit tests for conversion --- evm_rpc_types/src/response/alloy.rs | 52 ++++--- evm_rpc_types/src/response/test.rs | 204 ++++++++++++++++++++-------- tests/tests.rs | 153 +++++++++++++-------- 3 files changed, 280 insertions(+), 129 deletions(-) diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index 7c0f15bc..d8501886 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -1,5 +1,6 @@ -use crate::{Block, LogEntry, RpcError, ValidationError}; +use crate::{Block, LogEntry, Nat256, RpcError, ValidationError}; use alloy_rpc_types::BlockTransactions; +use candid::Nat; impl TryFrom for alloy_rpc_types::Log { type Error = RpcError; @@ -21,11 +22,20 @@ impl TryFrom for alloy_rpc_types::Log { )))?, }, block_hash: entry.block_hash.map(alloy_primitives::BlockHash::from), - block_number: entry.block_number.map(u64::try_from).transpose()?, + block_number: entry + .block_number + .map(|value| u64_try_from_nat256(value, "block_number")) + .transpose()?, block_timestamp: None, transaction_hash: entry.transaction_hash.map(alloy_primitives::TxHash::from), - transaction_index: entry.transaction_index.map(u64::try_from).transpose()?, - log_index: entry.log_index.map(u64::try_from).transpose()?, + transaction_index: entry + .transaction_index + .map(|value| u64_try_from_nat256(value, "transaction_index")) + .transpose()?, + log_index: entry + .log_index + .map(|value| u64_try_from_nat256(value, "log_index")) + .transpose()?, removed: entry.removed, }) } @@ -52,21 +62,22 @@ impl TryFrom for alloy_rpc_types::Block { ), receipts_root: alloy_primitives::B256::from(value.receipts_root), logs_bloom: alloy_primitives::Bloom::from(value.logs_bloom), - difficulty: alloy_primitives::U256::from(u64::try_from( - value.difficulty.ok_or(RpcError::ValidationError( - ValidationError::Custom( - "Block does not have a difficulty field".to_string(), - ), - ))?, - )?), - number: alloy_primitives::BlockNumber::from(u64::try_from(value.number)?), - gas_limit: alloy_primitives::BlockNumber::from(u64::try_from(value.gas_limit)?), - gas_used: alloy_primitives::BlockNumber::from(u64::try_from(value.gas_used)?), - timestamp: alloy_primitives::BlockNumber::from(u64::try_from(value.timestamp)?), + difficulty: value.difficulty.ok_or( + RpcError::ValidationError(ValidationError::Custom( + "Block does not have a difficulty field".to_string(), + )), + )?.into(), + number: u64_try_from_nat256(value.number, "number")?.into(), + gas_limit: u64_try_from_nat256(value.gas_limit, "gas_limit")?, + gas_used: u64_try_from_nat256(value.gas_used, "gas_used")?, + timestamp: u64_try_from_nat256(value.timestamp, "timestamp")?, extra_data: alloy_primitives::Bytes::from(value.extra_data), mix_hash: alloy_primitives::B256::from(value.mix_hash), nonce: alloy_primitives::B64::try_from(value.nonce)?, - base_fee_per_gas: value.base_fee_per_gas.map(u64::try_from).transpose()?, + base_fee_per_gas: value + .base_fee_per_gas + .map(|value| u64_try_from_nat256(value, "base_fee_per_gas")) + .transpose()?, withdrawals_root: None, blob_gas_used: None, excess_blob_gas: None, @@ -92,3 +103,12 @@ impl TryFrom for alloy_rpc_types::Block { }) } } + +fn u64_try_from_nat256(value: Nat256, field_name: &str) -> Result { + u64::try_from(Nat::from(value).0).map_err(|err| { + RpcError::ValidationError(ValidationError::Custom(format!( + "Failed to convert block field `{}` to u64: {:?}", + field_name, err + ))) + }) +} diff --git a/evm_rpc_types/src/response/test.rs b/evm_rpc_types/src/response/test.rs index 7194e2f2..712f1d07 100644 --- a/evm_rpc_types/src/response/test.rs +++ b/evm_rpc_types/src/response/test.rs @@ -1,33 +1,24 @@ -use crate::{Hex, Hex20, Hex32}; -use proptest::prelude::Strategy; -use proptest::proptest; -use std::ops::RangeInclusive; +use crate::{Block, Hex, Hex20, Hex256, Hex32, LogEntry, Nat256}; +use num_bigint::BigUint; +use proptest::{ + arbitrary::any, collection::vec, option, prelude::Strategy, prop_assert_eq, prop_compose, + proptest, +}; +use serde_json::Value; +use std::{ops::RangeInclusive, str::FromStr}; +// To check conversion from `evm_rpc_types` to `alloy_rpc_types`, these tests generate an arbitrary +// (valid) type from the `evm_rpc_types` crate, convert it to the corresponding `alloy_rpc_types` +// type, and compare both serialized values. +// This is done so that we can check conversion for randomly generated values and not just a few +// hardcoded instances. #[cfg(feature = "alloy")] mod alloy_conversion_tests { use super::*; - use crate::{LogEntry, Nat256}; - use num_bigint::BigUint; - use proptest::arbitrary::any; - use proptest::option; - use serde_json::Value; - use std::str::FromStr; proptest! { #[test] fn should_convert_log_to_alloy(entry in arb_log_entry()) { - // Convert a number serialized as a hexadecimal string into an array of u32 digits. - // This is needed to compare a serialized `alloy_rpc_types::Log` with an - // `evm_rpc_types::LogEntry` since `transactionIndex`, `logIndex` and `blockNumber` get - // serialized as hex strings by alloy but as integers in `evm_rpc_types`. - fn hex_to_u32_digits(serialized: &mut Value, field: &str) { - if let Some(Value::String(hex)) = serialized.get(field) { - let hex = hex.strip_prefix("0x").unwrap_or(hex); - let digits = BigUint::parse_bytes(hex.as_bytes(), 16).unwrap().to_u32_digits(); - serialized[field] = digits.into(); - } - } - let serialized = serde_json::to_value(&entry).unwrap(); let mut alloy_serialized = serde_json::to_value(&alloy_rpc_types::Log::try_from(entry.clone()).unwrap()).unwrap(); @@ -35,48 +26,117 @@ mod alloy_conversion_tests { hex_to_u32_digits(&mut alloy_serialized, "logIndex"); hex_to_u32_digits(&mut alloy_serialized, "blockNumber"); - assert_eq!(serialized, alloy_serialized); + prop_assert_eq!(serialized, alloy_serialized); + } + + #[test] + fn should_convert_block_to_alloy(block in arb_block()) { + let serialized = serde_json::to_value(&block).unwrap(); + + let mut alloy_serialized = serde_json::to_value(&alloy_rpc_types::Block::try_from(block.clone()).unwrap()).unwrap(); + hex_to_u32_digits(&mut alloy_serialized, "baseFeePerGas"); + hex_to_u32_digits(&mut alloy_serialized, "number"); + hex_to_u32_digits(&mut alloy_serialized, "difficulty"); + hex_to_u32_digits(&mut alloy_serialized, "gasLimit"); + hex_to_u32_digits(&mut alloy_serialized, "gasUsed"); + hex_to_u32_digits(&mut alloy_serialized, "nonce"); + hex_to_u32_digits(&mut alloy_serialized, "size"); + hex_to_u32_digits(&mut alloy_serialized, "timestamp"); + hex_to_u32_digits(&mut alloy_serialized, "totalDifficulty"); + add_null_if_absent(&mut alloy_serialized, "baseFeePerGas"); + add_null_if_absent(&mut alloy_serialized, "totalDifficulty"); + + prop_assert_eq!(serialized, alloy_serialized); } } - #[test] - fn should_convert_block_to_alloy() { - todo!() + prop_compose! { + fn arb_block() + ( + base_fee_per_gas in option::of(arb_u64()), + number in arb_u64(), + difficulty in arb_nat256(), + extra_data in arb_hex(), + gas_limit in arb_u64(), + gas_used in arb_u64(), + hash in arb_hex32(), + logs_bloom in arb_hex256(), + miner in arb_hex20(), + mix_hash in arb_hex32(), + nonce in arb_u64(), + parent_hash in arb_hex32(), + receipts_root in arb_hex32(), + sha3_uncles in arb_hex32(), + size in arb_u64(), + state_root in arb_hex32(), + timestamp in arb_u64(), + total_difficulty in option::of(arb_nat256()), + transactions in vec(arb_hex32(), 0..100), + transactions_root in arb_hex32(), + uncles in vec(arb_hex32(), 0..100), + ) -> Block { + Block { + base_fee_per_gas, + number, + // alloy requires the `difficulty` field be present + difficulty: Some(difficulty), + extra_data, + gas_limit, + gas_used, + hash, + logs_bloom, + miner, + mix_hash, + nonce, + parent_hash, + receipts_root, + sha3_uncles, + size, + state_root, + timestamp, + total_difficulty, + transactions, + // alloy requires the `transactions_root` field be present + transactions_root: Some(transactions_root), + uncles, + } + } } - fn arb_log_entry() -> impl Strategy { + prop_compose! { + fn arb_log_entry() ( - arb_hex20(), - arb_hex(), - option::of(any::().prop_map(Nat256::from)), - option::of(arb_hex32()), - option::of(any::().prop_map(Nat256::from)), - option::of(arb_hex32()), - option::of(any::().prop_map(Nat256::from)), - any::(), - ) - .prop_map( - |( - address, - data, - block_number, - transaction_hash, - transaction_index, - block_hash, - log_index, - removed, - )| LogEntry { - address, - topics: vec![], - data, - block_number, - transaction_hash, - transaction_index, - block_hash, - log_index, - removed, - }, - ) + address in arb_hex20(), + topics in vec(arb_hex32(), 0..=4), + data in arb_hex(), + block_number in option::of(arb_u64()), + transaction_hash in option::of(arb_hex32()), + transaction_index in option::of(arb_u64()), + block_hash in option::of(arb_hex32()), + log_index in option::of(arb_u64()), + removed in any::(), + ) -> LogEntry { + LogEntry { + address, + topics, + data, + block_number, + transaction_hash, + transaction_index, + block_hash, + log_index, + removed, + } + } + } + + // `u64` wrapped in a `Nat256` + fn arb_u64() -> impl Strategy { + any::().prop_map(Nat256::from) + } + + fn arb_nat256() -> impl Strategy { + any::<[u8; 32]>().prop_map(Nat256::from_be_bytes) } fn arb_hex20() -> impl Strategy { @@ -87,9 +147,37 @@ mod alloy_conversion_tests { arb_var_len_hex_string(32..=32_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) } + fn arb_hex256() -> impl Strategy { + arb_var_len_hex_string(256..=256_usize).prop_map(|s| Hex256::from_str(s.as_str()).unwrap()) + } + fn arb_hex() -> impl Strategy { arb_var_len_hex_string(0..=100_usize).prop_map(|s| Hex::from_str(s.as_str()).unwrap()) } + + // This method checks if the given `serde_json::Value` contains the given field, and if so, + // it parses its value as a hexadecimal string and converts it to an array of u32 digits. + // This is needed to compare serialized values between `alloy_rpc_types` and `evm_rpc_types` + // since the former serialized integers as hex strings, but the latter as arrays of u32 digits. + fn hex_to_u32_digits(serialized: &mut Value, field: &str) { + if let Some(Value::String(hex)) = serialized.get(field) { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + let digits = BigUint::parse_bytes(hex.as_bytes(), 16) + .unwrap() + .to_u32_digits(); + serialized[field] = digits.into(); + } + } + + // This method checks if the given `serde_json` contains the given field, and if not, sets its + // value to `serde_json::Value::Null`. + // This is needed to compare serialized values because some fields are skipped during + // serialization in `alloy_rpc_types` but not `evm_rpc_types` + fn add_null_if_absent(serialized: &mut Value, field: &str) { + if serialized.get(field).is_none() { + serialized[field] = Value::Null; + } + } } fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { diff --git a/tests/tests.rs b/tests/tests.rs index bd68b931..eb25ca84 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -991,6 +991,7 @@ async fn eth_get_block_by_number_should_succeed() { .await .expect_consistent() .unwrap(); + offset += 3; assert_eq!(response, alloy_rpc_types::Block { header: alloy_rpc_types::Header { @@ -1025,60 +1026,101 @@ async fn eth_get_block_by_number_should_succeed() { transactions: BlockTransactions::Hashes(vec![]), withdrawals: None, }); - - offset += 3; } } -#[test] -fn eth_get_block_by_number_pre_london_fork_should_succeed() { - let [response_0, response_1, response_2] = json_rpc_sequential_id( - json!({"jsonrpc":"2.0","id":0,"result":{"number":"0x0","hash":"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3","transactions":[],"totalDifficulty":"0x400000000","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","extraData":"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa","nonce":"0x0000000000000042","miner":"0x0000000000000000000000000000000000000000","difficulty":"0x400000000","gasLimit":"0x1388","gasUsed":"0x0","uncles":[],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x21c","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateRoot":"0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544","mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000","parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000","timestamp":"0x0"}}), - ); +#[tokio::test] +async fn eth_get_block_by_number_pre_london_fork_should_succeed() { + let response = json!({ + "jsonrpc":"2.0", + "id":0, + "result":{ + "number":"0x0", + "hash":"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "transactions":[], + "totalDifficulty":"0x400000000", + "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "extraData":"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa", + "nonce":"0x0000000000000042", + "miner":"0x0000000000000000000000000000000000000000", + "difficulty":"0x400000000", + "gasLimit":"0x1388", + "gasUsed":"0x0", + "uncles":[], + "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size":"0x21c", + "transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "stateRoot":"0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544", + "mixHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash":"0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp":"0x0" + } + }); + + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; + let mut offset = 0; + for source in RPC_SERVICES { - let setup = EvmRpcSetup::new().mock_api_keys(); let response = setup - .eth_get_block_by_number(source.clone(), None, evm_rpc_types::BlockTag::Latest) - .mock_http_once(MockOutcallBuilder::new(200, response_0.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_1.clone())) - .mock_http_once(MockOutcallBuilder::new(200, response_2.clone())) - .wait() + .client() + .with_rpc_sources(source.clone()) + .mock_once( + evm_rpc_client::MockOutcallBuilder::new_success(iter::repeat_n( + response.clone(), + 3, + )) + .with_sequential_response_ids(offset), + ) + .build() + .get_block_by_number(BlockNumberOrTag::Latest) + .send() + .await .expect_consistent() .unwrap(); - assert_eq!( - response, - evm_rpc_types::Block { - base_fee_per_gas: None, - difficulty: Some(0x400000000_u64.into()), - extra_data: "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa".parse().unwrap(), - gas_limit: 0x1388_u32.into(), - gas_used: Nat256::ZERO, - hash: "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3".parse().unwrap(), - logs_bloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(), - miner: "0x0000000000000000000000000000000000000000".parse().unwrap(), - mix_hash: "0x0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(), - nonce: 0x0000000000000042_u32.into(), - number: Nat256::ZERO, - parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(), - receipts_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421".parse().unwrap(), - sha3_uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347".parse().unwrap(), - size: 0x21c_u32.into(), - state_root: "0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544".parse().unwrap(), - timestamp: Nat256::ZERO, + offset += 3; + + assert_eq!(response, alloy_rpc_types::Block { + header: alloy_rpc_types::Header { + hash: b256!("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"), + inner: alloy_consensus::Header { + parent_hash: b256!("0x0000000000000000000000000000000000000000000000000000000000000000"), + ommers_hash: b256!("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"), + beneficiary: address!("0x0000000000000000000000000000000000000000"), + state_root: b256!("0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544"), + transactions_root: b256!("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + receipts_root: b256!("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + logs_bloom: bloom!("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + difficulty: alloy_primitives::U256::from(0x400000000_u64), + number: 0_u64, + gas_limit: 0x1388_u64, + gas_used: 0_u64, + timestamp: 0_u64, + extra_data: bytes!("0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa"), + mix_hash: b256!("0x0000000000000000000000000000000000000000000000000000000000000000"), + nonce: alloy_primitives::B64::from(0x0000000000000042_u64), + base_fee_per_gas: None, + withdrawals_root: None, + blob_gas_used: None, + excess_blob_gas: None, + parent_beacon_block_root: None, + requests_hash: None, + }, total_difficulty: None, - transactions: vec![], - transactions_root: Some("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421".parse().unwrap()), - uncles: vec![], - } - ); + size: Some(alloy_primitives::U256::from(0x21c_u64)), + }, + uncles: vec![], + transactions: BlockTransactions::Hashes(vec![]), + withdrawals: None, + }); } } -#[test] -fn eth_get_block_by_number_should_be_consistent_when_total_difficulty_inconsistent() { - let setup = EvmRpcSetup::new().mock_api_keys(); +#[tokio::test] +async fn eth_get_block_by_number_should_be_consistent_when_total_difficulty_inconsistent() { + let setup = EvmRpcNonblockingSetup::new().await.mock_api_keys().await; let [response_0, mut response_1] = json_rpc_sequential_id( - json!({"jsonrpc":"2.0","result":{"baseFeePerGas":"0xd7232aa34","difficulty":"0x0","extraData":"0x546974616e2028746974616e6275696c6465722e78797a29","gasLimit":"0x1c9c380","gasUsed":"0xa768c4","hash":"0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae","logsBloom":"0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b","miner":"0x388c818ca8b9251b393131c08a736a67ccb19297","mixHash":"0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f","nonce":"0x0000000000000000","number":"0x11db01d","parentHash":"0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae","receiptsRoot":"0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0xcd35","stateRoot":"0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6","timestamp":"0x656f96f3","totalDifficulty":"0xc70d815d562d3cfa955","withdrawalsRoot":"0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48"},"id":0}), + json!({"jsonrpc":"2.0","result":{"baseFeePerGas":"0xd7232aa34","difficulty":"0x0","extraData":"0x546974616e2028746974616e6275696c6465722e78797a29","gasLimit":"0x1c9c380","gasUsed":"0xa768c4","hash":"0xc3674be7b9d95580d7f23c03d32e946f2b453679ee6505e3a778f003c5a3cfae","logsBloom":"0x3e6b8420e1a13038902c24d6c2a9720a7ad4860cdc870cd5c0490011e43631134f608935bd83171247407da2c15d85014f9984608c03684c74aad48b20bc24022134cdca5f2e9d2dee3b502a8ccd39eff8040b1d96601c460e119c408c620b44fa14053013220847045556ea70484e67ec012c322830cf56ef75e09bd0db28a00f238adfa587c9f80d7e30d3aba2863e63a5cad78954555966b1055a4936643366a0bb0b1bac68d0e6267fc5bf8304d404b0c69041125219aa70562e6a5a6362331a414a96d0716990a10161b87dd9568046a742d4280014975e232b6001a0360970e569d54404b27807d7a44c949ac507879d9d41ec8842122da6772101bc8b","miner":"0x388c818ca8b9251b393131c08a736a67ccb19297","mixHash":"0x516a58424d4883a3614da00a9c6f18cd5cd54335a08388229a993a8ecf05042f","nonce":"0x0000000000000000","number":"0x11db01d","parentHash":"0x43325027f6adf9befb223f8ae80db057daddcd7b48e41f60cd94bfa8877181ae","receiptsRoot":"0x66934c3fd9c547036fe0e56ad01bc43c84b170be7c4030a86805ddcdab149929","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0xcd35","stateRoot":"0x13552447dd62f11ad885f21a583c4fa34144efe923c7e35fb018d6710f06b2b6","timestamp":"0x656f96f3","totalDifficulty":"0xc70d815d562d3cfa955","withdrawalsRoot":"0xecae44b2c53871003c5cc75285995764034c9b5978a904229d36c1280b141d48","transactionsRoot":"0x93a1ad3d067009259b508cc95fde63b5efd7e9d8b55754314c173fdde8c0826a"},"id":0}), ); assert_eq!( Some(json!("0xc70d815d562d3cfa955")), @@ -1088,22 +1130,23 @@ fn eth_get_block_by_number_should_be_consistent_when_total_difficulty_inconsiste .remove("totalDifficulty") ); let response = setup - .eth_get_block_by_number( - RpcServices::EthMainnet(Some(vec![ - EthMainnetService::Ankr, - EthMainnetService::PublicNode, - ])), - None, - evm_rpc_types::BlockTag::Latest, - ) - .mock_http_once(MockOutcallBuilder::new(200, response_0)) - .mock_http_once(MockOutcallBuilder::new(200, response_1)) - .wait() + .client() + .with_rpc_sources(RpcServices::EthMainnet(Some(vec![ + EthMainnetService::Ankr, + EthMainnetService::PublicNode, + ]))) + .mock_once(evm_rpc_client::MockOutcallBuilder::new_success([ + response_0, response_1, + ])) + .build() + .get_block_by_number(BlockNumberOrTag::Latest) + .send() + .await .expect_consistent() .unwrap(); - assert_eq!(response.number, 18_722_845_u32.into()); - assert_eq!(response.total_difficulty, None); + assert_eq!(response.number(), 18_722_845_u64); + assert_eq!(response.header.total_difficulty, None); } #[test] From 882dbe5c6f824036c35c5bdada8cd07a64bff31d Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 27 Aug 2025 13:33:07 +0200 Subject: [PATCH 43/57] XC-412: Formatting --- evm_rpc_types/src/response/alloy.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index d8501886..cf6e0dbf 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -62,11 +62,12 @@ impl TryFrom for alloy_rpc_types::Block { ), receipts_root: alloy_primitives::B256::from(value.receipts_root), logs_bloom: alloy_primitives::Bloom::from(value.logs_bloom), - difficulty: value.difficulty.ok_or( - RpcError::ValidationError(ValidationError::Custom( + difficulty: value + .difficulty + .ok_or(RpcError::ValidationError(ValidationError::Custom( "Block does not have a difficulty field".to_string(), - )), - )?.into(), + )))? + .into(), number: u64_try_from_nat256(value.number, "number")?.into(), gas_limit: u64_try_from_nat256(value.gas_limit, "gas_limit")?, gas_used: u64_try_from_nat256(value.gas_used, "gas_used")?, From 50d9bc809a27e2c8362dff6eb7f4325cd166ee85 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 27 Aug 2025 13:35:57 +0200 Subject: [PATCH 44/57] XC-412: Fix error message --- evm_rpc_types/src/response/alloy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index cf6e0dbf..ecd73e13 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -108,7 +108,7 @@ impl TryFrom for alloy_rpc_types::Block { fn u64_try_from_nat256(value: Nat256, field_name: &str) -> Result { u64::try_from(Nat::from(value).0).map_err(|err| { RpcError::ValidationError(ValidationError::Custom(format!( - "Failed to convert block field `{}` to u64: {:?}", + "Failed to convert field `{}` to u64: {:?}", field_name, err ))) }) From 279475528ab20a230f47fd17d2693e81f0098c60 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Wed, 27 Aug 2025 13:36:53 +0200 Subject: [PATCH 45/57] XC-412: Remove useless conversion --- evm_rpc_types/src/response/alloy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index ecd73e13..187c1c92 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -68,7 +68,7 @@ impl TryFrom for alloy_rpc_types::Block { "Block does not have a difficulty field".to_string(), )))? .into(), - number: u64_try_from_nat256(value.number, "number")?.into(), + number: u64_try_from_nat256(value.number, "number")?, gas_limit: u64_try_from_nat256(value.gas_limit, "gas_limit")?, gas_used: u64_try_from_nat256(value.gas_used, "gas_used")?, timestamp: u64_try_from_nat256(value.timestamp, "timestamp")?, From 0011c38c9de6c5aeef7a4e9c68e33e2834a63df1 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 4 Sep 2025 17:17:43 +0200 Subject: [PATCH 46/57] XC-412: Remove `runtime/pocket_ic` --- evm_rpc_client/src/runtime/pocket_ic/mock.rs | 249 ------------------- evm_rpc_client/src/runtime/pocket_ic/mod.rs | 223 ----------------- 2 files changed, 472 deletions(-) delete mode 100644 evm_rpc_client/src/runtime/pocket_ic/mock.rs delete mode 100644 evm_rpc_client/src/runtime/pocket_ic/mod.rs diff --git a/evm_rpc_client/src/runtime/pocket_ic/mock.rs b/evm_rpc_client/src/runtime/pocket_ic/mock.rs deleted file mode 100644 index c1df483a..00000000 --- a/evm_rpc_client/src/runtime/pocket_ic/mock.rs +++ /dev/null @@ -1,249 +0,0 @@ -use canhttp::http::json::JsonRpcRequest; -use dyn_clone::DynClone; -use ic_cdk::api::call::RejectionCode; -use pocket_ic::common::rest::{ - CanisterHttpHeader, CanisterHttpMethod, CanisterHttpReject, CanisterHttpReply, - CanisterHttpRequest, CanisterHttpResponse, -}; -use serde_json::Value; -use std::fmt::Debug; -use std::{ - collections::{BTreeSet, VecDeque}, - iter, - str::FromStr, -}; -use url::{Host, Url}; - -#[derive(Clone, Default)] -pub struct MockOutcallQueue(VecDeque>); - -trait MockOutcallIterator: Iterator + Send + DynClone {} -dyn_clone::clone_trait_object!(MockOutcallIterator); - -impl MockOutcallIterator for T where T: Iterator + Clone + Send + 'static {} - -impl MockOutcallQueue { - pub fn push(&mut self, outcall: impl Into, repeat: MockOutcallRepeat) { - self.0.push_back(match repeat { - MockOutcallRepeat::Once => Box::new(iter::once(outcall.into())), - MockOutcallRepeat::Times(n) => Box::new(iter::repeat_n(outcall.into(), n)), - MockOutcallRepeat::Forever => Box::new(iter::repeat(outcall.into())), - }) - } -} - -impl Iterator for MockOutcallQueue { - type Item = MockOutcall; - - fn next(&mut self) -> Option { - while let Some(iter) = self.0.front_mut() { - match iter.next() { - Some(item) => return Some(item), - None => { - self.0.pop_front(); - } - } - } - None - } -} - -#[derive(Clone, Default)] -pub enum MockOutcallRepeat { - #[default] - Once, - Times(usize), - Forever, -} - -pub fn once() -> MockOutcallRepeat { - MockOutcallRepeat::Once -} - -pub fn times(n: usize) -> MockOutcallRepeat { - MockOutcallRepeat::Times(n) -} - -pub fn forever() -> MockOutcallRepeat { - MockOutcallRepeat::Forever -} - -pub struct MockOutcallBody(pub Vec); - -impl From<&Value> for MockOutcallBody { - fn from(value: &Value) -> Self { - value.to_string().into() - } -} - -impl From for MockOutcallBody { - fn from(value: Value) -> Self { - Self::from(serde_json::to_vec(&value).unwrap()) - } -} - -impl From for MockOutcallBody { - fn from(string: String) -> Self { - string.as_bytes().to_vec().into() - } -} - -impl<'a> From<&'a str> for MockOutcallBody { - fn from(string: &'a str) -> Self { - string.to_string().into() - } -} - -impl From> for MockOutcallBody { - fn from(bytes: Vec) -> Self { - MockOutcallBody(bytes) - } -} - -#[derive(Clone, Debug)] -pub struct MockOutcallBuilder(MockOutcall); - -impl MockOutcallBuilder { - pub fn new(responses: impl IntoIterator) -> Self { - Self(MockOutcall { - method: None, - url: None, - host: None, - request_headers: None, - request_body: None, - max_response_bytes: None, - responses: responses.into_iter().collect(), - }) - } - - pub fn new_success(bodies: impl IntoIterator>) -> Self { - MockOutcallBuilder::new(bodies.into_iter().map(|body| { - CanisterHttpResponse::CanisterHttpReply(CanisterHttpReply { - status: 200, - headers: vec![], - body: body.into().0, - }) - })) - } - - pub fn new_reject(code: RejectionCode, num_providers: usize, message: impl ToString) -> Self { - MockOutcallBuilder::new(vec![ - CanisterHttpResponse::CanisterHttpReject( - CanisterHttpReject { - reject_code: code as u64, - message: message.to_string(), - } - ); - num_providers - ]) - } - - pub fn with_method(mut self, method: CanisterHttpMethod) -> Self { - self.0.method = Some(method); - self - } - - pub fn with_url(mut self, url: impl ToString) -> Self { - self.0.url = Some(url.to_string()); - self - } - - pub fn with_host(mut self, host: &str) -> Self { - self.0.host = Some(Host::parse(host).expect("BUG: invalid host for a URL")); - self - } - - pub fn with_request_headers(mut self, headers: Vec<(impl ToString, impl ToString)>) -> Self { - self.0.request_headers = Some( - headers - .into_iter() - .map(|(name, value)| CanisterHttpHeader { - name: name.to_string(), - value: value.to_string(), - }) - .collect(), - ); - self - } - - pub fn with_raw_request_body(self, body: &str) -> Self { - self.with_request_body(serde_json::from_str(body).unwrap()) - } - - pub fn with_request_body(mut self, body: Value) -> Self { - self.0.request_body = Some(serde_json::from_value(body).unwrap()); - self - } - - pub fn with_max_response_bytes(mut self, max_response_bytes: u64) -> Self { - self.0.max_response_bytes = Some(max_response_bytes); - self - } - - pub fn with_sequential_response_ids(mut self, first_id: u64) -> Self { - for (i, response) in self.0.responses.iter_mut().enumerate() { - if let CanisterHttpResponse::CanisterHttpReply(reply) = response { - let mut body: Value = - serde_json::from_slice(&reply.body).expect("Invalid response body"); - *body.get_mut("id").expect("Missing request ID") = - Value::Number((first_id + i as u64).into()); - reply.body = serde_json::to_vec(&body).expect("Invalid response body"); - } - } - self - } - - pub fn build(self) -> MockOutcall { - self.0 - } -} - -impl From for MockOutcall { - fn from(builder: MockOutcallBuilder) -> Self { - builder.build() - } -} - -#[derive(Clone, Debug)] -pub struct MockOutcall { - pub method: Option, - pub url: Option, - pub host: Option, - pub request_headers: Option>, - pub request_body: Option>, - pub max_response_bytes: Option, - pub responses: Vec, -} - -impl MockOutcall { - pub fn assert_matches(&self, request: &CanisterHttpRequest) { - let req_url = Url::from_str(&request.url).expect("BUG: invalid URL"); - if let Some(ref url) = self.url { - let mock_url = Url::from_str(url).unwrap(); - assert_eq!(mock_url, req_url); - } - if let Some(ref host) = self.host { - assert_eq!( - host, - &req_url.host().expect("BUG: missing host in URL").to_owned() - ); - } - if let Some(ref method) = self.method { - assert_eq!(method, &request.http_method); - } - if let Some(ref headers) = self.request_headers { - assert_eq!( - headers.iter().collect::>(), - request.headers.iter().collect::>() - ); - } - if let Some(ref expected_body) = self.request_body { - let actual_body: JsonRpcRequest = serde_json::from_slice(&request.body) - .expect("BUG: failed to parse JSON request body"); - assert_eq!(expected_body, &actual_body); - } - if let Some(max_response_bytes) = self.max_response_bytes { - assert_eq!(Some(max_response_bytes), request.max_response_bytes); - } - } -} diff --git a/evm_rpc_client/src/runtime/pocket_ic/mod.rs b/evm_rpc_client/src/runtime/pocket_ic/mod.rs deleted file mode 100644 index 1635fd4e..00000000 --- a/evm_rpc_client/src/runtime/pocket_ic/mod.rs +++ /dev/null @@ -1,223 +0,0 @@ -#[allow(missing_docs)] -mod mock; - -use crate::{ClientBuilder, Runtime}; -use async_trait::async_trait; -use candid::{decode_args, utils::ArgumentEncoder, CandidType, Principal}; -use ic_error_types::RejectCode; -pub use mock::{ - forever, once, times, MockOutcall, MockOutcallBody, MockOutcallBuilder, MockOutcallQueue, - MockOutcallRepeat, -}; -use pocket_ic::common::rest::{ - CanisterHttpReject, CanisterHttpRequest, CanisterHttpResponse, MockCanisterHttpResponse, -}; -use pocket_ic::nonblocking::PocketIc; -use pocket_ic::RejectResponse; -use serde::de::DeserializeOwned; -use std::iter; -use std::sync::Mutex; -use std::time::Duration; - -const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; -const MAX_TICKS: usize = 10; - -/// Runtime used in tests with PocketIC. -pub struct PocketIcRuntime<'a> { - /// Main entry point for interacting with PocketIC. - pub env: &'a PocketIc, - /// Default caller [`Principal`] when making inter-canister calls. - pub caller: Principal, - /// Queue that holds the mocked HTTP outcall requests and responses. - /// - /// This field is in a [`Mutex`] so we can use interior mutability to pop the next element from - /// the queue (i.e., perform a mutable operation) within the [`Runtime::update_call`] method which - /// takes an immutable reference to `self`. Furthermore, this has to be thread-safe to be used - /// in multithreaded tests. - pub mocks: Mutex, - /// Default controller [`Principal`] when making inter-canister calls. - pub controller: Principal, -} - -impl Clone for PocketIcRuntime<'_> { - fn clone(&self) -> Self { - Self { - env: self.env, - caller: self.caller, - mocks: Mutex::new(self.mocks.lock().unwrap().clone()), - controller: self.controller, - } - } -} - -#[async_trait] -impl Runtime for PocketIcRuntime<'_> { - async fn update_call( - &self, - id: Principal, - method: &str, - args: In, - _cycles: u128, - ) -> Result - where - In: ArgumentEncoder + Send, - Out: CandidType + DeserializeOwned, - { - // Forward the call through the wallet canister to attach cycles - let message_id = self - .env - .submit_call(id, self.caller, method, encode_args(args)) - .await - .unwrap(); - self.execute_mocks().await; - self.env - .await_call(message_id) - .await - .map(decode_call_response) - .map_err(parse_reject_response)? - } - - async fn query_call( - &self, - id: Principal, - method: &str, - args: In, - ) -> Result - where - In: ArgumentEncoder + Send, - Out: CandidType + DeserializeOwned, - { - self.env - .query_call(id, self.caller, method, encode_args(args)) - .await - .map(decode_call_response) - .map_err(parse_reject_response)? - } -} - -impl PocketIcRuntime<'_> { - // Loops, polling for pending canister HTTP requests and answering them with queued mocks. - // Each batch of requests consumes one mock, with requests validated and responded to via - // `PocketIc::mock_canister_http_response`. Panics if a mock has leftover responses. - async fn execute_mocks(&self) { - loop { - let pending_requests = tick_until_http_requests(self.env).await; - if pending_requests.is_empty() { - return; - } - - if let Some(mock) = { - let mut mocks = self.mocks.lock().unwrap(); - mocks.next() - } { - let mut mocked_responses = mock.responses.clone().into_iter(); - - for (request, response) in iter::zip(pending_requests, mocked_responses.by_ref()) { - mock.assert_matches(&request); - let mock_response = MockCanisterHttpResponse { - subnet_id: request.subnet_id, - request_id: request.request_id, - response: check_response_size(&request, response), - additional_responses: vec![], - }; - self.env.mock_canister_http_response(mock_response).await; - } - - if mocked_responses.next().is_some() { - panic!("Some mocked responses were not consumed") - } - } - } - } -} - -fn check_response_size( - request: &CanisterHttpRequest, - response: CanisterHttpResponse, -) -> CanisterHttpResponse { - if let CanisterHttpResponse::CanisterHttpReply(reply) = &response { - let max_response_bytes = request - .max_response_bytes - .unwrap_or(DEFAULT_MAX_RESPONSE_BYTES); - if reply.body.len() as u64 > max_response_bytes { - // Approximate replica behavior since headers are not accounted for. - return CanisterHttpResponse::CanisterHttpReject(CanisterHttpReject { - reject_code: RejectCode::SysFatal as u64, - message: format!("Http body exceeds size limit of {max_response_bytes} bytes.",), - }); - } - } - 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) -} - -pub fn encode_args(args: In) -> Vec { - candid::encode_args(args).expect("Failed to encode arguments.") -} - -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 - ), - ) - }) -} - -async fn tick_until_http_requests(env: &PocketIc) -> Vec { - let mut requests = Vec::new(); - for _ in 0..MAX_TICKS { - requests = env.get_canister_http().await; - if !requests.is_empty() { - break; - } - env.tick().await; - env.advance_time(Duration::from_nanos(1)).await; - } - requests -} - -impl ClientBuilder> { - /// Add a mock outcall to the queue. - pub fn mock(self, outcall: impl Into, repeat: MockOutcallRepeat) -> Self { - self.with_runtime(|r| { - r.mocks.lock().unwrap().push(outcall, repeat); - r - }) - } - - /// Add a mock outcall to the queue, executed once. - pub fn mock_once(self, outcall: impl Into) -> Self { - self.mock(outcall.into(), once()) - } - - /// Add a sequence of mock outcalls to the queue, each executed once. - pub fn mock_sequence( - mut self, - outcalls: impl IntoIterator>, - ) -> Self { - for outcall in outcalls.into_iter() { - self = self.mock_once(outcall); - } - self - } -} From 027e2c0aea30d243d350a28314cd8276dd92ca8e Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 4 Sep 2025 17:21:46 +0200 Subject: [PATCH 47/57] XC-412: Remove unneeded dependencies --- Cargo.lock | 6 ------ Cargo.toml | 1 - evm_rpc_client/Cargo.toml | 9 --------- 3 files changed, 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4052150c..c9b1c87a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1582,22 +1582,16 @@ dependencies = [ name = "evm_rpc_client" version = "1.4.0" dependencies = [ - "alloy-consensus", "alloy-primitives", "alloy-rpc-types", "async-trait", "candid", - "canhttp", - "dyn-clone", "evm_rpc_types", "ic-cdk", "ic-error-types", - "pocket-ic", "serde", - "serde_json", "strum 0.27.2", "tokio", - "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 22f6b618..aa62a892 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,6 @@ canlog = { version = "0.2.0", features = ["derive"] } candid_parser = { version = "0.1.4" } ciborium = "0.2.2" derive_more = { version = "2.0.1", features = ["from", "into"] } -dyn-clone = "1.0.20" ethnum = { version = "1.5.0", features = ["serde"] } futures = "0.3.31" futures-channel = "0.3.31" diff --git a/evm_rpc_client/Cargo.toml b/evm_rpc_client/Cargo.toml index 524b18d6..cf13b9d4 100644 --- a/evm_rpc_client/Cargo.toml +++ b/evm_rpc_client/Cargo.toml @@ -11,24 +11,15 @@ repository = "https://github.com/dfinity/evm-rpc-canister" documentation = "https://docs.rs/evm_rpc_client" [dependencies] -alloy-consensus = { workspace = true } alloy-primitives = { workspace = true } alloy-rpc-types = { workspace = true } async-trait = { workspace = true } candid = { workspace = true } -canhttp = { workspace = true, optional = true } -dyn-clone = { workspace = true } evm_rpc_types = { path = "../evm_rpc_types", features = ["alloy"] } ic-cdk = { workspace = true } ic-error-types = { workspace = true } -pocket-ic = { workspace = true, optional = true } serde = { workspace = true } -serde_json = { workspace = true, optional = true } strum = { workspace = true } -url = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["full"] } - -[features] -pocket-ic = ["dep:canhttp", "dep:pocket-ic", "dep:serde_json"] \ No newline at end of file From a0b941b2c5a2d0167b8144f18435d186858df15d Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 5 Sep 2025 14:59:47 +0200 Subject: [PATCH 48/57] XC-412: Hide import in docs --- evm_rpc_client/src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index e35c44d9..b2bc81be 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -330,11 +330,10 @@ impl EvmRpcClient { /// use alloy_rpc_types::BlockNumberOrTag; /// use evm_rpc_client::EvmRpcClient; /// - /// # use evm_rpc_types::{Block, Hex, Hex20, Hex32, MultiRpcResult, Nat256}; + /// # use evm_rpc_types::{Block, Hex, Hex20, Hex32, Hex256, MultiRpcResult, Nat256}; /// # use std::str::FromStr; /// # #[tokio::main] /// # async fn main() -> Result<(), Box> { - /// use evm_rpc_types::Hex256; /// let client = EvmRpcClient::builder_for_ic() /// # .with_default_stub_response(MultiRpcResult::Consistent(Ok(Block { /// # base_fee_per_gas: None, @@ -363,7 +362,6 @@ impl EvmRpcClient { /// /// let result = client /// .get_block_by_number(BlockNumberOrTag::Number(23225439)) - /// .with_cycles(10_000_000_000) /// .send() /// .await /// .expect_consistent() From ea496050f953ee449e0e79b9a1b13b6ebe38fd5c Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 5 Sep 2025 15:00:28 +0200 Subject: [PATCH 49/57] XC-412: Remove `with_cycles` from docs --- evm_rpc_client/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/evm_rpc_client/src/lib.rs b/evm_rpc_client/src/lib.rs index b2bc81be..5afeefdc 100644 --- a/evm_rpc_client/src/lib.rs +++ b/evm_rpc_client/src/lib.rs @@ -416,7 +416,6 @@ impl EvmRpcClient { /// /// let result = client /// .get_logs(vec![address!("0xdac17f958d2ee523a2206206994597c13d831ec7")]) - /// .with_cycles(10_000_000_000) /// .send() /// .await /// .expect_consistent(); From 17bc345d9b901f23de0efa35c75b8021c69c2d9f Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 5 Sep 2025 15:02:19 +0200 Subject: [PATCH 50/57] XC-412: Remove `alloy` from default features --- evm_rpc_types/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/evm_rpc_types/Cargo.toml b/evm_rpc_types/Cargo.toml index ce1583e7..df825e8e 100644 --- a/evm_rpc_types/Cargo.toml +++ b/evm_rpc_types/Cargo.toml @@ -30,7 +30,6 @@ proptest = { workspace = true } serde_json = { workspace = true } [features] -default = ["alloy"] alloy = [ "dep:alloy-consensus", "dep:alloy-primitives", From dde85efe22fe050d947d34d38722f9a168baac75 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 5 Sep 2025 15:32:07 +0200 Subject: [PATCH 51/57] XC-412: Add more prop tests for `alloy` conversions --- evm_rpc_types/src/tests.rs | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/evm_rpc_types/src/tests.rs b/evm_rpc_types/src/tests.rs index 1bccb8d8..42a57fdc 100644 --- a/evm_rpc_types/src/tests.rs +++ b/evm_rpc_types/src/tests.rs @@ -185,14 +185,25 @@ mod hex_string { #[cfg(feature = "alloy")] mod alloy_conversion_tests { use super::*; - use alloy_primitives::{Address, Bytes, B256}; + use alloy_primitives::{Address, Bloom, Bytes, B256, B64, U256}; + use std::any; proptest! { #[test] - fn should_convert_to_and_from_alloy(hex20 in arb_hex20(), hex32 in arb_hex32(), hex in arb_hex()) { + fn should_convert_to_and_from_alloy( + hex20 in arb_hex20(), + hex32 in arb_hex32(), + hex256 in arb_hex256(), + hex in arb_hex(), + wrapped_u64 in arb_u64(), + nat256 in arb_nat256(), + ) { prop_assert_eq!(hex20.clone(), Hex20::from(Address::from(hex20))); prop_assert_eq!(hex32.clone(), Hex32::from(B256::from(hex32))); + prop_assert_eq!(hex256.clone(), Hex256::from(Bloom::from(hex256))); prop_assert_eq!(hex.clone(), Hex::from(Bytes::from(hex))); + prop_assert_eq!(wrapped_u64.clone(), Nat256::from(B64::from(wrapped_u64))); + prop_assert_eq!(nat256.clone(), Nat256::from(U256::from(nat256))); } } @@ -204,9 +215,21 @@ mod alloy_conversion_tests { arb_var_len_hex_string(32..=32_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) } + fn arb_hex256() -> impl Strategy { + arb_var_len_hex_string(256..=256_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) + } + fn arb_hex() -> impl Strategy { arb_var_len_hex_string(0..=100_usize).prop_map(|s| Hex::from_str(s.as_str()).unwrap()) } + + fn arb_u64() -> impl Strategy { + any::().prop_map(Nat256::from) + } + + fn arb_nat256() -> impl Strategy { + any::<[u8; 32]>().prop_map(Nat256::from_be_bytes) + } } fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { From 7d371863a95013d18bfa5f55b0cb8547f317dca7 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Fri, 5 Sep 2025 17:40:44 +0200 Subject: [PATCH 52/57] XC-412: Handle pre- and post-Paris `difficulty` --- evm_rpc_types/src/alloy.rs | 7 +++--- evm_rpc_types/src/response/alloy.rs | 39 ++++++++++++++++++----------- evm_rpc_types/src/tests.rs | 5 ++-- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/evm_rpc_types/src/alloy.rs b/evm_rpc_types/src/alloy.rs index 2e2fa7d7..fbd9b9fa 100644 --- a/evm_rpc_types/src/alloy.rs +++ b/evm_rpc_types/src/alloy.rs @@ -1,3 +1,4 @@ +use alloy_primitives::ruint::{ToUintError, UintTryFrom}; use crate::{Hex, Hex20, Hex256, Hex32, Nat256, RpcError}; impl From for alloy_primitives::Address { @@ -42,9 +43,9 @@ impl From for alloy_primitives::U256 { } } -impl From for Nat256 { - fn from(value: alloy_primitives::U256) -> Self { - Nat256::from_be_bytes(value.to_be_bytes()) +impl UintTryFrom for alloy_primitives::U256 { + fn uint_try_from(value: Nat256) -> Result> { + Ok(alloy_primitives::U256::from_be_bytes(value.into_be_bytes())) } } diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index 187c1c92..b6d208e0 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -1,4 +1,5 @@ -use crate::{Block, LogEntry, Nat256, RpcError, ValidationError}; +use crate::{Block, Hex32, LogEntry, Nat256, RpcError, ValidationError}; +use alloy_primitives::{B256, U256}; use alloy_rpc_types::BlockTransactions; use candid::Nat; @@ -53,21 +54,10 @@ impl TryFrom for alloy_rpc_types::Block { ommers_hash: alloy_primitives::BlockHash::from(value.sha3_uncles), beneficiary: alloy_primitives::Address::from(value.miner), state_root: alloy_primitives::B256::from(value.state_root), - transactions_root: alloy_primitives::B256::from( - value.transactions_root.ok_or(RpcError::ValidationError( - ValidationError::Custom( - "Block does not have a transactions root field".to_string(), - ), - ))?, - ), + transactions_root: validate_transactions_root(value.transactions_root)?, receipts_root: alloy_primitives::B256::from(value.receipts_root), logs_bloom: alloy_primitives::Bloom::from(value.logs_bloom), - difficulty: value - .difficulty - .ok_or(RpcError::ValidationError(ValidationError::Custom( - "Block does not have a difficulty field".to_string(), - )))? - .into(), + difficulty: validate_difficulty(&value.number, value.difficulty)?, number: u64_try_from_nat256(value.number, "number")?, gas_limit: u64_try_from_nat256(value.gas_limit, "gas_limit")?, gas_used: u64_try_from_nat256(value.gas_used, "gas_used")?, @@ -105,6 +95,27 @@ impl TryFrom for alloy_rpc_types::Block { } } +fn validate_difficulty(number: &Nat256, difficulty: Option) -> Result { + const PARIS_BLOCK: u64 = 15_537_394; + if number.as_ref() < &Nat::from(PARIS_BLOCK) { + difficulty + .map(U256::from) + .ok_or(RpcError::ValidationError(ValidationError::Custom( + "Block before Paris upgrade but missing difficulty".into(), + ))) + } else { + Ok(difficulty.map(U256::from).unwrap_or_default()) + } +} + +fn validate_transactions_root(transactions_root: Option) -> Result { + transactions_root + .map(alloy_primitives::B256::from) + .ok_or(RpcError::ValidationError(ValidationError::Custom( + "Block does not have a transactions root field".to_string(), + ))) +} + fn u64_try_from_nat256(value: Nat256, field_name: &str) -> Result { u64::try_from(Nat::from(value).0).map_err(|err| { RpcError::ValidationError(ValidationError::Custom(format!( diff --git a/evm_rpc_types/src/tests.rs b/evm_rpc_types/src/tests.rs index 42a57fdc..a5bbfb30 100644 --- a/evm_rpc_types/src/tests.rs +++ b/evm_rpc_types/src/tests.rs @@ -186,7 +186,6 @@ mod hex_string { mod alloy_conversion_tests { use super::*; use alloy_primitives::{Address, Bloom, Bytes, B256, B64, U256}; - use std::any; proptest! { #[test] @@ -202,7 +201,7 @@ mod alloy_conversion_tests { prop_assert_eq!(hex32.clone(), Hex32::from(B256::from(hex32))); prop_assert_eq!(hex256.clone(), Hex256::from(Bloom::from(hex256))); prop_assert_eq!(hex.clone(), Hex::from(Bytes::from(hex))); - prop_assert_eq!(wrapped_u64.clone(), Nat256::from(B64::from(wrapped_u64))); + prop_assert_eq!(wrapped_u64.clone(), Nat256::from(B64::try_from(wrapped_u64).unwrap())); prop_assert_eq!(nat256.clone(), Nat256::from(U256::from(nat256))); } } @@ -216,7 +215,7 @@ mod alloy_conversion_tests { } fn arb_hex256() -> impl Strategy { - arb_var_len_hex_string(256..=256_usize).prop_map(|s| Hex32::from_str(s.as_str()).unwrap()) + arb_var_len_hex_string(256..=256_usize).prop_map(|s| Hex256::from_str(s.as_str()).unwrap()) } fn arb_hex() -> impl Strategy { From 84160e99020c43600ab00601f4a791c9bff8aed1 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 8 Sep 2025 07:51:34 +0200 Subject: [PATCH 53/57] XC-412: Formatting --- evm_rpc_types/src/alloy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evm_rpc_types/src/alloy.rs b/evm_rpc_types/src/alloy.rs index fbd9b9fa..feeace4c 100644 --- a/evm_rpc_types/src/alloy.rs +++ b/evm_rpc_types/src/alloy.rs @@ -1,5 +1,5 @@ -use alloy_primitives::ruint::{ToUintError, UintTryFrom}; use crate::{Hex, Hex20, Hex256, Hex32, Nat256, RpcError}; +use alloy_primitives::ruint::{ToUintError, UintTryFrom}; impl From for alloy_primitives::Address { fn from(value: Hex20) -> Self { From 2ee22d1fed2e8d9762f817793559380278ac4c68 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 8 Sep 2025 07:56:52 +0200 Subject: [PATCH 54/57] XC-412: Fix `Nat256` to `U256` conversion --- evm_rpc_types/src/alloy.rs | 6 +++--- evm_rpc_types/src/response/alloy.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/evm_rpc_types/src/alloy.rs b/evm_rpc_types/src/alloy.rs index feeace4c..4ecc823d 100644 --- a/evm_rpc_types/src/alloy.rs +++ b/evm_rpc_types/src/alloy.rs @@ -37,9 +37,9 @@ impl From for Hex { } } -impl From for alloy_primitives::U256 { - fn from(value: Nat256) -> Self { - alloy_primitives::U256::from_be_bytes(value.into_be_bytes()) +impl From for Nat256 { + fn from(value: alloy_primitives::U256) -> Self { + Nat256::from_be_bytes(value.to_be_bytes()) } } diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index b6d208e0..f71a9005 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -75,8 +75,8 @@ impl TryFrom for alloy_rpc_types::Block { parent_beacon_block_root: None, requests_hash: None, }, - total_difficulty: value.total_difficulty.map(|value| value.into()), - size: Some(value.size.into()), + total_difficulty: value.total_difficulty.map(U256::from), + size: Some(U256::from(value.size)), }, uncles: value .uncles From 60e1f558cbd22fb460df82c1ff6782585edb7394 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 8 Sep 2025 09:29:40 +0200 Subject: [PATCH 55/57] XC-412: Split unit tests in pre- and post-Paris --- evm_rpc_types/src/response/test.rs | 93 +++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/evm_rpc_types/src/response/test.rs b/evm_rpc_types/src/response/test.rs index 712f1d07..3c634d0c 100644 --- a/evm_rpc_types/src/response/test.rs +++ b/evm_rpc_types/src/response/test.rs @@ -1,8 +1,11 @@ use crate::{Block, Hex, Hex20, Hex256, Hex32, LogEntry, Nat256}; use num_bigint::BigUint; use proptest::{ - arbitrary::any, collection::vec, option, prelude::Strategy, prop_assert_eq, prop_compose, - proptest, + arbitrary::any, + collection::vec, + option, + prelude::{Just, Strategy}, + prop_assert_eq, prop_compose, proptest, }; use serde_json::Value; use std::{ops::RangeInclusive, str::FromStr}; @@ -16,6 +19,8 @@ use std::{ops::RangeInclusive, str::FromStr}; mod alloy_conversion_tests { use super::*; + const PARIS_BLOCK: u64 = 15_537_394; + proptest! { #[test] fn should_convert_log_to_alloy(entry in arb_log_entry()) { @@ -30,32 +35,72 @@ mod alloy_conversion_tests { } #[test] - fn should_convert_block_to_alloy(block in arb_block()) { + fn should_convert_pre_paris_block_to_alloy(block in arb_pre_paris_block()) { let serialized = serde_json::to_value(&block).unwrap(); - let mut alloy_serialized = serde_json::to_value(&alloy_rpc_types::Block::try_from(block.clone()).unwrap()).unwrap(); - hex_to_u32_digits(&mut alloy_serialized, "baseFeePerGas"); - hex_to_u32_digits(&mut alloy_serialized, "number"); - hex_to_u32_digits(&mut alloy_serialized, "difficulty"); - hex_to_u32_digits(&mut alloy_serialized, "gasLimit"); - hex_to_u32_digits(&mut alloy_serialized, "gasUsed"); - hex_to_u32_digits(&mut alloy_serialized, "nonce"); - hex_to_u32_digits(&mut alloy_serialized, "size"); - hex_to_u32_digits(&mut alloy_serialized, "timestamp"); - hex_to_u32_digits(&mut alloy_serialized, "totalDifficulty"); - add_null_if_absent(&mut alloy_serialized, "baseFeePerGas"); - add_null_if_absent(&mut alloy_serialized, "totalDifficulty"); + let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); + let alloy_serialized = serialize_alloy_block(alloy_block); + + prop_assert_eq!(serialized, alloy_serialized); + } + + #[test] + fn should_convert_post_paris_block_to_alloy(block in arb_post_paris_block()) { + let mut serialized = serde_json::to_value(&block).unwrap(); + + let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); + let alloy_serialized = serialize_alloy_block(alloy_block); + + // For post-Paris blocks, the difficulty field is optional. However, alloy requires the + // value to always be present (i.e., it uses a value of 0x0 instead of null). + // To be able to compare the serialized values, we therefore convert null values of + // difficulty to a serialized 0x0 (i.e., an empty array). + // NOTE: We do this AFTER converting to alloy, only to compare the serialized values. + null_to_zero(&mut serialized, "difficulty"); prop_assert_eq!(serialized, alloy_serialized); } } + fn serialize_alloy_block(block: alloy_rpc_types::Block) -> Value { + let mut alloy_serialized = serde_json::to_value(&block).unwrap(); + hex_to_u32_digits(&mut alloy_serialized, "baseFeePerGas"); + hex_to_u32_digits(&mut alloy_serialized, "number"); + hex_to_u32_digits(&mut alloy_serialized, "difficulty"); + hex_to_u32_digits(&mut alloy_serialized, "gasLimit"); + hex_to_u32_digits(&mut alloy_serialized, "gasUsed"); + hex_to_u32_digits(&mut alloy_serialized, "nonce"); + hex_to_u32_digits(&mut alloy_serialized, "size"); + hex_to_u32_digits(&mut alloy_serialized, "timestamp"); + hex_to_u32_digits(&mut alloy_serialized, "totalDifficulty"); + add_null_if_absent(&mut alloy_serialized, "baseFeePerGas"); + add_null_if_absent(&mut alloy_serialized, "totalDifficulty"); + alloy_serialized + } + + fn arb_pre_paris_block() -> impl Strategy { + arb_block( + (0..PARIS_BLOCK).prop_map(Nat256::from), + arb_nat256().prop_map(Some), + ) + } + + fn arb_post_paris_block() -> impl Strategy { + arb_block( + (PARIS_BLOCK..).prop_map(Nat256::from), + option::of(Just(Nat256::ZERO)), + ) + } + prop_compose! { - fn arb_block() + fn arb_block( + number_strategy: impl Strategy, + difficulty_strategy: impl Strategy> + ) ( base_fee_per_gas in option::of(arb_u64()), - number in arb_u64(), - difficulty in arb_nat256(), + number in number_strategy, + difficulty in difficulty_strategy, extra_data in arb_hex(), gas_limit in arb_u64(), gas_used in arb_u64(), @@ -78,8 +123,7 @@ mod alloy_conversion_tests { Block { base_fee_per_gas, number, - // alloy requires the `difficulty` field be present - difficulty: Some(difficulty), + difficulty, extra_data, gas_limit, gas_used, @@ -96,7 +140,8 @@ mod alloy_conversion_tests { timestamp, total_difficulty, transactions, - // alloy requires the `transactions_root` field be present + // The `transactionsRoot` field is mandatory as per the Ethereum JSON-RPC API. + // See: https://ethereum.github.io/execution-apis/api-documentation/ transactions_root: Some(transactions_root), uncles, } @@ -178,6 +223,12 @@ mod alloy_conversion_tests { serialized[field] = Value::Null; } } + + fn null_to_zero(serialized: &mut Value, field: &str) { + if let Some(Value::Null) = serialized.get(field) { + serialized[field] = Value::Array(vec![]); + } + } } fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { From 20d8c70e7f3aa45dba9909198fa69f07e20eb6ee Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Mon, 8 Sep 2025 15:18:43 +0200 Subject: [PATCH 56/57] XC-412: Move canonicalization to functions --- evm_rpc_types/src/response/alloy.rs | 7 ++- evm_rpc_types/src/response/test.rs | 73 ++++++++++++++--------------- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/evm_rpc_types/src/response/alloy.rs b/evm_rpc_types/src/response/alloy.rs index f71a9005..ea483c83 100644 --- a/evm_rpc_types/src/response/alloy.rs +++ b/evm_rpc_types/src/response/alloy.rs @@ -104,7 +104,12 @@ fn validate_difficulty(number: &Nat256, difficulty: Option) -> Result Ok(U256::ZERO), + _ => Err(RpcError::ValidationError(ValidationError::Custom( + "Block after Paris upgrade with non-zero difficulty".into(), + ))), + } } } diff --git a/evm_rpc_types/src/response/test.rs b/evm_rpc_types/src/response/test.rs index 3c634d0c..06b99a65 100644 --- a/evm_rpc_types/src/response/test.rs +++ b/evm_rpc_types/src/response/test.rs @@ -26,12 +26,10 @@ mod alloy_conversion_tests { fn should_convert_log_to_alloy(entry in arb_log_entry()) { let serialized = serde_json::to_value(&entry).unwrap(); - let mut alloy_serialized = serde_json::to_value(&alloy_rpc_types::Log::try_from(entry.clone()).unwrap()).unwrap(); - hex_to_u32_digits(&mut alloy_serialized, "transactionIndex"); - hex_to_u32_digits(&mut alloy_serialized, "logIndex"); - hex_to_u32_digits(&mut alloy_serialized, "blockNumber"); + let alloy_log = alloy_rpc_types::Log::try_from(entry.clone()).unwrap(); + let alloy_serialized = serde_json::to_value(&alloy_log).unwrap(); - prop_assert_eq!(serialized, alloy_serialized); + prop_assert_eq!(canonicalize_log(serialized), canonicalize_log(alloy_serialized)); } #[test] @@ -39,43 +37,50 @@ mod alloy_conversion_tests { let serialized = serde_json::to_value(&block).unwrap(); let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); - let alloy_serialized = serialize_alloy_block(alloy_block); + let alloy_serialized = serde_json::to_value(&alloy_block).unwrap(); - prop_assert_eq!(serialized, alloy_serialized); + prop_assert_eq!(canonicalize_block(serialized), canonicalize_block(alloy_serialized)); } #[test] fn should_convert_post_paris_block_to_alloy(block in arb_post_paris_block()) { - let mut serialized = serde_json::to_value(&block).unwrap(); + let serialized = serde_json::to_value(&block).unwrap(); let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); - let alloy_serialized = serialize_alloy_block(alloy_block); - - // For post-Paris blocks, the difficulty field is optional. However, alloy requires the - // value to always be present (i.e., it uses a value of 0x0 instead of null). - // To be able to compare the serialized values, we therefore convert null values of - // difficulty to a serialized 0x0 (i.e., an empty array). - // NOTE: We do this AFTER converting to alloy, only to compare the serialized values. - null_to_zero(&mut serialized, "difficulty"); + let alloy_serialized = serde_json::to_value(&alloy_block).unwrap(); - prop_assert_eq!(serialized, alloy_serialized); + prop_assert_eq!(canonicalize_block(serialized), canonicalize_block(alloy_serialized)); } } - fn serialize_alloy_block(block: alloy_rpc_types::Block) -> Value { - let mut alloy_serialized = serde_json::to_value(&block).unwrap(); - hex_to_u32_digits(&mut alloy_serialized, "baseFeePerGas"); - hex_to_u32_digits(&mut alloy_serialized, "number"); - hex_to_u32_digits(&mut alloy_serialized, "difficulty"); - hex_to_u32_digits(&mut alloy_serialized, "gasLimit"); - hex_to_u32_digits(&mut alloy_serialized, "gasUsed"); - hex_to_u32_digits(&mut alloy_serialized, "nonce"); - hex_to_u32_digits(&mut alloy_serialized, "size"); - hex_to_u32_digits(&mut alloy_serialized, "timestamp"); - hex_to_u32_digits(&mut alloy_serialized, "totalDifficulty"); - add_null_if_absent(&mut alloy_serialized, "baseFeePerGas"); - add_null_if_absent(&mut alloy_serialized, "totalDifficulty"); - alloy_serialized + fn canonicalize_log(mut serialized_log: Value) -> Value { + // Convert hex-encoded numerical values to arrays of `u32` digits. + hex_to_u32_digits(&mut serialized_log, "transactionIndex"); + hex_to_u32_digits(&mut serialized_log, "logIndex"); + hex_to_u32_digits(&mut serialized_log, "blockNumber"); + serialized_log + } + + fn canonicalize_block(mut serialized_block: Value) -> Value { + // For post-Paris blocks, the difficulty field is optional. However, the `difficulty` field + // is mandatory in the `alloy_rpc_types::Block` type. Therefore, convert `null` values to 0. + if let Some(Value::Null) = serialized_block.get("difficulty") { + serialized_block["difficulty"] = Value::from("0x0"); + } + // Convert hex-encoded numerical values to arrays of `u32` digits. + hex_to_u32_digits(&mut serialized_block, "baseFeePerGas"); + hex_to_u32_digits(&mut serialized_block, "number"); + hex_to_u32_digits(&mut serialized_block, "difficulty"); + hex_to_u32_digits(&mut serialized_block, "gasLimit"); + hex_to_u32_digits(&mut serialized_block, "gasUsed"); + hex_to_u32_digits(&mut serialized_block, "nonce"); + hex_to_u32_digits(&mut serialized_block, "size"); + hex_to_u32_digits(&mut serialized_block, "timestamp"); + hex_to_u32_digits(&mut serialized_block, "totalDifficulty"); + // Add `null` for values that alloy skips during serialization when they are absent. + add_null_if_absent(&mut serialized_block, "baseFeePerGas"); + add_null_if_absent(&mut serialized_block, "totalDifficulty"); + serialized_block } fn arb_pre_paris_block() -> impl Strategy { @@ -223,12 +228,6 @@ mod alloy_conversion_tests { serialized[field] = Value::Null; } } - - fn null_to_zero(serialized: &mut Value, field: &str) { - if let Some(Value::Null) = serialized.get(field) { - serialized[field] = Value::Array(vec![]); - } - } } fn arb_var_len_hex_string(num_bytes_range: RangeInclusive) -> impl Strategy { From bdbdac70cf81dcbc13fa154dbac7a4e235d6f6c0 Mon Sep 17 00:00:00 2001 From: Louis Pahlavi Date: Thu, 11 Sep 2025 09:48:17 +0200 Subject: [PATCH 57/57] XC-412: Apply canonicalization less generously --- evm_rpc_types/src/response/test.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/evm_rpc_types/src/response/test.rs b/evm_rpc_types/src/response/test.rs index 06b99a65..93027c58 100644 --- a/evm_rpc_types/src/response/test.rs +++ b/evm_rpc_types/src/response/test.rs @@ -29,7 +29,7 @@ mod alloy_conversion_tests { let alloy_log = alloy_rpc_types::Log::try_from(entry.clone()).unwrap(); let alloy_serialized = serde_json::to_value(&alloy_log).unwrap(); - prop_assert_eq!(canonicalize_log(serialized), canonicalize_log(alloy_serialized)); + prop_assert_eq!(serialized, canonicalize_log(alloy_serialized)); } #[test] @@ -39,17 +39,26 @@ mod alloy_conversion_tests { let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); let alloy_serialized = serde_json::to_value(&alloy_block).unwrap(); - prop_assert_eq!(canonicalize_block(serialized), canonicalize_block(alloy_serialized)); + prop_assert_eq!(serialized, canonicalize_block(alloy_serialized)); } #[test] fn should_convert_post_paris_block_to_alloy(block in arb_post_paris_block()) { + // For post-Paris blocks, the difficulty field is optional. However, the `difficulty` field + // is mandatory in the `alloy_rpc_types::Block` type. Therefore, convert `null` values to 0. + fn canonicalize_difficulty (mut serialized_block: Value) -> Value { + if let Some(Value::Null) = serialized_block.get("difficulty") { + serialized_block["difficulty"] = Value::from(Vec::::new()); + } + serialized_block + } + let serialized = serde_json::to_value(&block).unwrap(); let alloy_block = alloy_rpc_types::Block::try_from(block.clone()).unwrap(); let alloy_serialized = serde_json::to_value(&alloy_block).unwrap(); - prop_assert_eq!(canonicalize_block(serialized), canonicalize_block(alloy_serialized)); + prop_assert_eq!(canonicalize_difficulty(serialized), canonicalize_block(alloy_serialized)); } } @@ -62,11 +71,6 @@ mod alloy_conversion_tests { } fn canonicalize_block(mut serialized_block: Value) -> Value { - // For post-Paris blocks, the difficulty field is optional. However, the `difficulty` field - // is mandatory in the `alloy_rpc_types::Block` type. Therefore, convert `null` values to 0. - if let Some(Value::Null) = serialized_block.get("difficulty") { - serialized_block["difficulty"] = Value::from("0x0"); - } // Convert hex-encoded numerical values to arrays of `u32` digits. hex_to_u32_digits(&mut serialized_block, "baseFeePerGas"); hex_to_u32_digits(&mut serialized_block, "number");