From 411fc84791bb3ab55a8f14a2c5942c805d5f1efa Mon Sep 17 00:00:00 2001 From: Wodann Date: Sat, 11 May 2024 00:45:09 +0000 Subject: [PATCH 01/18] WIP: split --- Cargo.lock | 39 ++++++++++++++----- crates/edr_eth/Cargo.toml | 22 +++-------- crates/edr_rpc_client/Cargo.toml | 30 ++++++++++++++ .../remote => edr_rpc_client/src}/client.rs | 17 ++++---- .../remote => edr_rpc_client/src}/jsonrpc.rs | 0 crates/edr_rpc_client/src/lib.rs | 11 ++++++ .../src}/reqwest_error.rs | 0 crates/edr_rpc_eth/Cargo.toml | 6 +++ .../remote => edr_rpc_eth/src}/block_spec.rs | 0 .../src}/cacheable_method_invocation.rs | 0 .../eth => edr_rpc_eth/src}/call_request.rs | 0 .../remote => edr_rpc_eth/src}/chain_id.rs | 0 .../src/remote => edr_rpc_eth/src}/eth.rs | 4 -- .../src/remote => edr_rpc_eth/src}/filter.rs | 0 .../src/remote.rs => edr_rpc_eth/src/lib.rs} | 8 ++-- .../remote => edr_rpc_eth/src}/override.rs | 0 .../src}/request_methods.rs | 0 17 files changed, 91 insertions(+), 46 deletions(-) create mode 100644 crates/edr_rpc_client/Cargo.toml rename crates/{edr_eth/src/remote => edr_rpc_client/src}/client.rs (99%) rename crates/{edr_eth/src/remote => edr_rpc_client/src}/jsonrpc.rs (100%) create mode 100644 crates/edr_rpc_client/src/lib.rs rename crates/{edr_eth/src/remote/client => edr_rpc_client/src}/reqwest_error.rs (100%) create mode 100644 crates/edr_rpc_eth/Cargo.toml rename crates/{edr_eth/src/remote => edr_rpc_eth/src}/block_spec.rs (100%) rename crates/{edr_eth/src/remote => edr_rpc_eth/src}/cacheable_method_invocation.rs (100%) rename crates/{edr_eth/src/remote/eth => edr_rpc_eth/src}/call_request.rs (100%) rename crates/{edr_eth/src/remote => edr_rpc_eth/src}/chain_id.rs (100%) rename crates/{edr_eth/src/remote => edr_rpc_eth/src}/eth.rs (98%) rename crates/{edr_eth/src/remote => edr_rpc_eth/src}/filter.rs (100%) rename crates/{edr_eth/src/remote.rs => edr_rpc_eth/src/lib.rs} (68%) rename crates/{edr_eth/src/remote => edr_rpc_eth/src}/override.rs (100%) rename crates/{edr_eth/src/remote => edr_rpc_eth/src}/request_methods.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index aac32c0c6..b5047c0d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1050,35 +1050,25 @@ dependencies = [ "c-kzg", "edr_defaults", "edr_test_utils", - "futures", "hash-db", "hash256-std-hasher", "hashbrown 0.14.3", "hex", - "hyper", "itertools 0.10.5", "k256", "lazy_static", "log", - "mockito", "paste", - "regex", - "reqwest", - "reqwest-middleware", - "reqwest-retry", - "reqwest-tracing", "revm-primitives", "serde", "serde_json", "serial_test", "sha2", "sha3", - "tempfile", "thiserror", "tokio", "tracing", "triehash", - "url", "uuid", "walkdir", ] @@ -1181,6 +1171,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "edr_rpc_client" +version = "0.3.5" +dependencies = [ + "anyhow", + "edr_eth", + "futures", + "hyper", + "lazy_static", + "log", + "mockito", + "regex", + "reqwest", + "reqwest-middleware", + "reqwest-retry", + "reqwest-tracing", + "serde", + "serde_json", + "tempfile", + "thiserror", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "edr_rpc_eth" +version = "0.1.0" + [[package]] name = "edr_solidity" version = "0.3.5" diff --git a/crates/edr_eth/Cargo.toml b/crates/edr_eth/Cargo.toml index c7b520221..eb1a35d6e 100644 --- a/crates/edr_eth/Cargo.toml +++ b/crates/edr_eth/Cargo.toml @@ -7,50 +7,38 @@ edition = "2021" anyhow = "1.0.75" alloy-rlp = { version = "0.3", default-features = false, features = ["derive"] } c-kzg = { version = "1.0.0", default-features = false } -futures = { version = "0.3.28", default-features = false } hash-db = { version = "0.15.2", default-features = false } hash256-std-hasher = { version = "0.15.2", default-features = false } hashbrown = { version = "0.14.3", default-features = false, features = ["ahash", "allocator-api2", "inline-more"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } -hyper = { version = "0.14.27", default-features = false } itertools = { version = "0.10.5", default-features = false, features = ["use_alloc"] } k256 = { version = "0.13.1", default-features = false, features = ["arithmetic", "ecdsa", "pkcs8", ] } -lazy_static = { version = "1.4.0", default-features = false } log = { version = "0.4.17", default-features = false } -reqwest = { version = "0.11", features = ["blocking", "json"] } -reqwest-middleware = { version = "0.2.4", default-features = false } -reqwest-retry = { version = "0.3.0", default-features = false } -reqwest-tracing = { version = "0.4.7", default-features = false, optional = true } revm-primitives = { git = "https://github.com/NomicFoundation/revm", rev = "aceb093", version = "3.1", default-features = false, features = ["c-kzg", "hashbrown"] } serde = { version = "1.0.147", default-features = false, features = ["derive"], optional = true } -serde_json = { version = "1.0.89", optional = true } sha2 = { version = "0.10.8", default-features = false } sha3 = { version = "0.10.8", default-features = false } thiserror = { version = "1.0.37", default-features = false } -tokio = { version = "1.21.2", default-features = false, features = ["fs", "macros", "sync"] } tracing = { version = "0.1.37", features = ["attributes", "std"], optional = true } triehash = { version = "0.8.4", default-features = false } uuid = { version = "1.4.1", default-features = false, features = ["v4"] } -url = "2.4.1" -regex = "1.10.0" [dev-dependencies] anyhow = "1.0.75" assert-json-diff = "2.0.2" edr_defaults = { version = "0.3.5", path = "../edr_defaults" } +edr_test_utils = { version = "0.3.5", path = "../edr_test_utils" } lazy_static = "1.4.0" -mockito = { version = "1.0.2", default-features = false } paste = { version = "1.0.14", default-features = false } -edr_test_utils = { version = "0.3.5", path = "../edr_test_utils" } +serde_json = { version = "1.0.89" } serial_test = "2.0.0" -tempfile = { version = "3.7.1", default-features = false } tokio = { version = "1.23.0", features = ["macros"] } walkdir = { version = "2.3.3", default-features = false } [features] default = ["std"] rand = ["revm-primitives/rand"] -serde = ["dep:serde", "c-kzg/serde", "revm-primitives/serde", "serde_json"] -std = ["futures/std", "hash256-std-hasher/std", "hash-db/std", "hex/std", "itertools/use_std", "k256/std", "k256/precomputed-tables", "revm-primitives/std", "serde?/std", "sha2/std", "sha3/std", "triehash/std", "uuid/std"] -tracing = ["dep:tracing", "reqwest-tracing"] +serde = ["dep:serde", "c-kzg/serde", "revm-primitives/serde"] +std = ["hash256-std-hasher/std", "hash-db/std", "hex/std", "itertools/use_std", "k256/std", "k256/precomputed-tables", "revm-primitives/std", "serde?/std", "sha2/std", "sha3/std", "triehash/std", "uuid/std"] +tracing = ["dep:tracing"] test-remote = ["serde"] diff --git a/crates/edr_rpc_client/Cargo.toml b/crates/edr_rpc_client/Cargo.toml new file mode 100644 index 000000000..ae6736bf3 --- /dev/null +++ b/crates/edr_rpc_client/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "edr_rpc_client" +version = "0.3.5" +edition = "2021" + +[dependencies] +anyhow = { version = "1.0.75", default-features = false, features = ["std"] } +edr_eth = { version = "0.3.5", path = "../edr_eth", features = ["serde"] } +futures = { version = "0.3.28", default-features = false, features = ["std"] } +hyper = { version = "0.14.27", default-features = false } +lazy_static = { version = "1.4.0", default-features = false } +log = { version = "0.4.17", default-features = false } +regex = "1.10.0" +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json"] } +reqwest-middleware = { version = "0.2.4", default-features = false } +reqwest-retry = { version = "0.3.0", default-features = false } +reqwest-tracing = { version = "0.4.7", default-features = false, optional = true } +serde = { version = "1.0.147", default-features = false, features = ["derive", "std"] } +serde_json = { version = "1.0.89" } +thiserror = { version = "1.0.37", default-features = false } +tokio = { version = "1.21.2", default-features = false, features = ["fs", "macros", "sync"] } +tracing = { version = "0.1.37", default-features = false, features = ["attributes", "std"], optional = true } +url = { version = "2.4.1", default-features = false } + +[dev-dependencies] +mockito = { version = "1.0.2", default-features = false } +tempfile = { version = "3.7.1", default-features = false } + +[features] +tracing = ["dep:tracing", "dep:reqwest-tracing"] diff --git a/crates/edr_eth/src/remote/client.rs b/crates/edr_rpc_client/src/client.rs similarity index 99% rename from crates/edr_eth/src/remote/client.rs rename to crates/edr_rpc_client/src/client.rs index 6c3609052..250d2af52 100644 --- a/crates/edr_eth/src/remote/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -1,5 +1,3 @@ -mod reqwest_error; - use std::{ fmt::Debug, io, @@ -8,6 +6,13 @@ use std::{ time::{Duration, Instant}, }; +use edr_eth::{ + eth, + filter::{LogFilterOptions, OneOrMore}, + jsonrpc, + request_methods::RequestMethod, + BlockSpec, Bytecode, PreEip1898BlockSpec, KECCAK_EMPTY, +}; use futures::{future, stream::StreamExt, TryFutureExt}; use hyper::header::HeaderValue; pub use hyper::{header, http::Error as HttpError, HeaderMap}; @@ -16,18 +21,10 @@ use reqwest_middleware::{ClientBuilder as HttpClientBuilder, ClientWithMiddlewar use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; #[cfg(feature = "tracing")] use reqwest_tracing::TracingMiddleware; -use revm_primitives::{Bytecode, KECCAK_EMPTY}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tokio::sync::{OnceCell, RwLock}; use uuid::Uuid; -use super::{ - eth, - filter::{LogFilterOptions, OneOrMore}, - jsonrpc, - request_methods::RequestMethod, - BlockSpec, PreEip1898BlockSpec, -}; pub use crate::remote::client::reqwest_error::{MiddlewareError, ReqwestError}; use crate::{ block::{block_time, is_safe_block_number, IsSafeBlockNumberArgs}, diff --git a/crates/edr_eth/src/remote/jsonrpc.rs b/crates/edr_rpc_client/src/jsonrpc.rs similarity index 100% rename from crates/edr_eth/src/remote/jsonrpc.rs rename to crates/edr_rpc_client/src/jsonrpc.rs diff --git a/crates/edr_rpc_client/src/lib.rs b/crates/edr_rpc_client/src/lib.rs new file mode 100644 index 000000000..29bf127da --- /dev/null +++ b/crates/edr_rpc_client/src/lib.rs @@ -0,0 +1,11 @@ +#![warn(missing_docs)] + +//! Ethereum JSON-RPC client + +mod client; +mod reqwest_error; + +/// Types specific to JSON-RPC +pub mod jsonrpc; + +pub use client::{RpcClient, RpcClientError}; diff --git a/crates/edr_eth/src/remote/client/reqwest_error.rs b/crates/edr_rpc_client/src/reqwest_error.rs similarity index 100% rename from crates/edr_eth/src/remote/client/reqwest_error.rs rename to crates/edr_rpc_client/src/reqwest_error.rs diff --git a/crates/edr_rpc_eth/Cargo.toml b/crates/edr_rpc_eth/Cargo.toml new file mode 100644 index 000000000..fc69ae579 --- /dev/null +++ b/crates/edr_rpc_eth/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "edr_rpc_eth" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/edr_eth/src/remote/block_spec.rs b/crates/edr_rpc_eth/src/block_spec.rs similarity index 100% rename from crates/edr_eth/src/remote/block_spec.rs rename to crates/edr_rpc_eth/src/block_spec.rs diff --git a/crates/edr_eth/src/remote/cacheable_method_invocation.rs b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs similarity index 100% rename from crates/edr_eth/src/remote/cacheable_method_invocation.rs rename to crates/edr_rpc_eth/src/cacheable_method_invocation.rs diff --git a/crates/edr_eth/src/remote/eth/call_request.rs b/crates/edr_rpc_eth/src/call_request.rs similarity index 100% rename from crates/edr_eth/src/remote/eth/call_request.rs rename to crates/edr_rpc_eth/src/call_request.rs diff --git a/crates/edr_eth/src/remote/chain_id.rs b/crates/edr_rpc_eth/src/chain_id.rs similarity index 100% rename from crates/edr_eth/src/remote/chain_id.rs rename to crates/edr_rpc_eth/src/chain_id.rs diff --git a/crates/edr_eth/src/remote/eth.rs b/crates/edr_rpc_eth/src/eth.rs similarity index 98% rename from crates/edr_eth/src/remote/eth.rs rename to crates/edr_rpc_eth/src/eth.rs index d79694212..6d0381d7b 100644 --- a/crates/edr_eth/src/remote/eth.rs +++ b/crates/edr_rpc_eth/src/eth.rs @@ -6,12 +6,8 @@ // - https://github.com/gakonst/ethers-rs/blob/7e6c3ba98363bdf6131e8284f186cc2c70ff48c3/LICENSE-MIT // For the original context, see https://github.com/gakonst/ethers-rs/tree/7e6c3ba98363bdf6131e8284f186cc2c70ff48c3 -/// Input type for `eth_call` and `eth_estimateGas` -mod call_request; - use std::fmt::Debug; -pub use self::call_request::CallRequest; use crate::{ access_list::AccessListItem, withdrawal::Withdrawal, Address, Bloom, Bytes, B256, B64, U256, }; diff --git a/crates/edr_eth/src/remote/filter.rs b/crates/edr_rpc_eth/src/filter.rs similarity index 100% rename from crates/edr_eth/src/remote/filter.rs rename to crates/edr_rpc_eth/src/filter.rs diff --git a/crates/edr_eth/src/remote.rs b/crates/edr_rpc_eth/src/lib.rs similarity index 68% rename from crates/edr_eth/src/remote.rs rename to crates/edr_rpc_eth/src/lib.rs index 821e71d99..2f5b1c489 100644 --- a/crates/edr_eth/src/remote.rs +++ b/crates/edr_rpc_eth/src/lib.rs @@ -1,19 +1,17 @@ mod block_spec; mod cacheable_method_invocation; +/// Input type for `eth_call` and `eth_estimateGas` +mod call_request; mod chain_id; -/// an Ethereum JSON-RPC client -pub mod client; /// ethereum objects as specifically used in the JSON-RPC interface pub mod eth; /// data types for use with filter-based RPC methods pub mod filter; -/// data types specific to JSON-RPC but not specific to Ethereum. -pub mod jsonrpc; mod r#override; mod request_methods; pub use self::{ block_spec::{BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec}, - client::{RpcClient, RpcClientError}, + call_request::CallRequest, r#override::*, }; diff --git a/crates/edr_eth/src/remote/override.rs b/crates/edr_rpc_eth/src/override.rs similarity index 100% rename from crates/edr_eth/src/remote/override.rs rename to crates/edr_rpc_eth/src/override.rs diff --git a/crates/edr_eth/src/remote/request_methods.rs b/crates/edr_rpc_eth/src/request_methods.rs similarity index 100% rename from crates/edr_eth/src/remote/request_methods.rs rename to crates/edr_rpc_eth/src/request_methods.rs From 504dab0ca3f8f316c2927c7843ff5ea8f036ead3 Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 27 May 2024 22:56:00 +0000 Subject: [PATCH 02/18] WIP: make RPC client generic --- Cargo.lock | 20 +- crates/edr_eth/Cargo.toml | 4 +- .../src/block_spec.rs | 0 crates/{edr_rpc_eth => edr_eth}/src/filter.rs | 29 +- crates/edr_eth/src/lib.rs | 11 +- crates/edr_eth/src/log.rs | 27 +- crates/edr_eth/src/receipt.rs | 63 -- crates/edr_eth/tests/receipt.rs | 48 - crates/edr_rpc_client/Cargo.toml | 4 + crates/edr_rpc_client/src/cache.rs | 112 +++ crates/edr_rpc_client/src/cache/block_spec.rs | 88 ++ crates/edr_rpc_client/src/cache/filter.rs | 102 +++ crates/edr_rpc_client/src/cache/hasher.rs | 168 ++++ crates/edr_rpc_client/src/cache/key.rs | 144 +++ .../src/chain_id.rs | 0 crates/edr_rpc_client/src/client.rs | 460 ++-------- crates/edr_rpc_client/src/jsonrpc.rs | 27 +- crates/edr_rpc_client/src/lib.rs | 14 +- crates/edr_rpc_eth/Cargo.toml | 15 +- .../src/cacheable_method_invocation.rs | 835 +++++------------ crates/edr_rpc_eth/src/call_request.rs | 7 +- crates/edr_rpc_eth/src/client.rs | 328 +++++++ crates/edr_rpc_eth/src/eth.rs | 93 -- crates/edr_rpc_eth/src/lib.rs | 12 +- crates/edr_rpc_eth/src/override.rs | 4 +- crates/edr_rpc_eth/src/request_methods.rs | 23 +- crates/edr_rpc_eth/src/transaction.rs | 94 ++ crates/edr_rpc_eth/tests/client.rs | 846 ++++++++++++++++++ crates/edr_rpc_eth/tests/receipt.rs | 106 +++ 29 files changed, 2397 insertions(+), 1287 deletions(-) rename crates/{edr_rpc_eth => edr_eth}/src/block_spec.rs (100%) rename crates/{edr_rpc_eth => edr_eth}/src/filter.rs (86%) delete mode 100644 crates/edr_eth/tests/receipt.rs create mode 100644 crates/edr_rpc_client/src/cache.rs create mode 100644 crates/edr_rpc_client/src/cache/block_spec.rs create mode 100644 crates/edr_rpc_client/src/cache/filter.rs create mode 100644 crates/edr_rpc_client/src/cache/hasher.rs create mode 100644 crates/edr_rpc_client/src/cache/key.rs rename crates/{edr_rpc_eth => edr_rpc_client}/src/chain_id.rs (100%) create mode 100644 crates/edr_rpc_eth/src/client.rs create mode 100644 crates/edr_rpc_eth/src/transaction.rs create mode 100644 crates/edr_rpc_eth/tests/client.rs create mode 100644 crates/edr_rpc_eth/tests/receipt.rs diff --git a/Cargo.lock b/Cargo.lock index b5047c0d3..71b160c8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,9 +445,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", @@ -1069,7 +1069,6 @@ dependencies = [ "tokio", "tracing", "triehash", - "uuid", "walkdir", ] @@ -1178,6 +1177,7 @@ dependencies = [ "anyhow", "edr_eth", "futures", + "hex", "hyper", "lazy_static", "log", @@ -1189,16 +1189,28 @@ dependencies = [ "reqwest-tracing", "serde", "serde_json", + "sha3", "tempfile", "thiserror", "tokio", "tracing", "url", + "uuid", ] [[package]] name = "edr_rpc_eth" -version = "0.1.0" +version = "0.3.5" +dependencies = [ + "anyhow", + "async-trait", + "edr_eth", + "edr_rpc_client", + "hex", + "serde", + "serde_json", + "thiserror", +] [[package]] name = "edr_solidity" diff --git a/crates/edr_eth/Cargo.toml b/crates/edr_eth/Cargo.toml index eb1a35d6e..8d4ccb991 100644 --- a/crates/edr_eth/Cargo.toml +++ b/crates/edr_eth/Cargo.toml @@ -21,7 +21,6 @@ sha3 = { version = "0.10.8", default-features = false } thiserror = { version = "1.0.37", default-features = false } tracing = { version = "0.1.37", features = ["attributes", "std"], optional = true } triehash = { version = "0.8.4", default-features = false } -uuid = { version = "1.4.1", default-features = false, features = ["v4"] } [dev-dependencies] anyhow = "1.0.75" @@ -39,6 +38,5 @@ walkdir = { version = "2.3.3", default-features = false } default = ["std"] rand = ["revm-primitives/rand"] serde = ["dep:serde", "c-kzg/serde", "revm-primitives/serde"] -std = ["hash256-std-hasher/std", "hash-db/std", "hex/std", "itertools/use_std", "k256/std", "k256/precomputed-tables", "revm-primitives/std", "serde?/std", "sha2/std", "sha3/std", "triehash/std", "uuid/std"] +std = ["hash256-std-hasher/std", "hash-db/std", "hex/std", "itertools/use_std", "k256/std", "k256/precomputed-tables", "revm-primitives/std", "serde?/std", "sha2/std", "sha3/std", "triehash/std"] tracing = ["dep:tracing"] -test-remote = ["serde"] diff --git a/crates/edr_rpc_eth/src/block_spec.rs b/crates/edr_eth/src/block_spec.rs similarity index 100% rename from crates/edr_rpc_eth/src/block_spec.rs rename to crates/edr_eth/src/block_spec.rs diff --git a/crates/edr_rpc_eth/src/filter.rs b/crates/edr_eth/src/filter.rs similarity index 86% rename from crates/edr_rpc_eth/src/filter.rs rename to crates/edr_eth/src/filter.rs index d34d1b6ac..13561bef8 100644 --- a/crates/edr_rpc_eth/src/filter.rs +++ b/crates/edr_eth/src/filter.rs @@ -1,8 +1,6 @@ use std::mem::take; -use hashbrown::HashSet; - -use crate::{log::FilterLog, remote::BlockSpec, Address, Bytes, B256}; +use crate::{block_spec::BlockSpec, log::FilterLog, Address, Bytes, B256}; /// A type that can be used to pass either one or many objects to a JSON-RPC /// request @@ -176,28 +174,3 @@ impl<'a> serde::Deserialize<'a> for SubscriptionType { deserializer.deserialize_identifier(SubscriptionTypeVisitor) } } - -/// Whether the log address matches the address filter. -pub fn matches_address_filter(log_address: &Address, address_filter: &HashSet
) -> bool { - address_filter.is_empty() || address_filter.contains(log_address) -} - -/// Whether the log topics match the topics filter. -pub fn matches_topics_filter(log_topics: &[B256], topics_filter: &[Option>]) -> bool { - if topics_filter.len() > log_topics.len() { - return false; - } - - topics_filter - .iter() - .zip(log_topics.iter()) - .all(|(normalized_topics, log_topic)| { - normalized_topics - .as_ref() - .map_or(true, |normalized_topics| { - normalized_topics - .iter() - .any(|normalized_topic| *normalized_topic == *log_topic) - }) - }) -} diff --git a/crates/edr_eth/src/lib.rs b/crates/edr_eth/src/lib.rs index 1b563b3e4..9249105a8 100644 --- a/crates/edr_eth/src/lib.rs +++ b/crates/edr_eth/src/lib.rs @@ -13,13 +13,14 @@ pub mod account; pub mod beacon; /// Ethereum block types pub mod block; +/// Ethereum block spec +mod block_spec; +/// Ethereum types for filter-based RPC methods +pub mod filter; /// Ethereum log types pub mod log; /// Ethereum receipt types pub mod receipt; -/// Remote node interaction -#[cfg(feature = "serde")] -pub mod remote; /// Ethereum gas related types pub mod reward_percentile; /// RLP traits and functions @@ -42,9 +43,11 @@ pub mod withdrawal; pub use revm_primitives::{ alloy_primitives::{Bloom, BloomInput, B512, B64, U64}, - hex_literal, AccountInfo, Address, Bytes, HashMap, SpecId, B256, U256, + hex_literal, AccountInfo, Address, Bytes, HashMap, HashSet, SpecId, B256, U256, }; +pub use self::block_spec::{BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec}; + /// A secret key pub type Secret = B256; /// A public key diff --git a/crates/edr_eth/src/log.rs b/crates/edr_eth/src/log.rs index c70d92434..c3b058d29 100644 --- a/crates/edr_eth/src/log.rs +++ b/crates/edr_eth/src/log.rs @@ -9,7 +9,7 @@ pub use self::{ filter::FilterLog, receipt::ReceiptLog, }; -use crate::{Bloom, BloomInput}; +use crate::{Address, Bloom, BloomInput, HashSet, B256}; /// Adds the log to a bloom hash. pub fn add_log_to_bloom(log: &Log, bloom: &mut Bloom) { @@ -19,3 +19,28 @@ pub fn add_log_to_bloom(log: &Log, bloom: &mut Bloom) { .iter() .for_each(|topic| bloom.accrue(BloomInput::Raw(topic.as_slice()))); } + +/// Whether the log address matches the address filter. +pub fn matches_address_filter(log_address: &Address, address_filter: &HashSet
) -> bool { + address_filter.is_empty() || address_filter.contains(log_address) +} + +/// Whether the log topics match the topics filter. +pub fn matches_topics_filter(log_topics: &[B256], topics_filter: &[Option>]) -> bool { + if topics_filter.len() > log_topics.len() { + return false; + } + + topics_filter + .iter() + .zip(log_topics.iter()) + .all(|(normalized_topics, log_topic)| { + normalized_topics + .as_ref() + .map_or(true, |normalized_topics| { + normalized_topics + .iter() + .any(|normalized_topic| *normalized_topic == *log_topic) + }) + }) +} diff --git a/crates/edr_eth/src/receipt.rs b/crates/edr_eth/src/receipt.rs index 4d03d7b0b..351fa060e 100644 --- a/crates/edr_eth/src/receipt.rs +++ b/crates/edr_eth/src/receipt.rs @@ -516,67 +516,4 @@ mod tests { eip2930 => TypedReceiptData::Eip2930 { status: 1 }, eip1559 => TypedReceiptData::Eip1559 { status: 0 }, } - - #[cfg(feature = "test-remote")] - mod alchemy { - use super::*; - - macro_rules! impl_test_receipt_rlp_encoding { - ($( - $name:ident: $transaction_hash:literal => $encoding:literal, - )+) => { - $( - paste::item! { - #[tokio::test] - async fn []() { - use edr_test_utils::env::get_alchemy_url; - use tempfile::TempDir; - - use crate::{remote::RpcClient, B256}; - - let tempdir = TempDir::new().unwrap(); - let client = RpcClient::new(&get_alchemy_url(), tempdir.path().into(), None).unwrap(); - - let transaction_hash = B256::from_slice(&hex::decode($transaction_hash).unwrap()); - - let receipt = client - .get_transaction_receipt(&transaction_hash) - .await - .expect("Should succeed") - .expect("Receipt must exist"); - - // Generated by Hardhat - let expected = hex::decode($encoding).unwrap(); - - assert_eq!(alloy_rlp::encode(&receipt).to_vec(), expected); - - let decoded = TypedReceipt::::decode(&mut expected.as_slice()).unwrap(); - let receipt = TypedReceipt { - data: receipt.inner.inner.data, - spec_id: SpecId::LATEST, - cumulative_gas_used: receipt.inner.inner.cumulative_gas_used, - logs_bloom: receipt.inner.inner.logs_bloom, - logs: receipt.inner.inner.logs.into_iter().map(|log| { - log.inner.inner.inner.clone() - }).collect(), - }; - - assert_eq!(decoded, receipt); - } - } - )+ - }; - } - - impl_test_receipt_rlp_encoding! { - pre_eip658: "427b0b68b1ccc46b01d99ed399b61c4ae681e22216035eb6953afc83ef463e17" - => "f90128a08af7cdcc6991b441f6285f4298df6add6506b3fd3ca559b0682fb2d57929652f825208b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", - post_eip658: "1421a887a02301ae127bf2cd4c006116053c9dc4a255e69ea403a2d77c346cf5" - => "f9010801825208b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", - eip2930: "01361649690dbff1ac1da1a7351a125b7cb0f26b9c5e017c4aefe90135b9be14" - => "01f90109018351e668b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", - eip1559: "3dac2080b4c423029fcc9c916bc430cde441badfe736fc6d1fe9325348af80fd" - => "02f91c7701830ea827b9010000201000000000000004040080005010004000000000000000000000280004102000000000000400004001800001010003000000080020000000004100200010020000000000000800000008080220a04000a00100400040000000008000000010200200001000000000b000000000004100804000000540100040100009002000010100000010000000004000100000000000004020000828000244001000080200006000020400080000804100004000800801a000000401010000000000000100000200080000010000000088140008202280000000100008008200102800c010200000000320000002000020000000004000010080100000000040000000f91b6cf9017c949008d19f58aabd9ed0d60971565aa8510560ab41f842a0a07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17a0000000000000000000000000f967aa80d80d6f22df627219c5113a118b57d0efb901200000000000000000000000009e46a38f5daabe8683e10793b06749eef7d733d1000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000069e10de76676d080000000000000000000000000000000000000000000000000000128e23797a37671e700000000000000000000000000000000000000000000004efc8c538e1084000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000038d11030660bf632eb31345d6b5f3e6ba2d2311cc783f3764c405f5272200dd670f967aa80d80d6f22df627219c5113a118b57d0ef61e0b5bc0000000000000000f9017c949008d19f58aabd9ed0d60971565aa8510560ab41f842a0a07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17a000000000000000000000000003bada9ff1cf0d0264664b43977ed08feee32584b90120000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000c7283b66eb1eb5fb86327f08e1b5816b0720212b000000000000000000000000000000000000000000000000fbec1330f280b3fd000000000000000000000000000000000000000000000a322816740bfd499e6600000000000000000000000000000000000000000000000000b46ffd87079be800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000380437203aec40e6c16642904f125e7d97bf45f39a51a729d775b8e3ea3cc667cd03bada9ff1cf0d0264664b43977ed08feee3258461e0b5d90000000000000000f89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000f967aa80d80d6f22df627219c5113a118b57d0efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a00000000000000000000000000000000000000000000069e10de76676d0800000f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000003bada9ff1cf0d0264664b43977ed08feee32584a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a0000000000000000000000000000000000000000000000000fbec1330f280b3fdf89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000006992115b12e8bffc0000f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000005aaa0053fa5c28e8c558d4c648cc129bea45018a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000000000000000000000000000065ca4d7e75041e08f89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000005aaa0053fa5c28e8c558d4c648cc129bea45018a0000000000000000000000000000000000000000000002330b073b0f83ffeaaaaf9011c9405aaa0053fa5c28e8c558d4c648cc129bea45018f863a0c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b8a0000000000000000000000000000000000000000000002330b073b0f83ffeaaaaffffffffffffffffffffffffffffffffffffffffffffffff9a35b2818afbe1f8000000000000000000000000000000000000000001af7ab4eebc6eaabac2b8540000000000000000000000000000000000000000000008462c8201774345ee54fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe778cf89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000119c71d3bbac22029622cbaec24854d3d32d2828a000000000000000000000000000000000000000000000466160e761f07ffd5556f89b94119c71d3bbac22029622cbaec24854d3d32d2828f842a0b9ed0243fdf00f0545c63a0af8850c090d86bb46682baec4bf3c496814fe4f02a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b8401fd446014fc12b89bb0aee29f847d7fbf7e4237a671c70945013f952ca4a567a00000000000000000000000000000000000000000000000000000000a6444848f89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000ca90ac7c132da0602b69b84af2b6a69a905379a2a000000000000000000000000000000000000000000000466160e761f07ffd5556f89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000ca90ac7c132da0602b69b84af2b6a69a905379a2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000000000000000000000000000000000000afdf72bb8f89b94dac17f958d2ee523a2206206994597c13d831ec7f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000001111111254fb6c44bac0bed2854e76f90643097da00000000000000000000000000000000000000000000000000000000afdf72bb8f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000945bcf562085de2d5875b9e2012ed5fd5cfab927a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000000c96c4c288f973ff8f89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000945bcf562085de2d5875b9e2012ed5fd5cfab927a00000000000000000000000000000000000000000000000000000000afdf72bb8f879941111111254fb6c44bac0bed2854e76f90643097de1a0c3b639f02b125bfa160e50739b8c44eb2d1b6908e2b6d5925c6d770f2ca78127b840a7036786313db68944e1789fa8a69f90c41e29301442c6731a151688acbe6a26000000000000000000000000000000000000000000000000c96c4c288f973ff8f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000dd9f24efc84d93deef3c8745c837ab63e80abd27a00000000000000000000000000000000000000000000000000654620f6124ec19f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000000000000000000000000000128e23797a37671e7f8f99411111112542d85b3ef69ae05771c2dccff4faa26e1a0d6d4f5681c246c9f42c203e287975af1601f8df8035a9251f79aab5c8f09e2f8b8c00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab410000000000000000000000009e46a38f5daabe8683e10793b06749eef7d733d1000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41000000000000000000000000000000000000000000006992115b12e8bffc000000000000000000000000000000000000000000000000000128e23797a37671e7f89b949008d19f58aabd9ed0d60971565aa8510560ab41f842a0ed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2a000000000000000000000000011111112542d85b3ef69ae05771c2dccff4faa26b84000000000000000000000000000000000000000000000000000000000000000007c02520000000000000000000000000000000000000000000000000000000000f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000000fb37a3336b791815f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8a0000000000000000000000000000000000000000000000000fb37a3336b791815f8dd94ba12222222228d8ba445958a75a0704d566bf2c8f884a02170c741c41531aec20e7c107c24eecfdd15e69c9bb0a8dd37b1840b9e0b207ba00b09dea16768f0799065c475be02919503cb2a3500020000000000000000001aa0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a00000000000000000000000006b175474e89094c44da98b954eedeac495271d0fb840000000000000000000000000000000000000000000000000fb37a3336b791815000000000000000000000000000000000000000000000c74f130b221944b84caf89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8a0000000000000000000000000000000000000000000000000fb37a3336b791815f89b946b175474e89094c44da98b954eedeac495271d0ff863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000c74f130b221944b84caf89b94956f47f50a910163d8bf957cf5846d573e7f87caf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000bb2e5c2ff298fd96e166f90c8abacaf714df14f8a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000c74da893438a10088bff89b946b175474e89094c44da98b954eedeac495271d0ff863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000bb2e5c2ff298fd96e166f90c8abacaf714df14f8a0000000000000000000000000000000000000000000000c74f130b221944b84caf9011c94bb2e5c2ff298fd96e166f90c8abacaf714df14f8f863a0c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b8a0000000000000000000000000000000000000000000000c74f130b221944b84cafffffffffffffffffffffffffffffffffffffffffffff38b2576cbc75eff77410000000000000000000000000000000000000001000f16fba6ff427463b0357d00000000000000000000000000000000000000000fefa81144cbfbd548a25a7c0000000000000000000000000000000000000000000000000000000000000004f89b94956f47f50a910163d8bf957cf5846d573e7f87caf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000009928e4046d7c6513326ccea028cd3e7a91c7590aa0000000000000000000000000000000000000000000000c74da893438a10088bff89b94c7283b66eb1eb5fb86327f08e1b5816b0720212bf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009928e4046d7c6513326ccea028cd3e7a91c7590aa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f879949928e4046d7c6513326ccea028cd3e7a91c7590ae1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b8400000000000000000000000000000000000000000007f015873850f3f6be1a2500000000000000000000000000000000000000000006821664c5f0a4b332570a2f8fc949928e4046d7c6513326ccea028cd3e7a91c7590af863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b880000000000000000000000000000000000000000000000c74da893438a10088bf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f89b94c7283b66eb1eb5fb86327f08e1b5816b0720212bf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a0000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f8f99411111112542d85b3ef69ae05771c2dccff4faa26e1a0d6d4f5681c246c9f42c203e287975af1601f8df8035a9251f79aab5c8f09e2f8b8c00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000c7283b66eb1eb5fb86327f08e1b5816b0720212b0000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41000000000000000000000000000000000000000000000000fb37a3336b791815000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f89b949008d19f58aabd9ed0d60971565aa8510560ab41f842a0ed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2a000000000000000000000000011111112542d85b3ef69ae05771c2dccff4faa26b84000000000000000000000000000000000000000000000000000000000000000007c02520000000000000000000000000000000000000000000000000000000000f87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000000000000000000000000000128e23797a37671e7f89b949008d19f58aabd9ed0d60971565aa8510560ab41f842a0ed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2a0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b84000000000000000000000000000000000000000000000000000000000000000002e1a7d4d00000000000000000000000000000000000000000000000000000000f89b94c7283b66eb1eb5fb86327f08e1b5816b0720212bf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000003bada9ff1cf0d0264664b43977ed08feee32584a0000000000000000000000000000000000000000000000a322816740bfd499e66f85a949008d19f58aabd9ed0d60971565aa8510560ab41f842a040338ce1a7c49204f0099533b1e9a7ee0a3d261f84974ab7af36105b8c4e9db4a0000000000000000000000000de1c59bc25d806ad9ddcbe246c4b5e550564571880", - } - } } diff --git a/crates/edr_eth/tests/receipt.rs b/crates/edr_eth/tests/receipt.rs deleted file mode 100644 index b6ebb3851..000000000 --- a/crates/edr_eth/tests/receipt.rs +++ /dev/null @@ -1,48 +0,0 @@ -#[cfg(feature = "test-remote")] -mod remote { - use serial_test::serial; - - macro_rules! impl_test_remote_block_receipt_root { - ($( - $name:ident => $block_number:literal, - )+) => { - $( - paste::item! { - #[tokio::test] - #[serial] - async fn []() { - use edr_eth::{remote::{RpcClient, PreEip1898BlockSpec}, trie::ordered_trie_root}; - use edr_test_utils::env::get_alchemy_url; - - let client = RpcClient::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None).expect("url ok"); - - let block = client - .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::Number($block_number)) - .await - .expect("Should succeed"); - - let receipts = client.get_transaction_receipts(block.transactions.iter().map(|transaction| &transaction.hash)) - .await - .expect("Should succeed") - .expect("All receipts of a block should exist"); - - let receipts_root = ordered_trie_root( - receipts - .into_iter() - .map(|receipt| alloy_rlp::encode(&**receipt)), - ); - - assert_eq!(block.receipts_root, receipts_root); - } - } - )+ - }; - } - - impl_test_remote_block_receipt_root! { - pre_eip658 => 1_500_000u64, - post_eip658 => 5_370_000u64, - eip2930 => 12_751_000u64, // block contains at least one transaction with type 1 - eip1559 => 14_000_000u64, // block contains at least one transaction with type 2 - } -} diff --git a/crates/edr_rpc_client/Cargo.toml b/crates/edr_rpc_client/Cargo.toml index ae6736bf3..9263894fe 100644 --- a/crates/edr_rpc_client/Cargo.toml +++ b/crates/edr_rpc_client/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = { version = "1.0.75", default-features = false, features = ["std"] } edr_eth = { version = "0.3.5", path = "../edr_eth", features = ["serde"] } futures = { version = "0.3.28", default-features = false, features = ["std"] } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } hyper = { version = "0.14.27", default-features = false } lazy_static = { version = "1.4.0", default-features = false } log = { version = "0.4.17", default-features = false } @@ -17,14 +18,17 @@ reqwest-retry = { version = "0.3.0", default-features = false } reqwest-tracing = { version = "0.4.7", default-features = false, optional = true } serde = { version = "1.0.147", default-features = false, features = ["derive", "std"] } serde_json = { version = "1.0.89" } +sha3 = { version = "0.10.8", default-features = false, features = ["std"] } thiserror = { version = "1.0.37", default-features = false } tokio = { version = "1.21.2", default-features = false, features = ["fs", "macros", "sync"] } tracing = { version = "0.1.37", default-features = false, features = ["attributes", "std"], optional = true } url = { version = "2.4.1", default-features = false } +uuid = { version = "1.4.1", default-features = false, features = ["std", "v4"] } [dev-dependencies] mockito = { version = "1.0.2", default-features = false } tempfile = { version = "3.7.1", default-features = false } [features] +test-remote = [] tracing = ["dep:tracing", "dep:reqwest-tracing"] diff --git a/crates/edr_rpc_client/src/cache.rs b/crates/edr_rpc_client/src/cache.rs new file mode 100644 index 000000000..0bdf7fa10 --- /dev/null +++ b/crates/edr_rpc_client/src/cache.rs @@ -0,0 +1,112 @@ +mod block_spec; +mod filter; +mod hasher; +/// Types for indexing the cache. +pub mod key; + +use std::{ + fmt::Debug, + io, + path::{Path, PathBuf}, + time::Instant, +}; + +use serde::de::DeserializeOwned; + +pub use self::hasher::Hasher as CacheKeyHasher; +use self::key::{ReadCacheKey, WriteCacheKey}; +use crate::RpcClientError; + +/// Trait for types that can be cached. +pub trait CacheableMethod: Sized { + type MethodWithResolvableBlockTag: Clone + Debug; + + /// Creates a method for requesting the block number. + fn block_number_request() -> Self; + + /// Creates a method for requesting the chain ID. + fn chain_id_request() -> Self; + + /// Resolves a block tag to a block number for the provided method. + fn resolve_block_tag(method: Self::MethodWithResolvableBlockTag, block_number: u64) -> Self; + + fn read_cache_key(&self) -> Option; + + fn write_cache_key(&self) -> Option>; +} + +#[derive(Debug, Clone)] +pub(crate) struct CachedBlockNumber { + pub block_number: u64, + pub timestamp: Instant, +} + +impl CachedBlockNumber { + /// Creates a new instance with the current time. + pub fn new(block_number: u64) -> Self { + Self { + block_number, + timestamp: Instant::now(), + } + } +} + +/// Wrapper for IO and JSON errors specific to the cache. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// An IO error + #[error(transparent)] + Io(#[from] io::Error), + /// A JSON parsing error + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +#[derive(Debug, Clone)] +pub(crate) struct Response { + pub value: serde_json::Value, + pub path: PathBuf, +} + +impl Response { + pub async fn parse(self) -> Result { + match serde_json::from_value(self.value.clone()) { + Ok(result) => Ok(result), + Err(error) => { + // Remove the file from cache if the contents don't match the expected type. + // This can happen for example if a new field is added to a type. + remove_from_cache(&self.path).await?; + Err(RpcClientError::InvalidResponse { + response: self.value.to_string(), + expected_type: std::any::type_name::(), + error, + }) + } + } + } +} + +/// Don't fail the request, just log an error if we fail to read/write from +/// cache. +pub(crate) fn log_error(cache_key: &str, message: &'static str, error: impl Into) { + let cache_error = RpcClientError::CacheError { + message: message.to_string(), + cache_key: cache_key.to_string(), + error: error.into(), + }; + log::error!("{cache_error}"); +} + +pub(crate) async fn remove_from_cache(path: &Path) -> Result<(), RpcClientError> { + match tokio::fs::remove_file(path).await { + Ok(_) => Ok(()), + Err(error) => { + log_error( + path.to_str().unwrap_or(""), + "failed to remove from RPC response cache", + error, + ); + Ok(()) + } + } +} diff --git a/crates/edr_rpc_client/src/cache/block_spec.rs b/crates/edr_rpc_client/src/cache/block_spec.rs new file mode 100644 index 000000000..1863ab74c --- /dev/null +++ b/crates/edr_rpc_client/src/cache/block_spec.rs @@ -0,0 +1,88 @@ +use edr_eth::{BlockSpec, BlockTag, Eip1898BlockSpec, B256}; + +use super::key::CacheKeyVariant; + +/// A block argument specification that is potentially cacheable. +#[derive(Clone, Debug)] +pub enum CacheableBlockSpec<'a> { + /// Block number + Number { block_number: u64 }, + /// Block hash + Hash { + block_hash: &'a B256, + require_canonical: Option, + }, + /// "earliest" block tag + Earliest, + /// "safe" block tag + Safe, + /// "finalized" block tag + Finalized, +} + +impl<'a> CacheKeyVariant for CacheableBlockSpec<'a> { + fn cache_key_variant(&self) -> u8 { + match self { + CacheableBlockSpec::Number { .. } => 0, + CacheableBlockSpec::Hash { .. } => 1, + CacheableBlockSpec::Earliest => 2, + CacheableBlockSpec::Safe => 3, + CacheableBlockSpec::Finalized => 4, + } + } +} + +/// Error type for [`CacheableBlockSpec::try_from`]. +#[derive(thiserror::Error, Debug)] +#[error("Block spec is not cacheable: {0:?}")] +pub struct BlockSpecNotCacheableError(Option); + +impl<'a> TryFrom<&'a BlockSpec> for CacheableBlockSpec<'a> { + type Error = BlockSpecNotCacheableError; + + fn try_from(value: &'a BlockSpec) -> Result { + match value { + BlockSpec::Number(block_number) => Ok(CacheableBlockSpec::Number { + block_number: *block_number, + }), + BlockSpec::Tag(tag) => match tag { + // Latest and pending can be never resolved to a safe block number. + BlockTag::Latest | BlockTag::Pending => { + Err(BlockSpecNotCacheableError(Some(value.clone()))) + } + // Earliest, safe and finalized are potentially resolvable to a safe block number. + BlockTag::Earliest => Ok(CacheableBlockSpec::Earliest), + BlockTag::Safe => Ok(CacheableBlockSpec::Safe), + BlockTag::Finalized => Ok(CacheableBlockSpec::Finalized), + }, + BlockSpec::Eip1898(spec) => match spec { + Eip1898BlockSpec::Hash { + block_hash, + require_canonical, + } => Ok(CacheableBlockSpec::Hash { + block_hash, + require_canonical: *require_canonical, + }), + Eip1898BlockSpec::Number { block_number } => Ok(CacheableBlockSpec::Number { + block_number: *block_number, + }), + }, + } + } +} + +impl<'a> TryFrom<&'a Option> for CacheableBlockSpec<'a> { + type Error = BlockSpecNotCacheableError; + + fn try_from(value: &'a Option) -> Result { + match value { + None => Err(BlockSpecNotCacheableError(None)), + Some(block_spec) => CacheableBlockSpec::try_from(block_spec), + } + } +} + +/// Error type for [`Hasher::hash_block_spec`]. +#[derive(thiserror::Error, Debug)] +#[error("A block tag is not cacheable.")] +pub struct BlockTagNotCacheableError; diff --git a/crates/edr_rpc_client/src/cache/filter.rs b/crates/edr_rpc_client/src/cache/filter.rs new file mode 100644 index 000000000..4bca1cf8e --- /dev/null +++ b/crates/edr_rpc_client/src/cache/filter.rs @@ -0,0 +1,102 @@ +use edr_eth::{ + filter::{LogFilterOptions, OneOrMore}, + Address, B256, +}; + +use super::{block_spec::CacheableBlockSpec, key::CacheKeyVariant}; + +/// A cacheable input for the `eth_getLogs` method. +#[derive(Clone, Debug)] +pub(super) struct CacheableLogFilterOptions<'a> { + /// The range + pub range: CacheableLogFilterRange<'a>, + /// The addresses + pub addresses: Vec<&'a Address>, + /// The topics + pub topics: Vec>>, +} + +/// Error type for [`CacheableLogFilterOptions::try_from`] and +/// [`CacheableLogFilterRange::try_from`]. +#[derive(thiserror::Error, Debug)] +#[error("Method is not cacheable: {0:?}")] +pub(super) struct LogFilterOptionsNotCacheableError(LogFilterOptions); + +impl<'a> TryFrom<&'a LogFilterOptions> for CacheableLogFilterOptions<'a> { + type Error = LogFilterOptionsNotCacheableError; + + fn try_from(value: &'a LogFilterOptions) -> Result { + let range = CacheableLogFilterRange::try_from(value)?; + + Ok(Self { + range, + addresses: value + .address + .as_ref() + .map_or(Vec::new(), |address| match address { + OneOrMore::One(address) => vec![address], + OneOrMore::Many(addresses) => addresses.iter().collect(), + }), + topics: value.topics.as_ref().map_or(Vec::new(), |topics| { + topics + .iter() + .map(|options| { + options.as_ref().map(|options| match options { + OneOrMore::One(topic) => vec![topic], + OneOrMore::Many(topics) => topics.iter().collect(), + }) + }) + .collect() + }), + }) + } +} + +/// A cacheable range input for the `eth_getLogs` method. +#[derive(Clone, Debug)] +pub(super) enum CacheableLogFilterRange<'a> { + /// The `block_hash` argument + Hash(&'a B256), + Range { + /// The `from_block` argument + from_block: CacheableBlockSpec<'a>, + /// The `to_block` argument + to_block: CacheableBlockSpec<'a>, + }, +} + +impl<'a> CacheKeyVariant for CacheableLogFilterRange<'a> { + fn cache_key_variant(&self) -> u8 { + match self { + CacheableLogFilterRange::Hash(_) => 0, + CacheableLogFilterRange::Range { .. } => 1, + } + } +} + +impl<'a> TryFrom<&'a LogFilterOptions> for CacheableLogFilterRange<'a> { + type Error = LogFilterOptionsNotCacheableError; + + fn try_from(value: &'a LogFilterOptions) -> Result { + let map_err = |_| LogFilterOptionsNotCacheableError(value.clone()); + + if let Some(from_block) = &value.from_block { + if let Some(to_block) = &value.to_block { + if value.block_hash.is_none() { + let range = Self::Range { + from_block: from_block.try_into().map_err(map_err)?, + to_block: to_block.try_into().map_err(map_err)?, + }; + + return Ok(range); + } + } + } else if let Some(block_hash) = &value.block_hash { + if value.from_block.is_none() { + return Ok(Self::Hash(block_hash)); + } + } + + Err(LogFilterOptionsNotCacheableError(value.clone())) + } +} diff --git a/crates/edr_rpc_client/src/cache/hasher.rs b/crates/edr_rpc_client/src/cache/hasher.rs new file mode 100644 index 000000000..9f173489b --- /dev/null +++ b/crates/edr_rpc_client/src/cache/hasher.rs @@ -0,0 +1,168 @@ +use edr_eth::{reward_percentile::RewardPercentile, Address, B256, U256}; +use sha3::{digest::FixedOutput, Digest, Sha3_256}; + +use super::{ + block_spec::{BlockTagNotCacheableError, CacheableBlockSpec}, + filter::{CacheableLogFilterOptions, CacheableLogFilterRange}, + key::CacheKeyVariant, +}; + +#[derive(Debug, Clone)] +pub struct Hasher { + hasher: Sha3_256, +} + +// The methods take `mut self` instead of `&mut self` to make sure no hash is +// constructed if one of the method arguments are invalid (in which case the +// method returns None and consumes self). +// +// Before variants of an enum are hashed, a variant marker is hashed before +// hashing the values of the variants to distinguish between them. E.g. the hash +// of `Enum::Foo(1u8)` should not equal the hash of `Enum::Bar(1u8)`, since +// these are not logically equivalent. This matches the behavior of the `Hash` +// derivation of the Rust standard library for enums. +// +// Instead of ignoring `None` values, the same pattern is followed for Options +// in order to let us distinguish between `[None, Some("a")]` and `[Some("a")]`. +// Note that if we use the cache key variant `0u8` for `None`, it's ok if `None` +// and `0u8`, hash to the same values since a type where `Option` and `u8` are +// valid values must be wrapped in an enum in Rust and the enum cache key +// variant prefix will distinguish between them. This wouldn't be the case with +// JSON though. +// +// When adding new types such as sequences or strings, [prefix +// collisions](https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions) should be +// considered. +impl Hasher { + pub fn new() -> Self { + Self { + hasher: Sha3_256::new(), + } + } + + pub fn hash_bytes(mut self, bytes: impl AsRef<[u8]>) -> Self { + self.hasher.update(bytes); + + self + } + + pub fn hash_u8(self, value: u8) -> Self { + self.hash_bytes(value.to_le_bytes()) + } + + pub fn hash_bool(self, value: bool) -> Self { + self.hash_u8(u8::from(value)) + } + + pub fn hash_address(self, address: &Address) -> Self { + self.hash_bytes(address) + } + + pub fn hash_u64(self, value: u64) -> Self { + self.hash_bytes(value.to_le_bytes()) + } + + pub fn hash_u256(self, value: &U256) -> Self { + self.hash_bytes(value.as_le_bytes()) + } + + pub fn hash_b256(self, value: &B256) -> Self { + self.hash_bytes(value) + } + + pub fn hash_block_spec( + self, + block_spec: &CacheableBlockSpec<'_>, + ) -> Result { + let this = self.hash_u8(block_spec.cache_key_variant()); + + match block_spec { + CacheableBlockSpec::Number { block_number } => Ok(this.hash_u64(*block_number)), + CacheableBlockSpec::Hash { + block_hash, + require_canonical, + } => { + let this = this + .hash_b256(block_hash) + .hash_u8(require_canonical.cache_key_variant()); + match require_canonical { + Some(require_canonical) => Ok(this.hash_bool(*require_canonical)), + None => Ok(this), + } + } + CacheableBlockSpec::Earliest + | CacheableBlockSpec::Safe + | CacheableBlockSpec::Finalized => Err(BlockTagNotCacheableError), + } + } + + pub fn hash_log_filter_options( + self, + params: &CacheableLogFilterOptions<'_>, + ) -> Result { + // Destructuring to make sure we get a compiler error here if the fields change. + let CacheableLogFilterOptions { + range, + addresses, + topics, + } = params; + + let mut this = self + .hash_log_filter_range(range)? + .hash_u64(addresses.len() as u64); + + for address in addresses { + this = this.hash_address(address); + } + + this = this.hash_u64(topics.len() as u64); + for options in topics { + this = this.hash_u8(options.cache_key_variant()); + if let Some(options) = options { + this = this.hash_u64(options.len() as u64); + for option in options { + this = this.hash_b256(option); + } + } + } + + Ok(this) + } + + pub fn hash_log_filter_range( + self, + params: &CacheableLogFilterRange<'_>, + ) -> Result { + let this = self.hash_u8(params.cache_key_variant()); + + match params { + CacheableLogFilterRange::Hash(block_hash) => Ok(this.hash_b256(block_hash)), + CacheableLogFilterRange::Range { + from_block, + to_block, + } => Ok(this + .hash_block_spec(from_block)? + .hash_block_spec(to_block)?), + } + } + + pub fn hash_reward_percentile(self, value: &RewardPercentile) -> Self { + const RESOLUTION: f64 = 100.0; + // `RewardPercentile` is an f64 in range [0, 100], so this is guaranteed not to + // overflow. + self.hash_u64((value.as_ref() * RESOLUTION).floor() as u64) + } + + pub fn hash_reward_percentiles(self, value: &[RewardPercentile]) -> Self { + let mut this = self.hash_u64(value.len() as u64); + for v in value { + this = this.hash_reward_percentile(v); + } + this + } + + /// Finalizes the hash and returns it as a hex-encoded string. + pub fn finalize(self) -> String { + hex::encode(self.hasher.finalize_fixed()) + } +} diff --git a/crates/edr_rpc_client/src/cache/key.rs b/crates/edr_rpc_client/src/cache/key.rs new file mode 100644 index 000000000..990a25584 --- /dev/null +++ b/crates/edr_rpc_client/src/cache/key.rs @@ -0,0 +1,144 @@ +use edr_eth::block::{is_safe_block_number, IsSafeBlockNumberArgs}; + +use super::{block_spec::CacheableBlockSpec, filter::CacheableLogFilterRange, CacheKeyHasher}; +use crate::CacheableMethod; + +/// Trait for retrieving the unique id of an enum variant. +// This could be replaced by the unstable +// [`core::intrinsics::discriminant_value`](https://dev-doc.rust-lang.org/beta/core/intrinsics/fn.discriminant_value.html) +// function once it becomes stable. +pub trait CacheKeyVariant { + fn cache_key_variant(&self) -> u8; +} + +impl CacheKeyVariant for Option { + fn cache_key_variant(&self) -> u8 { + match self { + None => 0, + Some(_) => 1, + } + } +} + +/// A cache key that can be used to read from the cache. +/// It's based on not-fully resolved data, so it's not safe to write to this +/// cache key. Specifically, it's not checked whether the block number is safe +/// to cache (safe from reorgs). This is ok for reading from the cache, since +/// the result will be a cache miss if the block number is not safe to cache and +/// not having to resolve this data for reading offers performance advantages. +#[derive(Debug, Clone, PartialEq, Eq)] +#[repr(transparent)] +pub struct ReadCacheKey(String); + +impl AsRef for ReadCacheKey { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[derive(Clone, Debug)] +pub enum WriteCacheKey { + /// It needs to be checked whether the block number is safe (reorg-free) + /// before writing to the cache. + NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber), + /// The method invocation contains a symbolic block spec (e.g. "finalized") + /// that needs to be resolved to a block number before the result can be + /// cached. + NeedsBlockNumber(CacheKeyForBlockTag), + /// The cache key is fully resolved and can be used to write to the cache. + Resolved(String), +} + +impl WriteCacheKey { + fn finalize(hasher: CacheKeyHasher) -> Self { + Self::Resolved(hasher.finalize()) + } + + fn needs_range_check( + hasher: CacheKeyHasher, + range: CacheableLogFilterRange<'_>, + ) -> Option { + match range { + CacheableLogFilterRange::Hash(_) => Some(Self::finalize(hasher)), + CacheableLogFilterRange::Range { to_block, .. } => { + // TODO should we check that to < from? + Self::needs_safety_check(hasher, to_block) + } + } + } + + fn needs_safety_check( + hasher: CacheKeyHasher, + block_spec: CacheableBlockSpec<'_>, + ) -> Option { + match block_spec { + CacheableBlockSpec::Number { block_number } => { + Some(Self::NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber { + hasher: Box::new(hasher), + block_number, + })) + } + CacheableBlockSpec::Hash { .. } => Some(Self::finalize(hasher)), + CacheableBlockSpec::Earliest + | CacheableBlockSpec::Safe + | CacheableBlockSpec::Finalized => None, + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct CacheKeyForUncheckedBlockNumber { + // Boxed to keep the size of the enum small. + hasher: Box, + pub(super) block_number: u64, +} + +impl CacheKeyForUncheckedBlockNumber { + /// Check whether the block number is safe to cache before returning a cache + /// key. + pub fn validate_block_number(self, chain_id: u64, latest_block_number: u64) -> Option { + let is_safe = is_safe_block_number(IsSafeBlockNumberArgs { + chain_id, + latest_block_number, + block_number: self.block_number, + }); + if is_safe { + Some(self.hasher.finalize()) + } else { + None + } + } +} + +#[derive(Debug, Clone)] +pub(crate) enum ResolvedSymbolicTag { + /// It needs to be checked whether the block number is safe (reorg-free) + /// before writing to the cache. + NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber), + /// The cache key is fully resolved and can be used to write to the cache. + Resolved(String), +} + +#[derive(Debug, Clone)] +pub(crate) struct CacheKeyForBlockTag { + method: MethodT::MethodWithResolvableBlockTag, +} + +impl CacheKeyForBlockTag { + /// Check whether the block number is safe to cache before returning a cache + /// key. + pub fn resolve_symbolic_tag(self, block_number: u64) -> Option { + let resolved_block_spec = CacheableBlockSpec::Number { block_number }; + let resolved_method = MethodT::resolve_block_tag(self.method, block_number); + + resolved_method.write_cache_key().map(|key| match key { + WriteCacheKey::NeedsSafetyCheck(cache_key) => { + ResolvedSymbolicTag::NeedsSafetyCheck(cache_key) + } + WriteCacheKey::Resolved(cache_key) => ResolvedSymbolicTag::Resolved(cache_key), + WriteCacheKey::NeedsBlockNumber(_) => { + unreachable!("resolved block spec should not need block number") + } + }) + } +} diff --git a/crates/edr_rpc_eth/src/chain_id.rs b/crates/edr_rpc_client/src/chain_id.rs similarity index 100% rename from crates/edr_rpc_eth/src/chain_id.rs rename to crates/edr_rpc_client/src/chain_id.rs diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index 250d2af52..c0cf49415 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -1,21 +1,19 @@ use std::{ fmt::Debug, io, + marker::PhantomData, path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, - time::{Duration, Instant}, + time::Duration, }; use edr_eth::{ - eth, - filter::{LogFilterOptions, OneOrMore}, - jsonrpc, - request_methods::RequestMethod, - BlockSpec, Bytecode, PreEip1898BlockSpec, KECCAK_EMPTY, + block::{block_time, is_safe_block_number, IsSafeBlockNumberArgs}, + U64, }; use futures::{future, stream::StreamExt, TryFutureExt}; use hyper::header::HeaderValue; -pub use hyper::{header, http::Error as HttpError, HeaderMap}; +pub use hyper::{header, http, HeaderMap}; use reqwest::Client as HttpClient; use reqwest_middleware::{ClientBuilder as HttpClientBuilder, ClientWithMiddleware}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; @@ -25,22 +23,14 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use tokio::sync::{OnceCell, RwLock}; use uuid::Uuid; -pub use crate::remote::client::reqwest_error::{MiddlewareError, ReqwestError}; use crate::{ - block::{block_time, is_safe_block_number, IsSafeBlockNumberArgs}, - log::FilterLog, - receipt::BlockReceipt, - remote::{ - cacheable_method_invocation::{ - try_read_cache_key, try_write_cache_key, CacheKeyForSymbolicBlockTag, - CacheKeyForUncheckedBlockNumber, ReadCacheKey, ResolvedSymbolicTag, WriteCacheKey, - }, - chain_id::chain_id_from_url, - eth::FeeHistoryResult, - jsonrpc::Id, + cache::{ + self, + key::{CacheKeyForBlockTag, CacheKeyForUncheckedBlockNumber, ResolvedSymbolicTag}, + remove_from_cache, CachedBlockNumber, }, - reward_percentile::RewardPercentile, - AccountInfo, Address, Bytes, B256, U256, U64, + chain_id::chain_id_from_url, + jsonrpc, CacheableMethod, MiddlewareError, ReadCacheKey, ReqwestError, WriteCacheKey, }; const RPC_CACHE_DIR: &str = "rpc_cache"; @@ -90,7 +80,7 @@ pub enum RpcClientError { /// The response text response: String, /// The invalid id - id: Id, + id: jsonrpc::Id, }, /// Invalid URL format @@ -114,7 +104,7 @@ pub enum RpcClientError { /// The cache key for the error cache_key: String, /// The underlying error - error: CacheError, + error: cache::Error, }, /// Failed to join a tokio task. @@ -122,35 +112,11 @@ pub enum RpcClientError { JoinError(#[from] tokio::task::JoinError), } -/// Wrapper for IO and JSON errors specific to the cache. -#[derive(thiserror::Error, Debug)] -pub enum CacheError { - /// An IO error - #[error(transparent)] - Io(#[from] io::Error), - /// A JSON parsing error - #[error(transparent)] - Json(#[from] serde_json::Error), -} - -/// A JSON-RPC request -#[derive(Deserialize, Serialize)] -pub struct Request { - /// JSON-RPC version - #[serde(rename = "jsonrpc")] - pub version: jsonrpc::Version, - /// the method to invoke, with its parameters - #[serde(flatten)] - pub method: RequestMethod, - /// the request ID, to be correlated via the response's ID - pub id: Id, -} - /// A client for executing RPC methods on a remote Ethereum node. /// The client caches responses based on chain id, so it's important to not use /// it with local nodes. #[derive(Debug)] -pub struct RpcClient { +pub struct RpcClient { url: url::Url, chain_id: OnceCell, cached_block_number: RwLock>, @@ -158,9 +124,10 @@ pub struct RpcClient { next_id: AtomicU64, rpc_cache_dir: PathBuf, tmp_dir: PathBuf, + _phantom: PhantomData, } -impl RpcClient { +impl RpcClient { /// Create a new instance, given a remote node URL. /// The cache directory is the global EDR cache directory configured by the /// user. @@ -214,15 +181,16 @@ impl RpcClient { next_id: AtomicU64::new(0), rpc_cache_dir: cache_dir.join(RPC_CACHE_DIR), tmp_dir, + _phantom: PhantomData, }) } - fn parse_response_str( + fn parse_response_str( response: String, - ) -> Result, RpcClientError> { + ) -> Result, RpcClientError> { serde_json::from_str(&response).map_err(|error| RpcClientError::InvalidResponse { response, - expected_type: std::any::type_name::>(), + expected_type: std::any::type_name::>(), error, }) } @@ -277,13 +245,13 @@ impl RpcClient { async fn read_response_from_cache( &self, cache_key: &ReadCacheKey, - ) -> Result, RpcClientError> { + ) -> Result, RpcClientError> { let path = self.make_cache_path(cache_key.as_ref()).await?; match tokio::fs::read_to_string(&path).await { Ok(contents) => match serde_json::from_str(&contents) { - Ok(value) => Ok(Some(ResponseValue { value, path })), + Ok(value) => Ok(Some(cache::Response { value, path })), Err(error) => { - log_cache_error( + cache::log_error( cache_key.as_ref(), "failed to deserialize item from RPC response cache", error, @@ -295,7 +263,7 @@ impl RpcClient { Err(error) => { match error.kind() { io::ErrorKind::NotFound => (), - _ => log_cache_error( + _ => cache::log_error( cache_key.as_ref(), "failed to read from RPC response cache", error, @@ -309,7 +277,7 @@ impl RpcClient { async fn try_from_cache( &self, cache_key: Option<&ReadCacheKey>, - ) -> Result, RpcClientError> { + ) -> Result, RpcClientError> { if let Some(cache_key) = cache_key { self.read_response_from_cache(cache_key).await } else { @@ -351,11 +319,11 @@ impl RpcClient { Ok(safety_checker.validate_block_number(chain_id, latest_block_number)) } - async fn resolve_block_tag( + async fn resolve_block_tag( &self, - block_tag_resolver: CacheKeyForSymbolicBlockTag, - result: T, - resolve_block_number: impl Fn(T) -> Option, + block_tag_resolver: CacheKeyForBlockTag, + result: ResultT, + resolve_block_number: impl Fn(ResultT) -> Option, ) -> Result, RpcClientError> { if let Some(block_number) = resolve_block_number(result) { if let Some(resolved_cache_key) = block_tag_resolver.resolve_symbolic_tag(block_number) @@ -371,15 +339,13 @@ impl RpcClient { Ok(None) } - async fn resolve_write_key( + async fn resolve_write_key( &self, - method: &RequestMethod, - result: T, - resolve_block_number: impl Fn(T) -> Option, + method: &MethodT, + result: ResultT, + resolve_block_number: impl Fn(ResultT) -> Option, ) -> Result, RpcClientError> { - let write_cache_key = try_write_cache_key(method); - - if let Some(cache_key) = write_cache_key { + if let Some(cache_key) = method.write_cache_key() { match cache_key { WriteCacheKey::NeedsSafetyCheck(safety_checker) => { self.validate_block_number(safety_checker).await @@ -395,11 +361,11 @@ impl RpcClient { } } - async fn try_write_response_to_cache( + async fn try_write_response_to_cache( &self, - method: &RequestMethod, - result: &T, - resolve_block_number: impl Fn(&T) -> Option, + method: &MethodT, + result: &ResultT, + resolve_block_number: impl Fn(&ResultT) -> Option, ) -> Result<(), RpcClientError> { if let Some(cache_key) = self .resolve_write_key(method, result, resolve_block_number) @@ -427,7 +393,7 @@ impl RpcClient { match tokio::fs::write(&tmp_path, contents).await { Ok(_) => (), Err(error) => { - log_cache_error( + cache::log_error( cache_key, "failed to write to tempfile for RPC response cache", error, @@ -449,7 +415,7 @@ impl RpcClient { match tokio::fs::rename(&tmp_path, cache_path).await { Ok(_) => (), Err(error) => { - log_cache_error( + cache::log_error( cache_key, "failed to rename temporary file for RPC response cache", error, @@ -463,7 +429,7 @@ impl RpcClient { Ok(_) => (), Err(error) => match error.kind() { io::ErrorKind::NotFound => (), - _ => log_cache_error( + _ => cache::log_error( cache_key, "failed to remove temporary file for RPC response cache", error, @@ -474,10 +440,10 @@ impl RpcClient { Ok(()) } - async fn send_request_and_extract_result( + async fn send_request_and_extract_result( &self, request: SerializedRequest, - ) -> Result { + ) -> Result { future::ready( self.send_request_body(&request) .await @@ -509,19 +475,16 @@ impl RpcClient { .map_err(|err| RpcClientError::CorruptedResponse(err.into())) } - fn serialize_request( - &self, - input: &RequestMethod, - ) -> Result { - let id = Id::Num(self.next_id.fetch_add(1, Ordering::Relaxed)); + fn serialize_request(&self, input: &MethodT) -> Result { + let id = jsonrpc::Id::Num(self.next_id.fetch_add(1, Ordering::Relaxed)); Self::serialize_request_with_id(input, id) } fn serialize_request_with_id( - method: &RequestMethod, - id: Id, + method: &MethodT, + id: jsonrpc::Id, ) -> Result { - let request = serde_json::to_value(Request { + let request = serde_json::to_value(jsonrpc::Request { version: jsonrpc::Version::V2_0, id, method, @@ -531,19 +494,22 @@ impl RpcClient { Ok(SerializedRequest(request)) } - async fn call( + /// Calls the provided JSON-RPC method and returns the result. + pub async fn call( &self, - method: RequestMethod, - ) -> Result { + method: MethodT, + ) -> Result { self.call_with_resolver(method, |_| None).await } - async fn call_with_resolver( + /// Calls the provided JSON-RPC method, uses the provided resolver to + /// resolve the result, and returns the result. + pub async fn call_with_resolver( &self, - method: RequestMethod, - resolve_block_number: impl Fn(&T) -> Option, - ) -> Result { - let read_cache_key = try_read_cache_key(&method); + method: MethodT, + resolve_block_number: impl Fn(&SuccessT) -> Option, + ) -> Result { + let read_cache_key = method.read_cache_key(); let request = self.serialize_request(&method)?; @@ -574,7 +540,7 @@ impl RpcClient { #[cfg(feature = "tracing")] tracing::trace!("Cache miss: {}", method.name()); - let result: T = self.send_request_and_extract_result(request).await?; + let result: SuccessT = self.send_request_and_extract_result(request).await?; self.try_write_response_to_cache(&method, &result, &resolve_block_number) .await?; @@ -587,7 +553,7 @@ impl RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip_all))] async fn call_without_cache( &self, - method: RequestMethod, + method: MethodT, ) -> Result { let request = self.serialize_request(&method)?; @@ -598,7 +564,7 @@ impl RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] pub async fn block_number(&self) -> Result { let block_number = self - .call_without_cache::(RequestMethod::BlockNumber(())) + .call_without_cache::(MethodT::block_number_request()) .await? .as_limbs()[0]; @@ -634,7 +600,7 @@ impl RpcClient { if let Some(chain_id) = chain_id_from_url(&self.url) { Ok(chain_id) } else { - self.call_without_cache::(RequestMethod::ChainId(())) + self.call_without_cache::(MethodT::chain_id_request()) .await .map(|chain_id| chain_id.as_limbs()[0]) } @@ -642,274 +608,6 @@ impl RpcClient { .await?; Ok(chain_id) } - - /// Calls `eth_feeHistory` and returns the fee history. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn fee_history( - &self, - block_count: u64, - newest_block: BlockSpec, - reward_percentiles: Option>, - ) -> Result { - self.call(RequestMethod::FeeHistory( - U256::from(block_count), - newest_block, - reward_percentiles, - )) - .await - } - - /// Fetch the latest block number, chain id and network id concurrently. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn fetch_fork_metadata(&self) -> Result { - let network_id = self.network_id(); - let block_number = self.block_number(); - let chain_id = self.chain_id(); - - let (network_id, block_number, chain_id) = - tokio::try_join!(network_id, block_number, chain_id)?; - - Ok(ForkMetadata { - chain_id, - network_id, - latest_block_number: block_number, - }) - } - - /// Submits three concurrent RPC method invocations in order to obtain - /// the set of data contained in [`AccountInfo`]. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_account_info( - &self, - address: &Address, - block: Option, - ) -> Result { - let balance = self.get_balance(address, block.clone()); - let nonce = self.get_transaction_count(address, block.clone()); - let code = self.get_code(address, block.clone()); - - let (balance, nonce, code) = tokio::try_join!(balance, nonce, code)?; - - let code = if code.is_empty() { - None - } else { - Some(Bytecode::new_raw(code)) - }; - - Ok(AccountInfo { - balance, - code_hash: code.as_ref().map_or(KECCAK_EMPTY, Bytecode::hash_slow), - code, - nonce: nonce.to(), - }) - } - - /// Fetch account infos for multiple addresses using concurrent requests. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_account_infos( - &self, - addresses: &[Address], - block: Option, - ) -> Result, RpcClientError> { - futures::stream::iter(addresses.iter()) - .map(|address| self.get_account_info(address, block.clone())) - .buffered(MAX_PARALLEL_REQUESTS / 3 + 1) - .collect::>>() - .await - .into_iter() - .collect() - } - - /// Calls `eth_getBlockByHash` and returns the transaction's hash. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_block_by_hash( - &self, - hash: &B256, - ) -> Result>, RpcClientError> { - self.call(RequestMethod::GetBlockByHash(*hash, false)).await - } - - /// Calls `eth_getBalance`. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_balance( - &self, - address: &Address, - block: Option, - ) -> Result { - self.call(RequestMethod::GetBalance(*address, block)).await - } - - /// Calls `eth_getBlockByHash` and returns the transaction's data. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_block_by_hash_with_transaction_data( - &self, - hash: &B256, - ) -> Result>, RpcClientError> { - self.call(RequestMethod::GetBlockByHash(*hash, true)).await - } - - /// Calls `eth_getBlockByNumber` and returns the transaction's hash. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_block_by_number( - &self, - spec: PreEip1898BlockSpec, - ) -> Result>, RpcClientError> { - self.call_with_resolver( - RequestMethod::GetBlockByNumber(spec, false), - |block: &Option>| block.as_ref().and_then(|block| block.number), - ) - .await - } - - /// Calls `eth_getBlockByNumber` and returns the transaction's data. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_block_by_number_with_transaction_data( - &self, - spec: PreEip1898BlockSpec, - ) -> Result, RpcClientError> { - self.call_with_resolver( - RequestMethod::GetBlockByNumber(spec, true), - |block: ð::Block| block.number, - ) - .await - } - - /// Calls `eth_getCode`. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_code( - &self, - address: &Address, - block: Option, - ) -> Result { - self.call(RequestMethod::GetCode(*address, block)).await - } - - /// Calls `eth_getLogs` using a starting and ending block (inclusive). - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_logs_by_range( - &self, - from_block: BlockSpec, - to_block: BlockSpec, - address: Option>, - topics: Option>>>, - ) -> Result, RpcClientError> { - self.call(RequestMethod::GetLogs(LogFilterOptions { - from_block: Some(from_block), - to_block: Some(to_block), - block_hash: None, - address, - topics, - })) - .await - } - - /// Calls `eth_getTransactionByHash`. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_transaction_by_hash( - &self, - tx_hash: &B256, - ) -> Result, RpcClientError> { - self.call(RequestMethod::GetTransactionByHash(*tx_hash)) - .await - } - - /// Calls `eth_getTransactionCount`. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_transaction_count( - &self, - address: &Address, - block: Option, - ) -> Result { - self.call(RequestMethod::GetTransactionCount(*address, block)) - .await - } - - /// Calls `eth_getTransactionReceipt`. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_transaction_receipt( - &self, - tx_hash: &B256, - ) -> Result, RpcClientError> { - self.call(RequestMethod::GetTransactionReceipt(*tx_hash)) - .await - } - - /// Methods for retrieving multiple transaction receipts using concurrent - /// requests. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_transaction_receipts( - &self, - hashes: impl IntoIterator + Debug, - ) -> Result>, RpcClientError> { - let requests = hashes - .into_iter() - .map(|transaction_hash| self.get_transaction_receipt(transaction_hash)); - - futures::stream::iter(requests) - .buffered(MAX_PARALLEL_REQUESTS) - .collect::, RpcClientError>>>() - .await - .into_iter() - .collect() - } - - /// Calls `eth_getStorageAt`. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn get_storage_at( - &self, - address: &Address, - position: U256, - block: Option, - ) -> Result, RpcClientError> { - self.call(RequestMethod::GetStorageAt(*address, position, block)) - .await - } - - /// Calls `net_version`. - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - pub async fn network_id(&self) -> Result { - self.call::(RequestMethod::NetVersion(())) - .await - .map(|network_id| network_id.as_limbs()[0]) - } -} - -async fn remove_from_cache(path: &Path) -> Result<(), RpcClientError> { - match tokio::fs::remove_file(path).await { - Ok(_) => Ok(()), - Err(error) => { - log_cache_error( - path.to_str().unwrap_or(""), - "failed to remove from RPC response cache", - error, - ); - Ok(()) - } - } -} - -#[derive(Debug, Clone)] -struct ResponseValue { - value: serde_json::Value, - path: PathBuf, -} - -impl ResponseValue { - async fn parse(self) -> Result { - match serde_json::from_value(self.value.clone()) { - Ok(result) => Ok(result), - Err(error) => { - // Remove the file from cache if the contents don't match the expected type. - // This can happen for example if a new field is added to a type. - remove_from_cache(&self.path).await?; - Err(RpcClientError::InvalidResponse { - response: self.value.to_string(), - expected_type: std::any::type_name::(), - error, - }) - } - } - } } /// Metadata about a forked chain. @@ -923,32 +621,6 @@ pub struct ForkMetadata { pub latest_block_number: u64, } -#[derive(Debug, Clone)] -struct CachedBlockNumber { - block_number: u64, - timestamp: Instant, -} - -impl CachedBlockNumber { - fn new(block_number: u64) -> Self { - Self { - block_number, - timestamp: Instant::now(), - } - } -} - -/// Don't fail the request, just log an error if we fail to read/write from -/// cache. -fn log_cache_error(cache_key: &str, message: &'static str, error: impl Into) { - let cache_error = RpcClientError::CacheError { - message: message.to_string(), - cache_key: cache_key.to_string(), - error: error.into(), - }; - log::error!("{cache_error}"); -} - /// Ensure that the directory exists. async fn ensure_cache_directory( directory: impl AsRef, @@ -1030,7 +702,7 @@ mod tests { .expect("failed to parse hash from string"); let error = TestRpcClient::new(&server.url()) - .call::>(RequestMethod::GetTransactionByHash(hash)) + .call::>(MethodT::GetTransactionByHash(hash)) .await .expect_err("should have failed to due to a HTTP status error"); @@ -1087,7 +759,7 @@ mod tests { .expect("failed to parse hash from string"); let error = TestRpcClient::new(&alchemy_url) - .call::>(RequestMethod::GetTransactionByHash(hash)) + .call::>(MethodT::GetTransactionByHash(hash)) .await .expect_err("should have failed to interpret response as a Transaction"); @@ -1113,7 +785,7 @@ mod tests { .expect("failed to parse hash from string"); let error = TestRpcClient::new(alchemy_url) - .call::>(RequestMethod::GetTransactionByHash(hash)) + .call::>(MethodT::GetTransactionByHash(hash)) .await .expect_err("should have failed to connect due to a garbage domain name"); diff --git a/crates/edr_rpc_client/src/jsonrpc.rs b/crates/edr_rpc_client/src/jsonrpc.rs index 049efc9e4..577d7c817 100644 --- a/crates/edr_rpc_client/src/jsonrpc.rs +++ b/crates/edr_rpc_client/src/jsonrpc.rs @@ -18,9 +18,22 @@ pub struct Error { pub data: Option, } +/// A JSON-RPC request +#[derive(Deserialize, Serialize)] +pub struct Request { + /// JSON-RPC version + #[serde(rename = "jsonrpc")] + pub version: Version, + /// the method to invoke, with its parameters + #[serde(flatten)] + pub method: MethodT, + /// the request ID, to be correlated via the response's ID + pub id: Id, +} + /// Represents a JSON-RPC 2.0 response. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Response { +pub struct Response { /// A String specifying the version of the JSON-RPC protocol. pub jsonrpc: Version, // @@ -31,13 +44,13 @@ pub struct Response { pub id: Id, /// Response data. #[serde(flatten)] - pub data: ResponseData, + pub data: ResponseData, } /// Represents JSON-RPC 2.0 success response. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] #[serde(untagged)] -pub enum ResponseData { +pub enum ResponseData { /// an error response Error { /// the error @@ -46,14 +59,14 @@ pub enum ResponseData { /// a success response Success { /// the result - result: T, + result: SuccessT, }, } -impl ResponseData { +impl ResponseData { /// Returns a [`Result`] where `Success` is mapped to `Ok` and `Error` to /// `Err`. - pub fn into_result(self) -> Result { + pub fn into_result(self) -> Result { match self { ResponseData::Success { result } => Ok(result), ResponseData::Error { error } => Err(error), @@ -62,7 +75,7 @@ impl ResponseData { /// convenience constructor for an error response pub fn new_error(code: i16, message: &str, data: Option) -> Self { - ResponseData::::Error { + ResponseData::::Error { error: Error { code, message: String::from(message), diff --git a/crates/edr_rpc_client/src/lib.rs b/crates/edr_rpc_client/src/lib.rs index 29bf127da..caac37328 100644 --- a/crates/edr_rpc_client/src/lib.rs +++ b/crates/edr_rpc_client/src/lib.rs @@ -2,10 +2,18 @@ //! Ethereum JSON-RPC client +/// Types for caching JSON-RPC responses +mod cache; mod client; -mod reqwest_error; - /// Types specific to JSON-RPC pub mod jsonrpc; +mod reqwest_error; -pub use client::{RpcClient, RpcClientError}; +pub use self::{ + cache::{ + key::{ReadCacheKey, WriteCacheKey}, + CacheKeyHasher, CacheableMethod, + }, + client::{RpcClient, RpcClientError}, + reqwest_error::{MiddlewareError, ReqwestError}, +}; diff --git a/crates/edr_rpc_eth/Cargo.toml b/crates/edr_rpc_eth/Cargo.toml index fc69ae579..006edfccb 100644 --- a/crates/edr_rpc_eth/Cargo.toml +++ b/crates/edr_rpc_eth/Cargo.toml @@ -1,6 +1,19 @@ [package] name = "edr_rpc_eth" -version = "0.1.0" +version = "0.3.5" edition = "2021" [dependencies] +async-trait = { version = "0.1.80", default-features = false } +edr_eth = { version = "0.3.5", path = "../edr_eth", features = ["serde"] } +edr_rpc_client = { version = "0.3.5", path = "../edr_rpc_client" } +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +serde = { version = "1.0.147", default-features = false, features = ["derive", "std"] } +thiserror = { version = "1.0.37", default-features = false } + +[dev-dependencies] +anyhow = { version = "1.0.75", default-features = false, features = ["std"] } +serde_json = { version = "1.0.89" } + +[features] +test-remote = [] diff --git a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs index af8d1170b..9fa7810a6 100644 --- a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs +++ b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs @@ -1,14 +1,16 @@ -use sha3::{digest::FixedOutput, Digest, Sha3_256}; - -use super::filter::{LogFilterOptions, OneOrMore}; -use crate::{ +use edr_eth::{ block::{is_safe_block_number, IsSafeBlockNumberArgs}, - remote::{ - request_methods::RequestMethod, BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec, - }, reward_percentile::RewardPercentile, Address, B256, U256, }; +use edr_rpc_client::{CacheKeyHasher, CacheableMethod}; +use sha3::{digest::FixedOutput, Digest, Sha3_256}; + +use crate::{ + filter::{LogFilterOptions, OneOrMore}, + request_methods::RequestMethod, + BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec, +}; pub(super) fn try_read_cache_key(method: &RequestMethod) -> Option { CacheableRequestMethod::try_from(method) @@ -79,58 +81,68 @@ enum CacheableRequestMethod<'a> { } impl<'a> CacheableRequestMethod<'a> { - fn read_cache_key(self) -> Option { - let cache_key = Hasher::new().hash_method(&self).ok()?.finalize(); - Some(ReadCacheKey(cache_key)) - } - + // Allow to keep same structure as other RequestMethod and other methods. #[allow(clippy::match_same_arms)] - fn write_cache_key(self) -> Option { - match Hasher::new().hash_method(&self) { - Err(SymbolicBlogTagError) => WriteCacheKey::needs_block_number(self), - Ok(hasher) => match self { - CacheableRequestMethod::FeeHistory { - block_count: _, - newest_block, - reward_percentiles: _, - } => WriteCacheKey::needs_safety_check(hasher, newest_block), - CacheableRequestMethod::GetBalance { - address: _, - block_spec, - } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetBlockByNumber { - block_spec, - include_tx_data: _, - } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetBlockByHash { - block_hash: _, - include_tx_data: _, - } => Some(WriteCacheKey::finalize(hasher)), - CacheableRequestMethod::GetCode { - address: _, - block_spec, - } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetLogs { - params: CacheableLogFilterOptions { range, .. }, - } => WriteCacheKey::needs_range_check(hasher, range), - CacheableRequestMethod::GetStorageAt { - address: _, - position: _, - block_spec, - } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetTransactionByHash { - transaction_hash: _, - } => Some(WriteCacheKey::finalize(hasher)), - CacheableRequestMethod::GetTransactionCount { - address: _, - block_spec, - } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetTransactionReceipt { - transaction_hash: _, - } => Some(WriteCacheKey::finalize(hasher)), - CacheableRequestMethod::NetVersion => Some(WriteCacheKey::finalize(hasher)), - }, - } + fn key_hasher(self) -> Result { + let hasher = CacheKeyHasher::new(); + let hasher = hasher.hash_u8(method.cache_key_variant()); + + let hasher = match method { + CacheableRequestMethod::FeeHistory { + block_count, + newest_block, + reward_percentiles, + } => { + let hasher = hasher + .hash_u256(block_count) + .hash_block_spec(newest_block)? + .hash_u8(reward_percentiles.cache_key_variant()); + match reward_percentiles { + Some(reward_percentiles) => hasher.hash_reward_percentiles(reward_percentiles), + None => hasher, + } + } + CacheableRequestMethod::GetBalance { + address, + block_spec, + } => hasher.hash_address(address).hash_block_spec(block_spec)?, + CacheableRequestMethod::GetBlockByNumber { + block_spec, + include_tx_data, + } => hasher + .hash_block_spec(block_spec)? + .hash_bool(include_tx_data), + CacheableRequestMethod::GetBlockByHash { + block_hash, + include_tx_data, + } => hasher.hash_b256(block_hash).hash_bool(include_tx_data), + CacheableRequestMethod::GetCode { + address, + block_spec, + } => hasher.hash_address(address).hash_block_spec(block_spec)?, + CacheableRequestMethod::GetLogs { params } => hasher.hash_log_filter_options(params)?, + CacheableRequestMethod::GetStorageAt { + address, + position, + block_spec, + } => hasher + .hash_address(address) + .hash_u256(position) + .hash_block_spec(block_spec)?, + CacheableRequestMethod::GetTransactionByHash { transaction_hash } => { + hasher.hash_b256(transaction_hash) + } + CacheableRequestMethod::GetTransactionCount { + address, + block_spec, + } => hasher.hash_address(address).hash_block_spec(block_spec)?, + CacheableRequestMethod::GetTransactionReceipt { transaction_hash } => { + hasher.hash_b256(transaction_hash) + } + CacheableRequestMethod::NetVersion => hasher, + }; + + Ok(hasher) } } @@ -215,70 +227,102 @@ impl<'a> TryFrom<&'a RequestMethod> for CacheableRequestMethod<'a> { } } -/// A block argument specification that is potentially cacheable. -#[derive(Clone, Debug)] -enum CacheableBlockSpec<'a> { - /// Block number - Number { block_number: u64 }, - /// Block hash - Hash { - block_hash: &'a B256, - require_canonical: Option, - }, - /// "earliest" block tag - Earliest, - /// "safe" block tag - Safe, - /// "finalized" block tag - Finalized, +/// Method invocations where, if the block spec argument is symbolic, it can be +/// resolved to a block number from the response. +#[derive(Debug, Clone)] +enum MethodWithResolvableSymbolicBlockSpec { + GetBlockByNumber { include_tx_data: bool }, } -/// Error type for [`CacheableBlockSpec::try_from`]. -#[derive(thiserror::Error, Debug)] -#[error("Block spec is not cacheable: {0:?}")] -struct BlockSpecNotCacheableError(Option); +impl MethodWithResolvableSymbolicBlockSpec { + fn new(method: CacheableRequestMethod<'_>) -> Option { + match method { + CacheableRequestMethod::GetBlockByNumber { + include_tx_data, + block_spec: _, + } => Some(Self::GetBlockByNumber { include_tx_data }), + _ => None, + } + } +} -impl<'a> TryFrom<&'a BlockSpec> for CacheableBlockSpec<'a> { - type Error = BlockSpecNotCacheableError; +impl<'a> CacheableMethod for RequestMethod<'a> { + type MethodWithResolvableBlockTag = MethodWithResolvableSymbolicBlockSpec; - fn try_from(value: &'a BlockSpec) -> Result { - match value { - BlockSpec::Number(block_number) => Ok(CacheableBlockSpec::Number { - block_number: *block_number, - }), - BlockSpec::Tag(tag) => match tag { - // Latest and pending can be never resolved to a safe block number. - BlockTag::Latest | BlockTag::Pending => { - Err(BlockSpecNotCacheableError(Some(value.clone()))) - } - // Earliest, safe and finalized are potentially resolvable to a safe block number. - BlockTag::Earliest => Ok(CacheableBlockSpec::Earliest), - BlockTag::Safe => Ok(CacheableBlockSpec::Safe), - BlockTag::Finalized => Ok(CacheableBlockSpec::Finalized), - }, - BlockSpec::Eip1898(spec) => match spec { - Eip1898BlockSpec::Hash { - block_hash, - require_canonical, - } => Ok(CacheableBlockSpec::Hash { - block_hash, - require_canonical: *require_canonical, - }), - Eip1898BlockSpec::Number { block_number } => Ok(CacheableBlockSpec::Number { - block_number: *block_number, - }), + fn block_number_request() -> Self { + Self::BlockNumber(()) + } + + fn chain_id_request() -> Self { + Self::ChainId(()) + } + + fn resolve_block_tag(method: Self::MethodWithResolvableBlockTag, block_number: u64) -> Self { + match self.method { + MethodWithResolvableSymbolicBlockSpec::GetBlockByNumber { + include_tx_data, .. + } => CacheableRequestMethod::GetBlockByNumber { + block_spec: resolved_block_spec, + include_tx_data, }, - } + }; } -} -impl<'a> TryFrom<&'a Option> for CacheableBlockSpec<'a> { - type Error = BlockSpecNotCacheableError; + fn read_cache_key(self) -> Option { + let cacheable_method = CacheableRequestMethod::try_from(&self).ok()?; - fn try_from(value: &'a Option) -> Result { - match value { - None => Err(BlockSpecNotCacheableError(None)), - Some(block_spec) => CacheableBlockSpec::try_from(block_spec), + let cache_key = cacheable_method.key_hasher().ok()?.finalize(); + Some(ReadCacheKey(cache_key)) + } + + #[allow(clippy::match_same_arms)] + fn write_cache_key(self) -> Option { + let cacheable_method = CacheableRequestMethod::try_from(&self).ok()?; + + match cacheable_method.key_hasher() { + Err(SymbolicBlogTagError) => WriteCacheKey::needs_block_number(self), + Ok(hasher) => match self { + CacheableRequestMethod::FeeHistory { + block_count: _, + newest_block, + reward_percentiles: _, + } => WriteCacheKey::needs_safety_check(hasher, newest_block), + CacheableRequestMethod::GetBalance { + address: _, + block_spec, + } => WriteCacheKey::needs_safety_check(hasher, block_spec), + CacheableRequestMethod::GetBlockByNumber { + block_spec, + include_tx_data: _, + } => WriteCacheKey::needs_safety_check(hasher, block_spec), + CacheableRequestMethod::GetBlockByHash { + block_hash: _, + include_tx_data: _, + } => Some(WriteCacheKey::finalize(hasher)), + CacheableRequestMethod::GetCode { + address: _, + block_spec, + } => WriteCacheKey::needs_safety_check(hasher, block_spec), + CacheableRequestMethod::GetLogs { + params: CacheableLogFilterOptions { range, .. }, + } => WriteCacheKey::needs_range_check(hasher, range), + CacheableRequestMethod::GetStorageAt { + address: _, + position: _, + block_spec, + } => WriteCacheKey::needs_safety_check(hasher, block_spec), + CacheableRequestMethod::GetTransactionByHash { + transaction_hash: _, + } => Some(WriteCacheKey::finalize(hasher)), + CacheableRequestMethod::GetTransactionCount { + address: _, + block_spec, + } => WriteCacheKey::needs_safety_check(hasher, block_spec), + CacheableRequestMethod::GetTransactionReceipt { + transaction_hash: _, + } => Some(WriteCacheKey::finalize(hasher)), + CacheableRequestMethod::NetVersion => Some(WriteCacheKey::finalize(hasher)), + }, } } } @@ -310,485 +354,6 @@ impl<'a> TryFrom<&'a PreEip1898BlockSpec> for CacheableBlockSpec<'a> { } } -/// A cacheable range input for the `eth_getLogs` method. -#[derive(Clone, Debug)] -enum CacheableLogFilterRange<'a> { - /// The `block_hash` argument - Hash(&'a B256), - Range { - /// The `from_block` argument - from_block: CacheableBlockSpec<'a>, - /// The `to_block` argument - to_block: CacheableBlockSpec<'a>, - }, -} - -impl<'a> TryFrom<&'a LogFilterOptions> for CacheableLogFilterRange<'a> { - type Error = LogFilterOptionsNotCacheableError; - - fn try_from(value: &'a LogFilterOptions) -> Result { - let map_err = |_| LogFilterOptionsNotCacheableError(value.clone()); - - if let Some(from_block) = &value.from_block { - if let Some(to_block) = &value.to_block { - if value.block_hash.is_none() { - let range = Self::Range { - from_block: from_block.try_into().map_err(map_err)?, - to_block: to_block.try_into().map_err(map_err)?, - }; - - return Ok(range); - } - } - } else if let Some(block_hash) = &value.block_hash { - if value.from_block.is_none() { - return Ok(Self::Hash(block_hash)); - } - } - - Err(LogFilterOptionsNotCacheableError(value.clone())) - } -} - -/// A cacheable input for the `eth_getLogs` method. -#[derive(Clone, Debug)] -struct CacheableLogFilterOptions<'a> { - /// The range - range: CacheableLogFilterRange<'a>, - /// The address - address: Vec<&'a Address>, - /// The topics - topics: Vec>>, -} - -/// Error type for [`CacheableBlockSpec::try_from`]. -#[derive(thiserror::Error, Debug)] -#[error("Method is not cacheable: {0:?}")] -struct LogFilterOptionsNotCacheableError(LogFilterOptions); - -impl<'a> TryFrom<&'a LogFilterOptions> for CacheableLogFilterOptions<'a> { - type Error = LogFilterOptionsNotCacheableError; - - fn try_from(value: &'a LogFilterOptions) -> Result { - let range = CacheableLogFilterRange::try_from(value)?; - - Ok(Self { - range, - address: value - .address - .as_ref() - .map_or(Vec::new(), |address| match address { - OneOrMore::One(address) => vec![address], - OneOrMore::Many(addresses) => addresses.iter().collect(), - }), - topics: value.topics.as_ref().map_or(Vec::new(), |topics| { - topics - .iter() - .map(|options| { - options.as_ref().map(|options| match options { - OneOrMore::One(topic) => vec![topic], - OneOrMore::Many(topics) => topics.iter().collect(), - }) - }) - .collect() - }), - }) - } -} - -#[derive(Debug, Clone)] -pub(super) enum WriteCacheKey { - /// It needs to be checked whether the block number is safe (reorg-free) - /// before writing to the cache. - NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber), - /// The method invocation contains a symbolic block spec (e.g. "finalized") - /// that needs to be resolved to a block number before the result can be - /// cached. - NeedsBlockNumber(CacheKeyForSymbolicBlockTag), - /// The cache key is fully resolved and can be used to write to the cache. - Resolved(String), -} - -impl WriteCacheKey { - fn finalize(hasher: Hasher) -> Self { - Self::Resolved(hasher.finalize()) - } - - fn needs_range_check(hasher: Hasher, range: CacheableLogFilterRange<'_>) -> Option { - match range { - CacheableLogFilterRange::Hash(_) => Some(Self::finalize(hasher)), - CacheableLogFilterRange::Range { to_block, .. } => { - // TODO should we check that to < from? - Self::needs_safety_check(hasher, to_block) - } - } - } - - fn needs_safety_check(hasher: Hasher, block_spec: CacheableBlockSpec<'_>) -> Option { - match block_spec { - CacheableBlockSpec::Number { block_number } => { - Some(Self::NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber { - hasher: Box::new(hasher), - block_number, - })) - } - CacheableBlockSpec::Hash { .. } => Some(Self::finalize(hasher)), - CacheableBlockSpec::Earliest - | CacheableBlockSpec::Safe - | CacheableBlockSpec::Finalized => None, - } - } - - fn needs_block_number(method: CacheableRequestMethod<'_>) -> Option { - Some(Self::NeedsBlockNumber(CacheKeyForSymbolicBlockTag { - method: MethodWithResolvableSymbolicBlockSpec::new(method)?, - })) - } -} - -#[derive(Debug, Clone)] -pub(super) struct CacheKeyForUncheckedBlockNumber { - // Boxed to keep the size of the enum small. - hasher: Box, - pub(super) block_number: u64, -} - -impl CacheKeyForUncheckedBlockNumber { - /// Check whether the block number is safe to cache before returning a cache - /// key. - pub fn validate_block_number(self, chain_id: u64, latest_block_number: u64) -> Option { - let is_safe = is_safe_block_number(IsSafeBlockNumberArgs { - chain_id, - latest_block_number, - block_number: self.block_number, - }); - if is_safe { - Some(self.hasher.finalize()) - } else { - None - } - } -} - -#[derive(Debug, Clone)] -pub(super) enum ResolvedSymbolicTag { - /// It needs to be checked whether the block number is safe (reorg-free) - /// before writing to the cache. - NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber), - /// The cache key is fully resolved and can be used to write to the cache. - Resolved(String), -} - -#[derive(Debug, Clone)] -pub(super) struct CacheKeyForSymbolicBlockTag { - method: MethodWithResolvableSymbolicBlockSpec, -} - -impl CacheKeyForSymbolicBlockTag { - /// Check whether the block number is safe to cache before returning a cache - /// key. - pub(super) fn resolve_symbolic_tag(self, block_number: u64) -> Option { - let resolved_block_spec = CacheableBlockSpec::Number { block_number }; - - let resolved_method = match self.method { - MethodWithResolvableSymbolicBlockSpec::GetBlockByNumber { - include_tx_data, .. - } => CacheableRequestMethod::GetBlockByNumber { - block_spec: resolved_block_spec, - include_tx_data, - }, - }; - - resolved_method.write_cache_key().map(|key| match key { - WriteCacheKey::NeedsSafetyCheck(cache_key) => { - ResolvedSymbolicTag::NeedsSafetyCheck(cache_key) - } - WriteCacheKey::Resolved(cache_key) => ResolvedSymbolicTag::Resolved(cache_key), - WriteCacheKey::NeedsBlockNumber(_) => { - unreachable!("resolved block spec should not need block number") - } - }) - } -} - -/// Method invocations where, if the block spec argument is symbolic, it can be -/// resolved to a block number from the response. -#[derive(Debug, Clone)] -pub(super) enum MethodWithResolvableSymbolicBlockSpec { - GetBlockByNumber { include_tx_data: bool }, -} - -impl<'a> MethodWithResolvableSymbolicBlockSpec { - fn new(method: CacheableRequestMethod<'a>) -> Option { - match method { - CacheableRequestMethod::GetBlockByNumber { - include_tx_data, - block_spec: _, - } => Some(Self::GetBlockByNumber { include_tx_data }), - _ => None, - } - } -} - -/// A cache key that can be used to read from the cache. -/// It's based on not-fully resolved data, so it's not safe to write to this -/// cache key. Specifically, it's not checked whether the block number is safe -/// to cache (safe from reorgs). This is ok for reading from the cache, since -/// the result will be a cache miss if the block number is not safe to cache and -/// not having to resolve this data for reading offers performance advantages. -#[derive(Debug, Clone, PartialEq, Eq)] -#[repr(transparent)] -pub(super) struct ReadCacheKey(String); - -impl AsRef for ReadCacheKey { - fn as_ref(&self) -> &str { - &self.0 - } -} - -#[derive(Debug, Clone)] -struct Hasher { - hasher: Sha3_256, -} - -// The methods take `mut self` instead of `&mut self` to make sure no hash is -// constructed if one of the method arguments are invalid (in which case the -// method returns None and consumes self). -// -// Before variants of an enum are hashed, a variant marker is hashed before -// hashing the values of the variants to distinguish between them. E.g. the hash -// of `Enum::Foo(1u8)` should not equal the hash of `Enum::Bar(1u8)`, since -// these are not logically equivalent. This matches the behavior of the `Hash` -// derivation of the Rust standard library for enums. -// -// Instead of ignoring `None` values, the same pattern is followed for Options -// in order to let us distinguish between `[None, Some("a")]` and `[Some("a")]`. -// Note that if we use the cache key variant `0u8` for `None`, it's ok if `None` -// and `0u8`, hash to the same values since a type where `Option` and `u8` are -// valid values must be wrapped in an enum in Rust and the enum cache key -// variant prefix will distinguish between them. This wouldn't be the case with -// JSON though. -// -// When adding new types such as sequences or strings, [prefix -// collisions](https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions) should be -// considered. -impl Hasher { - fn new() -> Self { - Self { - hasher: Sha3_256::new(), - } - } - - fn hash_bytes(mut self, bytes: impl AsRef<[u8]>) -> Self { - self.hasher.update(bytes); - - self - } - - fn hash_u8(self, value: u8) -> Self { - self.hash_bytes(value.to_le_bytes()) - } - - fn hash_bool(self, value: &bool) -> Self { - self.hash_u8(u8::from(*value)) - } - - fn hash_address(self, address: &Address) -> Self { - self.hash_bytes(address) - } - - fn hash_u64(self, value: u64) -> Self { - self.hash_bytes(value.to_le_bytes()) - } - - fn hash_u256(self, value: &U256) -> Self { - self.hash_bytes(value.as_le_bytes()) - } - - fn hash_b256(self, value: &B256) -> Self { - self.hash_bytes(value) - } - - fn hash_block_spec( - self, - block_spec: &CacheableBlockSpec<'_>, - ) -> Result { - let this = self.hash_u8(block_spec.cache_key_variant()); - - match block_spec { - CacheableBlockSpec::Number { block_number } => Ok(this.hash_u64(*block_number)), - CacheableBlockSpec::Hash { - block_hash, - require_canonical, - } => { - let this = this - .hash_b256(block_hash) - .hash_u8(require_canonical.cache_key_variant()); - match require_canonical { - Some(require_canonical) => Ok(this.hash_bool(require_canonical)), - None => Ok(this), - } - } - CacheableBlockSpec::Earliest - | CacheableBlockSpec::Safe - | CacheableBlockSpec::Finalized => Err(SymbolicBlogTagError), - } - } - - fn hash_log_filter_options( - self, - params: &CacheableLogFilterOptions<'_>, - ) -> Result { - // Destructuring to make sure we get a compiler error here if the fields change. - let CacheableLogFilterOptions { - range, - address, - topics, - } = params; - - let mut this = self - .hash_log_filter_range(range)? - .hash_u64(address.len() as u64); - - for address in address { - this = this.hash_address(address); - } - - this = this.hash_u64(topics.len() as u64); - for options in topics { - this = this.hash_u8(options.cache_key_variant()); - if let Some(options) = options { - this = this.hash_u64(options.len() as u64); - for option in options { - this = this.hash_b256(option); - } - } - } - - Ok(this) - } - - fn hash_log_filter_range( - self, - params: &CacheableLogFilterRange<'_>, - ) -> Result { - let this = self.hash_u8(params.cache_key_variant()); - - match params { - CacheableLogFilterRange::Hash(block_hash) => Ok(this.hash_b256(block_hash)), - CacheableLogFilterRange::Range { - from_block, - to_block, - } => Ok(this - .hash_block_spec(from_block)? - .hash_block_spec(to_block)?), - } - } - - // Allow to keep same structure as other RequestMethod and other methods. - #[allow(clippy::match_same_arms)] - fn hash_method( - self, - method: &CacheableRequestMethod<'_>, - ) -> Result { - let this = self.hash_u8(method.cache_key_variant()); - - let this = match method { - CacheableRequestMethod::FeeHistory { - block_count, - newest_block, - reward_percentiles, - } => { - let this = this - .hash_u256(block_count) - .hash_block_spec(newest_block)? - .hash_u8(reward_percentiles.cache_key_variant()); - match reward_percentiles { - Some(reward_percentiles) => this.hash_reward_percentiles(reward_percentiles), - None => this, - } - } - CacheableRequestMethod::GetBalance { - address, - block_spec, - } => this.hash_address(address).hash_block_spec(block_spec)?, - CacheableRequestMethod::GetBlockByNumber { - block_spec, - include_tx_data, - } => this.hash_block_spec(block_spec)?.hash_bool(include_tx_data), - CacheableRequestMethod::GetBlockByHash { - block_hash, - include_tx_data, - } => this.hash_b256(block_hash).hash_bool(include_tx_data), - CacheableRequestMethod::GetCode { - address, - block_spec, - } => this.hash_address(address).hash_block_spec(block_spec)?, - CacheableRequestMethod::GetLogs { params } => this.hash_log_filter_options(params)?, - CacheableRequestMethod::GetStorageAt { - address, - position, - block_spec, - } => this - .hash_address(address) - .hash_u256(position) - .hash_block_spec(block_spec)?, - CacheableRequestMethod::GetTransactionByHash { transaction_hash } => { - this.hash_b256(transaction_hash) - } - CacheableRequestMethod::GetTransactionCount { - address, - block_spec, - } => this.hash_address(address).hash_block_spec(block_spec)?, - CacheableRequestMethod::GetTransactionReceipt { transaction_hash } => { - this.hash_b256(transaction_hash) - } - CacheableRequestMethod::NetVersion => this, - }; - - Ok(this) - } - - fn hash_reward_percentile(self, value: &RewardPercentile) -> Self { - const RESOLUTION: f64 = 100.0; - // `RewardPercentile` is an f64 in range [0, 100], so this is guaranteed not to - // overflow. - self.hash_u64((value.as_ref() * RESOLUTION).floor() as u64) - } - - fn hash_reward_percentiles(self, value: &[RewardPercentile]) -> Self { - let mut this = self.hash_u64(value.len() as u64); - for v in value { - this = this.hash_reward_percentile(v); - } - this - } - - fn finalize(self) -> String { - hex::encode(self.hasher.finalize_fixed()) - } -} - -#[derive(thiserror::Error, Debug)] -#[error("A symbolic block tag is not hashable.")] -struct SymbolicBlogTagError; - -// This could be replaced by the unstable -// [`core::intrinsics::discriminant_value`](https://dev-doc.rust-lang.org/beta/core/intrinsics/fn.discriminant_value.html) -// function once it becomes stable. -trait CacheKeyVariant { - fn cache_key_variant(&self) -> u8; -} - -impl CacheKeyVariant for Option { - fn cache_key_variant(&self) -> u8 { - match self { - None => 0, - Some(_) => 1, - } - } -} - impl<'a> CacheKeyVariant for &'a CacheableRequestMethod<'a> { fn cache_key_variant(&self) -> u8 { match self { @@ -814,26 +379,70 @@ impl<'a> CacheKeyVariant for &'a CacheableRequestMethod<'a> { } } -impl<'a> CacheKeyVariant for CacheableBlockSpec<'a> { - fn cache_key_variant(&self) -> u8 { - match self { - CacheableBlockSpec::Number { .. } => 0, - CacheableBlockSpec::Hash { .. } => 1, - CacheableBlockSpec::Earliest => 2, - CacheableBlockSpec::Safe => 3, - CacheableBlockSpec::Finalized => 4, - } - } -} - -impl<'a> CacheKeyVariant for CacheableLogFilterRange<'a> { - fn cache_key_variant(&self) -> u8 { - match self { - CacheableLogFilterRange::Hash(_) => 0, - CacheableLogFilterRange::Range { .. } => 1, - } - } -} +// // Allow to keep same structure as other RequestMethod and other methods. +// #[allow(clippy::match_same_arms)] +// fn hash_method( +// self, +// method: &CacheableRequestMethod<'_>, +// ) -> Result { +// let this = self.hash_u8(method.cache_key_variant()); + +// let this = match method { +// CacheableRequestMethod::FeeHistory { +// block_count, +// newest_block, +// reward_percentiles, +// } => { +// let this = this +// .hash_u256(block_count) +// .hash_block_spec(newest_block)? +// .hash_u8(reward_percentiles.cache_key_variant()); +// match reward_percentiles { +// Some(reward_percentiles) => +// this.hash_reward_percentiles(reward_percentiles), None => +// this, } +// } +// CacheableRequestMethod::GetBalance { +// address, +// block_spec, +// } => this.hash_address(address).hash_block_spec(block_spec)?, +// CacheableRequestMethod::GetBlockByNumber { +// block_spec, +// include_tx_data, +// } => this.hash_block_spec(block_spec)?.hash_bool(include_tx_data), +// CacheableRequestMethod::GetBlockByHash { +// block_hash, +// include_tx_data, +// } => this.hash_b256(block_hash).hash_bool(include_tx_data), +// CacheableRequestMethod::GetCode { +// address, +// block_spec, +// } => this.hash_address(address).hash_block_spec(block_spec)?, +// CacheableRequestMethod::GetLogs { params } => +// this.hash_log_filter_options(params)?, +// CacheableRequestMethod::GetStorageAt { +// address, +// position, +// block_spec, +// } => this +// .hash_address(address) +// .hash_u256(position) +// .hash_block_spec(block_spec)?, +// CacheableRequestMethod::GetTransactionByHash { transaction_hash } => +// { this.hash_b256(transaction_hash) +// } +// CacheableRequestMethod::GetTransactionCount { +// address, +// block_spec, +// } => this.hash_address(address).hash_block_spec(block_spec)?, +// CacheableRequestMethod::GetTransactionReceipt { transaction_hash } => +// { this.hash_b256(transaction_hash) +// } +// CacheableRequestMethod::NetVersion => this, +// }; + +// Ok(this) +// } #[cfg(test)] mod test { diff --git a/crates/edr_rpc_eth/src/call_request.rs b/crates/edr_rpc_eth/src/call_request.rs index e2bc82a5a..fb1bf2f7a 100644 --- a/crates/edr_rpc_eth/src/call_request.rs +++ b/crates/edr_rpc_eth/src/call_request.rs @@ -1,4 +1,4 @@ -use crate::{access_list::AccessListItem, Address, Bytes, B256, U256}; +use edr_eth::{access_list::AccessListItem, Address, Bytes, B256, U256}; /// For specifying input to methods requiring a transaction object, like /// `eth_call` and `eth_estimateGas` @@ -9,7 +9,10 @@ pub struct CallRequest { pub from: Option
, /// the address to which the transaction should be sent pub to: Option
, - #[cfg_attr(feature = "serde", serde(default, with = "crate::serde::optional_u64"))] + #[cfg_attr( + feature = "serde", + serde(default, with = "edr_eth::serde::optional_u64") + )] /// gas pub gas: Option, /// gas price diff --git a/crates/edr_rpc_eth/src/client.rs b/crates/edr_rpc_eth/src/client.rs new file mode 100644 index 000000000..0cadc3ebc --- /dev/null +++ b/crates/edr_rpc_eth/src/client.rs @@ -0,0 +1,328 @@ +use async_trait::async_trait; +use edr_rpc_client::RpcClient; + +use crate::{request_methods::RequestMethod, Transaction}; + +#[async_traitt] +pub trait EthClientExt { + /// Calls `eth_feeHistory` and returns the fee history. + async fn fee_history( + &self, + block_count: u64, + newest_block: BlockSpec, + reward_percentiles: Option>, + ) -> Result; + + /// Fetches the latest block number, chain ID, and network ID concurrently. + async fn fork_metadata(&self) -> Result; + + /// Submits three concurrent RPC method invocations in order to obtain + /// the set of data contained in [`AccountInfo`]. + async fn get_account_info( + &self, + address: &Address, + block: Option, + ) -> Result; + + /// Fetches account infos for multiple addresses using concurrent requests. + async fn get_account_infos( + &self, + addresses: &[Address], + block: Option, + ) -> Result, RpcClientError>; + + /// Calls `eth_getBlockByHash` and returns the transaction's hash. + async fn get_block_by_hash( + &self, + hash: &B256, + ) -> Result>, RpcClientError>; + + /// Calls `eth_getBalance`. + async fn get_balance( + &self, + address: &Address, + block: Option, + ) -> Result; + + /// Calls `eth_getBlockByHash` and returns the transaction's data. + async fn get_block_by_hash_with_transaction_data( + &self, + hash: &B256, + ) -> Result>, RpcClientError>; + + /// Calls `eth_getBlockByNumber` and returns the transaction's hash. + async fn get_block_by_number( + &self, + spec: PreEip1898BlockSpec, + ) -> Result>, RpcClientError>; + + /// Calls `eth_getBlockByNumber` and returns the transaction's data. + async fn get_block_by_number_with_transaction_data( + &self, + spec: PreEip1898BlockSpec, + ) -> Result, RpcClientError>; + + /// Calls `eth_getCode`. + async fn get_code( + &self, + address: &Address, + block: Option, + ) -> Result; + + /// Calls `eth_getLogs` using a starting and ending block (inclusive). + async fn get_logs_by_range( + &self, + from_block: BlockSpec, + to_block: BlockSpec, + address: Option>, + topics: Option>>>, + ) -> Result, RpcClientError>; + + /// Calls `eth_getTransactionByHash`. + async fn get_transaction_by_hash( + &self, + tx_hash: &B256, + ) -> Result, RpcClientError>; + + /// Calls `eth_getTransactionCount`. + async fn get_transaction_count( + &self, + address: &Address, + block: Option, + ) -> Result; + + /// Calls `eth_getTransactionReceipt`. + async fn get_transaction_receipt( + &self, + tx_hash: &B256, + ) -> Result, RpcClientError>; + + /// Methods for retrieving multiple transaction receipts using concurrent + /// requests. + async fn get_transaction_receipts( + &self, + hashes: impl IntoIterator + Debug, + ) -> Result>, RpcClientError>; + + /// Calls `eth_getStorageAt`. + async fn get_storage_at( + &self, + address: &Address, + position: U256, + block: Option, + ) -> Result, RpcClientError>; + + /// Calls `net_version`. + async fn network_id(&self) -> Result; +} + +impl EthClientExt for RpcClient { + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn fee_history( + &self, + block_count: u64, + newest_block: BlockSpec, + reward_percentiles: Option>, + ) -> Result { + self.call(MethodT::FeeHistory( + U256::from(block_count), + newest_block, + reward_percentiles, + )) + .await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn fork_metadata(&self) -> Result { + let network_id = self.network_id(); + let block_number = self.block_number(); + let chain_id = self.chain_id(); + + let (network_id, block_number, chain_id) = + tokio::try_join!(network_id, block_number, chain_id)?; + + Ok(ForkMetadata { + chain_id, + network_id, + latest_block_number: block_number, + }) + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_account_info( + &self, + address: &Address, + block: Option, + ) -> Result { + let balance = self.get_balance(address, block.clone()); + let nonce = self.get_transaction_count(address, block.clone()); + let code = self.get_code(address, block.clone()); + + let (balance, nonce, code) = tokio::try_join!(balance, nonce, code)?; + + let code = if code.is_empty() { + None + } else { + Some(Bytecode::new_raw(code)) + }; + + Ok(AccountInfo { + balance, + code_hash: code.as_ref().map_or(KECCAK_EMPTY, Bytecode::hash_slow), + code, + nonce: nonce.to(), + }) + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_account_infos( + &self, + addresses: &[Address], + block: Option, + ) -> Result, RpcClientError> { + futures::stream::iter(addresses.iter()) + .map(|address| self.get_account_info(address, block.clone())) + .buffered(MAX_PARALLEL_REQUESTS / 3 + 1) + .collect::>>() + .await + .into_iter() + .collect() + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_block_by_hash( + &self, + hash: &B256, + ) -> Result>, RpcClientError> { + self.call(MethodT::GetBlockByHash(*hash, false)).await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_balance( + &self, + address: &Address, + block: Option, + ) -> Result { + self.call(MethodT::GetBalance(*address, block)).await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_block_by_hash_with_transaction_data( + &self, + hash: &B256, + ) -> Result>, RpcClientError> { + self.call(MethodT::GetBlockByHash(*hash, true)).await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_block_by_number( + &self, + spec: PreEip1898BlockSpec, + ) -> Result>, RpcClientError> { + self.call_with_resolver( + MethodT::GetBlockByNumber(spec, false), + |block: &Option>| block.as_ref().and_then(|block| block.number), + ) + .await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_block_by_number_with_transaction_data( + &self, + spec: PreEip1898BlockSpec, + ) -> Result, RpcClientError> { + self.call_with_resolver( + MethodT::GetBlockByNumber(spec, true), + |block: ð::Block| block.number, + ) + .await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_code( + &self, + address: &Address, + block: Option, + ) -> Result { + self.call(MethodT::GetCode(*address, block)).await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_logs_by_range( + &self, + from_block: BlockSpec, + to_block: BlockSpec, + address: Option>, + topics: Option>>>, + ) -> Result, RpcClientError> { + self.call(MethodT::GetLogs(LogFilterOptions { + from_block: Some(from_block), + to_block: Some(to_block), + block_hash: None, + address, + topics, + })) + .await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_transaction_by_hash( + &self, + tx_hash: &B256, + ) -> Result, RpcClientError> { + self.call(MethodT::GetTransactionByHash(*tx_hash)).await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_transaction_count( + &self, + address: &Address, + block: Option, + ) -> Result { + self.call(MethodT::GetTransactionCount(*address, block)) + .await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_transaction_receipt( + &self, + tx_hash: &B256, + ) -> Result, RpcClientError> { + self.call(MethodT::GetTransactionReceipt(*tx_hash)).await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_transaction_receipts( + &self, + hashes: impl IntoIterator + Debug, + ) -> Result>, RpcClientError> { + let requests = hashes + .into_iter() + .map(|transaction_hash| self.get_transaction_receipt(transaction_hash)); + + futures::stream::iter(requests) + .buffered(MAX_PARALLEL_REQUESTS) + .collect::, RpcClientError>>>() + .await + .into_iter() + .collect() + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn get_storage_at( + &self, + address: &Address, + position: U256, + block: Option, + ) -> Result, RpcClientError> { + self.call(MethodT::GetStorageAt(*address, position, block)) + .await + } + + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + async fn network_id(&self) -> Result { + self.call::(MethodT::NetVersion(())) + .await + .map(|network_id| network_id.as_limbs()[0]) + } +} diff --git a/crates/edr_rpc_eth/src/eth.rs b/crates/edr_rpc_eth/src/eth.rs index 6d0381d7b..a48641e48 100644 --- a/crates/edr_rpc_eth/src/eth.rs +++ b/crates/edr_rpc_eth/src/eth.rs @@ -12,99 +12,6 @@ use crate::{ access_list::AccessListItem, withdrawal::Withdrawal, Address, Bloom, Bytes, B256, B64, U256, }; -/// transaction -#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Transaction { - /// hash of the transaction - pub hash: B256, - /// the number of transactions made by the sender prior to this one - #[serde(with = "crate::serde::u64")] - pub nonce: u64, - /// hash of the block where this transaction was in - pub block_hash: Option, - /// block number where this transaction was in - pub block_number: Option, - /// integer of the transactions index position in the block. null when its - /// pending - #[serde(with = "crate::serde::optional_u64")] - pub transaction_index: Option, - /// address of the sender - pub from: Address, - /// address of the receiver. null when its a contract creation transaction. - pub to: Option
, - /// value transferred in Wei - pub value: U256, - /// gas price provided by the sender in Wei - pub gas_price: U256, - /// gas provided by the sender - pub gas: U256, - /// the data sent along with the transaction - pub input: Bytes, - /// ECDSA recovery id - #[serde(with = "crate::serde::u64")] - pub v: u64, - /// Y-parity for EIP-2930 and EIP-1559 transactions. In theory these - /// transactions types shouldn't have a `v` field, but in practice they - /// are returned by nodes. - #[serde( - default, - rename = "yParity", - skip_serializing_if = "Option::is_none", - with = "crate::serde::optional_u64" - )] - pub y_parity: Option, - /// ECDSA signature r - pub r: U256, - /// ECDSA signature s - pub s: U256, - /// chain ID - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "crate::serde::optional_u64" - )] - pub chain_id: Option, - /// integer of the transaction type, 0x0 for legacy transactions, 0x1 for - /// access list types, 0x2 for dynamic fees - #[serde( - rename = "type", - default, - skip_serializing_if = "Option::is_none", - with = "crate::serde::optional_u64" - )] - pub transaction_type: Option, - /// access list - #[serde(default, skip_serializing_if = "Option::is_none")] - pub access_list: Option>, - /// max fee per gas - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_fee_per_gas: Option, - /// max priority fee per gas - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_priority_fee_per_gas: Option, - /// The maximum total fee per gas the sender is willing to pay for blob gas - /// in wei (EIP-4844) - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_fee_per_blob_gas: Option, - /// List of versioned blob hashes associated with the transaction's EIP-4844 - /// data blobs. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub blob_versioned_hashes: Option>, -} - -impl Transaction { - /// Returns whether the transaction has odd Y parity. - pub fn odd_y_parity(&self) -> bool { - self.v == 1 || self.v == 28 - } - - /// Returns whether the transaction is a legacy transaction. - pub fn is_legacy(&self) -> bool { - matches!(self.transaction_type, None | Some(0)) && matches!(self.v, 27 | 28) - } -} - /// Error that occurs when trying to convert the JSON-RPC `TransactionReceipt` /// type. #[derive(Debug, thiserror::Error)] diff --git a/crates/edr_rpc_eth/src/lib.rs b/crates/edr_rpc_eth/src/lib.rs index 2f5b1c489..e42c2aef8 100644 --- a/crates/edr_rpc_eth/src/lib.rs +++ b/crates/edr_rpc_eth/src/lib.rs @@ -1,17 +1,11 @@ -mod block_spec; mod cacheable_method_invocation; /// Input type for `eth_call` and `eth_estimateGas` mod call_request; -mod chain_id; +mod client; /// ethereum objects as specifically used in the JSON-RPC interface pub mod eth; -/// data types for use with filter-based RPC methods -pub mod filter; mod r#override; mod request_methods; +mod transaction; -pub use self::{ - block_spec::{BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec}, - call_request::CallRequest, - r#override::*, -}; +pub use self::{call_request::CallRequest, r#override::*, transaction::Transaction}; diff --git a/crates/edr_rpc_eth/src/override.rs b/crates/edr_rpc_eth/src/override.rs index f37aa995a..41ee5e227 100644 --- a/crates/edr_rpc_eth/src/override.rs +++ b/crates/edr_rpc_eth/src/override.rs @@ -1,4 +1,4 @@ -use crate::{Address, Bytes, HashMap, B256, U256}; +use edr_eth::{Address, Bytes, HashMap, B256, U256}; /// Type representing a set of overrides for storage information. pub type StorageOverride = HashMap; @@ -12,7 +12,7 @@ pub struct AccountOverrideOptions { #[serde( default, skip_serializing_if = "Option::is_none", - with = "crate::serde::optional_u64" + with = "edr_eth::serde::optional_u64" )] /// Account nonce override. pub nonce: Option, diff --git a/crates/edr_rpc_eth/src/request_methods.rs b/crates/edr_rpc_eth/src/request_methods.rs index 6717cd950..d455e7421 100644 --- a/crates/edr_rpc_eth/src/request_methods.rs +++ b/crates/edr_rpc_eth/src/request_methods.rs @@ -1,10 +1,6 @@ -use revm_primitives::{Address, B256}; +use edr_eth::{reward_percentile::RewardPercentile, Address, B256, U256}; -use crate::{ - remote::{filter::LogFilterOptions, BlockSpec, PreEip1898BlockSpec}, - reward_percentile::RewardPercentile, - U256, -}; +use crate::{filter::LogFilterOptions, BlockSpec, PreEip1898BlockSpec}; /// Methods for requests to a remote Ethereum node. Only contains methods /// supported by the [`crate::remote::client::RpcClient`]. @@ -12,7 +8,7 @@ use crate::{ #[serde(tag = "method", content = "params")] pub enum RequestMethod { /// `eth_blockNumber` - #[serde(rename = "eth_blockNumber", with = "crate::serde::empty_params")] + #[serde(rename = "eth_blockNumber", with = "edr_eth::serde::empty_params")] BlockNumber(()), /// `eth_feeHistory` #[serde(rename = "eth_feeHistory")] @@ -26,7 +22,7 @@ pub enum RequestMethod { Option>, ), /// `eth_chainId` - #[serde(rename = "eth_chainId", with = "crate::serde::empty_params")] + #[serde(rename = "eth_chainId", with = "edr_eth::serde::empty_params")] ChainId(()), /// `eth_getBalance` #[serde(rename = "eth_getBalance")] @@ -64,7 +60,7 @@ pub enum RequestMethod { Option, ), /// `eth_getLogs` - #[serde(rename = "eth_getLogs", with = "crate::serde::sequence")] + #[serde(rename = "eth_getLogs", with = "edr_eth::serde::sequence")] GetLogs(LogFilterOptions), /// `eth_getStorageAt` #[serde(rename = "eth_getStorageAt")] @@ -79,7 +75,7 @@ pub enum RequestMethod { Option, ), /// `eth_getTransactionByHash` - #[serde(rename = "eth_getTransactionByHash", with = "crate::serde::sequence")] + #[serde(rename = "eth_getTransactionByHash", with = "edr_eth::serde::sequence")] GetTransactionByHash(B256), /// `eth_getTransactionCount` #[serde(rename = "eth_getTransactionCount")] @@ -92,10 +88,13 @@ pub enum RequestMethod { Option, ), /// `eth_getTransactionReceipt` - #[serde(rename = "eth_getTransactionReceipt", with = "crate::serde::sequence")] + #[serde( + rename = "eth_getTransactionReceipt", + with = "edr_eth::serde::sequence" + )] GetTransactionReceipt(B256), /// `net_version` - #[serde(rename = "net_version", with = "crate::serde::empty_params")] + #[serde(rename = "net_version", with = "edr_eth::serde::empty_params")] NetVersion(()), } diff --git a/crates/edr_rpc_eth/src/transaction.rs b/crates/edr_rpc_eth/src/transaction.rs new file mode 100644 index 000000000..0a82ed74d --- /dev/null +++ b/crates/edr_rpc_eth/src/transaction.rs @@ -0,0 +1,94 @@ +use edr_eth::{access_list::AccessListItem, Address, Bytes, B256, U256}; + +/// RPC transaction +#[derive(Clone, Debug, PartialEq, Eq, Default, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Transaction { + /// hash of the transaction + pub hash: B256, + /// the number of transactions made by the sender prior to this one + #[serde(with = "edr_eth::serde::u64")] + pub nonce: u64, + /// hash of the block where this transaction was in + pub block_hash: Option, + /// block number where this transaction was in + pub block_number: Option, + /// integer of the transactions index position in the block. null when its + /// pending + #[serde(with = "edr_eth::serde::optional_u64")] + pub transaction_index: Option, + /// address of the sender + pub from: Address, + /// address of the receiver. null when its a contract creation transaction. + pub to: Option
, + /// value transferred in Wei + pub value: U256, + /// gas price provided by the sender in Wei + pub gas_price: U256, + /// gas provided by the sender + pub gas: U256, + /// the data sent along with the transaction + pub input: Bytes, + /// ECDSA recovery id + #[serde(with = "edr_eth::serde::u64")] + pub v: u64, + /// Y-parity for EIP-2930 and EIP-1559 transactions. In theory these + /// transactions types shouldn't have a `v` field, but in practice they + /// are returned by nodes. + #[serde( + default, + rename = "yParity", + skip_serializing_if = "Option::is_none", + with = "edr_eth::serde::optional_u64" + )] + pub y_parity: Option, + /// ECDSA signature r + pub r: U256, + /// ECDSA signature s + pub s: U256, + /// chain ID + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "edr_eth::serde::optional_u64" + )] + pub chain_id: Option, + /// integer of the transaction type, 0x0 for legacy transactions, 0x1 for + /// access list types, 0x2 for dynamic fees + #[serde( + rename = "type", + default, + skip_serializing_if = "Option::is_none", + with = "edr_eth::serde::optional_u64" + )] + pub transaction_type: Option, + /// access list + #[serde(default, skip_serializing_if = "Option::is_none")] + pub access_list: Option>, + /// max fee per gas + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_fee_per_gas: Option, + /// max priority fee per gas + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_priority_fee_per_gas: Option, + /// The maximum total fee per gas the sender is willing to pay for blob gas + /// in wei (EIP-4844) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_fee_per_blob_gas: Option, + /// List of versioned blob hashes associated with the transaction's EIP-4844 + /// data blobs. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub blob_versioned_hashes: Option>, +} + +impl Transaction { + /// Returns whether the transaction has odd Y parity. + pub fn odd_y_parity(&self) -> bool { + self.v == 1 || self.v == 28 + } + + /// Returns whether the transaction is a legacy transaction. + pub fn is_legacy(&self) -> bool { + matches!(self.transaction_type, None | Some(0)) && matches!(self.v, 27 | 28) + } +} diff --git a/crates/edr_rpc_eth/tests/client.rs b/crates/edr_rpc_eth/tests/client.rs new file mode 100644 index 000000000..9f4c3112f --- /dev/null +++ b/crates/edr_rpc_eth/tests/client.rs @@ -0,0 +1,846 @@ +use std::{ops::Deref, str::FromStr}; + +use reqwest::StatusCode; +use tempfile::TempDir; + +use super::*; + +struct TestRpcClient { + client: RpcClient, + + // Need to keep the tempdir around to prevent it from being deleted + // Only accessed when feature = "test-remote", hence the allow. + #[allow(dead_code)] + cache_dir: TempDir, +} + +impl TestRpcClient { + fn new(url: &str) -> Self { + let tempdir = TempDir::new().unwrap(); + Self { + client: RpcClient::new(url, tempdir.path().into(), None).expect("url ok"), + cache_dir: tempdir, + } + } +} + +impl Deref for TestRpcClient { + type Target = RpcClient; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +#[tokio::test] +async fn send_request_body_400_status() { + const STATUS_CODE: u16 = 400; + + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("POST", "/") + .with_status(STATUS_CODE.into()) + .with_header("content-type", "text/plain") + .create_async() + .await; + + let hash = B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933022222") + .expect("failed to parse hash from string"); + + let error = TestRpcClient::new(&server.url()) + .call::>(MethodT::GetTransactionByHash(hash)) + .await + .expect_err("should have failed to due to a HTTP status error"); + + if let RpcClientError::HttpStatus(error) = error { + assert_eq!( + reqwest::Error::from(error).status(), + Some(StatusCode::from_u16(STATUS_CODE).unwrap()) + ); + } else { + unreachable!("Invalid error: {error}"); + } + + mock.assert_async().await; +} + +#[cfg(feature = "test-remote")] +mod alchemy { + use std::fs::File; + + use edr_test_utils::env::get_alchemy_url; + use futures::future::join_all; + use walkdir::WalkDir; + + use super::*; + use crate::Bytes; + + // The maximum block number that Alchemy allows + const MAX_BLOCK_NUMBER: u64 = u64::MAX >> 1; + + impl TestRpcClient { + fn files_in_cache(&self) -> Vec { + let mut files = Vec::new(); + for entry in WalkDir::new(&self.cache_dir) + .follow_links(true) + .into_iter() + .filter_map(Result::ok) + { + if entry.file_type().is_file() { + files.push(entry.path().to_owned()); + } + } + files + } + } + + #[tokio::test] + async fn call_bad_api_key() { + let api_key = "invalid-api-key"; + let alchemy_url = format!("https://eth-mainnet.g.alchemy.com/v2/{api_key}"); + + let hash = + B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933022222") + .expect("failed to parse hash from string"); + + let error = TestRpcClient::new(&alchemy_url) + .call::>(MethodT::GetTransactionByHash(hash)) + .await + .expect_err("should have failed to interpret response as a Transaction"); + + assert!(!error.to_string().contains(api_key)); + + if let RpcClientError::HttpStatus(error) = error { + assert_eq!( + reqwest::Error::from(error).status(), + Some(StatusCode::from_u16(401).unwrap()) + ); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn call_failed_to_send_error() { + let alchemy_url = "https://xxxeth-mainnet.g.alchemy.com/"; + + let hash = + B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933051111") + .expect("failed to parse hash from string"); + + let error = TestRpcClient::new(alchemy_url) + .call::>(MethodT::GetTransactionByHash(hash)) + .await + .expect_err("should have failed to connect due to a garbage domain name"); + + if let RpcClientError::FailedToSend(error) = error { + assert!(error.to_string().contains("dns error")); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn test_is_cacheable_block_number() { + let client = TestRpcClient::new(&get_alchemy_url()); + + let latest_block_number = client.block_number().await.unwrap(); + + { + assert!(client.cached_block_number.read().await.is_some()); + } + + // Latest block number is never cacheable + assert!(!client + .is_cacheable_block_number(latest_block_number) + .await + .unwrap()); + + assert!(client.is_cacheable_block_number(16220843).await.unwrap()); + } + + #[tokio::test] + async fn get_account_info_works_from_cache() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + let block_spec = BlockSpec::Number(16220843); + + assert_eq!(client.files_in_cache().len(), 0); + + // Populate cache + client + .get_account_info(&dai_address, Some(block_spec.clone())) + .await + .expect("should have succeeded"); + + assert_eq!(client.files_in_cache().len(), 3); + + // Returned from cache + let account_info = client + .get_account_info(&dai_address, Some(block_spec)) + .await + .expect("should have succeeded"); + + assert_eq!(client.files_in_cache().len(), 3); + + assert_eq!(account_info.balance, U256::ZERO); + assert_eq!(account_info.nonce, 1); + assert_ne!(account_info.code_hash, KECCAK_EMPTY); + assert!(account_info.code.is_some()); + } + + #[tokio::test] + async fn get_account_info_works_with_partial_cache() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + let block_spec = BlockSpec::Number(16220843); + + assert_eq!(client.files_in_cache().len(), 0); + + // Populate cache + client + .get_transaction_count(&dai_address, Some(block_spec.clone())) + .await + .expect("should have succeeded"); + + assert_eq!(client.files_in_cache().len(), 1); + + let account_info = client + .get_account_info(&dai_address, Some(block_spec.clone())) + .await + .expect("should have succeeded"); + + assert_eq!(client.files_in_cache().len(), 3); + + assert_eq!(account_info.balance, U256::ZERO); + assert_eq!(account_info.nonce, 1); + assert_ne!(account_info.code_hash, KECCAK_EMPTY); + assert!(account_info.code.is_some()); + } + + #[tokio::test] + async fn get_account_info_unknown_block() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let error = TestRpcClient::new(&alchemy_url) + .get_account_info(&dai_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) + .await + .expect_err("should have failed"); + + if let RpcClientError::JsonRpcError { error, .. } = error { + assert_eq!(error.code, -32602); + assert_eq!(error.message, "Unknown block number"); + assert!(error.data.is_none()); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn get_account_infos() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + let hardhat_default_address = + Address::from_str("0xbe862ad9abfe6f22bcb087716c7d89a26051f74c") + .expect("failed to parse address"); + + let account_infos = TestRpcClient::new(&alchemy_url) + .get_account_infos( + &[dai_address, hardhat_default_address], + Some(BlockSpec::latest()), + ) + .await + .expect("should have succeeded"); + + assert_eq!(account_infos.len(), 2); + } + + #[tokio::test] + async fn get_block_by_hash_some() { + let alchemy_url = get_alchemy_url(); + + let hash = + B256::from_str("0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812") + .expect("failed to parse hash from string"); + + let block = TestRpcClient::new(&alchemy_url) + .get_block_by_hash(&hash) + .await + .expect("should have succeeded"); + + assert!(block.is_some()); + let block = block.unwrap(); + + assert_eq!(block.hash, Some(hash)); + assert_eq!(block.transactions.len(), 192); + } + + #[tokio::test] + async fn get_block_by_hash_with_transaction_data_some() { + let alchemy_url = get_alchemy_url(); + + let hash = + B256::from_str("0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812") + .expect("failed to parse hash from string"); + + let block = TestRpcClient::new(&alchemy_url) + .get_block_by_hash_with_transaction_data(&hash) + .await + .expect("should have succeeded"); + + assert!(block.is_some()); + let block = block.unwrap(); + + assert_eq!(block.hash, Some(hash)); + assert_eq!(block.transactions.len(), 192); + } + + #[tokio::test] + async fn get_block_by_number_finalized_resolves() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + assert_eq!(client.files_in_cache().len(), 0); + + client + .get_block_by_number(PreEip1898BlockSpec::finalized()) + .await + .expect("should have succeeded"); + + // Finalized tag should be resolved and stored in cache. + assert_eq!(client.files_in_cache().len(), 1); + } + + #[tokio::test] + async fn get_block_by_number_some() { + let alchemy_url = get_alchemy_url(); + + let block_number = 16222385; + + let block = TestRpcClient::new(&alchemy_url) + .get_block_by_number(PreEip1898BlockSpec::Number(block_number)) + .await + .expect("should have succeeded") + .expect("Block must exist"); + + assert_eq!(block.number, Some(block_number)); + assert_eq!(block.transactions.len(), 102); + } + + #[tokio::test] + async fn get_block_by_number_with_transaction_data_unsafe_no_cache() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + assert_eq!(client.files_in_cache().len(), 0); + + let block_number = client.block_number().await.unwrap(); + + // Check that the block number call caches the largest known block number + { + assert!(client.cached_block_number.read().await.is_some()); + } + + assert_eq!(client.files_in_cache().len(), 0); + + let block = client + .get_block_by_number(PreEip1898BlockSpec::Number(block_number)) + .await + .expect("should have succeeded") + .expect("Block must exist"); + + // Unsafe block number shouldn't be cached + assert_eq!(client.files_in_cache().len(), 0); + + assert_eq!(block.number, Some(block_number)); + } + + #[tokio::test] + async fn get_block_with_transaction_data_cached() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + let block_spec = PreEip1898BlockSpec::Number(16220843); + + assert_eq!(client.files_in_cache().len(), 0); + + let block_from_remote = client + .get_block_by_number_with_transaction_data(block_spec.clone()) + .await + .expect("should have from remote"); + + assert_eq!(client.files_in_cache().len(), 1); + + let block_from_cache = client + .get_block_by_number_with_transaction_data(block_spec.clone()) + .await + .expect("should have from remote"); + + assert_eq!(block_from_remote, block_from_cache); + } + + #[tokio::test] + async fn get_earliest_block_with_transaction_data_resolves() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + assert_eq!(client.files_in_cache().len(), 0); + + client + .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::earliest()) + .await + .expect("should have succeeded"); + + // Earliest tag should be resolved to block number and it should be cached. + assert_eq!(client.files_in_cache().len(), 1); + } + + #[tokio::test] + async fn get_latest_block() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number(PreEip1898BlockSpec::latest()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_latest_block_with_transaction_data() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::latest()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_pending_block() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number(PreEip1898BlockSpec::pending()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_pending_block_with_transaction_data() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::pending()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_logs_some() { + let alchemy_url = get_alchemy_url(); + let logs = TestRpcClient::new(&alchemy_url) + .get_logs_by_range( + BlockSpec::Number(10496585), + BlockSpec::Number(10496585), + Some(OneOrMore::One( + Address::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + .expect("failed to parse data"), + )), + None, + ) + .await + .expect("failed to get logs"); + + assert_eq!(logs.len(), 12); + // TODO: assert more things about the log(s) + // TODO: consider asserting something about the logs bloom + } + + #[tokio::test] + async fn get_logs_future_from_block() { + let alchemy_url = get_alchemy_url(); + let error = TestRpcClient::new(&alchemy_url) + .get_logs_by_range( + BlockSpec::Number(MAX_BLOCK_NUMBER), + BlockSpec::Number(MAX_BLOCK_NUMBER), + Some(OneOrMore::One( + Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") + .expect("failed to parse data"), + )), + None, + ) + .await + .expect_err("should have failed to get logs"); + + if let RpcClientError::JsonRpcError { error, .. } = error { + assert_eq!(error.code, -32000); + assert_eq!(error.message, "One of the blocks specified in filter (fromBlock, toBlock or blockHash) cannot be found."); + assert!(error.data.is_none()); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn get_logs_future_to_block() { + let alchemy_url = get_alchemy_url(); + let logs = TestRpcClient::new(&alchemy_url) + .get_logs_by_range( + BlockSpec::Number(10496585), + BlockSpec::Number(MAX_BLOCK_NUMBER), + Some(OneOrMore::One( + Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") + .expect("failed to parse data"), + )), + None, + ) + .await + .expect("should have succeeded"); + + assert_eq!(logs, []); + } + + #[tokio::test] + async fn get_transaction_by_hash_some() { + let alchemy_url = get_alchemy_url(); + + let hash = + B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a") + .expect("failed to parse hash from string"); + + let tx = TestRpcClient::new(&alchemy_url) + .get_transaction_by_hash(&hash) + .await + .expect("failed to get transaction by hash"); + + assert!(tx.is_some()); + let tx = tx.unwrap(); + + assert_eq!( + tx.block_hash, + Some( + B256::from_str( + "0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8" + ) + .expect("couldn't parse data") + ) + ); + assert_eq!( + tx.block_number, + Some(U256::from_str_radix("a74fde", 16).expect("couldn't parse data")) + ); + assert_eq!(tx.hash, hash); + assert_eq!( + tx.from, + Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") + .expect("couldn't parse data") + ); + assert_eq!( + tx.gas, + U256::from_str_radix("30d40", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.gas_price, + U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.input, + Bytes::from(hex::decode("a9059cbb000000000000000000000000e2c1e729e05f34c07d80083982ccd9154045dcc600000000000000000000000000000000000000000000000000000004a817c800").unwrap()) + ); + assert_eq!( + tx.nonce, + u64::from_str_radix("653b", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.r, + U256::from_str_radix( + "eb56df45bd355e182fba854506bc73737df275af5a323d30f98db13fdf44393a", + 16 + ) + .expect("couldn't parse data") + ); + assert_eq!( + tx.s, + U256::from_str_radix( + "2c6efcd210cdc7b3d3191360f796ca84cab25a52ed8f72efff1652adaabc1c83", + 16 + ) + .expect("couldn't parse data") + ); + assert_eq!( + tx.to, + Some( + Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") + .expect("couldn't parse data") + ) + ); + assert_eq!( + tx.transaction_index, + Some(u64::from_str_radix("88", 16).expect("couldn't parse data")) + ); + assert_eq!( + tx.v, + u64::from_str_radix("1c", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.value, + U256::from_str_radix("0", 16).expect("couldn't parse data") + ); + } + + #[tokio::test] + async fn get_transaction_count_some() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let transaction_count = TestRpcClient::new(&alchemy_url) + .get_transaction_count(&dai_address, Some(BlockSpec::Number(16220843))) + .await + .expect("should have succeeded"); + + assert_eq!(transaction_count, U256::from(1)); + } + + #[tokio::test] + async fn get_transaction_count_future_block() { + let alchemy_url = get_alchemy_url(); + + let missing_address = Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") + .expect("failed to parse address"); + + let error = TestRpcClient::new(&alchemy_url) + .get_transaction_count(&missing_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) + .await + .expect_err("should have failed"); + + if let RpcClientError::JsonRpcError { error, .. } = error { + assert_eq!(error.code, -32602); + assert_eq!(error.message, "Unknown block number"); + assert!(error.data.is_none()); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn get_transaction_receipt_some() { + let alchemy_url = get_alchemy_url(); + + let hash = + B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a") + .expect("failed to parse hash from string"); + + let receipt = TestRpcClient::new(&alchemy_url) + .get_transaction_receipt(&hash) + .await + .expect("failed to get transaction by hash"); + + assert!(receipt.is_some()); + let receipt = receipt.unwrap(); + + assert_eq!( + receipt.block_hash, + B256::from_str("0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8") + .expect("couldn't parse data") + ); + assert_eq!(receipt.block_number, 0xa74fde); + assert_eq!(receipt.contract_address, None); + assert_eq!(receipt.cumulative_gas_used(), 0x56c81b); + assert_eq!( + receipt.effective_gas_price, + Some(U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data")) + ); + assert_eq!( + receipt.from, + Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") + .expect("couldn't parse data") + ); + assert_eq!( + receipt.gas_used, + u64::from_str_radix("a0f9", 16).expect("couldn't parse data") + ); + assert_eq!(receipt.logs().len(), 1); + assert_eq!(receipt.state_root(), None); + assert_eq!(receipt.status_code(), Some(1)); + assert_eq!( + receipt.to, + Some( + Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") + .expect("couldn't parse data") + ) + ); + assert_eq!(receipt.transaction_hash, hash); + assert_eq!(receipt.transaction_index, 136); + assert_eq!(receipt.transaction_type(), 0); + } + + #[tokio::test] + async fn get_storage_at_some() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let total_supply = TestRpcClient::new(&alchemy_url) + .get_storage_at( + &dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + + assert_eq!( + total_supply, + Some( + U256::from_str_radix( + "000000000000000000000000000000000000000010a596ae049e066d4991945c", + 16 + ) + .expect("failed to parse storage location") + ) + ); + } + + #[tokio::test] + async fn get_storage_at_latest() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let _total_supply = TestRpcClient::new(&alchemy_url) + .get_storage_at( + &dai_address, + U256::from_str_radix( + "0000000000000000000000000000000000000000000000000000000000000001", + 16, + ) + .expect("failed to parse storage location"), + Some(BlockSpec::latest()), + ) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_storage_at_future_block() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let storage_slot = TestRpcClient::new(&alchemy_url) + .get_storage_at( + &dai_address, + U256::from(1), + Some(BlockSpec::Number(MAX_BLOCK_NUMBER)), + ) + .await + .expect("should have succeeded"); + + assert!(storage_slot.is_none()); + } + + #[tokio::test] + async fn network_id_success() { + let alchemy_url = get_alchemy_url(); + + let version = TestRpcClient::new(&alchemy_url) + .network_id() + .await + .expect("should have succeeded"); + + assert_eq!(version, 1); + } + + #[tokio::test] + async fn stores_result_in_cache() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let total_supply = client + .get_storage_at( + &dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + + let cached_files = client.files_in_cache(); + assert_eq!(cached_files.len(), 1); + + let mut file = File::open(&cached_files[0]).expect("failed to open file"); + let cached_result: Option = + serde_json::from_reader(&mut file).expect("failed to parse"); + + assert_eq!(total_supply, cached_result); + } + + #[tokio::test] + async fn concurrent_writes_to_cache_smoke_test() { + let client = TestRpcClient::new(&get_alchemy_url()); + + let test_contents = "some random test data 42"; + let cache_key = "cache-key"; + + assert_eq!(client.files_in_cache().len(), 0); + + join_all((0..100).map(|_| client.write_response_to_cache(cache_key, test_contents))).await; + + assert_eq!(client.files_in_cache().len(), 1); + + let contents = tokio::fs::read_to_string(&client.files_in_cache()[0]) + .await + .unwrap(); + assert_eq!(contents, serde_json::to_string(test_contents).unwrap()); + } + + #[tokio::test] + async fn handles_invalid_type_in_cache_single_call() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + client + .get_storage_at( + &dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + + // Write some valid JSON, but invalid U256 + tokio::fs::write(&client.files_in_cache()[0], "\"not-hex\"") + .await + .unwrap(); + + client + .get_storage_at( + &dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + } +} diff --git a/crates/edr_rpc_eth/tests/receipt.rs b/crates/edr_rpc_eth/tests/receipt.rs new file mode 100644 index 000000000..f2016bb37 --- /dev/null +++ b/crates/edr_rpc_eth/tests/receipt.rs @@ -0,0 +1,106 @@ +#[cfg(feature = "test-remote")] +mod remote { + use serial_test::serial; + + macro_rules! impl_test_remote_block_receipt_root { + ($( + $name:ident => $block_number:literal, + )+) => { + $( + paste::item! { + #[tokio::test] + #[serial] + async fn []() { + use edr_eth::{remote::{RpcClient, PreEip1898BlockSpec}, trie::ordered_trie_root}; + use edr_test_utils::env::get_alchemy_url; + + let client = RpcClient::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None).expect("url ok"); + + let block = client + .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::Number($block_number)) + .await + .expect("Should succeed"); + + let receipts = client.get_transaction_receipts(block.transactions.iter().map(|transaction| &transaction.hash)) + .await + .expect("Should succeed") + .expect("All receipts of a block should exist"); + + let receipts_root = ordered_trie_root( + receipts + .into_iter() + .map(|receipt| alloy_rlp::encode(&**receipt)), + ); + + assert_eq!(block.receipts_root, receipts_root); + } + } + )+ + }; + } + + impl_test_remote_block_receipt_root! { + pre_eip658 => 1_500_000u64, + post_eip658 => 5_370_000u64, + eip2930 => 12_751_000u64, // block contains at least one transaction with type 1 + eip1559 => 14_000_000u64, // block contains at least one transaction with type 2 + } + + macro_rules! impl_test_receipt_rlp_encoding { + ($( + $name:ident: $transaction_hash:literal => $encoding:literal, + )+) => { + $( + paste::item! { + #[tokio::test] + async fn []() { + use edr_test_utils::env::get_alchemy_url; + use tempfile::TempDir; + + use crate::{remote::RpcClient, B256}; + + let tempdir = TempDir::new().unwrap(); + let client = RpcClient::new(&get_alchemy_url(), tempdir.path().into(), None).unwrap(); + + let transaction_hash = B256::from_slice(&hex::decode($transaction_hash).unwrap()); + + let receipt = client + .get_transaction_receipt(&transaction_hash) + .await + .expect("Should succeed") + .expect("Receipt must exist"); + + // Generated by Hardhat + let expected = hex::decode($encoding).unwrap(); + + assert_eq!(alloy_rlp::encode(&receipt).to_vec(), expected); + + let decoded = TypedReceipt::::decode(&mut expected.as_slice()).unwrap(); + let receipt = TypedReceipt { + data: receipt.inner.inner.data, + spec_id: SpecId::LATEST, + cumulative_gas_used: receipt.inner.inner.cumulative_gas_used, + logs_bloom: receipt.inner.inner.logs_bloom, + logs: receipt.inner.inner.logs.into_iter().map(|log| { + log.inner.inner.inner.clone() + }).collect(), + }; + + assert_eq!(decoded, receipt); + } + } + )+ + }; + } + + impl_test_receipt_rlp_encoding! { + pre_eip658: "427b0b68b1ccc46b01d99ed399b61c4ae681e22216035eb6953afc83ef463e17" + => "f90128a08af7cdcc6991b441f6285f4298df6add6506b3fd3ca559b0682fb2d57929652f825208b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", + post_eip658: "1421a887a02301ae127bf2cd4c006116053c9dc4a255e69ea403a2d77c346cf5" + => "f9010801825208b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", + eip2930: "01361649690dbff1ac1da1a7351a125b7cb0f26b9c5e017c4aefe90135b9be14" + => "01f90109018351e668b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", + eip1559: "3dac2080b4c423029fcc9c916bc430cde441badfe736fc6d1fe9325348af80fd" + => "02f91c7701830ea827b9010000201000000000000004040080005010004000000000000000000000280004102000000000000400004001800001010003000000080020000000004100200010020000000000000800000008080220a04000a00100400040000000008000000010200200001000000000b000000000004100804000000540100040100009002000010100000010000000004000100000000000004020000828000244001000080200006000020400080000804100004000800801a000000401010000000000000100000200080000010000000088140008202280000000100008008200102800c010200000000320000002000020000000004000010080100000000040000000f91b6cf9017c949008d19f58aabd9ed0d60971565aa8510560ab41f842a0a07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17a0000000000000000000000000f967aa80d80d6f22df627219c5113a118b57d0efb901200000000000000000000000009e46a38f5daabe8683e10793b06749eef7d733d1000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee0000000000000000000000000000000000000000000069e10de76676d080000000000000000000000000000000000000000000000000000128e23797a37671e700000000000000000000000000000000000000000000004efc8c538e1084000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000038d11030660bf632eb31345d6b5f3e6ba2d2311cc783f3764c405f5272200dd670f967aa80d80d6f22df627219c5113a118b57d0ef61e0b5bc0000000000000000f9017c949008d19f58aabd9ed0d60971565aa8510560ab41f842a0a07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17a000000000000000000000000003bada9ff1cf0d0264664b43977ed08feee32584b90120000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000c7283b66eb1eb5fb86327f08e1b5816b0720212b000000000000000000000000000000000000000000000000fbec1330f280b3fd000000000000000000000000000000000000000000000a322816740bfd499e6600000000000000000000000000000000000000000000000000b46ffd87079be800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000380437203aec40e6c16642904f125e7d97bf45f39a51a729d775b8e3ea3cc667cd03bada9ff1cf0d0264664b43977ed08feee3258461e0b5d90000000000000000f89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000f967aa80d80d6f22df627219c5113a118b57d0efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a00000000000000000000000000000000000000000000069e10de76676d0800000f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000003bada9ff1cf0d0264664b43977ed08feee32584a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a0000000000000000000000000000000000000000000000000fbec1330f280b3fdf89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000006992115b12e8bffc0000f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000005aaa0053fa5c28e8c558d4c648cc129bea45018a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000000000000000000000000000065ca4d7e75041e08f89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000005aaa0053fa5c28e8c558d4c648cc129bea45018a0000000000000000000000000000000000000000000002330b073b0f83ffeaaaaf9011c9405aaa0053fa5c28e8c558d4c648cc129bea45018f863a0c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b8a0000000000000000000000000000000000000000000002330b073b0f83ffeaaaaffffffffffffffffffffffffffffffffffffffffffffffff9a35b2818afbe1f8000000000000000000000000000000000000000001af7ab4eebc6eaabac2b8540000000000000000000000000000000000000000000008462c8201774345ee54fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe778cf89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000119c71d3bbac22029622cbaec24854d3d32d2828a000000000000000000000000000000000000000000000466160e761f07ffd5556f89b94119c71d3bbac22029622cbaec24854d3d32d2828f842a0b9ed0243fdf00f0545c63a0af8850c090d86bb46682baec4bf3c496814fe4f02a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b8401fd446014fc12b89bb0aee29f847d7fbf7e4237a671c70945013f952ca4a567a00000000000000000000000000000000000000000000000000000000a6444848f89b949e46a38f5daabe8683e10793b06749eef7d733d1f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000ca90ac7c132da0602b69b84af2b6a69a905379a2a000000000000000000000000000000000000000000000466160e761f07ffd5556f89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000ca90ac7c132da0602b69b84af2b6a69a905379a2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000000000000000000000000000000000000afdf72bb8f89b94dac17f958d2ee523a2206206994597c13d831ec7f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000001111111254fb6c44bac0bed2854e76f90643097da00000000000000000000000000000000000000000000000000000000afdf72bb8f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000945bcf562085de2d5875b9e2012ed5fd5cfab927a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000000c96c4c288f973ff8f89b94dac17f958d2ee523a2206206994597c13d831ec7f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000945bcf562085de2d5875b9e2012ed5fd5cfab927a00000000000000000000000000000000000000000000000000000000afdf72bb8f879941111111254fb6c44bac0bed2854e76f90643097de1a0c3b639f02b125bfa160e50739b8c44eb2d1b6908e2b6d5925c6d770f2ca78127b840a7036786313db68944e1789fa8a69f90c41e29301442c6731a151688acbe6a26000000000000000000000000000000000000000000000000c96c4c288f973ff8f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000dd9f24efc84d93deef3c8745c837ab63e80abd27a00000000000000000000000000000000000000000000000000654620f6124ec19f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000000000000000000000000000128e23797a37671e7f8f99411111112542d85b3ef69ae05771c2dccff4faa26e1a0d6d4f5681c246c9f42c203e287975af1601f8df8035a9251f79aab5c8f09e2f8b8c00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab410000000000000000000000009e46a38f5daabe8683e10793b06749eef7d733d1000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41000000000000000000000000000000000000000000006992115b12e8bffc000000000000000000000000000000000000000000000000000128e23797a37671e7f89b949008d19f58aabd9ed0d60971565aa8510560ab41f842a0ed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2a000000000000000000000000011111112542d85b3ef69ae05771c2dccff4faa26b84000000000000000000000000000000000000000000000000000000000000000007c02520000000000000000000000000000000000000000000000000000000000f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000000fb37a3336b791815f89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a08c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8a0000000000000000000000000000000000000000000000000fb37a3336b791815f8dd94ba12222222228d8ba445958a75a0704d566bf2c8f884a02170c741c41531aec20e7c107c24eecfdd15e69c9bb0a8dd37b1840b9e0b207ba00b09dea16768f0799065c475be02919503cb2a3500020000000000000000001aa0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2a00000000000000000000000006b175474e89094c44da98b954eedeac495271d0fb840000000000000000000000000000000000000000000000000fb37a3336b791815000000000000000000000000000000000000000000000c74f130b221944b84caf89b94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8a0000000000000000000000000000000000000000000000000fb37a3336b791815f89b946b175474e89094c44da98b954eedeac495271d0ff863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000ba12222222228d8ba445958a75a0704d566bf2c8a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000c74f130b221944b84caf89b94956f47f50a910163d8bf957cf5846d573e7f87caf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa0000000000000000000000000bb2e5c2ff298fd96e166f90c8abacaf714df14f8a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000c74da893438a10088bff89b946b175474e89094c44da98b954eedeac495271d0ff863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000bb2e5c2ff298fd96e166f90c8abacaf714df14f8a0000000000000000000000000000000000000000000000c74f130b221944b84caf9011c94bb2e5c2ff298fd96e166f90c8abacaf714df14f8f863a0c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b8a0000000000000000000000000000000000000000000000c74f130b221944b84cafffffffffffffffffffffffffffffffffffffffffffff38b2576cbc75eff77410000000000000000000000000000000000000001000f16fba6ff427463b0357d00000000000000000000000000000000000000000fefa81144cbfbd548a25a7c0000000000000000000000000000000000000000000000000000000000000004f89b94956f47f50a910163d8bf957cf5846d573e7f87caf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000009928e4046d7c6513326ccea028cd3e7a91c7590aa0000000000000000000000000000000000000000000000c74da893438a10088bff89b94c7283b66eb1eb5fb86327f08e1b5816b0720212bf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009928e4046d7c6513326ccea028cd3e7a91c7590aa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a0000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f879949928e4046d7c6513326ccea028cd3e7a91c7590ae1a01c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1b8400000000000000000000000000000000000000000007f015873850f3f6be1a2500000000000000000000000000000000000000000006821664c5f0a4b332570a2f8fc949928e4046d7c6513326ccea028cd3e7a91c7590af863a0d78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2b880000000000000000000000000000000000000000000000c74da893438a10088bf00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f89b94c7283b66eb1eb5fb86327f08e1b5816b0720212bf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000027239549dd40e1d60f5b80b0c4196923745b1fd2a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a0000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f8f99411111112542d85b3ef69ae05771c2dccff4faa26e1a0d6d4f5681c246c9f42c203e287975af1601f8df8035a9251f79aab5c8f09e2f8b8c00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2000000000000000000000000c7283b66eb1eb5fb86327f08e1b5816b0720212b0000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41000000000000000000000000000000000000000000000000fb37a3336b791815000000000000000000000000000000000000000000000a2faafe6664fcd2fd91f89b949008d19f58aabd9ed0d60971565aa8510560ab41f842a0ed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2a000000000000000000000000011111112542d85b3ef69ae05771c2dccff4faa26b84000000000000000000000000000000000000000000000000000000000000000007c02520000000000000000000000000000000000000000000000000000000000f87a94c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a07fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65a00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000000000000000000000000000128e23797a37671e7f89b949008d19f58aabd9ed0d60971565aa8510560ab41f842a0ed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2a0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2b84000000000000000000000000000000000000000000000000000000000000000002e1a7d4d00000000000000000000000000000000000000000000000000000000f89b94c7283b66eb1eb5fb86327f08e1b5816b0720212bf863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa00000000000000000000000009008d19f58aabd9ed0d60971565aa8510560ab41a000000000000000000000000003bada9ff1cf0d0264664b43977ed08feee32584a0000000000000000000000000000000000000000000000a322816740bfd499e66f85a949008d19f58aabd9ed0d60971565aa8510560ab41f842a040338ce1a7c49204f0099533b1e9a7ee0a3d261f84974ab7af36105b8c4e9db4a0000000000000000000000000de1c59bc25d806ad9ddcbe246c4b5e550564571880", + } +} From 9400ca98269959d8926d65fcbc8b1b0a3510ab46 Mon Sep 17 00:00:00 2001 From: Wodann Date: Wed, 29 May 2024 00:45:15 +0000 Subject: [PATCH 03/18] WIP --- crates/edr_rpc_client/src/cache.rs | 27 +- crates/edr_rpc_client/src/cache/block_spec.rs | 36 +- .../src/{ => cache}/chain_id.rs | 2 +- crates/edr_rpc_client/src/cache/filter.rs | 9 +- crates/edr_rpc_client/src/cache/hasher.rs | 4 +- crates/edr_rpc_client/src/cache/key.rs | 68 +- crates/edr_rpc_client/src/client.rs | 895 +----------------- crates/edr_rpc_client/src/lib.rs | 6 +- crates/edr_rpc_eth/Cargo.toml | 1 + .../src/cacheable_method_invocation.rs | 295 ++---- crates/edr_rpc_eth/src/client.rs | 18 +- crates/edr_rpc_eth/src/fork.rs | 10 + crates/edr_rpc_eth/src/lib.rs | 2 + crates/edr_rpc_eth/src/request_methods.rs | 28 +- 14 files changed, 261 insertions(+), 1140 deletions(-) rename crates/edr_rpc_client/src/{ => cache}/chain_id.rs (93%) create mode 100644 crates/edr_rpc_eth/src/fork.rs diff --git a/crates/edr_rpc_client/src/cache.rs b/crates/edr_rpc_client/src/cache.rs index 0bdf7fa10..7bc344cc8 100644 --- a/crates/edr_rpc_client/src/cache.rs +++ b/crates/edr_rpc_client/src/cache.rs @@ -1,5 +1,8 @@ -mod block_spec; -mod filter; +/// Types for caching block specifications. +pub mod block_spec; +pub(crate) mod chain_id; +/// Types for caching filters. +pub mod filter; mod hasher; /// Types for indexing the cache. pub mod key; @@ -13,12 +16,17 @@ use std::{ use serde::de::DeserializeOwned; -pub use self::hasher::Hasher as CacheKeyHasher; +pub use self::hasher::KeyHasher; use self::key::{ReadCacheKey, WriteCacheKey}; use crate::RpcClientError; /// Trait for types that can be cached. pub trait CacheableMethod: Sized { + /// The type representing the cached method. + type Cached<'method>: Into>; + + /// The type representing a subset of methods containing a [`BlockTag`] + /// which can be resolved to a block number. type MethodWithResolvableBlockTag: Clone + Debug; /// Creates a method for requesting the block number. @@ -27,11 +35,22 @@ pub trait CacheableMethod: Sized { /// Creates a method for requesting the chain ID. fn chain_id_request() -> Self; + #[cfg(feature = "tracing")] + /// Returns the name of the method. + fn name(&self) -> &'static str; + /// Resolves a block tag to a block number for the provided method. - fn resolve_block_tag(method: Self::MethodWithResolvableBlockTag, block_number: u64) -> Self; + fn resolve_block_tag<'method>( + method: Self::MethodWithResolvableBlockTag, + block_number: u64, + ) -> Self::Cached<'method> where; + /// Returns the instance's [`ReadCacheKey`] if it can be read from the + /// cache. fn read_cache_key(&self) -> Option; + /// Returns the instance's [`WriteCacheKey`] if it can be written to the + /// cache. fn write_cache_key(&self) -> Option>; } diff --git a/crates/edr_rpc_client/src/cache/block_spec.rs b/crates/edr_rpc_client/src/cache/block_spec.rs index 1863ab74c..b50b55d93 100644 --- a/crates/edr_rpc_client/src/cache/block_spec.rs +++ b/crates/edr_rpc_client/src/cache/block_spec.rs @@ -1,4 +1,4 @@ -use edr_eth::{BlockSpec, BlockTag, Eip1898BlockSpec, B256}; +use edr_eth::{BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec, B256}; use super::key::CacheKeyVariant; @@ -6,10 +6,15 @@ use super::key::CacheKeyVariant; #[derive(Clone, Debug)] pub enum CacheableBlockSpec<'a> { /// Block number - Number { block_number: u64 }, + Number { + /// Block number + block_number: u64, + }, /// Block hash Hash { + /// Block hash block_hash: &'a B256, + /// Whether an error should be returned if the block is not canonical require_canonical: Option, }, /// "earliest" block tag @@ -82,6 +87,33 @@ impl<'a> TryFrom<&'a Option> for CacheableBlockSpec<'a> { } } +/// Error type for [`CacheableBlockSpec::try_from`]. +#[derive(thiserror::Error, Debug)] +#[error("Block spec is not cacheable: {0:?}")] +pub struct PreEip1898BlockSpecNotCacheableError(PreEip1898BlockSpec); + +impl<'a> TryFrom<&'a PreEip1898BlockSpec> for CacheableBlockSpec<'a> { + type Error = PreEip1898BlockSpecNotCacheableError; + + fn try_from(value: &'a PreEip1898BlockSpec) -> Result { + match value { + PreEip1898BlockSpec::Number(block_number) => Ok(CacheableBlockSpec::Number { + block_number: *block_number, + }), + PreEip1898BlockSpec::Tag(tag) => match tag { + // Latest and pending can never be resolved to a safe block number. + BlockTag::Latest | BlockTag::Pending => { + Err(PreEip1898BlockSpecNotCacheableError(value.clone())) + } + // Earliest, safe and finalized are potentially resolvable to a safe block number. + BlockTag::Earliest => Ok(CacheableBlockSpec::Earliest), + BlockTag::Safe => Ok(CacheableBlockSpec::Safe), + BlockTag::Finalized => Ok(CacheableBlockSpec::Finalized), + }, + } + } +} + /// Error type for [`Hasher::hash_block_spec`]. #[derive(thiserror::Error, Debug)] #[error("A block tag is not cacheable.")] diff --git a/crates/edr_rpc_client/src/chain_id.rs b/crates/edr_rpc_client/src/cache/chain_id.rs similarity index 93% rename from crates/edr_rpc_client/src/chain_id.rs rename to crates/edr_rpc_client/src/cache/chain_id.rs index 6a927e583..49fac8673 100644 --- a/crates/edr_rpc_client/src/chain_id.rs +++ b/crates/edr_rpc_client/src/cache/chain_id.rs @@ -1,4 +1,4 @@ -pub(super) fn chain_id_from_url(url: &url::Url) -> Option { +pub fn chain_id_from_url(url: &url::Url) -> Option { let host = url.host_str()?; match host { "mainnet.infura.io" | "eth-mainnet.g.alchemy.com" => Some(1), diff --git a/crates/edr_rpc_client/src/cache/filter.rs b/crates/edr_rpc_client/src/cache/filter.rs index 4bca1cf8e..969e0d0d1 100644 --- a/crates/edr_rpc_client/src/cache/filter.rs +++ b/crates/edr_rpc_client/src/cache/filter.rs @@ -7,8 +7,8 @@ use super::{block_spec::CacheableBlockSpec, key::CacheKeyVariant}; /// A cacheable input for the `eth_getLogs` method. #[derive(Clone, Debug)] -pub(super) struct CacheableLogFilterOptions<'a> { - /// The range +pub struct CacheableLogFilterOptions<'a> { + /// The range pub range: CacheableLogFilterRange<'a>, /// The addresses pub addresses: Vec<&'a Address>, @@ -20,7 +20,7 @@ pub(super) struct CacheableLogFilterOptions<'a> { /// [`CacheableLogFilterRange::try_from`]. #[derive(thiserror::Error, Debug)] #[error("Method is not cacheable: {0:?}")] -pub(super) struct LogFilterOptionsNotCacheableError(LogFilterOptions); +pub struct LogFilterOptionsNotCacheableError(LogFilterOptions); impl<'a> TryFrom<&'a LogFilterOptions> for CacheableLogFilterOptions<'a> { type Error = LogFilterOptionsNotCacheableError; @@ -54,9 +54,10 @@ impl<'a> TryFrom<&'a LogFilterOptions> for CacheableLogFilterOptions<'a> { /// A cacheable range input for the `eth_getLogs` method. #[derive(Clone, Debug)] -pub(super) enum CacheableLogFilterRange<'a> { +pub enum CacheableLogFilterRange<'a> { /// The `block_hash` argument Hash(&'a B256), + /// A range of blocks Range { /// The `from_block` argument from_block: CacheableBlockSpec<'a>, diff --git a/crates/edr_rpc_client/src/cache/hasher.rs b/crates/edr_rpc_client/src/cache/hasher.rs index 9f173489b..46b9c825f 100644 --- a/crates/edr_rpc_client/src/cache/hasher.rs +++ b/crates/edr_rpc_client/src/cache/hasher.rs @@ -8,7 +8,7 @@ use super::{ }; #[derive(Debug, Clone)] -pub struct Hasher { +pub struct KeyHasher { hasher: Sha3_256, } @@ -33,7 +33,7 @@ pub struct Hasher { // When adding new types such as sequences or strings, [prefix // collisions](https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions) should be // considered. -impl Hasher { +impl KeyHasher { pub fn new() -> Self { Self { hasher: Sha3_256::new(), diff --git a/crates/edr_rpc_client/src/cache/key.rs b/crates/edr_rpc_client/src/cache/key.rs index 990a25584..678457a3a 100644 --- a/crates/edr_rpc_client/src/cache/key.rs +++ b/crates/edr_rpc_client/src/cache/key.rs @@ -1,13 +1,16 @@ use edr_eth::block::{is_safe_block_number, IsSafeBlockNumberArgs}; -use super::{block_spec::CacheableBlockSpec, filter::CacheableLogFilterRange, CacheKeyHasher}; -use crate::CacheableMethod; +use super::{ + block_spec::CacheableBlockSpec, filter::CacheableLogFilterRange, hasher::KeyHasher, + CacheableMethod, +}; /// Trait for retrieving the unique id of an enum variant. // This could be replaced by the unstable // [`core::intrinsics::discriminant_value`](https://dev-doc.rust-lang.org/beta/core/intrinsics/fn.discriminant_value.html) // function once it becomes stable. pub trait CacheKeyVariant { + /// Returns the unique id of the enum variant. fn cache_key_variant(&self) -> u8; } @@ -26,16 +29,24 @@ impl CacheKeyVariant for Option { /// to cache (safe from reorgs). This is ok for reading from the cache, since /// the result will be a cache miss if the block number is not safe to cache and /// not having to resolve this data for reading offers performance advantages. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[repr(transparent)] pub struct ReadCacheKey(String); +impl ReadCacheKey { + /// Finalizes the provided [`KeyHasher`] and return the resolved cache key. + pub fn finalize(hasher: KeyHasher) -> Self { + Self(hasher.finalize()) + } +} + impl AsRef for ReadCacheKey { fn as_ref(&self) -> &str { &self.0 } } +/// A cache key that can be used to write to the cache. #[derive(Clone, Debug)] pub enum WriteCacheKey { /// It needs to be checked whether the block number is safe (reorg-free) @@ -44,18 +55,22 @@ pub enum WriteCacheKey { /// The method invocation contains a symbolic block spec (e.g. "finalized") /// that needs to be resolved to a block number before the result can be /// cached. - NeedsBlockNumber(CacheKeyForBlockTag), + NeedsBlockTagResolution(CacheKeyForUnresolvedBlockTag), /// The cache key is fully resolved and can be used to write to the cache. Resolved(String), } impl WriteCacheKey { - fn finalize(hasher: CacheKeyHasher) -> Self { + /// Finalizes the provided [`KeyHasher`] and return the resolved cache + /// key. + pub fn finalize(hasher: KeyHasher) -> Self { Self::Resolved(hasher.finalize()) } - fn needs_range_check( - hasher: CacheKeyHasher, + /// Checks whether the block number is safe to cache before returning a + /// cache key. + pub fn needs_range_check( + hasher: KeyHasher, range: CacheableLogFilterRange<'_>, ) -> Option { match range { @@ -67,8 +82,10 @@ impl WriteCacheKey { } } - fn needs_safety_check( - hasher: CacheKeyHasher, + /// Checks whether the block number is safe to cache before returning a + /// cache key. + pub fn needs_safety_check( + hasher: KeyHasher, block_spec: CacheableBlockSpec<'_>, ) -> Option { match block_spec { @@ -84,12 +101,24 @@ impl WriteCacheKey { | CacheableBlockSpec::Finalized => None, } } + + /// Checks whether a block tag needs to be resolved before returning a cache + /// key. + pub fn needs_block_tag_resolution(method: MethodT::Cached<'_>) -> Option { + let method = method.into()?; + + Some(Self::NeedsBlockTagResolution( + CacheKeyForUnresolvedBlockTag { method }, + )) + } } -#[derive(Debug, Clone)] -pub(crate) struct CacheKeyForUncheckedBlockNumber { +/// A cache key for which the block number needs to be checked before writing to +/// the cache. +#[derive(Clone, Debug)] +pub struct CacheKeyForUncheckedBlockNumber { // Boxed to keep the size of the enum small. - hasher: Box, + hasher: Box, pub(super) block_number: u64, } @@ -110,7 +139,7 @@ impl CacheKeyForUncheckedBlockNumber { } } -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub(crate) enum ResolvedSymbolicTag { /// It needs to be checked whether the block number is safe (reorg-free) /// before writing to the cache. @@ -119,16 +148,17 @@ pub(crate) enum ResolvedSymbolicTag { Resolved(String), } -#[derive(Debug, Clone)] -pub(crate) struct CacheKeyForBlockTag { +/// A cache key for which the block tag needs to be resolved before writing to +/// the cache. +#[derive(Clone, Debug)] +pub struct CacheKeyForUnresolvedBlockTag { method: MethodT::MethodWithResolvableBlockTag, } -impl CacheKeyForBlockTag { +impl CacheKeyForUnresolvedBlockTag { /// Check whether the block number is safe to cache before returning a cache /// key. - pub fn resolve_symbolic_tag(self, block_number: u64) -> Option { - let resolved_block_spec = CacheableBlockSpec::Number { block_number }; + pub(crate) fn resolve_block_tag(self, block_number: u64) -> Option { let resolved_method = MethodT::resolve_block_tag(self.method, block_number); resolved_method.write_cache_key().map(|key| match key { @@ -136,7 +166,7 @@ impl CacheKeyForBlockTag { ResolvedSymbolicTag::NeedsSafetyCheck(cache_key) } WriteCacheKey::Resolved(cache_key) => ResolvedSymbolicTag::Resolved(cache_key), - WriteCacheKey::NeedsBlockNumber(_) => { + WriteCacheKey::NeedsBlockTagResolution(_) => { unreachable!("resolved block spec should not need block number") } }) diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index c0cf49415..920bf050b 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -11,9 +11,9 @@ use edr_eth::{ block::{block_time, is_safe_block_number, IsSafeBlockNumberArgs}, U64, }; -use futures::{future, stream::StreamExt, TryFutureExt}; +use futures::{future, TryFutureExt}; use hyper::header::HeaderValue; -pub use hyper::{header, http, HeaderMap}; +pub use hyper::{header, HeaderMap}; use reqwest::Client as HttpClient; use reqwest_middleware::{ClientBuilder as HttpClientBuilder, ClientWithMiddleware}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; @@ -26,11 +26,14 @@ use uuid::Uuid; use crate::{ cache::{ self, - key::{CacheKeyForBlockTag, CacheKeyForUncheckedBlockNumber, ResolvedSymbolicTag}, - remove_from_cache, CachedBlockNumber, + chain_id::chain_id_from_url, + key::{ + CacheKeyForUncheckedBlockNumber, CacheKeyForUnresolvedBlockTag, ReadCacheKey, + ResolvedSymbolicTag, WriteCacheKey, + }, + remove_from_cache, CacheableMethod, CachedBlockNumber, }, - chain_id::chain_id_from_url, - jsonrpc, CacheableMethod, MiddlewareError, ReadCacheKey, ReqwestError, WriteCacheKey, + jsonrpc, MiddlewareError, ReqwestError, }; const RPC_CACHE_DIR: &str = "rpc_cache"; @@ -40,9 +43,6 @@ const EXPONENT_BASE: u32 = 2; const MIN_RETRY_INTERVAL: Duration = Duration::from_secs(1); const MAX_RETRY_INTERVAL: Duration = Duration::from_secs(32); const MAX_RETRIES: u32 = 9; -// Constrain parallel requests to avoid rate limiting on transport level and -// thundering herd during backoff. -const MAX_PARALLEL_REQUESTS: usize = 20; /// Specialized error types #[derive(Debug, thiserror::Error)] @@ -321,13 +321,12 @@ impl RpcClient { async fn resolve_block_tag( &self, - block_tag_resolver: CacheKeyForBlockTag, + block_tag_resolver: CacheKeyForUnresolvedBlockTag, result: ResultT, resolve_block_number: impl Fn(ResultT) -> Option, ) -> Result, RpcClientError> { if let Some(block_number) = resolve_block_number(result) { - if let Some(resolved_cache_key) = block_tag_resolver.resolve_symbolic_tag(block_number) - { + if let Some(resolved_cache_key) = block_tag_resolver.resolve_block_tag(block_number) { return match resolved_cache_key { ResolvedSymbolicTag::NeedsSafetyCheck(safety_checker) => { self.validate_block_number(safety_checker).await @@ -350,7 +349,7 @@ impl RpcClient { WriteCacheKey::NeedsSafetyCheck(safety_checker) => { self.validate_block_number(safety_checker).await } - WriteCacheKey::NeedsBlockNumber(block_tag_resolver) => { + WriteCacheKey::NeedsBlockTagResolution(block_tag_resolver) => { self.resolve_block_tag(block_tag_resolver, result, resolve_block_number) .await } @@ -610,17 +609,6 @@ impl RpcClient { } } -/// Metadata about a forked chain. -#[derive(Clone, Debug)] -pub struct ForkMetadata { - /// Chain id as returned by `eth_chainId` - pub chain_id: u64, - /// Network id as returned by `net_version` - pub network_id: u64, - /// The latest block number as returned by `eth_blockNumber` - pub latest_block_number: u64, -} - /// Ensure that the directory exists. async fn ensure_cache_directory( directory: impl AsRef, @@ -649,861 +637,4 @@ impl SerializedRequest { } #[cfg(test)] -mod tests { - use std::{ops::Deref, str::FromStr}; - - use reqwest::StatusCode; - use tempfile::TempDir; - - use super::*; - - struct TestRpcClient { - client: RpcClient, - - // Need to keep the tempdir around to prevent it from being deleted - // Only accessed when feature = "test-remote", hence the allow. - #[allow(dead_code)] - cache_dir: TempDir, - } - - impl TestRpcClient { - fn new(url: &str) -> Self { - let tempdir = TempDir::new().unwrap(); - Self { - client: RpcClient::new(url, tempdir.path().into(), None).expect("url ok"), - cache_dir: tempdir, - } - } - } - - impl Deref for TestRpcClient { - type Target = RpcClient; - - fn deref(&self) -> &Self::Target { - &self.client - } - } - - #[tokio::test] - async fn send_request_body_400_status() { - const STATUS_CODE: u16 = 400; - - let mut server = mockito::Server::new_async().await; - - let mock = server - .mock("POST", "/") - .with_status(STATUS_CODE.into()) - .with_header("content-type", "text/plain") - .create_async() - .await; - - let hash = - B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933022222") - .expect("failed to parse hash from string"); - - let error = TestRpcClient::new(&server.url()) - .call::>(MethodT::GetTransactionByHash(hash)) - .await - .expect_err("should have failed to due to a HTTP status error"); - - if let RpcClientError::HttpStatus(error) = error { - assert_eq!( - reqwest::Error::from(error).status(), - Some(StatusCode::from_u16(STATUS_CODE).unwrap()) - ); - } else { - unreachable!("Invalid error: {error}"); - } - - mock.assert_async().await; - } - - #[cfg(feature = "test-remote")] - mod alchemy { - use std::fs::File; - - use edr_test_utils::env::get_alchemy_url; - use futures::future::join_all; - use walkdir::WalkDir; - - use super::*; - use crate::Bytes; - - // The maximum block number that Alchemy allows - const MAX_BLOCK_NUMBER: u64 = u64::MAX >> 1; - - impl TestRpcClient { - fn files_in_cache(&self) -> Vec { - let mut files = Vec::new(); - for entry in WalkDir::new(&self.cache_dir) - .follow_links(true) - .into_iter() - .filter_map(Result::ok) - { - if entry.file_type().is_file() { - files.push(entry.path().to_owned()); - } - } - files - } - } - - #[tokio::test] - async fn call_bad_api_key() { - let api_key = "invalid-api-key"; - let alchemy_url = format!("https://eth-mainnet.g.alchemy.com/v2/{api_key}"); - - let hash = B256::from_str( - "0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933022222", - ) - .expect("failed to parse hash from string"); - - let error = TestRpcClient::new(&alchemy_url) - .call::>(MethodT::GetTransactionByHash(hash)) - .await - .expect_err("should have failed to interpret response as a Transaction"); - - assert!(!error.to_string().contains(api_key)); - - if let RpcClientError::HttpStatus(error) = error { - assert_eq!( - reqwest::Error::from(error).status(), - Some(StatusCode::from_u16(401).unwrap()) - ); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn call_failed_to_send_error() { - let alchemy_url = "https://xxxeth-mainnet.g.alchemy.com/"; - - let hash = B256::from_str( - "0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933051111", - ) - .expect("failed to parse hash from string"); - - let error = TestRpcClient::new(alchemy_url) - .call::>(MethodT::GetTransactionByHash(hash)) - .await - .expect_err("should have failed to connect due to a garbage domain name"); - - if let RpcClientError::FailedToSend(error) = error { - assert!(error.to_string().contains("dns error")); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn test_is_cacheable_block_number() { - let client = TestRpcClient::new(&get_alchemy_url()); - - let latest_block_number = client.block_number().await.unwrap(); - - { - assert!(client.cached_block_number.read().await.is_some()); - } - - // Latest block number is never cacheable - assert!(!client - .is_cacheable_block_number(latest_block_number) - .await - .unwrap()); - - assert!(client.is_cacheable_block_number(16220843).await.unwrap()); - } - - #[tokio::test] - async fn get_account_info_works_from_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - let block_spec = BlockSpec::Number(16220843); - - assert_eq!(client.files_in_cache().len(), 0); - - // Populate cache - client - .get_account_info(&dai_address, Some(block_spec.clone())) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 3); - - // Returned from cache - let account_info = client - .get_account_info(&dai_address, Some(block_spec)) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 3); - - assert_eq!(account_info.balance, U256::ZERO); - assert_eq!(account_info.nonce, 1); - assert_ne!(account_info.code_hash, KECCAK_EMPTY); - assert!(account_info.code.is_some()); - } - - #[tokio::test] - async fn get_account_info_works_with_partial_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - let block_spec = BlockSpec::Number(16220843); - - assert_eq!(client.files_in_cache().len(), 0); - - // Populate cache - client - .get_transaction_count(&dai_address, Some(block_spec.clone())) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 1); - - let account_info = client - .get_account_info(&dai_address, Some(block_spec.clone())) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 3); - - assert_eq!(account_info.balance, U256::ZERO); - assert_eq!(account_info.nonce, 1); - assert_ne!(account_info.code_hash, KECCAK_EMPTY); - assert!(account_info.code.is_some()); - } - - #[tokio::test] - async fn get_account_info_unknown_block() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let error = TestRpcClient::new(&alchemy_url) - .get_account_info(&dai_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) - .await - .expect_err("should have failed"); - - if let RpcClientError::JsonRpcError { error, .. } = error { - assert_eq!(error.code, -32602); - assert_eq!(error.message, "Unknown block number"); - assert!(error.data.is_none()); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn get_account_infos() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - let hardhat_default_address = - Address::from_str("0xbe862ad9abfe6f22bcb087716c7d89a26051f74c") - .expect("failed to parse address"); - - let account_infos = TestRpcClient::new(&alchemy_url) - .get_account_infos( - &[dai_address, hardhat_default_address], - Some(BlockSpec::latest()), - ) - .await - .expect("should have succeeded"); - - assert_eq!(account_infos.len(), 2); - } - - #[tokio::test] - async fn get_block_by_hash_some() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812", - ) - .expect("failed to parse hash from string"); - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash(&hash) - .await - .expect("should have succeeded"); - - assert!(block.is_some()); - let block = block.unwrap(); - - assert_eq!(block.hash, Some(hash)); - assert_eq!(block.transactions.len(), 192); - } - - #[tokio::test] - async fn get_block_by_hash_with_transaction_data_some() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812", - ) - .expect("failed to parse hash from string"); - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash_with_transaction_data(&hash) - .await - .expect("should have succeeded"); - - assert!(block.is_some()); - let block = block.unwrap(); - - assert_eq!(block.hash, Some(hash)); - assert_eq!(block.transactions.len(), 192); - } - - #[tokio::test] - async fn get_block_by_number_finalized_resolves() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - assert_eq!(client.files_in_cache().len(), 0); - - client - .get_block_by_number(PreEip1898BlockSpec::finalized()) - .await - .expect("should have succeeded"); - - // Finalized tag should be resolved and stored in cache. - assert_eq!(client.files_in_cache().len(), 1); - } - - #[tokio::test] - async fn get_block_by_number_some() { - let alchemy_url = get_alchemy_url(); - - let block_number = 16222385; - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(PreEip1898BlockSpec::Number(block_number)) - .await - .expect("should have succeeded") - .expect("Block must exist"); - - assert_eq!(block.number, Some(block_number)); - assert_eq!(block.transactions.len(), 102); - } - - #[tokio::test] - async fn get_block_by_number_with_transaction_data_unsafe_no_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - assert_eq!(client.files_in_cache().len(), 0); - - let block_number = client.block_number().await.unwrap(); - - // Check that the block number call caches the largest known block number - { - assert!(client.cached_block_number.read().await.is_some()); - } - - assert_eq!(client.files_in_cache().len(), 0); - - let block = client - .get_block_by_number(PreEip1898BlockSpec::Number(block_number)) - .await - .expect("should have succeeded") - .expect("Block must exist"); - - // Unsafe block number shouldn't be cached - assert_eq!(client.files_in_cache().len(), 0); - - assert_eq!(block.number, Some(block_number)); - } - - #[tokio::test] - async fn get_block_with_transaction_data_cached() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - let block_spec = PreEip1898BlockSpec::Number(16220843); - - assert_eq!(client.files_in_cache().len(), 0); - - let block_from_remote = client - .get_block_by_number_with_transaction_data(block_spec.clone()) - .await - .expect("should have from remote"); - - assert_eq!(client.files_in_cache().len(), 1); - - let block_from_cache = client - .get_block_by_number_with_transaction_data(block_spec.clone()) - .await - .expect("should have from remote"); - - assert_eq!(block_from_remote, block_from_cache); - } - - #[tokio::test] - async fn get_earliest_block_with_transaction_data_resolves() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - assert_eq!(client.files_in_cache().len(), 0); - - client - .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::earliest()) - .await - .expect("should have succeeded"); - - // Earliest tag should be resolved to block number and it should be cached. - assert_eq!(client.files_in_cache().len(), 1); - } - - #[tokio::test] - async fn get_latest_block() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(PreEip1898BlockSpec::latest()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_latest_block_with_transaction_data() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::latest()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_pending_block() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(PreEip1898BlockSpec::pending()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_pending_block_with_transaction_data() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::pending()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_logs_some() { - let alchemy_url = get_alchemy_url(); - let logs = TestRpcClient::new(&alchemy_url) - .get_logs_by_range( - BlockSpec::Number(10496585), - BlockSpec::Number(10496585), - Some(OneOrMore::One( - Address::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") - .expect("failed to parse data"), - )), - None, - ) - .await - .expect("failed to get logs"); - - assert_eq!(logs.len(), 12); - // TODO: assert more things about the log(s) - // TODO: consider asserting something about the logs bloom - } - - #[tokio::test] - async fn get_logs_future_from_block() { - let alchemy_url = get_alchemy_url(); - let error = TestRpcClient::new(&alchemy_url) - .get_logs_by_range( - BlockSpec::Number(MAX_BLOCK_NUMBER), - BlockSpec::Number(MAX_BLOCK_NUMBER), - Some(OneOrMore::One( - Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse data"), - )), - None, - ) - .await - .expect_err("should have failed to get logs"); - - if let RpcClientError::JsonRpcError { error, .. } = error { - assert_eq!(error.code, -32000); - assert_eq!(error.message, "One of the blocks specified in filter (fromBlock, toBlock or blockHash) cannot be found."); - assert!(error.data.is_none()); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn get_logs_future_to_block() { - let alchemy_url = get_alchemy_url(); - let logs = TestRpcClient::new(&alchemy_url) - .get_logs_by_range( - BlockSpec::Number(10496585), - BlockSpec::Number(MAX_BLOCK_NUMBER), - Some(OneOrMore::One( - Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse data"), - )), - None, - ) - .await - .expect("should have succeeded"); - - assert_eq!(logs, []); - } - - #[tokio::test] - async fn get_transaction_by_hash_some() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a", - ) - .expect("failed to parse hash from string"); - - let tx = TestRpcClient::new(&alchemy_url) - .get_transaction_by_hash(&hash) - .await - .expect("failed to get transaction by hash"); - - assert!(tx.is_some()); - let tx = tx.unwrap(); - - assert_eq!( - tx.block_hash, - Some( - B256::from_str( - "0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8" - ) - .expect("couldn't parse data") - ) - ); - assert_eq!( - tx.block_number, - Some(U256::from_str_radix("a74fde", 16).expect("couldn't parse data")) - ); - assert_eq!(tx.hash, hash); - assert_eq!( - tx.from, - Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") - .expect("couldn't parse data") - ); - assert_eq!( - tx.gas, - U256::from_str_radix("30d40", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.gas_price, - U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.input, - Bytes::from(hex::decode("a9059cbb000000000000000000000000e2c1e729e05f34c07d80083982ccd9154045dcc600000000000000000000000000000000000000000000000000000004a817c800").unwrap()) - ); - assert_eq!( - tx.nonce, - u64::from_str_radix("653b", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.r, - U256::from_str_radix( - "eb56df45bd355e182fba854506bc73737df275af5a323d30f98db13fdf44393a", - 16 - ) - .expect("couldn't parse data") - ); - assert_eq!( - tx.s, - U256::from_str_radix( - "2c6efcd210cdc7b3d3191360f796ca84cab25a52ed8f72efff1652adaabc1c83", - 16 - ) - .expect("couldn't parse data") - ); - assert_eq!( - tx.to, - Some( - Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") - .expect("couldn't parse data") - ) - ); - assert_eq!( - tx.transaction_index, - Some(u64::from_str_radix("88", 16).expect("couldn't parse data")) - ); - assert_eq!( - tx.v, - u64::from_str_radix("1c", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.value, - U256::from_str_radix("0", 16).expect("couldn't parse data") - ); - } - - #[tokio::test] - async fn get_transaction_count_some() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let transaction_count = TestRpcClient::new(&alchemy_url) - .get_transaction_count(&dai_address, Some(BlockSpec::Number(16220843))) - .await - .expect("should have succeeded"); - - assert_eq!(transaction_count, U256::from(1)); - } - - #[tokio::test] - async fn get_transaction_count_future_block() { - let alchemy_url = get_alchemy_url(); - - let missing_address = Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse address"); - - let error = TestRpcClient::new(&alchemy_url) - .get_transaction_count(&missing_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) - .await - .expect_err("should have failed"); - - if let RpcClientError::JsonRpcError { error, .. } = error { - assert_eq!(error.code, -32602); - assert_eq!(error.message, "Unknown block number"); - assert!(error.data.is_none()); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn get_transaction_receipt_some() { - let alchemy_url = get_alchemy_url(); - - let hash = B256::from_str( - "0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a", - ) - .expect("failed to parse hash from string"); - - let receipt = TestRpcClient::new(&alchemy_url) - .get_transaction_receipt(&hash) - .await - .expect("failed to get transaction by hash"); - - assert!(receipt.is_some()); - let receipt = receipt.unwrap(); - - assert_eq!( - receipt.block_hash, - B256::from_str( - "0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8" - ) - .expect("couldn't parse data") - ); - assert_eq!(receipt.block_number, 0xa74fde); - assert_eq!(receipt.contract_address, None); - assert_eq!(receipt.cumulative_gas_used(), 0x56c81b); - assert_eq!( - receipt.effective_gas_price, - Some(U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data")) - ); - assert_eq!( - receipt.from, - Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") - .expect("couldn't parse data") - ); - assert_eq!( - receipt.gas_used, - u64::from_str_radix("a0f9", 16).expect("couldn't parse data") - ); - assert_eq!(receipt.logs().len(), 1); - assert_eq!(receipt.state_root(), None); - assert_eq!(receipt.status_code(), Some(1)); - assert_eq!( - receipt.to, - Some( - Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") - .expect("couldn't parse data") - ) - ); - assert_eq!(receipt.transaction_hash, hash); - assert_eq!(receipt.transaction_index, 136); - assert_eq!(receipt.transaction_type(), 0); - } - - #[tokio::test] - async fn get_storage_at_some() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let total_supply = TestRpcClient::new(&alchemy_url) - .get_storage_at( - &dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - - assert_eq!( - total_supply, - Some( - U256::from_str_radix( - "000000000000000000000000000000000000000010a596ae049e066d4991945c", - 16 - ) - .expect("failed to parse storage location") - ) - ); - } - - #[tokio::test] - async fn get_storage_at_latest() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let _total_supply = TestRpcClient::new(&alchemy_url) - .get_storage_at( - &dai_address, - U256::from_str_radix( - "0000000000000000000000000000000000000000000000000000000000000001", - 16, - ) - .expect("failed to parse storage location"), - Some(BlockSpec::latest()), - ) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_storage_at_future_block() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let storage_slot = TestRpcClient::new(&alchemy_url) - .get_storage_at( - &dai_address, - U256::from(1), - Some(BlockSpec::Number(MAX_BLOCK_NUMBER)), - ) - .await - .expect("should have succeeded"); - - assert!(storage_slot.is_none()); - } - - #[tokio::test] - async fn network_id_success() { - let alchemy_url = get_alchemy_url(); - - let version = TestRpcClient::new(&alchemy_url) - .network_id() - .await - .expect("should have succeeded"); - - assert_eq!(version, 1); - } - - #[tokio::test] - async fn stores_result_in_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let total_supply = client - .get_storage_at( - &dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - - let cached_files = client.files_in_cache(); - assert_eq!(cached_files.len(), 1); - - let mut file = File::open(&cached_files[0]).expect("failed to open file"); - let cached_result: Option = - serde_json::from_reader(&mut file).expect("failed to parse"); - - assert_eq!(total_supply, cached_result); - } - - #[tokio::test] - async fn concurrent_writes_to_cache_smoke_test() { - let client = TestRpcClient::new(&get_alchemy_url()); - - let test_contents = "some random test data 42"; - let cache_key = "cache-key"; - - assert_eq!(client.files_in_cache().len(), 0); - - join_all((0..100).map(|_| client.write_response_to_cache(cache_key, test_contents))) - .await; - - assert_eq!(client.files_in_cache().len(), 1); - - let contents = tokio::fs::read_to_string(&client.files_in_cache()[0]) - .await - .unwrap(); - assert_eq!(contents, serde_json::to_string(test_contents).unwrap()); - } - - #[tokio::test] - async fn handles_invalid_type_in_cache_single_call() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - client - .get_storage_at( - &dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - - // Write some valid JSON, but invalid U256 - tokio::fs::write(&client.files_in_cache()[0], "\"not-hex\"") - .await - .unwrap(); - - client - .get_storage_at( - &dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - } - } -} +mod tests {} diff --git a/crates/edr_rpc_client/src/lib.rs b/crates/edr_rpc_client/src/lib.rs index caac37328..f01dd7f35 100644 --- a/crates/edr_rpc_client/src/lib.rs +++ b/crates/edr_rpc_client/src/lib.rs @@ -3,17 +3,13 @@ //! Ethereum JSON-RPC client /// Types for caching JSON-RPC responses -mod cache; +pub mod cache; mod client; /// Types specific to JSON-RPC pub mod jsonrpc; mod reqwest_error; pub use self::{ - cache::{ - key::{ReadCacheKey, WriteCacheKey}, - CacheKeyHasher, CacheableMethod, - }, client::{RpcClient, RpcClientError}, reqwest_error::{MiddlewareError, ReqwestError}, }; diff --git a/crates/edr_rpc_eth/Cargo.toml b/crates/edr_rpc_eth/Cargo.toml index 006edfccb..74962e0f8 100644 --- a/crates/edr_rpc_eth/Cargo.toml +++ b/crates/edr_rpc_eth/Cargo.toml @@ -17,3 +17,4 @@ serde_json = { version = "1.0.89" } [features] test-remote = [] +tracing = ["edr_rpc_client/tracing"] diff --git a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs index 9fa7810a6..5aef41516 100644 --- a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs +++ b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs @@ -1,28 +1,16 @@ -use edr_eth::{ - block::{is_safe_block_number, IsSafeBlockNumberArgs}, - reward_percentile::RewardPercentile, - Address, B256, U256, -}; -use edr_rpc_client::{CacheKeyHasher, CacheableMethod}; -use sha3::{digest::FixedOutput, Digest, Sha3_256}; - -use crate::{ - filter::{LogFilterOptions, OneOrMore}, - request_methods::RequestMethod, - BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec, +use edr_eth::{reward_percentile::RewardPercentile, Address, B256, U256}; +use edr_rpc_client::cache::{ + self, + block_spec::{ + BlockSpecNotCacheableError, BlockTagNotCacheableError, CacheableBlockSpec, + PreEip1898BlockSpecNotCacheableError, + }, + filter::{CacheableLogFilterOptions, LogFilterOptionsNotCacheableError}, + key::{CacheKeyVariant, ReadCacheKey, WriteCacheKey}, + CacheableMethod, }; -pub(super) fn try_read_cache_key(method: &RequestMethod) -> Option { - CacheableRequestMethod::try_from(method) - .ok() - .and_then(CacheableRequestMethod::read_cache_key) -} - -pub(super) fn try_write_cache_key(method: &RequestMethod) -> Option { - CacheableRequestMethod::try_from(method) - .ok() - .and_then(CacheableRequestMethod::write_cache_key) -} +use crate::request_methods::RequestMethod; /// Potentially cacheable Ethereum JSON-RPC methods. #[derive(Clone, Debug)] @@ -83,11 +71,11 @@ enum CacheableRequestMethod<'a> { impl<'a> CacheableRequestMethod<'a> { // Allow to keep same structure as other RequestMethod and other methods. #[allow(clippy::match_same_arms)] - fn key_hasher(self) -> Result { - let hasher = CacheKeyHasher::new(); - let hasher = hasher.hash_u8(method.cache_key_variant()); + fn key_hasher(self) -> Result { + let hasher = cache::KeyHasher::new(); + let hasher = hasher.hash_u8(self.cache_key_variant()); - let hasher = match method { + let hasher = match self { CacheableRequestMethod::FeeHistory { block_count, newest_block, @@ -95,7 +83,7 @@ impl<'a> CacheableRequestMethod<'a> { } => { let hasher = hasher .hash_u256(block_count) - .hash_block_spec(newest_block)? + .hash_block_spec(&newest_block)? .hash_u8(reward_percentiles.cache_key_variant()); match reward_percentiles { Some(reward_percentiles) => hasher.hash_reward_percentiles(reward_percentiles), @@ -105,12 +93,12 @@ impl<'a> CacheableRequestMethod<'a> { CacheableRequestMethod::GetBalance { address, block_spec, - } => hasher.hash_address(address).hash_block_spec(block_spec)?, + } => hasher.hash_address(address).hash_block_spec(&block_spec)?, CacheableRequestMethod::GetBlockByNumber { block_spec, include_tx_data, } => hasher - .hash_block_spec(block_spec)? + .hash_block_spec(&block_spec)? .hash_bool(include_tx_data), CacheableRequestMethod::GetBlockByHash { block_hash, @@ -119,8 +107,10 @@ impl<'a> CacheableRequestMethod<'a> { CacheableRequestMethod::GetCode { address, block_spec, - } => hasher.hash_address(address).hash_block_spec(block_spec)?, - CacheableRequestMethod::GetLogs { params } => hasher.hash_log_filter_options(params)?, + } => hasher.hash_address(address).hash_block_spec(&block_spec)?, + CacheableRequestMethod::GetLogs { params } => { + hasher.hash_log_filter_options(¶ms)? + } CacheableRequestMethod::GetStorageAt { address, position, @@ -128,14 +118,14 @@ impl<'a> CacheableRequestMethod<'a> { } => hasher .hash_address(address) .hash_u256(position) - .hash_block_spec(block_spec)?, + .hash_block_spec(&block_spec)?, CacheableRequestMethod::GetTransactionByHash { transaction_hash } => { hasher.hash_b256(transaction_hash) } CacheableRequestMethod::GetTransactionCount { address, block_spec, - } => hasher.hash_address(address).hash_block_spec(block_spec)?, + } => hasher.hash_address(address).hash_block_spec(&block_spec)?, CacheableRequestMethod::GetTransactionReceipt { transaction_hash } => { hasher.hash_b256(transaction_hash) } @@ -230,24 +220,25 @@ impl<'a> TryFrom<&'a RequestMethod> for CacheableRequestMethod<'a> { /// Method invocations where, if the block spec argument is symbolic, it can be /// resolved to a block number from the response. #[derive(Debug, Clone)] -enum MethodWithResolvableSymbolicBlockSpec { +enum MethodWithResolvableBlockSpec { GetBlockByNumber { include_tx_data: bool }, } -impl MethodWithResolvableSymbolicBlockSpec { - fn new(method: CacheableRequestMethod<'_>) -> Option { - match method { +impl<'method> Into> for CacheableRequestMethod<'method> { + fn into(self) -> Option { + match self { CacheableRequestMethod::GetBlockByNumber { include_tx_data, block_spec: _, - } => Some(Self::GetBlockByNumber { include_tx_data }), + } => Some(MethodWithResolvableBlockSpec::GetBlockByNumber { include_tx_data }), _ => None, } } } -impl<'a> CacheableMethod for RequestMethod<'a> { - type MethodWithResolvableBlockTag = MethodWithResolvableSymbolicBlockSpec; +impl CacheableMethod for RequestMethod { + type Cached<'method> = CacheableRequestMethod<'method>; + type MethodWithResolvableBlockTag = MethodWithResolvableBlockSpec; fn block_number_request() -> Self { Self::BlockNumber(()) @@ -257,30 +248,54 @@ impl<'a> CacheableMethod for RequestMethod<'a> { Self::ChainId(()) } - fn resolve_block_tag(method: Self::MethodWithResolvableBlockTag, block_number: u64) -> Self { - match self.method { - MethodWithResolvableSymbolicBlockSpec::GetBlockByNumber { + #[cfg(feature = "tracing")] + fn name(&self) -> &'static str { + match self { + Self::BlockNumber(_) => "eth_blockNumber", + Self::FeeHistory(_, _, _) => "eth_feeHistory", + Self::ChainId(_) => "eth_chainId", + Self::GetBalance(_, _) => "eth_getBalance", + Self::GetBlockByNumber(_, _) => "eth_getBlockByNumber", + Self::GetBlockByHash(_, _) => "eth_getBlockByHash", + Self::GetCode(_, _) => "eth_getCode", + Self::GetLogs(_) => "eth_getLogs", + Self::GetStorageAt(_, _, _) => "eth_getStorageAt", + Self::GetTransactionByHash(_) => "eth_getTransactionByHash", + Self::GetTransactionCount(_, _) => "eth_getTransactionCount", + Self::GetTransactionReceipt(_) => "eth_getTransactionReceipt", + Self::NetVersion(_) => "net_version", + } + } + + fn resolve_block_tag<'method>( + method: Self::MethodWithResolvableBlockTag, + block_number: u64, + ) -> Self::Cached<'method> { + let resolved_block_spec = CacheableBlockSpec::Number { block_number }; + + match method { + MethodWithResolvableBlockSpec::GetBlockByNumber { include_tx_data, .. } => CacheableRequestMethod::GetBlockByNumber { block_spec: resolved_block_spec, include_tx_data, }, - }; + } } - fn read_cache_key(self) -> Option { - let cacheable_method = CacheableRequestMethod::try_from(&self).ok()?; + fn read_cache_key(&self) -> Option { + let cacheable_method = CacheableRequestMethod::try_from(self).ok()?; - let cache_key = cacheable_method.key_hasher().ok()?.finalize(); - Some(ReadCacheKey(cache_key)) + let key_hasher = cacheable_method.key_hasher().ok()?; + Some(ReadCacheKey::finalize(key_hasher)) } #[allow(clippy::match_same_arms)] - fn write_cache_key(self) -> Option { - let cacheable_method = CacheableRequestMethod::try_from(&self).ok()?; + fn write_cache_key(&self) -> Option> { + let cacheable_method = CacheableRequestMethod::try_from(self).ok()?; match cacheable_method.key_hasher() { - Err(SymbolicBlogTagError) => WriteCacheKey::needs_block_number(self), + Err(SymbolicBlogTagError) => WriteCacheKey::needs_block_tag_resolution(self), Ok(hasher) => match self { CacheableRequestMethod::FeeHistory { block_count: _, @@ -327,34 +342,7 @@ impl<'a> CacheableMethod for RequestMethod<'a> { } } -/// Error type for [`CacheableBlockSpec::try_from`]. -#[derive(thiserror::Error, Debug)] -#[error("Block spec is not cacheable: {0:?}")] -struct PreEip1898BlockSpecNotCacheableError(PreEip1898BlockSpec); - -impl<'a> TryFrom<&'a PreEip1898BlockSpec> for CacheableBlockSpec<'a> { - type Error = PreEip1898BlockSpecNotCacheableError; - - fn try_from(value: &'a PreEip1898BlockSpec) -> Result { - match value { - PreEip1898BlockSpec::Number(block_number) => Ok(CacheableBlockSpec::Number { - block_number: *block_number, - }), - PreEip1898BlockSpec::Tag(tag) => match tag { - // Latest and pending can never be resolved to a safe block number. - BlockTag::Latest | BlockTag::Pending => { - Err(PreEip1898BlockSpecNotCacheableError(value.clone())) - } - // Earliest, safe and finalized are potentially resolvable to a safe block number. - BlockTag::Earliest => Ok(CacheableBlockSpec::Earliest), - BlockTag::Safe => Ok(CacheableBlockSpec::Safe), - BlockTag::Finalized => Ok(CacheableBlockSpec::Finalized), - }, - } - } -} - -impl<'a> CacheKeyVariant for &'a CacheableRequestMethod<'a> { +impl<'a> CacheKeyVariant for CacheableRequestMethod<'a> { fn cache_key_variant(&self) -> u8 { match self { // The commented out methods have been removed as they're not currently in use by the @@ -379,78 +367,16 @@ impl<'a> CacheKeyVariant for &'a CacheableRequestMethod<'a> { } } -// // Allow to keep same structure as other RequestMethod and other methods. -// #[allow(clippy::match_same_arms)] -// fn hash_method( -// self, -// method: &CacheableRequestMethod<'_>, -// ) -> Result { -// let this = self.hash_u8(method.cache_key_variant()); - -// let this = match method { -// CacheableRequestMethod::FeeHistory { -// block_count, -// newest_block, -// reward_percentiles, -// } => { -// let this = this -// .hash_u256(block_count) -// .hash_block_spec(newest_block)? -// .hash_u8(reward_percentiles.cache_key_variant()); -// match reward_percentiles { -// Some(reward_percentiles) => -// this.hash_reward_percentiles(reward_percentiles), None => -// this, } -// } -// CacheableRequestMethod::GetBalance { -// address, -// block_spec, -// } => this.hash_address(address).hash_block_spec(block_spec)?, -// CacheableRequestMethod::GetBlockByNumber { -// block_spec, -// include_tx_data, -// } => this.hash_block_spec(block_spec)?.hash_bool(include_tx_data), -// CacheableRequestMethod::GetBlockByHash { -// block_hash, -// include_tx_data, -// } => this.hash_b256(block_hash).hash_bool(include_tx_data), -// CacheableRequestMethod::GetCode { -// address, -// block_spec, -// } => this.hash_address(address).hash_block_spec(block_spec)?, -// CacheableRequestMethod::GetLogs { params } => -// this.hash_log_filter_options(params)?, -// CacheableRequestMethod::GetStorageAt { -// address, -// position, -// block_spec, -// } => this -// .hash_address(address) -// .hash_u256(position) -// .hash_block_spec(block_spec)?, -// CacheableRequestMethod::GetTransactionByHash { transaction_hash } => -// { this.hash_b256(transaction_hash) -// } -// CacheableRequestMethod::GetTransactionCount { -// address, -// block_spec, -// } => this.hash_address(address).hash_block_spec(block_spec)?, -// CacheableRequestMethod::GetTransactionReceipt { transaction_hash } => -// { this.hash_b256(transaction_hash) -// } -// CacheableRequestMethod::NetVersion => this, -// }; - -// Ok(this) -// } - #[cfg(test)] mod test { + use edr_eth::{BlockSpec, Eip1898BlockSpec}; + use edr_rpc_client::cache::filter::CacheableLogFilterRange; + use super::*; #[test] fn test_hash_length() { - let hash = Hasher::new().hash_u8(0).finalize(); + let hash = cache::KeyHasher::new().hash_u8(0).finalize(); // 32 bytes as hex assert_eq!(hash.len(), 2 * 32); } @@ -460,14 +386,14 @@ mod test { let block_number = u64::default(); let block_hash = B256::default(); - let hash_one = Hasher::new() + let hash_one = cache::KeyHasher::new() .hash_block_spec(&CacheableBlockSpec::Hash { block_hash: &block_hash, require_canonical: None, }) .unwrap() .finalize(); - let hash_two = Hasher::new() + let hash_two = cache::KeyHasher::new() .hash_block_spec(&CacheableBlockSpec::Number { block_number }) .unwrap() .finalize(); @@ -481,25 +407,25 @@ mod test { let to = CacheableBlockSpec::Number { block_number: 2 }; let address = Address::default(); - let hash_one = Hasher::new() + let hash_one = cache::KeyHasher::new() .hash_log_filter_options(&CacheableLogFilterOptions { range: CacheableLogFilterRange::Range { from_block: from.clone(), to_block: to.clone(), }, - address: vec![&address], + addresses: vec![&address], topics: Vec::new(), }) .unwrap() .finalize(); - let hash_two = Hasher::new() + let hash_two = cache::KeyHasher::new() .hash_log_filter_options(&CacheableLogFilterOptions { range: CacheableLogFilterRange::Range { from_block: to, to_block: from, }, - address: vec![&address], + addresses: vec![&address], topics: Vec::new(), }) .unwrap() @@ -511,16 +437,12 @@ mod test { #[test] fn test_same_arguments_keys_not_equal() { let value = B256::default(); - let key_one = CacheableRequestMethod::GetTransactionByHash { - transaction_hash: &value, - } - .read_cache_key() - .unwrap(); - let key_two = CacheableRequestMethod::GetTransactionReceipt { - transaction_hash: &value, - } - .read_cache_key() - .unwrap(); + let key_one = RequestMethod::GetTransactionByHash(value) + .read_cache_key() + .unwrap(); + let key_two = RequestMethod::GetTransactionReceipt(value) + .read_cache_key() + .unwrap(); assert_ne!(key_one, key_two); } @@ -530,26 +452,21 @@ mod test { let address = Address::default(); let position = U256::default(); - let key_one = CacheableRequestMethod::GetStorageAt { - address: &address, - position: &position, - block_spec: CacheableBlockSpec::Hash { - block_hash: &B256::default(), + let key_one = RequestMethod::GetStorageAt( + address, + position, + Some(BlockSpec::Eip1898(Eip1898BlockSpec::Hash { + block_hash: B256::default(), require_canonical: None, - }, - } + })), + ) .read_cache_key() .unwrap(); - let key_two = CacheableRequestMethod::GetStorageAt { - address: &address, - position: &position, - block_spec: CacheableBlockSpec::Number { - block_number: u64::default(), - }, - } - .read_cache_key() - .unwrap(); + let key_two = + RequestMethod::GetStorageAt(address, position, Some(BlockSpec::Number(u64::default()))) + .read_cache_key() + .unwrap(); assert_ne!(key_one, key_two); } @@ -559,23 +476,15 @@ mod test { let address = Address::default(); let position = U256::default(); let block_number = u64::default(); - let block_spec = CacheableBlockSpec::Number { block_number }; + let block_spec = Some(BlockSpec::Number(block_number)); - let key_one = CacheableRequestMethod::GetStorageAt { - address: &address, - position: &position, - block_spec: block_spec.clone(), - } - .read_cache_key() - .unwrap(); + let key_one = RequestMethod::GetStorageAt(address, position, block_spec.clone()) + .read_cache_key() + .unwrap(); - let key_two = CacheableRequestMethod::GetStorageAt { - address: &address, - position: &position, - block_spec, - } - .read_cache_key() - .unwrap(); + let key_two = RequestMethod::GetStorageAt(address, position, block_spec) + .read_cache_key() + .unwrap(); assert_eq!(key_one, key_two); } diff --git a/crates/edr_rpc_eth/src/client.rs b/crates/edr_rpc_eth/src/client.rs index 0cadc3ebc..601f0a298 100644 --- a/crates/edr_rpc_eth/src/client.rs +++ b/crates/edr_rpc_eth/src/client.rs @@ -1,9 +1,19 @@ +use std::fmt::Debug; + use async_trait::async_trait; -use edr_rpc_client::RpcClient; +use edr_eth::{ + filter::OneOrMore, log::FilterLog, receipt::BlockReceipt, reward_percentile::RewardPercentile, + AccountInfo, Address, BlockSpec, Bytes, PreEip1898BlockSpec, B256, U256, +}; +use edr_rpc_client::{RpcClient, RpcClientError}; + +use crate::{fork::ForkMetadata, request_methods::RequestMethod, Transaction}; -use crate::{request_methods::RequestMethod, Transaction}; +// Constrain parallel requests to avoid rate limiting on transport level and +// thundering herd during backoff. +const MAX_PARALLEL_REQUESTS: usize = 20; -#[async_traitt] +#[async_trait] pub trait EthClientExt { /// Calls `eth_feeHistory` and returns the fee history. async fn fee_history( @@ -124,7 +134,7 @@ impl EthClientExt for RpcClient { newest_block: BlockSpec, reward_percentiles: Option>, ) -> Result { - self.call(MethodT::FeeHistory( + self.call(RequestMethod::FeeHistory( U256::from(block_count), newest_block, reward_percentiles, diff --git a/crates/edr_rpc_eth/src/fork.rs b/crates/edr_rpc_eth/src/fork.rs new file mode 100644 index 000000000..e142af90f --- /dev/null +++ b/crates/edr_rpc_eth/src/fork.rs @@ -0,0 +1,10 @@ +/// Metadata about a forked chain. +#[derive(Clone, Debug)] +pub struct ForkMetadata { + /// Chain id as returned by `eth_chainId` + pub chain_id: u64, + /// Network id as returned by `net_version` + pub network_id: u64, + /// The latest block number as returned by `eth_blockNumber` + pub latest_block_number: u64, +} diff --git a/crates/edr_rpc_eth/src/lib.rs b/crates/edr_rpc_eth/src/lib.rs index e42c2aef8..8a02da4bf 100644 --- a/crates/edr_rpc_eth/src/lib.rs +++ b/crates/edr_rpc_eth/src/lib.rs @@ -4,6 +4,8 @@ mod call_request; mod client; /// ethereum objects as specifically used in the JSON-RPC interface pub mod eth; +/// Types related to forking a remote blockchain. +pub mod fork; mod r#override; mod request_methods; mod transaction; diff --git a/crates/edr_rpc_eth/src/request_methods.rs b/crates/edr_rpc_eth/src/request_methods.rs index d455e7421..39f31ba04 100644 --- a/crates/edr_rpc_eth/src/request_methods.rs +++ b/crates/edr_rpc_eth/src/request_methods.rs @@ -1,6 +1,7 @@ -use edr_eth::{reward_percentile::RewardPercentile, Address, B256, U256}; - -use crate::{filter::LogFilterOptions, BlockSpec, PreEip1898BlockSpec}; +use edr_eth::{ + filter::LogFilterOptions, reward_percentile::RewardPercentile, Address, BlockSpec, + PreEip1898BlockSpec, B256, U256, +}; /// Methods for requests to a remote Ethereum node. Only contains methods /// supported by the [`crate::remote::client::RpcClient`]. @@ -97,24 +98,3 @@ pub enum RequestMethod { #[serde(rename = "net_version", with = "edr_eth::serde::empty_params")] NetVersion(()), } - -impl RequestMethod { - #[cfg(feature = "tracing")] - pub fn name(&self) -> &'static str { - match self { - Self::BlockNumber(_) => "eth_blockNumber", - Self::FeeHistory(_, _, _) => "eth_feeHistory", - Self::ChainId(_) => "eth_chainId", - Self::GetBalance(_, _) => "eth_getBalance", - Self::GetBlockByNumber(_, _) => "eth_getBlockByNumber", - Self::GetBlockByHash(_, _) => "eth_getBlockByHash", - Self::GetCode(_, _) => "eth_getCode", - Self::GetLogs(_) => "eth_getLogs", - Self::GetStorageAt(_, _, _) => "eth_getStorageAt", - Self::GetTransactionByHash(_) => "eth_getTransactionByHash", - Self::GetTransactionCount(_, _) => "eth_getTransactionCount", - Self::GetTransactionReceipt(_) => "eth_getTransactionReceipt", - Self::NetVersion(_) => "net_version", - } - } -} From 3765757eb61435c6751fc87acd3c6dce17777fac Mon Sep 17 00:00:00 2001 From: Wodann Date: Wed, 29 May 2024 21:35:10 +0000 Subject: [PATCH 04/18] WIP: generic trait EthClientExt --- Cargo.lock | 8 +- crates/edr_eth/src/fee_history.rs | 35 +++ crates/edr_eth/src/lib.rs | 4 +- crates/edr_rpc_client/Cargo.toml | 4 - crates/edr_rpc_client/src/cache.rs | 26 +- crates/edr_rpc_client/src/cache/block_spec.rs | 4 +- crates/edr_rpc_client/src/cache/hasher.rs | 10 +- crates/edr_rpc_client/src/cache/key.rs | 12 +- crates/edr_rpc_client/src/client.rs | 11 +- crates/edr_rpc_eth/Cargo.toml | 8 +- crates/edr_rpc_eth/src/{eth.rs => block.rs} | 76 ++---- .../src/cacheable_method_invocation.rs | 236 +++++++++--------- crates/edr_rpc_eth/src/chain_spec.rs | 25 ++ crates/edr_rpc_eth/src/client.rs | 142 ++++++----- crates/edr_rpc_eth/src/lib.rs | 13 +- crates/edr_rpc_eth/tests/client.rs | 15 +- 16 files changed, 350 insertions(+), 279 deletions(-) create mode 100644 crates/edr_eth/src/fee_history.rs rename crates/edr_rpc_eth/src/{eth.rs => block.rs} (51%) create mode 100644 crates/edr_rpc_eth/src/chain_spec.rs diff --git a/Cargo.lock b/Cargo.lock index 71b160c8b..097f14dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1181,7 +1181,6 @@ dependencies = [ "hyper", "lazy_static", "log", - "mockito", "regex", "reqwest", "reqwest-middleware", @@ -1190,7 +1189,6 @@ dependencies = [ "serde", "serde_json", "sha3", - "tempfile", "thiserror", "tokio", "tracing", @@ -1206,10 +1204,16 @@ dependencies = [ "async-trait", "edr_eth", "edr_rpc_client", + "futures", "hex", + "mockito", + "reqwest", "serde", "serde_json", + "tempfile", "thiserror", + "tokio", + "tracing", ] [[package]] diff --git a/crates/edr_eth/src/fee_history.rs b/crates/edr_eth/src/fee_history.rs new file mode 100644 index 000000000..dd23b871e --- /dev/null +++ b/crates/edr_eth/src/fee_history.rs @@ -0,0 +1,35 @@ +use crate::U256; + +/// Fee history for the returned block range. This can be a subsection of the +/// requested range if not all blocks are available. +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FeeHistoryResult { + /// Lowest number block of returned range. + #[serde(with = "crate::serde::u64")] + pub oldest_block: u64, + /// An array of block base fees per gas. This includes the next block after + /// the newest of the returned range, because this value can be derived from + /// the newest block. Zeroes are returned for pre-EIP-1559 blocks. + pub base_fee_per_gas: Vec, + /// An array of block gas used ratios. These are calculated as the ratio of + /// gas used and gas limit. + pub gas_used_ratio: Vec, + /// A two-dimensional array of effective priority fees per gas at the + /// requested block percentiles. + #[serde(skip_serializing_if = "Option::is_none")] + pub reward: Option>>, +} + +impl FeeHistoryResult { + /// Constructs a new `FeeHistoryResult` with the oldest block and otherwise + /// default fields. + pub fn new(oldest_block: u64) -> Self { + Self { + oldest_block, + base_fee_per_gas: Vec::default(), + gas_used_ratio: Vec::default(), + reward: Option::default(), + } + } +} diff --git a/crates/edr_eth/src/lib.rs b/crates/edr_eth/src/lib.rs index 9249105a8..0a98992dd 100644 --- a/crates/edr_eth/src/lib.rs +++ b/crates/edr_eth/src/lib.rs @@ -15,6 +15,8 @@ pub mod beacon; pub mod block; /// Ethereum block spec mod block_spec; +/// Ethereum fee history types +pub mod fee_history; /// Ethereum types for filter-based RPC methods pub mod filter; /// Ethereum log types @@ -43,7 +45,7 @@ pub mod withdrawal; pub use revm_primitives::{ alloy_primitives::{Bloom, BloomInput, B512, B64, U64}, - hex_literal, AccountInfo, Address, Bytes, HashMap, HashSet, SpecId, B256, U256, + hex_literal, AccountInfo, Address, Bytecode, Bytes, HashMap, HashSet, SpecId, B256, U256, }; pub use self::block_spec::{BlockSpec, BlockTag, Eip1898BlockSpec, PreEip1898BlockSpec}; diff --git a/crates/edr_rpc_client/Cargo.toml b/crates/edr_rpc_client/Cargo.toml index 9263894fe..aa375fc0d 100644 --- a/crates/edr_rpc_client/Cargo.toml +++ b/crates/edr_rpc_client/Cargo.toml @@ -25,10 +25,6 @@ tracing = { version = "0.1.37", default-features = false, features = ["attribute url = { version = "2.4.1", default-features = false } uuid = { version = "1.4.1", default-features = false, features = ["std", "v4"] } -[dev-dependencies] -mockito = { version = "1.0.2", default-features = false } -tempfile = { version = "3.7.1", default-features = false } - [features] test-remote = [] tracing = ["dep:tracing", "dep:reqwest-tracing"] diff --git a/crates/edr_rpc_client/src/cache.rs b/crates/edr_rpc_client/src/cache.rs index 7bc344cc8..1051ed1b9 100644 --- a/crates/edr_rpc_client/src/cache.rs +++ b/crates/edr_rpc_client/src/cache.rs @@ -20,14 +20,12 @@ pub use self::hasher::KeyHasher; use self::key::{ReadCacheKey, WriteCacheKey}; use crate::RpcClientError; -/// Trait for types that can be cached. +/// Trait for RPC method types that can be cached. pub trait CacheableMethod: Sized { /// The type representing the cached method. - type Cached<'method>: Into>; - - /// The type representing a subset of methods containing a [`BlockTag`] - /// which can be resolved to a block number. - type MethodWithResolvableBlockTag: Clone + Debug; + type Cached<'method>: CachedMethod + TryFrom<&'method Self> + where + Self: 'method; /// Creates a method for requesting the block number. fn block_number_request() -> Self; @@ -38,20 +36,24 @@ pub trait CacheableMethod: Sized { #[cfg(feature = "tracing")] /// Returns the name of the method. fn name(&self) -> &'static str; +} + +/// Trait for RPC method types that will be cached to disk. +pub trait CachedMethod: Into> { + /// The type representing a subset of methods containing a [`BlockTag`] + /// which can be resolved to a block number. + type MethodWithResolvableBlockTag: Clone + Debug; /// Resolves a block tag to a block number for the provided method. - fn resolve_block_tag<'method>( - method: Self::MethodWithResolvableBlockTag, - block_number: u64, - ) -> Self::Cached<'method> where; + fn resolve_block_tag(method: Self::MethodWithResolvableBlockTag, block_number: u64) -> Self; /// Returns the instance's [`ReadCacheKey`] if it can be read from the /// cache. - fn read_cache_key(&self) -> Option; + fn read_cache_key(self) -> Option; /// Returns the instance's [`WriteCacheKey`] if it can be written to the /// cache. - fn write_cache_key(&self) -> Option>; + fn write_cache_key(self) -> Option>; } #[derive(Debug, Clone)] diff --git a/crates/edr_rpc_client/src/cache/block_spec.rs b/crates/edr_rpc_client/src/cache/block_spec.rs index b50b55d93..c2a59b2b5 100644 --- a/crates/edr_rpc_client/src/cache/block_spec.rs +++ b/crates/edr_rpc_client/src/cache/block_spec.rs @@ -116,5 +116,5 @@ impl<'a> TryFrom<&'a PreEip1898BlockSpec> for CacheableBlockSpec<'a> { /// Error type for [`Hasher::hash_block_spec`]. #[derive(thiserror::Error, Debug)] -#[error("A block tag is not cacheable.")] -pub struct BlockTagNotCacheableError; +#[error("A block tag is not resolved.")] +pub struct UnresolvedBlockTagError; diff --git a/crates/edr_rpc_client/src/cache/hasher.rs b/crates/edr_rpc_client/src/cache/hasher.rs index 46b9c825f..f086fe60c 100644 --- a/crates/edr_rpc_client/src/cache/hasher.rs +++ b/crates/edr_rpc_client/src/cache/hasher.rs @@ -2,7 +2,7 @@ use edr_eth::{reward_percentile::RewardPercentile, Address, B256, U256}; use sha3::{digest::FixedOutput, Digest, Sha3_256}; use super::{ - block_spec::{BlockTagNotCacheableError, CacheableBlockSpec}, + block_spec::{CacheableBlockSpec, UnresolvedBlockTagError}, filter::{CacheableLogFilterOptions, CacheableLogFilterRange}, key::CacheKeyVariant, }; @@ -73,7 +73,7 @@ impl KeyHasher { pub fn hash_block_spec( self, block_spec: &CacheableBlockSpec<'_>, - ) -> Result { + ) -> Result { let this = self.hash_u8(block_spec.cache_key_variant()); match block_spec { @@ -92,14 +92,14 @@ impl KeyHasher { } CacheableBlockSpec::Earliest | CacheableBlockSpec::Safe - | CacheableBlockSpec::Finalized => Err(BlockTagNotCacheableError), + | CacheableBlockSpec::Finalized => Err(UnresolvedBlockTagError), } } pub fn hash_log_filter_options( self, params: &CacheableLogFilterOptions<'_>, - ) -> Result { + ) -> Result { // Destructuring to make sure we get a compiler error here if the fields change. let CacheableLogFilterOptions { range, @@ -132,7 +132,7 @@ impl KeyHasher { pub fn hash_log_filter_range( self, params: &CacheableLogFilterRange<'_>, - ) -> Result { + ) -> Result { let this = self.hash_u8(params.cache_key_variant()); match params { diff --git a/crates/edr_rpc_client/src/cache/key.rs b/crates/edr_rpc_client/src/cache/key.rs index 678457a3a..cb8fe3a5d 100644 --- a/crates/edr_rpc_client/src/cache/key.rs +++ b/crates/edr_rpc_client/src/cache/key.rs @@ -2,7 +2,7 @@ use edr_eth::block::{is_safe_block_number, IsSafeBlockNumberArgs}; use super::{ block_spec::CacheableBlockSpec, filter::CacheableLogFilterRange, hasher::KeyHasher, - CacheableMethod, + CachedMethod, }; /// Trait for retrieving the unique id of an enum variant. @@ -48,7 +48,7 @@ impl AsRef for ReadCacheKey { /// A cache key that can be used to write to the cache. #[derive(Clone, Debug)] -pub enum WriteCacheKey { +pub enum WriteCacheKey { /// It needs to be checked whether the block number is safe (reorg-free) /// before writing to the cache. NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber), @@ -60,7 +60,7 @@ pub enum WriteCacheKey { Resolved(String), } -impl WriteCacheKey { +impl WriteCacheKey { /// Finalizes the provided [`KeyHasher`] and return the resolved cache /// key. pub fn finalize(hasher: KeyHasher) -> Self { @@ -104,7 +104,7 @@ impl WriteCacheKey { /// Checks whether a block tag needs to be resolved before returning a cache /// key. - pub fn needs_block_tag_resolution(method: MethodT::Cached<'_>) -> Option { + pub fn needs_block_tag_resolution(method: MethodT) -> Option { let method = method.into()?; Some(Self::NeedsBlockTagResolution( @@ -151,11 +151,11 @@ pub(crate) enum ResolvedSymbolicTag { /// A cache key for which the block tag needs to be resolved before writing to /// the cache. #[derive(Clone, Debug)] -pub struct CacheKeyForUnresolvedBlockTag { +pub struct CacheKeyForUnresolvedBlockTag { method: MethodT::MethodWithResolvableBlockTag, } -impl CacheKeyForUnresolvedBlockTag { +impl CacheKeyForUnresolvedBlockTag { /// Check whether the block number is safe to cache before returning a cache /// key. pub(crate) fn resolve_block_tag(self, block_number: u64) -> Option { diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index 920bf050b..d59e63a8e 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -31,7 +31,7 @@ use crate::{ CacheKeyForUncheckedBlockNumber, CacheKeyForUnresolvedBlockTag, ReadCacheKey, ResolvedSymbolicTag, WriteCacheKey, }, - remove_from_cache, CacheableMethod, CachedBlockNumber, + remove_from_cache, CacheableMethod, CachedBlockNumber, CachedMethod, }, jsonrpc, MiddlewareError, ReqwestError, }; @@ -321,7 +321,7 @@ impl RpcClient { async fn resolve_block_tag( &self, - block_tag_resolver: CacheKeyForUnresolvedBlockTag, + block_tag_resolver: CacheKeyForUnresolvedBlockTag>, result: ResultT, resolve_block_number: impl Fn(ResultT) -> Option, ) -> Result, RpcClientError> { @@ -344,7 +344,9 @@ impl RpcClient { result: ResultT, resolve_block_number: impl Fn(ResultT) -> Option, ) -> Result, RpcClientError> { - if let Some(cache_key) = method.write_cache_key() { + let cached_method = MethodT::Cached::try_from(method).ok(); + + if let Some(cache_key) = cached_method.and_then(CachedMethod::write_cache_key) { match cache_key { WriteCacheKey::NeedsSafetyCheck(safety_checker) => { self.validate_block_number(safety_checker).await @@ -508,7 +510,8 @@ impl RpcClient { method: MethodT, resolve_block_number: impl Fn(&SuccessT) -> Option, ) -> Result { - let read_cache_key = method.read_cache_key(); + let cached_method = MethodT::Cached::try_from(&method).ok(); + let read_cache_key = cached_method.and_then(CachedMethod::read_cache_key); let request = self.serialize_request(&method)?; diff --git a/crates/edr_rpc_eth/Cargo.toml b/crates/edr_rpc_eth/Cargo.toml index 74962e0f8..c21d5cf88 100644 --- a/crates/edr_rpc_eth/Cargo.toml +++ b/crates/edr_rpc_eth/Cargo.toml @@ -7,14 +7,20 @@ edition = "2021" async-trait = { version = "0.1.80", default-features = false } edr_eth = { version = "0.3.5", path = "../edr_eth", features = ["serde"] } edr_rpc_client = { version = "0.3.5", path = "../edr_rpc_client" } +futures = { version = "0.3.28", default-features = false, features = ["std"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } serde = { version = "1.0.147", default-features = false, features = ["derive", "std"] } thiserror = { version = "1.0.37", default-features = false } +tokio = { version = "1.21.2", default-features = false, features = ["macros"] } +tracing = { version = "0.1.37", default-features = false, features = ["attributes", "std"], optional = true } [dev-dependencies] anyhow = { version = "1.0.75", default-features = false, features = ["std"] } +mockito = { version = "1.0.2", default-features = false } +reqwest = { version = "0.11", default-features = false, features = ["blocking", "json"] } serde_json = { version = "1.0.89" } +tempfile = { version = "3.7.1", default-features = false } [features] test-remote = [] -tracing = ["edr_rpc_client/tracing"] +tracing = ["dep:tracing", "edr_rpc_client/tracing"] diff --git a/crates/edr_rpc_eth/src/eth.rs b/crates/edr_rpc_eth/src/block.rs similarity index 51% rename from crates/edr_rpc_eth/src/eth.rs rename to crates/edr_rpc_eth/src/block.rs index a48641e48..029bf3169 100644 --- a/crates/edr_rpc_eth/src/eth.rs +++ b/crates/edr_rpc_eth/src/block.rs @@ -1,30 +1,14 @@ -#![cfg(feature = "serde")] - -// Parts of this code were adapted from github.com/gakonst/ethers-rs and are -// distributed under its licenses: -// - https://github.com/gakonst/ethers-rs/blob/7e6c3ba98363bdf6131e8284f186cc2c70ff48c3/LICENSE-APACHE -// - https://github.com/gakonst/ethers-rs/blob/7e6c3ba98363bdf6131e8284f186cc2c70ff48c3/LICENSE-MIT -// For the original context, see https://github.com/gakonst/ethers-rs/tree/7e6c3ba98363bdf6131e8284f186cc2c70ff48c3 - use std::fmt::Debug; -use crate::{ - access_list::AccessListItem, withdrawal::Withdrawal, Address, Bloom, Bytes, B256, B64, U256, -}; +use edr_eth::{withdrawal::Withdrawal, Address, Bloom, Bytes, B256, B64, U256}; +use serde::{Deserialize, Serialize}; -/// Error that occurs when trying to convert the JSON-RPC `TransactionReceipt` -/// type. -#[derive(Debug, thiserror::Error)] -pub enum ReceiptConversionError { - /// The transaction type is not supported. - #[error("Unsupported type {0}")] - UnsupportedType(u64), -} +use crate::chain_spec::GetBlockNumber; /// block object returned by `eth_getBlockBy*` -#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct Block { +pub struct Block { /// Hash of the block pub hash: Option, /// hash of the parent block. @@ -38,20 +22,20 @@ pub struct Block { /// the root of the receipts trie of the block pub receipts_root: B256, /// the block number. None when its pending block. - #[serde(with = "crate::serde::optional_u64")] + #[serde(with = "edr_eth::serde::optional_u64")] pub number: Option, /// the total used gas by all transactions in this block - #[serde(with = "crate::serde::u64")] + #[serde(with = "edr_eth::serde::u64")] pub gas_used: u64, /// the maximum gas allowed in this block - #[serde(with = "crate::serde::u64")] + #[serde(with = "edr_eth::serde::u64")] pub gas_limit: u64, /// the "extra data" field of this block pub extra_data: Bytes, /// the bloom filter for the logs of the block pub logs_bloom: Bloom, /// the unix timestamp for when the block was collated - #[serde(with = "crate::serde::u64")] + #[serde(with = "edr_eth::serde::u64")] pub timestamp: u64, /// integer of the difficulty for this blocket pub difficulty: U256, @@ -63,9 +47,9 @@ pub struct Block { /// Array of transaction objects, or 32 Bytes transaction hashes depending /// on the last given parameter #[serde(default)] - pub transactions: Vec, + pub transactions: Vec, /// the length of the RLP encoding of this block in bytes - #[serde(with = "crate::serde::u64")] + #[serde(with = "edr_eth::serde::u64")] pub size: u64, /// Mix hash. None when it's a pending block. pub mix_hash: Option, @@ -87,7 +71,7 @@ pub struct Block { #[serde( default, skip_serializing_if = "Option::is_none", - with = "crate::serde::optional_u64" + with = "edr_eth::serde::optional_u64" )] pub blob_gas_used: Option, /// A running total of blob gas consumed in excess of the target, prior to @@ -95,7 +79,7 @@ pub struct Block { #[serde( default, skip_serializing_if = "Option::is_none", - with = "crate::serde::optional_u64" + with = "edr_eth::serde::optional_u64" )] pub excess_blob_gas: Option, /// Root of the parent beacon block @@ -103,36 +87,8 @@ pub struct Block { pub parent_beacon_block_root: Option, } -/// Fee history for the returned block range. This can be a subsection of the -/// requested range if not all blocks are available. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FeeHistoryResult { - /// Lowest number block of returned range. - #[serde(with = "crate::serde::u64")] - pub oldest_block: u64, - /// An array of block base fees per gas. This includes the next block after - /// the newest of the returned range, because this value can be derived from - /// the newest block. Zeroes are returned for pre-EIP-1559 blocks. - pub base_fee_per_gas: Vec, - /// An array of block gas used ratios. These are calculated as the ratio of - /// gas used and gas limit. - pub gas_used_ratio: Vec, - /// A two-dimensional array of effective priority fees per gas at the - /// requested block percentiles. - #[serde(skip_serializing_if = "Option::is_none")] - pub reward: Option>>, -} - -impl FeeHistoryResult { - /// Constructs a new `FeeHistoryResult` with the oldest block and otherwise - /// default fields. - pub fn new(oldest_block: u64) -> Self { - Self { - oldest_block, - base_fee_per_gas: Vec::default(), - gas_used_ratio: Vec::default(), - reward: Option::default(), - } +impl GetBlockNumber for Block { + fn number(&self) -> Option { + self.number } } diff --git a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs index 5aef41516..d25ef1c37 100644 --- a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs +++ b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs @@ -2,19 +2,19 @@ use edr_eth::{reward_percentile::RewardPercentile, Address, B256, U256}; use edr_rpc_client::cache::{ self, block_spec::{ - BlockSpecNotCacheableError, BlockTagNotCacheableError, CacheableBlockSpec, - PreEip1898BlockSpecNotCacheableError, + BlockSpecNotCacheableError, CacheableBlockSpec, PreEip1898BlockSpecNotCacheableError, + UnresolvedBlockTagError, }, filter::{CacheableLogFilterOptions, LogFilterOptionsNotCacheableError}, key::{CacheKeyVariant, ReadCacheKey, WriteCacheKey}, - CacheableMethod, + CacheableMethod, CachedMethod, }; use crate::request_methods::RequestMethod; /// Potentially cacheable Ethereum JSON-RPC methods. #[derive(Clone, Debug)] -enum CacheableRequestMethod<'a> { +pub enum CachedRequestMethod<'a> { /// `eth_feeHistory` FeeHistory { block_count: &'a U256, @@ -68,68 +68,66 @@ enum CacheableRequestMethod<'a> { NetVersion, } -impl<'a> CacheableRequestMethod<'a> { +impl<'a> CachedRequestMethod<'a> { // Allow to keep same structure as other RequestMethod and other methods. #[allow(clippy::match_same_arms)] - fn key_hasher(self) -> Result { + fn key_hasher(&self) -> Result { let hasher = cache::KeyHasher::new(); let hasher = hasher.hash_u8(self.cache_key_variant()); let hasher = match self { - CacheableRequestMethod::FeeHistory { + CachedRequestMethod::FeeHistory { block_count, newest_block, reward_percentiles, } => { let hasher = hasher .hash_u256(block_count) - .hash_block_spec(&newest_block)? + .hash_block_spec(newest_block)? .hash_u8(reward_percentiles.cache_key_variant()); match reward_percentiles { Some(reward_percentiles) => hasher.hash_reward_percentiles(reward_percentiles), None => hasher, } } - CacheableRequestMethod::GetBalance { + CachedRequestMethod::GetBalance { address, block_spec, - } => hasher.hash_address(address).hash_block_spec(&block_spec)?, - CacheableRequestMethod::GetBlockByNumber { + } => hasher.hash_address(address).hash_block_spec(block_spec)?, + CachedRequestMethod::GetBlockByNumber { block_spec, include_tx_data, } => hasher - .hash_block_spec(&block_spec)? - .hash_bool(include_tx_data), - CacheableRequestMethod::GetBlockByHash { + .hash_block_spec(block_spec)? + .hash_bool(*include_tx_data), + CachedRequestMethod::GetBlockByHash { block_hash, include_tx_data, - } => hasher.hash_b256(block_hash).hash_bool(include_tx_data), - CacheableRequestMethod::GetCode { + } => hasher.hash_b256(block_hash).hash_bool(*include_tx_data), + CachedRequestMethod::GetCode { address, block_spec, - } => hasher.hash_address(address).hash_block_spec(&block_spec)?, - CacheableRequestMethod::GetLogs { params } => { - hasher.hash_log_filter_options(¶ms)? - } - CacheableRequestMethod::GetStorageAt { + } => hasher.hash_address(address).hash_block_spec(block_spec)?, + CachedRequestMethod::GetLogs { params } => hasher.hash_log_filter_options(params)?, + CachedRequestMethod::GetStorageAt { address, position, block_spec, } => hasher .hash_address(address) .hash_u256(position) - .hash_block_spec(&block_spec)?, - CacheableRequestMethod::GetTransactionByHash { transaction_hash } => { + .hash_block_spec(block_spec)?, + CachedRequestMethod::GetTransactionByHash { transaction_hash } => { hasher.hash_b256(transaction_hash) } - CacheableRequestMethod::GetTransactionCount { + CachedRequestMethod::GetTransactionCount { address, block_spec, - } => hasher.hash_address(address).hash_block_spec(&block_spec)?, - CacheableRequestMethod::GetTransactionReceipt { transaction_hash } => { + } => hasher.hash_address(address).hash_block_spec(block_spec)?, + CachedRequestMethod::GetTransactionReceipt { transaction_hash } => { hasher.hash_b256(transaction_hash) } - CacheableRequestMethod::NetVersion => hasher, + CachedRequestMethod::NetVersion => hasher, }; Ok(hasher) @@ -138,7 +136,7 @@ impl<'a> CacheableRequestMethod<'a> { /// Error type for [`CacheableRequestMethod::try_from`]. #[derive(thiserror::Error, Debug)] -enum MethodNotCacheableError { +pub enum MethodNotCacheableError { #[error(transparent)] BlockSpec(#[from] BlockSpecNotCacheableError), #[error("Method is not cacheable: {0:?}")] @@ -149,63 +147,61 @@ enum MethodNotCacheableError { PreEip18989BlockSpec(#[from] PreEip1898BlockSpecNotCacheableError), } -impl<'a> TryFrom<&'a RequestMethod> for CacheableRequestMethod<'a> { +impl<'a> TryFrom<&'a RequestMethod> for CachedRequestMethod<'a> { type Error = MethodNotCacheableError; fn try_from(value: &'a RequestMethod) -> Result { match value { RequestMethod::FeeHistory(block_count, newest_block, reward_percentiles) => { - Ok(CacheableRequestMethod::FeeHistory { + Ok(CachedRequestMethod::FeeHistory { block_count, newest_block: newest_block.try_into()?, reward_percentiles, }) } - RequestMethod::GetBalance(address, block_spec) => { - Ok(CacheableRequestMethod::GetBalance { - address, - block_spec: block_spec.try_into()?, - }) - } + RequestMethod::GetBalance(address, block_spec) => Ok(CachedRequestMethod::GetBalance { + address, + block_spec: block_spec.try_into()?, + }), RequestMethod::GetBlockByNumber(block_spec, include_tx_data) => { - Ok(CacheableRequestMethod::GetBlockByNumber { + Ok(CachedRequestMethod::GetBlockByNumber { block_spec: block_spec.try_into()?, include_tx_data: *include_tx_data, }) } RequestMethod::GetBlockByHash(block_hash, include_tx_data) => { - Ok(CacheableRequestMethod::GetBlockByHash { + Ok(CachedRequestMethod::GetBlockByHash { block_hash, include_tx_data: *include_tx_data, }) } - RequestMethod::GetCode(address, block_spec) => Ok(CacheableRequestMethod::GetCode { + RequestMethod::GetCode(address, block_spec) => Ok(CachedRequestMethod::GetCode { address, block_spec: block_spec.try_into()?, }), - RequestMethod::GetLogs(params) => Ok(CacheableRequestMethod::GetLogs { + RequestMethod::GetLogs(params) => Ok(CachedRequestMethod::GetLogs { params: params.try_into()?, }), RequestMethod::GetStorageAt(address, position, block_spec) => { - Ok(CacheableRequestMethod::GetStorageAt { + Ok(CachedRequestMethod::GetStorageAt { address, position, block_spec: block_spec.try_into()?, }) } RequestMethod::GetTransactionByHash(transaction_hash) => { - Ok(CacheableRequestMethod::GetTransactionByHash { transaction_hash }) + Ok(CachedRequestMethod::GetTransactionByHash { transaction_hash }) } RequestMethod::GetTransactionCount(address, block_spec) => { - Ok(CacheableRequestMethod::GetTransactionCount { + Ok(CachedRequestMethod::GetTransactionCount { address, block_spec: block_spec.try_into()?, }) } RequestMethod::GetTransactionReceipt(transaction_hash) => { - Ok(CacheableRequestMethod::GetTransactionReceipt { transaction_hash }) + Ok(CachedRequestMethod::GetTransactionReceipt { transaction_hash }) } - RequestMethod::NetVersion(_) => Ok(CacheableRequestMethod::NetVersion), + RequestMethod::NetVersion(_) => Ok(CachedRequestMethod::NetVersion), // Explicit to make sure if a new method is added, it is not forgotten here. // Chain id is not cacheable since a remote might change its chain id e.g. if it's a @@ -220,14 +216,14 @@ impl<'a> TryFrom<&'a RequestMethod> for CacheableRequestMethod<'a> { /// Method invocations where, if the block spec argument is symbolic, it can be /// resolved to a block number from the response. #[derive(Debug, Clone)] -enum MethodWithResolvableBlockSpec { +pub enum MethodWithResolvableBlockSpec { GetBlockByNumber { include_tx_data: bool }, } -impl<'method> Into> for CacheableRequestMethod<'method> { - fn into(self) -> Option { - match self { - CacheableRequestMethod::GetBlockByNumber { +impl<'method> From> for Option { + fn from(val: CachedRequestMethod<'method>) -> Self { + match val { + CachedRequestMethod::GetBlockByNumber { include_tx_data, block_spec: _, } => Some(MethodWithResolvableBlockSpec::GetBlockByNumber { include_tx_data }), @@ -237,8 +233,7 @@ impl<'method> Into> for CacheableRequestMe } impl CacheableMethod for RequestMethod { - type Cached<'method> = CacheableRequestMethod<'method>; - type MethodWithResolvableBlockTag = MethodWithResolvableBlockSpec; + type Cached<'method> = CachedRequestMethod<'method>; fn block_number_request() -> Self { Self::BlockNumber(()) @@ -266,110 +261,106 @@ impl CacheableMethod for RequestMethod { Self::NetVersion(_) => "net_version", } } +} + +impl<'method> CachedMethod for CachedRequestMethod<'method> { + type MethodWithResolvableBlockTag = MethodWithResolvableBlockSpec; - fn resolve_block_tag<'method>( - method: Self::MethodWithResolvableBlockTag, - block_number: u64, - ) -> Self::Cached<'method> { + fn resolve_block_tag(method: Self::MethodWithResolvableBlockTag, block_number: u64) -> Self { let resolved_block_spec = CacheableBlockSpec::Number { block_number }; match method { MethodWithResolvableBlockSpec::GetBlockByNumber { include_tx_data, .. - } => CacheableRequestMethod::GetBlockByNumber { + } => CachedRequestMethod::GetBlockByNumber { block_spec: resolved_block_spec, include_tx_data, }, } } - fn read_cache_key(&self) -> Option { - let cacheable_method = CacheableRequestMethod::try_from(self).ok()?; - - let key_hasher = cacheable_method.key_hasher().ok()?; + fn read_cache_key(self) -> Option { + let key_hasher = self.key_hasher().ok()?; Some(ReadCacheKey::finalize(key_hasher)) } #[allow(clippy::match_same_arms)] - fn write_cache_key(&self) -> Option> { - let cacheable_method = CacheableRequestMethod::try_from(self).ok()?; - - match cacheable_method.key_hasher() { - Err(SymbolicBlogTagError) => WriteCacheKey::needs_block_tag_resolution(self), + fn write_cache_key(self) -> Option> { + match self.key_hasher() { + Err(UnresolvedBlockTagError) => WriteCacheKey::needs_block_tag_resolution(self), Ok(hasher) => match self { - CacheableRequestMethod::FeeHistory { + CachedRequestMethod::FeeHistory { block_count: _, newest_block, reward_percentiles: _, } => WriteCacheKey::needs_safety_check(hasher, newest_block), - CacheableRequestMethod::GetBalance { + CachedRequestMethod::GetBalance { address: _, block_spec, } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetBlockByNumber { + CachedRequestMethod::GetBlockByNumber { block_spec, include_tx_data: _, } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetBlockByHash { + CachedRequestMethod::GetBlockByHash { block_hash: _, include_tx_data: _, } => Some(WriteCacheKey::finalize(hasher)), - CacheableRequestMethod::GetCode { + CachedRequestMethod::GetCode { address: _, block_spec, } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetLogs { + CachedRequestMethod::GetLogs { params: CacheableLogFilterOptions { range, .. }, } => WriteCacheKey::needs_range_check(hasher, range), - CacheableRequestMethod::GetStorageAt { + CachedRequestMethod::GetStorageAt { address: _, position: _, block_spec, } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetTransactionByHash { + CachedRequestMethod::GetTransactionByHash { transaction_hash: _, } => Some(WriteCacheKey::finalize(hasher)), - CacheableRequestMethod::GetTransactionCount { + CachedRequestMethod::GetTransactionCount { address: _, block_spec, } => WriteCacheKey::needs_safety_check(hasher, block_spec), - CacheableRequestMethod::GetTransactionReceipt { + CachedRequestMethod::GetTransactionReceipt { transaction_hash: _, } => Some(WriteCacheKey::finalize(hasher)), - CacheableRequestMethod::NetVersion => Some(WriteCacheKey::finalize(hasher)), + CachedRequestMethod::NetVersion => Some(WriteCacheKey::finalize(hasher)), }, } } } -impl<'a> CacheKeyVariant for CacheableRequestMethod<'a> { +impl<'a> CacheKeyVariant for CachedRequestMethod<'a> { fn cache_key_variant(&self) -> u8 { match self { // The commented out methods have been removed as they're not currently in use by the // RPC client. If they're added back, they should keep their old variant // number. CacheableRequestMethod::ChainId => 0, - CacheableRequestMethod::GetBalance { .. } => 1, - CacheableRequestMethod::GetBlockByNumber { .. } => 2, - CacheableRequestMethod::GetBlockByHash { .. } => 3, + CachedRequestMethod::GetBalance { .. } => 1, + CachedRequestMethod::GetBlockByNumber { .. } => 2, + CachedRequestMethod::GetBlockByHash { .. } => 3, // CacheableRequestMethod::GetBlockTransactionCountByHash { .. } => 4, // CacheableRequestMethod::GetBlockTransactionCountByNumber { .. } => 5, - CacheableRequestMethod::GetCode { .. } => 6, - CacheableRequestMethod::GetLogs { .. } => 7, - CacheableRequestMethod::GetStorageAt { .. } => 8, + CachedRequestMethod::GetCode { .. } => 6, + CachedRequestMethod::GetLogs { .. } => 7, + CachedRequestMethod::GetStorageAt { .. } => 8, // CacheableRequestMethod::GetTransactionByBlockHashAndIndex { .. } => 9, // CacheableRequestMethod::GetTransactionByBlockNumberAndIndex { .. } => 10, - CacheableRequestMethod::GetTransactionByHash { .. } => 11, - CacheableRequestMethod::GetTransactionCount { .. } => 12, - CacheableRequestMethod::GetTransactionReceipt { .. } => 13, - CacheableRequestMethod::NetVersion => 14, - CacheableRequestMethod::FeeHistory { .. } => 15, + CachedRequestMethod::GetTransactionByHash { .. } => 11, + CachedRequestMethod::GetTransactionCount { .. } => 12, + CachedRequestMethod::GetTransactionReceipt { .. } => 13, + CachedRequestMethod::NetVersion => 14, + CachedRequestMethod::FeeHistory { .. } => 15, } } } #[cfg(test)] mod test { - use edr_eth::{BlockSpec, Eip1898BlockSpec}; use edr_rpc_client::cache::filter::CacheableLogFilterRange; use super::*; @@ -437,12 +428,16 @@ mod test { #[test] fn test_same_arguments_keys_not_equal() { let value = B256::default(); - let key_one = RequestMethod::GetTransactionByHash(value) - .read_cache_key() - .unwrap(); - let key_two = RequestMethod::GetTransactionReceipt(value) - .read_cache_key() - .unwrap(); + let key_one = CachedRequestMethod::GetTransactionByHash { + transaction_hash: &value, + } + .read_cache_key() + .unwrap(); + let key_two = CachedRequestMethod::GetTransactionReceipt { + transaction_hash: &value, + } + .read_cache_key() + .unwrap(); assert_ne!(key_one, key_two); } @@ -452,21 +447,26 @@ mod test { let address = Address::default(); let position = U256::default(); - let key_one = RequestMethod::GetStorageAt( - address, - position, - Some(BlockSpec::Eip1898(Eip1898BlockSpec::Hash { - block_hash: B256::default(), + let key_one = CachedRequestMethod::GetStorageAt { + address: &address, + position: &position, + block_spec: CacheableBlockSpec::Hash { + block_hash: &B256::default(), require_canonical: None, - })), - ) + }, + } .read_cache_key() .unwrap(); - let key_two = - RequestMethod::GetStorageAt(address, position, Some(BlockSpec::Number(u64::default()))) - .read_cache_key() - .unwrap(); + let key_two = CachedRequestMethod::GetStorageAt { + address: &address, + position: &position, + block_spec: CacheableBlockSpec::Number { + block_number: u64::default(), + }, + } + .read_cache_key() + .unwrap(); assert_ne!(key_one, key_two); } @@ -476,15 +476,23 @@ mod test { let address = Address::default(); let position = U256::default(); let block_number = u64::default(); - let block_spec = Some(BlockSpec::Number(block_number)); + let block_spec = CacheableBlockSpec::Number { block_number }; - let key_one = RequestMethod::GetStorageAt(address, position, block_spec.clone()) - .read_cache_key() - .unwrap(); + let key_one = CachedRequestMethod::GetStorageAt { + address: &address, + position: &position, + block_spec: block_spec.clone(), + } + .read_cache_key() + .unwrap(); - let key_two = RequestMethod::GetStorageAt(address, position, block_spec) - .read_cache_key() - .unwrap(); + let key_two = CachedRequestMethod::GetStorageAt { + address: &address, + position: &position, + block_spec, + } + .read_cache_key() + .unwrap(); assert_eq!(key_one, key_two); } diff --git a/crates/edr_rpc_eth/src/chain_spec.rs b/crates/edr_rpc_eth/src/chain_spec.rs new file mode 100644 index 000000000..e90eb1d3b --- /dev/null +++ b/crates/edr_rpc_eth/src/chain_spec.rs @@ -0,0 +1,25 @@ +use serde::{de::DeserializeOwned, Serialize}; + +/// Trait for specifying chain-specific RPC client types. +pub trait ChainSpec { + /// Type representing a block + type Block: GetBlockNumber + DeserializeOwned + Serialize + where + Data: Default + DeserializeOwned + Serialize; + + /// Type representing the transaction. + type Transaction: Default + DeserializeOwned + Serialize; +} + +pub trait GetBlockNumber { + fn number(&self) -> Option; +} + +/// Chain specification for the Ethereum JSON-RPC API. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct EthChainSpec; + +impl ChainSpec for EthChainSpec { + type Block = crate::block::Block where Data: Default + DeserializeOwned + Serialize; + type Transaction = crate::transaction::Transaction; +} diff --git a/crates/edr_rpc_eth/src/client.rs b/crates/edr_rpc_eth/src/client.rs index 601f0a298..c3994403d 100644 --- a/crates/edr_rpc_eth/src/client.rs +++ b/crates/edr_rpc_eth/src/client.rs @@ -2,19 +2,29 @@ use std::fmt::Debug; use async_trait::async_trait; use edr_eth::{ - filter::OneOrMore, log::FilterLog, receipt::BlockReceipt, reward_percentile::RewardPercentile, - AccountInfo, Address, BlockSpec, Bytes, PreEip1898BlockSpec, B256, U256, + account::KECCAK_EMPTY, + fee_history::FeeHistoryResult, + filter::{LogFilterOptions, OneOrMore}, + log::FilterLog, + receipt::BlockReceipt, + reward_percentile::RewardPercentile, + AccountInfo, Address, BlockSpec, Bytecode, Bytes, PreEip1898BlockSpec, B256, U256, U64, }; use edr_rpc_client::{RpcClient, RpcClientError}; +use futures::StreamExt; -use crate::{fork::ForkMetadata, request_methods::RequestMethod, Transaction}; +use crate::{ + chain_spec::{ChainSpec, GetBlockNumber}, + fork::ForkMetadata, + request_methods::RequestMethod, +}; // Constrain parallel requests to avoid rate limiting on transport level and // thundering herd during backoff. const MAX_PARALLEL_REQUESTS: usize = 20; #[async_trait] -pub trait EthClientExt { +pub trait EthClientExt { /// Calls `eth_feeHistory` and returns the fee history. async fn fee_history( &self, @@ -30,7 +40,7 @@ pub trait EthClientExt { /// the set of data contained in [`AccountInfo`]. async fn get_account_info( &self, - address: &Address, + address: Address, block: Option, ) -> Result; @@ -44,38 +54,38 @@ pub trait EthClientExt { /// Calls `eth_getBlockByHash` and returns the transaction's hash. async fn get_block_by_hash( &self, - hash: &B256, - ) -> Result>, RpcClientError>; + hash: B256, + ) -> Result>, RpcClientError>; /// Calls `eth_getBalance`. async fn get_balance( &self, - address: &Address, + address: Address, block: Option, ) -> Result; /// Calls `eth_getBlockByHash` and returns the transaction's data. async fn get_block_by_hash_with_transaction_data( &self, - hash: &B256, - ) -> Result>, RpcClientError>; + hash: B256, + ) -> Result>, RpcClientError>; /// Calls `eth_getBlockByNumber` and returns the transaction's hash. async fn get_block_by_number( &self, spec: PreEip1898BlockSpec, - ) -> Result>, RpcClientError>; + ) -> Result>, RpcClientError>; /// Calls `eth_getBlockByNumber` and returns the transaction's data. async fn get_block_by_number_with_transaction_data( &self, spec: PreEip1898BlockSpec, - ) -> Result, RpcClientError>; + ) -> Result, RpcClientError>; /// Calls `eth_getCode`. async fn get_code( &self, - address: &Address, + address: Address, block: Option, ) -> Result; @@ -91,33 +101,33 @@ pub trait EthClientExt { /// Calls `eth_getTransactionByHash`. async fn get_transaction_by_hash( &self, - tx_hash: &B256, - ) -> Result, RpcClientError>; + tx_hash: B256, + ) -> Result, RpcClientError>; /// Calls `eth_getTransactionCount`. async fn get_transaction_count( &self, - address: &Address, + address: Address, block: Option, ) -> Result; /// Calls `eth_getTransactionReceipt`. async fn get_transaction_receipt( &self, - tx_hash: &B256, + tx_hash: B256, ) -> Result, RpcClientError>; /// Methods for retrieving multiple transaction receipts using concurrent /// requests. async fn get_transaction_receipts( &self, - hashes: impl IntoIterator + Debug, + hashes: impl IntoIterator + Debug + Send, ) -> Result>, RpcClientError>; /// Calls `eth_getStorageAt`. async fn get_storage_at( &self, - address: &Address, + address: Address, position: U256, block: Option, ) -> Result, RpcClientError>; @@ -126,7 +136,13 @@ pub trait EthClientExt { async fn network_id(&self) -> Result; } -impl EthClientExt for RpcClient { +#[async_trait] +impl EthClientExt for RpcClient +where + ChainSpecT::Block: Send + Sync, + ChainSpecT::Block: Send + Sync, + ChainSpecT::Transaction: Send + Sync, +{ #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn fee_history( &self, @@ -144,7 +160,7 @@ impl EthClientExt for RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn fork_metadata(&self) -> Result { - let network_id = self.network_id(); + let network_id = EthClientExt::::network_id(self); let block_number = self.block_number(); let chain_id = self.chain_id(); @@ -161,12 +177,12 @@ impl EthClientExt for RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_account_info( &self, - address: &Address, + address: Address, block: Option, ) -> Result { - let balance = self.get_balance(address, block.clone()); - let nonce = self.get_transaction_count(address, block.clone()); - let code = self.get_code(address, block.clone()); + let balance = EthClientExt::::get_balance(self, address, block.clone()); + let nonce = EthClientExt::::get_transaction_count(self, address, block.clone()); + let code = EthClientExt::::get_code(self, address, block.clone()); let (balance, nonce, code) = tokio::try_join!(balance, nonce, code)?; @@ -190,8 +206,12 @@ impl EthClientExt for RpcClient { addresses: &[Address], block: Option, ) -> Result, RpcClientError> { - futures::stream::iter(addresses.iter()) - .map(|address| self.get_account_info(address, block.clone())) + let addresses = addresses.to_vec(); + + futures::stream::iter(addresses.into_iter()) + .map(|address| { + EthClientExt::::get_account_info(self, address, block.clone()) + }) .buffered(MAX_PARALLEL_REQUESTS / 3 + 1) .collect::>>() .await @@ -202,36 +222,38 @@ impl EthClientExt for RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_block_by_hash( &self, - hash: &B256, - ) -> Result>, RpcClientError> { - self.call(MethodT::GetBlockByHash(*hash, false)).await + hash: B256, + ) -> Result>, RpcClientError> { + self.call(RequestMethod::GetBlockByHash(hash, false)).await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_balance( &self, - address: &Address, + address: Address, block: Option, ) -> Result { - self.call(MethodT::GetBalance(*address, block)).await + self.call(RequestMethod::GetBalance(address, block)).await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_block_by_hash_with_transaction_data( &self, - hash: &B256, - ) -> Result>, RpcClientError> { - self.call(MethodT::GetBlockByHash(*hash, true)).await + hash: B256, + ) -> Result>, RpcClientError> { + self.call(RequestMethod::GetBlockByHash(hash, true)).await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_block_by_number( &self, spec: PreEip1898BlockSpec, - ) -> Result>, RpcClientError> { + ) -> Result>, RpcClientError> { self.call_with_resolver( - MethodT::GetBlockByNumber(spec, false), - |block: &Option>| block.as_ref().and_then(|block| block.number), + RequestMethod::GetBlockByNumber(spec, false), + |block: &Option>| { + block.as_ref().and_then(GetBlockNumber::number) + }, ) .await } @@ -240,10 +262,10 @@ impl EthClientExt for RpcClient { async fn get_block_by_number_with_transaction_data( &self, spec: PreEip1898BlockSpec, - ) -> Result, RpcClientError> { + ) -> Result, RpcClientError> { self.call_with_resolver( - MethodT::GetBlockByNumber(spec, true), - |block: ð::Block| block.number, + RequestMethod::GetBlockByNumber(spec, true), + |block: &ChainSpecT::Block| block.number(), ) .await } @@ -251,10 +273,10 @@ impl EthClientExt for RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_code( &self, - address: &Address, + address: Address, block: Option, ) -> Result { - self.call(MethodT::GetCode(*address, block)).await + self.call(RequestMethod::GetCode(address, block)).await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] @@ -265,7 +287,7 @@ impl EthClientExt for RpcClient { address: Option>, topics: Option>>>, ) -> Result, RpcClientError> { - self.call(MethodT::GetLogs(LogFilterOptions { + self.call(RequestMethod::GetLogs(LogFilterOptions { from_block: Some(from_block), to_block: Some(to_block), block_hash: None, @@ -278,37 +300,43 @@ impl EthClientExt for RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_transaction_by_hash( &self, - tx_hash: &B256, - ) -> Result, RpcClientError> { - self.call(MethodT::GetTransactionByHash(*tx_hash)).await + tx_hash: B256, + ) -> Result, RpcClientError> { + self.call(RequestMethod::GetTransactionByHash(tx_hash)) + .await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_transaction_count( &self, - address: &Address, + address: Address, block: Option, ) -> Result { - self.call(MethodT::GetTransactionCount(*address, block)) + self.call(RequestMethod::GetTransactionCount(address, block)) .await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_transaction_receipt( &self, - tx_hash: &B256, + tx_hash: B256, ) -> Result, RpcClientError> { - self.call(MethodT::GetTransactionReceipt(*tx_hash)).await + self.call(RequestMethod::GetTransactionReceipt(tx_hash)) + .await } - #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + // #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", + // skip(self)))] async fn get_transaction_receipts( &self, - hashes: impl IntoIterator + Debug, + hashes: impl IntoIterator + Debug + Send, ) -> Result>, RpcClientError> { let requests = hashes .into_iter() - .map(|transaction_hash| self.get_transaction_receipt(transaction_hash)); + .map(|transaction_hash| { + EthClientExt::::get_transaction_receipt(self, transaction_hash) + }) + .collect::>(); futures::stream::iter(requests) .buffered(MAX_PARALLEL_REQUESTS) @@ -321,17 +349,17 @@ impl EthClientExt for RpcClient { #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn get_storage_at( &self, - address: &Address, + address: Address, position: U256, block: Option, ) -> Result, RpcClientError> { - self.call(MethodT::GetStorageAt(*address, position, block)) + self.call(RequestMethod::GetStorageAt(address, position, block)) .await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] async fn network_id(&self) -> Result { - self.call::(MethodT::NetVersion(())) + self.call::(RequestMethod::NetVersion(())) .await .map(|network_id| network_id.as_limbs()[0]) } diff --git a/crates/edr_rpc_eth/src/lib.rs b/crates/edr_rpc_eth/src/lib.rs index 8a02da4bf..2b1cd6641 100644 --- a/crates/edr_rpc_eth/src/lib.rs +++ b/crates/edr_rpc_eth/src/lib.rs @@ -1,13 +1,18 @@ +/// Types for Ethereum JSON-RPC blocks +pub mod block; mod cacheable_method_invocation; /// Input type for `eth_call` and `eth_estimateGas` mod call_request; -mod client; -/// ethereum objects as specifically used in the JSON-RPC interface -pub mod eth; +pub mod chain_spec; +/// Types related to the Ethereum JSON-RPC API +pub mod client; /// Types related to forking a remote blockchain. pub mod fork; mod r#override; mod request_methods; mod transaction; -pub use self::{call_request::CallRequest, r#override::*, transaction::Transaction}; +pub use self::{ + call_request::CallRequest, r#override::*, request_methods::RequestMethod, + transaction::Transaction, +}; diff --git a/crates/edr_rpc_eth/tests/client.rs b/crates/edr_rpc_eth/tests/client.rs index 9f4c3112f..fa35f3bf4 100644 --- a/crates/edr_rpc_eth/tests/client.rs +++ b/crates/edr_rpc_eth/tests/client.rs @@ -1,12 +1,13 @@ use std::{ops::Deref, str::FromStr}; +use edr_eth::B256; +use edr_rpc_client::{RpcClient, RpcClientError}; +use edr_rpc_eth::{client::EthClientExt, RequestMethod}; use reqwest::StatusCode; use tempfile::TempDir; -use super::*; - struct TestRpcClient { - client: RpcClient, + client: RpcClient, // Need to keep the tempdir around to prevent it from being deleted // Only accessed when feature = "test-remote", hence the allow. @@ -25,7 +26,7 @@ impl TestRpcClient { } impl Deref for TestRpcClient { - type Target = RpcClient; + type Target = RpcClient; fn deref(&self) -> &Self::Target { &self.client @@ -49,7 +50,7 @@ async fn send_request_body_400_status() { .expect("failed to parse hash from string"); let error = TestRpcClient::new(&server.url()) - .call::>(MethodT::GetTransactionByHash(hash)) + .get_transaction_by_hash(hash) .await .expect_err("should have failed to due to a HTTP status error"); @@ -105,7 +106,7 @@ mod alchemy { .expect("failed to parse hash from string"); let error = TestRpcClient::new(&alchemy_url) - .call::>(MethodT::GetTransactionByHash(hash)) + .call::>(RequestMethod::GetTransactionByHash(hash)) .await .expect_err("should have failed to interpret response as a Transaction"); @@ -130,7 +131,7 @@ mod alchemy { .expect("failed to parse hash from string"); let error = TestRpcClient::new(alchemy_url) - .call::>(MethodT::GetTransactionByHash(hash)) + .call::>(RequestMethod::GetTransactionByHash(hash)) .await .expect_err("should have failed to connect due to a garbage domain name"); From 48b4ac6688219c0069ed06d97a5e15c84d19b4e3 Mon Sep 17 00:00:00 2001 From: Wodann Date: Wed, 29 May 2024 22:13:54 +0000 Subject: [PATCH 05/18] WIP: try composition --- Cargo.lock | 4 +- crates/edr_eth/Cargo.toml | 1 - crates/edr_rpc_client/src/client.rs | 3 - crates/edr_rpc_client/src/lib.rs | 2 +- crates/edr_rpc_eth/Cargo.toml | 3 +- crates/edr_rpc_eth/src/block.rs | 2 +- crates/edr_rpc_eth/src/client.rs | 328 +++++++----------- crates/edr_rpc_eth/src/lib.rs | 3 +- .../src/{chain_spec.rs => spec.rs} | 8 +- crates/edr_rpc_eth/tests/client.rs | 13 +- 10 files changed, 150 insertions(+), 217 deletions(-) rename crates/edr_rpc_eth/src/{chain_spec.rs => spec.rs} (82%) diff --git a/Cargo.lock b/Cargo.lock index 097f14dca..7481c17c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1069,7 +1069,6 @@ dependencies = [ "tokio", "tracing", "triehash", - "walkdir", ] [[package]] @@ -1201,9 +1200,9 @@ name = "edr_rpc_eth" version = "0.3.5" dependencies = [ "anyhow", - "async-trait", "edr_eth", "edr_rpc_client", + "edr_test_utils", "futures", "hex", "mockito", @@ -1214,6 +1213,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "walkdir", ] [[package]] diff --git a/crates/edr_eth/Cargo.toml b/crates/edr_eth/Cargo.toml index 8d4ccb991..ca5115e41 100644 --- a/crates/edr_eth/Cargo.toml +++ b/crates/edr_eth/Cargo.toml @@ -32,7 +32,6 @@ paste = { version = "1.0.14", default-features = false } serde_json = { version = "1.0.89" } serial_test = "2.0.0" tokio = { version = "1.23.0", features = ["macros"] } -walkdir = { version = "2.3.3", default-features = false } [features] default = ["std"] diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index d59e63a8e..8d05ab743 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -638,6 +638,3 @@ impl SerializedRequest { self.0.to_string() } } - -#[cfg(test)] -mod tests {} diff --git a/crates/edr_rpc_client/src/lib.rs b/crates/edr_rpc_client/src/lib.rs index f01dd7f35..0710560d0 100644 --- a/crates/edr_rpc_client/src/lib.rs +++ b/crates/edr_rpc_client/src/lib.rs @@ -10,6 +10,6 @@ pub mod jsonrpc; mod reqwest_error; pub use self::{ - client::{RpcClient, RpcClientError}, + client::*, reqwest_error::{MiddlewareError, ReqwestError}, }; diff --git a/crates/edr_rpc_eth/Cargo.toml b/crates/edr_rpc_eth/Cargo.toml index c21d5cf88..018dcb4ba 100644 --- a/crates/edr_rpc_eth/Cargo.toml +++ b/crates/edr_rpc_eth/Cargo.toml @@ -4,7 +4,6 @@ version = "0.3.5" edition = "2021" [dependencies] -async-trait = { version = "0.1.80", default-features = false } edr_eth = { version = "0.3.5", path = "../edr_eth", features = ["serde"] } edr_rpc_client = { version = "0.3.5", path = "../edr_rpc_client" } futures = { version = "0.3.28", default-features = false, features = ["std"] } @@ -16,10 +15,12 @@ tracing = { version = "0.1.37", default-features = false, features = ["attribute [dev-dependencies] anyhow = { version = "1.0.75", default-features = false, features = ["std"] } +edr_test_utils = { version = "0.3.5", path = "../edr_test_utils" } mockito = { version = "1.0.2", default-features = false } reqwest = { version = "0.11", default-features = false, features = ["blocking", "json"] } serde_json = { version = "1.0.89" } tempfile = { version = "3.7.1", default-features = false } +walkdir = { version = "2.3.3", default-features = false } [features] test-remote = [] diff --git a/crates/edr_rpc_eth/src/block.rs b/crates/edr_rpc_eth/src/block.rs index 029bf3169..e6be6fae9 100644 --- a/crates/edr_rpc_eth/src/block.rs +++ b/crates/edr_rpc_eth/src/block.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use edr_eth::{withdrawal::Withdrawal, Address, Bloom, Bytes, B256, B64, U256}; use serde::{Deserialize, Serialize}; -use crate::chain_spec::GetBlockNumber; +use crate::spec::GetBlockNumber; /// block object returned by `eth_getBlockBy*` #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] diff --git a/crates/edr_rpc_eth/src/client.rs b/crates/edr_rpc_eth/src/client.rs index c3994403d..f6a4dd2f8 100644 --- a/crates/edr_rpc_eth/src/client.rs +++ b/crates/edr_rpc_eth/src/client.rs @@ -1,6 +1,5 @@ -use std::fmt::Debug; +use std::{fmt::Debug, path::PathBuf}; -use async_trait::async_trait; use edr_eth::{ account::KECCAK_EMPTY, fee_history::FeeHistoryResult, @@ -10,159 +9,70 @@ use edr_eth::{ reward_percentile::RewardPercentile, AccountInfo, Address, BlockSpec, Bytecode, Bytes, PreEip1898BlockSpec, B256, U256, U64, }; -use edr_rpc_client::{RpcClient, RpcClientError}; +use edr_rpc_client::RpcClient; +pub use edr_rpc_client::{header, HeaderMap, RpcClientError}; use futures::StreamExt; use crate::{ - chain_spec::{ChainSpec, GetBlockNumber}, fork::ForkMetadata, request_methods::RequestMethod, + spec::{GetBlockNumber, RpcSpec}, }; // Constrain parallel requests to avoid rate limiting on transport level and // thundering herd during backoff. const MAX_PARALLEL_REQUESTS: usize = 20; -#[async_trait] -pub trait EthClientExt { - /// Calls `eth_feeHistory` and returns the fee history. - async fn fee_history( - &self, - block_count: u64, - newest_block: BlockSpec, - reward_percentiles: Option>, - ) -> Result; - - /// Fetches the latest block number, chain ID, and network ID concurrently. - async fn fork_metadata(&self) -> Result; - - /// Submits three concurrent RPC method invocations in order to obtain - /// the set of data contained in [`AccountInfo`]. - async fn get_account_info( - &self, - address: Address, - block: Option, - ) -> Result; - - /// Fetches account infos for multiple addresses using concurrent requests. - async fn get_account_infos( - &self, - addresses: &[Address], - block: Option, - ) -> Result, RpcClientError>; - - /// Calls `eth_getBlockByHash` and returns the transaction's hash. - async fn get_block_by_hash( - &self, - hash: B256, - ) -> Result>, RpcClientError>; - - /// Calls `eth_getBalance`. - async fn get_balance( - &self, - address: Address, - block: Option, - ) -> Result; +// where +// RpcSpecT::Block: Send + Sync, +// RpcSpecT::Block: Send + Sync, +// RpcSpecT::Transaction: Send + Sync, - /// Calls `eth_getBlockByHash` and returns the transaction's data. - async fn get_block_by_hash_with_transaction_data( - &self, - hash: B256, - ) -> Result>, RpcClientError>; - - /// Calls `eth_getBlockByNumber` and returns the transaction's hash. - async fn get_block_by_number( - &self, - spec: PreEip1898BlockSpec, - ) -> Result>, RpcClientError>; - - /// Calls `eth_getBlockByNumber` and returns the transaction's data. - async fn get_block_by_number_with_transaction_data( - &self, - spec: PreEip1898BlockSpec, - ) -> Result, RpcClientError>; - - /// Calls `eth_getCode`. - async fn get_code( - &self, - address: Address, - block: Option, - ) -> Result; - - /// Calls `eth_getLogs` using a starting and ending block (inclusive). - async fn get_logs_by_range( - &self, - from_block: BlockSpec, - to_block: BlockSpec, - address: Option>, - topics: Option>>>, - ) -> Result, RpcClientError>; - - /// Calls `eth_getTransactionByHash`. - async fn get_transaction_by_hash( - &self, - tx_hash: B256, - ) -> Result, RpcClientError>; - - /// Calls `eth_getTransactionCount`. - async fn get_transaction_count( - &self, - address: Address, - block: Option, - ) -> Result; - - /// Calls `eth_getTransactionReceipt`. - async fn get_transaction_receipt( - &self, - tx_hash: B256, - ) -> Result, RpcClientError>; - - /// Methods for retrieving multiple transaction receipts using concurrent - /// requests. - async fn get_transaction_receipts( - &self, - hashes: impl IntoIterator + Debug + Send, - ) -> Result>, RpcClientError>; - - /// Calls `eth_getStorageAt`. - async fn get_storage_at( - &self, - address: Address, - position: U256, - block: Option, - ) -> Result, RpcClientError>; - - /// Calls `net_version`. - async fn network_id(&self) -> Result; +pub struct EthRpcClient { + inner: RpcClient, + _phantom: std::marker::PhantomData, } -#[async_trait] -impl EthClientExt for RpcClient -where - ChainSpecT::Block: Send + Sync, - ChainSpecT::Block: Send + Sync, - ChainSpecT::Transaction: Send + Sync, -{ +impl EthRpcClient { + /// Creates a new instance, given a remote node URL. + /// + /// The cache directory is the global EDR cache directory configured by the + /// user. + pub fn new( + url: &str, + cache_dir: PathBuf, + extra_headers: Option, + ) -> Result { + let inner = RpcClient::new(url, cache_dir, extra_headers)?; + Ok(Self { + inner, + _phantom: std::marker::PhantomData, + }) + } + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn fee_history( + /// Calls `eth_feeHistory` and returns the fee history. + pub async fn fee_history( &self, block_count: u64, newest_block: BlockSpec, reward_percentiles: Option>, ) -> Result { - self.call(RequestMethod::FeeHistory( - U256::from(block_count), - newest_block, - reward_percentiles, - )) - .await + self.inner + .call(RequestMethod::FeeHistory( + U256::from(block_count), + newest_block, + reward_percentiles, + )) + .await } #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn fork_metadata(&self) -> Result { - let network_id = EthClientExt::::network_id(self); - let block_number = self.block_number(); - let chain_id = self.chain_id(); + /// Fetches the latest block number, chain ID, and network ID concurrently. + pub async fn fork_metadata(&self) -> Result { + let network_id = self.network_id(); + let block_number = self.inner.block_number(); + let chain_id = self.inner.chain_id(); let (network_id, block_number, chain_id) = tokio::try_join!(network_id, block_number, chain_id)?; @@ -174,15 +84,17 @@ where }) } + /// Submits three concurrent RPC method invocations in order to obtain + /// the set of data contained in [`AccountInfo`]. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_account_info( + pub async fn get_account_info( &self, address: Address, block: Option, ) -> Result { - let balance = EthClientExt::::get_balance(self, address, block.clone()); - let nonce = EthClientExt::::get_transaction_count(self, address, block.clone()); - let code = EthClientExt::::get_code(self, address, block.clone()); + let balance = self.get_balance(address, block.clone()); + let nonce = self.get_transaction_count(address, block.clone()); + let code = self.get_code(address, block.clone()); let (balance, nonce, code) = tokio::try_join!(balance, nonce, code)?; @@ -200,18 +112,15 @@ where }) } + /// Fetches account infos for multiple addresses using concurrent requests. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_account_infos( + pub async fn get_account_infos( &self, addresses: &[Address], block: Option, ) -> Result, RpcClientError> { - let addresses = addresses.to_vec(); - - futures::stream::iter(addresses.into_iter()) - .map(|address| { - EthClientExt::::get_account_info(self, address, block.clone()) - }) + futures::stream::iter(addresses.iter()) + .map(|address| self.get_account_info(*address, block.clone())) .buffered(MAX_PARALLEL_REQUESTS / 3 + 1) .collect::>>() .await @@ -219,123 +128,146 @@ where .collect() } + /// Calls `eth_getBlockByHash` and returns the transaction's hash. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_block_by_hash( + pub async fn get_block_by_hash( &self, hash: B256, - ) -> Result>, RpcClientError> { - self.call(RequestMethod::GetBlockByHash(hash, false)).await + ) -> Result>, RpcClientError> { + self.inner + .call(RequestMethod::GetBlockByHash(hash, false)) + .await } + /// Calls `eth_getBalance`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_balance( + pub async fn get_balance( &self, address: Address, block: Option, ) -> Result { - self.call(RequestMethod::GetBalance(address, block)).await + self.inner + .call(RequestMethod::GetBalance(address, block)) + .await } + /// Calls `eth_getBlockByHash` and returns the transaction's data. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_block_by_hash_with_transaction_data( + pub async fn get_block_by_hash_with_transaction_data( &self, hash: B256, - ) -> Result>, RpcClientError> { - self.call(RequestMethod::GetBlockByHash(hash, true)).await + ) -> Result>, RpcClientError> { + self.inner + .call(RequestMethod::GetBlockByHash(hash, true)) + .await } + /// Calls `eth_getBlockByNumber` and returns the transaction's hash. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_block_by_number( + pub async fn get_block_by_number( &self, spec: PreEip1898BlockSpec, - ) -> Result>, RpcClientError> { - self.call_with_resolver( - RequestMethod::GetBlockByNumber(spec, false), - |block: &Option>| { - block.as_ref().and_then(GetBlockNumber::number) - }, - ) - .await + ) -> Result>, RpcClientError> { + self.inner + .call_with_resolver( + RequestMethod::GetBlockByNumber(spec, false), + |block: &Option>| { + block.as_ref().and_then(GetBlockNumber::number) + }, + ) + .await } + /// Calls `eth_getBlockByNumber` and returns the transaction's data. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_block_by_number_with_transaction_data( + pub async fn get_block_by_number_with_transaction_data( &self, spec: PreEip1898BlockSpec, - ) -> Result, RpcClientError> { - self.call_with_resolver( - RequestMethod::GetBlockByNumber(spec, true), - |block: &ChainSpecT::Block| block.number(), - ) - .await + ) -> Result, RpcClientError> { + self.inner + .call_with_resolver( + RequestMethod::GetBlockByNumber(spec, true), + |block: &RpcSpecT::Block| block.number(), + ) + .await } + /// Calls `eth_getCode`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_code( + pub async fn get_code( &self, address: Address, block: Option, ) -> Result { - self.call(RequestMethod::GetCode(address, block)).await + self.inner + .call(RequestMethod::GetCode(address, block)) + .await } + /// Calls `eth_getLogs` using a starting and ending block (inclusive). #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_logs_by_range( + pub async fn get_logs_by_range( &self, from_block: BlockSpec, to_block: BlockSpec, address: Option>, topics: Option>>>, ) -> Result, RpcClientError> { - self.call(RequestMethod::GetLogs(LogFilterOptions { - from_block: Some(from_block), - to_block: Some(to_block), - block_hash: None, - address, - topics, - })) - .await + self.inner + .call(RequestMethod::GetLogs(LogFilterOptions { + from_block: Some(from_block), + to_block: Some(to_block), + block_hash: None, + address, + topics, + })) + .await } + /// Calls `eth_getTransactionByHash`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_transaction_by_hash( + pub async fn get_transaction_by_hash( &self, tx_hash: B256, - ) -> Result, RpcClientError> { - self.call(RequestMethod::GetTransactionByHash(tx_hash)) + ) -> Result, RpcClientError> { + self.inner + .call(RequestMethod::GetTransactionByHash(tx_hash)) .await } + /// Calls `eth_getTransactionCount`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_transaction_count( + pub async fn get_transaction_count( &self, address: Address, block: Option, ) -> Result { - self.call(RequestMethod::GetTransactionCount(address, block)) + self.inner + .call(RequestMethod::GetTransactionCount(address, block)) .await } + /// Calls `eth_getTransactionReceipt`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_transaction_receipt( + pub async fn get_transaction_receipt( &self, tx_hash: B256, ) -> Result, RpcClientError> { - self.call(RequestMethod::GetTransactionReceipt(tx_hash)) + self.inner + .call(RequestMethod::GetTransactionReceipt(tx_hash)) .await } - // #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", - // skip(self)))] - async fn get_transaction_receipts( + /// Methods for retrieving multiple transaction receipts using concurrent + /// requests. + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + pub async fn get_transaction_receipts( &self, - hashes: impl IntoIterator + Debug + Send, + hashes: impl IntoIterator + Debug, ) -> Result>, RpcClientError> { let requests = hashes .into_iter() - .map(|transaction_hash| { - EthClientExt::::get_transaction_receipt(self, transaction_hash) - }) + .map(|transaction_hash| self.get_transaction_receipt(*transaction_hash)) .collect::>(); futures::stream::iter(requests) @@ -346,20 +278,24 @@ where .collect() } + /// Calls `eth_getStorageAt`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn get_storage_at( + pub async fn get_storage_at( &self, address: Address, position: U256, block: Option, ) -> Result, RpcClientError> { - self.call(RequestMethod::GetStorageAt(address, position, block)) + self.inner + .call(RequestMethod::GetStorageAt(address, position, block)) .await } + /// Calls `net_version`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] - async fn network_id(&self) -> Result { - self.call::(RequestMethod::NetVersion(())) + pub async fn network_id(&self) -> Result { + self.inner + .call::(RequestMethod::NetVersion(())) .await .map(|network_id| network_id.as_limbs()[0]) } diff --git a/crates/edr_rpc_eth/src/lib.rs b/crates/edr_rpc_eth/src/lib.rs index 2b1cd6641..6457c62a5 100644 --- a/crates/edr_rpc_eth/src/lib.rs +++ b/crates/edr_rpc_eth/src/lib.rs @@ -3,13 +3,14 @@ pub mod block; mod cacheable_method_invocation; /// Input type for `eth_call` and `eth_estimateGas` mod call_request; -pub mod chain_spec; /// Types related to the Ethereum JSON-RPC API pub mod client; /// Types related to forking a remote blockchain. pub mod fork; mod r#override; mod request_methods; +/// Types for Ethereum JSON-RPC API specification. +pub mod spec; mod transaction; pub use self::{ diff --git a/crates/edr_rpc_eth/src/chain_spec.rs b/crates/edr_rpc_eth/src/spec.rs similarity index 82% rename from crates/edr_rpc_eth/src/chain_spec.rs rename to crates/edr_rpc_eth/src/spec.rs index e90eb1d3b..3a19be480 100644 --- a/crates/edr_rpc_eth/src/chain_spec.rs +++ b/crates/edr_rpc_eth/src/spec.rs @@ -1,7 +1,7 @@ use serde::{de::DeserializeOwned, Serialize}; -/// Trait for specifying chain-specific RPC client types. -pub trait ChainSpec { +/// Trait for specifying Ethereum-based JSON-RPC method types. +pub trait RpcSpec { /// Type representing a block type Block: GetBlockNumber + DeserializeOwned + Serialize where @@ -17,9 +17,9 @@ pub trait GetBlockNumber { /// Chain specification for the Ethereum JSON-RPC API. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct EthChainSpec; +pub struct EthRpcSpec; -impl ChainSpec for EthChainSpec { +impl RpcSpec for EthRpcSpec { type Block = crate::block::Block where Data: Default + DeserializeOwned + Serialize; type Transaction = crate::transaction::Transaction; } diff --git a/crates/edr_rpc_eth/tests/client.rs b/crates/edr_rpc_eth/tests/client.rs index fa35f3bf4..3fb9e5ae8 100644 --- a/crates/edr_rpc_eth/tests/client.rs +++ b/crates/edr_rpc_eth/tests/client.rs @@ -1,13 +1,13 @@ use std::{ops::Deref, str::FromStr}; use edr_eth::B256; -use edr_rpc_client::{RpcClient, RpcClientError}; -use edr_rpc_eth::{client::EthClientExt, RequestMethod}; +use edr_rpc_client::RpcClientError; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec, RequestMethod}; use reqwest::StatusCode; use tempfile::TempDir; struct TestRpcClient { - client: RpcClient, + client: EthRpcClient, // Need to keep the tempdir around to prevent it from being deleted // Only accessed when feature = "test-remote", hence the allow. @@ -19,14 +19,14 @@ impl TestRpcClient { fn new(url: &str) -> Self { let tempdir = TempDir::new().unwrap(); Self { - client: RpcClient::new(url, tempdir.path().into(), None).expect("url ok"), + client: EthRpcClient::new(url, tempdir.path().into(), None).expect("url ok"), cache_dir: tempdir, } } } impl Deref for TestRpcClient { - type Target = RpcClient; + type Target = EthRpcClient; fn deref(&self) -> &Self::Target { &self.client @@ -68,14 +68,13 @@ async fn send_request_body_400_status() { #[cfg(feature = "test-remote")] mod alchemy { - use std::fs::File; + use std::{fs::File, path::PathBuf}; use edr_test_utils::env::get_alchemy_url; use futures::future::join_all; use walkdir::WalkDir; use super::*; - use crate::Bytes; // The maximum block number that Alchemy allows const MAX_BLOCK_NUMBER: u64 = u64::MAX >> 1; From fa61e37d124a168a51431de64795b380ba445b6d Mon Sep 17 00:00:00 2001 From: Wodann Date: Fri, 31 May 2024 05:51:23 +0000 Subject: [PATCH 06/18] WIP: fix all compiler errors --- Cargo.lock | 5 + crates/edr_evm/Cargo.toml | 1 + crates/edr_evm/src/block.rs | 9 +- crates/edr_evm/src/block/remote.rs | 8 +- crates/edr_evm/src/blockchain/forked.rs | 12 +- crates/edr_evm/src/blockchain/remote.rs | 28 +-- .../edr_evm/src/blockchain/storage/sparse.rs | 2 +- crates/edr_evm/src/state.rs | 3 +- crates/edr_evm/src/state/fork.rs | 15 +- crates/edr_evm/src/state/overrides.rs | 7 +- crates/edr_evm/src/state/remote.rs | 22 +- crates/edr_evm/src/state/remote/cached.rs | 5 +- crates/edr_evm/src/transaction/executable.rs | 5 +- crates/edr_rpc_client/Cargo.toml | 5 + crates/edr_rpc_client/src/client.rs | 233 ++++++++++++++++++ crates/edr_rpc_eth/Cargo.toml | 1 + crates/edr_rpc_eth/src/client.rs | 22 ++ crates/edr_rpc_eth/src/lib.rs | 4 +- crates/edr_rpc_eth/tests/client.rs | 161 ++---------- 19 files changed, 348 insertions(+), 200 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7481c17c1..ce1ca5fac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1084,6 +1084,7 @@ dependencies = [ "dyn-clone", "edr_defaults", "edr_eth", + "edr_rpc_eth", "edr_test_utils", "futures", "hasher", @@ -1175,6 +1176,7 @@ version = "0.3.5" dependencies = [ "anyhow", "edr_eth", + "edr_test_utils", "futures", "hex", "hyper", @@ -1188,11 +1190,13 @@ dependencies = [ "serde", "serde_json", "sha3", + "tempfile", "thiserror", "tokio", "tracing", "url", "uuid", + "walkdir", ] [[package]] @@ -1209,6 +1213,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serial_test", "tempfile", "thiserror", "tokio", diff --git a/crates/edr_evm/Cargo.toml b/crates/edr_evm/Cargo.toml index 3857673a2..e60ade5d5 100644 --- a/crates/edr_evm/Cargo.toml +++ b/crates/edr_evm/Cargo.toml @@ -18,6 +18,7 @@ once_cell = { version = "1.18.0", default-features = false, features = ["alloc", parking_lot = { version = "0.12.1", default-features = false } edr_defaults = { version = "0.3.5", path = "../edr_defaults" } edr_eth = { version = "0.3.5", path = "../edr_eth", features = ["rand", "serde"] } +edr_rpc_eth = { version = "0.3.5", path = "../edr_rpc_eth" } revm = { git = "https://github.com/NomicFoundation/revm", rev = "aceb093", version = "8.0", default-features = false, features = ["c-kzg", "dev", "serde"] } rpds = { version = "1.1.0", default-features = false, features = ["std"] } serde = { version = "1.0.158", default-features = false, features = ["std"] } diff --git a/crates/edr_evm/src/block.rs b/crates/edr_evm/src/block.rs index 552ae5906..88a9ed897 100644 --- a/crates/edr_evm/src/block.rs +++ b/crates/edr_evm/src/block.rs @@ -6,8 +6,7 @@ use std::{fmt::Debug, sync::Arc}; use auto_impl::auto_impl; use edr_eth::{ - block, receipt::BlockReceipt, remote::eth, transaction::Transaction, withdrawal::Withdrawal, - B256, U256, + block, receipt::BlockReceipt, transaction::Transaction, withdrawal::Withdrawal, B256, U256, }; pub use self::{ @@ -71,7 +70,9 @@ impl Clone for BlockAndTotalDifficulty { } } -impl From> for eth::Block { +impl From> + for edr_rpc_eth::Block +{ fn from(value: BlockAndTotalDifficulty) -> Self { let transactions = value .block @@ -81,7 +82,7 @@ impl From> for eth:: .collect(); let header = value.block.header(); - eth::Block { + edr_rpc_eth::Block { hash: Some(*value.block.hash()), parent_hash: header.parent_hash, sha3_uncles: header.ommers_hash, diff --git a/crates/edr_evm/src/block/remote.rs b/crates/edr_evm/src/block/remote.rs index 6ee82bfe9..ef868caa3 100644 --- a/crates/edr_evm/src/block/remote.rs +++ b/crates/edr_evm/src/block/remote.rs @@ -3,11 +3,11 @@ use std::sync::{Arc, OnceLock}; use edr_eth::{ block::{BlobGas, Header}, receipt::BlockReceipt, - remote::{eth, RpcClient}, transaction::Transaction, withdrawal::Withdrawal, B256, }; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use tokio::runtime; use crate::{ @@ -54,15 +54,15 @@ pub struct RemoteBlock { /// The length of the RLP encoding of this block in bytes size: u64, // The RPC client is needed to lazily fetch receipts - rpc_client: Arc, + rpc_client: Arc>, runtime: runtime::Handle, } impl RemoteBlock { /// Constructs a new instance with the provided JSON-RPC block and client. pub fn new( - block: eth::Block, - rpc_client: Arc, + block: edr_rpc_eth::Block, + rpc_client: Arc>, runtime: runtime::Handle, ) -> Result { let header = Header { diff --git a/crates/edr_evm/src/blockchain/forked.rs b/crates/edr_evm/src/blockchain/forked.rs index 764c4d0ea..0cf306a3e 100644 --- a/crates/edr_evm/src/blockchain/forked.rs +++ b/crates/edr_evm/src/blockchain/forked.rs @@ -5,9 +5,13 @@ use edr_eth::{ block::{largest_safe_block_number, safe_block_depth, LargestSafeBlockNumberArgs}, log::FilterLog, receipt::BlockReceipt, - remote::{client::ForkMetadata, BlockSpec, RpcClient, RpcClientError}, spec::{chain_hardfork_activations, chain_name, HardforkActivations}, - AccountInfo, Address, Bytes, B256, U256, + AccountInfo, Address, BlockSpec, Bytes, B256, U256, +}; +use edr_rpc_eth::{ + client::{EthRpcClient, RpcClientError}, + fork::ForkMetadata, + spec::EthRpcSpec, }; use parking_lot::Mutex; use revm::{ @@ -104,7 +108,7 @@ impl ForkedBlockchain { runtime: runtime::Handle, chain_id_override: Option, spec_id: SpecId, - rpc_client: Arc, + rpc_client: Arc>, fork_block_number: Option, irregular_state: &mut IrregularState, state_root_generator: Arc>, @@ -114,7 +118,7 @@ impl ForkedBlockchain { chain_id: remote_chain_id, network_id, latest_block_number, - } = rpc_client.fetch_fork_metadata().await?; + } = rpc_client.fork_metadata().await?; let recommended_block_number = recommended_fork_block_number(RecommendedForkBlockNumberArgs { diff --git a/crates/edr_evm/src/blockchain/remote.rs b/crates/edr_evm/src/blockchain/remote.rs index b186bf915..4178fd5c2 100644 --- a/crates/edr_evm/src/blockchain/remote.rs +++ b/crates/edr_evm/src/blockchain/remote.rs @@ -2,11 +2,10 @@ use std::sync::Arc; use async_rwlock::{RwLock, RwLockUpgradableReadGuard}; use edr_eth::{ - log::FilterLog, - receipt::BlockReceipt, - remote::{self, filter::OneOrMore, BlockSpec, PreEip1898BlockSpec, RpcClient}, - Address, B256, U256, + filter::OneOrMore, log::FilterLog, receipt::BlockReceipt, Address, BlockSpec, + PreEip1898BlockSpec, B256, U256, }; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use revm::primitives::HashSet; use tokio::runtime; @@ -15,7 +14,7 @@ use crate::{blockchain::ForkedBlockchainError, Block, RemoteBlock}; #[derive(Debug)] pub struct RemoteBlockchain { - client: Arc, + client: Arc>, cache: RwLock>, runtime: runtime::Handle, } @@ -24,7 +23,7 @@ impl, const FORCE_CACHING: bool> RemoteBlockchain { /// Constructs a new instance with the provided RPC client. - pub fn new(client: Arc, runtime: runtime::Handle) -> Self { + pub fn new(client: Arc>, runtime: runtime::Handle) -> Self { Self { client, cache: RwLock::new(SparseBlockchainStorage::default()), @@ -46,7 +45,7 @@ impl, const FORCE_CACHING: bool> if let Some(block) = self .client - .get_block_by_hash_with_transaction_data(hash) + .get_block_by_hash_with_transaction_data(*hash) .await? { self.fetch_and_cache_block(cache, block) @@ -96,7 +95,7 @@ impl, const FORCE_CACHING: bool> if let Some(transaction) = self .client - .get_transaction_by_hash(transaction_hash) + .get_transaction_by_hash(*transaction_hash) .await? { self.block_by_hash(&transaction.block_hash.expect("Not a pending transaction")) @@ -107,7 +106,7 @@ impl, const FORCE_CACHING: bool> } /// Retrieves the instance's RPC client. - pub fn client(&self) -> &Arc { + pub fn client(&self) -> &Arc> { &self.client } @@ -166,7 +165,7 @@ impl, const FORCE_CACHING: bool> Ok(Some(receipt.clone())) } else if let Some(receipt) = self .client - .get_transaction_receipt(transaction_hash) + .get_transaction_receipt(*transaction_hash) .await? { Ok(Some({ @@ -195,7 +194,7 @@ impl, const FORCE_CACHING: bool> Ok(Some(difficulty)) } else if let Some(block) = self .client - .get_block_by_hash_with_transaction_data(hash) + .get_block_by_hash_with_transaction_data(*hash) .await? { let total_difficulty = block @@ -215,7 +214,7 @@ impl, const FORCE_CACHING: bool> async fn fetch_and_cache_block( &self, cache: RwLockUpgradableReadGuard<'_, SparseBlockchainStorage>, - block: remote::eth::Block, + block: edr_rpc_eth::Block, ) -> Result { let total_difficulty = block .total_difficulty @@ -243,8 +242,6 @@ impl, const FORCE_CACHING: bool> #[cfg(all(test, feature = "test-remote"))] mod tests { - - use edr_eth::remote::RpcClient; use edr_test_utils::env::get_alchemy_url; use super::*; @@ -254,7 +251,8 @@ mod tests { let tempdir = tempfile::tempdir().expect("can create tempdir"); let rpc_client = - RpcClient::new(&get_alchemy_url(), tempdir.path().to_path_buf(), None).expect("url ok"); + EthRpcClient::::new(&get_alchemy_url(), tempdir.path().to_path_buf(), None) + .expect("url ok"); // Latest block number is always unsafe to cache let block_number = rpc_client.block_number().await.unwrap(); diff --git a/crates/edr_evm/src/blockchain/storage/sparse.rs b/crates/edr_evm/src/blockchain/storage/sparse.rs index 363bcf25e..3be8b96f2 100644 --- a/crates/edr_evm/src/blockchain/storage/sparse.rs +++ b/crates/edr_evm/src/blockchain/storage/sparse.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use edr_eth::{ + log::{matches_address_filter, matches_topics_filter}, receipt::BlockReceipt, - remote::filter::{matches_address_filter, matches_topics_filter}, transaction::Transaction, Address, B256, U256, }; diff --git a/crates/edr_evm/src/state.rs b/crates/edr_evm/src/state.rs index 1a14f0c8a..397baa925 100644 --- a/crates/edr_evm/src/state.rs +++ b/crates/edr_evm/src/state.rs @@ -11,7 +11,8 @@ mod trie; use std::fmt::Debug; use dyn_clone::DynClone; -use edr_eth::{remote::RpcClientError, B256}; +use edr_eth::B256; +use edr_rpc_eth::client::RpcClientError; use revm::{db::StateRef, DatabaseCommit}; pub use self::{ diff --git a/crates/edr_evm/src/state/fork.rs b/crates/edr_evm/src/state/fork.rs index c12612993..4ba080fa8 100644 --- a/crates/edr_evm/src/state/fork.rs +++ b/crates/edr_evm/src/state/fork.rs @@ -1,6 +1,7 @@ use std::sync::Arc; -use edr_eth::{remote::RpcClient, trie::KECCAK_NULL_RLP, Address, B256, U256}; +use edr_eth::{trie::KECCAK_NULL_RLP, Address, B256, U256}; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; use revm::{ db::components::{State, StateRef}, @@ -29,7 +30,7 @@ impl ForkState { /// Constructs a new instance pub fn new( runtime: runtime::Handle, - rpc_client: Arc, + rpc_client: Arc>, hash_generator: Arc>, fork_block_number: u64, state_root: B256, @@ -226,7 +227,7 @@ mod tests { str::FromStr, }; - use edr_eth::remote::PreEip1898BlockSpec; + use edr_eth::PreEip1898BlockSpec; use edr_test_utils::env::get_alchemy_url; use super::*; @@ -251,8 +252,12 @@ mod tests { let tempdir = tempfile::tempdir().expect("can create tempdir"); let runtime = runtime::Handle::current(); - let rpc_client = RpcClient::new(&get_alchemy_url(), tempdir.path().to_path_buf(), None) - .expect("url ok"); + let rpc_client = EthRpcClient::::new( + &get_alchemy_url(), + tempdir.path().to_path_buf(), + None, + ) + .expect("url ok"); let block = rpc_client .get_block_by_number(PreEip1898BlockSpec::Number(FORK_BLOCK)) diff --git a/crates/edr_evm/src/state/overrides.rs b/crates/edr_evm/src/state/overrides.rs index a4798ddfe..eccb821d2 100644 --- a/crates/edr_evm/src/state/overrides.rs +++ b/crates/edr_evm/src/state/overrides.rs @@ -1,10 +1,7 @@ use std::fmt::Debug; -use edr_eth::{ - account::KECCAK_EMPTY, - remote::{AccountOverrideOptions, StateOverrideOptions}, - Address, B256, U256, -}; +use edr_eth::{account::KECCAK_EMPTY, Address, B256, U256}; +use edr_rpc_eth::{AccountOverrideOptions, StateOverrideOptions}; use revm::{ db::StateRef, primitives::{AccountInfo, Bytecode, HashMap}, diff --git a/crates/edr_evm/src/state/remote.rs b/crates/edr_evm/src/state/remote.rs index c24dfb8d4..3a4e6758c 100644 --- a/crates/edr_evm/src/state/remote.rs +++ b/crates/edr_evm/src/state/remote.rs @@ -3,9 +3,10 @@ mod cached; use std::sync::Arc; pub use cached::CachedRemoteState; -use edr_eth::{ - remote::{BlockSpec, PreEip1898BlockSpec, RpcClient, RpcClientError}, - Address, B256, U256, +use edr_eth::{Address, BlockSpec, PreEip1898BlockSpec, B256, U256}; +use edr_rpc_eth::{ + client::{EthRpcClient, RpcClientError}, + spec::EthRpcSpec, }; use revm::{ db::StateRef, @@ -18,7 +19,7 @@ use super::StateError; /// A state backed by a remote Ethereum node #[derive(Debug)] pub struct RemoteState { - client: Arc, + client: Arc>, runtime: runtime::Handle, block_number: u64, } @@ -26,7 +27,11 @@ pub struct RemoteState { impl RemoteState { /// Construct a new instance using an RPC client for a remote Ethereum node /// and a block number from which data will be pulled. - pub fn new(runtime: runtime::Handle, client: Arc, block_number: u64) -> Self { + pub fn new( + runtime: runtime::Handle, + client: Arc>, + block_number: u64, + ) -> Self { Self { client, runtime, @@ -73,7 +78,7 @@ impl StateRef for RemoteState { self.runtime .block_on( self.client - .get_account_info(&address, Some(BlockSpec::Number(self.block_number))), + .get_account_info(address, Some(BlockSpec::Number(self.block_number))), ) .map_err(StateError::Remote) })?)) @@ -89,7 +94,7 @@ impl StateRef for RemoteState { Ok(tokio::task::block_in_place(move || { self.runtime .block_on(self.client.get_storage_at( - &address, + address, index, Some(BlockSpec::Number(self.block_number)), )) @@ -117,7 +122,8 @@ mod tests { .expect("couldn't convert OsString into a String"); let rpc_client = - RpcClient::new(&alchemy_url, tempdir.path().to_path_buf(), None).expect("url ok"); + EthRpcClient::::new(&alchemy_url, tempdir.path().to_path_buf(), None) + .expect("url ok"); let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") .expect("failed to parse address"); diff --git a/crates/edr_evm/src/state/remote/cached.rs b/crates/edr_evm/src/state/remote/cached.rs index a391ee7ec..d74ea79ab 100644 --- a/crates/edr_evm/src/state/remote/cached.rs +++ b/crates/edr_evm/src/state/remote/cached.rs @@ -122,7 +122,7 @@ impl State for CachedRemoteState { mod tests { use std::{str::FromStr, sync::Arc}; - use edr_eth::remote::RpcClient; + use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use edr_test_utils::env::get_alchemy_url; use tokio::runtime; @@ -133,7 +133,8 @@ mod tests { let tempdir = tempfile::tempdir().expect("can create tempdir"); let rpc_client = - RpcClient::new(&get_alchemy_url(), tempdir.path().to_path_buf(), None).expect("url ok"); + EthRpcClient::::new(&get_alchemy_url(), tempdir.path().to_path_buf(), None) + .expect("url ok"); let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") .expect("failed to parse address"); diff --git a/crates/edr_evm/src/transaction/executable.rs b/crates/edr_evm/src/transaction/executable.rs index e5e2f2595..63582e8fe 100644 --- a/crates/edr_evm/src/transaction/executable.rs +++ b/crates/edr_evm/src/transaction/executable.rs @@ -2,7 +2,6 @@ use std::sync::OnceLock; use alloy_rlp::BufMut; use edr_eth::{ - remote, signature::Signature, transaction::{ Eip1559SignedTransaction, Eip155SignedTransaction, Eip2930SignedTransaction, @@ -179,10 +178,10 @@ pub enum TransactionConversionError { MissingReceiverAddress, } -impl TryFrom for ExecutableTransaction { +impl TryFrom for ExecutableTransaction { type Error = TransactionConversionError; - fn try_from(value: remote::eth::Transaction) -> Result { + fn try_from(value: edr_rpc_eth::Transaction) -> Result { let kind = if let Some(to) = &value.to { TxKind::Call(*to) } else { diff --git a/crates/edr_rpc_client/Cargo.toml b/crates/edr_rpc_client/Cargo.toml index aa375fc0d..e15a01ac7 100644 --- a/crates/edr_rpc_client/Cargo.toml +++ b/crates/edr_rpc_client/Cargo.toml @@ -25,6 +25,11 @@ tracing = { version = "0.1.37", default-features = false, features = ["attribute url = { version = "2.4.1", default-features = false } uuid = { version = "1.4.1", default-features = false, features = ["std", "v4"] } +[dev-dependencies] +edr_test_utils = { version = "0.3.5", path = "../edr_test_utils" } +tempfile = { version = "3.7.1", default-features = false } +walkdir = { version = "2.3.3", default-features = false } + [features] test-remote = [] tracing = ["dep:tracing", "dep:reqwest-tracing"] diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index 8d05ab743..04bbc37ec 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -512,6 +512,7 @@ impl RpcClient { ) -> Result { let cached_method = MethodT::Cached::try_from(&method).ok(); let read_cache_key = cached_method.and_then(CachedMethod::read_cache_key); + println!("read_cache_key: {read_cache_key:?}"); let request = self.serialize_request(&method)?; @@ -638,3 +639,235 @@ impl SerializedRequest { self.0.to_string() } } + +#[cfg(test)] +mod tests { + use std::ops::Deref; + + use hyper::StatusCode; + use tempfile::TempDir; + use walkdir::WalkDir; + + use self::cache::{key::CacheKeyVariant, KeyHasher}; + use super::*; + + #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] + #[serde(tag = "method", content = "params")] + enum TestMethod { + #[serde(rename = "eth_blockNumber", with = "edr_eth::serde::empty_params")] + BlockNumber(()), + #[serde(rename = "eth_chainId", with = "edr_eth::serde::empty_params")] + ChainId(()), + #[serde(rename = "net_version", with = "edr_eth::serde::empty_params")] + NetVersion(()), + } + + enum CachedTestMethod { + NetVersion, + } + + impl CachedTestMethod { + fn key_hasher(&self) -> cache::KeyHasher { + KeyHasher::new().hash_u8(self.cache_key_variant()) + } + } + + impl From for Option<()> { + fn from(_: CachedTestMethod) -> Self { + None + } + } + + impl<'method> TryFrom<&'method TestMethod> for CachedTestMethod { + type Error = (); + + fn try_from(method: &'method TestMethod) -> Result { + match method { + TestMethod::NetVersion(_) => Ok(Self::NetVersion), + _ => Err(()), + } + } + } + + impl CacheKeyVariant for CachedTestMethod { + fn cache_key_variant(&self) -> u8 { + match self { + Self::NetVersion => 0, + } + } + } + + impl CachedMethod for CachedTestMethod { + type MethodWithResolvableBlockTag = (); + + fn resolve_block_tag( + _method: Self::MethodWithResolvableBlockTag, + _block_number: u64, + ) -> Self { + unreachable!("No methods with resolvable block tags exist") + } + + fn read_cache_key(self) -> Option { + Some(ReadCacheKey::finalize(self.key_hasher())) + } + + fn write_cache_key(self) -> Option> { + Some(WriteCacheKey::finalize(self.key_hasher())) + } + } + + impl CacheableMethod for TestMethod { + type Cached<'method> = CachedTestMethod + where + Self: 'method; + + fn block_number_request() -> Self { + Self::BlockNumber(()) + } + + fn chain_id_request() -> Self { + Self::ChainId(()) + } + + #[cfg(feature = "tracing")] + fn name(&self) -> &'static str { + match self { + Self::BlockNumber(_) => "eth_blockNumber", + Self::ChainId(_) => "eth_chainId", + Self::NetVersion(_) => "net_version", + } + } + } + + struct TestRpcClient { + client: RpcClient, + + // Need to keep the tempdir around to prevent it from being deleted + // Only accessed when feature = "test-remote", hence the allow. + #[allow(dead_code)] + cache_dir: TempDir, + } + + impl TestRpcClient { + fn new(url: &str) -> Self { + let tempdir = TempDir::new().unwrap(); + Self { + client: RpcClient::new(url, tempdir.path().into(), None).expect("url ok"), + cache_dir: tempdir, + } + } + + fn files_in_cache(&self) -> Vec { + let mut files = Vec::new(); + for entry in WalkDir::new(&self.cache_dir) + .follow_links(true) + .into_iter() + .filter_map(Result::ok) + { + if entry.file_type().is_file() { + files.push(entry.path().to_owned()); + } + } + files + } + } + + impl Deref for TestRpcClient { + type Target = RpcClient; + + fn deref(&self) -> &Self::Target { + &self.client + } + } + + #[tokio::test] + async fn call_bad_api_key() { + let api_key = "invalid-api-key"; + let alchemy_url = format!("https://eth-mainnet.g.alchemy.com/v2/{api_key}"); + + let error = TestRpcClient::new(&alchemy_url) + .call::(TestMethod::BlockNumber(())) + .await + .expect_err("should have failed to interpret response as a Transaction"); + + assert!(!error.to_string().contains(api_key)); + + if let RpcClientError::HttpStatus(error) = error { + assert_eq!( + reqwest::Error::from(error).status(), + Some(StatusCode::from_u16(401).unwrap()) + ); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn call_failed_to_send_error() { + let alchemy_url = "https://xxxeth-mainnet.g.alchemy.com/"; + + let error = TestRpcClient::new(alchemy_url) + .call::(TestMethod::BlockNumber(())) + .await + .expect_err("should have failed to connect due to a garbage domain name"); + + if let RpcClientError::FailedToSend(error) = error { + assert!(error.to_string().contains("dns error")); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[cfg(feature = "test-remote")] + mod alchemy { + use edr_eth::U64; + use edr_test_utils::env::get_alchemy_url; + + use super::*; + + #[tokio::test] + async fn is_cacheable_block_number() { + let client = TestRpcClient::new(&get_alchemy_url()); + + let latest_block_number = client.block_number().await.unwrap(); + + { + assert!(client.cached_block_number.read().await.is_some()); + } + + // Latest block number is never cacheable + assert!(!client + .is_cacheable_block_number(latest_block_number) + .await + .unwrap()); + + assert!(client.is_cacheable_block_number(16220843).await.unwrap()); + } + + #[tokio::test] + async fn network_id_from_cache() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + assert_eq!(client.files_in_cache().len(), 0); + + // Populate cache + client + .call::(TestMethod::NetVersion(())) + .await + .expect("should have succeeded"); + + assert_eq!(client.files_in_cache().len(), 3); + + // Returned from cache + let network_id = client + .call::(TestMethod::NetVersion(())) + .await + .expect("should have succeeded"); + + assert_eq!(client.files_in_cache().len(), 3); + + assert_eq!(network_id, U64::ZERO); + } + } +} diff --git a/crates/edr_rpc_eth/Cargo.toml b/crates/edr_rpc_eth/Cargo.toml index 018dcb4ba..b7e35d1bf 100644 --- a/crates/edr_rpc_eth/Cargo.toml +++ b/crates/edr_rpc_eth/Cargo.toml @@ -19,6 +19,7 @@ edr_test_utils = { version = "0.3.5", path = "../edr_test_utils" } mockito = { version = "1.0.2", default-features = false } reqwest = { version = "0.11", default-features = false, features = ["blocking", "json"] } serde_json = { version = "1.0.89" } +serial_test = "2.0.0" tempfile = { version = "3.7.1", default-features = false } walkdir = { version = "2.3.3", default-features = false } diff --git a/crates/edr_rpc_eth/src/client.rs b/crates/edr_rpc_eth/src/client.rs index f6a4dd2f8..b254880bd 100644 --- a/crates/edr_rpc_eth/src/client.rs +++ b/crates/edr_rpc_eth/src/client.rs @@ -28,6 +28,7 @@ const MAX_PARALLEL_REQUESTS: usize = 20; // RpcSpecT::Block: Send + Sync, // RpcSpecT::Transaction: Send + Sync, +#[derive(Debug)] pub struct EthRpcClient { inner: RpcClient, _phantom: std::marker::PhantomData, @@ -50,6 +51,18 @@ impl EthRpcClient { }) } + /// Calls `eth_blockNumber` and returns the block number. + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + pub async fn block_number(&self) -> Result { + self.inner.block_number().await + } + + /// Calls `eth_chainId` and returns the chain ID. + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + pub async fn chain_id(&self) -> Result { + self.inner.chain_id().await + } + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] /// Calls `eth_feeHistory` and returns the fee history. pub async fn fee_history( @@ -291,6 +304,15 @@ impl EthRpcClient { .await } + /// Whether the block number should be cached based on its depth. + #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] + pub async fn is_cacheable_block_number( + &self, + block_number: u64, + ) -> Result { + self.inner.is_cacheable_block_number(block_number).await + } + /// Calls `net_version`. #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))] pub async fn network_id(&self) -> Result { diff --git a/crates/edr_rpc_eth/src/lib.rs b/crates/edr_rpc_eth/src/lib.rs index 6457c62a5..f5cb649ff 100644 --- a/crates/edr_rpc_eth/src/lib.rs +++ b/crates/edr_rpc_eth/src/lib.rs @@ -1,5 +1,5 @@ /// Types for Ethereum JSON-RPC blocks -pub mod block; +mod block; mod cacheable_method_invocation; /// Input type for `eth_call` and `eth_estimateGas` mod call_request; @@ -14,6 +14,6 @@ pub mod spec; mod transaction; pub use self::{ - call_request::CallRequest, r#override::*, request_methods::RequestMethod, + block::Block, call_request::CallRequest, r#override::*, request_methods::RequestMethod, transaction::Transaction, }; diff --git a/crates/edr_rpc_eth/tests/client.rs b/crates/edr_rpc_eth/tests/client.rs index 3fb9e5ae8..0cb8148e9 100644 --- a/crates/edr_rpc_eth/tests/client.rs +++ b/crates/edr_rpc_eth/tests/client.rs @@ -23,6 +23,20 @@ impl TestRpcClient { cache_dir: tempdir, } } + + fn files_in_cache(&self) -> Vec { + let mut files = Vec::new(); + for entry in WalkDir::new(&self.cache_dir) + .follow_links(true) + .into_iter() + .filter_map(Result::ok) + { + if entry.file_type().is_file() { + files.push(entry.path().to_owned()); + } + } + files + } } impl Deref for TestRpcClient { @@ -70,6 +84,7 @@ async fn send_request_body_400_status() { mod alchemy { use std::{fs::File, path::PathBuf}; + use edr_eth::{account::KECCAK_EMPTY, Address, BlockSpec, U256}; use edr_test_utils::env::get_alchemy_url; use futures::future::join_all; use walkdir::WalkDir; @@ -79,152 +94,6 @@ mod alchemy { // The maximum block number that Alchemy allows const MAX_BLOCK_NUMBER: u64 = u64::MAX >> 1; - impl TestRpcClient { - fn files_in_cache(&self) -> Vec { - let mut files = Vec::new(); - for entry in WalkDir::new(&self.cache_dir) - .follow_links(true) - .into_iter() - .filter_map(Result::ok) - { - if entry.file_type().is_file() { - files.push(entry.path().to_owned()); - } - } - files - } - } - - #[tokio::test] - async fn call_bad_api_key() { - let api_key = "invalid-api-key"; - let alchemy_url = format!("https://eth-mainnet.g.alchemy.com/v2/{api_key}"); - - let hash = - B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933022222") - .expect("failed to parse hash from string"); - - let error = TestRpcClient::new(&alchemy_url) - .call::>(RequestMethod::GetTransactionByHash(hash)) - .await - .expect_err("should have failed to interpret response as a Transaction"); - - assert!(!error.to_string().contains(api_key)); - - if let RpcClientError::HttpStatus(error) = error { - assert_eq!( - reqwest::Error::from(error).status(), - Some(StatusCode::from_u16(401).unwrap()) - ); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn call_failed_to_send_error() { - let alchemy_url = "https://xxxeth-mainnet.g.alchemy.com/"; - - let hash = - B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933051111") - .expect("failed to parse hash from string"); - - let error = TestRpcClient::new(alchemy_url) - .call::>(RequestMethod::GetTransactionByHash(hash)) - .await - .expect_err("should have failed to connect due to a garbage domain name"); - - if let RpcClientError::FailedToSend(error) = error { - assert!(error.to_string().contains("dns error")); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn test_is_cacheable_block_number() { - let client = TestRpcClient::new(&get_alchemy_url()); - - let latest_block_number = client.block_number().await.unwrap(); - - { - assert!(client.cached_block_number.read().await.is_some()); - } - - // Latest block number is never cacheable - assert!(!client - .is_cacheable_block_number(latest_block_number) - .await - .unwrap()); - - assert!(client.is_cacheable_block_number(16220843).await.unwrap()); - } - - #[tokio::test] - async fn get_account_info_works_from_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - let block_spec = BlockSpec::Number(16220843); - - assert_eq!(client.files_in_cache().len(), 0); - - // Populate cache - client - .get_account_info(&dai_address, Some(block_spec.clone())) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 3); - - // Returned from cache - let account_info = client - .get_account_info(&dai_address, Some(block_spec)) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 3); - - assert_eq!(account_info.balance, U256::ZERO); - assert_eq!(account_info.nonce, 1); - assert_ne!(account_info.code_hash, KECCAK_EMPTY); - assert!(account_info.code.is_some()); - } - - #[tokio::test] - async fn get_account_info_works_with_partial_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - let block_spec = BlockSpec::Number(16220843); - - assert_eq!(client.files_in_cache().len(), 0); - - // Populate cache - client - .get_transaction_count(&dai_address, Some(block_spec.clone())) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 1); - - let account_info = client - .get_account_info(&dai_address, Some(block_spec.clone())) - .await - .expect("should have succeeded"); - - assert_eq!(client.files_in_cache().len(), 3); - - assert_eq!(account_info.balance, U256::ZERO); - assert_eq!(account_info.nonce, 1); - assert_ne!(account_info.code_hash, KECCAK_EMPTY); - assert!(account_info.code.is_some()); - } - #[tokio::test] async fn get_account_info_unknown_block() { let alchemy_url = get_alchemy_url(); From ca5be33568e947220c04f35755b815cf9509f030 Mon Sep 17 00:00:00 2001 From: Wodann Date: Fri, 31 May 2024 06:20:46 +0000 Subject: [PATCH 07/18] fix: errors --- crates/edr_rpc_client/Cargo.toml | 2 +- crates/edr_rpc_client/src/client.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/edr_rpc_client/Cargo.toml b/crates/edr_rpc_client/Cargo.toml index e15a01ac7..34f2dbb0d 100644 --- a/crates/edr_rpc_client/Cargo.toml +++ b/crates/edr_rpc_client/Cargo.toml @@ -12,7 +12,7 @@ hyper = { version = "0.14.27", default-features = false } lazy_static = { version = "1.4.0", default-features = false } log = { version = "0.4.17", default-features = false } regex = "1.10.0" -reqwest = { version = "0.11", default-features = false, features = ["blocking", "json"] } +reqwest = { version = "0.11", features = ["blocking", "json"] } reqwest-middleware = { version = "0.2.4", default-features = false } reqwest-retry = { version = "0.3.0", default-features = false } reqwest-tracing = { version = "0.4.7", default-features = false, optional = true } diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index 04bbc37ec..7c2d64726 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -512,7 +512,6 @@ impl RpcClient { ) -> Result { let cached_method = MethodT::Cached::try_from(&method).ok(); let read_cache_key = cached_method.and_then(CachedMethod::read_cache_key); - println!("read_cache_key: {read_cache_key:?}"); let request = self.serialize_request(&method)?; From cd5af7bb2212ab6285a857c3ade606aee994e978 Mon Sep 17 00:00:00 2001 From: Wodann Date: Fri, 31 May 2024 06:29:30 +0000 Subject: [PATCH 08/18] fix: test --- crates/edr_rpc_client/src/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index 7c2d64726..a760f85eb 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -856,7 +856,7 @@ mod tests { .await .expect("should have succeeded"); - assert_eq!(client.files_in_cache().len(), 3); + assert_eq!(client.files_in_cache().len(), 1); // Returned from cache let network_id = client @@ -864,9 +864,9 @@ mod tests { .await .expect("should have succeeded"); - assert_eq!(client.files_in_cache().len(), 3); + assert_eq!(client.files_in_cache().len(), 1); - assert_eq!(network_id, U64::ZERO); + assert_eq!(network_id, U64::from(1u64)); } } } From 74e6711982445d9b01f828f3e69e727fcccb5963 Mon Sep 17 00:00:00 2001 From: Wodann Date: Fri, 31 May 2024 07:03:31 +0000 Subject: [PATCH 09/18] fix: compiler errors --- Cargo.lock | 3 +++ crates/edr_evm/tests/blockchain.rs | 5 ++-- crates/edr_evm/tests/issue_4974.rs | 5 ++-- crates/edr_evm/tests/issues.rs | 5 ++-- crates/edr_evm/tests/optimism.rs | 5 ++-- crates/edr_evm/tests/transaction.rs | 5 ++-- crates/edr_napi/Cargo.toml | 1 + crates/edr_napi/src/provider.rs | 2 +- crates/edr_napi/src/subscribe.rs | 4 ++-- crates/edr_provider/Cargo.toml | 1 + crates/edr_provider/src/data.rs | 23 ++++++++++--------- crates/edr_provider/src/error.rs | 6 ++--- crates/edr_provider/src/filter.rs | 2 +- crates/edr_provider/src/filter/criteria.rs | 4 ++-- crates/edr_provider/src/requests/debug.rs | 6 ++--- .../src/requests/eth/blockchain.rs | 2 +- .../edr_provider/src/requests/eth/blocks.rs | 16 +++++-------- crates/edr_provider/src/requests/eth/call.rs | 4 ++-- .../edr_provider/src/requests/eth/filter.rs | 7 ++---- crates/edr_provider/src/requests/eth/gas.rs | 14 ++++------- crates/edr_provider/src/requests/eth/state.rs | 2 +- .../src/requests/eth/transactions.rs | 15 ++++++------ crates/edr_provider/src/requests/methods.rs | 9 +++----- .../edr_provider/src/requests/validation.rs | 4 ++-- crates/edr_provider/src/subscribe.rs | 2 +- crates/edr_provider/src/test_utils.rs | 9 ++++---- crates/edr_provider/tests/eip4844.rs | 16 ++++++------- .../tests/eth_request_serialization.rs | 13 ++++------- crates/edr_provider/tests/issue_324.rs | 3 ++- crates/edr_provider/tests/issue_325.rs | 3 +-- crates/edr_provider/tests/issue_326.rs | 6 ++--- crates/edr_provider/tests/issue_356.rs | 3 ++- crates/edr_provider/tests/issue_361.rs | 5 ++-- crates/edr_provider/tests/timestamp.rs | 7 ++---- crates/edr_rpc_client/src/client.rs | 3 ++- .../src/{reqwest_error.rs => error.rs} | 1 + crates/edr_rpc_client/src/lib.rs | 8 +++---- crates/edr_rpc_eth/src/lib.rs | 2 ++ crates/tools/Cargo.toml | 1 + crates/tools/src/remote_block.rs | 4 ++-- crates/tools/src/scenario.rs | 2 +- 41 files changed, 112 insertions(+), 126 deletions(-) rename crates/edr_rpc_client/src/{reqwest_error.rs => error.rs} (99%) diff --git a/Cargo.lock b/Cargo.lock index ce1ca5fac..982781baa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1116,6 +1116,7 @@ dependencies = [ "edr_eth", "edr_evm", "edr_provider", + "edr_rpc_eth", "itertools 0.12.1", "k256", "lazy_static", @@ -1148,6 +1149,7 @@ dependencies = [ "edr_defaults", "edr_eth", "edr_evm", + "edr_rpc_eth", "edr_test_utils", "indexmap 2.2.6", "itertools 0.12.1", @@ -3614,6 +3616,7 @@ dependencies = [ "edr_eth", "edr_evm", "edr_provider", + "edr_rpc_eth", "flate2", "indicatif", "mimalloc", diff --git a/crates/edr_evm/tests/blockchain.rs b/crates/edr_evm/tests/blockchain.rs index 9dda3f2ce..46fb62906 100644 --- a/crates/edr_evm/tests/blockchain.rs +++ b/crates/edr_evm/tests/blockchain.rs @@ -36,15 +36,16 @@ const REMOTE_BLOCK_LAST_TRANSACTION_HASH: &str = async fn create_forked_dummy_blockchain( fork_block_number: Option, ) -> Box> { - use edr_eth::remote::RpcClient; use edr_evm::{ blockchain::ForkedBlockchain, state::IrregularState, HashMap, RandomHashGenerator, }; + use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use edr_test_utils::env::get_alchemy_url; use parking_lot::Mutex; let rpc_client = - RpcClient::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None).expect("url ok"); + EthRpcClient::::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None) + .expect("url ok"); let mut irregular_state = IrregularState::default(); Box::new( diff --git a/crates/edr_evm/tests/issue_4974.rs b/crates/edr_evm/tests/issue_4974.rs index 5d14dd6ea..77be996e6 100644 --- a/crates/edr_evm/tests/issue_4974.rs +++ b/crates/edr_evm/tests/issue_4974.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use edr_defaults::CACHE_DIR; -use edr_eth::{remote::RpcClient, HashMap, SpecId}; +use edr_eth::{HashMap, SpecId}; use edr_evm::{blockchain::ForkedBlockchain, state::IrregularState, RandomHashGenerator}; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use parking_lot::Mutex; use tokio::runtime; @@ -13,7 +14,7 @@ async fn issue_4974() -> anyhow::Result<()> { const FORK_BLOCK_NUMBER: u64 = 12_508_443; let url = "https://coston-api.flare.network/ext/bc/C/rpc"; - let rpc_client = RpcClient::new(url, CACHE_DIR.into(), None)?; + let rpc_client = EthRpcClient::::new(url, CACHE_DIR.into(), None)?; let mut irregular_state = IrregularState::default(); let state_root_generator = Arc::new(Mutex::new(RandomHashGenerator::with_seed("test"))); let hardfork_activation_overrides = HashMap::new(); diff --git a/crates/edr_evm/tests/issues.rs b/crates/edr_evm/tests/issues.rs index 2b6b737ca..6088eb1cd 100644 --- a/crates/edr_evm/tests/issues.rs +++ b/crates/edr_evm/tests/issues.rs @@ -3,11 +3,12 @@ use std::{str::FromStr, sync::Arc}; use edr_defaults::CACHE_DIR; -use edr_eth::{remote::RpcClient, Address, U256}; +use edr_eth::{Address, U256}; use edr_evm::{ state::{AccountModifierFn, ForkState, StateDebug}, RandomHashGenerator, }; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use edr_test_utils::env::get_alchemy_url; use parking_lot::Mutex; use tokio::runtime; @@ -19,7 +20,7 @@ async fn issue_4984() -> anyhow::Result<()> { let contract_address = Address::from_str(TEST_CONTRACT_ADDRESS).unwrap(); - let rpc_client = RpcClient::new( + let rpc_client = EthRpcClient::::new( &get_alchemy_url().replace("mainnet", "sepolia"), CACHE_DIR.into(), None, diff --git a/crates/edr_evm/tests/optimism.rs b/crates/edr_evm/tests/optimism.rs index e77a00f69..bbd772046 100644 --- a/crates/edr_evm/tests/optimism.rs +++ b/crates/edr_evm/tests/optimism.rs @@ -3,12 +3,13 @@ use std::sync::Arc; use edr_defaults::CACHE_DIR; -use edr_eth::{remote::RpcClient, HashMap, SpecId}; +use edr_eth::{HashMap, SpecId}; use edr_evm::{ blockchain::{Blockchain, ForkedBlockchain}, state::IrregularState, RandomHashGenerator, }; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use edr_test_utils::env::get_alchemy_url; use parking_lot::Mutex; use tokio::runtime; @@ -18,7 +19,7 @@ async fn unknown_transaction_types() -> anyhow::Result<()> { const BLOCK_NUMBER_WITH_TRANSACTIONS: u64 = 117_156_000; let url = get_alchemy_url().replace("eth-", "opt-"); - let rpc_client = RpcClient::new(&url, CACHE_DIR.into(), None)?; + let rpc_client = EthRpcClient::::new(&url, CACHE_DIR.into(), None)?; let mut irregular_state = IrregularState::default(); let state_root_generator = Arc::new(Mutex::new(RandomHashGenerator::with_seed("test"))); let hardfork_activation_overrides = HashMap::new(); diff --git a/crates/edr_evm/tests/transaction.rs b/crates/edr_evm/tests/transaction.rs index e6d329e88..01f8b0e31 100644 --- a/crates/edr_evm/tests/transaction.rs +++ b/crates/edr_evm/tests/transaction.rs @@ -10,13 +10,14 @@ mod alchemy { async fn []() { use edr_eth::{ transaction::Transaction, - remote::{RpcClient, PreEip1898BlockSpec}, + PreEip1898BlockSpec, B256 }; use edr_evm::ExecutableTransaction; + use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use edr_test_utils::env::get_alchemy_url; - let client = RpcClient::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None).expect("url ok"); + let client = EthRpcClient::::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None).expect("url ok"); let block = client .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::Number($block_number)) diff --git a/crates/edr_napi/Cargo.toml b/crates/edr_napi/Cargo.toml index daee3155e..d131215aa 100644 --- a/crates/edr_napi/Cargo.toml +++ b/crates/edr_napi/Cargo.toml @@ -20,6 +20,7 @@ edr_defaults = { version = "0.3.5", path = "../edr_defaults" } edr_evm = { version = "0.3.5", path = "../edr_evm", features = ["tracing"]} edr_eth = { version = "0.3.5", path = "../edr_eth" } edr_provider = { version = "0.3.5", path = "../edr_provider" } +edr_rpc_eth = { version = "0.3.5", path = "../edr_rpc_eth" } serde_json = { version = "1.0.85", default-features = false, features = ["alloc"] } thiserror = { version = "1.0.37", default-features = false } tracing = { version = "0.1.37", default-features = false, features = ["std"] } diff --git a/crates/edr_napi/src/provider.rs b/crates/edr_napi/src/provider.rs index 541437426..df854087f 100644 --- a/crates/edr_napi/src/provider.rs +++ b/crates/edr_napi/src/provider.rs @@ -2,8 +2,8 @@ mod config; use std::sync::Arc; -use edr_eth::remote::jsonrpc; use edr_provider::{time::CurrentTime, InvalidRequestReason}; +use edr_rpc_eth::jsonrpc; use napi::{tokio::runtime, Env, JsFunction, JsObject, Status}; use napi_derive::napi; diff --git a/crates/edr_napi/src/subscribe.rs b/crates/edr_napi/src/subscribe.rs index 15963274f..476e5747c 100644 --- a/crates/edr_napi/src/subscribe.rs +++ b/crates/edr_napi/src/subscribe.rs @@ -1,4 +1,4 @@ -use edr_eth::{remote::eth, B256}; +use edr_eth::B256; use napi::{ bindgen_prelude::BigInt, threadsafe_function::{ @@ -28,7 +28,7 @@ impl SubscriberCallback { let result = match ctx.value.result { edr_provider::SubscriptionEventData::Logs(logs) => ctx.env.to_js_value(&logs), edr_provider::SubscriptionEventData::NewHeads(block) => { - let block = eth::Block::::from(block); + let block = edr_rpc_eth::Block::::from(block); ctx.env.to_js_value(&block) } edr_provider::SubscriptionEventData::NewPendingTransactions(tx_hash) => { diff --git a/crates/edr_provider/Cargo.toml b/crates/edr_provider/Cargo.toml index 44c330a94..605381da3 100644 --- a/crates/edr_provider/Cargo.toml +++ b/crates/edr_provider/Cargo.toml @@ -12,6 +12,7 @@ dyn-clone = { version = "1.0.13", default-features = false } edr_defaults = { version = "0.3.5", path = "../edr_defaults" } edr_eth = { version = "0.3.5", path = "../edr_eth", features = ["rand"] } edr_evm = { version = "0.3.5", path = "../edr_evm", features = ["tracing"] } +edr_rpc_eth = { version = "0.3.5", path = "../edr_rpc_eth" } indexmap = { version = "2.0.0", default-features = false, features = ["std"] } itertools = { version = "0.12.0", default-features = false, features = ["use_alloc"] } k256 = { version = "0.13.1", default-features = false, features = ["arithmetic", "ecdsa", "pem", "pkcs8", "precomputed-tables", "std"] } diff --git a/crates/edr_provider/src/data.rs b/crates/edr_provider/src/data.rs index 3f5e8faeb..d09b16c38 100644 --- a/crates/edr_provider/src/data.rs +++ b/crates/edr_provider/src/data.rs @@ -18,18 +18,14 @@ use edr_eth::{ calculate_next_base_fee_per_blob_gas, calculate_next_base_fee_per_gas, miner_reward, BlobGas, BlockOptions, }, + fee_history::FeeHistoryResult, + filter::{FilteredEvents, LogOutput, SubscriptionType}, log::FilterLog, receipt::BlockReceipt, - remote::{ - client::{HeaderMap, HttpError}, - eth::FeeHistoryResult, - filter::{FilteredEvents, LogOutput, SubscriptionType}, - BlockSpec, BlockTag, Eip1898BlockSpec, RpcClient, RpcClientError, - }, reward_percentile::RewardPercentile, signature::{RecoveryMessage, Signature}, transaction::{Transaction, TransactionRequestAndSender, TransactionType}, - Address, Bytes, SpecId, B256, U256, + Address, BlockSpec, BlockTag, Bytes, Eip1898BlockSpec, SpecId, B256, U256, }; use edr_evm::{ blockchain::{ @@ -50,6 +46,11 @@ use edr_evm::{ OrderedTransaction, RandomHashGenerator, StorageSlot, SyncBlock, TracerEip3155, TxEnv, KECCAK_EMPTY, }; +use edr_rpc_eth::{ + client::{EthRpcClient, HeaderMap, RpcClientError}, + error::HttpError, + spec::EthRpcSpec, +}; use gas::gas_used_ratio; use indexmap::IndexMap; use itertools::izip; @@ -174,7 +175,7 @@ pub struct ProviderData, // Must be set if the provider is created with a fork config. // Hack to get around the type erasure with the dyn blockchain trait. - rpc_client: Option>, + rpc_client: Option>>, instance_id: B256, is_auto_mining: bool, next_block_base_fee_per_gas: Option, @@ -2332,7 +2333,7 @@ fn block_time_offset_seconds( struct BlockchainAndState { blockchain: Box>, fork_metadata: Option, - rpc_client: Option>, + rpc_client: Option>>, state: Box>, irregular_state: IrregularState, prev_randao_generator: RandomHashGenerator, @@ -2359,7 +2360,7 @@ fn create_blockchain_and_state( .map(|headers| HeaderMap::try_from(headers).map_err(CreationError::InvalidHttpHeaders)) .transpose()?; - let rpc_client = Arc::new(RpcClient::new( + let rpc_client = Arc::new(EthRpcClient::::new( &fork_config.json_rpc_url, config.cache_dir.clone(), http_headers.clone(), @@ -2699,8 +2700,8 @@ mod tests { use alloy_sol_types::{sol, SolCall}; use anyhow::Context; - use edr_eth::remote::eth::CallRequest; use edr_evm::{hex, MineOrdering, TransactionError}; + use edr_rpc_eth::CallRequest; use edr_test_utils::env::get_alchemy_url; use serde_json::json; diff --git a/crates/edr_provider/src/error.rs b/crates/edr_provider/src/error.rs index 6457fd5f7..2b3962aa2 100644 --- a/crates/edr_provider/src/error.rs +++ b/crates/edr_provider/src/error.rs @@ -2,10 +2,7 @@ use core::fmt::Debug; use std::num::TryFromIntError; use alloy_sol_types::{ContractError, SolInterface}; -use edr_eth::{ - remote::{filter::SubscriptionType, jsonrpc, BlockSpec, BlockTag, RpcClientError}, - Address, Bytes, SpecId, B256, U256, -}; +use edr_eth::{filter::SubscriptionType, Address, BlockSpec, BlockTag, Bytes, SpecId, B256, U256}; use edr_evm::{ blockchain::BlockchainError, hex, @@ -14,6 +11,7 @@ use edr_evm::{ DebugTraceError, ExecutionResult, HaltReason, MemPoolAddTransactionError, MineBlockError, MineTransactionError, OutOfGasError, TransactionCreationError, TransactionError, }; +use edr_rpc_eth::{client::RpcClientError, jsonrpc}; use crate::{data::CreationError, IntervalConfigConversionError}; diff --git a/crates/edr_provider/src/filter.rs b/crates/edr_provider/src/filter.rs index a11a01fd3..de2cd3f83 100644 --- a/crates/edr_provider/src/filter.rs +++ b/crates/edr_provider/src/filter.rs @@ -6,7 +6,7 @@ use std::{ }; use edr_eth::{ - remote::filter::{FilteredEvents, LogOutput, SubscriptionType}, + filter::{FilteredEvents, LogOutput, SubscriptionType}, B256, }; diff --git a/crates/edr_provider/src/filter/criteria.rs b/crates/edr_provider/src/filter/criteria.rs index 51f9d58cf..494df1d33 100644 --- a/crates/edr_provider/src/filter/criteria.rs +++ b/crates/edr_provider/src/filter/criteria.rs @@ -1,6 +1,6 @@ use edr_eth::{ - log::FilterLog, - remote::filter::{matches_address_filter, matches_topics_filter, LogOutput}, + filter::LogOutput, + log::{matches_address_filter, matches_topics_filter, FilterLog}, Address, Bloom, BloomInput, B256, }; use edr_evm::HashSet; diff --git a/crates/edr_provider/src/requests/debug.rs b/crates/edr_provider/src/requests/debug.rs index 4c7e4161f..4c7854fed 100644 --- a/crates/edr_provider/src/requests/debug.rs +++ b/crates/edr_provider/src/requests/debug.rs @@ -1,10 +1,8 @@ use core::fmt::Debug; -use edr_eth::{ - remote::{eth::CallRequest, BlockSpec}, - B256, -}; +use edr_eth::{BlockSpec, B256}; use edr_evm::{state::StateOverrides, DebugTraceResult}; +use edr_rpc_eth::CallRequest; use serde::{Deserialize, Deserializer}; use crate::{ diff --git a/crates/edr_provider/src/requests/eth/blockchain.rs b/crates/edr_provider/src/requests/eth/blockchain.rs index 0dc3e59f0..10fa9a2c7 100644 --- a/crates/edr_provider/src/requests/eth/blockchain.rs +++ b/crates/edr_provider/src/requests/eth/blockchain.rs @@ -1,6 +1,6 @@ use core::fmt::Debug; -use edr_eth::{remote::BlockSpec, Address, U256, U64}; +use edr_eth::{Address, BlockSpec, U256, U64}; use crate::{ data::ProviderData, requests::validation::validate_post_merge_block_tags, time::TimeSinceEpoch, diff --git a/crates/edr_provider/src/requests/eth/blocks.rs b/crates/edr_provider/src/requests/eth/blocks.rs index c3b7daa4e..46aeea3cc 100644 --- a/crates/edr_provider/src/requests/eth/blocks.rs +++ b/crates/edr_provider/src/requests/eth/blocks.rs @@ -1,11 +1,7 @@ use core::fmt::Debug; use std::sync::Arc; -use edr_eth::{ - remote::{eth, BlockSpec, PreEip1898BlockSpec}, - transaction::Transaction, - SpecId, B256, U256, U64, -}; +use edr_eth::{transaction::Transaction, BlockSpec, PreEip1898BlockSpec, SpecId, B256, U256, U64}; use edr_evm::{blockchain::BlockchainError, SyncBlock}; use crate::{ @@ -19,14 +15,14 @@ use crate::{ #[serde(untagged)] pub enum HashOrTransaction { Hash(B256), - Transaction(eth::Transaction), + Transaction(edr_rpc_eth::Transaction), } pub fn handle_get_block_by_hash_request( data: &ProviderData, block_hash: B256, transaction_detail_flag: bool, -) -> Result>, ProviderError> { +) -> Result>, ProviderError> { data.block_by_hash(&block_hash)? .map(|block| { let total_difficulty = data.total_difficulty_by_hash(block.hash())?; @@ -46,7 +42,7 @@ pub fn handle_get_block_by_number_request, block_spec: PreEip1898BlockSpec, transaction_detail_flag: bool, -) -> Result>, ProviderError> { +) -> Result>, ProviderError> { block_by_number(data, &block_spec.into())? .map( |BlockByNumberResult { @@ -143,7 +139,7 @@ fn block_to_rpc_output( pending: bool, total_difficulty: Option, transaction_detail_flag: bool, -) -> Result, ProviderError> { +) -> Result, ProviderError> { let header = block.header(); let transactions: Vec = if transaction_detail_flag { @@ -173,7 +169,7 @@ fn block_to_rpc_output( let nonce = if pending { None } else { Some(header.nonce) }; let number = if pending { None } else { Some(header.number) }; - Ok(eth::Block { + Ok(edr_rpc_eth::Block { hash: Some(*block.hash()), parent_hash: header.parent_hash, sha3_uncles: header.ommers_hash, diff --git a/crates/edr_provider/src/requests/eth/call.rs b/crates/edr_provider/src/requests/eth/call.rs index 7e35295cd..e4d531b7b 100644 --- a/crates/edr_provider/src/requests/eth/call.rs +++ b/crates/edr_provider/src/requests/eth/call.rs @@ -1,14 +1,14 @@ use core::fmt::Debug; use edr_eth::{ - remote::{eth::CallRequest, BlockSpec, StateOverrideOptions}, transaction::{ Eip1559TransactionRequest, Eip155TransactionRequest, Eip2930TransactionRequest, TransactionRequest, }, - Bytes, SpecId, U256, + BlockSpec, Bytes, SpecId, U256, }; use edr_evm::{state::StateOverrides, trace::Trace, ExecutableTransaction}; +use edr_rpc_eth::{CallRequest, StateOverrideOptions}; use crate::{ data::ProviderData, requests::validation::validate_call_request, time::TimeSinceEpoch, diff --git a/crates/edr_provider/src/requests/eth/filter.rs b/crates/edr_provider/src/requests/eth/filter.rs index f1159b841..d529766db 100644 --- a/crates/edr_provider/src/requests/eth/filter.rs +++ b/crates/edr_provider/src/requests/eth/filter.rs @@ -2,11 +2,8 @@ use core::fmt::Debug; use std::iter; use edr_eth::{ - remote::{ - filter::{FilteredEvents, LogFilterOptions, LogOutput, OneOrMore, SubscriptionType}, - BlockSpec, BlockTag, Eip1898BlockSpec, - }, - U256, + filter::{FilteredEvents, LogFilterOptions, LogOutput, OneOrMore, SubscriptionType}, + BlockSpec, BlockTag, Eip1898BlockSpec, U256, }; use edr_evm::HashSet; diff --git a/crates/edr_provider/src/requests/eth/gas.rs b/crates/edr_provider/src/requests/eth/gas.rs index 0e08a6bdb..f9ed087a9 100644 --- a/crates/edr_provider/src/requests/eth/gas.rs +++ b/crates/edr_provider/src/requests/eth/gas.rs @@ -1,14 +1,11 @@ use core::fmt::Debug; use edr_eth::{ - remote::{ - eth::{CallRequest, FeeHistoryResult}, - BlockSpec, - }, - reward_percentile::RewardPercentile, - SpecId, U256, U64, + fee_history::FeeHistoryResult, reward_percentile::RewardPercentile, BlockSpec, SpecId, U256, + U64, }; use edr_evm::{state::StateOverrides, trace::Trace, ExecutableTransaction}; +use edr_rpc_eth::CallRequest; use super::resolve_call_request_inner; use crate::{ @@ -151,10 +148,7 @@ fn resolve_estimate_gas_request, block_hash: B256, index: U256, -) -> Result, ProviderError> { +) -> Result, ProviderError> { let index = rpc_index_to_usize(&index)?; data.block_by_hash(&block_hash)? @@ -50,7 +49,7 @@ pub fn handle_get_transaction_by_block_spec_and_index< data: &mut ProviderData, block_spec: PreEip1898BlockSpec, index: U256, -) -> Result, ProviderError> { +) -> Result, ProviderError> { validate_post_merge_block_tags(data.spec_id(), &block_spec)?; let index = rpc_index_to_usize(&index)?; @@ -74,7 +73,7 @@ pub fn handle_get_transaction_by_block_spec_and_index< pub fn handle_pending_transactions( data: &ProviderData, -) -> Result, ProviderError> { +) -> Result, ProviderError> { let spec_id = data.spec_id(); data.pending_transactions() .map(|pending_transaction| { @@ -99,7 +98,7 @@ fn rpc_index_to_usize( pub fn handle_get_transaction_by_hash( data: &ProviderData, transaction_hash: B256, -) -> Result, ProviderError> { +) -> Result, ProviderError> { data.transaction_by_hash(&transaction_hash)? .map(|tx| transaction_to_rpc_result(tx, data.spec_id())) .transpose() @@ -150,7 +149,7 @@ fn transaction_from_block( pub fn transaction_to_rpc_result( transaction_and_block: TransactionAndBlock, spec_id: SpecId, -) -> Result> { +) -> Result> { fn gas_price_for_post_eip1559( signed_transaction: &SignedTransaction, block: Option<&Arc>>, @@ -226,7 +225,7 @@ pub fn transaction_to_rpc_result( block_data.as_ref().map(|bd| bd.transaction_index) }; - Ok(remote::eth::Transaction { + Ok(edr_rpc_eth::Transaction { hash: *signed_transaction.transaction_hash(), nonce: signed_transaction.nonce(), block_hash, diff --git a/crates/edr_provider/src/requests/methods.rs b/crates/edr_provider/src/requests/methods.rs index 5ee9a6c3d..45c7d383e 100644 --- a/crates/edr_provider/src/requests/methods.rs +++ b/crates/edr_provider/src/requests/methods.rs @@ -1,14 +1,11 @@ use alloy_dyn_abi::eip712::TypedData; use edr_eth::{ - remote::{ - eth::CallRequest, - filter::{LogFilterOptions, SubscriptionType}, - BlockSpec, PreEip1898BlockSpec, StateOverrideOptions, - }, + filter::{LogFilterOptions, SubscriptionType}, serde::{optional_single_to_sequence, sequence_to_optional_single}, transaction::EthTransactionRequest, - Address, Bytes, B256, U256, U64, + Address, BlockSpec, Bytes, PreEip1898BlockSpec, B256, U256, U64, }; +use edr_rpc_eth::{CallRequest, StateOverrideOptions}; use super::serde::RpcAddress; use crate::requests::{ diff --git a/crates/edr_provider/src/requests/validation.rs b/crates/edr_provider/src/requests/validation.rs index 65b895501..17ff503e6 100644 --- a/crates/edr_provider/src/requests/validation.rs +++ b/crates/edr_provider/src/requests/validation.rs @@ -2,11 +2,11 @@ use core::fmt::Debug; use edr_eth::{ access_list::AccessListItem, - remote::{eth::CallRequest, BlockSpec, BlockTag, PreEip1898BlockSpec}, transaction::{EthTransactionRequest, SignedTransaction}, - Address, SpecId, B256, U256, + Address, BlockSpec, BlockTag, PreEip1898BlockSpec, SpecId, B256, U256, }; use edr_evm::Bytes; +use edr_rpc_eth::CallRequest; use crate::ProviderError; diff --git a/crates/edr_provider/src/subscribe.rs b/crates/edr_provider/src/subscribe.rs index b10169f32..9850133d5 100644 --- a/crates/edr_provider/src/subscribe.rs +++ b/crates/edr_provider/src/subscribe.rs @@ -1,5 +1,5 @@ use dyn_clone::DynClone; -use edr_eth::{remote::filter::LogOutput, B256, U256}; +use edr_eth::{filter::LogOutput, B256, U256}; use edr_evm::{blockchain::BlockchainError, BlockAndTotalDifficulty}; /// Subscription event. diff --git a/crates/edr_provider/src/test_utils.rs b/crates/edr_provider/src/test_utils.rs index d4c479a6a..8e2b4b336 100644 --- a/crates/edr_provider/src/test_utils.rs +++ b/crates/edr_provider/src/test_utils.rs @@ -4,13 +4,12 @@ use anyhow::anyhow; use edr_eth::{ block::{miner_reward, BlobGas, BlockOptions}, receipt::BlockReceipt, - remote::{PreEip1898BlockSpec, RpcClient}, signature::secret_key_from_str, spec::chain_hardfork_activations, transaction::EthTransactionRequest, trie::KECCAK_NULL_RLP, withdrawal::Withdrawal, - Address, Bytes, HashMap, SpecId, B256, U256, + Address, Bytes, HashMap, PreEip1898BlockSpec, SpecId, B256, U256, }; use edr_evm::{ alloy_primitives::U160, @@ -19,6 +18,7 @@ use edr_evm::{ Block, BlockBuilder, CfgEnv, CfgEnvWithHandlerCfg, DebugContext, ExecutionResultWithContext, RandomHashGenerator, RemoteBlock, }; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use super::*; use crate::{config::MiningConfig, requests::hardhat::rpc_types::ForkConfig}; @@ -139,7 +139,8 @@ pub async fn run_full_block(url: String, block_number: u64, chain_id: u64) -> an })); let replay_block = { - let rpc_client = RpcClient::new(&url, default_config.cache_dir.clone(), None)?; + let rpc_client = + EthRpcClient::::new(&url, default_config.cache_dir.clone(), None)?; let block = rpc_client .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::Number(block_number)) @@ -148,7 +149,7 @@ pub async fn run_full_block(url: String, block_number: u64, chain_id: u64) -> an RemoteBlock::new(block, Arc::new(rpc_client), runtime.clone())? }; - let rpc_client = RpcClient::new(&url, default_config.cache_dir.clone(), None)?; + let rpc_client = EthRpcClient::::new(&url, default_config.cache_dir.clone(), None)?; let mut irregular_state = IrregularState::default(); let state_root_generator = Arc::new(parking_lot::Mutex::new(RandomHashGenerator::with_seed( edr_defaults::STATE_ROOT_HASH_SEED, diff --git a/crates/edr_provider/tests/eip4844.rs b/crates/edr_provider/tests/eip4844.rs index 89c4701a4..811f32a63 100644 --- a/crates/edr_provider/tests/eip4844.rs +++ b/crates/edr_provider/tests/eip4844.rs @@ -4,7 +4,6 @@ use std::{convert::Infallible, str::FromStr}; use edr_defaults::SECRET_KEYS; use edr_eth::{ - remote::{self, eth::CallRequest, PreEip1898BlockSpec}, rlp::{self, Decodable}, signature::{secret_key_from_str, secret_key_to_address}, transaction::{ @@ -14,7 +13,7 @@ use edr_eth::{ }, Eip4844TransactionRequest, EthTransactionRequest, SignedTransaction, Transaction, }, - AccountInfo, Address, Bytes, SpecId, B256, U256, + AccountInfo, Address, Bytes, PreEip1898BlockSpec, SpecId, B256, U256, }; use edr_evm::{EnvKzgSettings, ExecutableTransaction, KECCAK_EMPTY}; use edr_provider::{ @@ -22,6 +21,7 @@ use edr_provider::{ time::CurrentTime, MethodInvocation, NoopLogger, Provider, ProviderError, ProviderRequest, }; +use edr_rpc_eth::CallRequest; use tokio::runtime; /// Helper struct to modify the pooled transaction from the value in @@ -361,7 +361,7 @@ async fn get_transaction() -> anyhow::Result<()> { MethodInvocation::GetTransactionByHash(transaction_hash), ))?; - let transaction: remote::eth::Transaction = serde_json::from_value(result.result)?; + let transaction: edr_rpc_eth::Transaction = serde_json::from_value(result.result)?; let transaction = ExecutableTransaction::try_from(transaction)?; assert_eq!(transaction.into_inner().0, expected); @@ -409,7 +409,7 @@ async fn block_header() -> anyhow::Result<()> { MethodInvocation::GetBlockByNumber(PreEip1898BlockSpec::latest(), false), ))?; - let first_block: remote::eth::Block = serde_json::from_value(result.result)?; + let first_block: edr_rpc_eth::Block = serde_json::from_value(result.result)?; assert_eq!(first_block.blob_gas_used, Some(BYTES_PER_BLOB as u64)); assert_eq!( @@ -433,7 +433,7 @@ async fn block_header() -> anyhow::Result<()> { MethodInvocation::GetBlockByNumber(PreEip1898BlockSpec::latest(), false), ))?; - let second_block: remote::eth::Block = serde_json::from_value(result.result)?; + let second_block: edr_rpc_eth::Block = serde_json::from_value(result.result)?; assert_eq!(second_block.blob_gas_used, Some(4 * BYTES_PER_BLOB as u64)); assert_eq!( @@ -457,7 +457,7 @@ async fn block_header() -> anyhow::Result<()> { MethodInvocation::GetBlockByNumber(PreEip1898BlockSpec::latest(), false), ))?; - let third_block: remote::eth::Block = serde_json::from_value(result.result)?; + let third_block: edr_rpc_eth::Block = serde_json::from_value(result.result)?; assert_eq!(third_block.blob_gas_used, Some(5 * BYTES_PER_BLOB as u64)); assert_eq!( @@ -475,7 +475,7 @@ async fn block_header() -> anyhow::Result<()> { MethodInvocation::GetBlockByNumber(PreEip1898BlockSpec::latest(), false), ))?; - let fourth_block: remote::eth::Block = serde_json::from_value(result.result)?; + let fourth_block: edr_rpc_eth::Block = serde_json::from_value(result.result)?; assert_eq!(fourth_block.blob_gas_used, Some(0u64)); assert_eq!( @@ -494,7 +494,7 @@ async fn block_header() -> anyhow::Result<()> { MethodInvocation::GetBlockByNumber(PreEip1898BlockSpec::latest(), false), ))?; - let fifth_block: remote::eth::Block = serde_json::from_value(result.result)?; + let fifth_block: edr_rpc_eth::Block = serde_json::from_value(result.result)?; assert_eq!(fifth_block.blob_gas_used, Some(0u64)); assert_eq!( diff --git a/crates/edr_provider/tests/eth_request_serialization.rs b/crates/edr_provider/tests/eth_request_serialization.rs index ea932fcd7..5a2ef73b5 100644 --- a/crates/edr_provider/tests/eth_request_serialization.rs +++ b/crates/edr_provider/tests/eth_request_serialization.rs @@ -1,16 +1,13 @@ mod common; use edr_eth::{ - remote::{ - eth::CallRequest, - filter::{LogFilterOptions, LogOutput, OneOrMore}, - BlockSpec, BlockTag, PreEip1898BlockSpec, - }, + filter::{LogFilterOptions, LogOutput, OneOrMore}, transaction::EthTransactionRequest, - Address, Bytes, B256, U256, U64, + Address, BlockSpec, BlockTag, Bytes, PreEip1898BlockSpec, B256, U256, U64, }; use edr_evm::alloy_primitives::U160; use edr_provider::{IntervalConfigRequest, MethodInvocation, U64OrUsize}; +use edr_rpc_eth::CallRequest; use crate::common::{ help_test_method_invocation_serde, help_test_method_invocation_serde_with_expected, @@ -368,14 +365,14 @@ macro_rules! impl_serde_eth_subscribe_tests { paste::item! { #[test] fn []() { - use edr_eth::remote::filter::SubscriptionType; + use edr_eth::filter::SubscriptionType; help_test_method_invocation_serde(MethodInvocation::Subscribe($variant, None)); } #[test] fn []() { - use edr_eth::remote::filter::SubscriptionType; + use edr_eth::filter::SubscriptionType; help_test_method_invocation_serde(MethodInvocation::Subscribe($variant, Some(LogFilterOptions { from_block: Some(BlockSpec::Number(1000)), diff --git a/crates/edr_provider/tests/issue_324.rs b/crates/edr_provider/tests/issue_324.rs index 84b7310d6..4dbe1e744 100644 --- a/crates/edr_provider/tests/issue_324.rs +++ b/crates/edr_provider/tests/issue_324.rs @@ -2,11 +2,12 @@ use std::str::FromStr; -use edr_eth::{remote::eth::CallRequest, Address, Bytes, SpecId, U256}; +use edr_eth::{Address, Bytes, SpecId, U256}; use edr_provider::{ hardhat_rpc_types::ForkConfig, test_utils::create_test_config_with_fork, time::CurrentTime, MethodInvocation, NoopLogger, Provider, ProviderRequest, }; +use edr_rpc_eth::CallRequest; use edr_test_utils::env::get_alchemy_url; use tokio::runtime; diff --git a/crates/edr_provider/tests/issue_325.rs b/crates/edr_provider/tests/issue_325.rs index b48898a15..c976206be 100644 --- a/crates/edr_provider/tests/issue_325.rs +++ b/crates/edr_provider/tests/issue_325.rs @@ -1,8 +1,7 @@ #![cfg(feature = "test-utils")] use edr_eth::{ - remote::PreEip1898BlockSpec, transaction::EthTransactionRequest, AccountInfo, Address, SpecId, - B256, + transaction::EthTransactionRequest, AccountInfo, Address, PreEip1898BlockSpec, SpecId, B256, }; use edr_evm::KECCAK_EMPTY; use edr_provider::{ diff --git a/crates/edr_provider/tests/issue_326.rs b/crates/edr_provider/tests/issue_326.rs index 7272a4b2c..f2d959a7f 100644 --- a/crates/edr_provider/tests/issue_326.rs +++ b/crates/edr_provider/tests/issue_326.rs @@ -2,16 +2,14 @@ use std::str::FromStr; -use edr_eth::{ - remote::eth::CallRequest, transaction::EthTransactionRequest, AccountInfo, Address, SpecId, - U256, -}; +use edr_eth::{transaction::EthTransactionRequest, AccountInfo, Address, SpecId, U256}; use edr_evm::KECCAK_EMPTY; use edr_provider::{ test_utils::{create_test_config_with_fork, one_ether}, time::CurrentTime, MethodInvocation, MiningConfig, NoopLogger, Provider, ProviderRequest, }; +use edr_rpc_eth::CallRequest; use tokio::runtime; #[tokio::test(flavor = "multi_thread")] diff --git a/crates/edr_provider/tests/issue_356.rs b/crates/edr_provider/tests/issue_356.rs index d6b33e4a4..e69c3448e 100644 --- a/crates/edr_provider/tests/issue_356.rs +++ b/crates/edr_provider/tests/issue_356.rs @@ -3,11 +3,12 @@ use std::str::FromStr; use anyhow::Context; -use edr_eth::{remote::eth::CallRequest, Address, Bytes, SpecId}; +use edr_eth::{Address, Bytes, SpecId}; use edr_provider::{ hardhat_rpc_types::ForkConfig, test_utils::create_test_config_with_fork, time::CurrentTime, MethodInvocation, NoopLogger, Provider, ProviderRequest, }; +use edr_rpc_eth::CallRequest; use edr_test_utils::env::get_alchemy_url; use sha3::{Digest, Keccak256}; use tokio::runtime; diff --git a/crates/edr_provider/tests/issue_361.rs b/crates/edr_provider/tests/issue_361.rs index 455737105..f9705b575 100644 --- a/crates/edr_provider/tests/issue_361.rs +++ b/crates/edr_provider/tests/issue_361.rs @@ -1,9 +1,8 @@ #![cfg(feature = "test-utils")] use edr_eth::{ - remote::{filter::LogFilterOptions, BlockSpec}, - transaction::EthTransactionRequest, - AccountInfo, Address, SpecId, + filter::LogFilterOptions, transaction::EthTransactionRequest, AccountInfo, Address, BlockSpec, + SpecId, }; use edr_evm::KECCAK_EMPTY; use edr_provider::{ diff --git a/crates/edr_provider/tests/timestamp.rs b/crates/edr_provider/tests/timestamp.rs index e37dc070e..819118ca6 100644 --- a/crates/edr_provider/tests/timestamp.rs +++ b/crates/edr_provider/tests/timestamp.rs @@ -2,10 +2,7 @@ use std::{convert::Infallible, sync::Arc}; -use edr_eth::{ - remote::{eth, PreEip1898BlockSpec}, - B256, U64, -}; +use edr_eth::{PreEip1898BlockSpec, B256, U64}; use edr_provider::{ test_utils::create_test_config, time::{MockTime, TimeSinceEpoch}, @@ -71,7 +68,7 @@ impl TimestampFixture { MethodInvocation::GetBlockByNumber(PreEip1898BlockSpec::latest(), false), ))?; - let block: eth::Block = serde_json::from_value(result.result)?; + let block: edr_rpc_eth::Block = serde_json::from_value(result.result)?; Ok(block.timestamp) } diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index a760f85eb..e9330bde5 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -33,7 +33,8 @@ use crate::{ }, remove_from_cache, CacheableMethod, CachedBlockNumber, CachedMethod, }, - jsonrpc, MiddlewareError, ReqwestError, + error::{MiddlewareError, ReqwestError}, + jsonrpc, }; const RPC_CACHE_DIR: &str = "rpc_cache"; diff --git a/crates/edr_rpc_client/src/reqwest_error.rs b/crates/edr_rpc_client/src/error.rs similarity index 99% rename from crates/edr_rpc_client/src/reqwest_error.rs rename to crates/edr_rpc_client/src/error.rs index c70ffd844..850c6241b 100644 --- a/crates/edr_rpc_client/src/reqwest_error.rs +++ b/crates/edr_rpc_client/src/error.rs @@ -1,5 +1,6 @@ use std::error::Error; +pub use hyper::http::Error as HttpError; use lazy_static::lazy_static; use regex::{Captures, Regex, Replacer}; use url::Url; diff --git a/crates/edr_rpc_client/src/lib.rs b/crates/edr_rpc_client/src/lib.rs index 0710560d0..6899b7eb7 100644 --- a/crates/edr_rpc_client/src/lib.rs +++ b/crates/edr_rpc_client/src/lib.rs @@ -5,11 +5,9 @@ /// Types for caching JSON-RPC responses pub mod cache; mod client; +/// Types for JSON-RPC error reporting. +pub mod error; /// Types specific to JSON-RPC pub mod jsonrpc; -mod reqwest_error; -pub use self::{ - client::*, - reqwest_error::{MiddlewareError, ReqwestError}, -}; +pub use self::client::*; diff --git a/crates/edr_rpc_eth/src/lib.rs b/crates/edr_rpc_eth/src/lib.rs index f5cb649ff..6305b2537 100644 --- a/crates/edr_rpc_eth/src/lib.rs +++ b/crates/edr_rpc_eth/src/lib.rs @@ -13,6 +13,8 @@ mod request_methods; pub mod spec; mod transaction; +pub use edr_rpc_client::{error, header, jsonrpc, HeaderMap}; + pub use self::{ block::Block, call_request::CallRequest, r#override::*, request_methods::RequestMethod, transaction::Transaction, diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index c1f32e130..6e45ef080 100644 --- a/crates/tools/Cargo.toml +++ b/crates/tools/Cargo.toml @@ -12,6 +12,7 @@ edr_defaults = { version = "0.3.5", path = "../edr_defaults" } edr_eth = { version = "0.3.5", path = "../edr_eth" } edr_evm = { version = "0.3.5", path = "../edr_evm", features = ["tracing"] } edr_provider = { version = "0.3.5", path = "../edr_provider", features = ["test-utils"] } +edr_rpc_eth = { version = "0.3.5", path = "../edr_rpc_eth" } flate2 = "1.0.28" indicatif = { version = "0.17.7", features = ["rayon"] } mimalloc = { version = "0.1.39", default-features = false } diff --git a/crates/tools/src/remote_block.rs b/crates/tools/src/remote_block.rs index 1c2bbc57c..577439528 100644 --- a/crates/tools/src/remote_block.rs +++ b/crates/tools/src/remote_block.rs @@ -1,8 +1,8 @@ -use edr_eth::remote::RpcClient; use edr_provider::test_utils::run_full_block; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; pub async fn replay(url: String, block_number: Option, chain_id: u64) -> anyhow::Result<()> { - let rpc_client = RpcClient::new(&url, edr_defaults::CACHE_DIR.into(), None)?; + let rpc_client = EthRpcClient::::new(&url, edr_defaults::CACHE_DIR.into(), None)?; let block_number = if let Some(block_number) = block_number { block_number diff --git a/crates/tools/src/scenario.rs b/crates/tools/src/scenario.rs index 620e4468e..291004632 100644 --- a/crates/tools/src/scenario.rs +++ b/crates/tools/src/scenario.rs @@ -6,9 +6,9 @@ use std::{ }; use anyhow::Context; -use edr_eth::remote::jsonrpc; use edr_evm::blockchain::BlockchainError; use edr_provider::{time::CurrentTime, Logger, ProviderError, ProviderRequest}; +use edr_rpc_eth::jsonrpc; use flate2::bufread::GzDecoder; use indicatif::ProgressBar; use serde::Deserialize; From 9960722f5b75faa7c7af863c78274d1ea7681906 Mon Sep 17 00:00:00 2001 From: Wodann Date: Fri, 31 May 2024 08:01:32 +0000 Subject: [PATCH 10/18] fix: last test --- Cargo.lock | 3 +- crates/edr_eth/Cargo.toml | 3 + .../{edr_rpc_eth => edr_eth}/tests/receipt.rs | 18 +- crates/edr_rpc_client/src/client.rs | 183 ++++++++++++++++-- crates/edr_rpc_eth/Cargo.toml | 1 - .../src/cacheable_method_invocation.rs | 12 +- crates/edr_rpc_eth/tests/client.rs | 84 ++------ 7 files changed, 201 insertions(+), 103 deletions(-) rename crates/{edr_rpc_eth => edr_eth}/tests/receipt.rs (95%) diff --git a/Cargo.lock b/Cargo.lock index 982781baa..a2585c9ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,6 +1049,7 @@ dependencies = [ "assert-json-diff", "c-kzg", "edr_defaults", + "edr_rpc_eth", "edr_test_utils", "hash-db", "hash256-std-hasher", @@ -1065,6 +1066,7 @@ dependencies = [ "serial_test", "sha2", "sha3", + "tempfile", "thiserror", "tokio", "tracing", @@ -1215,7 +1217,6 @@ dependencies = [ "reqwest", "serde", "serde_json", - "serial_test", "tempfile", "thiserror", "tokio", diff --git a/crates/edr_eth/Cargo.toml b/crates/edr_eth/Cargo.toml index ca5115e41..10a51647f 100644 --- a/crates/edr_eth/Cargo.toml +++ b/crates/edr_eth/Cargo.toml @@ -26,11 +26,13 @@ triehash = { version = "0.8.4", default-features = false } anyhow = "1.0.75" assert-json-diff = "2.0.2" edr_defaults = { version = "0.3.5", path = "../edr_defaults" } +edr_rpc_eth = { version = "0.3.5", path = "../edr_rpc_eth" } edr_test_utils = { version = "0.3.5", path = "../edr_test_utils" } lazy_static = "1.4.0" paste = { version = "1.0.14", default-features = false } serde_json = { version = "1.0.89" } serial_test = "2.0.0" +tempfile = { version = "3.7.1", default-features = false } tokio = { version = "1.23.0", features = ["macros"] } [features] @@ -38,4 +40,5 @@ default = ["std"] rand = ["revm-primitives/rand"] serde = ["dep:serde", "c-kzg/serde", "revm-primitives/serde"] std = ["hash256-std-hasher/std", "hash-db/std", "hex/std", "itertools/use_std", "k256/std", "k256/precomputed-tables", "revm-primitives/std", "serde?/std", "sha2/std", "sha3/std", "triehash/std"] +test-remote = [] tracing = ["dep:tracing"] diff --git a/crates/edr_rpc_eth/tests/receipt.rs b/crates/edr_eth/tests/receipt.rs similarity index 95% rename from crates/edr_rpc_eth/tests/receipt.rs rename to crates/edr_eth/tests/receipt.rs index f2016bb37..35bae76d3 100644 --- a/crates/edr_rpc_eth/tests/receipt.rs +++ b/crates/edr_eth/tests/receipt.rs @@ -10,11 +10,12 @@ mod remote { paste::item! { #[tokio::test] #[serial] - async fn []() { - use edr_eth::{remote::{RpcClient, PreEip1898BlockSpec}, trie::ordered_trie_root}; + async fn []() { + use edr_eth::{PreEip1898BlockSpec, trie::ordered_trie_root}; + use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use edr_test_utils::env::get_alchemy_url; - let client = RpcClient::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None).expect("url ok"); + let client = EthRpcClient::::new(&get_alchemy_url(), edr_defaults::CACHE_DIR.into(), None).expect("url ok"); let block = client .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::Number($block_number)) @@ -53,19 +54,20 @@ mod remote { $( paste::item! { #[tokio::test] - async fn []() { + async fn []() { + use alloy_rlp::Decodable; + use edr_eth::{log::Log, receipt::TypedReceipt, B256, SpecId}; + use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use edr_test_utils::env::get_alchemy_url; use tempfile::TempDir; - use crate::{remote::RpcClient, B256}; - let tempdir = TempDir::new().unwrap(); - let client = RpcClient::new(&get_alchemy_url(), tempdir.path().into(), None).unwrap(); + let client = EthRpcClient::::new(&get_alchemy_url(), tempdir.path().into(), None).unwrap(); let transaction_hash = B256::from_slice(&hex::decode($transaction_hash).unwrap()); let receipt = client - .get_transaction_receipt(&transaction_hash) + .get_transaction_receipt(transaction_hash) .await .expect("Should succeed") .expect("Receipt must exist"); diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index e9330bde5..e25a48363 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -644,11 +644,18 @@ impl SerializedRequest { mod tests { use std::ops::Deref; + use edr_eth::PreEip1898BlockSpec; use hyper::StatusCode; use tempfile::TempDir; use walkdir::WalkDir; - use self::cache::{key::CacheKeyVariant, KeyHasher}; + use self::cache::{ + block_spec::{ + CacheableBlockSpec, PreEip1898BlockSpecNotCacheableError, UnresolvedBlockTagError, + }, + key::CacheKeyVariant, + KeyHasher, + }; use super::*; #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] @@ -658,66 +665,137 @@ mod tests { BlockNumber(()), #[serde(rename = "eth_chainId", with = "edr_eth::serde::empty_params")] ChainId(()), + #[serde(rename = "eth_getBlockByNumber")] + GetBlockByNumber( + PreEip1898BlockSpec, + /// include transaction data + bool, + ), #[serde(rename = "net_version", with = "edr_eth::serde::empty_params")] NetVersion(()), } - enum CachedTestMethod { + enum CachedTestMethod<'method> { + GetBlockByNumber { + block_spec: CacheableBlockSpec<'method>, + + /// include transaction data + include_tx_data: bool, + }, NetVersion, } - impl CachedTestMethod { - fn key_hasher(&self) -> cache::KeyHasher { - KeyHasher::new().hash_u8(self.cache_key_variant()) + impl<'method> CachedTestMethod<'method> { + fn key_hasher(&self) -> Result { + let hasher = KeyHasher::new().hash_u8(self.cache_key_variant()); + + let hasher = match self { + Self::GetBlockByNumber { + block_spec, + include_tx_data, + } => hasher + .hash_block_spec(block_spec)? + .hash_bool(*include_tx_data), + Self::NetVersion => hasher, + }; + + Ok(hasher) } } - impl From for Option<()> { - fn from(_: CachedTestMethod) -> Self { - None + #[derive(Clone, Debug)] + enum TestMethodWithResolvableBlockSpec { + GetBlockByNumber { include_tx_data: bool }, + } + + impl<'method> From> for Option { + fn from(value: CachedTestMethod<'method>) -> Self { + match value { + CachedTestMethod::GetBlockByNumber { + block_spec: _, + include_tx_data, + } => Some(TestMethodWithResolvableBlockSpec::GetBlockByNumber { include_tx_data }), + CachedTestMethod::NetVersion => None, + } } } - impl<'method> TryFrom<&'method TestMethod> for CachedTestMethod { - type Error = (); + #[derive(Debug, thiserror::Error)] + enum TestMethodNotCacheableError { + #[error("Method is not cacheable: {0:?}")] + Method(TestMethod), + #[error(transparent)] + PreEip1898BlockSpec(#[from] PreEip1898BlockSpecNotCacheableError), + } + + impl<'method> TryFrom<&'method TestMethod> for CachedTestMethod<'method> { + type Error = TestMethodNotCacheableError; fn try_from(method: &'method TestMethod) -> Result { match method { + TestMethod::GetBlockByNumber(block_spec, include_tx_data) => { + Ok(Self::GetBlockByNumber { + block_spec: block_spec.try_into()?, + include_tx_data: *include_tx_data, + }) + } TestMethod::NetVersion(_) => Ok(Self::NetVersion), - _ => Err(()), + TestMethod::BlockNumber(_) | TestMethod::ChainId(_) => { + Err(TestMethodNotCacheableError::Method(method.clone())) + } } } } - impl CacheKeyVariant for CachedTestMethod { + impl<'method> CacheKeyVariant for CachedTestMethod<'method> { fn cache_key_variant(&self) -> u8 { match self { - Self::NetVersion => 0, + Self::GetBlockByNumber { .. } => 0, + Self::NetVersion => 1, } } } - impl CachedMethod for CachedTestMethod { - type MethodWithResolvableBlockTag = (); + impl<'method> CachedMethod for CachedTestMethod<'method> { + type MethodWithResolvableBlockTag = TestMethodWithResolvableBlockSpec; fn resolve_block_tag( - _method: Self::MethodWithResolvableBlockTag, - _block_number: u64, + method: Self::MethodWithResolvableBlockTag, + block_number: u64, ) -> Self { - unreachable!("No methods with resolvable block tags exist") + let resolved_block_spec = CacheableBlockSpec::Number { block_number }; + + match method { + TestMethodWithResolvableBlockSpec::GetBlockByNumber { include_tx_data } => { + Self::GetBlockByNumber { + block_spec: resolved_block_spec, + include_tx_data, + } + } + } } fn read_cache_key(self) -> Option { - Some(ReadCacheKey::finalize(self.key_hasher())) + let key_hasher = self.key_hasher().ok()?; + Some(ReadCacheKey::finalize(key_hasher)) } fn write_cache_key(self) -> Option> { - Some(WriteCacheKey::finalize(self.key_hasher())) + match self.key_hasher() { + Err(UnresolvedBlockTagError) => WriteCacheKey::needs_block_tag_resolution(self), + Ok(hasher) => match self { + CachedTestMethod::GetBlockByNumber { + block_spec, + include_tx_data: _, + } => WriteCacheKey::needs_safety_check(hasher, block_spec), + CachedTestMethod::NetVersion => Some(WriteCacheKey::finalize(hasher)), + }, + } } } impl CacheableMethod for TestMethod { - type Cached<'method> = CachedTestMethod + type Cached<'method> = CachedTestMethod<'method> where Self: 'method; @@ -734,6 +812,7 @@ mod tests { match self { Self::BlockNumber(_) => "eth_blockNumber", Self::ChainId(_) => "eth_chainId", + Self::GetBlockByNumber(_, _) => "eth_getBlockByNumber", Self::NetVersion(_) => "net_version", } } @@ -822,9 +901,71 @@ mod tests { mod alchemy { use edr_eth::U64; use edr_test_utils::env::get_alchemy_url; + use futures::future::join_all; use super::*; + #[tokio::test] + async fn concurrent_writes_to_cache_smoke_test() { + let client = TestRpcClient::new(&get_alchemy_url()); + + let test_contents = "some random test data 42"; + let cache_key = "cache-key"; + + assert_eq!(client.files_in_cache().len(), 0); + + join_all((0..100).map(|_| client.write_response_to_cache(cache_key, test_contents))) + .await; + + assert_eq!(client.files_in_cache().len(), 1); + + let contents = tokio::fs::read_to_string(&client.files_in_cache()[0]) + .await + .unwrap(); + assert_eq!(contents, serde_json::to_string(test_contents).unwrap()); + } + + #[tokio::test] + async fn get_block_by_number_with_transaction_data_unsafe_no_cache() -> anyhow::Result<()> { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + assert_eq!(client.files_in_cache().len(), 0); + + let block_number = client.block_number().await.unwrap(); + + // Check that the block number call caches the largest known block number + { + assert!(client.cached_block_number.read().await.is_some()); + } + + assert_eq!(client.files_in_cache().len(), 0); + + let block = client + .call_with_resolver::>( + TestMethod::GetBlockByNumber(PreEip1898BlockSpec::Number(block_number), false), + |block: &Option| { + block + .as_ref() + .and_then(|block| block.get("number")) + .and_then(serde_json::Value::as_u64) + }, + ) + .await + .expect("should have succeeded") + .expect("Block must exist"); + + // Unsafe block number shouldn't be cached + assert_eq!(client.files_in_cache().len(), 0); + + let number: U64 = + serde_json::from_value(block.get("number").expect("Must have number").clone())?; + + assert_eq!(number, U64::from(block_number)); + + Ok(()) + } + #[tokio::test] async fn is_cacheable_block_number() { let client = TestRpcClient::new(&get_alchemy_url()); diff --git a/crates/edr_rpc_eth/Cargo.toml b/crates/edr_rpc_eth/Cargo.toml index b7e35d1bf..018dcb4ba 100644 --- a/crates/edr_rpc_eth/Cargo.toml +++ b/crates/edr_rpc_eth/Cargo.toml @@ -19,7 +19,6 @@ edr_test_utils = { version = "0.3.5", path = "../edr_test_utils" } mockito = { version = "1.0.2", default-features = false } reqwest = { version = "0.11", default-features = false, features = ["blocking", "json"] } serde_json = { version = "1.0.89" } -serial_test = "2.0.0" tempfile = { version = "3.7.1", default-features = false } walkdir = { version = "2.3.3", default-features = false } diff --git a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs index d25ef1c37..18c63a0ce 100644 --- a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs +++ b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs @@ -270,12 +270,12 @@ impl<'method> CachedMethod for CachedRequestMethod<'method> { let resolved_block_spec = CacheableBlockSpec::Number { block_number }; match method { - MethodWithResolvableBlockSpec::GetBlockByNumber { - include_tx_data, .. - } => CachedRequestMethod::GetBlockByNumber { - block_spec: resolved_block_spec, - include_tx_data, - }, + MethodWithResolvableBlockSpec::GetBlockByNumber { include_tx_data } => { + CachedRequestMethod::GetBlockByNumber { + block_spec: resolved_block_spec, + include_tx_data, + } + } } } diff --git a/crates/edr_rpc_eth/tests/client.rs b/crates/edr_rpc_eth/tests/client.rs index 0cb8148e9..2d8561137 100644 --- a/crates/edr_rpc_eth/tests/client.rs +++ b/crates/edr_rpc_eth/tests/client.rs @@ -1,10 +1,11 @@ -use std::{ops::Deref, str::FromStr}; +use std::{ops::Deref, path::PathBuf, str::FromStr}; use edr_eth::B256; use edr_rpc_client::RpcClientError; -use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec, RequestMethod}; +use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use reqwest::StatusCode; use tempfile::TempDir; +use walkdir::WalkDir; struct TestRpcClient { client: EthRpcClient, @@ -82,12 +83,10 @@ async fn send_request_body_400_status() { #[cfg(feature = "test-remote")] mod alchemy { - use std::{fs::File, path::PathBuf}; + use std::fs::File; - use edr_eth::{account::KECCAK_EMPTY, Address, BlockSpec, U256}; + use edr_eth::{filter::OneOrMore, Address, BlockSpec, Bytes, PreEip1898BlockSpec, U256}; use edr_test_utils::env::get_alchemy_url; - use futures::future::join_all; - use walkdir::WalkDir; use super::*; @@ -102,7 +101,7 @@ mod alchemy { .expect("failed to parse address"); let error = TestRpcClient::new(&alchemy_url) - .get_account_info(&dai_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) + .get_account_info(dai_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) .await .expect_err("should have failed"); @@ -145,7 +144,7 @@ mod alchemy { .expect("failed to parse hash from string"); let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash(&hash) + .get_block_by_hash(hash) .await .expect("should have succeeded"); @@ -165,7 +164,7 @@ mod alchemy { .expect("failed to parse hash from string"); let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash_with_transaction_data(&hash) + .get_block_by_hash_with_transaction_data(hash) .await .expect("should have succeeded"); @@ -208,34 +207,6 @@ mod alchemy { assert_eq!(block.transactions.len(), 102); } - #[tokio::test] - async fn get_block_by_number_with_transaction_data_unsafe_no_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - assert_eq!(client.files_in_cache().len(), 0); - - let block_number = client.block_number().await.unwrap(); - - // Check that the block number call caches the largest known block number - { - assert!(client.cached_block_number.read().await.is_some()); - } - - assert_eq!(client.files_in_cache().len(), 0); - - let block = client - .get_block_by_number(PreEip1898BlockSpec::Number(block_number)) - .await - .expect("should have succeeded") - .expect("Block must exist"); - - // Unsafe block number shouldn't be cached - assert_eq!(client.files_in_cache().len(), 0); - - assert_eq!(block.number, Some(block_number)); - } - #[tokio::test] async fn get_block_with_transaction_data_cached() { let alchemy_url = get_alchemy_url(); @@ -390,7 +361,7 @@ mod alchemy { .expect("failed to parse hash from string"); let tx = TestRpcClient::new(&alchemy_url) - .get_transaction_by_hash(&hash) + .get_transaction_by_hash(hash) .await .expect("failed to get transaction by hash"); @@ -477,7 +448,7 @@ mod alchemy { .expect("failed to parse address"); let transaction_count = TestRpcClient::new(&alchemy_url) - .get_transaction_count(&dai_address, Some(BlockSpec::Number(16220843))) + .get_transaction_count(dai_address, Some(BlockSpec::Number(16220843))) .await .expect("should have succeeded"); @@ -492,7 +463,7 @@ mod alchemy { .expect("failed to parse address"); let error = TestRpcClient::new(&alchemy_url) - .get_transaction_count(&missing_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) + .get_transaction_count(missing_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) .await .expect_err("should have failed"); @@ -514,7 +485,7 @@ mod alchemy { .expect("failed to parse hash from string"); let receipt = TestRpcClient::new(&alchemy_url) - .get_transaction_receipt(&hash) + .get_transaction_receipt(hash) .await .expect("failed to get transaction by hash"); @@ -566,7 +537,7 @@ mod alchemy { let total_supply = TestRpcClient::new(&alchemy_url) .get_storage_at( - &dai_address, + dai_address, U256::from(1), Some(BlockSpec::Number(16220843)), ) @@ -594,7 +565,7 @@ mod alchemy { let _total_supply = TestRpcClient::new(&alchemy_url) .get_storage_at( - &dai_address, + dai_address, U256::from_str_radix( "0000000000000000000000000000000000000000000000000000000000000001", 16, @@ -615,7 +586,7 @@ mod alchemy { let storage_slot = TestRpcClient::new(&alchemy_url) .get_storage_at( - &dai_address, + dai_address, U256::from(1), Some(BlockSpec::Number(MAX_BLOCK_NUMBER)), ) @@ -646,7 +617,7 @@ mod alchemy { let total_supply = client .get_storage_at( - &dai_address, + dai_address, U256::from(1), Some(BlockSpec::Number(16220843)), ) @@ -663,25 +634,6 @@ mod alchemy { assert_eq!(total_supply, cached_result); } - #[tokio::test] - async fn concurrent_writes_to_cache_smoke_test() { - let client = TestRpcClient::new(&get_alchemy_url()); - - let test_contents = "some random test data 42"; - let cache_key = "cache-key"; - - assert_eq!(client.files_in_cache().len(), 0); - - join_all((0..100).map(|_| client.write_response_to_cache(cache_key, test_contents))).await; - - assert_eq!(client.files_in_cache().len(), 1); - - let contents = tokio::fs::read_to_string(&client.files_in_cache()[0]) - .await - .unwrap(); - assert_eq!(contents, serde_json::to_string(test_contents).unwrap()); - } - #[tokio::test] async fn handles_invalid_type_in_cache_single_call() { let alchemy_url = get_alchemy_url(); @@ -691,7 +643,7 @@ mod alchemy { client .get_storage_at( - &dai_address, + dai_address, U256::from(1), Some(BlockSpec::Number(16220843)), ) @@ -705,7 +657,7 @@ mod alchemy { client .get_storage_at( - &dai_address, + dai_address, U256::from(1), Some(BlockSpec::Number(16220843)), ) From b34068e14bf6e8e631106e5f412530a0a6b26944 Mon Sep 17 00:00:00 2001 From: Wodann Date: Fri, 31 May 2024 19:54:28 +0000 Subject: [PATCH 11/18] docs: clarify documentation and type names --- crates/edr_rpc_client/src/cache.rs | 24 +----- crates/edr_rpc_client/src/cache/hasher.rs | 21 +++-- crates/edr_rpc_client/src/cache/key.rs | 10 +-- crates/edr_rpc_client/src/client.rs | 84 ++++++++++++------- crates/edr_rpc_client/src/lib.rs | 3 +- .../src/cacheable_method_invocation.rs | 37 ++++---- crates/edr_rpc_eth/tests/client.rs | 36 ++++---- 7 files changed, 118 insertions(+), 97 deletions(-) diff --git a/crates/edr_rpc_client/src/cache.rs b/crates/edr_rpc_client/src/cache.rs index 1051ed1b9..6964747a7 100644 --- a/crates/edr_rpc_client/src/cache.rs +++ b/crates/edr_rpc_client/src/cache.rs @@ -20,28 +20,10 @@ pub use self::hasher::KeyHasher; use self::key::{ReadCacheKey, WriteCacheKey}; use crate::RpcClientError; -/// Trait for RPC method types that can be cached. -pub trait CacheableMethod: Sized { - /// The type representing the cached method. - type Cached<'method>: CachedMethod + TryFrom<&'method Self> - where - Self: 'method; - - /// Creates a method for requesting the block number. - fn block_number_request() -> Self; - - /// Creates a method for requesting the chain ID. - fn chain_id_request() -> Self; - - #[cfg(feature = "tracing")] - /// Returns the name of the method. - fn name(&self) -> &'static str; -} - -/// Trait for RPC method types that will be cached to disk. -pub trait CachedMethod: Into> { +/// Trait for RPC method types that can be cached to disk. +pub trait CacheableMethod: Into> { /// The type representing a subset of methods containing a [`BlockTag`] - /// which can be resolved to a block number. + /// which can potentially be resolved to a block number. type MethodWithResolvableBlockTag: Clone + Debug; /// Resolves a block tag to a block number for the provided method. diff --git a/crates/edr_rpc_client/src/cache/hasher.rs b/crates/edr_rpc_client/src/cache/hasher.rs index f086fe60c..ee0bc7fb7 100644 --- a/crates/edr_rpc_client/src/cache/hasher.rs +++ b/crates/edr_rpc_client/src/cache/hasher.rs @@ -7,7 +7,8 @@ use super::{ key::CacheKeyVariant, }; -#[derive(Debug, Clone)] +/// A hasher for cache keys. +#[derive(Clone, Debug, Default)] pub struct KeyHasher { hasher: Sha3_256, } @@ -34,42 +35,44 @@ pub struct KeyHasher { // collisions](https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions) should be // considered. impl KeyHasher { - pub fn new() -> Self { - Self { - hasher: Sha3_256::new(), - } - } - + /// Hashes a sequence of bytes. pub fn hash_bytes(mut self, bytes: impl AsRef<[u8]>) -> Self { self.hasher.update(bytes); self } + /// Hashes a `u8` value. pub fn hash_u8(self, value: u8) -> Self { self.hash_bytes(value.to_le_bytes()) } + /// Hashes a `bool` value. pub fn hash_bool(self, value: bool) -> Self { self.hash_u8(u8::from(value)) } + /// Hashes an `Address` value. pub fn hash_address(self, address: &Address) -> Self { self.hash_bytes(address) } + /// Hashes a `u64` value. pub fn hash_u64(self, value: u64) -> Self { self.hash_bytes(value.to_le_bytes()) } + /// Hashes a `U256` value. pub fn hash_u256(self, value: &U256) -> Self { self.hash_bytes(value.as_le_bytes()) } + /// Hashes a `B256` value. pub fn hash_b256(self, value: &B256) -> Self { self.hash_bytes(value) } + /// Hashes a [`CacheableBlockSpec`] value. pub fn hash_block_spec( self, block_spec: &CacheableBlockSpec<'_>, @@ -96,6 +99,7 @@ impl KeyHasher { } } + /// Hashes a [`CacheableLogFilterOptions`] value pub fn hash_log_filter_options( self, params: &CacheableLogFilterOptions<'_>, @@ -129,6 +133,7 @@ impl KeyHasher { Ok(this) } + /// Hashes a [`CacheableLogFilterRange`] value. pub fn hash_log_filter_range( self, params: &CacheableLogFilterRange<'_>, @@ -146,6 +151,7 @@ impl KeyHasher { } } + /// Hashes a single [`RewardPercentile`] value. pub fn hash_reward_percentile(self, value: &RewardPercentile) -> Self { const RESOLUTION: f64 = 100.0; // `RewardPercentile` is an f64 in range [0, 100], so this is guaranteed not to @@ -153,6 +159,7 @@ impl KeyHasher { self.hash_u64((value.as_ref() * RESOLUTION).floor() as u64) } + /// Hashes a sequence of [`RewardPercentile`] values. pub fn hash_reward_percentiles(self, value: &[RewardPercentile]) -> Self { let mut this = self.hash_u64(value.len() as u64); for v in value { diff --git a/crates/edr_rpc_client/src/cache/key.rs b/crates/edr_rpc_client/src/cache/key.rs index cb8fe3a5d..f8b7350d1 100644 --- a/crates/edr_rpc_client/src/cache/key.rs +++ b/crates/edr_rpc_client/src/cache/key.rs @@ -2,7 +2,7 @@ use edr_eth::block::{is_safe_block_number, IsSafeBlockNumberArgs}; use super::{ block_spec::CacheableBlockSpec, filter::CacheableLogFilterRange, hasher::KeyHasher, - CachedMethod, + CacheableMethod, }; /// Trait for retrieving the unique id of an enum variant. @@ -48,7 +48,7 @@ impl AsRef for ReadCacheKey { /// A cache key that can be used to write to the cache. #[derive(Clone, Debug)] -pub enum WriteCacheKey { +pub enum WriteCacheKey { /// It needs to be checked whether the block number is safe (reorg-free) /// before writing to the cache. NeedsSafetyCheck(CacheKeyForUncheckedBlockNumber), @@ -60,7 +60,7 @@ pub enum WriteCacheKey { Resolved(String), } -impl WriteCacheKey { +impl WriteCacheKey { /// Finalizes the provided [`KeyHasher`] and return the resolved cache /// key. pub fn finalize(hasher: KeyHasher) -> Self { @@ -151,11 +151,11 @@ pub(crate) enum ResolvedSymbolicTag { /// A cache key for which the block tag needs to be resolved before writing to /// the cache. #[derive(Clone, Debug)] -pub struct CacheKeyForUnresolvedBlockTag { +pub struct CacheKeyForUnresolvedBlockTag { method: MethodT::MethodWithResolvableBlockTag, } -impl CacheKeyForUnresolvedBlockTag { +impl CacheKeyForUnresolvedBlockTag { /// Check whether the block number is safe to cache before returning a cache /// key. pub(crate) fn resolve_block_tag(self, block_number: u64) -> Option { diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index e25a48363..2bb4eb16e 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -31,7 +31,7 @@ use crate::{ CacheKeyForUncheckedBlockNumber, CacheKeyForUnresolvedBlockTag, ReadCacheKey, ResolvedSymbolicTag, WriteCacheKey, }, - remove_from_cache, CacheableMethod, CachedBlockNumber, CachedMethod, + remove_from_cache, CacheableMethod, CachedBlockNumber, }, error::{MiddlewareError, ReqwestError}, jsonrpc, @@ -113,11 +113,35 @@ pub enum RpcClientError { JoinError(#[from] tokio::task::JoinError), } +/// Trait for RPC method types that support EVM-based blockchains. +pub trait RpcMethod { + /// A type representing the subset of RPC methods that can be cached. + type Cacheable<'method>: CacheableMethod + TryFrom<&'method Self> + where + Self: 'method; + + /// Creates a method for requesting the block number. + /// + /// This is used for caching purposes. + fn block_number_request() -> Self; + + /// Creates a method for requesting the chain ID. + /// + /// This is used for caching purposes. + fn chain_id_request() -> Self; + + #[cfg(feature = "tracing")] + /// Returns the name of the method. + fn name(&self) -> &'static str; +} + /// A client for executing RPC methods on a remote Ethereum node. -/// The client caches responses based on chain id, so it's important to not use -/// it with local nodes. +/// +/// The client caches responses based on chain ID, so it's important to not use +/// it with local nodes. For responses that depend on the block number, the +/// client only caches responses for finalized blocks. #[derive(Debug)] -pub struct RpcClient { +pub struct RpcClient { url: url::Url, chain_id: OnceCell, cached_block_number: RwLock>, @@ -128,7 +152,7 @@ pub struct RpcClient { _phantom: PhantomData, } -impl RpcClient { +impl RpcClient { /// Create a new instance, given a remote node URL. /// The cache directory is the global EDR cache directory configured by the /// user. @@ -322,7 +346,7 @@ impl RpcClient { async fn resolve_block_tag( &self, - block_tag_resolver: CacheKeyForUnresolvedBlockTag>, + block_tag_resolver: CacheKeyForUnresolvedBlockTag>, result: ResultT, resolve_block_number: impl Fn(ResultT) -> Option, ) -> Result, RpcClientError> { @@ -345,9 +369,9 @@ impl RpcClient { result: ResultT, resolve_block_number: impl Fn(ResultT) -> Option, ) -> Result, RpcClientError> { - let cached_method = MethodT::Cached::try_from(method).ok(); + let cached_method = MethodT::Cacheable::try_from(method).ok(); - if let Some(cache_key) = cached_method.and_then(CachedMethod::write_cache_key) { + if let Some(cache_key) = cached_method.and_then(CacheableMethod::write_cache_key) { match cache_key { WriteCacheKey::NeedsSafetyCheck(safety_checker) => { self.validate_block_number(safety_checker).await @@ -511,8 +535,8 @@ impl RpcClient { method: MethodT, resolve_block_number: impl Fn(&SuccessT) -> Option, ) -> Result { - let cached_method = MethodT::Cached::try_from(&method).ok(); - let read_cache_key = cached_method.and_then(CachedMethod::read_cache_key); + let cached_method = MethodT::Cacheable::try_from(&method).ok(); + let read_cache_key = cached_method.and_then(CacheableMethod::read_cache_key); let request = self.serialize_request(&method)?; @@ -647,7 +671,6 @@ mod tests { use edr_eth::PreEip1898BlockSpec; use hyper::StatusCode; use tempfile::TempDir; - use walkdir::WalkDir; use self::cache::{ block_spec::{ @@ -687,7 +710,7 @@ mod tests { impl<'method> CachedTestMethod<'method> { fn key_hasher(&self) -> Result { - let hasher = KeyHasher::new().hash_u8(self.cache_key_variant()); + let hasher = KeyHasher::default().hash_u8(self.cache_key_variant()); let hasher = match self { Self::GetBlockByNumber { @@ -756,7 +779,7 @@ mod tests { } } - impl<'method> CachedMethod for CachedTestMethod<'method> { + impl<'method> CacheableMethod for CachedTestMethod<'method> { type MethodWithResolvableBlockTag = TestMethodWithResolvableBlockSpec; fn resolve_block_tag( @@ -794,8 +817,8 @@ mod tests { } } - impl CacheableMethod for TestMethod { - type Cached<'method> = CachedTestMethod<'method> + impl RpcMethod for TestMethod { + type Cacheable<'method> = CachedTestMethod<'method> where Self: 'method; @@ -835,20 +858,6 @@ mod tests { cache_dir: tempdir, } } - - fn files_in_cache(&self) -> Vec { - let mut files = Vec::new(); - for entry in WalkDir::new(&self.cache_dir) - .follow_links(true) - .into_iter() - .filter_map(Result::ok) - { - if entry.file_type().is_file() { - files.push(entry.path().to_owned()); - } - } - files - } } impl Deref for TestRpcClient { @@ -902,9 +911,26 @@ mod tests { use edr_eth::U64; use edr_test_utils::env::get_alchemy_url; use futures::future::join_all; + use walkdir::WalkDir; use super::*; + impl TestRpcClient { + fn files_in_cache(&self) -> Vec { + let mut files = Vec::new(); + for entry in WalkDir::new(&self.cache_dir) + .follow_links(true) + .into_iter() + .filter_map(Result::ok) + { + if entry.file_type().is_file() { + files.push(entry.path().to_owned()); + } + } + files + } + } + #[tokio::test] async fn concurrent_writes_to_cache_smoke_test() { let client = TestRpcClient::new(&get_alchemy_url()); diff --git a/crates/edr_rpc_client/src/lib.rs b/crates/edr_rpc_client/src/lib.rs index 6899b7eb7..80ac22bb6 100644 --- a/crates/edr_rpc_client/src/lib.rs +++ b/crates/edr_rpc_client/src/lib.rs @@ -1,6 +1,7 @@ #![warn(missing_docs)] -//! Ethereum JSON-RPC client +//! Implementation of a JSON-RPC client for EVM-based blockchains that uses a +//! caching strategy based on finalized blocks and chain IDs. /// Types for caching JSON-RPC responses pub mod cache; diff --git a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs index 18c63a0ce..add86376d 100644 --- a/crates/edr_rpc_eth/src/cacheable_method_invocation.rs +++ b/crates/edr_rpc_eth/src/cacheable_method_invocation.rs @@ -1,13 +1,16 @@ use edr_eth::{reward_percentile::RewardPercentile, Address, B256, U256}; -use edr_rpc_client::cache::{ - self, - block_spec::{ - BlockSpecNotCacheableError, CacheableBlockSpec, PreEip1898BlockSpecNotCacheableError, - UnresolvedBlockTagError, +use edr_rpc_client::{ + cache::{ + self, + block_spec::{ + BlockSpecNotCacheableError, CacheableBlockSpec, PreEip1898BlockSpecNotCacheableError, + UnresolvedBlockTagError, + }, + filter::{CacheableLogFilterOptions, LogFilterOptionsNotCacheableError}, + key::{CacheKeyVariant, ReadCacheKey, WriteCacheKey}, + CacheableMethod, }, - filter::{CacheableLogFilterOptions, LogFilterOptionsNotCacheableError}, - key::{CacheKeyVariant, ReadCacheKey, WriteCacheKey}, - CacheableMethod, CachedMethod, + RpcMethod, }; use crate::request_methods::RequestMethod; @@ -72,7 +75,7 @@ impl<'a> CachedRequestMethod<'a> { // Allow to keep same structure as other RequestMethod and other methods. #[allow(clippy::match_same_arms)] fn key_hasher(&self) -> Result { - let hasher = cache::KeyHasher::new(); + let hasher = cache::KeyHasher::default(); let hasher = hasher.hash_u8(self.cache_key_variant()); let hasher = match self { @@ -232,8 +235,8 @@ impl<'method> From> for Option = CachedRequestMethod<'method>; +impl RpcMethod for RequestMethod { + type Cacheable<'method> = CachedRequestMethod<'method>; fn block_number_request() -> Self { Self::BlockNumber(()) @@ -263,7 +266,7 @@ impl CacheableMethod for RequestMethod { } } -impl<'method> CachedMethod for CachedRequestMethod<'method> { +impl<'method> CacheableMethod for CachedRequestMethod<'method> { type MethodWithResolvableBlockTag = MethodWithResolvableBlockSpec; fn resolve_block_tag(method: Self::MethodWithResolvableBlockTag, block_number: u64) -> Self { @@ -367,7 +370,7 @@ mod test { #[test] fn test_hash_length() { - let hash = cache::KeyHasher::new().hash_u8(0).finalize(); + let hash = cache::KeyHasher::default().hash_u8(0).finalize(); // 32 bytes as hex assert_eq!(hash.len(), 2 * 32); } @@ -377,14 +380,14 @@ mod test { let block_number = u64::default(); let block_hash = B256::default(); - let hash_one = cache::KeyHasher::new() + let hash_one = cache::KeyHasher::default() .hash_block_spec(&CacheableBlockSpec::Hash { block_hash: &block_hash, require_canonical: None, }) .unwrap() .finalize(); - let hash_two = cache::KeyHasher::new() + let hash_two = cache::KeyHasher::default() .hash_block_spec(&CacheableBlockSpec::Number { block_number }) .unwrap() .finalize(); @@ -398,7 +401,7 @@ mod test { let to = CacheableBlockSpec::Number { block_number: 2 }; let address = Address::default(); - let hash_one = cache::KeyHasher::new() + let hash_one = cache::KeyHasher::default() .hash_log_filter_options(&CacheableLogFilterOptions { range: CacheableLogFilterRange::Range { from_block: from.clone(), @@ -410,7 +413,7 @@ mod test { .unwrap() .finalize(); - let hash_two = cache::KeyHasher::new() + let hash_two = cache::KeyHasher::default() .hash_log_filter_options(&CacheableLogFilterOptions { range: CacheableLogFilterRange::Range { from_block: to, diff --git a/crates/edr_rpc_eth/tests/client.rs b/crates/edr_rpc_eth/tests/client.rs index 2d8561137..a02d5ccbb 100644 --- a/crates/edr_rpc_eth/tests/client.rs +++ b/crates/edr_rpc_eth/tests/client.rs @@ -1,11 +1,10 @@ -use std::{ops::Deref, path::PathBuf, str::FromStr}; +use std::{ops::Deref, str::FromStr}; use edr_eth::B256; use edr_rpc_client::RpcClientError; use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; use reqwest::StatusCode; use tempfile::TempDir; -use walkdir::WalkDir; struct TestRpcClient { client: EthRpcClient, @@ -24,20 +23,6 @@ impl TestRpcClient { cache_dir: tempdir, } } - - fn files_in_cache(&self) -> Vec { - let mut files = Vec::new(); - for entry in WalkDir::new(&self.cache_dir) - .follow_links(true) - .into_iter() - .filter_map(Result::ok) - { - if entry.file_type().is_file() { - files.push(entry.path().to_owned()); - } - } - files - } } impl Deref for TestRpcClient { @@ -83,16 +68,33 @@ async fn send_request_body_400_status() { #[cfg(feature = "test-remote")] mod alchemy { - use std::fs::File; + use std::{fs::File, path::PathBuf}; use edr_eth::{filter::OneOrMore, Address, BlockSpec, Bytes, PreEip1898BlockSpec, U256}; use edr_test_utils::env::get_alchemy_url; + use walkdir::WalkDir; use super::*; // The maximum block number that Alchemy allows const MAX_BLOCK_NUMBER: u64 = u64::MAX >> 1; + impl TestRpcClient { + fn files_in_cache(&self) -> Vec { + let mut files = Vec::new(); + for entry in WalkDir::new(&self.cache_dir) + .follow_links(true) + .into_iter() + .filter_map(Result::ok) + { + if entry.file_type().is_file() { + files.push(entry.path().to_owned()); + } + } + files + } + } + #[tokio::test] async fn get_account_info_unknown_block() { let alchemy_url = get_alchemy_url(); From e929ecf214c9663c21c57552fd16336c7183412c Mon Sep 17 00:00:00 2001 From: Wodann Date: Fri, 31 May 2024 20:07:49 +0000 Subject: [PATCH 12/18] test: remove unnecessary test that was slowing down the test suite --- crates/edr_rpc_client/src/client.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/crates/edr_rpc_client/src/client.rs b/crates/edr_rpc_client/src/client.rs index 2bb4eb16e..9b2f5f9fe 100644 --- a/crates/edr_rpc_client/src/client.rs +++ b/crates/edr_rpc_client/src/client.rs @@ -890,22 +890,6 @@ mod tests { } } - #[tokio::test] - async fn call_failed_to_send_error() { - let alchemy_url = "https://xxxeth-mainnet.g.alchemy.com/"; - - let error = TestRpcClient::new(alchemy_url) - .call::(TestMethod::BlockNumber(())) - .await - .expect_err("should have failed to connect due to a garbage domain name"); - - if let RpcClientError::FailedToSend(error) = error { - assert!(error.to_string().contains("dns error")); - } else { - unreachable!("Invalid error: {error}"); - } - } - #[cfg(feature = "test-remote")] mod alchemy { use edr_eth::U64; From c1dc01bdca15f9375880ab42beb6545354a8f243 Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Jun 2024 15:12:37 +0000 Subject: [PATCH 13/18] fix: cargo hack --- Cargo.lock | 12 ++-- crates/edr_eth/src/block_spec.rs | 97 +++++++++++++++++++------------ crates/edr_eth/src/fee_history.rs | 9 +-- crates/edr_eth/src/filter.rs | 38 +++++++----- 4 files changed, 92 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2585c9ea..8551f9ce0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2466,9 +2466,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" dependencies = [ "num-traits", "plotters-backend", @@ -2479,15 +2479,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" dependencies = [ "plotters-backend", ] diff --git a/crates/edr_eth/src/block_spec.rs b/crates/edr_eth/src/block_spec.rs index ffddb66fa..7c8c1bc92 100644 --- a/crates/edr_eth/src/block_spec.rs +++ b/crates/edr_eth/src/block_spec.rs @@ -1,40 +1,38 @@ use std::{ - borrow::Cow, fmt, fmt::{Display, Formatter}, }; -use serde::de; - -use crate::{B256, U64}; - -const BLOCK_HASH_FIELD: &str = "blockHash"; -const REQUIRE_CANONICAL_FIELD: &str = "requireCanonical"; -const BLOCK_NUMBER_FIELD: &str = "blockNumber"; +use crate::B256; /// for representing block specifications per EIP-1898 -#[derive(Clone, Debug, PartialEq, serde::Serialize)] -#[serde(untagged)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum Eip1898BlockSpec { /// to represent the Object { blockHash, requireCanonical } in EIP-1898 - #[serde(rename_all = "camelCase")] + #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] Hash { /// the block hash block_hash: B256, /// whether the server should additionally raise a JSON-RPC error if the /// block is not in the canonical chain - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] require_canonical: Option, }, /// to represent the Object { blockNumber } in EIP-1898 - #[serde(rename_all = "camelCase")] + #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] Number { /// the block number - #[serde(serialize_with = "crate::serde::u64::serialize")] + #[cfg_attr( + feature = "serde", + serde(serialize_with = "crate::serde::u64::serialize") + )] block_number: u64, }, } +#[cfg(feature = "serde")] // We are not using a derived implementation for untagged enums, because we want // to be able to error on invalid combinations of fields impl<'de> serde::Deserialize<'de> for Eip1898BlockSpec { @@ -42,6 +40,16 @@ impl<'de> serde::Deserialize<'de> for Eip1898BlockSpec { where D: serde::Deserializer<'de>, { + use std::borrow::Cow; + + use serde::de; + + use crate::U64; + + const BLOCK_HASH_FIELD: &str = "blockHash"; + const REQUIRE_CANONICAL_FIELD: &str = "requireCanonical"; + const BLOCK_NUMBER_FIELD: &str = "blockNumber"; + #[derive(serde::Deserialize)] #[serde(field_identifier, rename_all = "camelCase")] enum Field { @@ -138,22 +146,23 @@ impl Display for Eip1898BlockSpec { } /// possible block tags as defined by the Ethereum JSON-RPC specification -#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum BlockTag { /// earliest - #[serde(rename = "earliest")] + #[cfg_attr(feature = "serde", serde(rename = "earliest"))] Earliest, /// latest - #[serde(rename = "latest")] + #[cfg_attr(feature = "serde", serde(rename = "latest"))] Latest, /// pending - #[serde(rename = "pending")] + #[cfg_attr(feature = "serde", serde(rename = "pending"))] Pending, /// safe - #[serde(rename = "safe")] + #[cfg_attr(feature = "serde", serde(rename = "safe"))] Safe, /// finalized - #[serde(rename = "finalized")] + #[cfg_attr(feature = "serde", serde(rename = "finalized"))] Finalized, } @@ -170,27 +179,37 @@ impl Display for BlockTag { } /// For specifying a block -#[derive(Clone, Debug, PartialEq, serde::Serialize)] -#[serde(untagged)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum BlockSpec { /// as a block number - Number(#[serde(serialize_with = "crate::serde::u64::serialize")] u64), + Number( + #[cfg_attr( + feature = "serde", + serde(serialize_with = "crate::serde::u64::serialize") + )] + u64, + ), /// as a block tag (eg "latest") Tag(BlockTag), /// as an EIP-1898-compliant block specifier Eip1898(Eip1898BlockSpec), } +#[cfg(feature = "serde")] // We are not using a derived implementation for untagged enums, because we want // to propagate custom error messages from the `Eip1898BlockSpec` deserializer. -impl<'de> de::Deserialize<'de> for BlockSpec { +impl<'de> serde::de::Deserialize<'de> for BlockSpec { fn deserialize(deserializer: D) -> Result where - D: de::Deserializer<'de>, + D: serde::de::Deserializer<'de>, { + use crate::U64; + struct BlockSpecVisitor; - impl<'de> de::Visitor<'de> for BlockSpecVisitor { + impl<'de> serde::de::Visitor<'de> for BlockSpecVisitor { type Value = BlockSpec; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -199,16 +218,17 @@ impl<'de> de::Deserialize<'de> for BlockSpec { fn visit_str(self, v: &str) -> Result where - E: de::Error, + E: serde::de::Error, { let result = if v.starts_with("0x") { - let number: U64 = - de::Deserialize::deserialize(de::value::StrDeserializer::new(v))?; + let number: U64 = serde::de::Deserialize::deserialize( + serde::de::value::StrDeserializer::new(v), + )?; BlockSpec::Number(number.try_into().expect("U64 fits into u64")) } else { // Forward to deserializer of `BlockTag` - BlockSpec::Tag(de::Deserialize::deserialize( - de::value::StrDeserializer::new(v), + BlockSpec::Tag(serde::de::Deserialize::deserialize( + serde::de::value::StrDeserializer::new(v), )?) }; Ok(result) @@ -216,11 +236,11 @@ impl<'de> de::Deserialize<'de> for BlockSpec { fn visit_map(self, map: M) -> Result where - M: de::MapAccess<'de>, + M: serde::de::MapAccess<'de>, { // Forward to deserializer of `Eip1898BlockSpec` - Ok(BlockSpec::Eip1898(de::Deserialize::deserialize( - de::value::MapAccessDeserializer::new(map), + Ok(BlockSpec::Eip1898(serde::de::Deserialize::deserialize( + serde::de::value::MapAccessDeserializer::new(map), )?)) } } @@ -278,11 +298,12 @@ impl Display for BlockSpec { } /// A block spec without EIP-1898 support. -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(untagged)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum PreEip1898BlockSpec { /// as a block number - Number(#[serde(with = "crate::serde::u64")] u64), + Number(#[cfg_attr(feature = "serde", serde(with = "crate::serde::u64"))] u64), /// as a block tag (eg "latest") Tag(BlockTag), } @@ -298,7 +319,7 @@ impl From for BlockSpec { impl_block_tags!(PreEip1898BlockSpec); -#[cfg(test)] +#[cfg(all(test, feature = "serde"))] mod tests { use serde_json::json; diff --git a/crates/edr_eth/src/fee_history.rs b/crates/edr_eth/src/fee_history.rs index dd23b871e..a1bb8d7d3 100644 --- a/crates/edr_eth/src/fee_history.rs +++ b/crates/edr_eth/src/fee_history.rs @@ -2,11 +2,12 @@ use crate::U256; /// Fee history for the returned block range. This can be a subsection of the /// requested range if not all blocks are available. -#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct FeeHistoryResult { /// Lowest number block of returned range. - #[serde(with = "crate::serde::u64")] + #[cfg_attr(feature = "serde", serde(with = "crate::serde::u64"))] pub oldest_block: u64, /// An array of block base fees per gas. This includes the next block after /// the newest of the returned range, because this value can be derived from @@ -17,7 +18,7 @@ pub struct FeeHistoryResult { pub gas_used_ratio: Vec, /// A two-dimensional array of effective priority fees per gas at the /// requested block percentiles. - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub reward: Option>>, } diff --git a/crates/edr_eth/src/filter.rs b/crates/edr_eth/src/filter.rs index 13561bef8..d49fb32dc 100644 --- a/crates/edr_eth/src/filter.rs +++ b/crates/edr_eth/src/filter.rs @@ -4,8 +4,9 @@ use crate::{block_spec::BlockSpec, log::FilterLog, Address, Bytes, B256}; /// A type that can be used to pass either one or many objects to a JSON-RPC /// request -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(untagged)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum OneOrMore { /// one object One(T), @@ -14,41 +15,43 @@ pub enum OneOrMore { } /// for specifying the inputs to `eth_newFilter` and `eth_getLogs` -#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct LogFilterOptions { /// beginning of a range of blocks - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub from_block: Option, /// end of a range of blocks - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub to_block: Option, /// a single block, specified by its hash - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub block_hash: Option, /// address - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub address: Option>, /// topics - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub topics: Option>>>, } /// represents the output of `eth_getFilterLogs` and `eth_getFilterChanges` when /// used with a log filter -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub struct LogOutput { /// true when the log was removed, due to a chain reorganization. false if /// it's a valid log pub removed: bool, /// integer of the log index position in the block. None when its pending /// log. - #[serde(with = "crate::serde::optional_u64")] + #[cfg_attr(feature = "serde", serde(with = "crate::serde::optional_u64"))] pub log_index: Option, /// integer of the transactions index position log was created from. None /// when its pending log. - #[serde(with = "crate::serde::optional_u64")] + #[cfg_attr(feature = "serde", serde(with = "crate::serde::optional_u64"))] pub transaction_index: Option, /// hash of the transactions this log was created from. None when its /// pending log. @@ -58,7 +61,7 @@ pub struct LogOutput { pub block_hash: Option, /// the block number where this log was in. null when its pending. None when /// its pending log. - #[serde(with = "crate::serde::optional_u64")] + #[cfg_attr(feature = "serde", serde(with = "crate::serde::optional_u64"))] pub block_number: Option, /// address from which this log originated. pub address: Address, @@ -88,8 +91,9 @@ impl From<&FilterLog> for LogOutput { } /// represents the output of `eth_getFilterChanges` -#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(untagged)] +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(untagged))] pub enum FilteredEvents { /// logs Logs(Vec), @@ -132,6 +136,7 @@ pub enum SubscriptionType { NewPendingTransactions, } +#[cfg(feature = "serde")] impl serde::Serialize for SubscriptionType { fn serialize(&self, serializer: S) -> Result where @@ -145,6 +150,7 @@ impl serde::Serialize for SubscriptionType { } } +#[cfg(feature = "serde")] impl<'a> serde::Deserialize<'a> for SubscriptionType { fn deserialize(deserializer: D) -> Result where From f1cb344cd26311221440ee4696ade1da479d64c7 Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Jun 2024 15:15:27 +0000 Subject: [PATCH 14/18] docs: add changelog --- .changeset/dirty-wombats-turn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dirty-wombats-turn.md diff --git a/.changeset/dirty-wombats-turn.md b/.changeset/dirty-wombats-turn.md new file mode 100644 index 000000000..72a9d24e2 --- /dev/null +++ b/.changeset/dirty-wombats-turn.md @@ -0,0 +1,5 @@ +--- +"@nomicfoundation/edr": patch +--- + +Refactored RpcClient to allow non-Ethereum L1 JSON-RPC requests From feb803c8fbf48a515cdc8928e50e23ea302ac29d Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Jun 2024 18:15:07 +0000 Subject: [PATCH 15/18] fix: doc links --- crates/edr_rpc_client/src/cache.rs | 5 +++-- crates/edr_rpc_client/src/cache/block_spec.rs | 2 +- crates/edr_rpc_eth/src/request_methods.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/edr_rpc_client/src/cache.rs b/crates/edr_rpc_client/src/cache.rs index 6964747a7..bc6179095 100644 --- a/crates/edr_rpc_client/src/cache.rs +++ b/crates/edr_rpc_client/src/cache.rs @@ -22,8 +22,9 @@ use crate::RpcClientError; /// Trait for RPC method types that can be cached to disk. pub trait CacheableMethod: Into> { - /// The type representing a subset of methods containing a [`BlockTag`] - /// which can potentially be resolved to a block number. + /// The type representing a subset of methods containing a + /// [`edr_eth::BlockTag`] which can potentially be resolved to a block + /// number. type MethodWithResolvableBlockTag: Clone + Debug; /// Resolves a block tag to a block number for the provided method. diff --git a/crates/edr_rpc_client/src/cache/block_spec.rs b/crates/edr_rpc_client/src/cache/block_spec.rs index c2a59b2b5..5cb86c71d 100644 --- a/crates/edr_rpc_client/src/cache/block_spec.rs +++ b/crates/edr_rpc_client/src/cache/block_spec.rs @@ -114,7 +114,7 @@ impl<'a> TryFrom<&'a PreEip1898BlockSpec> for CacheableBlockSpec<'a> { } } -/// Error type for [`Hasher::hash_block_spec`]. +/// Error type for [`KeyHasher::hash_block_spec`]. #[derive(thiserror::Error, Debug)] #[error("A block tag is not resolved.")] pub struct UnresolvedBlockTagError; diff --git a/crates/edr_rpc_eth/src/request_methods.rs b/crates/edr_rpc_eth/src/request_methods.rs index 39f31ba04..f29a9e455 100644 --- a/crates/edr_rpc_eth/src/request_methods.rs +++ b/crates/edr_rpc_eth/src/request_methods.rs @@ -4,7 +4,7 @@ use edr_eth::{ }; /// Methods for requests to a remote Ethereum node. Only contains methods -/// supported by the [`crate::remote::client::RpcClient`]. +/// supported by the [`edr_rpc_client::RpcClient`]. #[derive(Clone, Debug, PartialEq, serde::Serialize)] #[serde(tag = "method", content = "params")] pub enum RequestMethod { From 4f93f4aaba622c09420007369b5bc9f53f404fdd Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Jun 2024 18:21:14 +0000 Subject: [PATCH 16/18] fix: serde feature flag --- crates/edr_rpc_eth/src/call_request.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/edr_rpc_eth/src/call_request.rs b/crates/edr_rpc_eth/src/call_request.rs index fb1bf2f7a..2965a5b1c 100644 --- a/crates/edr_rpc_eth/src/call_request.rs +++ b/crates/edr_rpc_eth/src/call_request.rs @@ -3,16 +3,13 @@ use edr_eth::{access_list::AccessListItem, Address, Bytes, B256, U256}; /// For specifying input to methods requiring a transaction object, like /// `eth_call` and `eth_estimateGas` #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] -#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +#[serde(rename_all = "camelCase")] pub struct CallRequest { /// the address from which the transaction should be sent pub from: Option
, /// the address to which the transaction should be sent pub to: Option
, - #[cfg_attr( - feature = "serde", - serde(default, with = "edr_eth::serde::optional_u64") - )] + #[serde(default, with = "edr_eth::serde::optional_u64")] /// gas pub gas: Option, /// gas price @@ -24,12 +21,12 @@ pub struct CallRequest { /// transaction value pub value: Option, /// transaction data - #[cfg_attr(feature = "serde", serde(alias = "input"))] + #[serde(alias = "input")] pub data: Option, /// warm storage access pre-payment pub access_list: Option>, /// EIP-2718 type - #[cfg_attr(feature = "serde", serde(default, rename = "type"))] + #[serde(default, rename = "type")] pub transaction_type: Option, /// Blobs (EIP-4844) pub blobs: Option>, From fd72c0f8ed0ef4ac1c13ecba42171273606238ec Mon Sep 17 00:00:00 2001 From: Wodann Date: Mon, 3 Jun 2024 18:43:49 +0000 Subject: [PATCH 17/18] fix: docs --- crates/edr_rpc_client/src/cache/block_spec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/edr_rpc_client/src/cache/block_spec.rs b/crates/edr_rpc_client/src/cache/block_spec.rs index 5cb86c71d..fb47e2de6 100644 --- a/crates/edr_rpc_client/src/cache/block_spec.rs +++ b/crates/edr_rpc_client/src/cache/block_spec.rs @@ -114,7 +114,7 @@ impl<'a> TryFrom<&'a PreEip1898BlockSpec> for CacheableBlockSpec<'a> { } } -/// Error type for [`KeyHasher::hash_block_spec`]. +/// Error type for [`crate::cache::KeyHasher::hash_block_spec`]. #[derive(thiserror::Error, Debug)] #[error("A block tag is not resolved.")] pub struct UnresolvedBlockTagError; From 522509595cd418681dec6ecb671db87c2e404add Mon Sep 17 00:00:00 2001 From: Wodann Date: Tue, 4 Jun 2024 18:38:29 +0000 Subject: [PATCH 18/18] refactor: move client tests to source file --- crates/edr_rpc_eth/src/client.rs | 680 +++++++++++++++++++++++++++++ crates/edr_rpc_eth/tests/client.rs | 669 ---------------------------- 2 files changed, 680 insertions(+), 669 deletions(-) delete mode 100644 crates/edr_rpc_eth/tests/client.rs diff --git a/crates/edr_rpc_eth/src/client.rs b/crates/edr_rpc_eth/src/client.rs index b254880bd..c96eed268 100644 --- a/crates/edr_rpc_eth/src/client.rs +++ b/crates/edr_rpc_eth/src/client.rs @@ -322,3 +322,683 @@ impl EthRpcClient { .map(|network_id| network_id.as_limbs()[0]) } } + +#[cfg(test)] +mod tests { + use std::{ops::Deref, str::FromStr}; + + use reqwest::StatusCode; + use tempfile::TempDir; + + use super::*; + use crate::spec::EthRpcSpec; + + struct TestRpcClient { + client: EthRpcClient, + + // Need to keep the tempdir around to prevent it from being deleted + // Only accessed when feature = "test-remote", hence the allow. + #[allow(dead_code)] + cache_dir: TempDir, + } + + impl TestRpcClient { + fn new(url: &str) -> Self { + let tempdir = TempDir::new().unwrap(); + Self { + client: EthRpcClient::new(url, tempdir.path().into(), None).expect("url ok"), + cache_dir: tempdir, + } + } + } + + impl Deref for TestRpcClient { + type Target = EthRpcClient; + + fn deref(&self) -> &Self::Target { + &self.client + } + } + + #[tokio::test] + async fn send_request_body_400_status() { + const STATUS_CODE: u16 = 400; + + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("POST", "/") + .with_status(STATUS_CODE.into()) + .with_header("content-type", "text/plain") + .create_async() + .await; + + let hash = + B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933022222") + .expect("failed to parse hash from string"); + + let error = TestRpcClient::new(&server.url()) + .get_transaction_by_hash(hash) + .await + .expect_err("should have failed to due to a HTTP status error"); + + if let RpcClientError::HttpStatus(error) = error { + assert_eq!( + reqwest::Error::from(error).status(), + Some(StatusCode::from_u16(STATUS_CODE).unwrap()) + ); + } else { + unreachable!("Invalid error: {error}"); + } + + mock.assert_async().await; + } + + #[cfg(feature = "test-remote")] + mod alchemy { + use std::{fs::File, path::PathBuf}; + + use edr_eth::{filter::OneOrMore, Address, BlockSpec, Bytes, PreEip1898BlockSpec, U256}; + use edr_test_utils::env::get_alchemy_url; + use walkdir::WalkDir; + + use super::*; + + // The maximum block number that Alchemy allows + const MAX_BLOCK_NUMBER: u64 = u64::MAX >> 1; + + impl TestRpcClient { + fn files_in_cache(&self) -> Vec { + let mut files = Vec::new(); + for entry in WalkDir::new(&self.cache_dir) + .follow_links(true) + .into_iter() + .filter_map(Result::ok) + { + if entry.file_type().is_file() { + files.push(entry.path().to_owned()); + } + } + files + } + } + + #[tokio::test] + async fn get_account_info_unknown_block() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let error = TestRpcClient::new(&alchemy_url) + .get_account_info(dai_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) + .await + .expect_err("should have failed"); + + if let RpcClientError::JsonRpcError { error, .. } = error { + assert_eq!(error.code, -32602); + assert_eq!(error.message, "Unknown block number"); + assert!(error.data.is_none()); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn get_account_infos() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + let hardhat_default_address = + Address::from_str("0xbe862ad9abfe6f22bcb087716c7d89a26051f74c") + .expect("failed to parse address"); + + let account_infos = TestRpcClient::new(&alchemy_url) + .get_account_infos( + &[dai_address, hardhat_default_address], + Some(BlockSpec::latest()), + ) + .await + .expect("should have succeeded"); + + assert_eq!(account_infos.len(), 2); + } + + #[tokio::test] + async fn get_block_by_hash_some() { + let alchemy_url = get_alchemy_url(); + + let hash = B256::from_str( + "0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812", + ) + .expect("failed to parse hash from string"); + + let block = TestRpcClient::new(&alchemy_url) + .get_block_by_hash(hash) + .await + .expect("should have succeeded"); + + assert!(block.is_some()); + let block = block.unwrap(); + + assert_eq!(block.hash, Some(hash)); + assert_eq!(block.transactions.len(), 192); + } + + #[tokio::test] + async fn get_block_by_hash_with_transaction_data_some() { + let alchemy_url = get_alchemy_url(); + + let hash = B256::from_str( + "0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812", + ) + .expect("failed to parse hash from string"); + + let block = TestRpcClient::new(&alchemy_url) + .get_block_by_hash_with_transaction_data(hash) + .await + .expect("should have succeeded"); + + assert!(block.is_some()); + let block = block.unwrap(); + + assert_eq!(block.hash, Some(hash)); + assert_eq!(block.transactions.len(), 192); + } + + #[tokio::test] + async fn get_block_by_number_finalized_resolves() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + assert_eq!(client.files_in_cache().len(), 0); + + client + .get_block_by_number(PreEip1898BlockSpec::finalized()) + .await + .expect("should have succeeded"); + + // Finalized tag should be resolved and stored in cache. + assert_eq!(client.files_in_cache().len(), 1); + } + + #[tokio::test] + async fn get_block_by_number_some() { + let alchemy_url = get_alchemy_url(); + + let block_number = 16222385; + + let block = TestRpcClient::new(&alchemy_url) + .get_block_by_number(PreEip1898BlockSpec::Number(block_number)) + .await + .expect("should have succeeded") + .expect("Block must exist"); + + assert_eq!(block.number, Some(block_number)); + assert_eq!(block.transactions.len(), 102); + } + + #[tokio::test] + async fn get_block_with_transaction_data_cached() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + let block_spec = PreEip1898BlockSpec::Number(16220843); + + assert_eq!(client.files_in_cache().len(), 0); + + let block_from_remote = client + .get_block_by_number_with_transaction_data(block_spec.clone()) + .await + .expect("should have from remote"); + + assert_eq!(client.files_in_cache().len(), 1); + + let block_from_cache = client + .get_block_by_number_with_transaction_data(block_spec.clone()) + .await + .expect("should have from remote"); + + assert_eq!(block_from_remote, block_from_cache); + } + + #[tokio::test] + async fn get_earliest_block_with_transaction_data_resolves() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + + assert_eq!(client.files_in_cache().len(), 0); + + client + .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::earliest()) + .await + .expect("should have succeeded"); + + // Earliest tag should be resolved to block number and it should be cached. + assert_eq!(client.files_in_cache().len(), 1); + } + + #[tokio::test] + async fn get_latest_block() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number(PreEip1898BlockSpec::latest()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_latest_block_with_transaction_data() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::latest()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_pending_block() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number(PreEip1898BlockSpec::pending()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_pending_block_with_transaction_data() { + let alchemy_url = get_alchemy_url(); + + let _block = TestRpcClient::new(&alchemy_url) + .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::pending()) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_logs_some() { + let alchemy_url = get_alchemy_url(); + let logs = TestRpcClient::new(&alchemy_url) + .get_logs_by_range( + BlockSpec::Number(10496585), + BlockSpec::Number(10496585), + Some(OneOrMore::One( + Address::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") + .expect("failed to parse data"), + )), + None, + ) + .await + .expect("failed to get logs"); + + assert_eq!(logs.len(), 12); + // TODO: assert more things about the log(s) + // TODO: consider asserting something about the logs bloom + } + + #[tokio::test] + async fn get_logs_future_from_block() { + let alchemy_url = get_alchemy_url(); + let error = TestRpcClient::new(&alchemy_url) + .get_logs_by_range( + BlockSpec::Number(MAX_BLOCK_NUMBER), + BlockSpec::Number(MAX_BLOCK_NUMBER), + Some(OneOrMore::One( + Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") + .expect("failed to parse data"), + )), + None, + ) + .await + .expect_err("should have failed to get logs"); + + if let RpcClientError::JsonRpcError { error, .. } = error { + assert_eq!(error.code, -32000); + assert_eq!(error.message, "One of the blocks specified in filter (fromBlock, toBlock or blockHash) cannot be found."); + assert!(error.data.is_none()); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn get_logs_future_to_block() { + let alchemy_url = get_alchemy_url(); + let logs = TestRpcClient::new(&alchemy_url) + .get_logs_by_range( + BlockSpec::Number(10496585), + BlockSpec::Number(MAX_BLOCK_NUMBER), + Some(OneOrMore::One( + Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") + .expect("failed to parse data"), + )), + None, + ) + .await + .expect("should have succeeded"); + + assert_eq!(logs, []); + } + + #[tokio::test] + async fn get_transaction_by_hash_some() { + let alchemy_url = get_alchemy_url(); + + let hash = B256::from_str( + "0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a", + ) + .expect("failed to parse hash from string"); + + let tx = TestRpcClient::new(&alchemy_url) + .get_transaction_by_hash(hash) + .await + .expect("failed to get transaction by hash"); + + assert!(tx.is_some()); + let tx = tx.unwrap(); + + assert_eq!( + tx.block_hash, + Some( + B256::from_str( + "0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8" + ) + .expect("couldn't parse data") + ) + ); + assert_eq!( + tx.block_number, + Some(U256::from_str_radix("a74fde", 16).expect("couldn't parse data")) + ); + assert_eq!(tx.hash, hash); + assert_eq!( + tx.from, + Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") + .expect("couldn't parse data") + ); + assert_eq!( + tx.gas, + U256::from_str_radix("30d40", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.gas_price, + U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.input, + Bytes::from(hex::decode("a9059cbb000000000000000000000000e2c1e729e05f34c07d80083982ccd9154045dcc600000000000000000000000000000000000000000000000000000004a817c800").unwrap()) + ); + assert_eq!( + tx.nonce, + u64::from_str_radix("653b", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.r, + U256::from_str_radix( + "eb56df45bd355e182fba854506bc73737df275af5a323d30f98db13fdf44393a", + 16 + ) + .expect("couldn't parse data") + ); + assert_eq!( + tx.s, + U256::from_str_radix( + "2c6efcd210cdc7b3d3191360f796ca84cab25a52ed8f72efff1652adaabc1c83", + 16 + ) + .expect("couldn't parse data") + ); + assert_eq!( + tx.to, + Some( + Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") + .expect("couldn't parse data") + ) + ); + assert_eq!( + tx.transaction_index, + Some(u64::from_str_radix("88", 16).expect("couldn't parse data")) + ); + assert_eq!( + tx.v, + u64::from_str_radix("1c", 16).expect("couldn't parse data") + ); + assert_eq!( + tx.value, + U256::from_str_radix("0", 16).expect("couldn't parse data") + ); + } + + #[tokio::test] + async fn get_transaction_count_some() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let transaction_count = TestRpcClient::new(&alchemy_url) + .get_transaction_count(dai_address, Some(BlockSpec::Number(16220843))) + .await + .expect("should have succeeded"); + + assert_eq!(transaction_count, U256::from(1)); + } + + #[tokio::test] + async fn get_transaction_count_future_block() { + let alchemy_url = get_alchemy_url(); + + let missing_address = Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") + .expect("failed to parse address"); + + let error = TestRpcClient::new(&alchemy_url) + .get_transaction_count(missing_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) + .await + .expect_err("should have failed"); + + if let RpcClientError::JsonRpcError { error, .. } = error { + assert_eq!(error.code, -32602); + assert_eq!(error.message, "Unknown block number"); + assert!(error.data.is_none()); + } else { + unreachable!("Invalid error: {error}"); + } + } + + #[tokio::test] + async fn get_transaction_receipt_some() { + let alchemy_url = get_alchemy_url(); + + let hash = B256::from_str( + "0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a", + ) + .expect("failed to parse hash from string"); + + let receipt = TestRpcClient::new(&alchemy_url) + .get_transaction_receipt(hash) + .await + .expect("failed to get transaction by hash"); + + assert!(receipt.is_some()); + let receipt = receipt.unwrap(); + + assert_eq!( + receipt.block_hash, + B256::from_str( + "0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8" + ) + .expect("couldn't parse data") + ); + assert_eq!(receipt.block_number, 0xa74fde); + assert_eq!(receipt.contract_address, None); + assert_eq!(receipt.cumulative_gas_used(), 0x56c81b); + assert_eq!( + receipt.effective_gas_price, + Some(U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data")) + ); + assert_eq!( + receipt.from, + Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") + .expect("couldn't parse data") + ); + assert_eq!( + receipt.gas_used, + u64::from_str_radix("a0f9", 16).expect("couldn't parse data") + ); + assert_eq!(receipt.logs().len(), 1); + assert_eq!(receipt.state_root(), None); + assert_eq!(receipt.status_code(), Some(1)); + assert_eq!( + receipt.to, + Some( + Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") + .expect("couldn't parse data") + ) + ); + assert_eq!(receipt.transaction_hash, hash); + assert_eq!(receipt.transaction_index, 136); + assert_eq!(receipt.transaction_type(), 0); + } + + #[tokio::test] + async fn get_storage_at_some() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let total_supply = TestRpcClient::new(&alchemy_url) + .get_storage_at( + dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + + assert_eq!( + total_supply, + Some( + U256::from_str_radix( + "000000000000000000000000000000000000000010a596ae049e066d4991945c", + 16 + ) + .expect("failed to parse storage location") + ) + ); + } + + #[tokio::test] + async fn get_storage_at_latest() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let _total_supply = TestRpcClient::new(&alchemy_url) + .get_storage_at( + dai_address, + U256::from_str_radix( + "0000000000000000000000000000000000000000000000000000000000000001", + 16, + ) + .expect("failed to parse storage location"), + Some(BlockSpec::latest()), + ) + .await + .expect("should have succeeded"); + } + + #[tokio::test] + async fn get_storage_at_future_block() { + let alchemy_url = get_alchemy_url(); + + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let storage_slot = TestRpcClient::new(&alchemy_url) + .get_storage_at( + dai_address, + U256::from(1), + Some(BlockSpec::Number(MAX_BLOCK_NUMBER)), + ) + .await + .expect("should have succeeded"); + + assert!(storage_slot.is_none()); + } + + #[tokio::test] + async fn network_id_success() { + let alchemy_url = get_alchemy_url(); + + let version = TestRpcClient::new(&alchemy_url) + .network_id() + .await + .expect("should have succeeded"); + + assert_eq!(version, 1); + } + + #[tokio::test] + async fn stores_result_in_cache() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + let total_supply = client + .get_storage_at( + dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + + let cached_files = client.files_in_cache(); + assert_eq!(cached_files.len(), 1); + + let mut file = File::open(&cached_files[0]).expect("failed to open file"); + let cached_result: Option = + serde_json::from_reader(&mut file).expect("failed to parse"); + + assert_eq!(total_supply, cached_result); + } + + #[tokio::test] + async fn handles_invalid_type_in_cache_single_call() { + let alchemy_url = get_alchemy_url(); + let client = TestRpcClient::new(&alchemy_url); + let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") + .expect("failed to parse address"); + + client + .get_storage_at( + dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + + // Write some valid JSON, but invalid U256 + tokio::fs::write(&client.files_in_cache()[0], "\"not-hex\"") + .await + .unwrap(); + + client + .get_storage_at( + dai_address, + U256::from(1), + Some(BlockSpec::Number(16220843)), + ) + .await + .expect("should have succeeded"); + } + } +} diff --git a/crates/edr_rpc_eth/tests/client.rs b/crates/edr_rpc_eth/tests/client.rs deleted file mode 100644 index a02d5ccbb..000000000 --- a/crates/edr_rpc_eth/tests/client.rs +++ /dev/null @@ -1,669 +0,0 @@ -use std::{ops::Deref, str::FromStr}; - -use edr_eth::B256; -use edr_rpc_client::RpcClientError; -use edr_rpc_eth::{client::EthRpcClient, spec::EthRpcSpec}; -use reqwest::StatusCode; -use tempfile::TempDir; - -struct TestRpcClient { - client: EthRpcClient, - - // Need to keep the tempdir around to prevent it from being deleted - // Only accessed when feature = "test-remote", hence the allow. - #[allow(dead_code)] - cache_dir: TempDir, -} - -impl TestRpcClient { - fn new(url: &str) -> Self { - let tempdir = TempDir::new().unwrap(); - Self { - client: EthRpcClient::new(url, tempdir.path().into(), None).expect("url ok"), - cache_dir: tempdir, - } - } -} - -impl Deref for TestRpcClient { - type Target = EthRpcClient; - - fn deref(&self) -> &Self::Target { - &self.client - } -} - -#[tokio::test] -async fn send_request_body_400_status() { - const STATUS_CODE: u16 = 400; - - let mut server = mockito::Server::new_async().await; - - let mock = server - .mock("POST", "/") - .with_status(STATUS_CODE.into()) - .with_header("content-type", "text/plain") - .create_async() - .await; - - let hash = B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933022222") - .expect("failed to parse hash from string"); - - let error = TestRpcClient::new(&server.url()) - .get_transaction_by_hash(hash) - .await - .expect_err("should have failed to due to a HTTP status error"); - - if let RpcClientError::HttpStatus(error) = error { - assert_eq!( - reqwest::Error::from(error).status(), - Some(StatusCode::from_u16(STATUS_CODE).unwrap()) - ); - } else { - unreachable!("Invalid error: {error}"); - } - - mock.assert_async().await; -} - -#[cfg(feature = "test-remote")] -mod alchemy { - use std::{fs::File, path::PathBuf}; - - use edr_eth::{filter::OneOrMore, Address, BlockSpec, Bytes, PreEip1898BlockSpec, U256}; - use edr_test_utils::env::get_alchemy_url; - use walkdir::WalkDir; - - use super::*; - - // The maximum block number that Alchemy allows - const MAX_BLOCK_NUMBER: u64 = u64::MAX >> 1; - - impl TestRpcClient { - fn files_in_cache(&self) -> Vec { - let mut files = Vec::new(); - for entry in WalkDir::new(&self.cache_dir) - .follow_links(true) - .into_iter() - .filter_map(Result::ok) - { - if entry.file_type().is_file() { - files.push(entry.path().to_owned()); - } - } - files - } - } - - #[tokio::test] - async fn get_account_info_unknown_block() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let error = TestRpcClient::new(&alchemy_url) - .get_account_info(dai_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) - .await - .expect_err("should have failed"); - - if let RpcClientError::JsonRpcError { error, .. } = error { - assert_eq!(error.code, -32602); - assert_eq!(error.message, "Unknown block number"); - assert!(error.data.is_none()); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn get_account_infos() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - let hardhat_default_address = - Address::from_str("0xbe862ad9abfe6f22bcb087716c7d89a26051f74c") - .expect("failed to parse address"); - - let account_infos = TestRpcClient::new(&alchemy_url) - .get_account_infos( - &[dai_address, hardhat_default_address], - Some(BlockSpec::latest()), - ) - .await - .expect("should have succeeded"); - - assert_eq!(account_infos.len(), 2); - } - - #[tokio::test] - async fn get_block_by_hash_some() { - let alchemy_url = get_alchemy_url(); - - let hash = - B256::from_str("0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812") - .expect("failed to parse hash from string"); - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash(hash) - .await - .expect("should have succeeded"); - - assert!(block.is_some()); - let block = block.unwrap(); - - assert_eq!(block.hash, Some(hash)); - assert_eq!(block.transactions.len(), 192); - } - - #[tokio::test] - async fn get_block_by_hash_with_transaction_data_some() { - let alchemy_url = get_alchemy_url(); - - let hash = - B256::from_str("0x71d5e7c8ff9ea737034c16e333a75575a4a94d29482e0c2b88f0a6a8369c1812") - .expect("failed to parse hash from string"); - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_hash_with_transaction_data(hash) - .await - .expect("should have succeeded"); - - assert!(block.is_some()); - let block = block.unwrap(); - - assert_eq!(block.hash, Some(hash)); - assert_eq!(block.transactions.len(), 192); - } - - #[tokio::test] - async fn get_block_by_number_finalized_resolves() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - assert_eq!(client.files_in_cache().len(), 0); - - client - .get_block_by_number(PreEip1898BlockSpec::finalized()) - .await - .expect("should have succeeded"); - - // Finalized tag should be resolved and stored in cache. - assert_eq!(client.files_in_cache().len(), 1); - } - - #[tokio::test] - async fn get_block_by_number_some() { - let alchemy_url = get_alchemy_url(); - - let block_number = 16222385; - - let block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(PreEip1898BlockSpec::Number(block_number)) - .await - .expect("should have succeeded") - .expect("Block must exist"); - - assert_eq!(block.number, Some(block_number)); - assert_eq!(block.transactions.len(), 102); - } - - #[tokio::test] - async fn get_block_with_transaction_data_cached() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - let block_spec = PreEip1898BlockSpec::Number(16220843); - - assert_eq!(client.files_in_cache().len(), 0); - - let block_from_remote = client - .get_block_by_number_with_transaction_data(block_spec.clone()) - .await - .expect("should have from remote"); - - assert_eq!(client.files_in_cache().len(), 1); - - let block_from_cache = client - .get_block_by_number_with_transaction_data(block_spec.clone()) - .await - .expect("should have from remote"); - - assert_eq!(block_from_remote, block_from_cache); - } - - #[tokio::test] - async fn get_earliest_block_with_transaction_data_resolves() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - - assert_eq!(client.files_in_cache().len(), 0); - - client - .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::earliest()) - .await - .expect("should have succeeded"); - - // Earliest tag should be resolved to block number and it should be cached. - assert_eq!(client.files_in_cache().len(), 1); - } - - #[tokio::test] - async fn get_latest_block() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(PreEip1898BlockSpec::latest()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_latest_block_with_transaction_data() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::latest()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_pending_block() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number(PreEip1898BlockSpec::pending()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_pending_block_with_transaction_data() { - let alchemy_url = get_alchemy_url(); - - let _block = TestRpcClient::new(&alchemy_url) - .get_block_by_number_with_transaction_data(PreEip1898BlockSpec::pending()) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_logs_some() { - let alchemy_url = get_alchemy_url(); - let logs = TestRpcClient::new(&alchemy_url) - .get_logs_by_range( - BlockSpec::Number(10496585), - BlockSpec::Number(10496585), - Some(OneOrMore::One( - Address::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") - .expect("failed to parse data"), - )), - None, - ) - .await - .expect("failed to get logs"); - - assert_eq!(logs.len(), 12); - // TODO: assert more things about the log(s) - // TODO: consider asserting something about the logs bloom - } - - #[tokio::test] - async fn get_logs_future_from_block() { - let alchemy_url = get_alchemy_url(); - let error = TestRpcClient::new(&alchemy_url) - .get_logs_by_range( - BlockSpec::Number(MAX_BLOCK_NUMBER), - BlockSpec::Number(MAX_BLOCK_NUMBER), - Some(OneOrMore::One( - Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse data"), - )), - None, - ) - .await - .expect_err("should have failed to get logs"); - - if let RpcClientError::JsonRpcError { error, .. } = error { - assert_eq!(error.code, -32000); - assert_eq!(error.message, "One of the blocks specified in filter (fromBlock, toBlock or blockHash) cannot be found."); - assert!(error.data.is_none()); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn get_logs_future_to_block() { - let alchemy_url = get_alchemy_url(); - let logs = TestRpcClient::new(&alchemy_url) - .get_logs_by_range( - BlockSpec::Number(10496585), - BlockSpec::Number(MAX_BLOCK_NUMBER), - Some(OneOrMore::One( - Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse data"), - )), - None, - ) - .await - .expect("should have succeeded"); - - assert_eq!(logs, []); - } - - #[tokio::test] - async fn get_transaction_by_hash_some() { - let alchemy_url = get_alchemy_url(); - - let hash = - B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a") - .expect("failed to parse hash from string"); - - let tx = TestRpcClient::new(&alchemy_url) - .get_transaction_by_hash(hash) - .await - .expect("failed to get transaction by hash"); - - assert!(tx.is_some()); - let tx = tx.unwrap(); - - assert_eq!( - tx.block_hash, - Some( - B256::from_str( - "0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8" - ) - .expect("couldn't parse data") - ) - ); - assert_eq!( - tx.block_number, - Some(U256::from_str_radix("a74fde", 16).expect("couldn't parse data")) - ); - assert_eq!(tx.hash, hash); - assert_eq!( - tx.from, - Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") - .expect("couldn't parse data") - ); - assert_eq!( - tx.gas, - U256::from_str_radix("30d40", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.gas_price, - U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.input, - Bytes::from(hex::decode("a9059cbb000000000000000000000000e2c1e729e05f34c07d80083982ccd9154045dcc600000000000000000000000000000000000000000000000000000004a817c800").unwrap()) - ); - assert_eq!( - tx.nonce, - u64::from_str_radix("653b", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.r, - U256::from_str_radix( - "eb56df45bd355e182fba854506bc73737df275af5a323d30f98db13fdf44393a", - 16 - ) - .expect("couldn't parse data") - ); - assert_eq!( - tx.s, - U256::from_str_radix( - "2c6efcd210cdc7b3d3191360f796ca84cab25a52ed8f72efff1652adaabc1c83", - 16 - ) - .expect("couldn't parse data") - ); - assert_eq!( - tx.to, - Some( - Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") - .expect("couldn't parse data") - ) - ); - assert_eq!( - tx.transaction_index, - Some(u64::from_str_radix("88", 16).expect("couldn't parse data")) - ); - assert_eq!( - tx.v, - u64::from_str_radix("1c", 16).expect("couldn't parse data") - ); - assert_eq!( - tx.value, - U256::from_str_radix("0", 16).expect("couldn't parse data") - ); - } - - #[tokio::test] - async fn get_transaction_count_some() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let transaction_count = TestRpcClient::new(&alchemy_url) - .get_transaction_count(dai_address, Some(BlockSpec::Number(16220843))) - .await - .expect("should have succeeded"); - - assert_eq!(transaction_count, U256::from(1)); - } - - #[tokio::test] - async fn get_transaction_count_future_block() { - let alchemy_url = get_alchemy_url(); - - let missing_address = Address::from_str("0xffffffffffffffffffffffffffffffffffffffff") - .expect("failed to parse address"); - - let error = TestRpcClient::new(&alchemy_url) - .get_transaction_count(missing_address, Some(BlockSpec::Number(MAX_BLOCK_NUMBER))) - .await - .expect_err("should have failed"); - - if let RpcClientError::JsonRpcError { error, .. } = error { - assert_eq!(error.code, -32602); - assert_eq!(error.message, "Unknown block number"); - assert!(error.data.is_none()); - } else { - unreachable!("Invalid error: {error}"); - } - } - - #[tokio::test] - async fn get_transaction_receipt_some() { - let alchemy_url = get_alchemy_url(); - - let hash = - B256::from_str("0xc008e9f9bb92057dd0035496fbf4fb54f66b4b18b370928e46d6603933054d5a") - .expect("failed to parse hash from string"); - - let receipt = TestRpcClient::new(&alchemy_url) - .get_transaction_receipt(hash) - .await - .expect("failed to get transaction by hash"); - - assert!(receipt.is_some()); - let receipt = receipt.unwrap(); - - assert_eq!( - receipt.block_hash, - B256::from_str("0x88fadbb673928c61b9ede3694ae0589ac77ae38ec90a24a6e12e83f42f18c7e8") - .expect("couldn't parse data") - ); - assert_eq!(receipt.block_number, 0xa74fde); - assert_eq!(receipt.contract_address, None); - assert_eq!(receipt.cumulative_gas_used(), 0x56c81b); - assert_eq!( - receipt.effective_gas_price, - Some(U256::from_str_radix("1e449a99b8", 16).expect("couldn't parse data")) - ); - assert_eq!( - receipt.from, - Address::from_str("0x7d97fcdb98632a91be79d3122b4eb99c0c4223ee") - .expect("couldn't parse data") - ); - assert_eq!( - receipt.gas_used, - u64::from_str_radix("a0f9", 16).expect("couldn't parse data") - ); - assert_eq!(receipt.logs().len(), 1); - assert_eq!(receipt.state_root(), None); - assert_eq!(receipt.status_code(), Some(1)); - assert_eq!( - receipt.to, - Some( - Address::from_str("dac17f958d2ee523a2206206994597c13d831ec7") - .expect("couldn't parse data") - ) - ); - assert_eq!(receipt.transaction_hash, hash); - assert_eq!(receipt.transaction_index, 136); - assert_eq!(receipt.transaction_type(), 0); - } - - #[tokio::test] - async fn get_storage_at_some() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let total_supply = TestRpcClient::new(&alchemy_url) - .get_storage_at( - dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - - assert_eq!( - total_supply, - Some( - U256::from_str_radix( - "000000000000000000000000000000000000000010a596ae049e066d4991945c", - 16 - ) - .expect("failed to parse storage location") - ) - ); - } - - #[tokio::test] - async fn get_storage_at_latest() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let _total_supply = TestRpcClient::new(&alchemy_url) - .get_storage_at( - dai_address, - U256::from_str_radix( - "0000000000000000000000000000000000000000000000000000000000000001", - 16, - ) - .expect("failed to parse storage location"), - Some(BlockSpec::latest()), - ) - .await - .expect("should have succeeded"); - } - - #[tokio::test] - async fn get_storage_at_future_block() { - let alchemy_url = get_alchemy_url(); - - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let storage_slot = TestRpcClient::new(&alchemy_url) - .get_storage_at( - dai_address, - U256::from(1), - Some(BlockSpec::Number(MAX_BLOCK_NUMBER)), - ) - .await - .expect("should have succeeded"); - - assert!(storage_slot.is_none()); - } - - #[tokio::test] - async fn network_id_success() { - let alchemy_url = get_alchemy_url(); - - let version = TestRpcClient::new(&alchemy_url) - .network_id() - .await - .expect("should have succeeded"); - - assert_eq!(version, 1); - } - - #[tokio::test] - async fn stores_result_in_cache() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - let total_supply = client - .get_storage_at( - dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - - let cached_files = client.files_in_cache(); - assert_eq!(cached_files.len(), 1); - - let mut file = File::open(&cached_files[0]).expect("failed to open file"); - let cached_result: Option = - serde_json::from_reader(&mut file).expect("failed to parse"); - - assert_eq!(total_supply, cached_result); - } - - #[tokio::test] - async fn handles_invalid_type_in_cache_single_call() { - let alchemy_url = get_alchemy_url(); - let client = TestRpcClient::new(&alchemy_url); - let dai_address = Address::from_str("0x6b175474e89094c44da98b954eedeac495271d0f") - .expect("failed to parse address"); - - client - .get_storage_at( - dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - - // Write some valid JSON, but invalid U256 - tokio::fs::write(&client.files_in_cache()[0], "\"not-hex\"") - .await - .unwrap(); - - client - .get_storage_at( - dai_address, - U256::from(1), - Some(BlockSpec::Number(16220843)), - ) - .await - .expect("should have succeeded"); - } -}