diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 6ad48620..b1c44c8c 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -1,6 +1,6 @@ use ethers_core::{ abi::{Detokenize, Function, Token}, - types::{Address, BlockNumber, Bytes, NameOrAddress, TxHash, U256}, + types::{Address, BlockNumber, Bytes, Chain, NameOrAddress, TxHash, U256}, }; use ethers_providers::Middleware; @@ -17,34 +17,33 @@ use multicall_contract::MulticallContract; /// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding /// Multicall smart contract addresses as values pub static ADDRESS_BOOK: Lazy> = Lazy::new(|| { - let mut m = HashMap::new(); + fn decode_address(input: &str) -> Address { + Address::from_str(input).expect("Decoding failed") + } - // mainnet - let addr = - Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed"); - m.insert(U256::from(1u8), addr); - - // rinkeby - let addr = - Address::from_str("42ad527de7d4e9d9d011ac45b31d8551f8fe9821").expect("Decoding failed"); - m.insert(U256::from(4u8), addr); - - // goerli - let addr = - Address::from_str("77dca2c955b15e9de4dbbcf1246b4b85b651e50e").expect("Decoding failed"); - m.insert(U256::from(5u8), addr); - - // kovan - let addr = - Address::from_str("2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a").expect("Decoding failed"); - m.insert(U256::from(42u8), addr); - - // xdai - let addr = - Address::from_str("b5b692a88bdfc81ca69dcb1d924f59f0413a602a").expect("Decoding failed"); - m.insert(U256::from(100u8), addr); - - m + [ + ( + Chain::Mainnet.into(), + decode_address("eefba1e63905ef1d7acba5a8513c70307c1ce441"), + ), + ( + Chain::Rinkeby.into(), + decode_address("42ad527de7d4e9d9d011ac45b31d8551f8fe9821"), + ), + ( + Chain::Goerli.into(), + decode_address("77dca2c955b15e9de4dbbcf1246b4b85b651e50e"), + ), + ( + Chain::Kovan.into(), + decode_address("2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a"), + ), + ( + Chain::XDai.into(), + decode_address("b5b692a88bdfc81ca69dcb1d924f59f0413a602a"), + ), + ] + .into() }); /// A Multicall is an abstraction for sending batched calls/transactions to the Ethereum blockchain. diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs new file mode 100644 index 00000000..1b758de9 --- /dev/null +++ b/ethers-core/src/types/chain.rs @@ -0,0 +1,38 @@ +use std::fmt; + +use crate::types::U256; + +#[derive(Debug)] +pub enum Chain { + Mainnet, + Ropsten, + Rinkeby, + Goerli, + Kovan, + XDai, +} + +impl fmt::Display for Chain { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "{:?}", self) + } +} + +impl From for u32 { + fn from(chain: Chain) -> Self { + match chain { + Chain::Mainnet => 1, + Chain::Ropsten => 3, + Chain::Rinkeby => 4, + Chain::Goerli => 5, + Chain::Kovan => 42, + Chain::XDai => 100, + } + } +} + +impl From for U256 { + fn from(chain: Chain) -> Self { + u32::from(chain).into() + } +} diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs index da00054b..ed9f1999 100644 --- a/ethers-core/src/types/mod.rs +++ b/ethers-core/src/types/mod.rs @@ -48,5 +48,9 @@ pub use txpool::*; mod trace; pub use trace::*; +mod chain; +pub use chain::*; + mod proof; + pub use proof::*; diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs index 387b42db..c7d5795f 100644 --- a/ethers-etherscan/src/contract.rs +++ b/ethers-etherscan/src/contract.rs @@ -206,40 +206,42 @@ impl Client { &self, guid: impl AsRef, ) -> Result> { - let mut map = HashMap::new(); - map.insert("guid", guid.as_ref()); - let body = self.create_query("contract", "checkverifystatus", map); + let body = self.create_query( + "contract", + "checkverifystatus", + HashMap::from([("guid", guid.as_ref())]), + ); Ok(self.post_form(&body).await?) } /// Returns the contract ABI of a verified contract /// /// ```no_run - /// # use ethers_etherscan::{Chain, Client}; + /// # use ethers_etherscan::Client; + /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { - /// let client = Client::new(Chain::Mainnet, "API_KEY"); + /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let abi = client /// .contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) /// .await.unwrap(); /// # } /// ``` pub async fn contract_abi(&self, address: Address) -> Result { - let mut map = HashMap::new(); - map.insert("address", address); - let query = self.create_query("contract", "getabi", map); + let query = self.create_query("contract", "getabi", HashMap::from([("address", address)])); let resp: Response = self.get_json(&query).await?; Ok(serde_json::from_str(&resp.result)?) } /// Get Contract Source Code for Verified Contract Source Codes /// ```no_run - /// # use ethers_etherscan::{Chain, Client}; + /// # use ethers_etherscan::Client; + /// # use ethers_core::types::Chain; /// /// # #[tokio::main] /// # async fn main() { - /// let client = Client::new(Chain::Mainnet, "API_KEY"); + /// let client = Client::new(Chain::Mainnet, "API_KEY").unwrap(); /// let meta = client /// .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) /// .await.unwrap(); @@ -247,9 +249,11 @@ impl Client { /// # } /// ``` pub async fn contract_source_code(&self, address: Address) -> Result { - let mut map = HashMap::new(); - map.insert("address", address); - let query = self.create_query("contract", "getsourcecode", map); + let query = self.create_query( + "contract", + "getsourcecode", + HashMap::from([("address", address)]), + ); let response: Response> = self.get_json(&query).await?; Ok(ContractMetadata { items: response.result, @@ -259,8 +263,8 @@ impl Client { #[cfg(test)] mod tests { - use crate::contract::VerifyContract; - use crate::{Chain, Client}; + use crate::{contract::VerifyContract, Client}; + use ethers_core::types::Chain; #[tokio::test] #[ignore] diff --git a/ethers-etherscan/src/errors.rs b/ethers-etherscan/src/errors.rs index 1ffa9080..59db8874 100644 --- a/ethers-etherscan/src/errors.rs +++ b/ethers-etherscan/src/errors.rs @@ -1,7 +1,10 @@ +use ethers_core::types::Chain; use std::env::VarError; #[derive(Debug, thiserror::Error)] pub enum EtherscanError { + #[error("chain {0} not supported")] + ChainNotSupported(Chain), #[error("contract execution call failed: {0}")] ExecutionFailed(String), #[error("tx receipt failed")] diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs index eeb1ce55..98aa8da4 100644 --- a/ethers-etherscan/src/lib.rs +++ b/ethers-etherscan/src/lib.rs @@ -5,15 +5,15 @@ mod errors; mod transaction; use errors::EtherscanError; -use ethers_core::abi::Address; +use ethers_core::{abi::Address, types::Chain}; use reqwest::{header, Url}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{borrow::Cow, fmt}; +use std::borrow::Cow; pub type Result = std::result::Result; /// The Etherscan.io API client. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Client { /// Client that executes HTTP requests client: reqwest::Client, @@ -25,47 +25,37 @@ pub struct Client { etherscan_url: Url, } -#[derive(Debug)] -pub enum Chain { - Mainnet, - Ropsten, - Kovan, - Rinkeby, - Goerli, -} - -impl fmt::Display for Chain { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "{}", format!("{:?}", self).to_lowercase()) - } -} - impl Client { /// Create a new client with the correct endpoints based on the chain and provided API key - pub fn new(chain: Chain, api_key: impl Into) -> Self { + pub fn new(chain: Chain, api_key: impl Into) -> Result { let (etherscan_api_url, etherscan_url) = match chain { Chain::Mainnet => ( Url::parse("https://api.etherscan.io/api"), Url::parse("https://etherscan.io"), ), - Chain::Ropsten | Chain::Kovan | Chain::Rinkeby | Chain::Goerli => ( - Url::parse(&format!("https://api-{}.etherscan.io/api", chain)), - Url::parse(&format!("https://{}.etherscan.io", chain)), - ), + Chain::Ropsten | Chain::Kovan | Chain::Rinkeby | Chain::Goerli => { + let chain_name = chain.to_string().to_lowercase(); + + ( + Url::parse(&format!("https://api-{}.etherscan.io/api", chain_name)), + Url::parse(&format!("https://{}.etherscan.io", chain_name)), + ) + } + chain => return Err(EtherscanError::ChainNotSupported(chain)), }; - Self { + Ok(Self { client: Default::default(), api_key: api_key.into(), etherscan_api_url: etherscan_api_url.expect("is valid http"), etherscan_url: etherscan_url.expect("is valid http"), - } + }) } /// Create a new client with the correct endpoints based on the chain and API key /// from ETHERSCAN_API_KEY environment variable pub fn new_from_env(chain: Chain) -> Result { - Ok(Self::new(chain, std::env::var("ETHERSCAN_API_KEY")?)) + Self::new(chain, std::env::var("ETHERSCAN_API_KEY")?) } pub fn etherscan_api_url(&self) -> &Url { @@ -157,3 +147,17 @@ struct Query<'a, T: Serialize> { #[serde(flatten)] other: T, } + +#[cfg(test)] +mod tests { + use crate::{Client, EtherscanError}; + use ethers_core::types::Chain; + + #[test] + fn chain_not_supported() { + let err = Client::new_from_env(Chain::XDai).unwrap_err(); + + assert!(matches!(err, EtherscanError::ChainNotSupported(_))); + assert_eq!(err.to_string(), "chain XDai not supported"); + } +} diff --git a/ethers-etherscan/src/transaction.rs b/ethers-etherscan/src/transaction.rs index 8b6a104e..cd7c5b12 100644 --- a/ethers-etherscan/src/transaction.rs +++ b/ethers-etherscan/src/transaction.rs @@ -22,7 +22,11 @@ impl Client { let mut map = HashMap::new(); map.insert("txhash", tx_hash.as_ref()); - let query = self.create_query("transaction", "getstatus", map); + let query = self.create_query( + "transaction", + "getstatus", + HashMap::from([("txhash", tx_hash.as_ref())]), + ); let response: Response = self.get_json(&query).await?; if response.result.is_error == "0" { @@ -36,10 +40,11 @@ impl Client { /// Returns the status of a transaction execution: `false` for failed and `true` for successful pub async fn check_transaction_receipt_status(&self, tx_hash: impl AsRef) -> Result<()> { - let mut map = HashMap::new(); - map.insert("txhash", tx_hash.as_ref()); - - let query = self.create_query("transaction", "gettxreceiptstatus", map); + let query = self.create_query( + "transaction", + "gettxreceiptstatus", + HashMap::from([("txhash", tx_hash.as_ref())]), + ); let response: Response = self.get_json(&query).await?; match response.result.status.as_str() {