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::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 interval: Duration,
18 #[debug(skip)]
19 convert: Arc<dyn Fn(RpcBlock) -> PrimitiveBlock + Send + Sync>,
20}
21
22impl<RpcBlock, PrimitiveBlock> EtherscanBlockProvider<RpcBlock, PrimitiveBlock>
23where
24 RpcBlock: Serialize + DeserializeOwned,
25{
26 pub fn new(
28 base_url: String,
29 api_key: String,
30 convert: impl Fn(RpcBlock) -> PrimitiveBlock + Send + Sync + 'static,
31 ) -> Self {
32 Self {
33 http_client: Client::new(),
34 base_url,
35 api_key,
36 interval: Duration::from_secs(3),
37 convert: Arc::new(convert),
38 }
39 }
40
41 pub const fn with_interval(mut self, interval: Duration) -> Self {
43 self.interval = interval;
44 self
45 }
46
47 pub async fn load_block(
51 &self,
52 block_number_or_tag: BlockNumberOrTag,
53 ) -> eyre::Result<PrimitiveBlock> {
54 let tag = match block_number_or_tag {
55 BlockNumberOrTag::Number(num) => format!("{num:#02x}"),
56 tag => tag.to_string(),
57 };
58
59 let resp: Response<RpcBlock> = self
60 .http_client
61 .get(&self.base_url)
62 .query(&[
63 ("module", "proxy"),
64 ("action", "eth_getBlockByNumber"),
65 ("tag", &tag),
66 ("boolean", "true"),
67 ("apikey", &self.api_key),
68 ])
69 .send()
70 .await?
71 .json()
72 .await?;
73
74 let payload = resp.payload;
75 match payload {
76 ResponsePayload::Success(block) => Ok((self.convert)(block)),
77 ResponsePayload::Failure(err) => Err(eyre::eyre!("Failed to get block: {err}")),
78 }
79 }
80}
81
82impl<RpcBlock, PrimitiveBlock> BlockProvider for EtherscanBlockProvider<RpcBlock, PrimitiveBlock>
83where
84 RpcBlock: Serialize + DeserializeOwned + 'static,
85 PrimitiveBlock: reth_primitives_traits::Block + 'static,
86{
87 type Block = PrimitiveBlock;
88
89 async fn subscribe_blocks(&self, tx: mpsc::Sender<Self::Block>) {
90 let mut last_block_number: Option<u64> = None;
91 let mut interval = interval(self.interval);
92 loop {
93 interval.tick().await;
94 let block = match self.load_block(BlockNumberOrTag::Latest).await {
95 Ok(block) => block,
96 Err(err) => {
97 warn!(
98 target: "consensus::debug-client",
99 %err,
100 "Failed to fetch a block from Etherscan",
101 );
102 continue
103 }
104 };
105 let block_number = block.header().number();
106 if Some(block_number) == last_block_number {
107 continue;
108 }
109
110 if tx.send(block).await.is_err() {
111 break;
113 }
114
115 last_block_number = Some(block_number);
116 }
117 }
118
119 async fn get_block(&self, block_number: u64) -> eyre::Result<Self::Block> {
120 self.load_block(BlockNumberOrTag::Number(block_number)).await
121 }
122}