1use foundry_config::{
4 NamedChain::{
5 self, Arbitrum, Base, BinanceSmartChainTestnet, Celo, Mainnet, Optimism, Polygon, Sepolia,
6 },
7 RpcEndpointUrl, RpcEndpoints,
8};
9use rand::seq::SliceRandom;
10use std::{
11 env,
12 sync::{
13 LazyLock,
14 atomic::{AtomicUsize, Ordering},
15 },
16};
17
18macro_rules! shuffled_list {
19 ($name:ident, $e:expr $(,)?) => {
20 static $name: LazyLock<ShuffledList<&'static str>> =
21 LazyLock::new(|| ShuffledList::new($e));
22 };
23}
24
25struct ShuffledList<T> {
26 list: Vec<T>,
27 index: AtomicUsize,
28}
29
30impl<T> ShuffledList<T> {
31 fn new(mut list: Vec<T>) -> Self {
32 assert!(!list.is_empty());
33 list.shuffle(&mut rand::rng());
34 Self { list, index: AtomicUsize::new(0) }
35 }
36
37 fn next(&self) -> &T {
38 let index = self.index.fetch_add(1, Ordering::Relaxed);
39 &self.list[index % self.list.len()]
40 }
41}
42
43shuffled_list!(
44 HTTP_ARCHIVE_DOMAINS,
45 vec![
46 "ethereum.reth.rs/rpc",
48 ],
49);
50shuffled_list!(
51 HTTP_DOMAINS,
52 vec![
53 "ethereum.reth.rs/rpc",
55 ],
56);
57shuffled_list!(
58 WS_ARCHIVE_DOMAINS,
59 vec![
60 "ethereum.reth.rs/ws",
62 ],
63);
64shuffled_list!(
65 WS_DOMAINS,
66 vec![
67 "ethereum.reth.rs/ws",
69 ],
70);
71
72shuffled_list!(
74 DRPC_KEYS,
75 vec![
76 "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
77 "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
78 ],
79);
80
81shuffled_list!(
83 ETHERSCAN_KEYS,
84 vec![
85 "MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6",
86 "JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1",
87 "ZSMDY6BI2H55MBE3G9CUUQT4XYUDBB6ZSK",
88 "4FYHTY429IXYMJNS4TITKDMUKW5QRYDX61",
89 "QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I",
90 "VXMQ117UN58Y4RHWUB8K1UGCEA7UQEWK55",
91 "C7I2G4JTA5EPYS42Z8IZFEIMQNI5GXIJEV",
92 "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74",
93 "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG",
94 ],
95);
96
97pub fn rpc_endpoints() -> RpcEndpoints {
99 RpcEndpoints::new([
100 ("mainnet", RpcEndpointUrl::Url(next_http_archive_rpc_url())),
101 ("mainnet2", RpcEndpointUrl::Url(next_http_archive_rpc_url())),
102 ("sepolia", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Sepolia))),
103 ("optimism", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Optimism))),
104 ("arbitrum", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Arbitrum))),
105 ("polygon", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Polygon))),
106 ("bsc", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::BinanceSmartChain))),
107 ("avaxTestnet", RpcEndpointUrl::Url("https://api.avax-test.network/ext/bc/C/rpc".into())),
108 ("moonbeam", RpcEndpointUrl::Url("https://moonbeam-rpc.publicnode.com".into())),
109 ("polkadotTestnet", RpcEndpointUrl::Url("https://eth-rpc-testnet.polkadot.io".into())),
110 ("kusama", RpcEndpointUrl::Url("https://eth-rpc-kusama.polkadot.io".into())),
111 ("polkadot", RpcEndpointUrl::Url("https://eth-rpc.polkadot.io".into())),
112 ("rpcEnvAlias", RpcEndpointUrl::Env("${RPC_ENV_ALIAS}".into())),
113 ])
114}
115
116pub fn next_http_rpc_endpoint() -> String {
120 next_rpc_endpoint(NamedChain::Mainnet)
121}
122
123pub fn next_ws_rpc_endpoint() -> String {
127 next_ws_endpoint(NamedChain::Mainnet)
128}
129
130pub fn next_rpc_endpoint(chain: NamedChain) -> String {
132 next_url(false, chain)
133}
134
135pub fn next_ws_endpoint(chain: NamedChain) -> String {
137 next_url(true, chain)
138}
139
140pub fn next_http_archive_rpc_url() -> String {
142 next_archive_url(false)
143}
144
145pub fn next_ws_archive_rpc_url() -> String {
147 next_archive_url(true)
148}
149
150fn next_archive_url(is_ws: bool) -> String {
152 let domain = if is_ws { &WS_ARCHIVE_DOMAINS } else { &HTTP_ARCHIVE_DOMAINS }.next();
153 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
154 test_debug!("next_archive_url(is_ws={is_ws}) = {}", debug_url(&url));
155 url
156}
157
158pub fn next_etherscan_api_key() -> String {
160 let mut key = env::var("ETHERSCAN_KEY").unwrap_or_default();
161 if key.is_empty() {
162 key = ETHERSCAN_KEYS.next().to_string();
163 }
164 test_debug!("next_etherscan_api_key() = {}...", &key[..6]);
165 key
166}
167
168fn next_url(is_ws: bool, chain: NamedChain) -> String {
169 let url = next_url_inner(is_ws, chain);
170 test_debug!("next_url(is_ws={is_ws}, chain={chain:?}) = {}", debug_url(&url));
171 url
172}
173
174fn next_url_inner(is_ws: bool, chain: NamedChain) -> String {
175 if matches!(chain, Base) {
176 return "https://mainnet.base.org".to_string();
177 }
178
179 if matches!(chain, Optimism) {
180 return "https://mainnet.optimism.io".to_string();
181 }
182
183 if matches!(chain, BinanceSmartChainTestnet) {
184 return "https://bsc-testnet-rpc.publicnode.com".to_string();
185 }
186
187 if matches!(chain, Celo) {
188 return "https://celo.drpc.org".to_string();
189 }
190
191 if matches!(chain, Sepolia) {
192 let rpc_url = env::var("ETH_SEPOLIA_RPC").unwrap_or_default();
193 if !rpc_url.is_empty() {
194 return rpc_url;
195 }
196 }
197
198 if matches!(chain, Arbitrum) {
199 let rpc_url = env::var("ARBITRUM_RPC").unwrap_or_default();
200 if !rpc_url.is_empty() {
201 return rpc_url;
202 }
203 }
204
205 let reth_works = true;
206 let domain = if reth_works && matches!(chain, Mainnet) {
207 *(if is_ws { &WS_DOMAINS } else { &HTTP_DOMAINS }).next()
208 } else {
209 let key = DRPC_KEYS.next();
211 let network = match chain {
212 Mainnet => "ethereum",
213 Polygon => "polygon",
214 Arbitrum => "arbitrum",
215 Sepolia => "sepolia",
216 _ => "",
217 };
218 &format!("lb.drpc.org/ogrpc?network={network}&dkey={key}")
219 };
220
221 if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") }
222}
223
224fn debug_url(url: &str) -> impl std::fmt::Display + '_ {
226 let url = reqwest::Url::parse(url).unwrap();
227 format!(
228 "{scheme}://{host}{path}",
229 scheme = url.scheme(),
230 host = url.host_str().unwrap(),
231 path = url.path().get(..8).unwrap_or(url.path()),
232 )
233}
234
235#[cfg(test)]
236#[expect(clippy::disallowed_macros)]
237mod tests {
238 use super::*;
239 use alloy_primitives::address;
240 use foundry_config::Chain;
241
242 #[tokio::test]
243 #[ignore = "run manually"]
244 async fn test_etherscan_keys() {
245 let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
246 let mut first_abi = None;
247 let mut failed = Vec::new();
248 for (i, &key) in ETHERSCAN_KEYS.list.iter().enumerate() {
249 println!("trying key {i} ({key})");
250
251 let client = foundry_block_explorers::Client::builder()
252 .chain(Chain::mainnet())
253 .unwrap()
254 .with_api_key(key)
255 .build()
256 .unwrap();
257
258 let mut fail = |e: &str| {
259 eprintln!("key {i} ({key}) failed: {e}");
260 failed.push(key);
261 };
262
263 let abi = match client.contract_abi(address).await {
264 Ok(abi) => abi,
265 Err(e) => {
266 fail(&e.to_string());
267 continue;
268 }
269 };
270
271 if let Some(first_abi) = &first_abi {
272 if abi != *first_abi {
273 fail("abi mismatch");
274 }
275 } else {
276 first_abi = Some(abi);
277 }
278 }
279 if !failed.is_empty() {
280 panic!("failed keys: {failed:#?}");
281 }
282 }
283}