foundry_test_utils/
rpc.rs

1//! RPC API keys utilities.
2
3use 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        //
30        "reth-ethereum.ithaca.xyz/rpc",
31    ],
32);
33shuffled_list!(
34    HTTP_DOMAINS,
35    vec![
36        //
37        "reth-ethereum.ithaca.xyz/rpc",
38        "reth-ethereum-full.ithaca.xyz/rpc",
39    ],
40);
41shuffled_list!(
42    WS_ARCHIVE_DOMAINS,
43    vec![
44        //
45        "reth-ethereum.ithaca.xyz/ws",
46    ],
47);
48shuffled_list!(
49    WS_DOMAINS,
50    vec![
51        //
52        "reth-ethereum.ithaca.xyz/ws",
53        "reth-ethereum-full.ithaca.xyz/ws",
54    ],
55);
56
57// List of general purpose DRPC keys to rotate through
58shuffled_list!(
59    DRPC_KEYS,
60    vec![
61        "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
62        "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
63    ],
64);
65
66// List of etherscan keys.
67shuffled_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
82/// Returns the next index to use.
83fn next_idx() -> usize {
84    static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
85    NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
86}
87
88/// Returns the next item in the list to use.
89fn next<T>(list: &[T]) -> &T {
90    &list[next_idx() % list.len()]
91}
92
93/// Returns the next _mainnet_ rpc URL in inline
94///
95/// This will rotate all available rpc endpoints
96pub fn next_http_rpc_endpoint() -> String {
97    next_rpc_endpoint(NamedChain::Mainnet)
98}
99
100/// Returns the next _mainnet_ rpc URL in inline
101///
102/// This will rotate all available rpc endpoints
103pub fn next_ws_rpc_endpoint() -> String {
104    next_ws_endpoint(NamedChain::Mainnet)
105}
106
107/// Returns the next HTTP RPC URL.
108pub fn next_rpc_endpoint(chain: NamedChain) -> String {
109    next_url(false, chain)
110}
111
112/// Returns the next WS RPC URL.
113pub fn next_ws_endpoint(chain: NamedChain) -> String {
114    next_url(true, chain)
115}
116
117/// Returns a websocket URL that has access to archive state
118pub fn next_http_archive_rpc_url() -> String {
119    next_archive_url(false)
120}
121
122/// Returns an HTTP URL that has access to archive state
123pub fn next_ws_archive_rpc_url() -> String {
124    next_archive_url(true)
125}
126
127/// Returns a URL that has access to archive state.
128fn 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
135/// Returns the next etherscan api key.
136pub fn next_etherscan_api_key() -> String {
137    let key = next(&ETHERSCAN_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        // DRPC for other networks used in tests.
164        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}