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::{debug, 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    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    /// Create a new Etherscan block provider with the given base URL and API key.
28    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    /// Sets the interval at which the provider fetches new blocks.
45    pub const fn with_interval(mut self, interval: Duration) -> Self {
46        self.interval = interval;
47        self
48    }
49
50    /// Load block using Etherscan API. Note: only `BlockNumberOrTag::Latest`,
51    /// `BlockNumberOrTag::Earliest`, `BlockNumberOrTag::Pending`, `BlockNumberOrTag::Number(u64)`
52    /// are supported.
53    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            // only append chainid if not part of the base url already
72            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                // Channel closed.
121                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}