From b3ebd0900ed59cdffc7e79644954734617d3a1e9 Mon Sep 17 00:00:00 2001 From: gregorydemay <112856886+gregorydemay@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:23:45 +0100 Subject: [PATCH] feat: allow to override provider url upon installation (#346) Allow to override the URL that will be queried by the EVM RPC canister via a regex specified upon canister installation. This is allows testing against a local Ethereum development setup (such as [foundry](https://github.com/foundry-rs/foundry)) without changing the client calling the EVM RPC canister. --- .github/workflows/ci.yml | 11 +- candid/evm_rpc.did | 10 ++ dfx.json | 7 ++ evm_rpc_types/src/lib.rs | 2 +- evm_rpc_types/src/lifecycle/mod.rs | 14 +++ scripts/e2e | 1 + scripts/examples | 7 +- src/http.rs | 15 ++- src/main.rs | 12 +- src/memory.rs | 19 +++- src/rpc_client/eth_rpc/mod.rs | 23 ++-- src/types.rs | 175 +++++++++++++++++++---------- src/types/tests.rs | 130 +++++++++++++++++++++ tests/tests.rs | 3 +- 14 files changed, 340 insertions(+), 89 deletions(-) create mode 100644 src/types/tests.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0bada21..f664b513 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,16 @@ jobs: run: scripts/e2e - name: Run examples - run: scripts/examples + run: scripts/examples evm_rpc 'Number = 20000000' + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Run anvil + run: anvil & + + - name: Run local examples with Foundry + run: scripts/examples evm_rpc_local 'Number = 0' - name: Check formatting run: cargo fmt --all -- --check diff --git a/candid/evm_rpc.did b/candid/evm_rpc.did index 4f38aeee..d2f43fae 100644 --- a/candid/evm_rpc.did +++ b/candid/evm_rpc.did @@ -107,6 +107,7 @@ type InstallArgs = record { demo : opt bool; manageApiKeys : opt vec principal; logFilter : opt LogFilter; + overrideProvider : opt OverrideProvider; }; type Regex = text; type LogFilter = variant { @@ -115,6 +116,15 @@ type LogFilter = variant { ShowPattern : Regex; HidePattern : Regex; }; +type RegexSubstitution = record { + pattern : Regex; + replacement: text; +}; +// Override resolved provider. +// Useful for testing with a local Ethereum developer environment such as foundry. +type OverrideProvider = record { + overrideUrl : opt RegexSubstitution +}; type JsonRpcError = record { code : int64; message : text }; type LogEntry = record { transactionHash : opt text; diff --git a/dfx.json b/dfx.json index 2fcb0c8d..44e6f54d 100644 --- a/dfx.json +++ b/dfx.json @@ -24,6 +24,13 @@ "gzip": true, "init_arg": "(record {demo = opt true})" }, + "evm_rpc_local": { + "candid": "candid/evm_rpc.did", + "type": "rust", + "package": "evm_rpc", + "gzip": true, + "init_arg": "( record { overrideProvider = opt record { overrideUrl = opt record { pattern = \".*\"; replacement = \"http://127.0.0.1:8545\" } } })" + }, "evm_rpc_staging": { "candid": "candid/evm_rpc.did", "type": "rust", diff --git a/evm_rpc_types/src/lib.rs b/evm_rpc_types/src/lib.rs index 93324464..550451c3 100644 --- a/evm_rpc_types/src/lib.rs +++ b/evm_rpc_types/src/lib.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Debug, Display, Formatter}; use std::str::FromStr; -pub use lifecycle::{InstallArgs, LogFilter, RegexString}; +pub use lifecycle::{InstallArgs, LogFilter, OverrideProvider, RegexString, RegexSubstitution}; pub use request::{ AccessList, AccessListEntry, BlockTag, CallArgs, FeeHistoryArgs, GetLogsArgs, GetTransactionCountArgs, TransactionRequest, diff --git a/evm_rpc_types/src/lifecycle/mod.rs b/evm_rpc_types/src/lifecycle/mod.rs index 609c6776..a7c8b66d 100644 --- a/evm_rpc_types/src/lifecycle/mod.rs +++ b/evm_rpc_types/src/lifecycle/mod.rs @@ -8,6 +8,8 @@ pub struct InstallArgs { pub manage_api_keys: Option>, #[serde(rename = "logFilter")] pub log_filter: Option, + #[serde(rename = "overrideProvider")] + pub override_provider: Option, } #[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] @@ -18,5 +20,17 @@ pub enum LogFilter { HidePattern(RegexString), } +#[derive(Clone, Debug, Default, PartialEq, Eq, CandidType, Serialize, Deserialize)] +pub struct OverrideProvider { + #[serde(rename = "overrideUrl")] + pub override_url: Option, +} + #[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] pub struct RegexString(pub String); + +#[derive(Clone, Debug, PartialEq, Eq, CandidType, Serialize, Deserialize)] +pub struct RegexSubstitution { + pub pattern: RegexString, + pub replacement: String, +} diff --git a/scripts/e2e b/scripts/e2e index 7494c0da..34ccb8c7 100755 --- a/scripts/e2e +++ b/scripts/e2e @@ -4,6 +4,7 @@ dfx canister create --all && npm run generate && dfx deploy evm_rpc --mode reinstall -y && + dfx deploy evm_rpc_local --mode reinstall -y && dfx deploy evm_rpc_demo --mode reinstall -y && dfx deploy evm_rpc_staging --mode reinstall -y && dfx deploy e2e_rust && diff --git a/scripts/examples b/scripts/examples index ff5d1611..159874e9 100755 --- a/scripts/examples +++ b/scripts/examples @@ -1,16 +1,17 @@ #!/usr/bin/env bash # Run a variety of example RPC calls. +CANISTER_ID=${1:-evm_rpc} +# Use concrete block height to avoid flakiness on CI +BLOCK_HEIGHT=${2:-'Number = 20000000'} + NETWORK=local IDENTITY=default -CANISTER_ID=evm_rpc CYCLES=10000000000 WALLET=$(dfx identity get-wallet --network=$NETWORK --identity=$IDENTITY) RPC_SERVICE="EthMainnet=variant {PublicNode}" RPC_SERVICES=EthMainnet RPC_CONFIG="opt record {responseConsensus = opt variant {Threshold = record {total = opt (3 : nat8); min = 2 : nat8}}}" -# Use concrete block height to avoid flakiness on CI -BLOCK_HEIGHT="Number = 20000000" FLAGS="--network=$NETWORK --identity=$IDENTITY --with-cycles=$CYCLES --wallet=$WALLET" diff --git a/src/http.rs b/src/http.rs index c89eb7c0..8adb6501 100644 --- a/src/http.rs +++ b/src/http.rs @@ -2,7 +2,7 @@ use crate::{ accounting::{get_cost_with_collateral, get_http_request_cost}, add_metric_entry, constants::{CONTENT_TYPE_HEADER_LOWERCASE, CONTENT_TYPE_VALUE}, - memory::is_demo_active, + memory::{get_override_provider, is_demo_active}, types::{MetricRpcHost, MetricRpcMethod, ResolvedRpcService}, util::canonicalize_json, }; @@ -20,7 +20,7 @@ pub async fn json_rpc_request( max_response_bytes: u64, ) -> RpcResult { let cycles_cost = get_http_request_cost(json_rpc_payload.len() as u64, max_response_bytes); - let api = service.api(); + let api = service.api(&get_override_provider())?; let mut request_headers = api.headers.unwrap_or_default(); if !request_headers .iter() @@ -42,20 +42,19 @@ pub async fn json_rpc_request( vec![], )), }; - http_request(rpc_method, service, request, cycles_cost).await + http_request(rpc_method, request, cycles_cost).await } pub async fn http_request( rpc_method: MetricRpcMethod, - service: ResolvedRpcService, request: CanisterHttpRequestArgument, cycles_cost: u128, ) -> RpcResult { - let api = service.api(); - let parsed_url = match url::Url::parse(&api.url) { + let url = request.url.clone(); + let parsed_url = match url::Url::parse(&url) { Ok(url) => url, Err(_) => { - return Err(ValidationError::Custom(format!("Error parsing URL: {}", api.url)).into()) + return Err(ValidationError::Custom(format!("Error parsing URL: {}", url)).into()) } }; let host = match parsed_url.host_str() { @@ -63,7 +62,7 @@ pub async fn http_request( None => { return Err(ValidationError::Custom(format!( "Error parsing hostname from URL: {}", - api.url + url )) .into()) } diff --git a/src/main.rs b/src/main.rs index 0da4d43e..dae8d5ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,11 @@ use evm_rpc::http::get_http_response_body; use evm_rpc::logs::INFO; use evm_rpc::memory::{ insert_api_key, is_api_key_principal, is_demo_active, remove_api_key, set_api_key_principals, - set_demo_active, set_log_filter, + set_demo_active, set_log_filter, set_override_provider, }; use evm_rpc::metrics::encode_metrics; use evm_rpc::providers::{find_provider, resolve_rpc_service, PROVIDERS, SERVICE_PROVIDER_MAP}; -use evm_rpc::types::{LogFilter, Provider, ProviderId, RpcAccess, RpcAuth}; +use evm_rpc::types::{LogFilter, OverrideProvider, Provider, ProviderId, RpcAccess, RpcAuth}; use evm_rpc::{ http::{json_rpc_request, transform_http_request}, http_types, @@ -269,7 +269,13 @@ fn post_upgrade(args: evm_rpc_types::InstallArgs) { set_api_key_principals(principals); } if let Some(filter) = args.log_filter { - set_log_filter(LogFilter::from(filter)) + set_log_filter(LogFilter::try_from(filter).expect("ERROR: Invalid log filter")); + } + if let Some(override_provider) = args.override_provider { + set_override_provider( + OverrideProvider::try_from(override_provider) + .expect("ERROR: invalid override provider"), + ); } } diff --git a/src/memory.rs b/src/memory.rs index 03ede56c..9468f19f 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -7,12 +7,13 @@ use ic_stable_structures::{ use ic_stable_structures::{Cell, StableBTreeMap}; use std::cell::RefCell; -use crate::types::{ApiKey, LogFilter, Metrics, ProviderId}; +use crate::types::{ApiKey, LogFilter, Metrics, OverrideProvider, ProviderId}; const IS_DEMO_ACTIVE_MEMORY_ID: MemoryId = MemoryId::new(4); const API_KEY_MAP_MEMORY_ID: MemoryId = MemoryId::new(5); const MANAGE_API_KEYS_MEMORY_ID: MemoryId = MemoryId::new(6); const LOG_FILTER_MEMORY_ID: MemoryId = MemoryId::new(7); +const OVERRIDE_PROVIDER_MEMORY_ID: MemoryId = MemoryId::new(8); type StableMemory = VirtualMemory; @@ -31,7 +32,9 @@ thread_local! { static MANAGE_API_KEYS: RefCell> = RefCell::new(ic_stable_structures::Vec::init(MEMORY_MANAGER.with_borrow(|m| m.get(MANAGE_API_KEYS_MEMORY_ID))).expect("Unable to read API key principals from stable memory")); static LOG_FILTER: RefCell> = - RefCell::new(ic_stable_structures::Cell::init(MEMORY_MANAGER.with_borrow(|m| m.get(LOG_FILTER_MEMORY_ID)), LogFilter::default()).expect("Unable to read log message filter from stable memory")); + RefCell::new(Cell::init(MEMORY_MANAGER.with_borrow(|m| m.get(LOG_FILTER_MEMORY_ID)), LogFilter::default()).expect("Unable to read log message filter from stable memory")); + static OVERRIDE_PROVIDER: RefCell> = + RefCell::new(Cell::init(MEMORY_MANAGER.with_borrow(|m| m.get(OVERRIDE_PROVIDER_MEMORY_ID)), OverrideProvider::default()).expect("Unable to read provider override from stable memory")); } pub fn get_api_key(provider_id: ProviderId) -> Option { @@ -86,6 +89,18 @@ pub fn set_log_filter(filter: LogFilter) { }); } +pub fn get_override_provider() -> OverrideProvider { + OVERRIDE_PROVIDER.with_borrow(|provider| provider.get().clone()) +} + +pub fn set_override_provider(provider: OverrideProvider) { + OVERRIDE_PROVIDER.with_borrow_mut(|state| { + state + .set(provider) + .expect("Error while updating override provider") + }); +} + pub fn next_request_id() -> u64 { UNSTABLE_HTTP_REQUEST_COUNTER.with_borrow_mut(|counter| { let current_request_id = *counter; diff --git a/src/rpc_client/eth_rpc/mod.rs b/src/rpc_client/eth_rpc/mod.rs index 845b9a62..9841e1f0 100644 --- a/src/rpc_client/eth_rpc/mod.rs +++ b/src/rpc_client/eth_rpc/mod.rs @@ -3,7 +3,7 @@ use crate::accounting::get_http_request_cost; use crate::logs::{DEBUG, TRACE_HTTP}; -use crate::memory::next_request_id; +use crate::memory::{get_override_provider, next_request_id}; use crate::providers::resolve_rpc_service; use crate::rpc_client::eth_rpc_error::{sanitize_send_raw_transaction_result, Parser}; use crate::rpc_client::json::requests::JsonRpcRequest; @@ -11,9 +11,9 @@ use crate::rpc_client::json::responses::{ Block, FeeHistory, JsonRpcReply, JsonRpcResult, LogEntry, TransactionReceipt, }; use crate::rpc_client::numeric::{TransactionCount, Wei}; -use crate::types::MetricRpcMethod; +use crate::types::{MetricRpcMethod, OverrideProvider}; use candid::candid_method; -use evm_rpc_types::{HttpOutcallError, ProviderError, RpcApi, RpcError, RpcService}; +use evm_rpc_types::{HttpOutcallError, RpcApi, RpcError, RpcService}; use ic_canister_log::log; use ic_cdk::api::call::RejectionCode; use ic_cdk::api::management_canister::http_request::{ @@ -181,7 +181,7 @@ where method: eth_method.clone(), id: 1, }; - let api = resolve_api(provider)?; + let api = resolve_api(provider, &get_override_provider())?; let url = &api.url; let mut headers = vec![HttpHeader { name: "Content-Type".to_string(), @@ -221,9 +221,7 @@ where )), }; - let response = match http_request(provider, ð_method, request, effective_size_estimate) - .await - { + let response = match http_request(ð_method, request, effective_size_estimate).await { Err(RpcError::HttpOutcallError(HttpOutcallError::IcError { code, message })) if is_response_too_large(&code, &message) => { @@ -273,17 +271,18 @@ where } } -fn resolve_api(service: &RpcService) -> Result { - Ok(resolve_rpc_service(service.clone())?.api()) +fn resolve_api( + service: &RpcService, + override_provider: &OverrideProvider, +) -> Result { + resolve_rpc_service(service.clone())?.api(override_provider) } 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 @@ -293,7 +292,7 @@ async fn http_request( effective_response_size_estimate, ); let rpc_method = MetricRpcMethod(method.to_string()); - crate::http::http_request(rpc_method, service, request, cycles_cost).await + crate::http::http_request(rpc_method, request, cycles_cost).await } fn http_status_code(response: &HttpResponse) -> u16 { diff --git a/src/types.rs b/src/types.rs index 11833fe2..f71d1a3f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,8 +1,12 @@ +#[cfg(test)] +mod tests; + use crate::constants::{API_KEY_MAX_SIZE, API_KEY_REPLACE_STRING, MESSAGE_FILTER_MAX_SIZE}; use crate::memory::get_api_key; use crate::util::hostname_from_url; use crate::validate::validate_api_key; use candid::CandidType; +use evm_rpc_types::{RpcApi, RpcError, ValidationError}; use ic_cdk::api::call::RejectionCode; use ic_cdk::api::management_canister::http_request::HttpHeader; use ic_stable_structures::storable::Bound; @@ -15,16 +19,21 @@ use std::fmt; use zeroize::{Zeroize, ZeroizeOnDrop}; pub enum ResolvedRpcService { - Api(evm_rpc_types::RpcApi), + Api(RpcApi), Provider(Provider), } impl ResolvedRpcService { - pub fn api(&self) -> evm_rpc_types::RpcApi { - match self { + pub fn api(&self, override_provider: &OverrideProvider) -> Result { + let initial_api = match self { Self::Api(api) => api.clone(), Self::Provider(provider) => provider.api(), - } + }; + override_provider.apply(initial_api).map_err(|regex_error| { + RpcError::ValidationError(ValidationError::Custom(format!( + "BUG: regex should have been validated when initially set. Error: {regex_error}" + ))) + }) } } @@ -310,23 +319,32 @@ pub enum LogFilter { HidePattern(RegexString), } -impl From for LogFilter { - fn from(value: evm_rpc_types::LogFilter) -> Self { - match value { +impl TryFrom for LogFilter { + type Error = regex::Error; + + fn try_from(value: evm_rpc_types::LogFilter) -> Result { + Ok(match value { evm_rpc_types::LogFilter::ShowAll => LogFilter::ShowAll, evm_rpc_types::LogFilter::HideAll => LogFilter::HideAll, - evm_rpc_types::LogFilter::ShowPattern(regex) => LogFilter::ShowPattern(regex.into()), - evm_rpc_types::LogFilter::HidePattern(regex) => LogFilter::HidePattern(regex.into()), - } + evm_rpc_types::LogFilter::ShowPattern(regex) => { + LogFilter::ShowPattern(RegexString::try_from(regex)?) + } + evm_rpc_types::LogFilter::HidePattern(regex) => { + LogFilter::HidePattern(RegexString::try_from(regex)?) + } + }) } } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RegexString(String); -impl From for RegexString { - fn from(value: evm_rpc_types::RegexString) -> Self { - RegexString(value.0) +impl TryFrom for RegexString { + type Error = regex::Error; + + fn try_from(value: evm_rpc_types::RegexString) -> Result { + let ensure_regex_is_valid = Regex::new(&value.0)?; + Ok(Self(ensure_regex_is_valid.as_str().to_string())) } } @@ -337,9 +355,32 @@ impl From<&str> for RegexString { } impl RegexString { + /// Compile the string into a regular expression. + /// + /// This is a relatively expensive operation that's currently not cached. + pub fn compile(&self) -> Result { + Regex::new(&self.0) + } + pub fn try_is_valid(&self, value: &str) -> Result { - // Currently only used in the local replica. This can be optimized if eventually used in production. - Ok(Regex::new(&self.0)?.is_match(value)) + Ok(self.compile()?.is_match(value)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegexSubstitution { + pub pattern: RegexString, + pub replacement: String, +} + +impl TryFrom for RegexSubstitution { + type Error = regex::Error; + + fn try_from(value: evm_rpc_types::RegexSubstitution) -> Result { + Ok(Self { + pattern: RegexString::try_from(value.pattern)?, + replacement: value.replacement, + }) } } @@ -374,6 +415,68 @@ impl Storable for LogFilter { }; } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct OverrideProvider { + pub override_url: Option, +} + +impl OverrideProvider { + /// Override the resolved provider API (url and headers). + /// + /// # Limitations + /// + /// Currently, only the url can be replaced by regular expression. Headers will be reset. + /// + /// # Security considerations + /// + /// The resolved provider API may contain sensitive data (such as API keys) that may be extracted + /// by using the override mechanism. Since only the controller of the canister can set the override parameters, + /// upon canister initialization or upgrade, it's the controller's responsibility to ensure that this is not a problem + /// (e.g., if only used for local development). + pub fn apply(&self, api: RpcApi) -> Result { + match &self.override_url { + None => Ok(api), + Some(substitution) => { + let regex = substitution.pattern.compile()?; + let new_url = regex.replace_all(&api.url, &substitution.replacement); + Ok(RpcApi { + url: new_url.to_string(), + headers: None, + }) + } + } + } +} + +impl TryFrom for OverrideProvider { + type Error = regex::Error; + + fn try_from( + evm_rpc_types::OverrideProvider { override_url }: evm_rpc_types::OverrideProvider, + ) -> Result { + override_url + .map(RegexSubstitution::try_from) + .transpose() + .map(|substitution| Self { + override_url: substitution, + }) + } +} + +impl Storable for OverrideProvider { + fn to_bytes(&self) -> Cow<[u8]> { + serde_json::to_vec(self) + .expect("Error while serializing `OverrideProvider`") + .into() + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + serde_json::from_slice(&bytes).expect("Error while deserializing `Storable`") + } + + const BOUND: Bound = Bound::Unbounded; +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum RpcAuth { /// API key will be used in an Authorization header as Bearer token, e.g., @@ -385,45 +488,3 @@ pub enum RpcAuth { url_pattern: &'static str, }, } - -#[cfg(test)] -mod test { - use super::{LogFilter, RegexString}; - use ic_stable_structures::Storable; - - #[test] - fn test_message_filter_storable() { - let patterns: &[RegexString] = - &["[.]", "^DEBUG ", "(.*)?", "\\?"].map(|regex| regex.into()); - let cases = [ - vec![ - (LogFilter::ShowAll, r#""ShowAll""#.to_string()), - (LogFilter::HideAll, r#""HideAll""#.to_string()), - ], - patterns - .iter() - .map(|regex| { - ( - LogFilter::ShowPattern(regex.clone()), - format!(r#"{{"ShowPattern":{:?}}}"#, regex.0), - ) - }) - .collect(), - patterns - .iter() - .map(|regex| { - ( - LogFilter::HidePattern(regex.clone()), - format!(r#"{{"HidePattern":{:?}}}"#, regex.0), - ) - }) - .collect(), - ] - .concat(); - for (filter, expected_json) in cases { - let bytes = filter.to_bytes(); - assert_eq!(String::from_utf8(bytes.to_vec()).unwrap(), expected_json); - assert_eq!(filter, LogFilter::from_bytes(bytes)); - } - } -} diff --git a/src/types/tests.rs b/src/types/tests.rs new file mode 100644 index 00000000..26f74ffe --- /dev/null +++ b/src/types/tests.rs @@ -0,0 +1,130 @@ +use super::{LogFilter, OverrideProvider, RegexString, RegexSubstitution}; +use ic_stable_structures::Storable; +use proptest::prelude::{Just, Strategy}; +use proptest::{option, prop_oneof, proptest}; +use std::fmt::Debug; + +proptest! { + #[test] + fn should_encode_decode_log_filter(value in arb_log_filter()) { + test_encoding_decoding_roundtrip(&value); + } + + #[test] + fn should_encode_decode_override_provider(value in arb_override_provider()) { + test_encoding_decoding_roundtrip(&value); + } +} + +fn arb_regex() -> impl Strategy { + ".*".prop_map(|r| RegexString::from(r.as_str())) +} + +fn arb_regex_substitution() -> impl Strategy { + (arb_regex(), ".*").prop_map(|(pattern, replacement)| RegexSubstitution { + pattern, + replacement, + }) +} + +fn arb_log_filter() -> impl Strategy { + prop_oneof![ + Just(LogFilter::ShowAll), + Just(LogFilter::HideAll), + arb_regex().prop_map(LogFilter::ShowPattern), + arb_regex().prop_map(LogFilter::HidePattern), + ] +} + +fn arb_override_provider() -> impl Strategy { + option::of(arb_regex_substitution()).prop_map(|override_url| OverrideProvider { override_url }) +} + +fn test_encoding_decoding_roundtrip(value: &T) { + let bytes = value.to_bytes(); + let decoded_value = T::from_bytes(bytes); + assert_eq!(value, &decoded_value); +} + +mod override_provider { + use crate::providers::PROVIDERS; + use crate::types::{OverrideProvider, RegexSubstitution}; + use evm_rpc_types::RpcApi; + use ic_cdk::api::management_canister::http_request::HttpHeader; + + #[test] + fn should_override_provider_with_localhost() { + let override_provider = override_to_localhost(); + for provider in PROVIDERS { + let overriden_provider = override_provider.apply(provider.api()); + assert_eq!( + overriden_provider, + Ok(RpcApi { + url: "http://localhost:8545".to_string(), + headers: None + }) + ) + } + } + + #[test] + fn should_be_noop_when_empty() { + let no_override = OverrideProvider::default(); + for provider in PROVIDERS { + let initial_api = provider.api(); + let overriden_api = no_override.apply(initial_api.clone()); + assert_eq!(Ok(initial_api), overriden_api); + } + } + + #[test] + fn should_use_replacement_pattern() { + let identity_override = OverrideProvider { + override_url: Some(RegexSubstitution { + pattern: "(?.*)".into(), + replacement: "$url".to_string(), + }), + }; + for provider in PROVIDERS { + let initial_api = provider.api(); + let overriden_provider = identity_override.apply(initial_api.clone()); + assert_eq!(overriden_provider, Ok(initial_api)) + } + } + + #[test] + fn should_override_headers() { + let identity_override = OverrideProvider { + override_url: Some(RegexSubstitution { + pattern: "(.*)".into(), + replacement: "$1".to_string(), + }), + }; + for provider in PROVIDERS { + let provider_with_headers = RpcApi { + headers: Some(vec![HttpHeader { + name: "key".to_string(), + value: "123".to_string(), + }]), + ..provider.api() + }; + let overriden_provider = identity_override.apply(provider_with_headers.clone()); + assert_eq!( + overriden_provider, + Ok(RpcApi { + url: provider_with_headers.url, + headers: None + }) + ) + } + } + + fn override_to_localhost() -> OverrideProvider { + OverrideProvider { + override_url: Some(RegexSubstitution { + pattern: "^https://.*".into(), + replacement: "http://localhost:8545".to_string(), + }), + } + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 9d3fcff2..18f1bd27 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -91,9 +91,8 @@ impl Default for EvmRpcSetup { impl EvmRpcSetup { pub fn new() -> Self { Self::with_args(InstallArgs { - manage_api_keys: None, demo: Some(true), - log_filter: None, + ..Default::default() }) }