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}