foundry_test_utils/
rpc.rs
1use foundry_config::{
4 NamedChain,
5 NamedChain::{Arbitrum, Base, BinanceSmartChainTestnet, Mainnet, Optimism, Polygon, Sepolia},
6};
7use rand::seq::SliceRandom;
8use std::sync::{
9 LazyLock,
10 atomic::{AtomicUsize, Ordering},
11};
12
13fn shuffled<T>(mut vec: Vec<T>) -> Vec<T> {
14 vec.shuffle(&mut rand::rng());
15 vec
16}
17
18static RETH_ARCHIVE_HOSTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
20 shuffled(vec![
21 "reth-ethereum.ithaca.xyz",
23 ])
24});
25
26static RETH_HOSTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
28 shuffled(vec![
29 "reth-ethereum.ithaca.xyz",
31 "reth-ethereum-full.ithaca.xyz",
32 ])
33});
34
35static DRPC_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
37 shuffled(vec![
38 "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
40 "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
41 ])
42});
43
44static ETHERSCAN_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
46 shuffled(vec![
47 "MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6",
48 "JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1",
49 "ZSMDY6BI2H55MBE3G9CUUQT4XYUDBB6ZSK",
50 "4FYHTY429IXYMJNS4TITKDMUKW5QRYDX61",
51 "QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I",
52 "VXMQ117UN58Y4RHWUB8K1UGCEA7UQEWK55",
53 "C7I2G4JTA5EPYS42Z8IZFEIMQNI5GXIJEV",
54 "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74",
55 "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG",
56 ])
57});
58
59fn next_idx() -> usize {
61 static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
62 NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
63}
64
65fn next<T>(list: &[T]) -> &T {
67 &list[next_idx() % list.len()]
68}
69
70pub fn next_http_rpc_endpoint() -> String {
74 next_rpc_endpoint(NamedChain::Mainnet)
75}
76
77pub fn next_ws_rpc_endpoint() -> String {
81 next_ws_endpoint(NamedChain::Mainnet)
82}
83
84pub fn next_rpc_endpoint(chain: NamedChain) -> String {
86 next_url(false, chain)
87}
88
89pub fn next_ws_endpoint(chain: NamedChain) -> String {
91 next_url(true, chain)
92}
93
94pub fn next_http_archive_rpc_url() -> String {
96 next_archive_url(false)
97}
98
99pub fn next_ws_archive_rpc_url() -> String {
101 next_archive_url(true)
102}
103
104fn next_archive_url(is_ws: bool) -> String {
106 let urls = archive_urls(is_ws);
107 let url = next(urls);
108 eprintln!("--- next_archive_url(is_ws={is_ws}) = {url} ---");
109 url.clone()
110}
111
112fn archive_urls(is_ws: bool) -> &'static [String] {
113 static WS: LazyLock<Vec<String>> = LazyLock::new(|| get(true));
114 static HTTP: LazyLock<Vec<String>> = LazyLock::new(|| get(false));
115
116 fn get(is_ws: bool) -> Vec<String> {
117 let mut urls = vec![];
118
119 for &host in RETH_ARCHIVE_HOSTS.iter() {
120 if is_ws {
121 urls.push(format!("wss://{host}/ws"));
122 } else {
123 urls.push(format!("https://{host}/rpc"));
124 }
125 }
126
127 urls
128 }
129
130 if is_ws { &WS } else { &HTTP }
131}
132
133pub fn next_etherscan_api_key() -> String {
135 let key = next(ÐERSCAN_KEYS).to_string();
136 eprintln!("--- next_etherscan_api_key() = {key} ---");
137 key
138}
139
140fn next_url(is_ws: bool, chain: NamedChain) -> String {
141 if matches!(chain, Base) {
142 return "https://mainnet.base.org".to_string();
143 }
144
145 if matches!(chain, Optimism) {
146 return "https://mainnet.optimism.io".to_string();
147 }
148
149 if matches!(chain, BinanceSmartChainTestnet) {
150 return "https://bsc-testnet-rpc.publicnode.com".to_string();
151 }
152
153 let domain = if matches!(chain, Mainnet) {
154 let idx = next_idx() % RETH_HOSTS.len();
156 let host = RETH_HOSTS[idx];
157 if is_ws { format!("{host}/ws") } else { format!("{host}/rpc") }
158 } else {
159 let idx = next_idx() % DRPC_KEYS.len();
161 let key = DRPC_KEYS[idx];
162
163 let network = match chain {
164 Arbitrum => "arbitrum",
165 Polygon => "polygon",
166 Sepolia => "sepolia",
167 _ => "",
168 };
169 format!("lb.drpc.org/ogrpc?network={network}&dkey={key}")
170 };
171
172 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
173
174 eprintln!("--- next_url(is_ws={is_ws}, chain={chain:?}) = {url} ---");
175 url
176}
177
178#[cfg(test)]
179#[expect(clippy::disallowed_macros)]
180mod tests {
181 use super::*;
182 use alloy_primitives::address;
183 use foundry_block_explorers::EtherscanApiVersion;
184 use foundry_config::Chain;
185
186 #[tokio::test]
187 #[ignore = "run manually"]
188 async fn test_etherscan_keys() {
189 let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
190 let mut first_abi = None;
191 let mut failed = Vec::new();
192 for (i, &key) in ETHERSCAN_KEYS.iter().enumerate() {
193 println!("trying key {i} ({key})");
194
195 let client = foundry_block_explorers::Client::builder()
196 .chain(Chain::mainnet())
197 .unwrap()
198 .with_api_key(key)
199 .build()
200 .unwrap();
201
202 let mut fail = |e: &str| {
203 eprintln!("key {i} ({key}) failed: {e}");
204 failed.push(key);
205 };
206
207 let abi = match client.contract_abi(address).await {
208 Ok(abi) => abi,
209 Err(e) => {
210 fail(&e.to_string());
211 continue;
212 }
213 };
214
215 if let Some(first_abi) = &first_abi {
216 if abi != *first_abi {
217 fail("abi mismatch");
218 }
219 } else {
220 first_abi = Some(abi);
221 }
222 }
223 if !failed.is_empty() {
224 panic!("failed keys: {failed:#?}");
225 }
226 }
227
228 #[tokio::test]
229 #[ignore = "run manually"]
230 async fn test_etherscan_keys_compatibility() {
231 let address = address!("0x111111125421cA6dc452d289314280a0f8842A65");
232 let etherscan_key = "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46";
233 let client = foundry_block_explorers::Client::builder()
234 .with_api_key(etherscan_key)
235 .chain(Chain::optimism_mainnet())
236 .unwrap()
237 .build()
238 .unwrap();
239 if client.contract_abi(address).await.is_ok() {
240 panic!("v1 Optimism key should not work with v2 version")
241 }
242
243 let client = foundry_block_explorers::Client::builder()
244 .with_api_key(etherscan_key)
245 .with_api_version(EtherscanApiVersion::V1)
246 .chain(Chain::optimism_mainnet())
247 .unwrap()
248 .build()
249 .unwrap();
250 match client.contract_abi(address).await {
251 Ok(_) => {}
252 Err(_) => panic!("v1 Optimism key should work with v1 version"),
253 };
254 }
255}