reth_consensus_debug_client/providers/
etherscan.rs1use crate::BlockProvider;
2use alloy_consensus::BlockHeader;
3use alloy_eips::BlockNumberOrTag;
4use alloy_json_rpc::{Response, ResponsePayload};
5use reqwest::Client;
6use reth_tracing::tracing::{debug, warn};
7use serde::{de::DeserializeOwned, Serialize};
8use std::{sync::Arc, time::Duration};
9use tokio::{sync::mpsc, time::interval};
10
11#[derive(derive_more::Debug, Clone)]
13pub struct EtherscanBlockProvider<RpcBlock, PrimitiveBlock> {
14 http_client: Client,
15 base_url: String,
16 api_key: String,
17 chain_id: u64,
18 interval: Duration,
19 #[debug(skip)]
20 convert: Arc<dyn Fn(RpcBlock) -> PrimitiveBlock + Send + Sync>,
21}
22
23impl<RpcBlock, PrimitiveBlock> EtherscanBlockProvider<RpcBlock, PrimitiveBlock>
24where
25 RpcBlock: Serialize + DeserializeOwned,
26{
27 pub fn new(
29 base_url: String,
30 api_key: String,
31 chain_id: u64,
32 convert: impl Fn(RpcBlock) -> PrimitiveBlock + Send + Sync + 'static,
33 ) -> Self {
34 Self {
35 http_client: Client::new(),
36 base_url,
37 api_key,
38 chain_id,
39 interval: Duration::from_secs(3),
40 convert: Arc::new(convert),
41 }
42 }
43
44 pub const fn with_interval(mut self, interval: Duration) -> Self {
46 self.interval = interval;
47 self
48 }
49
50 pub async fn load_block(
54 &self,
55 block_number_or_tag: BlockNumberOrTag,
56 ) -> eyre::Result<PrimitiveBlock> {
57 let tag = match block_number_or_tag {
58 BlockNumberOrTag::Number(num) => format!("{num:#02x}"),
59 tag => tag.to_string(),
60 };
61
62 let mut req = self.http_client.get(&self.base_url).query(&[
63 ("module", "proxy"),
64 ("action", "eth_getBlockByNumber"),
65 ("tag", &tag),
66 ("boolean", "true"),
67 ("apikey", &self.api_key),
68 ]);
69
70 if !self.base_url.contains("chainid=") {
71 req = req.query(&[("chainid", &self.chain_id.to_string())]);
73 }
74
75 let resp = req.send().await?.text().await?;
76
77 debug!(target: "etherscan", %resp, "fetched block from etherscan");
78
79 let resp: Response<RpcBlock> = serde_json::from_str(&resp).inspect_err(|err| {
80 warn!(target: "etherscan", "Failed to parse block response from etherscan: {}", err);
81 })?;
82
83 let payload = resp.payload;
84 match payload {
85 ResponsePayload::Success(block) => Ok((self.convert)(block)),
86 ResponsePayload::Failure(err) => Err(eyre::eyre!("Failed to get block: {err}")),
87 }
88 }
89}
90
91impl<RpcBlock, PrimitiveBlock> BlockProvider for EtherscanBlockProvider<RpcBlock, PrimitiveBlock>
92where
93 RpcBlock: Serialize + DeserializeOwned + 'static,
94 PrimitiveBlock: reth_primitives_traits::Block + 'static,
95{
96 type Block = PrimitiveBlock;
97
98 async fn subscribe_blocks(&self, tx: mpsc::Sender<Self::Block>) {
99 let mut last_block_number: Option<u64> = None;
100 let mut interval = interval(self.interval);
101 loop {
102 interval.tick().await;
103 let block = match self.load_block(BlockNumberOrTag::Latest).await {
104 Ok(block) => block,
105 Err(err) => {
106 warn!(
107 target: "consensus::debug-client",
108 %err,
109 "Failed to fetch a block from Etherscan",
110 );
111 continue
112 }
113 };
114 let block_number = block.header().number();
115 if Some(block_number) == last_block_number {
116 continue;
117 }
118
119 if tx.send(block).await.is_err() {
120 break;
122 }
123
124 last_block_number = Some(block_number);
125 }
126 }
127
128 async fn get_block(&self, block_number: u64) -> eyre::Result<Self::Block> {
129 self.load_block(BlockNumberOrTag::Number(block_number)).await
130 }
131}