diff --git a/Cargo.lock b/Cargo.lock index c7186cf9..1cb80bc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1525,7 +1525,6 @@ name = "evm_rpc" version = "0.1.0" dependencies = [ "assert_matches", - "async-trait", "candid", "ethers-core", "ethnum", diff --git a/Cargo.toml b/Cargo.toml index d66aa5e5..b8ec989f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,10 +43,9 @@ serde = { workspace = true } serde_json = { workspace = true } thousands = "0.2" url = "2.5" -async-trait = "0.1" hex = "0.4" ethers-core = "2.0" -zeroize = {version = "1.8", features = ["zeroize_derive"]} +zeroize = { version = "1.8", features = ["zeroize_derive"] } [dev-dependencies] assert_matches = "1.5" diff --git a/src/candid_rpc.rs b/src/candid_rpc.rs index c5d42c1b..17a4e7ce 100644 --- a/src/candid_rpc.rs +++ b/src/candid_rpc.rs @@ -1,79 +1,15 @@ mod cketh_conversion; -use async_trait::async_trait; -use candid::Nat; -use ethers_core::{types::Transaction, utils::rlp}; -use evm_rpc_types::{ - Hex, Hex32, MultiRpcResult, ProviderError, RpcApi, RpcError, RpcResult, RpcService, - RpcServices, ValidationError, -}; -use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse}; - -use crate::constants::{ - DEFAULT_ETH_MAINNET_SERVICES, DEFAULT_ETH_SEPOLIA_SERVICES, DEFAULT_L2_MAINNET_SERVICES, -}; -use crate::rpc_client::{EthRpcClient, MultiCallError, RpcTransport}; +use crate::rpc_client::{EthRpcClient, MultiCallError}; use crate::{ - accounting::get_http_request_cost, add_metric_entry, constants::ETH_GET_LOGS_MAX_BLOCKS, - http::http_request, providers::resolve_rpc_service, - types::{MetricRpcHost, MetricRpcMethod, ResolvedRpcService, RpcMethod}, + types::{MetricRpcHost, ResolvedRpcService, RpcMethod}, }; - -#[derive(Clone, Debug, PartialEq, Eq)] -struct CanisterTransport; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl RpcTransport for CanisterTransport { - fn resolve_api(service: &RpcService) -> Result { - Ok(resolve_rpc_service(service.clone())?.api()) - } - - async fn http_request( - service: &RpcService, - method: &str, - request: CanisterHttpRequestArgument, - effective_response_size_estimate: u64, - ) -> Result { - let service = resolve_rpc_service(service.clone())?; - let cycles_cost = get_http_request_cost( - request - .body - .as_ref() - .map(|bytes| bytes.len() as u64) - .unwrap_or_default(), - effective_response_size_estimate, - ); - let rpc_method = MetricRpcMethod(method.to_string()); - http_request(rpc_method, service, request, cycles_cost).await - } -} - -fn check_services(services: Vec) -> RpcResult> { - if services.is_empty() { - Err(ProviderError::ProviderNotFound)?; - } - Ok(services) -} - -fn get_rpc_client( - source: RpcServices, - config: evm_rpc_types::RpcConfig, -) -> RpcResult> { - use crate::candid_rpc::cketh_conversion::{into_ethereum_network, into_rpc_services}; - - let chain = into_ethereum_network(&source); - let providers = check_services(into_rpc_services( - source, - DEFAULT_ETH_MAINNET_SERVICES, - DEFAULT_ETH_SEPOLIA_SERVICES, - DEFAULT_L2_MAINNET_SERVICES, - ))?; - Ok(EthRpcClient::new(chain, Some(providers), config)) -} +use candid::Nat; +use ethers_core::{types::Transaction, utils::rlp}; +use evm_rpc_types::{Hex, Hex32, MultiRpcResult, RpcResult, ValidationError}; fn process_result(method: RpcMethod, result: Result>) -> MultiRpcResult { match result { @@ -106,7 +42,7 @@ fn process_result(method: RpcMethod, result: Result>) -> } pub struct CandidRpcClient { - client: EthRpcClient, + client: EthRpcClient, } impl CandidRpcClient { @@ -115,7 +51,7 @@ impl CandidRpcClient { config: Option, ) -> RpcResult { Ok(Self { - client: get_rpc_client(source, config.unwrap_or_default())?, + client: EthRpcClient::new(source, config)?, }) } @@ -232,7 +168,7 @@ fn get_transaction_hash(raw_signed_transaction_hex: &Hex) -> Option { mod test { use super::*; use crate::rpc_client::{MultiCallError, MultiCallResults}; - use evm_rpc_types::RpcError; + use evm_rpc_types::{ProviderError, RpcError}; #[test] fn test_process_result_mapping() { diff --git a/src/candid_rpc/cketh_conversion.rs b/src/candid_rpc/cketh_conversion.rs index 777de11e..c31b1d3d 100644 --- a/src/candid_rpc/cketh_conversion.rs +++ b/src/candid_rpc/cketh_conversion.rs @@ -182,65 +182,6 @@ pub(super) fn from_send_raw_transaction_result( } } -pub(super) fn into_ethereum_network( - source: &evm_rpc_types::RpcServices, -) -> crate::rpc_client::EthereumNetwork { - match &source { - evm_rpc_types::RpcServices::Custom { chain_id, .. } => { - crate::rpc_client::EthereumNetwork::from(*chain_id) - } - evm_rpc_types::RpcServices::EthMainnet(_) => crate::rpc_client::EthereumNetwork::MAINNET, - evm_rpc_types::RpcServices::EthSepolia(_) => crate::rpc_client::EthereumNetwork::SEPOLIA, - evm_rpc_types::RpcServices::ArbitrumOne(_) => crate::rpc_client::EthereumNetwork::ARBITRUM, - evm_rpc_types::RpcServices::BaseMainnet(_) => crate::rpc_client::EthereumNetwork::BASE, - evm_rpc_types::RpcServices::OptimismMainnet(_) => { - crate::rpc_client::EthereumNetwork::OPTIMISM - } - } -} - -pub(super) fn into_rpc_services( - source: evm_rpc_types::RpcServices, - default_eth_mainnet_services: &[evm_rpc_types::EthMainnetService], - default_eth_sepolia_services: &[evm_rpc_types::EthSepoliaService], - default_l2_mainnet_services: &[evm_rpc_types::L2MainnetService], -) -> Vec { - match source { - evm_rpc_types::RpcServices::Custom { - chain_id: _, - services, - } => services - .into_iter() - .map(evm_rpc_types::RpcService::Custom) - .collect(), - evm_rpc_types::RpcServices::EthMainnet(services) => services - .unwrap_or_else(|| default_eth_mainnet_services.to_vec()) - .into_iter() - .map(evm_rpc_types::RpcService::EthMainnet) - .collect(), - evm_rpc_types::RpcServices::EthSepolia(services) => services - .unwrap_or_else(|| default_eth_sepolia_services.to_vec()) - .into_iter() - .map(evm_rpc_types::RpcService::EthSepolia) - .collect(), - evm_rpc_types::RpcServices::ArbitrumOne(services) => services - .unwrap_or_else(|| default_l2_mainnet_services.to_vec()) - .into_iter() - .map(evm_rpc_types::RpcService::ArbitrumOne) - .collect(), - evm_rpc_types::RpcServices::BaseMainnet(services) => services - .unwrap_or_else(|| default_l2_mainnet_services.to_vec()) - .into_iter() - .map(evm_rpc_types::RpcService::BaseMainnet) - .collect(), - evm_rpc_types::RpcServices::OptimismMainnet(services) => services - .unwrap_or_else(|| default_l2_mainnet_services.to_vec()) - .into_iter() - .map(evm_rpc_types::RpcService::OptimismMainnet) - .collect(), - } -} - pub(super) fn into_hash(value: Hex32) -> Hash { Hash(value.into()) } diff --git a/src/constants.rs b/src/constants.rs index a6b84c42..e6a6be73 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,5 +1,3 @@ -use evm_rpc_types::{EthMainnetService, EthSepoliaService, L2MainnetService}; - // HTTP outcall cost calculation // See https://internetcomputer.org/docs/current/developer-docs/gas-cost#special-features pub const INGRESS_OVERHEAD_BYTES: u128 = 100; @@ -36,23 +34,6 @@ pub const API_KEY_REPLACE_STRING: &str = "{API_KEY}"; pub const VALID_API_KEY_CHARS: &str = "0123456789ABCDEFGHIJKLMNOPQRTSUVWXYZabcdefghijklmnopqrstuvwxyz$-_.+!*"; -// Providers used by default (when passing `null` with `RpcServices`) -pub const DEFAULT_ETH_MAINNET_SERVICES: &[EthMainnetService] = &[ - EthMainnetService::Ankr, - EthMainnetService::Cloudflare, - EthMainnetService::PublicNode, -]; -pub const DEFAULT_ETH_SEPOLIA_SERVICES: &[EthSepoliaService] = &[ - EthSepoliaService::Ankr, - EthSepoliaService::BlockPi, - EthSepoliaService::PublicNode, -]; -pub const DEFAULT_L2_MAINNET_SERVICES: &[L2MainnetService] = &[ - L2MainnetService::Ankr, - L2MainnetService::BlockPi, - L2MainnetService::PublicNode, -]; - pub const CONTENT_TYPE_HEADER_LOWERCASE: &str = "content-type"; pub const CONTENT_TYPE_VALUE: &str = "application/json"; diff --git a/src/rpc_client/eth_rpc/mod.rs b/src/rpc_client/eth_rpc/mod.rs index b9702f7c..dc600327 100644 --- a/src/rpc_client/eth_rpc/mod.rs +++ b/src/rpc_client/eth_rpc/mod.rs @@ -1,16 +1,18 @@ //! This module contains definitions for communicating with an Ethereum API using the [JSON RPC](https://ethereum.org/en/developers/docs/apis/json-rpc/) //! interface. +use crate::accounting::get_http_request_cost; use crate::logs::{DEBUG, TRACE_HTTP}; use crate::memory::next_request_id; +use crate::providers::resolve_rpc_service; use crate::rpc_client::checked_amount::CheckedAmountOf; use crate::rpc_client::eth_rpc_error::{sanitize_send_raw_transaction_result, Parser}; use crate::rpc_client::numeric::{BlockNumber, LogIndex, TransactionCount, Wei, WeiPerGas}; use crate::rpc_client::responses::TransactionReceipt; -use crate::rpc_client::RpcTransport; +use crate::types::MetricRpcMethod; use candid::candid_method; use ethnum; -use evm_rpc_types::{HttpOutcallError, JsonRpcError, RpcError, RpcService}; +use evm_rpc_types::{HttpOutcallError, JsonRpcError, ProviderError, RpcApi, RpcError, RpcService}; use ic_canister_log::log; use ic_cdk::api::call::RejectionCode; use ic_cdk::api::management_canister::http_request::{ @@ -615,14 +617,13 @@ impl HttpResponsePayload for Option {} impl HttpResponsePayload for TransactionCount {} /// Calls a JSON-RPC method on an Ethereum node at the specified URL. -pub async fn call( +pub async fn call( provider: &RpcService, method: impl Into, params: I, mut response_size_estimate: ResponseSizeEstimate, ) -> Result where - T: RpcTransport, I: Serialize, O: DeserializeOwned + HttpResponsePayload, { @@ -633,7 +634,7 @@ where method: eth_method.clone(), id: 1, }; - let api = T::resolve_api(provider)?; + let api = resolve_api(provider)?; let url = &api.url; let mut headers = vec![HttpHeader { name: "Content-Type".to_string(), @@ -673,13 +674,8 @@ where )), }; - let response = match T::http_request( - provider, - ð_method, - request, - effective_size_estimate, - ) - .await + let response = match http_request(provider, ð_method, request, effective_size_estimate) + .await { Err(RpcError::HttpOutcallError(HttpOutcallError::IcError { code, message })) if is_response_too_large(&code, &message) => @@ -730,6 +726,29 @@ where } } +fn resolve_api(service: &RpcService) -> Result { + Ok(resolve_rpc_service(service.clone())?.api()) +} + +async fn http_request( + service: &RpcService, + method: &str, + request: CanisterHttpRequestArgument, + effective_response_size_estimate: u64, +) -> Result { + let service = resolve_rpc_service(service.clone())?; + let cycles_cost = get_http_request_cost( + request + .body + .as_ref() + .map(|bytes| bytes.len() as u64) + .unwrap_or_default(), + effective_response_size_estimate, + ); + let rpc_method = MetricRpcMethod(method.to_string()); + crate::http::http_request(rpc_method, service, request, cycles_cost).await +} + fn http_status_code(response: &HttpResponse) -> u16 { use num_traits::cast::ToPrimitive; // HTTP status code are always 3 decimal digits, hence at most 999. diff --git a/src/rpc_client/mod.rs b/src/rpc_client/mod.rs index 456111f4..d881f17a 100644 --- a/src/rpc_client/mod.rs +++ b/src/rpc_client/mod.rs @@ -5,68 +5,27 @@ use crate::rpc_client::eth_rpc::{ SendRawTransactionResult, HEADER_SIZE_LIMIT, }; use crate::rpc_client::numeric::TransactionCount; -use crate::rpc_client::providers::{ - ARBITRUM_PROVIDERS, BASE_PROVIDERS, MAINNET_PROVIDERS, OPTIMISM_PROVIDERS, SEPOLIA_PROVIDERS, - UNKNOWN_PROVIDERS, -}; use crate::rpc_client::requests::GetTransactionCountParams; use crate::rpc_client::responses::TransactionReceipt; -use async_trait::async_trait; use evm_rpc_types::{ - HttpOutcallError, JsonRpcError, ProviderError, RpcApi, RpcConfig, RpcError, RpcService, + EthMainnetService, EthSepoliaService, HttpOutcallError, JsonRpcError, L2MainnetService, + ProviderError, RpcConfig, RpcError, RpcService, RpcServices, }; use ic_canister_log::log; -use ic_cdk::api::management_canister::http_request::{CanisterHttpRequestArgument, HttpResponse}; use serde::{de::DeserializeOwned, Serialize}; use std::collections::BTreeMap; use std::fmt::Debug; -use std::marker::PhantomData; pub mod checked_amount; pub(crate) mod eth_rpc; mod eth_rpc_error; mod numeric; -mod providers; pub(crate) mod requests; pub(crate) mod responses; #[cfg(test)] mod tests; -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait RpcTransport: Debug { - fn resolve_api(provider: &RpcService) -> Result; - - async fn http_request( - provider: &RpcService, - method: &str, - request: CanisterHttpRequestArgument, - effective_size_estimate: u64, - ) -> Result; -} - -// Placeholder during refactoring -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct DefaultTransport; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl RpcTransport for DefaultTransport { - fn resolve_api(_provider: &RpcService) -> Result { - unimplemented!() - } - - async fn http_request( - _provider: &RpcService, - _method: &str, - _request: CanisterHttpRequestArgument, - _effective_size_estimate: u64, - ) -> Result { - unimplemented!() - } -} - #[derive(Clone, Copy, Default, Debug, Eq, PartialEq)] pub struct EthereumNetwork(u64); @@ -89,39 +48,91 @@ impl EthereumNetwork { } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct EthRpcClient { +pub struct EthRpcClient { chain: EthereumNetwork, - providers: Option>, + /// *Non-empty* list of providers to query. + providers: Vec, config: RpcConfig, - phantom: PhantomData, } -impl EthRpcClient { - pub const fn new( - chain: EthereumNetwork, - providers: Option>, - config: RpcConfig, - ) -> Self { - Self { +impl EthRpcClient { + pub fn new(source: RpcServices, config: Option) -> Result { + const DEFAULT_ETH_MAINNET_SERVICES: &[EthMainnetService] = &[ + EthMainnetService::Ankr, + EthMainnetService::Cloudflare, + EthMainnetService::PublicNode, + ]; + const DEFAULT_ETH_SEPOLIA_SERVICES: &[EthSepoliaService] = &[ + EthSepoliaService::Ankr, + EthSepoliaService::BlockPi, + EthSepoliaService::PublicNode, + ]; + const DEFAULT_L2_MAINNET_SERVICES: &[L2MainnetService] = &[ + L2MainnetService::Ankr, + L2MainnetService::BlockPi, + L2MainnetService::PublicNode, + ]; + + let (chain, providers): (_, Vec<_>) = match source { + RpcServices::Custom { chain_id, services } => ( + EthereumNetwork::from(chain_id), + services.into_iter().map(RpcService::Custom).collect(), + ), + RpcServices::EthMainnet(services) => ( + EthereumNetwork::MAINNET, + services + .unwrap_or_else(|| DEFAULT_ETH_MAINNET_SERVICES.to_vec()) + .into_iter() + .map(RpcService::EthMainnet) + .collect(), + ), + RpcServices::EthSepolia(services) => ( + EthereumNetwork::SEPOLIA, + services + .unwrap_or_else(|| DEFAULT_ETH_SEPOLIA_SERVICES.to_vec()) + .into_iter() + .map(RpcService::EthSepolia) + .collect(), + ), + RpcServices::ArbitrumOne(services) => ( + EthereumNetwork::ARBITRUM, + services + .unwrap_or_else(|| DEFAULT_L2_MAINNET_SERVICES.to_vec()) + .into_iter() + .map(RpcService::ArbitrumOne) + .collect(), + ), + RpcServices::BaseMainnet(services) => ( + EthereumNetwork::BASE, + services + .unwrap_or_else(|| DEFAULT_L2_MAINNET_SERVICES.to_vec()) + .into_iter() + .map(RpcService::BaseMainnet) + .collect(), + ), + RpcServices::OptimismMainnet(services) => ( + EthereumNetwork::OPTIMISM, + services + .unwrap_or_else(|| DEFAULT_L2_MAINNET_SERVICES.to_vec()) + .into_iter() + .map(RpcService::OptimismMainnet) + .collect(), + ), + }; + + if providers.is_empty() { + return Err(ProviderError::ProviderNotFound); + } + + Ok(Self { chain, providers, - config, - phantom: PhantomData, - } + config: config.unwrap_or_default(), + }) } fn providers(&self) -> &[RpcService] { - match self.providers { - Some(ref providers) => providers, - None => match self.chain { - EthereumNetwork::MAINNET => MAINNET_PROVIDERS, - EthereumNetwork::SEPOLIA => SEPOLIA_PROVIDERS, - EthereumNetwork::ARBITRUM => ARBITRUM_PROVIDERS, - EthereumNetwork::BASE => BASE_PROVIDERS, - EthereumNetwork::OPTIMISM => OPTIMISM_PROVIDERS, - _ => UNKNOWN_PROVIDERS, - }, - } + &self.providers } fn response_size_estimate(&self, estimate: u64) -> ResponseSizeEstimate { @@ -150,7 +161,7 @@ impl EthRpcClient { "[sequential_call_until_ok]: calling provider: {:?}", provider ); - let result = eth_rpc::call::( + let result = eth_rpc::call::<_, _>( provider, method.clone(), params.clone(), @@ -196,7 +207,7 @@ impl EthRpcClient { for provider in providers { log!(DEBUG, "[parallel_call]: will call provider: {:?}", provider); fut.push(async { - eth_rpc::call::( + eth_rpc::call::<_, _>( provider, method.clone(), params.clone(), diff --git a/src/rpc_client/tests.rs b/src/rpc_client/tests.rs index 27008169..e1918142 100644 --- a/src/rpc_client/tests.rs +++ b/src/rpc_client/tests.rs @@ -1,41 +1,57 @@ mod eth_rpc_client { - use crate::rpc_client::{DefaultTransport, EthRpcClient, EthereumNetwork}; - use evm_rpc_types::{EthMainnetService, EthSepoliaService, RpcConfig, RpcService}; + use crate::rpc_client::EthRpcClient; + use evm_rpc_types::{EthMainnetService, ProviderError, RpcService, RpcServices}; #[test] - fn should_retrieve_sepolia_providers_in_stable_order() { - let client: EthRpcClient = - EthRpcClient::new(EthereumNetwork::SEPOLIA, None, RpcConfig::default()); - - let providers = client.providers(); + fn should_fail_when_providers_explicitly_set_to_empty() { + for empty_source in [ + RpcServices::Custom { + chain_id: 1, + services: vec![], + }, + RpcServices::EthMainnet(Some(vec![])), + RpcServices::EthSepolia(Some(vec![])), + RpcServices::ArbitrumOne(Some(vec![])), + RpcServices::BaseMainnet(Some(vec![])), + RpcServices::OptimismMainnet(Some(vec![])), + ] { + assert_eq!( + EthRpcClient::new(empty_source, None), + Err(ProviderError::ProviderNotFound) + ); + } + } - assert_eq!( - providers, - &[ - RpcService::EthSepolia(EthSepoliaService::Alchemy), - RpcService::EthSepolia(EthSepoliaService::Ankr), - RpcService::EthSepolia(EthSepoliaService::BlockPi), - RpcService::EthSepolia(EthSepoliaService::PublicNode), - RpcService::EthSepolia(EthSepoliaService::Sepolia) - ] - ); + #[test] + fn should_use_default_providers() { + for empty_source in [ + RpcServices::EthMainnet(None), + RpcServices::EthSepolia(None), + RpcServices::ArbitrumOne(None), + RpcServices::BaseMainnet(None), + RpcServices::OptimismMainnet(None), + ] { + let client = EthRpcClient::new(empty_source, None).unwrap(); + assert!(!client.providers().is_empty()); + } } #[test] - fn should_retrieve_mainnet_providers_in_stable_order() { - let client: EthRpcClient = - EthRpcClient::new(EthereumNetwork::MAINNET, None, RpcConfig::default()); + fn should_use_specified_provider() { + let provider1 = EthMainnetService::Alchemy; + let provider2 = EthMainnetService::PublicNode; - let providers = client.providers(); + let client = EthRpcClient::new( + RpcServices::EthMainnet(Some(vec![provider1, provider2])), + None, + ) + .unwrap(); assert_eq!( - providers, + client.providers(), &[ - RpcService::EthMainnet(EthMainnetService::Alchemy), - RpcService::EthMainnet(EthMainnetService::Ankr), - RpcService::EthMainnet(EthMainnetService::PublicNode), - RpcService::EthMainnet(EthMainnetService::Cloudflare), - RpcService::EthMainnet(EthMainnetService::Llama) + RpcService::EthMainnet(provider1), + RpcService::EthMainnet(provider2) ] ); }