use crate::{ source_tree::{SourceTree, SourceTreeEntry}, utils::{deserialize_address_opt, deserialize_stringified_source_code}, Client, EtherscanError, Response, Result, }; use ethers_core::{ abi::{Abi, Address, RawAbi}, types::{serde_helpers::deserialize_stringified_u64, Bytes}, }; use semver::Version; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::Path}; #[cfg(feature = "ethers-solc")] use ethers_solc::{artifacts::Settings, EvmVersion, Project, ProjectBuilder, SolcConfig}; #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub enum SourceCodeLanguage { #[default] Solidity, Vyper, } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SourceCodeEntry { pub content: String, } impl> From for SourceCodeEntry { fn from(s: T) -> Self { Self { content: s.into() } } } /// The contract metadata's SourceCode field. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum SourceCodeMetadata { /// Contains metadata and path mapped source code. Metadata { /// Programming language of the sources. #[serde(default, skip_serializing_if = "Option::is_none")] language: Option, /// Source path => source code #[serde(default)] sources: HashMap, /// Compiler settings, None if the language is not Solidity. #[serde(default, skip_serializing_if = "Option::is_none")] settings: Option, }, /// Contains only the source code. SourceCode(String), } impl SourceCodeMetadata { pub fn source_code(&self) -> String { match self { Self::Metadata { sources, .. } => { sources.values().map(|s| s.content.clone()).collect::>().join("\n") } Self::SourceCode(s) => s.clone(), } } pub fn language(&self) -> Option { match self { Self::Metadata { language, .. } => language.clone(), Self::SourceCode(_) => None, } } pub fn sources(&self) -> HashMap { match self { Self::Metadata { sources, .. } => sources.clone(), Self::SourceCode(s) => HashMap::from([("Contract".into(), s.into())]), } } #[cfg(feature = "ethers-solc")] pub fn settings(&self) -> Result> { match self { Self::Metadata { settings, .. } => match settings { Some(value) => { if value.is_null() { Ok(None) } else { Ok(Some(serde_json::from_value(value.to_owned())?)) } } None => Ok(None), }, Self::SourceCode(_) => Ok(None), } } #[cfg(not(feature = "ethers-solc"))] pub fn settings(&self) -> Option<&serde_json::Value> { match self { Self::Metadata { settings, .. } => settings.as_ref(), Self::SourceCode(_) => None, } } } /// Etherscan contract metadata. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] pub struct Metadata { /// Includes metadata for compiler settings and language. #[serde(deserialize_with = "deserialize_stringified_source_code")] pub source_code: SourceCodeMetadata, /// The ABI of the contract. #[serde(rename = "ABI")] pub abi: String, /// The name of the contract. pub contract_name: String, /// The version that this contract was compiled with. If it is a Vyper contract, it will start /// with "vyper:". pub compiler_version: String, /// Whether the optimizer was used. This value should only be 0 or 1. #[serde(deserialize_with = "deserialize_stringified_u64")] pub optimization_used: u64, /// The number of optimizations performed. #[serde(deserialize_with = "deserialize_stringified_u64")] pub runs: u64, /// The constructor arguments the contract was deployed with. pub constructor_arguments: Bytes, /// The version of the EVM the contract was deployed in. Can be either a variant of EvmVersion /// or "Default" which indicates the compiler's default. #[serde(rename = "EVMVersion")] pub evm_version: String, // ? pub library: String, /// The license of the contract. pub license_type: String, /// Whether this contract is a proxy. This value should only be 0 or 1. #[serde(deserialize_with = "deserialize_stringified_u64")] pub proxy: u64, /// If this contract is a proxy, the address of its implementation. #[serde(deserialize_with = "deserialize_address_opt")] pub implementation: Option
, /// The swarm source of the contract. pub swarm_source: String, } impl Metadata { /// Returns the contract's source code. pub fn source_code(&self) -> String { self.source_code.source_code() } /// Returns the contract's programming language. pub fn language(&self) -> SourceCodeLanguage { self.source_code.language().unwrap_or_else(|| { if self.is_vyper() { SourceCodeLanguage::Vyper } else { SourceCodeLanguage::Solidity } }) } /// Returns the contract's path mapped source code. pub fn sources(&self) -> HashMap { self.source_code.sources() } /// Parses the Abi String as an [RawAbi] struct. pub fn raw_abi(&self) -> Result { Ok(serde_json::from_str(&self.abi)?) } /// Parses the Abi String as an [Abi] struct. pub fn abi(&self) -> Result { Ok(serde_json::from_str(&self.abi)?) } /// Parses the compiler version. pub fn compiler_version(&self) -> Result { let v = &self.compiler_version; let v = v.strip_prefix("vyper:").unwrap_or(v); let v = v.strip_prefix('v').unwrap_or(v); match v.parse() { Err(e) => { let v = v.replace('a', "-alpha."); let v = v.replace('b', "-beta."); v.parse().map_err(|_| EtherscanError::Unknown(format!("bad compiler version: {e}"))) } Ok(v) => Ok(v), } } /// Returns whether this contract is a Vyper or a Solidity contract. pub fn is_vyper(&self) -> bool { self.compiler_version.starts_with("vyper:") } /// Maps this contract's sources to a [SourceTreeEntry] vector. pub fn source_entries(&self) -> Vec { let root = Path::new(&self.contract_name); self.sources() .into_iter() .map(|(path, entry)| { let path = root.join(path); SourceTreeEntry { path, contents: entry.content } }) .collect() } /// Returns the source tree of this contract's sources. pub fn source_tree(&self) -> SourceTree { SourceTree { entries: self.source_entries() } } /// Returns the contract's compiler settings. #[cfg(feature = "ethers-solc")] pub fn settings(&self) -> Result { let mut settings = self.source_code.settings()?.unwrap_or_default(); if self.optimization_used == 1 && !settings.optimizer.enabled.unwrap_or_default() { settings.optimizer.enable(); settings.optimizer.runs(self.runs as usize); } settings.evm_version = self.evm_version()?; Ok(settings) } /// Creates a Solc [ProjectBuilder] with this contract's settings. #[cfg(feature = "ethers-solc")] pub fn project_builder(&self) -> Result { let solc_config = SolcConfig::builder().settings(self.settings()?).build(); Ok(Project::builder().solc_config(solc_config)) } /// Parses the EVM version. #[cfg(feature = "ethers-solc")] pub fn evm_version(&self) -> Result> { match self.evm_version.as_str() { "" | "Default" => { Ok(EvmVersion::default().normalize_version(&self.compiler_version()?)) } _ => { let evm_version = self .evm_version .parse() .map_err(|e| EtherscanError::Unknown(format!("bad evm version: {e}")))?; Ok(Some(evm_version)) } } } } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(transparent)] pub struct ContractMetadata { pub items: Vec, } impl IntoIterator for ContractMetadata { type Item = Metadata; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.items.into_iter() } } impl ContractMetadata { /// Returns the ABI of all contracts. pub fn abis(&self) -> Result> { self.items.iter().map(|c| c.abi()).collect() } /// Returns the raw ABI of all contracts. pub fn raw_abis(&self) -> Result> { self.items.iter().map(|c| c.raw_abi()).collect() } /// Returns the combined source code of all contracts. pub fn source_code(&self) -> String { self.items.iter().map(|c| c.source_code()).collect::>().join("\n") } /// Returns the combined [SourceTree] of all contracts. pub fn source_tree(&self) -> SourceTree { SourceTree { entries: self.items.iter().flat_map(|item| item.source_entries()).collect() } } } impl Client { /// Fetches a verified contract's ABI. /// /// # Example /// /// ```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 abi = client /// .contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) /// .await.unwrap(); /// # } /// ``` pub async fn contract_abi(&self, address: Address) -> Result { // apply caching if let Some(ref cache) = self.cache { // If this is None, then we have a cache miss if let Some(src) = cache.get_abi(address) { // If this is None, then the contract is not verified return match src { Some(src) => Ok(src), None => Err(EtherscanError::ContractCodeNotVerified(address)), } } } let query = self.create_query("contract", "getabi", HashMap::from([("address", address)])); let resp: Response = self.get_json(&query).await?; if resp.result.starts_with("Max rate limit reached") { return Err(EtherscanError::RateLimitExceeded) } if resp.result.starts_with("Contract source code not verified") { if let Some(ref cache) = self.cache { cache.set_abi(address, None); } return Err(EtherscanError::ContractCodeNotVerified(address)) } let abi = serde_json::from_str(&resp.result)?; if let Some(ref cache) = self.cache { cache.set_abi(address, Some(&abi)); } Ok(abi) } /// Fetches a contract's verified source code and its metadata. /// /// # Example /// /// ```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 meta = client /// .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) /// .await.unwrap(); /// let code = meta.source_code(); /// # } /// ``` pub async fn contract_source_code(&self, address: Address) -> Result { // apply caching if let Some(ref cache) = self.cache { // If this is None, then we have a cache miss if let Some(src) = cache.get_source(address) { // If this is None, then the contract is not verified return match src { Some(src) => Ok(src), None => Err(EtherscanError::ContractCodeNotVerified(address)), } } } let query = self.create_query("contract", "getsourcecode", HashMap::from([("address", address)])); let response = self.get(&query).await?; // Source code is not verified if response.contains("Contract source code not verified") { if let Some(ref cache) = self.cache { cache.set_source(address, None); } return Err(EtherscanError::ContractCodeNotVerified(address)) } let response: Response = self.sanitize_response(response)?; let result = response.result; if let Some(ref cache) = self.cache { cache.set_source(address, Some(&result)); } Ok(result) } } #[cfg(test)] mod tests { use super::*; use crate::tests::run_at_least_duration; use ethers_core::types::Chain; use serial_test::serial; use std::time::Duration; /// Abi of [0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413](https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413). const DAO_ABI: &str = "[{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"proposals\",\"outputs\":[{\"name\":\"recipient\",\"type\":\"address\"},{\"name\":\"amount\",\"type\":\"uint256\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"votingDeadline\",\"type\":\"uint256\"},{\"name\":\"open\",\"type\":\"bool\"},{\"name\":\"proposalPassed\",\"type\":\"bool\"},{\"name\":\"proposalHash\",\"type\":\"bytes32\"},{\"name\":\"proposalDeposit\",\"type\":\"uint256\"},{\"name\":\"newCurator\",\"type\":\"bool\"},{\"name\":\"yea\",\"type\":\"uint256\"},{\"name\":\"nay\",\"type\":\"uint256\"},{\"name\":\"creator\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_spender\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"approve\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"minTokensToCreate\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"rewardAccount\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"daoCreator\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalSupply\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"divisor\",\"outputs\":[{\"name\":\"divisor\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"extraBalance\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"}],\"name\":\"executeProposal\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFrom\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"unblockMe\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"totalRewardToken\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"actualBalance\",\"outputs\":[{\"name\":\"_actualBalance\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"closingTime\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"allowedRecipients\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferWithoutReward\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"refund\",\"outputs\":[],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"},{\"name\":\"_description\",\"type\":\"string\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"},{\"name\":\"_debatingPeriod\",\"type\":\"uint256\"},{\"name\":\"_newCurator\",\"type\":\"bool\"}],\"name\":\"newProposal\",\"outputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"DAOpaidOut\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"minQuorumDivisor\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_newContract\",\"type\":\"address\"}],\"name\":\"newContract\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"}],\"name\":\"balanceOf\",\"outputs\":[{\"name\":\"balance\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_allowed\",\"type\":\"bool\"}],\"name\":\"changeAllowedRecipients\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"halveMinQuorum\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"paidOut\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_newCurator\",\"type\":\"address\"}],\"name\":\"splitDAO\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"DAOrewardAccount\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"proposalDeposit\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"numberOfProposals\",\"outputs\":[{\"name\":\"_numberOfProposals\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"lastTimeMinQuorumMet\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_toMembers\",\"type\":\"bool\"}],\"name\":\"retrieveDAOReward\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"receiveEther\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transfer\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"isFueled\",\"outputs\":[{\"name\":\"\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_tokenHolder\",\"type\":\"address\"}],\"name\":\"createTokenProxy\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"}],\"name\":\"getNewDAOAddress\",\"outputs\":[{\"name\":\"_newDAO\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_supportsProposal\",\"type\":\"bool\"}],\"name\":\"vote\",\"outputs\":[{\"name\":\"_voteID\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[],\"name\":\"getMyReward\",\"outputs\":[{\"name\":\"_success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"rewardToken\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_from\",\"type\":\"address\"},{\"name\":\"_to\",\"type\":\"address\"},{\"name\":\"_value\",\"type\":\"uint256\"}],\"name\":\"transferFromWithoutReward\",\"outputs\":[{\"name\":\"success\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_owner\",\"type\":\"address\"},{\"name\":\"_spender\",\"type\":\"address\"}],\"name\":\"allowance\",\"outputs\":[{\"name\":\"remaining\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":false,\"inputs\":[{\"name\":\"_proposalDeposit\",\"type\":\"uint256\"}],\"name\":\"changeProposalDeposit\",\"outputs\":[],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"\",\"type\":\"address\"}],\"name\":\"blocked\",\"outputs\":[{\"name\":\"\",\"type\":\"uint256\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"curator\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[{\"name\":\"_proposalID\",\"type\":\"uint256\"},{\"name\":\"_recipient\",\"type\":\"address\"},{\"name\":\"_amount\",\"type\":\"uint256\"},{\"name\":\"_transactionData\",\"type\":\"bytes\"}],\"name\":\"checkProposalCode\",\"outputs\":[{\"name\":\"_codeChecksOut\",\"type\":\"bool\"}],\"type\":\"function\"},{\"constant\":true,\"inputs\":[],\"name\":\"privateCreation\",\"outputs\":[{\"name\":\"\",\"type\":\"address\"}],\"type\":\"function\"},{\"inputs\":[{\"name\":\"_curator\",\"type\":\"address\"},{\"name\":\"_daoCreator\",\"type\":\"address\"},{\"name\":\"_proposalDeposit\",\"type\":\"uint256\"},{\"name\":\"_minTokensToCreate\",\"type\":\"uint256\"},{\"name\":\"_closingTime\",\"type\":\"uint256\"},{\"name\":\"_privateCreation\",\"type\":\"address\"}],\"type\":\"constructor\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_from\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"Transfer\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_owner\",\"type\":\"address\"},{\"indexed\":true,\"name\":\"_spender\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"Approval\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"FuelingToDate\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"CreatedToken\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"to\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Refund\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"newCurator\",\"type\":\"bool\"},{\"indexed\":false,\"name\":\"description\",\"type\":\"string\"}],\"name\":\"ProposalAdded\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"position\",\"type\":\"bool\"},{\"indexed\":true,\"name\":\"voter\",\"type\":\"address\"}],\"name\":\"Voted\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"proposalID\",\"type\":\"uint256\"},{\"indexed\":false,\"name\":\"result\",\"type\":\"bool\"},{\"indexed\":false,\"name\":\"quorum\",\"type\":\"uint256\"}],\"name\":\"ProposalTallied\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_newCurator\",\"type\":\"address\"}],\"name\":\"NewCurator\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"name\":\"_recipient\",\"type\":\"address\"},{\"indexed\":false,\"name\":\"_allowed\",\"type\":\"bool\"}],\"name\":\"AllowedRecipientChanged\",\"type\":\"event\"}]"; #[allow(unused)] fn init_tracing() { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); } #[tokio::test] #[serial] #[ignore] async fn can_fetch_ftm_contract_abi() { init_tracing(); run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Fantom).unwrap(); let _abi = client .contract_abi("0x80AA7cb0006d5DDD91cce684229Ac6e398864606".parse().unwrap()) .await .unwrap(); }) .await; } #[tokio::test] #[serial] #[ignore] async fn can_fetch_contract_abi() { init_tracing(); run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let abi = client .contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) .await .unwrap(); assert_eq!(abi, serde_json::from_str(DAO_ABI).unwrap()); }) .await; } #[tokio::test] #[serial] #[ignore] async fn can_fetch_contract_source_code() { init_tracing(); run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let meta = client .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) .await .unwrap(); assert_eq!(meta.items.len(), 1); let item = &meta.items[0]; assert!(matches!(item.source_code, SourceCodeMetadata::SourceCode(_))); assert_eq!(item.source_code.sources().len(), 1); assert_eq!(item.abi().unwrap(), serde_json::from_str(DAO_ABI).unwrap()); }) .await } #[tokio::test] #[serial] #[ignore] async fn can_get_error_on_unverified_contract() { init_tracing(); run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let addr = "0xb5c31a0e22cae98ac08233e512bd627885aa24e5".parse().unwrap(); let err = client.contract_source_code(addr).await.unwrap_err(); assert!(matches!(err, EtherscanError::ContractCodeNotVerified(_))); }) .await } /// Query a contract that has a single string source entry instead of underlying JSON metadata. #[tokio::test] #[serial] #[ignore] async fn can_fetch_contract_source_tree_for_singleton_contract() { init_tracing(); run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let meta = client .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) .await .unwrap(); assert_eq!(meta.items.len(), 1); let item = &meta.items[0]; assert!(matches!(item.source_code, SourceCodeMetadata::SourceCode(_))); assert_eq!(item.source_code.sources().len(), 1); assert_eq!(item.abi().unwrap(), serde_json::from_str(DAO_ABI).unwrap()); }) .await } /// Query a contract that has many source entries as JSON metadata and ensure they are /// reflected. #[tokio::test] #[serial] #[ignore] async fn can_fetch_contract_source_tree_for_multi_entry_contract() { init_tracing(); run_at_least_duration(Duration::from_millis(250), async { let client = Client::new_from_env(Chain::Mainnet).unwrap(); let meta = client .contract_source_code("0x8d04a8c79cEB0889Bdd12acdF3Fa9D207eD3Ff63".parse().unwrap()) .await .unwrap(); assert_eq!(meta.items.len(), 1); assert!(matches!(meta.items[0].source_code, SourceCodeMetadata::Metadata { .. })); let source_tree = meta.source_tree(); assert_eq!(source_tree.entries.len(), 15); }) .await } }