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 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"),
];
const QUICKNODE_CHAINS: &[(u64, &str)] = &[
(CHAIN_ETHEREUM, "eth-mainnet"),
(CHAIN_ARBITRUM, "arbitrum-mainnet"),
(CHAIN_BASE, "base-mainnet"),
(CHAIN_OPTIMISM, "optimism"),
(CHAIN_POLYGON, "matic"),
(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,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Provider {
Infura,
Ankr,
Alchemy,
QuickNode,
}
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 {
Err("unknown provider")
}
}
}
impl Provider {
fn name(&self) -> &'static str {
match self {
Provider::Infura => "infura",
Provider::Ankr => "ankr",
Provider::Alchemy => "alchemy",
Provider::QuickNode => "quicknode",
}
}
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,
},
}
}
pub fn http_url(&self, api_key: &str, chain_id: u64) -> Result<Url, ProviderUrlError> {
self.build_url(api_key, chain_id, false)
}
pub fn ws_url(&self, api_key: &str, chain_id: u64) -> Result<Url, ProviderUrlError> {
self.build_url(api_key, chain_id, true)
}
fn build_url(
&self,
api_key: &str,
chain_id: u64,
websocket: bool,
) -> Result<Url, ProviderUrlError> {
let unsupported_chain_error = || ProviderUrlError::UnsupportedChain {
provider: self.name(),
chain_id,
};
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" };
Ok(format!("{scheme}://rpc.ankr.com/{chain}/{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" };
let path = if websocket { "ws/v2" } else { "v2" };
Ok(format!("{scheme}://{chain}.g.alchemy.com/{path}/{api_key}")
.parse()
.unwrap())
}
Provider::QuickNode => {
let chain =
chain_name(chain_id, QUICKNODE_CHAINS).ok_or_else(unsupported_chain_error)?;
let scheme = if websocket { "wss" } else { "https" };
Ok(format!("{scheme}://{chain}.quiknode.pro/{api_key}")
.parse()
.unwrap())
}
}
}
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)
);
}
#[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 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 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/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/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}/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 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/ws/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/ws/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"),
];
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/ws/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_http_url_mainnet() {
let url = Provider::QuickNode.http_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "https://eth-mainnet.quiknode.pro/mykey");
}
#[test]
fn quicknode_ws_url_mainnet() {
let url = Provider::QuickNode.ws_url("mykey", 1).unwrap();
assert_eq!(url.as_str(), "wss://eth-mainnet.quiknode.pro/mykey");
}
#[test]
fn quicknode_http_url_base() {
let url = Provider::QuickNode.http_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "https://base-mainnet.quiknode.pro/mykey");
}
#[test]
fn quicknode_ws_url_base() {
let url = Provider::QuickNode.ws_url("mykey", 8453).unwrap();
assert_eq!(url.as_str(), "wss://base-mainnet.quiknode.pro/mykey");
}
#[test]
fn quicknode_supports_tier1_and_tier2_chains() {
let cases = [
(CHAIN_ETHEREUM, "eth-mainnet"),
(CHAIN_ARBITRUM, "arbitrum-mainnet"),
(CHAIN_BASE, "base-mainnet"),
(CHAIN_OPTIMISM, "optimism"),
(CHAIN_POLYGON, "matic"),
(CHAIN_BNB_SMART_CHAIN, "bsc"),
];
for (chain_id, network) in cases {
let http_url = Provider::QuickNode.http_url("mykey", chain_id).unwrap();
let ws_url = Provider::QuickNode.ws_url("mykey", chain_id).unwrap();
let expected_http = format!("https://{network}.quiknode.pro/mykey");
let expected_ws = format!("wss://{network}.quiknode.pro/mykey");
assert_eq!(http_url.as_str(), expected_http);
assert_eq!(ws_url.as_str(), expected_ws);
}
}
#[test]
fn quicknode_unsupported_chain_fails() {
let result = Provider::QuickNode.http_url("key", 999);
assert_eq!(
result,
Err(ProviderUrlError::UnsupportedChain {
provider: "quicknode",
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);
}
}