use std::str::FromStr;
use reqwest::Url;
use thiserror::Error;
use crate::rpc::config::HttpRpcSettings;
const CHAIN_ETHEREUM: u64 = 1;
const CHAIN_OPTIMISM: u64 = 10;
const CHAIN_BNB_SMART_CHAIN: u64 = 56;
const CHAIN_POLYGON: u64 = 137;
const CHAIN_ARBITRUM: u64 = 42161;
const CHAIN_BASE: u64 = 8453;
const CHAIN_ROBINHOOD: u64 = 4663;
const INFURA_CHAINS: &[(u64, &str)] = &[
(CHAIN_ETHEREUM, "mainnet"),
(CHAIN_ARBITRUM, "arbitrum-mainnet"),
(CHAIN_BASE, "base-mainnet"),
(CHAIN_OPTIMISM, "optimism-mainnet"),
(CHAIN_POLYGON, "polygon-mainnet"),
(CHAIN_BNB_SMART_CHAIN, "bsc-mainnet"),
];
const ANKR_CHAINS: &[(u64, &str)] = &[
(CHAIN_ETHEREUM, "eth"),
(CHAIN_ARBITRUM, "arbitrum"),
(CHAIN_BASE, "base"),
(CHAIN_OPTIMISM, "optimism"),
(CHAIN_POLYGON, "polygon"),
(CHAIN_BNB_SMART_CHAIN, "bsc"),
];
const ALCHEMY_CHAINS: &[(u64, &str)] = &[
(CHAIN_ETHEREUM, "eth-mainnet"),
(CHAIN_ARBITRUM, "arb-mainnet"),
(CHAIN_BASE, "base-mainnet"),
(CHAIN_OPTIMISM, "opt-mainnet"),
(CHAIN_POLYGON, "polygon-mainnet"),
(CHAIN_BNB_SMART_CHAIN, "bnb-mainnet"),
(CHAIN_ROBINHOOD, "robinhood-mainnet"),
];
const DRPC_CHAINS: &[(u64, &str)] = &[
(CHAIN_ETHEREUM, "ethereum"),
(CHAIN_ARBITRUM, "arbitrum"),
(CHAIN_BASE, "base"),
(CHAIN_OPTIMISM, "optimism"),
(CHAIN_POLYGON, "polygon"),
(CHAIN_BNB_SMART_CHAIN, "bsc"),
];
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ProviderUrlError {
#[error("unsupported chain ID {chain_id} for provider {provider}")]
UnsupportedChain {
provider: &'static str,
chain_id: u64,
},
#[error("provider {provider} requires an API key")]
ApiKeyRequired { provider: &'static str },
#[error("provider {provider} requires a chain-keyed endpoint set")]
EndpointSetRequired { provider: &'static str },
#[error("no endpoint configured for provider {provider} and chain ID {chain_id}")]
EndpointNotConfigured {
provider: &'static str,
chain_id: u64,
},
#[error("invalid endpoint for provider {provider} and chain ID {chain_id}: {reason}")]
InvalidEndpoint {
provider: &'static str,
chain_id: u64,
reason: String,
},
}
#[derive(Debug, Clone, Copy)]
pub enum ProviderCredentials<'a> {
ApiKey(&'a str),
Endpoints(&'a [(u64, String)]),
}
impl<'a> From<&'a str> for ProviderCredentials<'a> {
fn from(api_key: &'a str) -> Self {
Self::ApiKey(api_key)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provider {
Infura,
Ankr,
Alchemy,
QuickNode,
Drpc,
}
impl FromStr for Provider {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lower = s.to_ascii_lowercase();
if lower.contains("infura.io") {
Ok(Self::Infura)
} else if lower.contains("ankr.com") {
Ok(Self::Ankr)
} else if lower.contains("alchemy.com") {
Ok(Self::Alchemy)
} else if lower.contains("quiknode.pro") || lower.contains("quicknode") {
Ok(Self::QuickNode)
} else if lower.contains("drpc.org") || lower.contains("drpc.live") {
Ok(Self::Drpc)
} else {
Err("unknown provider")
}
}
}
impl Provider {
pub(crate) fn name(&self) -> &'static str {
match self {
Provider::Infura => "infura",
Provider::Ankr => "ankr",
Provider::Alchemy => "alchemy",
Provider::QuickNode => "quicknode",
Provider::Drpc => "drpc",
}
}
pub fn rpc_settings(&self, url: impl AsRef<str>) -> HttpRpcSettings {
let url: Url = url.as_ref().parse().expect("Invalid URL");
match self {
Provider::Ankr => HttpRpcSettings {
url,
max_concurrency: 16,
max_batch_size: 5000,
max_logs_per_request: 60_000,
init_batch_size: 500,
},
Provider::Infura => HttpRpcSettings {
url,
max_concurrency: 8,
max_batch_size: 1000,
max_logs_per_request: 10_000,
init_batch_size: 500,
},
Provider::Alchemy => HttpRpcSettings {
url,
max_concurrency: 8,
max_batch_size: 1000,
max_logs_per_request: 10_000,
init_batch_size: 500,
},
Provider::QuickNode => HttpRpcSettings {
url,
max_concurrency: 8,
max_batch_size: 1000,
max_logs_per_request: 10_000,
init_batch_size: 500,
},
Provider::Drpc => HttpRpcSettings {
url,
max_concurrency: 32,
max_batch_size: 1000,
max_logs_per_request: 10_000,
init_batch_size: 500,
},
}
}
pub fn http_url<'a>(
&self,
credentials: impl Into<ProviderCredentials<'a>>,
chain_id: u64,
) -> Result<Url, ProviderUrlError> {
self.build_url(credentials.into(), chain_id, false)
}
pub fn ws_url<'a>(
&self,
credentials: impl Into<ProviderCredentials<'a>>,
chain_id: u64,
) -> Result<Url, ProviderUrlError> {
self.build_url(credentials.into(), chain_id, true)
}
fn build_url(
&self,
credentials: ProviderCredentials<'_>,
chain_id: u64,
websocket: bool,
) -> Result<Url, ProviderUrlError> {
let unsupported_chain_error = || ProviderUrlError::UnsupportedChain {
provider: self.name(),
chain_id,
};
let api_key = match credentials {
ProviderCredentials::ApiKey(api_key) => api_key,
ProviderCredentials::Endpoints(_) if *self != Provider::QuickNode => {
return Err(ProviderUrlError::ApiKeyRequired {
provider: self.name(),
});
}
ProviderCredentials::Endpoints(endpoints) => {
return self.quicknode_url(endpoints, chain_id, websocket);
}
};
match self {
Provider::Infura => {
let chain =
chain_name(chain_id, INFURA_CHAINS).ok_or_else(unsupported_chain_error)?;
let scheme = if websocket { "wss" } else { "https" };
let path = if websocket { "ws/v3" } else { "v3" };
Ok(format!("{scheme}://{chain}.infura.io/{path}/{api_key}")
.parse()
.unwrap())
}
Provider::Ankr => {
let chain =
chain_name(chain_id, ANKR_CHAINS).ok_or_else(unsupported_chain_error)?;
let scheme = if websocket { "wss" } else { "https" };
let path_infix = if websocket { "ws/" } else { "" };
Ok(
format!("{scheme}://rpc.ankr.com/{chain}/{path_infix}{api_key}")
.parse()
.unwrap(),
)
}
Provider::Alchemy => {
let chain =
chain_name(chain_id, ALCHEMY_CHAINS).ok_or_else(unsupported_chain_error)?;
let scheme = if websocket { "wss" } else { "https" };
Ok(format!("{scheme}://{chain}.g.alchemy.com/v2/{api_key}")
.parse()
.unwrap())
}
Provider::QuickNode => Err(ProviderUrlError::EndpointSetRequired {
provider: self.name(),
}),
Provider::Drpc => {
let chain =
chain_name(chain_id, DRPC_CHAINS).ok_or_else(unsupported_chain_error)?;
let scheme = if websocket { "wss" } else { "https" };
Ok(format!("{scheme}://lb.drpc.live/{chain}/{api_key}")
.parse()
.unwrap())
}
}
}
fn quicknode_url(
&self,
endpoints: &[(u64, String)],
chain_id: u64,
websocket: bool,
) -> Result<Url, ProviderUrlError> {
let endpoint = endpoints
.iter()
.find(|(id, _)| *id == chain_id)
.map(|(_, endpoint)| endpoint)
.ok_or(ProviderUrlError::EndpointNotConfigured {
provider: self.name(),
chain_id,
})?;
let mut url: Url =
endpoint
.parse::<Url>()
.map_err(|error| ProviderUrlError::InvalidEndpoint {
provider: self.name(),
chain_id,
reason: error.to_string(),
})?;
let scheme = if websocket { "wss" } else { "https" };
url.set_scheme(scheme)
.map_err(|_| ProviderUrlError::InvalidEndpoint {
provider: self.name(),
chain_id,
reason: format!("endpoint cannot be converted to {scheme}"),
})?;
Ok(url)
}
pub fn detect(url: &str) -> Result<Self, &'static str> {
url.parse()
}
}
fn chain_name(chain_id: u64, supported_chains: &[(u64, &'static str)]) -> Option<&'static str> {
supported_chains
.iter()
.find(|(id, _)| *id == chain_id)
.map(|(_, name)| *name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_from_url() {
assert_eq!(
Provider::detect("https://mainnet.infura.io/v3/abc123"),
Ok(Provider::Infura)
);
assert_eq!(
Provider::detect("https://rpc.ankr.com/eth"),
Ok(Provider::Ankr)
);
assert_eq!(
Provider::detect("https://eth-mainnet.g.alchemy.com/v2/abc"),
Ok(Provider::Alchemy)
);
assert_eq!(
Provider::detect("https://example.quiknode.pro/abc"),
Ok(Provider::QuickNode)
);
assert_eq!(
Provider::detect("https://lb.drpc.live/ethereum/abc"),
Ok(Provider::Drpc)
);
assert_eq!(
Provider::detect("https://lb.drpc.org/ethereum/abc"),
Ok(Provider::Drpc)
);
}
#[test]
fn detect_unknown_provider() {
let result = Provider::detect("https://example.com");
assert!(result.is_err());
}
#[test]
fn detect_case_insensitive() {
assert_eq!(
Provider::detect("HTTPS://INFURA.IO/V3/ABC"),
Ok(Provider::Infura)
);
assert_eq!(Provider::detect("https://ANKR.COM/eth"), Ok(Provider::Ankr));
assert_eq!(
Provider::detect("https://ALCHEMY.COM/v2/abc"),
Ok(Provider::Alchemy)
);
}
#[test]
fn detect_quicknode_variants() {
assert_eq!(
Provider::detect("https://example.quiknode.pro/abc"),
Ok(Provider::QuickNode)
);
assert_eq!(
Provider::detect("https://example.quicknode.pro/abc"),
Ok(Provider::QuickNode)
);
}
#[test]
fn infura_settings() {
let settings = Provider::Infura.rpc_settings("https://mainnet.infura.io/v3/test");
assert_eq!(settings.max_concurrency, 8);
assert_eq!(settings.max_batch_size, 1000);
assert_eq!(settings.max_logs_per_request, 10_000);
assert_eq!(settings.init_batch_size, 500);
}
#[test]
fn ankr_settings() {
let settings = Provider::Ankr.rpc_settings("https://rpc.ankr.com/eth");
assert_eq!(settings.max_concurrency, 16);
assert_eq!(settings.max_batch_size, 5000);
assert_eq!(settings.max_logs_per_request, 60_000);
assert_eq!(settings.init_batch_size, 500);
}
#[test]
fn alchemy_settings() {
let settings = Provider::Alchemy.rpc_settings("https://eth-mainnet.g.alchemy.com/v2/test");
assert_eq!(settings.max_concurrency, 8);
assert_eq!(settings.max_batch_size, 1000);
assert_eq!(settings.max_logs_per_request, 10_000);
assert_eq!(settings.init_batch_size, 500);
}
#[test]
fn quicknode_settings() {
let settings = Provider::QuickNode.rpc_settings("https://example.quiknode.pro/test");
assert_eq!(settings.max_concurrency, 8);
assert_eq!(settings.max_batch_size, 1000);
assert_eq!(settings.max_logs_per_request, 10_000);
assert_eq!(settings.init_batch_size, 500);
}
#[test]
fn drpc_settings() {
let settings = Provider::Drpc.rpc_settings("https://lb.drpc.live/ethereum/test");
assert_eq!(settings.max_concurrency, 32);
assert_eq!(settings.max_batch_size, 1000);
assert_eq!(settings.max_logs_per_request, 10_000);
assert_eq!(settings.init_batch_size, 500);
}
#[test]
fn infura_http_url_mainnet() {
let url = Provider::Infura.http_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "https://mainnet.infura.io/v3/mykey");
}
#[test]
fn infura_ws_url_mainnet() {
let url = Provider::Infura.ws_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "wss://mainnet.infura.io/ws/v3/mykey");
}
#[test]
fn infura_http_url_base() {
let url = Provider::Infura.http_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "https://base-mainnet.infura.io/v3/mykey");
}
#[test]
fn infura_ws_url_base() {
let url = Provider::Infura.ws_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "wss://base-mainnet.infura.io/ws/v3/mykey");
}
#[test]
fn infura_supports_tier1_and_tier2_chains() {
let cases = [
(CHAIN_ETHEREUM, "mainnet"),
(CHAIN_ARBITRUM, "arbitrum-mainnet"),
(CHAIN_BASE, "base-mainnet"),
(CHAIN_OPTIMISM, "optimism-mainnet"),
(CHAIN_POLYGON, "polygon-mainnet"),
(CHAIN_BNB_SMART_CHAIN, "bsc-mainnet"),
];
for (chain_id, network) in cases {
let http_url = Provider::Infura.http_url("mykey", chain_id).unwrap();
let ws_url = Provider::Infura.ws_url("mykey", chain_id).unwrap();
let expected_http = format!("https://{network}.infura.io/v3/mykey");
let expected_ws = format!("wss://{network}.infura.io/ws/v3/mykey");
assert_eq!(http_url.as_str(), expected_http);
assert_eq!(ws_url.as_str(), expected_ws);
}
}
#[test]
fn infura_unsupported_chain_fails() {
let result = Provider::Infura.http_url("key", 999);
assert_eq!(
result,
Err(ProviderUrlError::UnsupportedChain {
provider: "infura",
chain_id: 999
})
);
}
#[test]
fn infura_does_not_support_robinhood_chain() {
let result = Provider::Infura.http_url("key", CHAIN_ROBINHOOD);
assert_eq!(
result,
Err(ProviderUrlError::UnsupportedChain {
provider: "infura",
chain_id: 4663
})
);
}
#[test]
fn ankr_http_url_mainnet() {
let url = Provider::Ankr.http_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "https://rpc.ankr.com/eth/mykey");
}
#[test]
fn ankr_ws_url_mainnet() {
let url = Provider::Ankr.ws_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "wss://rpc.ankr.com/eth/ws/mykey");
}
#[test]
fn ankr_http_url_base() {
let url = Provider::Ankr.http_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "https://rpc.ankr.com/base/mykey");
}
#[test]
fn ankr_ws_url_base() {
let url = Provider::Ankr.ws_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "wss://rpc.ankr.com/base/ws/mykey");
}
#[test]
fn ankr_supports_tier1_and_tier2_chains() {
let cases = [
(CHAIN_ETHEREUM, "eth"),
(CHAIN_ARBITRUM, "arbitrum"),
(CHAIN_BASE, "base"),
(CHAIN_OPTIMISM, "optimism"),
(CHAIN_POLYGON, "polygon"),
(CHAIN_BNB_SMART_CHAIN, "bsc"),
];
for (chain_id, network) in cases {
let http_url = Provider::Ankr.http_url("mykey", chain_id).unwrap();
let ws_url = Provider::Ankr.ws_url("mykey", chain_id).unwrap();
let expected_http = format!("https://rpc.ankr.com/{network}/mykey");
let expected_ws = format!("wss://rpc.ankr.com/{network}/ws/mykey");
assert_eq!(http_url.as_str(), expected_http);
assert_eq!(ws_url.as_str(), expected_ws);
}
}
#[test]
fn ankr_unsupported_chain_fails() {
let result = Provider::Ankr.http_url("key", 999);
assert_eq!(
result,
Err(ProviderUrlError::UnsupportedChain {
provider: "ankr",
chain_id: 999
})
);
}
#[test]
fn ankr_does_not_support_robinhood_chain() {
let result = Provider::Ankr.http_url("key", CHAIN_ROBINHOOD);
assert_eq!(
result,
Err(ProviderUrlError::UnsupportedChain {
provider: "ankr",
chain_id: 4663
})
);
}
#[test]
fn alchemy_http_url_mainnet() {
let url = Provider::Alchemy.http_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "https://eth-mainnet.g.alchemy.com/v2/mykey");
}
#[test]
fn alchemy_ws_url_mainnet() {
let url = Provider::Alchemy.ws_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "wss://eth-mainnet.g.alchemy.com/v2/mykey");
}
#[test]
fn alchemy_http_url_base() {
let url = Provider::Alchemy.http_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "https://base-mainnet.g.alchemy.com/v2/mykey");
}
#[test]
fn alchemy_ws_url_base() {
let url = Provider::Alchemy.ws_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "wss://base-mainnet.g.alchemy.com/v2/mykey");
}
#[test]
fn alchemy_supports_tier1_and_tier2_chains() {
let cases = [
(CHAIN_ETHEREUM, "eth-mainnet"),
(CHAIN_ARBITRUM, "arb-mainnet"),
(CHAIN_BASE, "base-mainnet"),
(CHAIN_OPTIMISM, "opt-mainnet"),
(CHAIN_POLYGON, "polygon-mainnet"),
(CHAIN_BNB_SMART_CHAIN, "bnb-mainnet"),
(CHAIN_ROBINHOOD, "robinhood-mainnet"),
];
for (chain_id, network) in cases {
let http_url = Provider::Alchemy.http_url("mykey", chain_id).unwrap();
let ws_url = Provider::Alchemy.ws_url("mykey", chain_id).unwrap();
let expected_http = format!("https://{network}.g.alchemy.com/v2/mykey");
let expected_ws = format!("wss://{network}.g.alchemy.com/v2/mykey");
assert_eq!(http_url.as_str(), expected_http);
assert_eq!(ws_url.as_str(), expected_ws);
}
}
#[test]
fn alchemy_unsupported_chain_fails() {
let result = Provider::Alchemy.http_url("key", 999);
assert_eq!(
result,
Err(ProviderUrlError::UnsupportedChain {
provider: "alchemy",
chain_id: 999
})
);
}
#[test]
fn quicknode_selects_the_endpoint_for_the_requested_chain() {
let endpoints = vec![
(
CHAIN_ETHEREUM,
"https://eth-mainnet.quiknode.pro/ethereum-key".to_string(),
),
(
CHAIN_BASE,
"https://base-mainnet.quiknode.pro/base-key/?region=us-east".to_string(),
),
];
let credentials = ProviderCredentials::Endpoints(&endpoints);
assert_eq!(
Provider::QuickNode
.http_url(credentials, CHAIN_BASE)
.unwrap()
.as_str(),
"https://base-mainnet.quiknode.pro/base-key/?region=us-east"
);
assert_eq!(
Provider::QuickNode
.ws_url(credentials, CHAIN_BASE)
.unwrap()
.as_str(),
"wss://base-mainnet.quiknode.pro/base-key/?region=us-east"
);
}
#[test]
fn quicknode_rejects_an_api_key_or_missing_chain_endpoint() {
assert_eq!(
Provider::QuickNode.http_url("key", CHAIN_ETHEREUM),
Err(ProviderUrlError::EndpointSetRequired {
provider: "quicknode"
})
);
let endpoints = vec![(
CHAIN_ETHEREUM,
"https://eth-mainnet.quiknode.pro/ethereum-key".to_string(),
)];
assert_eq!(
Provider::QuickNode.http_url(ProviderCredentials::Endpoints(&endpoints), CHAIN_BASE),
Err(ProviderUrlError::EndpointNotConfigured {
provider: "quicknode",
chain_id: CHAIN_BASE,
})
);
}
#[test]
fn drpc_supports_tier1_and_tier2_chains() {
let cases = [
(CHAIN_ETHEREUM, "ethereum"),
(CHAIN_ARBITRUM, "arbitrum"),
(CHAIN_BASE, "base"),
(CHAIN_OPTIMISM, "optimism"),
(CHAIN_POLYGON, "polygon"),
(CHAIN_BNB_SMART_CHAIN, "bsc"),
];
for (chain_id, network) in cases {
let http_url = Provider::Drpc.http_url("mykey", chain_id).unwrap();
let ws_url = Provider::Drpc.ws_url("mykey", chain_id).unwrap();
let expected_http = format!("https://lb.drpc.live/{network}/mykey");
let expected_ws = format!("wss://lb.drpc.live/{network}/mykey");
assert_eq!(http_url.as_str(), expected_http);
assert_eq!(ws_url.as_str(), expected_ws);
}
}
#[test]
fn drpc_unsupported_chain_fails() {
let result = Provider::Drpc.http_url("key", 999);
assert_eq!(
result,
Err(ProviderUrlError::UnsupportedChain {
provider: "drpc",
chain_id: 999
})
);
}
#[test]
fn provider_is_copy() {
let p1 = Provider::Infura;
let p2 = p1;
assert_eq!(p1, p2);
}
#[test]
fn provider_is_hashable() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Provider::Infura);
set.insert(Provider::Ankr);
assert_eq!(set.len(), 2);
}
}