foundry_test_utils/
rpc.rs1use foundry_config::{
4 NamedChain,
5 NamedChain::{
6 Arbitrum, Base, BinanceSmartChainTestnet, Celo, Mainnet, Optimism, Polygon, Sepolia,
7 },
8};
9use rand::seq::SliceRandom;
10use std::sync::{
11 LazyLock,
12 atomic::{AtomicUsize, Ordering},
13};
14
15fn shuffled<T>(mut vec: Vec<T>) -> Vec<T> {
16 vec.shuffle(&mut rand::rng());
17 vec
18}
19
20macro_rules! shuffled_list {
21 ($name:ident, $e:expr $(,)?) => {
22 static $name: LazyLock<Vec<&'static str>> = LazyLock::new(|| shuffled($e));
23 };
24}
25
26shuffled_list!(
27 HTTP_ARCHIVE_DOMAINS,
28 vec![
29 "reth-ethereum.ithaca.xyz/rpc",
31 ],
32);
33shuffled_list!(
34 HTTP_DOMAINS,
35 vec![
36 "reth-ethereum.ithaca.xyz/rpc",
38 "reth-ethereum-full.ithaca.xyz/rpc",
39 ],
40);
41shuffled_list!(
42 WS_ARCHIVE_DOMAINS,
43 vec![
44 "reth-ethereum.ithaca.xyz/ws",
46 ],
47);
48shuffled_list!(
49 WS_DOMAINS,
50 vec![
51 "reth-ethereum.ithaca.xyz/ws",
53 "reth-ethereum-full.ithaca.xyz/ws",
54 ],
55);
56
57shuffled_list!(
59 DRPC_KEYS,
60 vec![
61 "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
62 "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
63 ],
64);
65
66shuffled_list!(
68 ETHERSCAN_KEYS,
69 vec![
70 "MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6",
71 "JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1",
72 "ZSMDY6BI2H55MBE3G9CUUQT4XYUDBB6ZSK",
73 "4FYHTY429IXYMJNS4TITKDMUKW5QRYDX61",
74 "QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I",
75 "VXMQ117UN58Y4RHWUB8K1UGCEA7UQEWK55",
76 "C7I2G4JTA5EPYS42Z8IZFEIMQNI5GXIJEV",
77 "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74",
78 "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG",
79 ],
80);
81
82fn next_idx() -> usize {
84 static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
85 NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
86}
87
88fn next<T>(list: &[T]) -> &T {
90 &list[next_idx() % list.len()]
91}
92
93pub fn next_http_rpc_endpoint() -> String {
97 next_rpc_endpoint(NamedChain::Mainnet)
98}
99
100pub fn next_ws_rpc_endpoint() -> String {
104 next_ws_endpoint(NamedChain::Mainnet)
105}
106
107pub fn next_rpc_endpoint(chain: NamedChain) -> String {
109 next_url(false, chain)
110}
111
112pub fn next_ws_endpoint(chain: NamedChain) -> String {
114 next_url(true, chain)
115}
116
117pub fn next_http_archive_rpc_url() -> String {
119 next_archive_url(false)
120}
121
122pub fn next_ws_archive_rpc_url() -> String {
124 next_archive_url(true)
125}
126
127fn next_archive_url(is_ws: bool) -> String {
129 let domain = next(if is_ws { &WS_ARCHIVE_DOMAINS } else { &HTTP_ARCHIVE_DOMAINS });
130 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
131 eprintln!("--- next_archive_url(is_ws={is_ws}) = {url} ---");
132 url
133}
134
135pub fn next_etherscan_api_key() -> String {
137 let key = next(ÐERSCAN_KEYS).to_string();
138 eprintln!("--- next_etherscan_api_key() = {key} ---");
139 key
140}
141
142fn next_url(is_ws: bool, chain: NamedChain) -> String {
143 if matches!(chain, Base) {
144 return "https://mainnet.base.org".to_string();
145 }
146
147 if matches!(chain, Optimism) {
148 return "https://mainnet.optimism.io".to_string();
149 }
150
151 if matches!(chain, BinanceSmartChainTestnet) {
152 return "https://bsc-testnet-rpc.publicnode.com".to_string();
153 }
154
155 if matches!(chain, Celo) {
156 return "https://celo.drpc.org".to_string();
157 }
158
159 let reth_works = true;
160 let domain = if reth_works && matches!(chain, Mainnet) {
161 *next(if is_ws { &WS_DOMAINS } else { &HTTP_DOMAINS })
162 } else {
163 let key = next(&DRPC_KEYS);
165
166 let network = match chain {
167 Mainnet => "ethereum",
168 Arbitrum => "arbitrum",
169 Polygon => "polygon",
170 Sepolia => "sepolia",
171 _ => "",
172 };
173 &format!("lb.drpc.org/ogrpc?network={network}&dkey={key}")
174 };
175
176 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
177
178 eprintln!("--- next_url(is_ws={is_ws}, chain={chain:?}) = {url} ---");
179 url
180}
181
182#[cfg(test)]
183#[expect(clippy::disallowed_macros)]
184mod tests {
185 use super::*;
186 use alloy_primitives::address;
187 use foundry_config::Chain;
188
189 #[tokio::test]
190 #[ignore = "run manually"]
191 async fn test_etherscan_keys() {
192 let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
193 let mut first_abi = None;
194 let mut failed = Vec::new();
195 for (i, &key) in ETHERSCAN_KEYS.iter().enumerate() {
196 println!("trying key {i} ({key})");
197
198 let client = foundry_block_explorers::Client::builder()
199 .chain(Chain::mainnet())
200 .unwrap()
201 .with_api_key(key)
202 .build()
203 .unwrap();
204
205 let mut fail = |e: &str| {
206 eprintln!("key {i} ({key}) failed: {e}");
207 failed.push(key);
208 };
209
210 let abi = match client.contract_abi(address).await {
211 Ok(abi) => abi,
212 Err(e) => {
213 fail(&e.to_string());
214 continue;
215 }
216 };
217
218 if let Some(first_abi) = &first_abi {
219 if abi != *first_abi {
220 fail("abi mismatch");
221 }
222 } else {
223 first_abi = Some(abi);
224 }
225 }
226 if !failed.is_empty() {
227 panic!("failed keys: {failed:#?}");
228 }
229 }
230}