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