foundry_test_utils/
rpc.rs

1//! RPC API keys utilities.
2
3use 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
18// List of public archive reth nodes to use
19static RETH_ARCHIVE_HOSTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
20    shuffled(vec![
21        //
22        "reth-ethereum.ithaca.xyz",
23    ])
24});
25
26// List of public reth nodes to use (archive and non archive)
27static RETH_HOSTS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
28    shuffled(vec![
29        //
30        "reth-ethereum.ithaca.xyz",
31        "reth-ethereum-full.ithaca.xyz",
32    ])
33});
34
35// List of general purpose DRPC keys to rotate through
36static DRPC_KEYS: LazyLock<Vec<&'static str>> = LazyLock::new(|| {
37    shuffled(vec![
38        //
39        "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
40        "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
41    ])
42});
43
44// List of etherscan keys.
45static 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
60/// Returns the next index to use.
61fn next_idx() -> usize {
62    static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
63    NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
64}
65
66/// Returns the next item in the list to use.
67fn next<T>(list: &[T]) -> &T {
68    &list[next_idx() % list.len()]
69}
70
71/// Returns the next _mainnet_ rpc URL in inline
72///
73/// This will rotate all available rpc endpoints
74pub fn next_http_rpc_endpoint() -> String {
75    next_rpc_endpoint(NamedChain::Mainnet)
76}
77
78/// Returns the next _mainnet_ rpc URL in inline
79///
80/// This will rotate all available rpc endpoints
81pub fn next_ws_rpc_endpoint() -> String {
82    next_ws_endpoint(NamedChain::Mainnet)
83}
84
85/// Returns the next HTTP RPC URL.
86pub fn next_rpc_endpoint(chain: NamedChain) -> String {
87    next_url(false, chain)
88}
89
90/// Returns the next WS RPC URL.
91pub fn next_ws_endpoint(chain: NamedChain) -> String {
92    next_url(true, chain)
93}
94
95/// Returns a websocket URL that has access to archive state
96pub fn next_http_archive_rpc_url() -> String {
97    next_archive_url(false)
98}
99
100/// Returns an HTTP URL that has access to archive state
101pub fn next_ws_archive_rpc_url() -> String {
102    next_archive_url(true)
103}
104
105/// Returns a URL that has access to archive state.
106fn 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
138/// Returns the next etherscan api key.
139pub fn next_etherscan_api_key() -> String {
140    let key = next(&ETHERSCAN_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        // For Mainnet pick one of Reth nodes.
160        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        // DRPC for other networks used in tests.
169        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}