foundry_blob_explorers/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(
3    missing_copy_implementations,
4    missing_debug_implementations,
5    missing_docs,
6    unreachable_pub,
7    rustdoc::all
8)]
9#![cfg_attr(not(test), warn(unused_crate_dependencies))]
10#![deny(unused_must_use, rust_2018_idioms)]
11#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
12
13use alloy_chains::{Chain, ChainKind, NamedChain};
14use alloy_primitives::B256;
15use alloy_rpc_types_eth::BlockHashOrNumber;
16pub use request::*;
17pub use response::*;
18use serde::de::DeserializeOwned;
19
20pub mod request;
21pub mod response;
22
23/// Client for [Blobscan API](https://api.blobscan.com/)
24///
25/// See also [Blobscan API Documentation](https://docs.blobscan.com/)
26#[derive(Clone, Debug)]
27pub struct Client {
28    /// Base URL for the API
29    pub(crate) baseurl: String,
30    pub(crate) client: reqwest::Client,
31}
32
33impl Client {
34    /// Create a new client.
35    ///
36    /// `baseurl` is the base URL provided to the internal [reqwest::Client]
37    pub fn new(baseurl: impl Into<String>) -> Self {
38        let client = {
39            let dur = std::time::Duration::from_secs(30);
40            reqwest::ClientBuilder::new().connect_timeout(dur).timeout(dur)
41        };
42        Self::new_with_client(baseurl, client.build().unwrap())
43    }
44
45    /// Create a new client instance for `blobscan.com` with the correct endpoint based on the
46    /// chain.
47    ///
48    /// At this time, only the following chains are supported by Blobscan:
49    /// - Ethereum Mainnet: <https://api.blobscan.com/>
50    /// - Sepolia Testnet: <https://api.sepolia.blobscan.com/>
51    /// - Holesky Testnet: <https://api.holesky.blobscan.com/>
52    ///
53    /// For other chains this will return `None`
54    pub fn new_chain(chain: Chain) -> Option<Self> {
55        Self::new_chain_with_client(chain, reqwest::Client::new())
56    }
57
58    /// Create a new client instance for `blobscan.com` with the given [reqwest::Client] and the
59    /// correct endpoint based on the chain.
60    ///
61    /// At this time, only the following chains are supported by Blobscan:
62    /// - Ethereum Mainnet: <https://api.blobscan.com/>
63    /// - Sepolia Testnet: <https://api.sepolia.blobscan.com/>
64    /// - Holesky Testnet: <https://api.holesky.blobscan.com/>
65    ///
66    /// For other chains this will return `None`
67    pub fn new_chain_with_client(chain: Chain, client: reqwest::Client) -> Option<Self> {
68        match chain.kind() {
69            ChainKind::Named(NamedChain::Mainnet) => {
70                Some(Self::new_with_client("https://api.blobscan.com/", client))
71            }
72            ChainKind::Named(NamedChain::Sepolia) | ChainKind::Named(NamedChain::Holesky) => {
73                Some(Self::new_with_client(format!("https://api.{chain}.blobscan.com/"), client))
74            }
75            _ => None,
76        }
77    }
78
79    /// Creates a new client instance for the Ethereum Mainnet with the correct endpoint: <https://api.blobscan.com/>
80    pub fn mainnet() -> Self {
81        Self::new_chain(Chain::mainnet()).unwrap()
82    }
83
84    /// Creates a new client instance for the sepolia Testnet with the correct endpoint: <https://api.sepolia.blobscan.com/>
85    pub fn sepolia() -> Self {
86        Self::new_chain(Chain::sepolia()).unwrap()
87    }
88
89    /// Creates a new client instance for the holesky Testnet with the correct endpoint: <https://api.holesky.blobscan.com/>
90    pub fn holesky() -> Self {
91        Self::new_chain(Chain::holesky()).unwrap()
92    }
93
94    /// Creates a new client instance for the Ethereum Mainnet with the given [reqwest::Client] and  the correct endpoint: <https://api.blobscan.com/>
95    pub fn mainnet_with_client(client: reqwest::Client) -> Self {
96        Self::new_chain_with_client(Chain::mainnet(), client).unwrap()
97    }
98
99    /// Creates a new client instance for the sepolia Testnet with the given [reqwest::Client] and  the correct endpoint: <https://api.sepolia.blobscan.com/>
100    pub fn sepolia_with_client(client: reqwest::Client) -> Self {
101        Self::new_chain_with_client(Chain::sepolia(), client).unwrap()
102    }
103
104    /// Creates a new client instance for the holesky Testnet with the given [reqwest::Client] and the correct endpoint: <https://api.holesky.blobscan.com/>
105    pub fn holesky_with_client(client: reqwest::Client) -> Self {
106        Self::new_chain_with_client(Chain::holesky(), client).unwrap()
107    }
108
109    /// Construct a new client with an existing [reqwest::Client] allowing more control over its
110    /// configuration.
111    ///
112    /// `baseurl` is the base URL provided to the internal
113    pub fn new_with_client(baseurl: impl Into<String>, client: reqwest::Client) -> Self {
114        let mut baseurl = baseurl.into();
115        if !baseurl.ends_with('/') {
116            baseurl.push('/');
117        }
118        Self { baseurl, client }
119    }
120
121    /// Get the base URL to which requests are made.
122    pub fn baseurl(&self) -> &str {
123        &self.baseurl
124    }
125
126    /// Get the internal `reqwest::Client` used to make requests.
127    pub fn client(&self) -> &reqwest::Client {
128        &self.client
129    }
130
131    async fn get_transaction<T: DeserializeOwned>(
132        &self,
133        hash: B256,
134        query: GetTransactionQuery,
135    ) -> reqwest::Result<T> {
136        self.client
137            .get(format!("{}transactions/{}", self.baseurl, hash))
138            .header(reqwest::header::ACCEPT, "application/json")
139            .query(&query)
140            .send()
141            .await?
142            .json()
143            .await
144    }
145
146    /// Retrieves the __full__ transaction details for given block transaction hash.
147    ///
148    /// Sends a `GET` request to `/transactions/{hash}`
149    ///
150    /// ### Example
151    ///
152    /// ```no_run
153    /// use alloy_primitives::b256;
154    /// use foundry_blob_explorers::Client;
155    /// # async fn demo() {
156    /// let client = Client::holesky();
157    /// let tx = client
158    ///     .transaction(b256!("d4f136048a56b9b62c9cdca0ce0dbb224295fd0e0170dbbc78891d132f639d60"))
159    ///     .await
160    ///     .unwrap();
161    /// println!("[{}] blob: {:?}", tx.hash, tx.blob_sidecar());
162    /// # }
163    /// ```
164    pub async fn transaction(&self, tx_hash: B256) -> reqwest::Result<TransactionDetails> {
165        self.get_transaction(tx_hash, Default::default()).await
166    }
167
168    /// Retrieves the specific transaction details for given transaction hash.
169    ///
170    /// Sends a `GET` request to `/transactions/{hash}`
171    pub async fn transaction_with_query(
172        &self,
173        tx_hash: B256,
174        query: GetTransactionQuery,
175    ) -> reqwest::Result<TransactionDetails> {
176        self.get_transaction(tx_hash, query).await
177    }
178
179    async fn get_block<T: DeserializeOwned>(
180        &self,
181        block: BlockHashOrNumber,
182        query: GetBlockQuery,
183    ) -> reqwest::Result<T> {
184        self.client
185            .get(format!("{}blocks/{}", self.baseurl, block))
186            .header(reqwest::header::ACCEPT, "application/json")
187            .query(&query)
188            .send()
189            .await?
190            .json()
191            .await
192    }
193
194    /// Retrieves the __full__ block details for given block number or hash.
195    ///
196    /// Sends a `GET` request to `/blocks/{id}`
197    ///
198    /// ### Example
199    ///
200    /// ```no_run
201    /// use foundry_blob_explorers::Client;
202    /// # async fn demo() {
203    /// let client = Client::holesky();
204    /// let block = client
205    ///     .block(
206    ///         "0xc3a0113f60107614d1bba950799903dadbc2116256a40b1fefb37e9d409f1866".parse().unwrap(),
207    ///     )
208    ///     .await
209    ///     .unwrap();
210    /// for (tx, sidecar) in block.blob_sidecars() {
211    ///     println!("[{}] blob: {:?}", tx, sidecar);
212    /// }
213    /// # }
214    /// ```
215    pub async fn block(
216        &self,
217        block: BlockHashOrNumber,
218    ) -> reqwest::Result<BlockResponse<FullTransactionDetails>> {
219        self.get_block(block, GetBlockQuery::default()).await
220    }
221
222    /// Retrieves the specific block details for given block number or hash.
223    ///
224    /// Sends a `GET` request to `/blocks/{id}`
225    pub async fn block_with_query(
226        &self,
227        block: BlockHashOrNumber,
228        query: GetBlockQuery,
229    ) -> reqwest::Result<BlockResponse<SelectedTransactionDetails>> {
230        self.get_block(block, query).await
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[tokio::test]
239    #[ignore]
240    async fn get_block_by_id() {
241        let block = "0xc3a0113f60107614d1bba950799903dadbc2116256a40b1fefb37e9d409f1866";
242        let client = Client::holesky();
243
244        let _block = client.block(block.parse().unwrap()).await.unwrap();
245        for (_tx, _sidecar) in _block.blob_sidecars() {
246            // iter
247        }
248    }
249
250    #[tokio::test]
251    #[ignore]
252    async fn get_single_transaction() {
253        let tx = "0xd4f136048a56b9b62c9cdca0ce0dbb224295fd0e0170dbbc78891d132f639d60";
254        let client = Client::holesky();
255
256        let tx = client.transaction(tx.parse().unwrap()).await.unwrap();
257        let _sidecar = tx.blob_sidecar();
258    }
259}