use std::{ borrow::Cow, collections::HashMap, fmt::{Display, Error, Formatter}, }; use ethers_core::{ abi::Address, types::{BlockNumber, Bytes, H256, U256, U64}, }; use serde::{Deserialize, Serialize}; use crate::{Client, EtherscanError, Query, Response, Result}; /// The raw response from the balance-related API endpoints #[derive(Debug, Serialize, Deserialize)] pub struct AccountBalance { pub account: Address, pub balance: String, } mod jsonstring { use super::*; use serde::{ de::{DeserializeOwned, Error as _}, ser::Error as _, Deserializer, Serializer, }; pub fn serialize( value: &GenesisOption, serializer: S, ) -> std::result::Result where T: Serialize, S: Serializer, { let json = match value { GenesisOption::None => Cow::from(""), GenesisOption::Genesis => Cow::from("GENESIS"), GenesisOption::Some(value) => { serde_json::to_string(value).map_err(S::Error::custom)?.into() } }; serializer.serialize_str(&json) } pub fn deserialize<'de, T, D>( deserializer: D, ) -> std::result::Result, D::Error> where T: DeserializeOwned, D: Deserializer<'de>, { let json = Cow::<'de, str>::deserialize(deserializer)?; if !json.is_empty() && !json.starts_with("GENESIS") { let value = serde_json::from_str(&format!("\"{}\"", &json)).map_err(D::Error::custom)?; Ok(GenesisOption::Some(value)) } else if json.starts_with("GENESIS") { Ok(GenesisOption::Genesis) } else { Ok(GenesisOption::None) } } } /// Possible values for some field responses #[derive(Debug)] pub enum GenesisOption { None, Genesis, Some(T), } impl From> for Option { fn from(value: GenesisOption) -> Self { match value { GenesisOption::Some(value) => Some(value), _ => None, } } } impl GenesisOption { pub fn is_genesis(&self) -> bool { matches!(self, GenesisOption::Genesis) } pub fn value(&self) -> Option<&T> { match self { GenesisOption::Some(value) => Some(value), _ => None, } } } /// The raw response from the transaction list API endpoint #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NormalTransaction { pub is_error: String, pub block_number: BlockNumber, pub time_stamp: String, #[serde(with = "jsonstring")] pub hash: GenesisOption, #[serde(with = "jsonstring")] pub nonce: GenesisOption, #[serde(with = "jsonstring")] pub block_hash: GenesisOption, pub transaction_index: Option, #[serde(with = "jsonstring")] pub from: GenesisOption
, pub to: Option
, pub value: U256, pub gas: U256, pub gas_price: Option, #[serde(rename = "txreceipt_status")] pub tx_receipt_status: String, #[serde(with = "jsonstring")] pub input: GenesisOption, #[serde(with = "jsonstring")] pub contract_address: GenesisOption
, pub gas_used: U256, pub cumulative_gas_used: U256, pub confirmations: U64, } /// The raw response from the internal transaction list API endpoint #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InternalTransaction { pub block_number: BlockNumber, pub time_stamp: String, pub hash: H256, pub from: Address, #[serde(with = "jsonstring")] pub to: GenesisOption
, pub value: U256, #[serde(with = "jsonstring")] pub contract_address: GenesisOption
, #[serde(with = "jsonstring")] pub input: GenesisOption, #[serde(rename = "type")] pub result_type: String, pub gas: U256, pub gas_used: U256, pub trace_id: String, pub is_error: String, pub err_code: String, } /// The raw response from the ERC20 transfer list API endpoint #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ERC20TokenTransferEvent { pub block_number: BlockNumber, pub time_stamp: String, pub hash: H256, pub nonce: U256, pub block_hash: H256, pub from: Address, pub contract_address: Address, pub to: Option
, pub value: U256, pub token_name: String, pub token_symbol: String, pub token_decimal: String, pub transaction_index: U64, pub gas: U256, pub gas_price: Option, pub gas_used: U256, pub cumulative_gas_used: U256, /// deprecated pub input: String, pub confirmations: U64, } /// The raw response from the ERC721 transfer list API endpoint #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ERC721TokenTransferEvent { pub block_number: BlockNumber, pub time_stamp: String, pub hash: H256, pub nonce: U256, pub block_hash: H256, pub from: Address, pub contract_address: Address, pub to: Option
, #[serde(rename = "tokenID")] pub token_id: String, pub token_name: String, pub token_symbol: String, pub token_decimal: String, pub transaction_index: U64, pub gas: U256, pub gas_price: Option, pub gas_used: U256, pub cumulative_gas_used: U256, /// deprecated pub input: String, pub confirmations: U64, } /// The raw response from the mined blocks API endpoint #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MinedBlock { pub block_number: BlockNumber, pub time_stamp: String, pub block_reward: String, } /// The pre-defined block parameter for balance API endpoints pub enum Tag { Earliest, Pending, Latest, } impl Display for Tag { fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { match self { Tag::Earliest => write!(f, "earliest"), Tag::Pending => write!(f, "pending"), Tag::Latest => write!(f, "latest"), } } } impl Default for Tag { fn default() -> Self { Tag::Latest } } /// The list sorting preference pub enum Sort { Asc, Desc, } impl Display for Sort { fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { match self { Sort::Asc => write!(f, "asc"), Sort::Desc => write!(f, "desc"), } } } /// Common optional arguments for the transaction or event list API endpoints pub struct TxListParams { start_block: u64, end_block: u64, page: u64, offset: u64, sort: Sort, } impl TxListParams { pub fn new(start_block: u64, end_block: u64, page: u64, offset: u64, sort: Sort) -> Self { Self { start_block, end_block, page, offset, sort } } } impl Default for TxListParams { fn default() -> Self { Self { start_block: 0, end_block: 99999999, page: 0, offset: 10000, sort: Sort::Asc } } } impl From for HashMap<&'static str, String> { fn from(tx_params: TxListParams) -> Self { let mut params = HashMap::new(); params.insert("startBlock", tx_params.start_block.to_string()); params.insert("endBlock", tx_params.end_block.to_string()); params.insert("page", tx_params.page.to_string()); params.insert("offset", tx_params.offset.to_string()); params.insert("sort", tx_params.sort.to_string()); params } } /// Options for querying internal transactions pub enum InternalTxQueryOption { ByAddress(Address), ByTransactionHash(H256), ByBlockRange, } /// Options for querying ERC20 or ERC721 token transfers pub enum TokenQueryOption { ByAddress(Address), ByContract(Address), ByAddressAndContract(Address, Address), } impl TokenQueryOption { pub fn into_params(self, list_params: TxListParams) -> HashMap<&'static str, String> { let mut params: HashMap<&'static str, String> = list_params.into(); match self { TokenQueryOption::ByAddress(address) => { params.insert("address", format!("{:?}", address)); params } TokenQueryOption::ByContract(contract) => { params.insert("contractaddress", format!("{:?}", contract)); params } TokenQueryOption::ByAddressAndContract(address, contract) => { params.insert("address", format!("{:?}", address)); params.insert("contractaddress", format!("{:?}", contract)); params } } } } /// The pre-defined block type for retrieving mined blocks pub enum BlockType { CanonicalBlocks, Uncles, } impl Display for BlockType { fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> { match self { BlockType::CanonicalBlocks => write!(f, "blocks"), BlockType::Uncles => write!(f, "uncles"), } } } impl Default for BlockType { fn default() -> Self { BlockType::CanonicalBlocks } } impl Client { /// Returns the Ether balance of a given address. /// /// ```no_run /// # use ethers_etherscan::Client; /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let balance = client /// .get_ether_balance_single(&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(), /// None).await.unwrap(); /// # } /// ``` pub async fn get_ether_balance_single( &self, address: &Address, tag: Option, ) -> Result { let tag_str = tag.unwrap_or_default().to_string(); let addr_str = format!("{:?}", address); let query = self.create_query( "account", "balance", HashMap::from([("address", &addr_str), ("tag", &tag_str)]), ); let response: Response = self.get_json(&query).await?; match response.status.as_str() { "0" => Err(EtherscanError::BalanceFailed), "1" => Ok(AccountBalance { account: *address, balance: response.result }), err => Err(EtherscanError::BadStatusCode(err.to_string())), } } /// Returns the balance of the accounts from a list of addresses. /// /// ```no_run /// # use ethers_etherscan::Client; /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let balances = client /// .get_ether_balance_multi(&vec![&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap()], /// None).await.unwrap(); /// # } /// ``` pub async fn get_ether_balance_multi( &self, addresses: &[&Address], tag: Option, ) -> Result> { let tag_str = tag.unwrap_or_default().to_string(); let addrs = addresses.iter().map(|x| format!("{:?}", x)).collect::>().join(","); let query: Query> = self.create_query( "account", "balancemulti", HashMap::from([("address", addrs.as_ref()), ("tag", tag_str.as_ref())]), ); let response: Response> = self.get_json(&query).await?; match response.status.as_str() { "0" => Err(EtherscanError::BalanceFailed), "1" => Ok(response.result), err => Err(EtherscanError::BadStatusCode(err.to_string())), } } /// Returns the list of transactions performed by an address, with optional pagination. /// /// ```no_run /// # use ethers_etherscan::Client; /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let txs = client /// .get_transactions(&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(), /// None).await.unwrap(); /// # } /// ``` pub async fn get_transactions( &self, address: &Address, params: Option, ) -> Result> { let mut tx_params: HashMap<&str, String> = params.unwrap_or_default().into(); tx_params.insert("address", format!("{:?}", address)); let query = self.create_query("account", "txlist", tx_params); let response: Response> = self.get_json(&query).await?; Ok(response.result) } /// Returns the list of internal transactions performed by an address or within a transaction, /// with optional pagination. /// /// ```no_run /// # use ethers_etherscan::{Client, account::InternalTxQueryOption}; /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let txs = client /// .get_internal_transactions( /// InternalTxQueryOption::ByAddress( /// "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3".parse().unwrap()), None).await.unwrap(); /// # } /// ``` pub async fn get_internal_transactions( &self, tx_query_option: InternalTxQueryOption, params: Option, ) -> Result> { let mut tx_params: HashMap<&str, String> = params.unwrap_or_default().into(); match tx_query_option { InternalTxQueryOption::ByAddress(address) => { tx_params.insert("address", format!("{:?}", address)); } InternalTxQueryOption::ByTransactionHash(tx_hash) => { tx_params.insert("txhash", format!("{:?}", tx_hash)); } _ => {} } let query = self.create_query("account", "txlistinternal", tx_params); let response: Response> = self.get_json(&query).await?; Ok(response.result) } /// Returns the list of ERC-20 tokens transferred by an address, with optional filtering by /// token contract. /// /// ```no_run /// # use ethers_etherscan::{Client, account::TokenQueryOption}; /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let txs = client /// .get_erc20_token_transfer_events( /// TokenQueryOption::ByAddress( /// "0x4e83362442b8d1bec281594cea3050c8eb01311c".parse().unwrap()), None).await.unwrap(); /// # } /// ``` pub async fn get_erc20_token_transfer_events( &self, event_query_option: TokenQueryOption, params: Option, ) -> Result> { let params = event_query_option.into_params(params.unwrap_or_default()); let query = self.create_query("account", "tokentx", params); let response: Response> = self.get_json(&query).await?; Ok(response.result) } /// Returns the list of ERC-721 ( NFT ) tokens transferred by an address, with optional /// filtering by token contract. /// /// ```no_run /// # use ethers_etherscan::{Client, account::TokenQueryOption}; /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let txs = client /// .get_erc721_token_transfer_events( /// TokenQueryOption::ByAddressAndContract( /// "0x6975be450864c02b4613023c2152ee0743572325".parse().unwrap(), /// "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse().unwrap(), /// ), None).await.unwrap(); /// # } /// ``` pub async fn get_erc721_token_transfer_events( &self, event_query_option: TokenQueryOption, params: Option, ) -> Result> { let params = event_query_option.into_params(params.unwrap_or_default()); let query = self.create_query("account", "tokennfttx", params); let response: Response> = self.get_json(&query).await?; Ok(response.result) } /// Returns the list of blocks mined by an address. /// /// ```no_run /// # use ethers_etherscan::Client; /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let blocks = client /// .get_mined_blocks(&"0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b".parse().unwrap(), None, None) /// .await.unwrap(); /// # } /// ``` pub async fn get_mined_blocks( &self, address: &Address, block_type: Option, page_and_offset: Option<(u64, u64)>, ) -> Result> { let mut params = HashMap::new(); params.insert("address", format!("{:?}", address)); params.insert("blocktype", block_type.unwrap_or_default().to_string()); if let Some((page, offset)) = page_and_offset { params.insert("page", page.to_string()); params.insert("offset", offset.to_string()); } let query = self.create_query("account", "getminedblocks", params); let response: Response> = self.get_json(&query).await?; Ok(response.result) } } #[cfg(test)] mod tests { use std::time::Duration; use serial_test::serial; use crate::{tests::run_at_least_duration, Chain}; use super::*; #[tokio::test] #[serial] async fn get_ether_balance_single_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let balance = client .get_ether_balance_single( &"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(), None, ) .await; assert!(balance.is_ok()); }) .await } #[tokio::test] #[serial] async fn get_ether_balance_multi_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let balances = client .get_ether_balance_multi( &[&"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap()], None, ) .await; assert!(balances.is_ok()); let balances = balances.unwrap(); assert!(balances.len() == 1); }) .await } #[tokio::test] #[serial] async fn get_transactions_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let txs = client .get_transactions( &"0x58eB28A67731c570Ef827C365c89B5751F9E6b0a".parse().unwrap(), None, ) .await; dbg!(&txs); assert!(txs.is_ok()); }) .await } #[tokio::test] #[serial] async fn get_internal_transactions_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let txs = client .get_internal_transactions( InternalTxQueryOption::ByAddress( "0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3".parse().unwrap(), ), None, ) .await; assert!(txs.is_ok()); }) .await } #[tokio::test] #[serial] async fn get_internal_transactions_by_tx_hash_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let txs = client .get_internal_transactions( InternalTxQueryOption::ByTransactionHash( "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170" .parse() .unwrap(), ), None, ) .await; assert!(txs.is_ok()); }) .await } #[tokio::test] #[serial] async fn get_erc20_transfer_events_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let txs = client .get_erc20_token_transfer_events( TokenQueryOption::ByAddress( "0x4e83362442b8d1bec281594cea3050c8eb01311c".parse().unwrap(), ), None, ) .await; assert!(txs.is_ok()); }) .await } #[tokio::test] #[serial] async fn get_erc721_transfer_events_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let txs = client .get_erc721_token_transfer_events( TokenQueryOption::ByAddressAndContract( "0x6975be450864c02b4613023c2152ee0743572325".parse().unwrap(), "0x06012c8cf97bead5deae237070f9587f8e7a266d".parse().unwrap(), ), None, ) .await; assert!(txs.is_ok()); }) .await } #[tokio::test] #[serial] async fn get_mined_blocks_success() { run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let blocks = client .get_mined_blocks( &"0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b".parse().unwrap(), None, None, ) .await; assert!(blocks.is_ok()); }) .await } }