use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use alloy::{providers::Provider, rpc::types::Filter};
use eyre::{Result, bail};
use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use tokio::time::sleep;
use crate::misc::shared_init::init_provider;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RpcEndpoint {
pub url: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct NativeCurrency {
pub name: String,
pub symbol: String,
pub decimals: u32,
}
#[derive(Debug, Serialize, Clone)]
pub struct Explorer {
pub url: String,
}
impl<'de> Deserialize<'de> for Explorer {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Url(String),
Obj { url: String },
}
Ok(match Raw::deserialize(deserializer)? {
Raw::Url(url) | Raw::Obj { url } => Explorer { url },
})
}
}
const CHAINLIST_URL: &str = "https://chainlist.org/rpcs.json";
const CACHE_KEY: &str = "chainlist_rpcs";
const CACHE_EXPIRY_SECONDS: u64 = 60;
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ChainInfo {
#[serde(rename = "chainId")]
pub chain_id: u64,
pub name: String,
pub chain: String,
#[serde(rename = "rpc")]
pub rpc_endpoints: Vec<RpcEndpoint>,
#[serde(rename = "nativeCurrency")]
pub native_currency: NativeCurrency,
#[serde(default)]
pub explorers: Vec<Explorer>,
#[serde(skip)]
pub benchmarked_rpc_urls: Vec<(String, u64)>,
}
pub async fn get_chain_info_no_benchmark(chain_id: u64) -> Result<ChainInfo> {
let chains = get_all_chains().await?;
let chain = chains
.into_iter()
.find(|c| c.chain_id == chain_id)
.ok_or_else(|| eyre::eyre!("Chain ID {} not found", chain_id))?;
Ok(chain)
}
pub async fn get_chain_id_from_rpc(rpc_url: &str) -> Result<u64> {
let provider = init_provider(rpc_url).await?;
let chain_id = provider.get_chain_id().await?;
Ok(chain_id)
}
pub async fn get_chain_info(chain_id: u64, timeout_ms: u64, limit: usize) -> Result<ChainInfo> {
let chains = get_all_chains().await?;
let mut chain = chains
.into_iter()
.find(|c| c.chain_id == chain_id)
.ok_or_else(|| eyre::eyre!("Chain ID {} not found", chain_id))?;
let benchmark_futures = chain
.rpc_endpoints
.iter()
.filter(|endpoint| endpoint.url.starts_with("https://"))
.filter(|endpoint| !endpoint.url.contains("${"))
.map(|endpoint| async move {
match benchmark_url(endpoint.url.clone(), timeout_ms).await {
Ok(duration) => Some((endpoint.url.clone(), duration)),
Err(_) => None,
}
})
.collect::<Vec<_>>();
let stream = futures_util::stream::iter(benchmark_futures)
.buffer_unordered(10)
.filter_map(|result| async move { result })
.take(limit);
let stream = hotpath::stream!(stream, log = true);
let mut benchmarked_rpc_urls: Vec<(String, u64)> = stream.collect().await;
benchmarked_rpc_urls.sort_by_key(|(_, duration)| *duration);
chain.benchmarked_rpc_urls = benchmarked_rpc_urls;
Ok(chain)
}
pub async fn get_all_chains() -> Result<Vec<ChainInfo>> {
let cache_dir = get_cache_dir();
if let Ok(cached_data) = get_cached_chains(&cache_dir).await {
return Ok(cached_data);
}
let client = reqwest::Client::new();
let response = client.get(CHAINLIST_URL).send().await?;
let status = response.status();
let body = response.text().await?;
let chains: Vec<ChainInfo> = serde_json::from_str(&body).map_err(|e| {
let snippet: String = body.chars().take(500).collect();
eyre::eyre!(
"Failed to parse chainlist JSON (status {}, {} bytes): {}\nbody snippet: {}",
status,
body.len(),
e,
snippet
)
})?;
if let Err(e) = cache_chains(&cache_dir, &chains).await {
eprintln!("Warning: Failed to cache chains data: {e}");
}
Ok(chains)
}
fn get_cache_dir() -> std::path::PathBuf {
let home_dir = home::home_dir().unwrap();
home_dir.join(".mevlog").join(".chainlist-rpcs")
}
async fn get_cached_chains(cache_dir: &std::path::Path) -> Result<Vec<ChainInfo>> {
let cached_data = cacache::read(cache_dir, CACHE_KEY).await?;
let cache_info = cacache::metadata(cache_dir, CACHE_KEY)
.await?
.ok_or_else(|| eyre::eyre!("Cache metadata not found"))?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
let cache_time = cache_info.time;
if now - cache_time > (CACHE_EXPIRY_SECONDS as u128 * 1000) {
bail!("Cache expired");
}
let chains: Vec<ChainInfo> = serde_json::from_slice(&cached_data)?;
Ok(chains)
}
async fn cache_chains(cache_dir: &std::path::Path, chains: &[ChainInfo]) -> Result<()> {
let data = serde_json::to_vec(chains)?;
cacache::write(cache_dir, CACHE_KEY, data).await?;
Ok(())
}
pub async fn benchmark_url(url: String, timeout_ms: u64) -> Result<u64> {
let provider = init_provider(&url).await?;
let latest = tokio::select! {
result = provider.get_block_number() => {
result.map_err(|_| eyre::eyre!("RPC URL returned an error"))?
}
_ = sleep(Duration::from_millis(timeout_ms)) => {
bail!("RPC URL timed out");
}
};
let block = latest.saturating_sub(10);
let filter = Filter::new().from_block(block).to_block(block);
let start = Instant::now();
tokio::select! {
result = provider.get_logs(&filter) => {
result.map_err(|_| eyre::eyre!("eth_getLogs not supported"))?;
Ok(start.elapsed().as_millis() as u64)
}
_ = sleep(Duration::from_millis(timeout_ms)) => {
bail!("RPC URL timed out");
}
}
}