reth_consensus_debug_client/providers/
etherscan.rs1use crate::PayloadProvider;
2use alloy_eips::BlockNumberOrTag;
3use alloy_json_rpc::{Response, ResponsePayload};
4use futures::future::BoxFuture;
5use reqwest::Client;
6use reth_node_api::ExecutionPayload;
7use serde::{de::DeserializeOwned, Serialize};
8use std::{sync::Arc, time::Duration};
9use tokio::{sync::mpsc, time::interval};
10use tracing::{debug, warn};
11
12#[derive(derive_more::Debug)]
14pub struct EtherscanBlockProvider<RpcBlock, ExecutionData> {
15 http_client: Client,
16 base_url: String,
17 api_key: String,
18 chain_id: u64,
19 interval: Duration,
20 #[debug(skip)]
21 convert: Arc<dyn Fn(RpcBlock) -> ExecutionData + Send + Sync>,
22}
23
24impl<RpcBlock, ExecutionData> EtherscanBlockProvider<RpcBlock, ExecutionData>
25where
26 RpcBlock: Serialize + DeserializeOwned,
27{
28 pub fn new(
30 base_url: String,
31 api_key: String,
32 chain_id: u64,
33 convert: impl Fn(RpcBlock) -> ExecutionData + Send + Sync + 'static,
34 ) -> Self {
35 Self {
36 http_client: Client::new(),
37 base_url,
38 api_key,
39 chain_id,
40 interval: Duration::from_secs(3),
41 convert: Arc::new(convert),
42 }
43 }
44
45 pub const fn with_interval(mut self, interval: Duration) -> Self {
47 self.interval = interval;
48 self
49 }
50
51 pub async fn load_payload(
55 &self,
56 block_number_or_tag: BlockNumberOrTag,
57 ) -> eyre::Result<ExecutionData> {
58 let resp = fetch_block(
59 &self.http_client,
60 &self.base_url,
61 &self.api_key,
62 self.chain_id,
63 block_number_or_tag,
64 )
65 .await?;
66
67 let resp: Response<RpcBlock> = serde_json::from_str(&resp).inspect_err(|err| {
68 warn!(target: "etherscan", "Failed to parse block response from etherscan: {}", err);
69 })?;
70
71 let payload = resp.payload;
72 match payload {
73 ResponsePayload::Success(block) => Ok((self.convert)(block)),
74 ResponsePayload::Failure(err) => Err(eyre::eyre!("Failed to get block: {err}")),
75 }
76 }
77}
78
79impl<RpcBlock, ExecutionData> Clone for EtherscanBlockProvider<RpcBlock, ExecutionData> {
82 fn clone(&self) -> Self {
83 Self {
84 http_client: self.http_client.clone(),
85 base_url: self.base_url.clone(),
86 api_key: self.api_key.clone(),
87 chain_id: self.chain_id,
88 interval: self.interval,
89 convert: Arc::clone(&self.convert),
90 }
91 }
92}
93
94impl<RpcBlock, ExecutionData> PayloadProvider for EtherscanBlockProvider<RpcBlock, ExecutionData>
95where
96 RpcBlock: Serialize + DeserializeOwned + 'static,
97 ExecutionData: ExecutionPayload,
98{
99 type ExecutionData = ExecutionData;
100
101 async fn subscribe_payloads(&self, tx: mpsc::Sender<Self::ExecutionData>) {
102 let mut last_block_number: Option<u64> = None;
103 let mut interval = interval(self.interval);
104 loop {
105 interval.tick().await;
106 let payload = match self.load_payload(BlockNumberOrTag::Latest).await {
107 Ok(payload) => payload,
108 Err(err) => {
109 warn!(
110 target: "consensus::debug-client",
111 %err,
112 "Failed to fetch a block from Etherscan",
113 );
114 continue
115 }
116 };
117 let block_number = payload.block_number();
118 if Some(block_number) == last_block_number {
119 continue;
120 }
121
122 if tx.send(payload).await.is_err() {
123 break;
125 }
126
127 last_block_number = Some(block_number);
128 }
129 }
130
131 async fn get_payload(&self, block_number: u64) -> eyre::Result<Self::ExecutionData> {
132 self.load_payload(BlockNumberOrTag::Number(block_number)).await
133 }
134}
135
136fn fetch_block<'a>(
140 http_client: &'a Client,
141 base_url: &'a str,
142 api_key: &'a str,
143 chain_id: u64,
144 block_number_or_tag: BlockNumberOrTag,
145) -> BoxFuture<'a, eyre::Result<String>> {
146 Box::pin(async move {
147 let tag = match block_number_or_tag {
148 BlockNumberOrTag::Number(num) => format!("{num:#x}"),
149 tag => tag.to_string(),
150 };
151
152 let mut req = http_client.get(base_url).query(&[
153 ("module", "proxy"),
154 ("action", "eth_getBlockByNumber"),
155 ("tag", &tag),
156 ("boolean", "true"),
157 ("apikey", api_key),
158 ]);
159
160 if !base_url.contains("chainid=") {
161 req = req.query(&[("chainid", &chain_id.to_string())]);
163 }
164
165 let resp = req.send().await?.text().await?;
166
167 debug!(target: "etherscan", %tag, response_bytes = resp.len(), "fetched block from etherscan");
168
169 Ok(resp)
170 })
171}