diff --git a/src/main.rs b/src/main.rs index 171ade96..c3b1c25b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -228,7 +228,7 @@ fn get_providers() -> Vec { } } }, - alias: provider.alias, + alias: provider.alias.map(evm_rpc_types::RpcService::from), } } PROVIDERS.iter().cloned().map(into_provider).collect() @@ -237,7 +237,11 @@ fn get_providers() -> Vec { #[query(name = "getServiceProviderMap")] #[candid_method(query, rename = "getServiceProviderMap")] fn get_service_provider_map() -> Vec<(evm_rpc_types::RpcService, ProviderId)> { - SERVICE_PROVIDER_MAP.with(|map| map.iter().map(|(k, v)| (k.clone(), *v)).collect()) + SERVICE_PROVIDER_MAP.with(|map| { + map.iter() + .map(|(k, v)| (evm_rpc_types::RpcService::from(*k), *v)) + .collect() + }) } #[query(name = "getNodesInSubnet")] diff --git a/src/providers.rs b/src/providers.rs index 39cf7b60..b127735e 100644 --- a/src/providers.rs +++ b/src/providers.rs @@ -1,7 +1,10 @@ +#[cfg(test)] +mod tests; + use evm_rpc_types::{ EthMainnetService, EthSepoliaService, L2MainnetService, ProviderError, RpcApi, RpcService, }; -use std::collections::HashMap; +use std::collections::BTreeMap; use crate::{ constants::{ @@ -21,7 +24,9 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://cloudflare-eth.com/v1/mainnet"), }, - alias: Some(RpcService::EthMainnet(EthMainnetService::Cloudflare)), + alias: Some(SupportedRpcService::EthMainnet( + EthMainnetService::Cloudflare, + )), }, Provider { provider_id: 1, @@ -32,7 +37,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://rpc.ankr.com/eth"), }, - alias: Some(RpcService::EthMainnet(EthMainnetService::Ankr)), + alias: Some(SupportedRpcService::EthMainnet(EthMainnetService::Ankr)), }, Provider { provider_id: 2, @@ -40,7 +45,9 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://ethereum-rpc.publicnode.com", }, - alias: Some(RpcService::EthMainnet(EthMainnetService::PublicNode)), + alias: Some(SupportedRpcService::EthMainnet( + EthMainnetService::PublicNode, + )), }, Provider { provider_id: 3, @@ -51,7 +58,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://ethereum.blockpi.network/v1/rpc/public"), }, - alias: Some(RpcService::EthMainnet(EthMainnetService::BlockPi)), + alias: Some(SupportedRpcService::EthMainnet(EthMainnetService::BlockPi)), }, Provider { provider_id: 4, @@ -59,7 +66,7 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://rpc.sepolia.org", }, - alias: Some(RpcService::EthSepolia(EthSepoliaService::Sepolia)), + alias: Some(SupportedRpcService::EthSepolia(EthSepoliaService::Sepolia)), }, Provider { provider_id: 5, @@ -70,7 +77,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://rpc.ankr.com/eth_sepolia"), }, - alias: Some(RpcService::EthSepolia(EthSepoliaService::Ankr)), + alias: Some(SupportedRpcService::EthSepolia(EthSepoliaService::Ankr)), }, Provider { provider_id: 6, @@ -81,7 +88,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://ethereum-sepolia.blockpi.network/v1/rpc/public"), }, - alias: Some(RpcService::EthSepolia(EthSepoliaService::BlockPi)), + alias: Some(SupportedRpcService::EthSepolia(EthSepoliaService::BlockPi)), }, Provider { provider_id: 7, @@ -89,7 +96,9 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://ethereum-sepolia-rpc.publicnode.com", }, - alias: Some(RpcService::EthSepolia(EthSepoliaService::PublicNode)), + alias: Some(SupportedRpcService::EthSepolia( + EthSepoliaService::PublicNode, + )), }, Provider { provider_id: 8, @@ -100,7 +109,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://eth-mainnet.g.alchemy.com/v2/demo"), }, - alias: Some(RpcService::EthMainnet(EthMainnetService::Alchemy)), + alias: Some(SupportedRpcService::EthMainnet(EthMainnetService::Alchemy)), }, Provider { provider_id: 9, @@ -111,7 +120,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://eth-sepolia.g.alchemy.com/v2/demo"), }, - alias: Some(RpcService::EthSepolia(EthSepoliaService::Alchemy)), + alias: Some(SupportedRpcService::EthSepolia(EthSepoliaService::Alchemy)), }, Provider { provider_id: 10, @@ -122,7 +131,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://rpc.ankr.com/arbitrum"), }, - alias: Some(RpcService::ArbitrumOne(L2MainnetService::Ankr)), + alias: Some(SupportedRpcService::ArbitrumOne(L2MainnetService::Ankr)), }, Provider { provider_id: 11, @@ -133,7 +142,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://arb-mainnet.g.alchemy.com/v2/demo"), }, - alias: Some(RpcService::ArbitrumOne(L2MainnetService::Alchemy)), + alias: Some(SupportedRpcService::ArbitrumOne(L2MainnetService::Alchemy)), }, Provider { provider_id: 12, @@ -144,7 +153,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://arbitrum.blockpi.network/v1/rpc/public"), }, - alias: Some(RpcService::ArbitrumOne(L2MainnetService::BlockPi)), + alias: Some(SupportedRpcService::ArbitrumOne(L2MainnetService::BlockPi)), }, Provider { provider_id: 13, @@ -152,7 +161,9 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://arbitrum-one-rpc.publicnode.com", }, - alias: Some(RpcService::ArbitrumOne(L2MainnetService::PublicNode)), + alias: Some(SupportedRpcService::ArbitrumOne( + L2MainnetService::PublicNode, + )), }, Provider { provider_id: 14, @@ -163,7 +174,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://rpc.ankr.com/base"), }, - alias: Some(RpcService::BaseMainnet(L2MainnetService::Ankr)), + alias: Some(SupportedRpcService::BaseMainnet(L2MainnetService::Ankr)), }, Provider { provider_id: 15, @@ -174,7 +185,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://base-mainnet.g.alchemy.com/v2/demo"), }, - alias: Some(RpcService::BaseMainnet(L2MainnetService::Alchemy)), + alias: Some(SupportedRpcService::BaseMainnet(L2MainnetService::Alchemy)), }, Provider { provider_id: 16, @@ -185,7 +196,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://base.blockpi.network/v1/rpc/public"), }, - alias: Some(RpcService::BaseMainnet(L2MainnetService::BlockPi)), + alias: Some(SupportedRpcService::BaseMainnet(L2MainnetService::BlockPi)), }, Provider { provider_id: 17, @@ -193,7 +204,9 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://base-rpc.publicnode.com", }, - alias: Some(RpcService::BaseMainnet(L2MainnetService::PublicNode)), + alias: Some(SupportedRpcService::BaseMainnet( + L2MainnetService::PublicNode, + )), }, Provider { provider_id: 18, @@ -204,7 +217,7 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://rpc.ankr.com/optimism"), }, - alias: Some(RpcService::OptimismMainnet(L2MainnetService::Ankr)), + alias: Some(SupportedRpcService::OptimismMainnet(L2MainnetService::Ankr)), }, Provider { provider_id: 19, @@ -215,7 +228,9 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://opt-mainnet.g.alchemy.com/v2/demo"), }, - alias: Some(RpcService::OptimismMainnet(L2MainnetService::Alchemy)), + alias: Some(SupportedRpcService::OptimismMainnet( + L2MainnetService::Alchemy, + )), }, Provider { provider_id: 20, @@ -226,7 +241,9 @@ pub const PROVIDERS: &[Provider] = &[ }, public_url: Some("https://optimism.blockpi.network/v1/rpc/public"), }, - alias: Some(RpcService::OptimismMainnet(L2MainnetService::BlockPi)), + alias: Some(SupportedRpcService::OptimismMainnet( + L2MainnetService::BlockPi, + )), }, Provider { provider_id: 21, @@ -234,7 +251,9 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://optimism-rpc.publicnode.com", }, - alias: Some(RpcService::OptimismMainnet(L2MainnetService::PublicNode)), + alias: Some(SupportedRpcService::OptimismMainnet( + L2MainnetService::PublicNode, + )), }, Provider { provider_id: 22, @@ -242,7 +261,7 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://eth.llamarpc.com", }, - alias: Some(RpcService::EthMainnet(EthMainnetService::Llama)), + alias: Some(SupportedRpcService::EthMainnet(EthMainnetService::Llama)), }, Provider { provider_id: 23, @@ -250,7 +269,7 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://arbitrum.llamarpc.com", }, - alias: Some(RpcService::ArbitrumOne(L2MainnetService::Llama)), + alias: Some(SupportedRpcService::ArbitrumOne(L2MainnetService::Llama)), }, Provider { provider_id: 24, @@ -258,7 +277,7 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://base.llamarpc.com", }, - alias: Some(RpcService::BaseMainnet(L2MainnetService::Llama)), + alias: Some(SupportedRpcService::BaseMainnet(L2MainnetService::Llama)), }, Provider { provider_id: 25, @@ -266,18 +285,20 @@ pub const PROVIDERS: &[Provider] = &[ access: RpcAccess::Unauthenticated { public_url: "https://optimism.llamarpc.com", }, - alias: Some(RpcService::OptimismMainnet(L2MainnetService::Llama)), + alias: Some(SupportedRpcService::OptimismMainnet( + L2MainnetService::Llama, + )), }, ]; thread_local! { - pub static PROVIDER_MAP: HashMap = + pub static PROVIDER_MAP: BTreeMap = PROVIDERS.iter() .map(|provider| (provider.provider_id, provider.clone())).collect(); - pub static SERVICE_PROVIDER_MAP: HashMap = + pub static SERVICE_PROVIDER_MAP: BTreeMap = PROVIDERS.iter() - .filter_map(|provider| Some((provider.alias.clone()?, provider.provider_id))) + .filter_map(|provider| Some((provider.alias?, provider.provider_id))) .collect(); } @@ -285,17 +306,6 @@ pub fn find_provider(f: impl Fn(&Provider) -> bool) -> Option<&'static Provider> PROVIDERS.iter().find(|&provider| f(provider)) } -fn lookup_provider_for_service(service: &RpcService) -> Result { - let provider_id = SERVICE_PROVIDER_MAP.with(|map| { - map.get(service) - .copied() - .ok_or(ProviderError::MissingRequiredProvider) - })?; - PROVIDER_MAP - .with(|map| map.get(&provider_id).cloned()) - .ok_or(ProviderError::ProviderNotFound) -} - pub fn get_known_chain_id(service: &RpcService) -> Option { match service { RpcService::Provider(_) => None, @@ -322,104 +332,108 @@ pub fn resolve_rpc_service(service: RpcService) -> Result ResolvedRpcService::Provider( - lookup_provider_for_service(&RpcService::EthMainnet(service))?, + lookup_provider_for_service(&SupportedRpcService::EthMainnet(service))?, ), RpcService::EthSepolia(service) => ResolvedRpcService::Provider( - lookup_provider_for_service(&RpcService::EthSepolia(service))?, + lookup_provider_for_service(&SupportedRpcService::EthSepolia(service))?, ), RpcService::ArbitrumOne(service) => ResolvedRpcService::Provider( - lookup_provider_for_service(&RpcService::ArbitrumOne(service))?, + lookup_provider_for_service(&SupportedRpcService::ArbitrumOne(service))?, ), RpcService::BaseMainnet(service) => ResolvedRpcService::Provider( - lookup_provider_for_service(&RpcService::BaseMainnet(service))?, + lookup_provider_for_service(&SupportedRpcService::BaseMainnet(service))?, ), RpcService::OptimismMainnet(service) => ResolvedRpcService::Provider( - lookup_provider_for_service(&RpcService::OptimismMainnet(service))?, + lookup_provider_for_service(&SupportedRpcService::OptimismMainnet(service))?, ), }) } -#[cfg(test)] -mod test { - use std::collections::{HashMap, HashSet}; +fn lookup_provider_for_service(service: &SupportedRpcService) -> Result { + let provider_id = SERVICE_PROVIDER_MAP.with(|map| { + map.get(service) + .copied() + .ok_or(ProviderError::MissingRequiredProvider) + })?; + PROVIDER_MAP + .with(|map| map.get(&provider_id).cloned()) + .ok_or(ProviderError::ProviderNotFound) +} - use crate::{ - constants::API_KEY_REPLACE_STRING, - types::{Provider, RpcAccess, RpcAuth}, - }; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] +pub enum SupportedRpcService { + EthMainnet(EthMainnetService), + EthSepolia(EthSepoliaService), + ArbitrumOne(L2MainnetService), + BaseMainnet(L2MainnetService), + OptimismMainnet(L2MainnetService), +} - use super::{PROVIDERS, SERVICE_PROVIDER_MAP}; +impl SupportedRpcService { + // Order of providers matters! + // The threshold consensus strategy will consider the first `total` providers in the order + // they are specified (taking the default ones first, followed by the non default ones if necessary) + // if the providers are not explicitly specified by the caller. + pub const fn eth_mainnet() -> &'static [SupportedRpcService] { + &[ + SupportedRpcService::EthMainnet(EthMainnetService::BlockPi), + SupportedRpcService::EthMainnet(EthMainnetService::Ankr), + SupportedRpcService::EthMainnet(EthMainnetService::PublicNode), + SupportedRpcService::EthMainnet(EthMainnetService::Llama), + SupportedRpcService::EthMainnet(EthMainnetService::Alchemy), + SupportedRpcService::EthMainnet(EthMainnetService::Cloudflare), + ] + } - #[test] - fn test_provider_id_sequence() { - for (i, provider) in PROVIDERS.iter().enumerate() { - assert_eq!(provider.provider_id, i as u64); - } + pub const fn eth_sepolia() -> &'static [SupportedRpcService] { + &[ + SupportedRpcService::EthSepolia(EthSepoliaService::PublicNode), + SupportedRpcService::EthSepolia(EthSepoliaService::Ankr), + SupportedRpcService::EthSepolia(EthSepoliaService::BlockPi), + SupportedRpcService::EthSepolia(EthSepoliaService::Alchemy), + SupportedRpcService::EthSepolia(EthSepoliaService::Sepolia), + ] } - #[test] - fn test_rpc_provider_url_patterns() { - for provider in PROVIDERS { - fn assert_not_url_pattern(url: &str, provider: &Provider) { - assert!( - !url.contains(API_KEY_REPLACE_STRING), - "Unexpected API key in URL for provider: {}", - provider.provider_id - ) - } - fn assert_url_pattern(url: &str, provider: &Provider) { - assert!( - url.contains(API_KEY_REPLACE_STRING), - "Missing API key in URL pattern for provider: {}", - provider.provider_id - ) - } - match &provider.access { - RpcAccess::Authenticated { auth, public_url } => { - match auth { - RpcAuth::BearerToken { url } => assert_not_url_pattern(url, provider), - RpcAuth::UrlParameter { url_pattern } => { - assert_url_pattern(url_pattern, provider) - } - } - if let Some(public_url) = public_url { - assert_not_url_pattern(public_url, provider); - } - } - RpcAccess::Unauthenticated { public_url } => { - assert_not_url_pattern(public_url, provider); - } - } - } + pub const fn arbitrum_one() -> &'static [SupportedRpcService] { + &[ + SupportedRpcService::ArbitrumOne(L2MainnetService::Llama), + SupportedRpcService::ArbitrumOne(L2MainnetService::BlockPi), + SupportedRpcService::ArbitrumOne(L2MainnetService::PublicNode), + SupportedRpcService::ArbitrumOne(L2MainnetService::Alchemy), + SupportedRpcService::ArbitrumOne(L2MainnetService::Ankr), + ] } - #[test] - fn test_no_duplicate_service_providers() { - SERVICE_PROVIDER_MAP.with(|map| { - assert_eq!( - map.len(), - map.keys().collect::>().len(), - "Duplicate service in mapping" - ); - assert_eq!( - map.len(), - map.values().collect::>().len(), - "Duplicate provider in mapping" - ); - }) + pub const fn base_mainnet() -> &'static [SupportedRpcService] { + &[ + SupportedRpcService::BaseMainnet(L2MainnetService::Llama), + SupportedRpcService::BaseMainnet(L2MainnetService::BlockPi), + SupportedRpcService::BaseMainnet(L2MainnetService::PublicNode), + SupportedRpcService::BaseMainnet(L2MainnetService::Alchemy), + SupportedRpcService::BaseMainnet(L2MainnetService::Ankr), + ] } - #[test] - fn test_service_provider_coverage() { - SERVICE_PROVIDER_MAP.with(|map| { - let inverse_map: HashMap<_, _> = map.iter().map(|(k, v)| (v, k)).collect(); - for provider in PROVIDERS { - assert!( - inverse_map.contains_key(&provider.provider_id), - "Missing service mapping for provider with ID: {}", - provider.provider_id - ); - } - }) + pub const fn optimism_mainnet() -> &'static [SupportedRpcService] { + &[ + SupportedRpcService::OptimismMainnet(L2MainnetService::Llama), + SupportedRpcService::OptimismMainnet(L2MainnetService::BlockPi), + SupportedRpcService::OptimismMainnet(L2MainnetService::PublicNode), + SupportedRpcService::OptimismMainnet(L2MainnetService::Alchemy), + SupportedRpcService::OptimismMainnet(L2MainnetService::Ankr), + ] + } +} + +impl From for RpcService { + fn from(value: SupportedRpcService) -> Self { + match value { + SupportedRpcService::EthMainnet(service) => RpcService::EthMainnet(service), + SupportedRpcService::EthSepolia(service) => RpcService::EthSepolia(service), + SupportedRpcService::ArbitrumOne(service) => RpcService::ArbitrumOne(service), + SupportedRpcService::BaseMainnet(service) => RpcService::BaseMainnet(service), + SupportedRpcService::OptimismMainnet(service) => RpcService::OptimismMainnet(service), + } } } diff --git a/src/providers/tests.rs b/src/providers/tests.rs new file mode 100644 index 00000000..e3b54b82 --- /dev/null +++ b/src/providers/tests.rs @@ -0,0 +1,140 @@ +mod static_map { + use crate::providers::{PROVIDERS, SERVICE_PROVIDER_MAP}; + use std::collections::{BTreeSet, HashMap}; + + use crate::{ + constants::API_KEY_REPLACE_STRING, + types::{Provider, RpcAccess, RpcAuth}, + }; + + #[test] + fn test_provider_id_sequence() { + for (i, provider) in PROVIDERS.iter().enumerate() { + assert_eq!(provider.provider_id, i as u64); + } + } + + #[test] + fn test_rpc_provider_url_patterns() { + for provider in PROVIDERS { + fn assert_not_url_pattern(url: &str, provider: &Provider) { + assert!( + !url.contains(API_KEY_REPLACE_STRING), + "Unexpected API key in URL for provider: {}", + provider.provider_id + ) + } + fn assert_url_pattern(url: &str, provider: &Provider) { + assert!( + url.contains(API_KEY_REPLACE_STRING), + "Missing API key in URL pattern for provider: {}", + provider.provider_id + ) + } + match &provider.access { + RpcAccess::Authenticated { auth, public_url } => { + match auth { + RpcAuth::BearerToken { url } => assert_not_url_pattern(url, provider), + RpcAuth::UrlParameter { url_pattern } => { + assert_url_pattern(url_pattern, provider) + } + } + if let Some(public_url) = public_url { + assert_not_url_pattern(public_url, provider); + } + } + RpcAccess::Unauthenticated { public_url } => { + assert_not_url_pattern(public_url, provider); + } + } + } + } + + #[test] + fn test_no_duplicate_service_providers() { + SERVICE_PROVIDER_MAP.with(|map| { + assert_eq!( + map.len(), + map.keys().collect::>().len(), + "Duplicate service in mapping" + ); + assert_eq!( + map.len(), + map.values().collect::>().len(), + "Duplicate provider in mapping" + ); + }) + } + + #[test] + fn test_service_provider_coverage() { + SERVICE_PROVIDER_MAP.with(|map| { + let inverse_map: HashMap<_, _> = map.iter().map(|(k, v)| (v, k)).collect(); + for provider in PROVIDERS { + assert!( + inverse_map.contains_key(&provider.provider_id), + "Missing service mapping for provider with ID: {}", + provider.provider_id + ); + } + }) + } +} + +mod supported_rpc_service { + use crate::providers::SupportedRpcService; + use evm_rpc_types::{EthMainnetService, EthSepoliaService, L2MainnetService}; + use std::collections::BTreeSet; + + #[test] + fn should_have_all_supported_providers() { + fn assert_same_set( + left: impl Iterator, + right: &[SupportedRpcService], + ) { + let left: BTreeSet<_> = left.collect(); + let right: BTreeSet<_> = right.iter().copied().collect(); + assert_eq!(left, right); + } + + assert_same_set( + EthMainnetService::all() + .iter() + .copied() + .map(SupportedRpcService::EthMainnet), + SupportedRpcService::eth_mainnet(), + ); + + assert_same_set( + EthSepoliaService::all() + .iter() + .copied() + .map(SupportedRpcService::EthSepolia), + SupportedRpcService::eth_sepolia(), + ); + + assert_same_set( + L2MainnetService::all() + .iter() + .copied() + .map(SupportedRpcService::ArbitrumOne), + SupportedRpcService::arbitrum_one(), + ); + + assert_same_set( + L2MainnetService::all() + .iter() + .copied() + .map(SupportedRpcService::BaseMainnet), + SupportedRpcService::base_mainnet(), + ); + + assert_same_set( + L2MainnetService::all() + .iter() + .copied() + .map(SupportedRpcService::OptimismMainnet), + SupportedRpcService::optimism_mainnet(), + ); + } +} diff --git a/src/rpc_client/mod.rs b/src/rpc_client/mod.rs index a4be0523..953488e5 100644 --- a/src/rpc_client/mod.rs +++ b/src/rpc_client/mod.rs @@ -1,6 +1,6 @@ use crate::http::http_client; use crate::memory::get_override_provider; -use crate::providers::resolve_rpc_service; +use crate::providers::{resolve_rpc_service, SupportedRpcService}; use crate::rpc_client::eth_rpc::{HttpResponsePayload, ResponseSizeEstimate, HEADER_SIZE_LIMIT}; use crate::rpc_client::numeric::TransactionCount; use crate::types::MetricRpcMethod; @@ -10,8 +10,7 @@ use canhttp::{ MaxResponseBytesRequestExtension, TransformContextRequestExtension, }; use evm_rpc_types::{ - ConsensusStrategy, EthMainnetService, EthSepoliaService, JsonRpcError, L2MainnetService, - ProviderError, RpcConfig, RpcError, RpcService, RpcServices, + ConsensusStrategy, JsonRpcError, ProviderError, RpcConfig, RpcError, RpcService, RpcServices, }; use ic_cdk::api::management_canister::http_request::TransformContext; use json::requests::{ @@ -65,107 +64,63 @@ pub struct Providers { } impl Providers { - // Order of providers matters! - // The threshold consensus strategy will consider the first `total` providers in the order - // they are specified (taking the default ones first, followed by the non default ones if necessary) - // if the providers are not explicitly specified by the caller. - const DEFAULT_ETH_MAINNET_SERVICES: &'static [EthMainnetService] = &[ - EthMainnetService::BlockPi, - EthMainnetService::Ankr, - EthMainnetService::PublicNode, - ]; - const NON_DEFAULT_ETH_MAINNET_SERVICES: &'static [EthMainnetService] = &[ - EthMainnetService::Llama, - EthMainnetService::Alchemy, - EthMainnetService::Cloudflare, - ]; - - const DEFAULT_ETH_SEPOLIA_SERVICES: &'static [EthSepoliaService] = &[ - EthSepoliaService::PublicNode, - EthSepoliaService::Ankr, - EthSepoliaService::BlockPi, - ]; - const NON_DEFAULT_ETH_SEPOLIA_SERVICES: &'static [EthSepoliaService] = - &[EthSepoliaService::Alchemy, EthSepoliaService::Sepolia]; - - const DEFAULT_L2_MAINNET_SERVICES: &'static [L2MainnetService] = &[ - L2MainnetService::Llama, - L2MainnetService::BlockPi, - L2MainnetService::PublicNode, - ]; - const NON_DEFAULT_L2_MAINNET_SERVICES: &'static [L2MainnetService] = - &[L2MainnetService::Alchemy, L2MainnetService::Ankr]; + const DEFAULT_NUM_PROVIDERS_FOR_EQUALITY: usize = 3; pub fn new(source: RpcServices, strategy: ConsensusStrategy) -> Result { - let (chain, providers): (_, BTreeSet<_>) = match source { - RpcServices::Custom { chain_id, services } => ( - EthereumNetwork::from(chain_id), - choose_providers(Some(services), &[], &[], strategy)? - .into_iter() - .map(RpcService::Custom) - .collect(), - ), - RpcServices::EthMainnet(services) => ( - EthereumNetwork::MAINNET, - choose_providers( - services, - Self::DEFAULT_ETH_MAINNET_SERVICES, - Self::NON_DEFAULT_ETH_MAINNET_SERVICES, - strategy, - )? - .into_iter() - .map(RpcService::EthMainnet) - .collect(), - ), - RpcServices::EthSepolia(services) => ( - EthereumNetwork::SEPOLIA, - choose_providers( - services, - Self::DEFAULT_ETH_SEPOLIA_SERVICES, - Self::NON_DEFAULT_ETH_SEPOLIA_SERVICES, - strategy, - )? - .into_iter() - .map(RpcService::EthSepolia) - .collect(), - ), - RpcServices::ArbitrumOne(services) => ( - EthereumNetwork::ARBITRUM, - choose_providers( - services, - Self::DEFAULT_L2_MAINNET_SERVICES, - Self::NON_DEFAULT_L2_MAINNET_SERVICES, - strategy, - )? - .into_iter() - .map(RpcService::ArbitrumOne) - .collect(), - ), - RpcServices::BaseMainnet(services) => ( - EthereumNetwork::BASE, - choose_providers( - services, - Self::DEFAULT_L2_MAINNET_SERVICES, - Self::NON_DEFAULT_L2_MAINNET_SERVICES, - strategy, - )? - .into_iter() - .map(RpcService::BaseMainnet) - .collect(), - ), - RpcServices::OptimismMainnet(services) => ( - EthereumNetwork::OPTIMISM, - choose_providers( - services, - Self::DEFAULT_L2_MAINNET_SERVICES, - Self::NON_DEFAULT_L2_MAINNET_SERVICES, - strategy, - )? - .into_iter() - .map(RpcService::OptimismMainnet) - .collect(), - ), - }; + fn user_defined_providers(source: RpcServices) -> Option> { + fn map_services( + services: impl Into>>, + f: F, + ) -> Option> + where + F: Fn(T) -> RpcService, + { + services.into().map(|s| s.into_iter().map(f).collect()) + } + match source { + RpcServices::Custom { services, .. } => map_services(services, RpcService::Custom), + RpcServices::EthMainnet(services) => map_services(services, RpcService::EthMainnet), + RpcServices::EthSepolia(services) => map_services(services, RpcService::EthSepolia), + RpcServices::ArbitrumOne(services) => { + map_services(services, RpcService::ArbitrumOne) + } + RpcServices::BaseMainnet(services) => { + map_services(services, RpcService::BaseMainnet) + } + RpcServices::OptimismMainnet(services) => { + map_services(services, RpcService::OptimismMainnet) + } + } + } + + fn supported_providers( + source: &RpcServices, + ) -> (EthereumNetwork, &'static [SupportedRpcService]) { + match source { + RpcServices::Custom { chain_id, .. } => (EthereumNetwork::from(*chain_id), &[]), + RpcServices::EthMainnet(_) => { + (EthereumNetwork::MAINNET, SupportedRpcService::eth_mainnet()) + } + RpcServices::EthSepolia(_) => { + (EthereumNetwork::SEPOLIA, SupportedRpcService::eth_sepolia()) + } + RpcServices::ArbitrumOne(_) => ( + EthereumNetwork::ARBITRUM, + SupportedRpcService::arbitrum_one(), + ), + RpcServices::BaseMainnet(_) => { + (EthereumNetwork::BASE, SupportedRpcService::base_mainnet()) + } + RpcServices::OptimismMainnet(_) => ( + EthereumNetwork::OPTIMISM, + SupportedRpcService::optimism_mainnet(), + ), + } + } + + let (chain, supported_providers) = supported_providers(&source); + let user_input = user_defined_providers(source); + let providers = choose_providers(user_input, supported_providers, strategy)?; if providers.is_empty() { return Err(ProviderError::ProviderNotFound); @@ -178,18 +133,21 @@ impl Providers { } } -fn choose_providers( - user_input: Option>, - default_providers: &[T], - non_default_providers: &[T], +fn choose_providers( + user_input: Option>, + supported_providers: &[SupportedRpcService], strategy: ConsensusStrategy, -) -> Result, ProviderError> -where - T: Clone + Ord, -{ +) -> Result, ProviderError> { match strategy { ConsensusStrategy::Equality => Ok(user_input - .unwrap_or_else(|| default_providers.to_vec()) + .unwrap_or_else(|| { + supported_providers + .iter() + .take(Providers::DEFAULT_NUM_PROVIDERS_FOR_EQUALITY) + .copied() + .map(RpcService::from) + .collect() + }) .into_iter() .collect()), ConsensusStrategy::Threshold { total, min } => { @@ -202,7 +160,6 @@ where } match user_input { None => { - let all_providers_len = default_providers.len() + non_default_providers.len(); let total = total.ok_or_else(|| { ProviderError::InvalidRpcConfig( "total must be specified when using default providers".to_string(), @@ -216,17 +173,18 @@ where ))); } + let all_providers_len = supported_providers.len(); if total > all_providers_len as u8 { return Err(ProviderError::InvalidRpcConfig(format!( "total {} is greater than the number of all supported providers {}", total, all_providers_len ))); } - let providers: BTreeSet<_> = default_providers + let providers: BTreeSet<_> = supported_providers .iter() - .chain(non_default_providers.iter()) .take(total as usize) - .cloned() + .copied() + .map(RpcService::from) .collect(); assert_eq!(providers.len(), total as usize, "BUG: duplicate providers"); Ok(providers) diff --git a/src/rpc_client/tests.rs b/src/rpc_client/tests.rs index fc9a91a3..95b01406 100644 --- a/src/rpc_client/tests.rs +++ b/src/rpc_client/tests.rs @@ -184,48 +184,10 @@ mod providers { use assert_matches::assert_matches; use evm_rpc_types::{ ConsensusStrategy, EthMainnetService, EthSepoliaService, L2MainnetService, ProviderError, - RpcService, RpcServices, + RpcServices, }; - use maplit::btreeset; use proptest::arbitrary::any; use proptest::proptest; - use std::collections::BTreeSet; - use std::fmt::Debug; - - #[test] - fn should_partition_providers_between_default_and_non_default() { - fn assert_is_partition(left: &[T], right: &[T], all: &[T]) { - let left_set = left.iter().collect::>(); - let right_set = right.iter().collect::>(); - let all_set = all.iter().collect::>(); - - assert!( - left_set.is_disjoint(&right_set), - "Non-empty intersection {:?}", - left_set.intersection(&right_set).collect::>() - ); - assert_eq!( - left_set.union(&right_set).copied().collect::>(), - all_set - ); - } - - assert_is_partition( - Providers::DEFAULT_ETH_MAINNET_SERVICES, - Providers::NON_DEFAULT_ETH_MAINNET_SERVICES, - EthMainnetService::all(), - ); - assert_is_partition( - Providers::DEFAULT_ETH_SEPOLIA_SERVICES, - Providers::NON_DEFAULT_ETH_SEPOLIA_SERVICES, - EthSepoliaService::all(), - ); - assert_is_partition( - Providers::DEFAULT_L2_MAINNET_SERVICES, - Providers::NON_DEFAULT_L2_MAINNET_SERVICES, - L2MainnetService::all(), - ) - } // Note that changing the number of providers is a non-trivial operation // that has consequences for all users of the EVM RPC canister: @@ -272,56 +234,6 @@ mod providers { } } - #[test] - fn should_choose_default_providers_first() { - let strategy = ConsensusStrategy::Threshold { - total: Some(4), - min: 3, - }; - - let providers = Providers::new(RpcServices::EthMainnet(None), strategy.clone()).unwrap(); - assert_eq!( - providers.services, - btreeset! { - Providers::DEFAULT_ETH_MAINNET_SERVICES[0], - Providers::DEFAULT_ETH_MAINNET_SERVICES[1], - Providers::DEFAULT_ETH_MAINNET_SERVICES[2], - EthMainnetService::Llama, - } - .into_iter() - .map(RpcService::EthMainnet) - .collect() - ); - - let providers = Providers::new(RpcServices::EthSepolia(None), strategy.clone()).unwrap(); - assert_eq!( - providers.services, - btreeset! { - Providers::DEFAULT_ETH_SEPOLIA_SERVICES[0], - Providers::DEFAULT_ETH_SEPOLIA_SERVICES[1], - Providers::DEFAULT_ETH_SEPOLIA_SERVICES[2], - EthSepoliaService::Alchemy, - } - .into_iter() - .map(RpcService::EthSepolia) - .collect() - ); - - let providers = Providers::new(RpcServices::ArbitrumOne(None), strategy.clone()).unwrap(); - assert_eq!( - providers.services, - btreeset! { - Providers::DEFAULT_L2_MAINNET_SERVICES[0], - Providers::DEFAULT_L2_MAINNET_SERVICES[1], - Providers::DEFAULT_L2_MAINNET_SERVICES[2], - L2MainnetService::Alchemy, - } - .into_iter() - .map(RpcService::ArbitrumOne) - .collect() - ); - } - #[test] fn should_fail_when_threshold_unspecified_with_default_providers() { let strategy = ConsensusStrategy::Threshold { diff --git a/src/types.rs b/src/types.rs index 3e66eff9..9787dc14 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,6 +3,7 @@ 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::providers::SupportedRpcService; use crate::util::hostname_from_url; use crate::validate::validate_api_key; use candid::CandidType; @@ -250,7 +251,7 @@ pub struct Provider { pub provider_id: ProviderId, pub chain_id: u64, pub access: RpcAccess, - pub alias: Option, + pub alias: Option, } impl Provider {