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    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
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    ])
57});
58
59/// Returns the next index to use.
60fn next_idx() -> usize {
61    static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
62    NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
63}
64
65/// Returns the next item in the list to use.
66fn next<T>(list: &[T]) -> &T {
67    &list[next_idx() % list.len()]
68}
69
70/// Returns the next _mainnet_ rpc URL in inline
71///
72/// This will rotate all available rpc endpoints
73pub fn next_http_rpc_endpoint() -> String {
74    next_rpc_endpoint(NamedChain::Mainnet)
75}
76
77/// Returns the next _mainnet_ rpc URL in inline
78///
79/// This will rotate all available rpc endpoints
80pub fn next_ws_rpc_endpoint() -> String {
81    next_ws_endpoint(NamedChain::Mainnet)
82}
83
84/// Returns the next HTTP RPC URL.
85pub fn next_rpc_endpoint(chain: NamedChain) -> String {
86    next_url(false, chain)
87}
88
89/// Returns the next WS RPC URL.
90pub fn next_ws_endpoint(chain: NamedChain) -> String {
91    next_url(true, chain)
92}
93
94/// Returns a websocket URL that has access to archive state
95pub fn next_http_archive_rpc_url() -> String {
96    next_archive_url(false)
97}
98
99/// Returns an HTTP URL that has access to archive state
100pub fn next_ws_archive_rpc_url() -> String {
101    next_archive_url(true)
102}
103
104/// Returns a URL that has access to archive state.
105fn 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
133/// Returns the next etherscan api key.
134pub fn next_etherscan_api_key() -> String {
135    let key = next(&ETHERSCAN_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        // For Mainnet pick one of Reth nodes.
155        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        // DRPC for other networks used in tests.
160        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}