reth_consensus_debug_client/providers/
etherscan.rs

1use 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/// Block provider that fetches new blocks from Etherscan API.
12#[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    /// Create a new Etherscan block provider with the given base URL and API key.
27    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    /// Sets the interval at which the provider fetches new blocks.
42    pub const fn with_interval(mut self, interval: Duration) -> Self {
43        self.interval = interval;
44        self
45    }
46
47    /// Load block using Etherscan API. Note: only `BlockNumberOrTag::Latest`,
48    /// `BlockNumberOrTag::Earliest`, `BlockNumberOrTag::Pending`, `BlockNumberOrTag::Number(u64)`
49    /// are supported.
50    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                // Channel closed.
112                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}