1use alloy_primitives::B256;
4use axum::{
5 Json, Router,
6 body::Bytes,
7 http::{HeaderMap, StatusCode},
8 response::IntoResponse,
9 routing::post,
10};
11use foundry_config::{
12 NamedChain::{
13 self, Arbitrum, Base, BinanceSmartChainTestnet, Celo, Gnosis, Hyperliquid, Mainnet,
14 Optimism, Polygon, Robinhood, Sepolia,
15 },
16 RpcEndpointUrl, RpcEndpoints,
17};
18use rand::seq::SliceRandom;
19use serde_json::{Value, json};
20use std::{
21 env,
22 sync::{
23 Arc, LazyLock, Mutex,
24 atomic::{AtomicBool, AtomicUsize, Ordering},
25 },
26};
27use tokio::sync::Notify;
28
29macro_rules! shuffled_list {
30 ($name:ident, $e:expr $(,)?) => {
31 static $name: LazyLock<ShuffledList<&'static str>> =
32 LazyLock::new(|| ShuffledList::new($e));
33 };
34}
35
36struct ShuffledList<T> {
37 list: Vec<T>,
38 index: AtomicUsize,
39}
40
41impl<T> ShuffledList<T> {
42 fn new(mut list: Vec<T>) -> Self {
43 assert!(!list.is_empty());
44 list.shuffle(&mut rand::rng());
45 Self { list, index: AtomicUsize::new(0) }
46 }
47
48 fn next(&self) -> &T {
49 let index = self.index.fetch_add(1, Ordering::Relaxed);
50 &self.list[index % self.list.len()]
51 }
52}
53
54shuffled_list!(
55 HTTP_ARCHIVE_DOMAINS,
56 vec![
57 "ethereum.reth.rs/rpc",
59 ],
60);
61shuffled_list!(
62 HTTP_DOMAINS,
63 vec![
64 "ethereum.reth.rs/rpc",
66 ],
67);
68shuffled_list!(
69 WS_ARCHIVE_DOMAINS,
70 vec![
71 "ethereum.reth.rs/ws",
73 ],
74);
75shuffled_list!(
76 WS_DOMAINS,
77 vec![
78 "ethereum.reth.rs/ws",
80 ],
81);
82
83shuffled_list!(
90 ARBITRUM_URLS,
91 vec![
92 "https://arb-pokt.nodies.app",
94 "https://arbitrum.gateway.tenderly.co",
95 ],
96);
97
98shuffled_list!(
100 DRPC_KEYS,
101 vec![
102 "Agc9NK9-6UzYh-vQDDM80Tv0A5UnBkUR8I3qssvAG40d",
103 "AjUPUPonSEInt2CZ_7A-ai3hMyxxBlsR8I4EssvAG40d",
104 ],
105);
106
107shuffled_list!(
109 ETHERSCAN_KEYS,
110 vec![
111 "MCAUM7WPE9XP5UQMZPCKIBUJHPM1C24FP6",
112 "JW6RWCG2C5QF8TANH4KC7AYIF1CX7RB5D1",
113 "ZSMDY6BI2H55MBE3G9CUUQT4XYUDBB6ZSK",
114 "4FYHTY429IXYMJNS4TITKDMUKW5QRYDX61",
115 "QYKNT5RHASZ7PGQE68FNQWH99IXVTVVD2I",
116 "VXMQ117UN58Y4RHWUB8K1UGCEA7UQEWK55",
117 "C7I2G4JTA5EPYS42Z8IZFEIMQNI5GXIJEV",
118 "A15KZUMZXXCK1P25Y1VP1WGIVBBHIZDS74",
119 "3IA6ASNQXN8WKN7PNFX7T72S9YG56X9FPG",
120 ],
121);
122
123pub fn rpc_endpoints() -> RpcEndpoints {
125 RpcEndpoints::new([
126 ("mainnet", RpcEndpointUrl::Url(next_http_archive_rpc_url())),
127 ("mainnet2", RpcEndpointUrl::Url(next_http_archive_rpc_url())),
128 ("sepolia", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Sepolia))),
129 ("optimism", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Optimism))),
130 ("base", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Base))),
131 ("arbitrum", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Arbitrum))),
132 ("polygon", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::Polygon))),
133 ("bsc", RpcEndpointUrl::Url(next_rpc_endpoint(NamedChain::BinanceSmartChain))),
134 ("avaxTestnet", RpcEndpointUrl::Url("https://api.avax-test.network/ext/bc/C/rpc".into())),
135 ("moonbeam", RpcEndpointUrl::Url("https://moonbeam.api.onfinality.io/public".into())),
136 ("polkadotTestnet", RpcEndpointUrl::Url("https://eth-rpc-testnet.polkadot.io".into())),
137 ("kusama", RpcEndpointUrl::Url("https://eth-rpc-kusama.polkadot.io".into())),
138 ("polkadot", RpcEndpointUrl::Url("https://eth-rpc.polkadot.io".into())),
139 ("rpcEnvAlias", RpcEndpointUrl::Env("${RPC_ENV_ALIAS}".into())),
140 ])
141}
142
143pub fn next_http_rpc_endpoint() -> String {
147 next_rpc_endpoint(NamedChain::Mainnet)
148}
149
150pub fn next_ws_rpc_endpoint() -> String {
154 next_ws_endpoint(NamedChain::Mainnet)
155}
156
157pub fn next_rpc_endpoint(chain: NamedChain) -> String {
159 next_url(false, chain)
160}
161
162pub fn next_tempo_mainnet_rpc_endpoint() -> String {
167 let url =
168 env_rpc_url("TEMPO_MAINNET_RPC_URL").unwrap_or_else(|| "https://rpc.tempo.xyz".to_string());
169 test_debug!("next_tempo_mainnet_rpc_endpoint() = {}", debug_url(&url));
170 url
171}
172
173pub fn next_tempo_testnet_rpc_endpoint() -> String {
177 let url = env_rpc_url("TEMPO_TESTNET_RPC_URL")
178 .unwrap_or_else(|| "https://rpc.moderato.tempo.xyz".to_string());
179 test_debug!("next_tempo_testnet_rpc_endpoint() = {}", debug_url(&url));
180 url
181}
182
183pub fn next_ws_endpoint(chain: NamedChain) -> String {
185 next_url(true, chain)
186}
187
188pub fn next_http_archive_rpc_url() -> String {
190 next_archive_url(false)
191}
192
193pub fn next_ws_archive_rpc_url() -> String {
195 next_archive_url(true)
196}
197
198fn next_archive_url(is_ws: bool) -> String {
200 let domain = if is_ws { &WS_ARCHIVE_DOMAINS } else { &HTTP_ARCHIVE_DOMAINS }.next();
201 let url = if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") };
202 test_debug!("next_archive_url(is_ws={is_ws}) = {}", debug_url(&url));
203 url
204}
205
206pub fn next_etherscan_api_key() -> String {
208 let mut key = env::var("ETHERSCAN_KEY").unwrap_or_default();
209 if key.is_empty() {
210 key = ETHERSCAN_KEYS.next().to_string();
211 }
212 test_debug!("next_etherscan_api_key() = {}...", &key[..6]);
213 key
214}
215
216fn next_url(is_ws: bool, chain: NamedChain) -> String {
217 let url = next_url_inner(is_ws, chain);
218 test_debug!("next_url(is_ws={is_ws}, chain={chain:?}) = {}", debug_url(&url));
219 url
220}
221
222fn next_url_inner(is_ws: bool, chain: NamedChain) -> String {
223 if matches!(chain, Base) {
224 return "https://mainnet.base.org".to_string();
225 }
226
227 if matches!(chain, Optimism) {
228 return "https://mainnet.optimism.io".to_string();
229 }
230
231 if matches!(chain, BinanceSmartChainTestnet) {
232 return "https://bsc-testnet.bnbchain.org".to_string();
233 }
234
235 if matches!(chain, Celo) {
236 return env_rpc_url("CELO_RPC").unwrap_or_else(|| "https://forno.celo.org".to_string());
239 }
240
241 if matches!(chain, Gnosis) {
242 return env_rpc_url("GNOSIS_RPC")
243 .unwrap_or_else(|| "https://rpc.gnosischain.com".to_string());
244 }
245
246 if matches!(chain, Hyperliquid) {
247 return env_rpc_url("HYPERLIQUID_RPC")
248 .unwrap_or_else(|| "https://rpc.hyperliquid.xyz/evm".to_string());
249 }
250
251 if matches!(chain, Robinhood) {
252 return env_rpc_url("ROBINHOOD_RPC")
253 .unwrap_or_else(|| "https://rpc.mainnet.chain.robinhood.com".to_string());
254 }
255
256 if matches!(chain, Sepolia) {
257 if let Some(rpc_url) = env_rpc_url("ETH_SEPOLIA_RPC") {
258 return rpc_url;
259 }
260 return "https://ethereum-sepolia-rpc.publicnode.com".to_string();
261 }
262
263 if matches!(chain, Arbitrum) {
264 return env_rpc_url("ARBITRUM_RPC").unwrap_or_else(|| (*ARBITRUM_URLS.next()).to_string());
265 }
266
267 let reth_works = true;
268 let domain = if reth_works && matches!(chain, Mainnet) {
269 *(if is_ws { &WS_DOMAINS } else { &HTTP_DOMAINS }).next()
270 } else {
271 let key = DRPC_KEYS.next();
273 let network = match chain {
274 Mainnet => "ethereum",
275 Polygon => "polygon",
276 Sepolia => "sepolia",
277 _ => "",
278 };
279 &format!("lb.drpc.org/ogrpc?network={network}&dkey={key}")
280 };
281
282 if is_ws { format!("wss://{domain}") } else { format!("https://{domain}") }
283}
284
285fn env_rpc_url(var: &str) -> Option<String> {
287 env::var(var).ok().filter(|url| !url.is_empty())
288}
289
290fn debug_url(url: &str) -> impl std::fmt::Display + '_ {
292 let url = reqwest::Url::parse(url).unwrap();
293 format!(
294 "{scheme}://{host}{path}",
295 scheme = url.scheme(),
296 host = url.host_str().unwrap(),
297 path = url.path().get(..8).unwrap_or(url.path()),
298 )
299}
300
301const MONAD_SYSTEM_ADDRESS: &str = "0x6f49a8f621353f12378d0046e7d7e4b9b249dc9e";
302
303pub async fn spawn_canonical_monad_system_rpc(endpoint: String, target_hash: B256) -> String {
305 let target_hash = target_hash.to_string();
306 let client = reqwest::Client::new();
307 let router = Router::new().route(
308 "/",
309 post(move |Json(request): Json<Value>| {
310 let client = client.clone();
311 let endpoint = endpoint.clone();
312 let target_hash = target_hash.clone();
313 async move {
314 let mut response = client
315 .post(endpoint)
316 .json(&request)
317 .send()
318 .await
319 .unwrap()
320 .json::<Value>()
321 .await
322 .unwrap();
323
324 canonicalize_monad_system_response(&request, &mut response, &target_hash);
325
326 Json(response)
327 }
328 }),
329 );
330 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
331 let address = listener.local_addr().unwrap();
332 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
333 format!("http://{address}")
334}
335
336pub async fn spawn_rpc_proxy_rejecting_method_after(
341 endpoint: String,
342 method: &'static str,
343 successful_calls: usize,
344) -> String {
345 spawn_rpc_proxy_rejecting_method(
346 endpoint,
347 method,
348 RpcMethodRejection::After(successful_calls),
349 StatusCode::FORBIDDEN,
350 -32004,
351 "method is not allowed",
352 )
353 .await
354}
355
356pub async fn spawn_rpc_proxy_rejecting_method_when_enabled(
358 endpoint: String,
359 method: &'static str,
360) -> (String, Arc<AtomicBool>) {
361 let enabled = Arc::new(AtomicBool::new(false));
362 let proxy = spawn_rpc_proxy_rejecting_method(
363 endpoint,
364 method,
365 RpcMethodRejection::Enabled(enabled.clone()),
366 StatusCode::FORBIDDEN,
367 -32004,
368 "method is not allowed",
369 )
370 .await;
371 (proxy, enabled)
372}
373
374pub async fn spawn_rpc_proxy_rejecting_method_after_when_enabled(
377 endpoint: String,
378 method: &'static str,
379 successful_calls: usize,
380) -> (String, Arc<AtomicBool>) {
381 let enabled = Arc::new(AtomicBool::new(true));
382 let proxy = spawn_rpc_proxy_rejecting_method(
383 endpoint,
384 method,
385 RpcMethodRejection::AfterWhenEnabled(successful_calls, enabled.clone()),
386 StatusCode::FORBIDDEN,
387 -32004,
388 "method is not allowed",
389 )
390 .await;
391 (proxy, enabled)
392}
393
394pub async fn spawn_rpc_proxy_method_not_found_before(
397 endpoint: String,
398 method: &'static str,
399 unavailable_calls: usize,
400) -> String {
401 spawn_rpc_proxy_rejecting_method(
402 endpoint,
403 method,
404 RpcMethodRejection::Before(unavailable_calls),
405 StatusCode::OK,
406 -32601,
407 "method not found",
408 )
409 .await
410}
411
412pub async fn spawn_rpc_proxy_internal_error_after(
415 endpoint: String,
416 method: &'static str,
417 successful_calls: usize,
418) -> String {
419 spawn_rpc_proxy_rejecting_method(
420 endpoint,
421 method,
422 RpcMethodRejection::After(successful_calls),
423 StatusCode::OK,
424 -32603,
425 "internal error",
426 )
427 .await
428}
429
430pub async fn spawn_rpc_proxy_canned_method(
435 endpoint: String,
436 method: &'static str,
437 result: Value,
438) -> (String, Arc<AtomicUsize>) {
439 let client = reqwest::Client::new();
440 let calls = Arc::new(AtomicUsize::new(0));
441 let proxy_calls = calls.clone();
442 let router = Router::new().route(
443 "/",
444 post(move |Json(request): Json<Value>| {
445 let client = client.clone();
446 let endpoint = endpoint.clone();
447 let calls = proxy_calls.clone();
448 let result = result.clone();
449 async move {
450 if request.get("method").and_then(Value::as_str) == Some(method) {
451 calls.fetch_add(1, Ordering::Relaxed);
452 let id = request.get("id").cloned().unwrap_or(Value::Null);
453 return Json(json!({
454 "jsonrpc": "2.0",
455 "id": id,
456 "result": result,
457 }));
458 }
459
460 let response = client
461 .post(endpoint)
462 .json(&request)
463 .send()
464 .await
465 .unwrap()
466 .json::<Value>()
467 .await
468 .unwrap();
469 Json(response)
470 }
471 }),
472 );
473 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
474 let address = listener.local_addr().unwrap();
475 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
476 (format!("http://{address}"), calls)
477}
478
479pub async fn spawn_rpc_proxy_retyping_first_block_transaction(
485 endpoint: String,
486 tx_type: &'static str,
487) -> String {
488 let client = reqwest::Client::new();
489 let router = Router::new().route(
490 "/",
491 post(move |Json(request): Json<Value>| {
492 let client = client.clone();
493 let endpoint = endpoint.clone();
494 async move {
495 let mut response = client
496 .post(endpoint)
497 .json(&request)
498 .send()
499 .await
500 .unwrap()
501 .json::<Value>()
502 .await
503 .unwrap();
504 let responses = match response.as_array_mut() {
505 Some(batch) => batch.iter_mut().collect::<Vec<_>>(),
506 None => vec![&mut response],
507 };
508 for response in responses {
509 if let Some(transactions) = response
510 .get_mut("result")
511 .and_then(|result| result.get_mut("transactions"))
512 .and_then(Value::as_array_mut)
513 && let Some(first) = transactions.first_mut().and_then(Value::as_object_mut)
514 {
515 first.insert("type".to_string(), Value::from(tx_type));
516 }
517 }
518 Json(response).into_response()
519 }
520 }),
521 );
522 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
523 let address = listener.local_addr().unwrap();
524 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
525 format!("http://{address}")
526}
527
528pub async fn spawn_rpc_proxy_mapping_method(
531 endpoint: String,
532 method: &'static str,
533 map: impl Fn(&Value, Value) -> Value + Send + Sync + 'static,
534) -> String {
535 let client = reqwest::Client::new();
536 let map = Arc::new(map);
537 let router = Router::new().route(
538 "/",
539 post(move |Json(request): Json<Value>| {
540 let client = client.clone();
541 let endpoint = endpoint.clone();
542 let map = map.clone();
543 async move {
544 let mut response = client
545 .post(endpoint)
546 .json(&request)
547 .send()
548 .await
549 .unwrap()
550 .json::<Value>()
551 .await
552 .unwrap();
553 if request.get("method").and_then(Value::as_str) == Some(method)
554 && let Some(result) = response.get_mut("result")
555 {
556 let params = request.get("params").cloned().unwrap_or(Value::Null);
557 *result = map(¶ms, result.take());
558 }
559 Json(response)
560 }
561 }),
562 );
563 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
564 let address = listener.local_addr().unwrap();
565 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
566 format!("http://{address}")
567}
568
569pub async fn spawn_rpc_proxy_recording_method(
571 endpoint: String,
572 method: &'static str,
573) -> (String, Arc<Mutex<Vec<Value>>>) {
574 let client = reqwest::Client::new();
575 let requests = Arc::new(Mutex::new(Vec::new()));
576 let router = Router::new().route(
577 "/",
578 post({
579 let requests = requests.clone();
580 move |Json(request): Json<Value>| {
581 let client = client.clone();
582 let endpoint = endpoint.clone();
583 let requests = requests.clone();
584 async move {
585 if request.get("method").and_then(Value::as_str) == Some(method) {
586 requests
587 .lock()
588 .unwrap()
589 .push(request.get("params").cloned().unwrap_or(Value::Null));
590 }
591 let response = client
592 .post(endpoint)
593 .json(&request)
594 .send()
595 .await
596 .unwrap()
597 .json::<Value>()
598 .await
599 .unwrap();
600 Json(response)
601 }
602 }
603 }),
604 );
605 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
606 let address = listener.local_addr().unwrap();
607 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
608 (format!("http://{address}"), requests)
609}
610
611pub async fn spawn_rpc_proxy_requiring_header(
614 endpoint: String,
615 header_name: &'static str,
616 header_value: &'static str,
617) -> String {
618 let client = reqwest::Client::new();
619 let router = Router::new().route(
620 "/",
621 post(move |headers: HeaderMap, Json(request): Json<Value>| {
622 let client = client.clone();
623 let endpoint = endpoint.clone();
624 async move {
625 if headers.get(header_name).and_then(|value| value.to_str().ok())
626 != Some(header_value)
627 {
628 let id = request.get("id").cloned().unwrap_or(Value::Null);
629 return (
630 StatusCode::UNAUTHORIZED,
631 Json(json!({
632 "jsonrpc": "2.0",
633 "id": id,
634 "error": {
635 "code": -32000,
636 "message": "unauthorized",
637 },
638 })),
639 )
640 .into_response();
641 }
642
643 let response = client
644 .post(endpoint)
645 .json(&request)
646 .send()
647 .await
648 .unwrap()
649 .json::<Value>()
650 .await
651 .unwrap();
652 Json(response).into_response()
653 }
654 }),
655 );
656 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
657 let address = listener.local_addr().unwrap();
658 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
659 format!("http://{address}")
660}
661
662pub async fn spawn_rpc_proxy_blocking_first_submission(
668 endpoint: String,
669 method: &'static str,
670 forward: bool,
671) -> (String, Arc<Mutex<Vec<Value>>>, Arc<Notify>, Arc<Notify>) {
672 let client = reqwest::Client::new();
673 let submissions = Arc::new(Mutex::new(Vec::new()));
674 let reached = Arc::new(Notify::new());
675 let release = Arc::new(Notify::new());
676 let router = Router::new().fallback({
677 let submissions = submissions.clone();
678 let reached = reached.clone();
679 let release = release.clone();
680 move |body: Bytes| {
681 let client = client.clone();
682 let endpoint = endpoint.clone();
683 let submissions = submissions.clone();
684 let reached = reached.clone();
685 let release = release.clone();
686 async move {
687 let request: Value = serde_json::from_slice(&body).unwrap();
688 let first = request.get("method").and_then(Value::as_str) == Some(method) && {
689 let mut submissions = submissions.lock().unwrap();
690 submissions.push(request.get("params").cloned().unwrap_or(Value::Null));
691 submissions.len() == 1
692 };
693 let response = if first && !forward {
694 Bytes::new()
695 } else {
696 client
697 .post(endpoint)
698 .header("content-type", "application/json")
699 .body(body)
700 .send()
701 .await
702 .unwrap()
703 .bytes()
704 .await
705 .unwrap()
706 };
707 if first {
708 reached.notify_one();
709 release.notified().await;
710 return StatusCode::SERVICE_UNAVAILABLE.into_response();
711 }
712 response.into_response()
713 }
714 }
715 });
716 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
717 let address = listener.local_addr().unwrap();
718 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
719 (format!("http://{address}"), submissions, reached, release)
720}
721
722#[derive(Clone)]
723enum RpcMethodRejection {
724 Before(usize),
725 After(usize),
726 Enabled(Arc<AtomicBool>),
727 AfterWhenEnabled(usize, Arc<AtomicBool>),
728}
729
730impl RpcMethodRejection {
731 fn rejects(&self, call: usize) -> bool {
732 match self {
733 Self::Before(rejected_calls) => call < *rejected_calls,
734 Self::After(successful_calls) => call >= *successful_calls,
735 Self::Enabled(enabled) => enabled.load(Ordering::SeqCst),
736 Self::AfterWhenEnabled(successful_calls, enabled) => {
737 call >= *successful_calls && enabled.load(Ordering::SeqCst)
738 }
739 }
740 }
741}
742
743async fn spawn_rpc_proxy_rejecting_method(
744 endpoint: String,
745 method: &'static str,
746 rejection: RpcMethodRejection,
747 rejection_status: StatusCode,
748 error_code: i64,
749 error_message: &'static str,
750) -> String {
751 let client = reqwest::Client::new();
752 let calls = std::sync::Arc::new(AtomicUsize::new(0));
753 let router = Router::new().route(
754 "/",
755 post(move |Json(request): Json<Value>| {
756 let client = client.clone();
757 let endpoint = endpoint.clone();
758 let calls = calls.clone();
759 let rejection = rejection.clone();
760 async move {
761 if request.get("method").and_then(Value::as_str) == Some(method)
762 && rejection.rejects(calls.fetch_add(1, Ordering::Relaxed))
763 {
764 let id = request.get("id").cloned().unwrap_or(Value::Null);
765 return (
766 rejection_status,
767 Json(json!({
768 "jsonrpc": "2.0",
769 "id": id,
770 "error": {
771 "code": error_code,
772 "message": error_message,
773 },
774 })),
775 )
776 .into_response();
777 }
778
779 let response = client
780 .post(endpoint)
781 .json(&request)
782 .send()
783 .await
784 .unwrap()
785 .json::<Value>()
786 .await
787 .unwrap();
788 Json(response).into_response()
789 }
790 }),
791 );
792 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
793 let address = listener.local_addr().unwrap();
794 tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
795 format!("http://{address}")
796}
797
798fn canonicalize_monad_system_response(request: &Value, response: &mut Value, target_hash: &str) {
799 if let Some(requests) = request.as_array() {
800 let Some(responses) = response.as_array_mut() else { return };
801 for response in responses {
802 let Some(response_id) = response.get("id") else { continue };
803 if let Some(request) =
804 requests.iter().find(|request| request.get("id") == Some(response_id))
805 {
806 canonicalize_monad_system_result(request, response, target_hash);
807 }
808 }
809 } else {
810 canonicalize_monad_system_result(request, response, target_hash);
811 }
812}
813
814fn canonicalize_monad_system_result(request: &Value, response: &mut Value, target_hash: &str) {
815 let Some(method) = request.get("method").and_then(Value::as_str) else { return };
816 let Some(result) = response.get_mut("result") else { return };
817
818 match method {
819 "eth_getTransactionByHash"
820 | "eth_getTransactionByBlockHashAndIndex"
821 | "eth_getTransactionByBlockNumberAndIndex" => {
822 canonicalize_monad_system_transaction(result, target_hash);
823 }
824 "eth_getBlockByHash" | "eth_getBlockByNumber" => {
825 if let Some(transactions) = result.get_mut("transactions").and_then(Value::as_array_mut)
826 {
827 for transaction in transactions {
828 canonicalize_monad_system_transaction(transaction, target_hash);
829 }
830 }
831 }
832 "eth_getTransactionReceipt" => {
833 canonicalize_monad_system_receipt(result, target_hash);
834 }
835 "eth_getBlockReceipts" => {
836 if let Some(receipts) = result.as_array_mut() {
837 for receipt in receipts {
838 canonicalize_monad_system_receipt(receipt, target_hash);
839 }
840 }
841 }
842 _ => {}
843 }
844}
845
846fn canonicalize_monad_system_transaction(transaction: &mut Value, target_hash: &str) {
847 let Some(transaction) = transaction.as_object_mut() else { return };
848 if !transaction
849 .get("hash")
850 .and_then(Value::as_str)
851 .is_some_and(|hash| hash.eq_ignore_ascii_case(target_hash))
852 {
853 return;
854 }
855
856 let tx_type = transaction.get("type").and_then(parse_rpc_quantity).unwrap_or_default();
857 let legacy_v = (tx_type != 0)
858 .then(|| {
859 let parity = transaction
860 .get("yParity")
861 .or_else(|| transaction.get("v"))
862 .and_then(parse_rpc_quantity)
863 .filter(|parity| *parity <= 1)?;
864 let v = if let Some(chain_id) = transaction.get("chainId").and_then(parse_rpc_quantity)
865 {
866 chain_id.checked_mul(2)?.checked_add(35 + parity)?
867 } else {
868 27 + parity
869 };
870 Some(format!("0x{v:x}"))
871 })
872 .flatten();
873
874 transaction.insert("from".to_string(), json!(MONAD_SYSTEM_ADDRESS));
875 transaction.insert("gas".to_string(), json!("0x0"));
876 transaction.insert("gasPrice".to_string(), json!("0x0"));
877 transaction.insert("type".to_string(), json!("0x0"));
878 if let Some(v) = legacy_v {
879 transaction.insert("v".to_string(), json!(v));
880 }
881 for field in [
882 "accessList",
883 "authorizationList",
884 "blobVersionedHashes",
885 "maxFeePerBlobGas",
886 "maxFeePerGas",
887 "maxPriorityFeePerGas",
888 "yParity",
889 ] {
890 transaction.remove(field);
891 }
892}
893
894fn parse_rpc_quantity(value: &Value) -> Option<u64> {
895 value.as_u64().or_else(|| {
896 value.as_str()?.strip_prefix("0x").and_then(|value| u64::from_str_radix(value, 16).ok())
897 })
898}
899
900fn canonicalize_monad_system_receipt(receipt: &mut Value, target_hash: &str) {
901 let Some(receipt) = receipt.as_object_mut() else { return };
902 if !receipt
903 .get("transactionHash")
904 .and_then(Value::as_str)
905 .is_some_and(|hash| hash.eq_ignore_ascii_case(target_hash))
906 {
907 return;
908 }
909
910 receipt.insert("cumulativeGasUsed".to_string(), json!("0x0"));
911 receipt.insert("effectiveGasPrice".to_string(), json!("0x0"));
912 receipt.insert("gasUsed".to_string(), json!("0x0"));
913 receipt.insert("type".to_string(), json!("0x0"));
914 receipt.remove("blobGasPrice");
915 receipt.remove("blobGasUsed");
916}
917
918#[cfg(test)]
919#[expect(clippy::disallowed_macros)]
920mod tests {
921 use super::*;
922 use alloy_primitives::address;
923 use foundry_config::Chain;
924
925 #[test]
926 fn canonical_monad_system_response_supports_batches() {
927 let target_hash = B256::with_last_byte(1).to_string();
928 let requests = json!([
929 {
930 "jsonrpc": "2.0",
931 "id": 1,
932 "method": "eth_getTransactionByHash",
933 "params": [target_hash],
934 },
935 {
936 "jsonrpc": "2.0",
937 "id": 2,
938 "method": "eth_getTransactionReceipt",
939 "params": [target_hash],
940 },
941 ]);
942 let mut responses = json!([
943 {
944 "jsonrpc": "2.0",
945 "id": 2,
946 "result": {
947 "transactionHash": target_hash,
948 "gasUsed": "0x5208",
949 },
950 },
951 {
952 "jsonrpc": "2.0",
953 "id": 1,
954 "result": {
955 "hash": target_hash,
956 "chainId": "0x7a69",
957 "gas": "0x5208",
958 "gasPrice": "0x1",
959 "r": "0x1",
960 "s": "0x1",
961 "type": "0x2",
962 "v": "0x1",
963 "yParity": "0x1",
964 },
965 },
966 ]);
967
968 canonicalize_monad_system_response(&requests, &mut responses, &target_hash);
969
970 assert_eq!(responses[0]["result"]["gasUsed"], "0x0");
971 assert_eq!(responses[1]["result"]["gas"], "0x0");
972 assert_eq!(responses[1]["result"]["from"], MONAD_SYSTEM_ADDRESS);
973 assert_eq!(responses[1]["result"]["type"], "0x0");
974 assert_eq!(responses[1]["result"]["r"], "0x1");
975 assert_eq!(responses[1]["result"]["s"], "0x1");
976 assert_eq!(responses[1]["result"]["v"], "0xf4f6");
977 assert!(responses[1]["result"].get("yParity").is_none());
978 }
979
980 #[test]
981 fn canonical_monad_system_response_ignores_malformed_requests() {
982 let request = json!({"jsonrpc": "2.0", "id": 1});
983 let mut response = json!({"jsonrpc": "2.0", "id": 1, "result": "unchanged"});
984
985 canonicalize_monad_system_response(&request, &mut response, &B256::ZERO.to_string());
986
987 assert_eq!(response["result"], "unchanged");
988 }
989
990 #[tokio::test]
991 #[ignore = "run manually"]
992 async fn test_etherscan_keys() {
993 let address = address!("0xdAC17F958D2ee523a2206206994597C13D831ec7");
994 let mut first_abi = None;
995 let mut failed = Vec::new();
996 for (i, &key) in ETHERSCAN_KEYS.list.iter().enumerate() {
997 println!("trying key {i} ({key})");
998
999 let client = foundry_block_explorers::Client::builder()
1000 .chain(Chain::mainnet())
1001 .unwrap()
1002 .with_api_key(key)
1003 .build()
1004 .unwrap();
1005
1006 let mut fail = |e: &str| {
1007 eprintln!("key {i} ({key}) failed: {e}");
1008 failed.push(key);
1009 };
1010
1011 let abi = match client.contract_abi(address).await {
1012 Ok(abi) => abi,
1013 Err(e) => {
1014 fail(&e.to_string());
1015 continue;
1016 }
1017 };
1018
1019 if let Some(first_abi) = &first_abi {
1020 if abi != *first_abi {
1021 fail("abi mismatch");
1022 }
1023 } else {
1024 first_abi = Some(abi);
1025 }
1026 }
1027 assert!(failed.is_empty(), "failed keys: {failed:#?}")
1028 }
1029}