1use alloy_primitives::B256;
4use axum::{Json, Router, http::StatusCode, response::IntoResponse, routing::post};
5use foundry_config::{
6 NamedChain::{
7 self, Arbitrum, Base, BinanceSmartChainTestnet, Celo, Gnosis, Hyperliquid, Mainnet,
8 Optimism, Polygon, Robinhood, Sepolia,
9 },
10 RpcEndpointUrl, RpcEndpoints,
11};
12use rand::seq::SliceRandom;
13use serde_json::{Value, json};
14use std::{
15 env,
16 sync::{
17 Arc, LazyLock,
18 atomic::{AtomicBool, AtomicUsize, Ordering},
19 },
20};
21
22macro_rules! shuffled_list {
23 ($name:ident, $e:expr $(,)?) => {
24 static $name: LazyLock<ShuffledList<&'static str>> =
25 LazyLock::new(|| ShuffledList::new($e));
26 };
27}
28
29struct ShuffledList<T> {
30 list: Vec<T>,
31 index: AtomicUsize,
32}
33
34impl<T> ShuffledList<T> {
35 fn new(mut list: Vec<T>) -> Self {
36 assert!(!list.is_empty());
37 list.shuffle(&mut rand::rng());
38 Self { list, index: AtomicUsize::new(0) }
39 }
40
41 fn next(&self) -> &T {
42 let index = self.index.fetch_add(1, Ordering::Relaxed);
43 &self.list[index % self.list.len()]
44 }
45}
46
47shuffled_list!(
48 HTTP_ARCHIVE_DOMAINS,
49 vec![
50 "ethereum.reth.rs/rpc",
52 ],
53);
54shuffled_list!(
55 HTTP_DOMAINS,
56 vec![
57 "ethereum.reth.rs/rpc",
59 ],
60);
61shuffled_list!(
62 WS_ARCHIVE_DOMAINS,
63 vec![
64 "ethereum.reth.rs/ws",
66 ],
67);
68shuffled_list!(
69 WS_DOMAINS,
70 vec![
71 "ethereum.reth.rs/ws",
73 ],
74);
75
76shuffled_list!(
83 ARBITRUM_URLS,
84 vec![
85 "https://arb-pokt.nodies.app",
87 "https://arbitrum.gateway.tenderly.co",
88 ],
89);
90
91shuffled_list!(
93 DRPC_KEYS,
94 vec![
95 "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
96 "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
97 ],
98);
99
100shuffled_list!(
102 ETHERSCAN_KEYS,
103 vec![
104 "MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6",
105 "JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1",
106 "ZSMDY6BI2H55MBE3G9CUUQT4XYUDBB6ZSK",
107 "4FYHTY429IXYMJNS4TITKDMUKW5QRYDX61",
108 "QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I",
109 "VXMQ117UN58Y4RHWUB8K1UGCEA7UQEWK55",
110 "C7I2G4JTA5EPYS42Z8IZFEIMQNI5GXIJEV",
111 "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74",
112 "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG",
113 ],
114);
115
116pub fn rpc_endpoints() -> RpcEndpoints {
118 RpcEndpoints::new([
119 ("mainnet", RpcEndpointUrl::Url(next_http_archive_rpc_url())),
120 ("mainnet2", RpcEndpointUrl::Url(next_http_archive_rpc_url())),
121 ("sepolia", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Sepolia))),
122 ("optimism", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Optimism))),
123 ("base", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Base))),
124 ("arbitrum", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Arbitrum))),
125 ("polygon", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Polygon))),
126 ("bsc", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::BinanceSmartChain))),
127 ("avaxTestnet", RpcEndpointUrl::Url("https://api.avax-test.network/ext/bc/C/rpc".into())),
128 ("moonbeam", RpcEndpointUrl::Url("https://moonbeam.api.onfinality.io/public".into())),
129 ("polkadotTestnet", RpcEndpointUrl::Url("https://eth-rpc-testnet.polkadot.io".into())),
130 ("kusama", RpcEndpointUrl::Url("https://eth-rpc-kusama.polkadot.io".into())),
131 ("polkadot", RpcEndpointUrl::Url("https://eth-rpc.polkadot.io".into())),
132 ("rpcEnvAlias", RpcEndpointUrl::Env("${RPC_ENV_ALIAS}".into())),
133 ])
134}
135
136pub fn next_http_rpc_endpoint() -> String {
140 next_rpc_endpoint(NamedChain::Mainnet)
141}
142
143pub fn next_ws_rpc_endpoint() -> String {
147 next_ws_endpoint(NamedChain::Mainnet)
148}
149
150pub fn next_rpc_endpoint(chain: NamedChain) -> String {
152 next_url(false, chain)
153}
154
155pub fn next_tempo_mainnet_rpc_endpoint() -> String {
160 let url =
161 env_rpc_url("TEMPO_MAINNET_RPC_URL").unwrap_or_else(|| "https://rpc.tempo.xyz".to_string());
162 test_debug!("next_tempo_mainnet_rpc_endpoint() = {}", debug_url(&url));
163 url
164}
165
166pub fn next_tempo_testnet_rpc_endpoint() -> String {
170 let url = env_rpc_url("TEMPO_TESTNET_RPC_URL")
171 .unwrap_or_else(|| "https://rpc.moderato.tempo.xyz".to_string());
172 test_debug!("next_tempo_testnet_rpc_endpoint() = {}", debug_url(&url));
173 url
174}
175
176pub fn next_ws_endpoint(chain: NamedChain) -> String {
178 next_url(true, chain)
179}
180
181pub fn next_http_archive_rpc_url() -> String {
183 next_archive_url(false)
184}
185
186pub fn next_ws_archive_rpc_url() -> String {
188 next_archive_url(true)
189}
190
191fn next_archive_url(is_ws: bool) -> String {
193 let domain = if is_ws { &WS_ARCHIVE_DOMAINS } else { &HTTP_ARCHIVE_DOMAINS }.next();
194 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
195 test_debug!("next_archive_url(is_ws={is_ws}) = {}", debug_url(&url));
196 url
197}
198
199pub fn next_etherscan_api_key() -> String {
201 let mut key = env::var("ETHERSCAN_KEY").unwrap_or_default();
202 if key.is_empty() {
203 key = ETHERSCAN_KEYS.next().to_string();
204 }
205 test_debug!("next_etherscan_api_key() = {}...", &key[..6]);
206 key
207}
208
209fn next_url(is_ws: bool, chain: NamedChain) -> String {
210 let url = next_url_inner(is_ws, chain);
211 test_debug!("next_url(is_ws={is_ws}, chain={chain:?}) = {}", debug_url(&url));
212 url
213}
214
215fn next_url_inner(is_ws: bool, chain: NamedChain) -> String {
216 if matches!(chain, Base) {
217 return "https://mainnet.base.org".to_string();
218 }
219
220 if matches!(chain, Optimism) {
221 return "https://mainnet.optimism.io".to_string();
222 }
223
224 if matches!(chain, BinanceSmartChainTestnet) {
225 return "https://bsc-testnet.bnbchain.org".to_string();
226 }
227
228 if matches!(chain, Celo) {
229 return env_rpc_url("CELO_RPC").unwrap_or_else(|| "https://forno.celo.org".to_string());
232 }
233
234 if matches!(chain, Gnosis) {
235 return env_rpc_url("GNOSIS_RPC")
236 .unwrap_or_else(|| "https://rpc.gnosischain.com".to_string());
237 }
238
239 if matches!(chain, Hyperliquid) {
240 return env_rpc_url("HYPERLIQUID_RPC")
241 .unwrap_or_else(|| "https://rpc.hyperliquid.xyz/evm".to_string());
242 }
243
244 if matches!(chain, Robinhood) {
245 return env_rpc_url("ROBINHOOD_RPC")
246 .unwrap_or_else(|| "https://rpc.mainnet.chain.robinhood.com".to_string());
247 }
248
249 if matches!(chain, Sepolia) {
250 if let Some(rpc_url) = env_rpc_url("ETH_SEPOLIA_RPC") {
251 return rpc_url;
252 }
253 return "https://ethereum-sepolia-rpc.publicnode.com".to_string();
254 }
255
256 if matches!(chain, Arbitrum) {
257 return env_rpc_url("ARBITRUM_RPC").unwrap_or_else(|| (*ARBITRUM_URLS.next()).to_string());
258 }
259
260 let reth_works = true;
261 let domain = if reth_works && matches!(chain, Mainnet) {
262 *(if is_ws { &WS_DOMAINS } else { &HTTP_DOMAINS }).next()
263 } else {
264 let key = DRPC_KEYS.next();
266 let network = match chain {
267 Mainnet => "ethereum",
268 Polygon => "polygon",
269 Sepolia => "sepolia",
270 _ => "",
271 };
272 &format!("lb.drpc.org/ogrpc?network={network}&dkey={key}")
273 };
274
275 if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") }
276}
277
278fn env_rpc_url(var: &str) -> Option<String> {
280 env::var(var).ok().filter(|url| !url.is_empty())
281}
282
283fn debug_url(url: &str) -> impl std::fmt::Display + '_ {
285 let url = reqwest::Url::parse(url).unwrap();
286 format!(
287 "{scheme}://{host}{path}",
288 scheme = url.scheme(),
289 host = url.host_str().unwrap(),
290 path = url.path().get(..8).unwrap_or(url.path()),
291 )
292}
293
294const MONAD_SYSTEM_ADDRESS: &str = "0x6f49a8f621353f12378d0046e7d7e4b9b249dc9e";
295
296pub async fn spawn_canonical_monad_system_rpc(endpoint: String, target_hash: B256) -> String {
298 let target_hash = target_hash.to_string();
299 let client = reqwest::Client::new();
300 let router = Router::new().route(
301 "/",
302 post(move |Json(request): Json<Value>| {
303 let client = client.clone();
304 let endpoint = endpoint.clone();
305 let target_hash = target_hash.clone();
306 async move {
307 let mut response = client
308 .post(endpoint)
309 .json(&request)
310 .send()
311 .await
312 .unwrap()
313 .json::<Value>()
314 .await
315 .unwrap();
316
317 canonicalize_monad_system_response(&request, &mut response, &target_hash);
318
319 Json(response)
320 }
321 }),
322 );
323 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
324 let address = listener.local_addr().unwrap();
325 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
326 format!("http://{address}")
327}
328
329pub async fn spawn_rpc_proxy_rejecting_method_after(
334 endpoint: String,
335 method: &'static str,
336 successful_calls: usize,
337) -> String {
338 spawn_rpc_proxy_rejecting_method(
339 endpoint,
340 method,
341 RpcMethodRejection::After(successful_calls),
342 StatusCode::FORBIDDEN,
343 -32004,
344 "method is not allowed",
345 )
346 .await
347}
348
349pub async fn spawn_rpc_proxy_rejecting_method_when_enabled(
351 endpoint: String,
352 method: &'static str,
353) -> (String, Arc<AtomicBool>) {
354 let enabled = Arc::new(AtomicBool::new(false));
355 let proxy = spawn_rpc_proxy_rejecting_method(
356 endpoint,
357 method,
358 RpcMethodRejection::Enabled(enabled.clone()),
359 StatusCode::FORBIDDEN,
360 -32004,
361 "method is not allowed",
362 )
363 .await;
364 (proxy, enabled)
365}
366
367pub async fn spawn_rpc_proxy_method_not_found_before(
370 endpoint: String,
371 method: &'static str,
372 unavailable_calls: usize,
373) -> String {
374 spawn_rpc_proxy_rejecting_method(
375 endpoint,
376 method,
377 RpcMethodRejection::Before(unavailable_calls),
378 StatusCode::OK,
379 -32601,
380 "method not found",
381 )
382 .await
383}
384
385pub async fn spawn_rpc_proxy_internal_error_after(
388 endpoint: String,
389 method: &'static str,
390 successful_calls: usize,
391) -> String {
392 spawn_rpc_proxy_rejecting_method(
393 endpoint,
394 method,
395 RpcMethodRejection::After(successful_calls),
396 StatusCode::OK,
397 -32603,
398 "internal error",
399 )
400 .await
401}
402
403pub async fn spawn_rpc_proxy_canned_method(
408 endpoint: String,
409 method: &'static str,
410 result: Value,
411) -> (String, Arc<AtomicUsize>) {
412 let client = reqwest::Client::new();
413 let calls = Arc::new(AtomicUsize::new(0));
414 let proxy_calls = calls.clone();
415 let router = Router::new().route(
416 "/",
417 post(move |Json(request): Json<Value>| {
418 let client = client.clone();
419 let endpoint = endpoint.clone();
420 let calls = proxy_calls.clone();
421 let result = result.clone();
422 async move {
423 if request.get("method").and_then(Value::as_str) == Some(method) {
424 calls.fetch_add(1, Ordering::Relaxed);
425 let id = request.get("id").cloned().unwrap_or(Value::Null);
426 return Json(json!({
427 "jsonrpc": "2.0",
428 "id": id,
429 "result": result,
430 }));
431 }
432
433 let response = client
434 .post(endpoint)
435 .json(&request)
436 .send()
437 .await
438 .unwrap()
439 .json::<Value>()
440 .await
441 .unwrap();
442 Json(response)
443 }
444 }),
445 );
446 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
447 let address = listener.local_addr().unwrap();
448 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
449 (format!("http://{address}"), calls)
450}
451
452pub async fn spawn_rpc_proxy_retyping_first_block_transaction(
458 endpoint: String,
459 tx_type: &'static str,
460) -> String {
461 let client = reqwest::Client::new();
462 let router = Router::new().route(
463 "/",
464 post(move |Json(request): Json<Value>| {
465 let client = client.clone();
466 let endpoint = endpoint.clone();
467 async move {
468 let mut response = client
469 .post(endpoint)
470 .json(&request)
471 .send()
472 .await
473 .unwrap()
474 .json::<Value>()
475 .await
476 .unwrap();
477 let responses = match response.as_array_mut() {
478 Some(batch) => batch.iter_mut().collect::<Vec<_>>(),
479 None => vec![&mut response],
480 };
481 for response in responses {
482 if let Some(transactions) = response
483 .get_mut("result")
484 .and_then(|result| result.get_mut("transactions"))
485 .and_then(Value::as_array_mut)
486 && let Some(first) = transactions.first_mut().and_then(Value::as_object_mut)
487 {
488 first.insert("type".to_string(), Value::from(tx_type));
489 }
490 }
491 Json(response).into_response()
492 }
493 }),
494 );
495 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
496 let address = listener.local_addr().unwrap();
497 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
498 format!("http://{address}")
499}
500
501pub async fn spawn_rpc_proxy_mapping_method(
504 endpoint: String,
505 method: &'static str,
506 map: impl Fn(&Value, Value) -> Value + Send + Sync + 'static,
507) -> String {
508 let client = reqwest::Client::new();
509 let map = Arc::new(map);
510 let router = Router::new().route(
511 "/",
512 post(move |Json(request): Json<Value>| {
513 let client = client.clone();
514 let endpoint = endpoint.clone();
515 let map = map.clone();
516 async move {
517 let mut response = client
518 .post(endpoint)
519 .json(&request)
520 .send()
521 .await
522 .unwrap()
523 .json::<Value>()
524 .await
525 .unwrap();
526 if request.get("method").and_then(Value::as_str) == Some(method)
527 && let Some(result) = response.get_mut("result")
528 {
529 let params = request.get("params").cloned().unwrap_or(Value::Null);
530 *result = map(¶ms, result.take());
531 }
532 Json(response)
533 }
534 }),
535 );
536 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
537 let address = listener.local_addr().unwrap();
538 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
539 format!("http://{address}")
540}
541
542#[derive(Clone)]
543enum RpcMethodRejection {
544 Before(usize),
545 After(usize),
546 Enabled(Arc<AtomicBool>),
547}
548
549impl RpcMethodRejection {
550 fn rejects(&self, call: usize) -> bool {
551 match self {
552 Self::Before(rejected_calls) => call < *rejected_calls,
553 Self::After(successful_calls) => call >= *successful_calls,
554 Self::Enabled(enabled) => enabled.load(Ordering::SeqCst),
555 }
556 }
557}
558
559async fn spawn_rpc_proxy_rejecting_method(
560 endpoint: String,
561 method: &'static str,
562 rejection: RpcMethodRejection,
563 rejection_status: StatusCode,
564 error_code: i64,
565 error_message: &'static str,
566) -> String {
567 let client = reqwest::Client::new();
568 let calls = std::sync::Arc::new(AtomicUsize::new(0));
569 let router = Router::new().route(
570 "/",
571 post(move |Json(request): Json<Value>| {
572 let client = client.clone();
573 let endpoint = endpoint.clone();
574 let calls = calls.clone();
575 let rejection = rejection.clone();
576 async move {
577 if request.get("method").and_then(Value::as_str) == Some(method)
578 && rejection.rejects(calls.fetch_add(1, Ordering::Relaxed))
579 {
580 let id = request.get("id").cloned().unwrap_or(Value::Null);
581 return (
582 rejection_status,
583 Json(json!({
584 "jsonrpc": "2.0",
585 "id": id,
586 "error": {
587 "code": error_code,
588 "message": error_message,
589 },
590 })),
591 )
592 .into_response();
593 }
594
595 let response = client
596 .post(endpoint)
597 .json(&request)
598 .send()
599 .await
600 .unwrap()
601 .json::<Value>()
602 .await
603 .unwrap();
604 Json(response).into_response()
605 }
606 }),
607 );
608 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
609 let address = listener.local_addr().unwrap();
610 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
611 format!("http://{address}")
612}
613
614fn canonicalize_monad_system_response(request: &Value, response: &mut Value, target_hash: &str) {
615 if let Some(requests) = request.as_array() {
616 let Some(responses) = response.as_array_mut() else { return };
617 for response in responses {
618 let Some(response_id) = response.get("id") else { continue };
619 if let Some(request) =
620 requests.iter().find(|request| request.get("id") == Some(response_id))
621 {
622 canonicalize_monad_system_result(request, response, target_hash);
623 }
624 }
625 } else {
626 canonicalize_monad_system_result(request, response, target_hash);
627 }
628}
629
630fn canonicalize_monad_system_result(request: &Value, response: &mut Value, target_hash: &str) {
631 let Some(method) = request.get("method").and_then(Value::as_str) else { return };
632 let Some(result) = response.get_mut("result") else { return };
633
634 match method {
635 "eth_getTransactionByHash"
636 | "eth_getTransactionByBlockHashAndIndex"
637 | "eth_getTransactionByBlockNumberAndIndex" => {
638 canonicalize_monad_system_transaction(result, target_hash);
639 }
640 "eth_getBlockByHash" | "eth_getBlockByNumber" => {
641 if let Some(transactions) = result.get_mut("transactions").and_then(Value::as_array_mut)
642 {
643 for transaction in transactions {
644 canonicalize_monad_system_transaction(transaction, target_hash);
645 }
646 }
647 }
648 "eth_getTransactionReceipt" => {
649 canonicalize_monad_system_receipt(result, target_hash);
650 }
651 "eth_getBlockReceipts" => {
652 if let Some(receipts) = result.as_array_mut() {
653 for receipt in receipts {
654 canonicalize_monad_system_receipt(receipt, target_hash);
655 }
656 }
657 }
658 _ => {}
659 }
660}
661
662fn canonicalize_monad_system_transaction(transaction: &mut Value, target_hash: &str) {
663 let Some(transaction) = transaction.as_object_mut() else { return };
664 if !transaction
665 .get("hash")
666 .and_then(Value::as_str)
667 .is_some_and(|hash| hash.eq_ignore_ascii_case(target_hash))
668 {
669 return;
670 }
671
672 let tx_type = transaction.get("type").and_then(parse_rpc_quantity).unwrap_or_default();
673 let legacy_v = (tx_type != 0)
674 .then(|| {
675 let parity = transaction
676 .get("yParity")
677 .or_else(|| transaction.get("v"))
678 .and_then(parse_rpc_quantity)
679 .filter(|parity| *parity <= 1)?;
680 let v = if let Some(chain_id) = transaction.get("chainId").and_then(parse_rpc_quantity)
681 {
682 chain_id.checked_mul(2)?.checked_add(35 + parity)?
683 } else {
684 27 + parity
685 };
686 Some(format!("0x{v:x}"))
687 })
688 .flatten();
689
690 transaction.insert("from".to_string(), json!(MONAD_SYSTEM_ADDRESS));
691 transaction.insert("gas".to_string(), json!("0x0"));
692 transaction.insert("gasPrice".to_string(), json!("0x0"));
693 transaction.insert("type".to_string(), json!("0x0"));
694 if let Some(v) = legacy_v {
695 transaction.insert("v".to_string(), json!(v));
696 }
697 for field in [
698 "accessList",
699 "authorizationList",
700 "blobVersionedHashes",
701 "maxFeePerBlobGas",
702 "maxFeePerGas",
703 "maxPriorityFeePerGas",
704 "yParity",
705 ] {
706 transaction.remove(field);
707 }
708}
709
710fn parse_rpc_quantity(value: &Value) -> Option<u64> {
711 value.as_u64().or_else(|| {
712 value.as_str()?.strip_prefix("0x").and_then(|value| u64::from_str_radix(value, 16).ok())
713 })
714}
715
716fn canonicalize_monad_system_receipt(receipt: &mut Value, target_hash: &str) {
717 let Some(receipt) = receipt.as_object_mut() else { return };
718 if !receipt
719 .get("transactionHash")
720 .and_then(Value::as_str)
721 .is_some_and(|hash| hash.eq_ignore_ascii_case(target_hash))
722 {
723 return;
724 }
725
726 receipt.insert("cumulativeGasUsed".to_string(), json!("0x0"));
727 receipt.insert("effectiveGasPrice".to_string(), json!("0x0"));
728 receipt.insert("gasUsed".to_string(), json!("0x0"));
729 receipt.insert("type".to_string(), json!("0x0"));
730 receipt.remove("blobGasPrice");
731 receipt.remove("blobGasUsed");
732}
733
734#[cfg(test)]
735#[expect(clippy::disallowed_macros)]
736mod tests {
737 use super::*;
738 use alloy_primitives::address;
739 use foundry_config::Chain;
740
741 #[test]
742 fn canonical_monad_system_response_supports_batches() {
743 let target_hash = B256::with_last_byte(1).to_string();
744 let requests = json!([
745 {
746 "jsonrpc": "2.0",
747 "id": 1,
748 "method": "eth_getTransactionByHash",
749 "params": [target_hash],
750 },
751 {
752 "jsonrpc": "2.0",
753 "id": 2,
754 "method": "eth_getTransactionReceipt",
755 "params": [target_hash],
756 },
757 ]);
758 let mut responses = json!([
759 {
760 "jsonrpc": "2.0",
761 "id": 2,
762 "result": {
763 "transactionHash": target_hash,
764 "gasUsed": "0x5208",
765 },
766 },
767 {
768 "jsonrpc": "2.0",
769 "id": 1,
770 "result": {
771 "hash": target_hash,
772 "chainId": "0x7a69",
773 "gas": "0x5208",
774 "gasPrice": "0x1",
775 "r": "0x1",
776 "s": "0x1",
777 "type": "0x2",
778 "v": "0x1",
779 "yParity": "0x1",
780 },
781 },
782 ]);
783
784 canonicalize_monad_system_response(&requests, &mut responses, &target_hash);
785
786 assert_eq!(responses[0]["result"]["gasUsed"], "0x0");
787 assert_eq!(responses[1]["result"]["gas"], "0x0");
788 assert_eq!(responses[1]["result"]["from"], MONAD_SYSTEM_ADDRESS);
789 assert_eq!(responses[1]["result"]["type"], "0x0");
790 assert_eq!(responses[1]["result"]["r"], "0x1");
791 assert_eq!(responses[1]["result"]["s"], "0x1");
792 assert_eq!(responses[1]["result"]["v"], "0xf4f6");
793 assert!(responses[1]["result"].get("yParity").is_none());
794 }
795
796 #[test]
797 fn canonical_monad_system_response_ignores_malformed_requests() {
798 let request = json!({"jsonrpc": "2.0", "id": 1});
799 let mut response = json!({"jsonrpc": "2.0", "id": 1, "result": "unchanged"});
800
801 canonicalize_monad_system_response(&request, &mut response, &B256::ZERO.to_string());
802
803 assert_eq!(response["result"], "unchanged");
804 }
805
806 #[tokio::test]
807 #[ignore = "run manually"]
808 async fn test_etherscan_keys() {
809 let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
810 let mut first_abi = None;
811 let mut failed = Vec::new();
812 for (i, &key) in ETHERSCAN_KEYS.list.iter().enumerate() {
813 println!("trying key {i} ({key})");
814
815 let client = foundry_block_explorers::Client::builder()
816 .chain(Chain::mainnet())
817 .unwrap()
818 .with_api_key(key)
819 .build()
820 .unwrap();
821
822 let mut fail = |e: &str| {
823 eprintln!("key {i} ({key}) failed: {e}");
824 failed.push(key);
825 };
826
827 let abi = match client.contract_abi(address).await {
828 Ok(abi) => abi,
829 Err(e) => {
830 fail(&e.to_string());
831 continue;
832 }
833 };
834
835 if let Some(first_abi) = &first_abi {
836 if abi != *first_abi {
837 fail("abi mismatch");
838 }
839 } else {
840 first_abi = Some(abi);
841 }
842 }
843 assert!(failed.is_empty(), "failed keys: {failed:#?}")
844 }
845}