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
13// List of public archive reth nodes to use
14static 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
20// List of public reth nodes to use (archive and non archive)
21static 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
27// List of general purpose DRPC keys to rotate through
28static 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
39// List of etherscan keys for mainnet
40static 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        // Optimism
53        // "JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46",
54    ];
55    keys.shuffle(&mut rand::thread_rng());
56    keys
57});
58
59// List of etherscan keys for Optimism.
60static ETHERSCAN_OPTIMISM_KEYS: LazyLock<Vec<&'static str>> =
61    LazyLock::new(|| vec!["JQNGFHINKS1W7Y5FRXU4SPBYF43J3NYK46"]);
62
63/// Returns the next index to use.
64fn next_idx() -> usize {
65    static NEXT_INDEX: AtomicUsize = AtomicUsize::new(0);
66    NEXT_INDEX.fetch_add(1, Ordering::SeqCst)
67}
68
69/// Returns the next item in the list to use.
70fn next<T>(list: &[T]) -> &T {
71    &list[next_idx() % list.len()]
72}
73
74/// Returns the next _mainnet_ rpc URL in inline
75///
76/// This will rotate all available rpc endpoints
77pub fn next_http_rpc_endpoint() -> String {
78    next_rpc_endpoint(NamedChain::Mainnet)
79}
80
81/// Returns the next _mainnet_ rpc URL in inline
82///
83/// This will rotate all available rpc endpoints
84pub fn next_ws_rpc_endpoint() -> String {
85    next_ws_endpoint(NamedChain::Mainnet)
86}
87
88/// Returns the next HTTP RPC URL.
89pub fn next_rpc_endpoint(chain: NamedChain) -> String {
90    next_url(false, chain)
91}
92
93/// Returns the next WS RPC URL.
94pub fn next_ws_endpoint(chain: NamedChain) -> String {
95    next_url(true, chain)
96}
97
98/// Returns a websocket URL that has access to archive state
99pub fn next_http_archive_rpc_url() -> String {
100    next_archive_url(false)
101}
102
103/// Returns an HTTP URL that has access to archive state
104pub fn next_ws_archive_rpc_url() -> String {
105    next_archive_url(true)
106}
107
108/// Returns a URL that has access to archive state.
109fn 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
141/// Returns the next etherscan api key
142pub fn next_mainnet_etherscan_api_key() -> String {
143    next_etherscan_api_key(NamedChain::Mainnet)
144}
145
146/// Returns the next etherscan api key for given chain.
147pub fn next_etherscan_api_key(chain: NamedChain) -> String {
148    let keys = match chain {
149        Optimism => &ETHERSCAN_OPTIMISM_KEYS,
150        _ => &ETHERSCAN_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        // For Mainnet pick one of Reth nodes.
168        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        // DRPC for other networks used in tests.
177        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}