foundry_test_utils/
rpc.rsuse foundry_config::{NamedChain, NamedChain::Optimism};
use rand::seq::SliceRandom;
use std::{
env,
sync::{
atomic::{AtomicUsize, Ordering},
LazyLock,
},
};
const ENV_WS_ARCHIVE_ENDPOINTS: &str = "WS_ARCHIVE_URLS";
const ENV_HTTP_ARCHIVE_ENDPOINTS: &str = "HTTP_ARCHIVE_URLS";
static INFURA_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
let mut keys = vec![
];
keys.shuffle(&mut rand::thread_rng());
keys
});
static ALCHEMY_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
let mut keys = vec![
"ib1f4u1ojm-9lJJypwkeZeG-75TJRB7O",
"7mTtk6IW4DwroGnKmG_bOWri2hyaGYhX",
"GL4M0hfzSYGU5e1_t804HoUDOObWP-FA",
"WV407BEiBmjNJfKo9Uo_55u0z0ITyCOX",
"Ge56dH9siMF4T0whP99sQXOcr2mFs8wZ",
"K-uNlqYoYCO9cdBHcifwCDAcEjDy1UHL",
"GWdgwabOE2XfBdLp_gIq-q6QHa7DSoag",
"Uz0cF5HCXFtpZlvd9NR7kHxfB_Wdpsx7",
"wWZMf1SOu9lT1GNIJHOX-5WL1MiYXycT",
"HACxy4wNUoD-oLlCq_v5LG0bclLc_DRL",
"_kCjfMjYo8x0rOm6YzmvSI0Qk-c8SO5I",
"kD-M-g5TKb957S3bbOXxXPeMUxm1uTuU",
"jQqqfTOQN_7A6gQEjzRYpVwXzxEBN9aj",
"jGiK5vwDfC3F4r0bqukm-W2GqgdrxdSr",
"Reoz-NZSjWczcAQOeVTz_Ejukb8mAton",
"-DQx9U-heCeTgYsAXwaTurmGytc-0mbR",
"sDNCLu_e99YZRkbWlVHiuM3BQ5uxYCZU",
"M6lfpxTBrywHOvKXOS4yb7cTTpa25ZQ9",
"UK8U_ogrbYB4lQFTGJHHDrbiS4UPnac6",
"Lc7oIGYeL_QvInzI0Wiu_pOZZDEKBrdf",
"UVatYU2Ax0rX6bDiqddeTRDdcCxzdpoE",
"bVjX9v-FpmUhf5R_oHIgwJx2kXvYPRbx",
];
keys.shuffle(&mut rand::thread_rng());
keys
});
static ETHERSCAN_MAINNET_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
let mut keys = vec![
"MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6",
"JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1",
"ZSMDY6BI2H55MBE3G9CUUQT4XYUDBB6ZSK",
"4FYHTY429IXYMJNS4TITKDMUKW5QRYDX61",
"QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I",
"VXMQ117UN58Y4RHWUB8K1UGCEA7UQEWK55",
"C7I2G4JTA5EPYS42Z8IZFEIMQNI5GXIJEV",
"A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74",
"3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG",
"ZUB97R31KSYX7NYVW6224Q6EYY6U56H591",
];
keys.shuffle(&mut rand::thread_rng());
keys
});
static ETHERSCAN_OPTIMISM_KEYS: LazyLock<Vec<&'static str>> =
LazyLock::new(|| vec!["JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46"]);
fn next_idx() -> usize {
static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
}
fn next<T>(list: &[T]) -> &T {
&list[next_idx() % list.len()]
}
pub fn next_http_rpc_endpoint() -> String {
next_rpc_endpoint(NamedChain::Mainnet)
}
pub fn next_ws_rpc_endpoint() -> String {
next_ws_endpoint(NamedChain::Mainnet)
}
pub fn next_rpc_endpoint(chain: NamedChain) -> String {
next_url(false, chain)
}
pub fn next_ws_endpoint(chain: NamedChain) -> String {
next_url(true, chain)
}
pub fn next_http_archive_rpc_url() -> String {
next_archive_url(false)
}
pub fn next_ws_archive_rpc_url() -> String {
next_archive_url(true)
}
fn next_archive_url(is_ws: bool) -> String {
let urls = archive_urls(is_ws);
let url = if env_archive_urls(is_ws).is_empty() {
next(urls)
} else {
urls.choose_weighted(&mut rand::thread_rng(), |url| {
if url.contains("reth") {
2usize
} else {
1usize
}
})
.unwrap()
};
eprintln!("--- next_archive_url(is_ws={is_ws}) = {url} ---");
url.clone()
}
fn archive_urls(is_ws: bool) -> &'static [String] {
static WS: LazyLock<Vec<String>> = LazyLock::new(|| get(true));
static HTTP: LazyLock<Vec<String>> = LazyLock::new(|| get(false));
fn get(is_ws: bool) -> Vec<String> {
let env_urls = env_archive_urls(is_ws);
if !env_urls.is_empty() {
let mut urls = env_urls.to_vec();
urls.shuffle(&mut rand::thread_rng());
return urls;
}
let mut urls = Vec::new();
for &key in ALCHEMY_KEYS.iter() {
if is_ws {
urls.push(format!("wss://eth-mainnet.g.alchemy.com/v2/{key}"));
} else {
urls.push(format!("https://eth-mainnet.g.alchemy.com/v2/{key}"));
}
}
urls
}
if is_ws {
&WS
} else {
&HTTP
}
}
fn env_archive_urls(is_ws: bool) -> &'static [String] {
static WS: LazyLock<Vec<String>> = LazyLock::new(|| get(true));
static HTTP: LazyLock<Vec<String>> = LazyLock::new(|| get(false));
fn get(is_ws: bool) -> Vec<String> {
let env = if is_ws { ENV_WS_ARCHIVE_ENDPOINTS } else { ENV_HTTP_ARCHIVE_ENDPOINTS };
let env = env::var(env).unwrap_or_default();
let env = env.trim();
if env.is_empty() {
return vec![];
}
env.split(',').map(str::trim).filter(|s| !s.is_empty()).map(ToString::to_string).collect()
}
if is_ws {
&WS
} else {
&HTTP
}
}
pub fn next_mainnet_etherscan_api_key() -> String {
next_etherscan_api_key(NamedChain::Mainnet)
}
pub fn next_etherscan_api_key(chain: NamedChain) -> String {
let keys = match chain {
Optimism => ÐERSCAN_OPTIMISM_KEYS,
_ => ÐERSCAN_MAINNET_KEYS,
};
let key = next(keys).to_string();
eprintln!("--- next_etherscan_api_key(chain={chain:?}) = {key} ---");
key
}
fn next_url(is_ws: bool, chain: NamedChain) -> String {
use NamedChain::*;
if matches!(chain, NamedChain::Base) {
return "https://mainnet.base.org".to_string();
}
let idx = next_idx() % (INFURA_KEYS.len() + ALCHEMY_KEYS.len());
let is_infura = idx < INFURA_KEYS.len();
let key = if is_infura { INFURA_KEYS[idx] } else { ALCHEMY_KEYS[idx - INFURA_KEYS.len()] };
let prefix = if is_infura {
match chain {
Optimism => "optimism",
Arbitrum => "arbitrum",
Polygon => "polygon",
_ => "",
}
} else {
match chain {
Optimism => "opt",
Arbitrum => "arb",
Polygon => "polygon",
_ => "eth",
}
};
let network = if is_infura {
match chain {
Mainnet | Optimism | Arbitrum | Polygon => "mainnet",
_ => chain.as_str(),
}
} else {
match chain {
Mainnet | Optimism | Arbitrum | Polygon => "mainnet",
_ => chain.as_str(),
}
};
let full = if prefix.is_empty() { network.to_string() } else { format!("{prefix}-{network}") };
let url = match (is_ws, is_infura) {
(false, true) => format!("https://{full}.infura.io/v3/{key}"),
(true, true) => format!("wss://{full}.infura.io/ws/v3/{key}"),
(false, false) => format!("https://{full}.g.alchemy.com/v2/{key}"),
(true, false) => format!("wss://{full}.g.alchemy.com/v2/{key}"),
};
eprintln!("--- next_url(is_ws={is_ws}, chain={chain:?}) = {url} ---");
url
}
#[cfg(test)]
#[allow(clippy::needless_return, clippy::disallowed_macros)]
mod tests {
use super::*;
use alloy_primitives::address;
use foundry_config::Chain;
#[tokio::test]
#[ignore = "run manually"]
async fn test_etherscan_keys() {
let address = address!("dAC17F958D2ee523a2206206994597C13D831ec7");
let mut first_abi = None;
let mut failed = Vec::new();
for (i, &key) in ETHERSCAN_MAINNET_KEYS.iter().enumerate() {
println!("trying key {i} ({key})");
let client = foundry_block_explorers::Client::builder()
.chain(Chain::mainnet())
.unwrap()
.with_api_key(key)
.build()
.unwrap();
let mut fail = |e: &str| {
eprintln!("key {i} ({key}) failed: {e}");
failed.push(key);
};
let abi = match client.contract_abi(address).await {
Ok(abi) => abi,
Err(e) => {
fail(&e.to_string());
continue;
}
};
if let Some(first_abi) = &first_abi {
if abi != *first_abi {
fail("abi mismatch");
}
} else {
first_abi = Some(abi);
}
}
if !failed.is_empty() {
panic!("failed keys: {failed:#?}");
}
}
}