Skip to main content

foundry_block_explorers/
account.rs

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