foundry_block_explorers/
account.rs

1use crate::{
2    block_number::BlockNumber,
3    serde_helpers::{
4        deserialize_stringified_block_number, deserialize_stringified_numeric,
5        deserialize_stringified_numeric_opt, deserialize_stringified_u64,
6        deserialize_stringified_u64_opt,
7    },
8    Client, EtherscanError, Query, Response, Result,
9};
10use alloy_primitives::{Address, Bytes, B256, U256};
11use serde::{Deserialize, Serialize};
12use std::{
13    borrow::Cow,
14    collections::HashMap,
15    fmt::{Display, Error, Formatter},
16};
17
18/// The raw response from the balance-related API endpoints
19#[derive(Clone, Debug, Serialize, Deserialize)]
20pub struct AccountBalance {
21    pub account: Address,
22    pub balance: String,
23}
24
25mod genesis_string {
26    use super::*;
27    use serde::{
28        de::{DeserializeOwned, Error as _},
29        ser::Error as _,
30        Deserializer, Serializer,
31    };
32
33    pub(crate) fn serialize<T, S>(
34        value: &GenesisOption<T>,
35        serializer: S,
36    ) -> std::result::Result<S::Ok, S::Error>
37    where
38        T: Serialize,
39        S: Serializer,
40    {
41        let json = match value {
42            GenesisOption::None => Cow::from(""),
43            GenesisOption::Genesis => Cow::from("GENESIS"),
44            GenesisOption::Some(value) => {
45                serde_json::to_string(value).map_err(S::Error::custom)?.into()
46            }
47        };
48        serializer.serialize_str(&json)
49    }
50
51    pub(crate) fn deserialize<'de, T, D>(
52        deserializer: D,
53    ) -> std::result::Result<GenesisOption<T>, D::Error>
54    where
55        T: DeserializeOwned,
56        D: Deserializer<'de>,
57    {
58        let json = Cow::<'de, str>::deserialize(deserializer)?;
59        if !json.is_empty() && !json.starts_with("GENESIS") {
60            serde_json::from_str(&format!("\"{}\"", &json))
61                .map(GenesisOption::Some)
62                .map_err(D::Error::custom)
63        } else if json.starts_with("GENESIS") {
64            Ok(GenesisOption::Genesis)
65        } else {
66            Ok(GenesisOption::None)
67        }
68    }
69}
70
71mod json_string {
72    use super::*;
73    use serde::{
74        de::{DeserializeOwned, Error as _},
75        ser::Error as _,
76        Deserializer, Serializer,
77    };
78
79    pub(crate) fn serialize<T, S>(
80        value: &Option<T>,
81        serializer: S,
82    ) -> std::result::Result<S::Ok, S::Error>
83    where
84        T: Serialize,
85        S: Serializer,
86    {
87        let json = match value {
88            Option::None => Cow::from(""),
89            Option::Some(value) => serde_json::to_string(value).map_err(S::Error::custom)?.into(),
90        };
91        serializer.serialize_str(&json)
92    }
93
94    pub(crate) fn deserialize<'de, T, D>(
95        deserializer: D,
96    ) -> std::result::Result<Option<T>, D::Error>
97    where
98        T: DeserializeOwned,
99        D: Deserializer<'de>,
100    {
101        let json = Cow::<'de, str>::deserialize(deserializer)?;
102        if json.is_empty() {
103            Ok(Option::None)
104        } else {
105            serde_json::from_str(&format!("\"{}\"", &json))
106                .map(Option::Some)
107                .map_err(D::Error::custom)
108        }
109    }
110}
111
112/// Possible values for some field responses.
113///
114/// Transactions from the Genesis block may contain fields that do not conform to the expected
115/// types.
116#[derive(Clone, Debug)]
117pub enum GenesisOption<T> {
118    None,
119    Genesis,
120    Some(T),
121}
122
123impl<T> From<GenesisOption<T>> for Option<T> {
124    fn from(value: GenesisOption<T>) -> Self {
125        match value {
126            GenesisOption::Some(value) => Some(value),
127            _ => None,
128        }
129    }
130}
131
132impl<T> GenesisOption<T> {
133    pub fn is_genesis(&self) -> bool {
134        matches!(self, GenesisOption::Genesis)
135    }
136
137    pub fn value(&self) -> Option<&T> {
138        match self {
139            GenesisOption::Some(value) => Some(value),
140            _ => None,
141        }
142    }
143}
144
145/// The raw response from the transaction list API endpoint
146#[derive(Clone, Debug, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct NormalTransaction {
149    pub is_error: String,
150    #[serde(deserialize_with = "deserialize_stringified_block_number")]
151    pub block_number: BlockNumber,
152    pub time_stamp: String,
153    #[serde(with = "genesis_string")]
154    pub hash: GenesisOption<B256>,
155    #[serde(with = "json_string")]
156    pub nonce: Option<U256>,
157    #[serde(with = "json_string")]
158    pub block_hash: Option<U256>,
159    #[serde(deserialize_with = "deserialize_stringified_u64_opt")]
160    pub transaction_index: Option<u64>,
161    #[serde(with = "genesis_string")]
162    pub from: GenesisOption<Address>,
163    #[serde(with = "json_string")]
164    pub to: Option<Address>,
165    #[serde(deserialize_with = "deserialize_stringified_numeric")]
166    pub value: U256,
167    #[serde(deserialize_with = "deserialize_stringified_numeric")]
168    pub gas: U256,
169    #[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
170    pub gas_price: Option<U256>,
171    #[serde(rename = "txreceipt_status")]
172    pub tx_receipt_status: String,
173    pub input: Bytes,
174    #[serde(with = "json_string")]
175    pub contract_address: Option<Address>,
176    #[serde(deserialize_with = "deserialize_stringified_numeric")]
177    pub gas_used: U256,
178    #[serde(deserialize_with = "deserialize_stringified_numeric")]
179    pub cumulative_gas_used: U256,
180    #[serde(deserialize_with = "deserialize_stringified_u64")]
181    pub confirmations: u64,
182    pub method_id: Option<Bytes>,
183    #[serde(with = "json_string")]
184    pub function_name: Option<String>,
185}
186
187/// The raw response from the internal transaction list API endpoint
188#[derive(Clone, Debug, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct InternalTransaction {
191    #[serde(deserialize_with = "deserialize_stringified_block_number")]
192    pub block_number: BlockNumber,
193    pub time_stamp: String,
194    pub hash: B256,
195    pub from: Address,
196    #[serde(with = "genesis_string")]
197    pub to: GenesisOption<Address>,
198    #[serde(deserialize_with = "deserialize_stringified_numeric")]
199    pub value: U256,
200    #[serde(with = "genesis_string")]
201    pub contract_address: GenesisOption<Address>,
202    #[serde(with = "genesis_string")]
203    pub input: GenesisOption<Bytes>,
204    #[serde(rename = "type")]
205    pub result_type: String,
206    #[serde(deserialize_with = "deserialize_stringified_numeric")]
207    pub gas: U256,
208    #[serde(deserialize_with = "deserialize_stringified_numeric")]
209    pub gas_used: U256,
210    pub trace_id: String,
211    pub is_error: String,
212    pub err_code: String,
213}
214
215/// The raw response from the ERC20 transfer list API endpoint
216#[derive(Clone, Debug, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct ERC20TokenTransferEvent {
219    #[serde(deserialize_with = "deserialize_stringified_block_number")]
220    pub block_number: BlockNumber,
221    pub time_stamp: String,
222    pub hash: B256,
223    #[serde(deserialize_with = "deserialize_stringified_numeric")]
224    pub nonce: U256,
225    pub block_hash: B256,
226    pub from: Address,
227    pub contract_address: Address,
228    pub to: Option<Address>,
229    #[serde(deserialize_with = "deserialize_stringified_numeric")]
230    pub value: U256,
231    pub token_name: String,
232    pub token_symbol: String,
233    pub token_decimal: String,
234    #[serde(deserialize_with = "deserialize_stringified_u64")]
235    pub transaction_index: u64,
236    #[serde(deserialize_with = "deserialize_stringified_numeric")]
237    pub gas: U256,
238    #[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
239    pub gas_price: Option<U256>,
240    #[serde(deserialize_with = "deserialize_stringified_numeric")]
241    pub gas_used: U256,
242    #[serde(deserialize_with = "deserialize_stringified_numeric")]
243    pub cumulative_gas_used: U256,
244    /// deprecated
245    pub input: String,
246    #[serde(deserialize_with = "deserialize_stringified_u64")]
247    pub confirmations: u64,
248}
249
250/// The raw response from the ERC721 transfer list API endpoint
251#[derive(Clone, Debug, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct ERC721TokenTransferEvent {
254    #[serde(deserialize_with = "deserialize_stringified_block_number")]
255    pub block_number: BlockNumber,
256    pub time_stamp: String,
257    pub hash: B256,
258    #[serde(deserialize_with = "deserialize_stringified_numeric")]
259    pub nonce: U256,
260    pub block_hash: B256,
261    pub from: Address,
262    pub contract_address: Address,
263    pub to: Option<Address>,
264    #[serde(rename = "tokenID")]
265    pub token_id: String,
266    pub token_name: String,
267    pub token_symbol: String,
268    pub token_decimal: String,
269    #[serde(deserialize_with = "deserialize_stringified_u64")]
270    pub transaction_index: u64,
271    #[serde(deserialize_with = "deserialize_stringified_numeric")]
272    pub gas: U256,
273    #[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
274    pub gas_price: Option<U256>,
275    #[serde(deserialize_with = "deserialize_stringified_numeric")]
276    pub gas_used: U256,
277    #[serde(deserialize_with = "deserialize_stringified_numeric")]
278    pub cumulative_gas_used: U256,
279    /// deprecated
280    pub input: String,
281    #[serde(deserialize_with = "deserialize_stringified_u64")]
282    pub confirmations: u64,
283}
284
285/// The raw response from the ERC1155 transfer list API endpoint
286#[derive(Clone, Debug, Serialize, Deserialize)]
287#[serde(rename_all = "camelCase")]
288pub struct ERC1155TokenTransferEvent {
289    #[serde(deserialize_with = "deserialize_stringified_block_number")]
290    pub block_number: BlockNumber,
291    pub time_stamp: String,
292    pub hash: B256,
293    #[serde(deserialize_with = "deserialize_stringified_numeric")]
294    pub nonce: U256,
295    pub block_hash: B256,
296    pub from: Address,
297    pub contract_address: Address,
298    pub to: Option<Address>,
299    #[serde(rename = "tokenID")]
300    pub token_id: String,
301    pub token_value: String,
302    pub token_name: String,
303    pub token_symbol: String,
304    #[serde(deserialize_with = "deserialize_stringified_u64")]
305    pub transaction_index: u64,
306    #[serde(deserialize_with = "deserialize_stringified_numeric")]
307    pub gas: U256,
308    #[serde(deserialize_with = "deserialize_stringified_numeric_opt")]
309    pub gas_price: Option<U256>,
310    #[serde(deserialize_with = "deserialize_stringified_numeric")]
311    pub gas_used: U256,
312    #[serde(deserialize_with = "deserialize_stringified_numeric")]
313    pub cumulative_gas_used: U256,
314    /// deprecated
315    pub input: String,
316    #[serde(deserialize_with = "deserialize_stringified_u64")]
317    pub confirmations: u64,
318}
319
320/// The raw response from the mined blocks API endpoint
321#[derive(Clone, Debug, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase")]
323pub struct MinedBlock {
324    #[serde(deserialize_with = "deserialize_stringified_block_number")]
325    pub block_number: BlockNumber,
326    pub time_stamp: String,
327    pub block_reward: String,
328}
329
330/// The pre-defined block parameter for balance API endpoints
331#[derive(Clone, Copy, Debug, Default)]
332pub enum Tag {
333    Earliest,
334    Pending,
335    #[default]
336    Latest,
337}
338
339impl Display for Tag {
340    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
341        match self {
342            Tag::Earliest => write!(f, "earliest"),
343            Tag::Pending => write!(f, "pending"),
344            Tag::Latest => write!(f, "latest"),
345        }
346    }
347}
348
349/// The list sorting preference
350#[derive(Clone, Copy, Debug)]
351pub enum Sort {
352    Asc,
353    Desc,
354}
355
356impl Display for Sort {
357    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
358        match self {
359            Sort::Asc => write!(f, "asc"),
360            Sort::Desc => write!(f, "desc"),
361        }
362    }
363}
364
365/// Common optional arguments for the transaction or event list API endpoints
366#[derive(Clone, Copy, Debug)]
367pub struct TxListParams {
368    pub start_block: u64,
369    pub end_block: u64,
370    pub page: u64,
371    pub offset: u64,
372    pub sort: Sort,
373}
374
375impl TxListParams {
376    pub fn new(start_block: u64, end_block: u64, page: u64, offset: u64, sort: Sort) -> Self {
377        Self { start_block, end_block, page, offset, sort }
378    }
379}
380
381impl Default for TxListParams {
382    fn default() -> Self {
383        Self { start_block: 0, end_block: 99999999, page: 0, offset: 10000, sort: Sort::Asc }
384    }
385}
386
387impl From<TxListParams> for HashMap<&'static str, String> {
388    fn from(tx_params: TxListParams) -> Self {
389        let mut params = HashMap::new();
390        params.insert("startBlock", tx_params.start_block.to_string());
391        params.insert("endBlock", tx_params.end_block.to_string());
392        params.insert("page", tx_params.page.to_string());
393        params.insert("offset", tx_params.offset.to_string());
394        params.insert("sort", tx_params.sort.to_string());
395        params
396    }
397}
398
399/// Options for querying internal transactions
400#[derive(Clone, Debug)]
401#[allow(missing_copy_implementations)]
402pub enum InternalTxQueryOption {
403    ByAddress(Address),
404    ByTransactionHash(B256),
405    ByBlockRange,
406}
407
408/// Options for querying ERC20 or ERC721 token transfers
409#[derive(Clone, Debug)]
410#[allow(missing_copy_implementations)]
411pub enum TokenQueryOption {
412    ByAddress(Address),
413    ByContract(Address),
414    ByAddressAndContract(Address, Address),
415}
416
417impl TokenQueryOption {
418    pub fn into_params(self, list_params: TxListParams) -> HashMap<&'static str, String> {
419        let mut params: HashMap<&'static str, String> = list_params.into();
420        match self {
421            TokenQueryOption::ByAddress(address) => {
422                params.insert("address", format!("{address:?}"));
423                params
424            }
425            TokenQueryOption::ByContract(contract) => {
426                params.insert("contractaddress", format!("{contract:?}"));
427                params
428            }
429            TokenQueryOption::ByAddressAndContract(address, contract) => {
430                params.insert("address", format!("{address:?}"));
431                params.insert("contractaddress", format!("{contract:?}"));
432                params
433            }
434        }
435    }
436}
437
438/// The pre-defined block type for retrieving mined blocks
439#[derive(Copy, Clone, Debug, Default)]
440pub enum BlockType {
441    #[default]
442    CanonicalBlocks,
443    Uncles,
444}
445
446impl Display for BlockType {
447    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
448        match self {
449            BlockType::CanonicalBlocks => write!(f, "blocks"),
450            BlockType::Uncles => write!(f, "uncles"),
451        }
452    }
453}
454
455impl Client {
456    /// Returns the Ether balance of a given address.
457    ///
458    /// # Examples
459    ///
460    /// ```no_run
461    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
462    /// let address = "0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse()?;
463    /// let balance = client.get_ether_balance_single(&address, None).await?;
464    /// # Ok(()) }
465    /// ```
466    pub async fn get_ether_balance_single(
467        &self,
468        address: &Address,
469        tag: Option<Tag>,
470    ) -> Result<AccountBalance> {
471        let tag_str = tag.unwrap_or_default().to_string();
472        let addr_str = format!("{address:?}");
473        let query = self.create_query(
474            "account",
475            "balance",
476            HashMap::from([("address", &addr_str), ("tag", &tag_str)]),
477        );
478        let response: Response<String> = self.get_json(&query).await?;
479
480        match response.status.as_str() {
481            "0" => Err(EtherscanError::BalanceFailed),
482            "1" => Ok(AccountBalance { account: *address, balance: response.result }),
483            err => Err(EtherscanError::BadStatusCode(err.to_string())),
484        }
485    }
486
487    /// Returns the balance of the accounts from a list of addresses.
488    ///
489    /// # Examples
490    ///
491    /// ```no_run
492    /// # use alloy_primitives::Address;
493    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
494    /// let addresses = [
495    ///     "0x3E3c00494d0b306a0739E480DBB5DB91FFb5d4CB".parse::<Address>()?,
496    ///     "0x7e9996ef050a9Fa7A01248e63271F69086aaFc9D".parse::<Address>()?,
497    /// ];
498    /// let balances = client.get_ether_balance_multi(&addresses, None).await?;
499    /// assert_eq!(addresses.len(), balances.len());
500    /// # Ok(()) }
501    /// ```
502    pub async fn get_ether_balance_multi(
503        &self,
504        addresses: &[Address],
505        tag: Option<Tag>,
506    ) -> Result<Vec<AccountBalance>> {
507        let tag_str = tag.unwrap_or_default().to_string();
508        let addrs = addresses.iter().map(|x| format!("{x:?}")).collect::<Vec<String>>().join(",");
509        let query: Query<'_, HashMap<&str, &str>> = self.create_query(
510            "account",
511            "balancemulti",
512            HashMap::from([("address", addrs.as_ref()), ("tag", tag_str.as_ref())]),
513        );
514        let response: Response<Vec<AccountBalance>> = self.get_json(&query).await?;
515
516        match response.status.as_str() {
517            "0" => Err(EtherscanError::BalanceFailed),
518            "1" => Ok(response.result),
519            err => Err(EtherscanError::BadStatusCode(err.to_string())),
520        }
521    }
522
523    /// Returns the list of transactions performed by an address, with optional pagination.
524    ///
525    /// # Examples
526    ///
527    /// ```no_run
528    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
529    /// let address = "0x1f162cf730564efD2Bb96eb27486A2801d76AFB6".parse()?;
530    /// let transactions = client.get_transactions(&address, None).await?;
531    /// # Ok(()) }
532    /// ```
533    pub async fn get_transactions(
534        &self,
535        address: &Address,
536        params: Option<TxListParams>,
537    ) -> Result<Vec<NormalTransaction>> {
538        let mut tx_params: HashMap<&str, String> = params.unwrap_or_default().into();
539        tx_params.insert("address", format!("{address:?}"));
540        let query = self.create_query("account", "txlist", tx_params);
541        let response: Response<Vec<NormalTransaction>> = self.get_json(&query).await?;
542
543        Ok(response.result)
544    }
545
546    /// Returns the list of internal transactions performed by an address or within a transaction,
547    /// with optional pagination.
548    ///
549    /// # Examples
550    ///
551    /// ```no_run
552    /// use foundry_block_explorers::account::InternalTxQueryOption;
553    ///
554    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
555    /// let address = "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3".parse()?;
556    /// let query = InternalTxQueryOption::ByAddress(address);
557    /// let internal_transactions = client.get_internal_transactions(query, None).await?;
558    /// # Ok(()) }
559    /// ```
560    pub async fn get_internal_transactions(
561        &self,
562        tx_query_option: InternalTxQueryOption,
563        params: Option<TxListParams>,
564    ) -> Result<Vec<InternalTransaction>> {
565        let mut tx_params: HashMap<&str, String> = params.unwrap_or_default().into();
566        match tx_query_option {
567            InternalTxQueryOption::ByAddress(address) => {
568                tx_params.insert("address", format!("{address:?}"));
569            }
570            InternalTxQueryOption::ByTransactionHash(tx_hash) => {
571                tx_params.insert("txhash", format!("{tx_hash:?}"));
572            }
573            _ => {}
574        }
575        let query = self.create_query("account", "txlistinternal", tx_params);
576        let response: Response<Vec<InternalTransaction>> = self.get_json(&query).await?;
577
578        Ok(response.result)
579    }
580
581    /// Returns the list of ERC-20 tokens transferred by an address, with optional filtering by
582    /// token contract.
583    ///
584    /// # Examples
585    ///
586    /// ```no_run
587    /// use foundry_block_explorers::account::TokenQueryOption;
588    ///
589    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
590    /// let address = "0x4e83362442b8d1bec281594cea3050c8eb01311c".parse()?;
591    /// let query = TokenQueryOption::ByAddress(address);
592    /// let events = client.get_erc20_token_transfer_events(query, None).await?;
593    /// # Ok(()) }
594    /// ```
595    pub async fn get_erc20_token_transfer_events(
596        &self,
597        event_query_option: TokenQueryOption,
598        params: Option<TxListParams>,
599    ) -> Result<Vec<ERC20TokenTransferEvent>> {
600        let params = event_query_option.into_params(params.unwrap_or_default());
601        let query = self.create_query("account", "tokentx", params);
602        let response: Response<Vec<ERC20TokenTransferEvent>> = self.get_json(&query).await?;
603
604        Ok(response.result)
605    }
606
607    /// Returns the list of ERC-721 ( NFT ) tokens transferred by an address, with optional
608    /// filtering by token contract.
609    ///
610    /// # Examples
611    ///
612    /// ```no_run
613    /// use foundry_block_explorers::account::TokenQueryOption;
614    ///
615    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
616    /// let contract = "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse()?;
617    /// let query = TokenQueryOption::ByContract(contract);
618    /// let events = client.get_erc721_token_transfer_events(query, None).await?;
619    /// # Ok(()) }
620    /// ```
621    pub async fn get_erc721_token_transfer_events(
622        &self,
623        event_query_option: TokenQueryOption,
624        params: Option<TxListParams>,
625    ) -> Result<Vec<ERC721TokenTransferEvent>> {
626        let params = event_query_option.into_params(params.unwrap_or_default());
627        let query = self.create_query("account", "tokennfttx", params);
628        let response: Response<Vec<ERC721TokenTransferEvent>> = self.get_json(&query).await?;
629
630        Ok(response.result)
631    }
632
633    /// Returns the list of ERC-1155 ( NFT ) tokens transferred by an address, with optional
634    /// filtering by token contract.
635    ///
636    /// # Examples
637    ///
638    /// ```no_run
639    /// use foundry_block_explorers::account::TokenQueryOption;
640    ///
641    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
642    /// let address = "0x216CD350a4044e7016f14936663e2880Dd2A39d7".parse()?;
643    /// let contract = "0x495f947276749ce646f68ac8c248420045cb7b5e".parse()?;
644    /// let query = TokenQueryOption::ByAddressAndContract(address, contract);
645    /// let events = client.get_erc1155_token_transfer_events(query, None).await?;
646    /// # Ok(()) }
647    /// ```
648    pub async fn get_erc1155_token_transfer_events(
649        &self,
650        event_query_option: TokenQueryOption,
651        params: Option<TxListParams>,
652    ) -> Result<Vec<ERC1155TokenTransferEvent>> {
653        let params = event_query_option.into_params(params.unwrap_or_default());
654        let query = self.create_query("account", "token1155tx", params);
655        let response: Response<Vec<ERC1155TokenTransferEvent>> = self.get_json(&query).await?;
656
657        Ok(response.result)
658    }
659
660    /// Returns the list of blocks mined by an address.
661    ///
662    /// # Examples
663    ///
664    /// ```no_run
665    /// # async fn foo(client: foundry_block_explorers::Client) -> Result<(), Box<dyn std::error::Error>> {
666    /// let address = "0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b".parse()?;
667    /// let blocks = client.get_mined_blocks(&address, None, None).await?;
668    /// # Ok(()) }
669    /// ```
670    pub async fn get_mined_blocks(
671        &self,
672        address: &Address,
673        block_type: Option<BlockType>,
674        page_and_offset: Option<(u64, u64)>,
675    ) -> Result<Vec<MinedBlock>> {
676        let mut params = HashMap::new();
677        params.insert("address", format!("{address:?}"));
678        params.insert("blocktype", block_type.unwrap_or_default().to_string());
679        if let Some((page, offset)) = page_and_offset {
680            params.insert("page", page.to_string());
681            params.insert("offset", offset.to_string());
682        }
683        let query = self.create_query("account", "getminedblocks", params);
684        let response: Response<Vec<MinedBlock>> = self.get_json(&query).await?;
685
686        Ok(response.result)
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693
694    // <https://github.com/gakonst/ethers-rs/issues/2612>
695    #[test]
696    fn can_parse_response_2612() {
697        let err = r#"{
698  "status": "1",
699  "message": "OK",
700  "result": [
701    {
702      "blockNumber": "18185184",
703      "timeStamp": "1695310607",
704      "hash": "0x95983231acd079498b7628c6b6dd4866f559a23120fbce590c5dd7f10c7628af",
705      "nonce": "1325609",
706      "blockHash": "0x61e106aa2446ba06fe0217eb5bd9dae98a72b56dad2c2197f60a0798ce9f0dc6",
707      "transactionIndex": "45",
708      "from": "0xae2fc483527b8ef99eb5d9b44875f005ba1fae13",
709      "to": "0x6b75d8af000000e20b7a7ddf000ba900b4009a80",
710      "value": "23283064365",
711      "gas": "107142",
712      "gasPrice": "15945612744",
713      "isError": "0",
714      "txreceipt_status": "1",
715      "input": "0xe061",
716      "contractAddress": "",
717      "cumulativeGasUsed": "3013734",
718      "gasUsed": "44879",
719      "confirmations": "28565",
720      "methodId": "0xe061",
721      "functionName": ""
722    }
723  ]
724}"#;
725        let _resp: Response<Vec<NormalTransaction>> = serde_json::from_str(err).unwrap();
726    }
727}