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
13static RETH_ARCHIVE_HOSTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
15 let mut hosts = vec!["reth-ethereum.ithaca.xyz"];
16 hosts.shuffle(&mut rand::thread_rng());
17 hosts
18});
19
20static RETH_HOSTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
22 let mut hosts = vec!["reth-ethereum.ithaca.xyz", "reth-ethereum-full.ithaca.xyz"];
23 hosts.shuffle(&mut rand::thread_rng());
24 hosts
25});
26
27static DRPC_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
29 let mut keys = vec![
30 "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
31 "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
32 ];
33
34 keys.shuffle(&mut rand::thread_rng());
35
36 keys
37});
38
39static ETHERSCAN_MAINNET_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
41 let mut keys = vec![
42 "MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6",
43 "JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1",
44 "ZSMDY6BI2H55MBE3G9CUUQT4XYUDBB6ZSK",
45 "4FYHTY429IXYMJNS4TITKDMUKW5QRYDX61",
46 "QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I",
47 "VXMQ117UN58Y4RHWUB8K1UGCEA7UQEWK55",
48 "C7I2G4JTA5EPYS42Z8IZFEIMQNI5GXIJEV",
49 "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74",
50 "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG",
51 "ZUB97R31KSYX7NYVW6224Q6EYY6U56H591",
52 ];
55 keys.shuffle(&mut rand::thread_rng());
56 keys
57});
58
59static ETHERSCAN_OPTIMISM_KEYS: LazyLock<Vec<&'static str>> =
61 LazyLock::new(|| vec!["JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46"]);
62
63fn next_idx() -> usize {
65 static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
66 NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
67}
68
69fn next<T>(list: &[T]) -> &T {
71 &list[next_idx() % list.len()]
72}
73
74pub fn next_http_rpc_endpoint() -> String {
78 next_rpc_endpoint(NamedChain::Mainnet)
79}
80
81pub fn next_ws_rpc_endpoint() -> String {
85 next_ws_endpoint(NamedChain::Mainnet)
86}
87
88pub fn next_rpc_endpoint(chain: NamedChain) -> String {
90 next_url(false, chain)
91}
92
93pub fn next_ws_endpoint(chain: NamedChain) -> String {
95 next_url(true, chain)
96}
97
98pub fn next_http_archive_rpc_url() -> String {
100 next_archive_url(false)
101}
102
103pub fn next_ws_archive_rpc_url() -> String {
105 next_archive_url(true)
106}
107
108fn next_archive_url(is_ws: bool) -> String {
110 let urls = archive_urls(is_ws);
111 let url = next(urls);
112 eprintln!("--- next_archive_url(is_ws={is_ws}) = {url} ---");
113 url.clone()
114}
115
116fn archive_urls(is_ws: bool) -> &'static [String] {
117 static WS: LazyLock<Vec<String>> = LazyLock::new(|| get(true));
118 static HTTP: LazyLock<Vec<String>> = LazyLock::new(|| get(false));
119
120 fn get(is_ws: bool) -> Vec<String> {
121 let mut urls = vec![];
122
123 for &host in RETH_ARCHIVE_HOSTS.iter() {
124 if is_ws {
125 urls.push(format!("wss://{host}/ws"));
126 } else {
127 urls.push(format!("https://{host}/rpc"));
128 }
129 }
130
131 urls
132 }
133
134 if is_ws {
135 &WS
136 } else {
137 &HTTP
138 }
139}
140
141pub fn next_mainnet_etherscan_api_key() -> String {
143 next_etherscan_api_key(NamedChain::Mainnet)
144}
145
146pub fn next_etherscan_api_key(chain: NamedChain) -> String {
148 let keys = match chain {
149 Optimism => ÐERSCAN_OPTIMISM_KEYS,
150 _ => ÐERSCAN_MAINNET_KEYS,
151 };
152 let key = next(keys).to_string();
153 eprintln!("--- next_etherscan_api_key(chain={chain:?}) = {key} ---");
154 key
155}
156
157fn next_url(is_ws: bool, chain: NamedChain) -> String {
158 if matches!(chain, Base) {
159 return "https://mainnet.base.org".to_string();
160 }
161
162 if matches!(chain, BinanceSmartChainTestnet) {
163 return "https://bsc-testnet-rpc.publicnode.com".to_string();
164 }
165
166 let domain = if matches!(chain, Mainnet) {
167 let idx = next_idx() % RETH_HOSTS.len();
169 let host = RETH_HOSTS[idx];
170 if is_ws {
171 format!("{host}/ws")
172 } else {
173 format!("{host}/rpc")
174 }
175 } else {
176 let idx = next_idx() % DRPC_KEYS.len();
178 let key = DRPC_KEYS[idx];
179
180 let network = match chain {
181 Optimism => "optimism",
182 Arbitrum => "arbitrum",
183 Polygon => "polygon",
184 Sepolia => "sepolia",
185 _ => "",
186 };
187 format!("lb.drpc.org/ogrpc?network={network}&dkey={key}")
188 };
189
190 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
191
192 eprintln!("--- next_url(is_ws={is_ws}, chain={chain:?}) = {url} ---");
193 url
194}
195
196#[cfg(test)]
197#[expect(clippy::disallowed_macros)]
198mod tests {
199 use super::*;
200 use alloy_primitives::address;
201 use foundry_config::Chain;
202
203 #[tokio::test]
204 #[ignore = "run manually"]
205 async fn test_etherscan_keys() {
206 let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
207 let mut first_abi = None;
208 let mut failed = Vec::new();
209 for (i, &key) in ETHERSCAN_MAINNET_KEYS.iter().enumerate() {
210 println!("trying key {i} ({key})");
211
212 let client = foundry_block_explorers::Client::builder()
213 .chain(Chain::mainnet())
214 .unwrap()
215 .with_api_key(key)
216 .build()
217 .unwrap();
218
219 let mut fail = |e: &str| {
220 eprintln!("key {i} ({key}) failed: {e}");
221 failed.push(key);
222 };
223
224 let abi = match client.contract_abi(address).await {
225 Ok(abi) => abi,
226 Err(e) => {
227 fail(&e.to_string());
228 continue;
229 }
230 };
231
232 if let Some(first_abi) = &first_abi {
233 if abi != *first_abi {
234 fail("abi mismatch");
235 }
236 } else {
237 first_abi = Some(abi);
238 }
239 }
240 if !failed.is_empty() {
241 panic!("failed keys: {failed:#?}");
242 }
243 }
244}