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 atomic::{AtomicUsize, Ordering},
10 LazyLock,
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 "ZUB97R31KSYX7NYVW6224Q6EYY6U56H591",
57 ])
58});
59
60fn next_idx() -> usize {
62 static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
63 NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
64}
65
66fn next<T>(list: &[T]) -> &T {
68 &list[next_idx() % list.len()]
69}
70
71pub fn next_http_rpc_endpoint() -> String {
75 next_rpc_endpoint(NamedChain::Mainnet)
76}
77
78pub fn next_ws_rpc_endpoint() -> String {
82 next_ws_endpoint(NamedChain::Mainnet)
83}
84
85pub fn next_rpc_endpoint(chain: NamedChain) -> String {
87 next_url(false, chain)
88}
89
90pub fn next_ws_endpoint(chain: NamedChain) -> String {
92 next_url(true, chain)
93}
94
95pub fn next_http_archive_rpc_url() -> String {
97 next_archive_url(false)
98}
99
100pub fn next_ws_archive_rpc_url() -> String {
102 next_archive_url(true)
103}
104
105fn next_archive_url(is_ws: bool) -> String {
107 let urls = archive_urls(is_ws);
108 let url = next(urls);
109 eprintln!("--- next_archive_url(is_ws={is_ws}) = {url} ---");
110 url.clone()
111}
112
113fn archive_urls(is_ws: bool) -> &'static [String] {
114 static WS: LazyLock<Vec<String>> = LazyLock::new(|| get(true));
115 static HTTP: LazyLock<Vec<String>> = LazyLock::new(|| get(false));
116
117 fn get(is_ws: bool) -> Vec<String> {
118 let mut urls = vec![];
119
120 for &host in RETH_ARCHIVE_HOSTS.iter() {
121 if is_ws {
122 urls.push(format!("wss://{host}/ws"));
123 } else {
124 urls.push(format!("https://{host}/rpc"));
125 }
126 }
127
128 urls
129 }
130
131 if is_ws {
132 &WS
133 } else {
134 &HTTP
135 }
136}
137
138pub fn next_etherscan_api_key() -> String {
140 let key = next(ÐERSCAN_KEYS).to_string();
141 eprintln!("--- next_etherscan_api_key() = {key} ---");
142 key
143}
144
145fn next_url(is_ws: bool, chain: NamedChain) -> String {
146 if matches!(chain, Base) {
147 return "https://mainnet.base.org".to_string();
148 }
149
150 if matches!(chain, Optimism) {
151 return "https://mainnet.optimism.io".to_string();
152 }
153
154 if matches!(chain, BinanceSmartChainTestnet) {
155 return "https://bsc-testnet-rpc.publicnode.com".to_string();
156 }
157
158 let domain = if matches!(chain, Mainnet) {
159 let idx = next_idx() % RETH_HOSTS.len();
161 let host = RETH_HOSTS[idx];
162 if is_ws {
163 format!("{host}/ws")
164 } else {
165 format!("{host}/rpc")
166 }
167 } else {
168 let idx = next_idx() % DRPC_KEYS.len();
170 let key = DRPC_KEYS[idx];
171
172 let network = match chain {
173 Arbitrum => "arbitrum",
174 Polygon => "polygon",
175 Sepolia => "sepolia",
176 _ => "",
177 };
178 format!("lb.drpc.org/ogrpc?network={network}&dkey={key}")
179 };
180
181 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
182
183 eprintln!("--- next_url(is_ws={is_ws}, chain={chain:?}) = {url} ---");
184 url
185}
186
187#[cfg(test)]
188#[expect(clippy::disallowed_macros)]
189mod tests {
190 use super::*;
191 use alloy_primitives::address;
192 use foundry_block_explorers::EtherscanApiVersion;
193 use foundry_config::Chain;
194
195 #[tokio::test]
196 #[ignore = "run manually"]
197 async fn test_etherscan_keys() {
198 let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
199 let mut first_abi = None;
200 let mut failed = Vec::new();
201 for (i, &key) in ETHERSCAN_KEYS.iter().enumerate() {
202 println!("trying key {i} ({key})");
203
204 let client = foundry_block_explorers::Client::builder()
205 .chain(Chain::mainnet())
206 .unwrap()
207 .with_api_key(key)
208 .build()
209 .unwrap();
210
211 let mut fail = |e: &str| {
212 eprintln!("key {i} ({key}) failed: {e}");
213 failed.push(key);
214 };
215
216 let abi = match client.contract_abi(address).await {
217 Ok(abi) => abi,
218 Err(e) => {
219 fail(&e.to_string());
220 continue;
221 }
222 };
223
224 if let Some(first_abi) = &first_abi {
225 if abi != *first_abi {
226 fail("abi mismatch");
227 }
228 } else {
229 first_abi = Some(abi);
230 }
231 }
232 if !failed.is_empty() {
233 panic!("failed keys: {failed:#?}");
234 }
235 }
236
237 #[tokio::test]
238 #[ignore = "run manually"]
239 async fn test_etherscan_keys_compatibility() {
240 let address = address!("0x111111125421cA6dc452d289314280a0f8842A65");
241 let ehterscan_key = "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46";
242 let client = foundry_block_explorers::Client::builder()
243 .with_api_key(ehterscan_key)
244 .chain(Chain::optimism_mainnet())
245 .unwrap()
246 .build()
247 .unwrap();
248 if client.contract_abi(address).await.is_ok() {
249 panic!("v1 Optimism key should not work with v2 version")
250 }
251
252 let client = foundry_block_explorers::Client::builder()
253 .with_api_key(ehterscan_key)
254 .with_api_version(EtherscanApiVersion::V1)
255 .chain(Chain::optimism_mainnet())
256 .unwrap()
257 .build()
258 .unwrap();
259 match client.contract_abi(address).await {
260 Ok(_) => {}
261 Err(_) => panic!("v1 Optimism key should work with v1 version"),
262 };
263 }
264}