From 071a41605ba2a9bffbf618921534ba5e0121f9d8 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Sun, 17 Oct 2021 13:01:20 +0300 Subject: [PATCH] feat(etherscan): transaction endpoints (#512) * feat(etherscan): transaction endpoints * bump ethers-etherscan version to 0.1.1 * feat: replace anyhow::Result with thiserror strong types Co-authored-by: Georgios Konstantopoulos --- Cargo.lock | 4 +- ethers-etherscan/Cargo.toml | 8 +- ethers-etherscan/src/contract.rs | 318 ++++++++++++++++++++ ethers-etherscan/src/errors.rs | 17 ++ ethers-etherscan/src/lib.rs | 431 +++++----------------------- ethers-etherscan/src/transaction.rs | 115 ++++++++ 6 files changed, 525 insertions(+), 368 deletions(-) create mode 100644 ethers-etherscan/src/contract.rs create mode 100644 ethers-etherscan/src/errors.rs create mode 100644 ethers-etherscan/src/transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 961a1f4f..9f7dea94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -965,13 +965,13 @@ dependencies = [ [[package]] name = "ethers-etherscan" -version = "0.1.0" +version = "0.1.1" dependencies = [ - "anyhow", "ethers-core", "reqwest", "serde", "serde_json", + "thiserror", "tokio", ] diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml index 53567f30..acc669de 100644 --- a/ethers-etherscan/Cargo.toml +++ b/ethers-etherscan/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ethers-etherscan" -version = "0.1.0" +version = "0.1.1" authors = ["Matthias Seitz ", "Georgios Konstantopoulos "] license = "MIT OR Apache-2.0" edition = "2018" @@ -9,7 +9,7 @@ documentation = "https://docs.rs/ethers" repository = "https://github.com/gakonst/ethers-rs" homepage = "https://docs.rs/ethers" description = """ -Rust API bindings for the etherscan.io web api +Rust API bindings for the etherscan.io web API """ keywords = ["ethereum", "web3", "etherscan", "ethers"] @@ -17,12 +17,12 @@ keywords = ["ethereum", "web3", "etherscan", "ethers"] ethers-core = { version = "^0.5.0", path = "../ethers-core", default-features = false } reqwest = { version = "0.11.5", features = ["json"] } serde = { version = "1.0.124", default-features = false, features = ["derive"] } -anyhow = "1.0.37" serde_json = { version = "1.0.64", default-features = false } +thiserror = "1.0.29" [dev-dependencies] tokio = { version = "1.5", features = ["macros", "rt-multi-thread"] } [package.metadata.docs.rs] all-features = true -rustdoc-args = ["--cfg", "docsrs"] \ No newline at end of file +rustdoc-args = ["--cfg", "docsrs"] diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs new file mode 100644 index 00000000..387b42db --- /dev/null +++ b/ethers-etherscan/src/contract.rs @@ -0,0 +1,318 @@ +use crate::{Client, Response, Result}; +use ethers_core::abi::{Abi, Address}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Arguments for verifying contracts +#[derive(Debug, Clone, Serialize)] +pub struct VerifyContract { + pub address: Address, + pub source: String, + #[serde(rename = "codeformat")] + pub code_format: CodeFormat, + /// if codeformat=solidity-standard-json-input, then expected as + /// `erc20.sol:erc20` + #[serde(rename = "contractname", skip_serializing_if = "Option::is_none")] + pub contract_name: Option, + #[serde(rename = "compilerversion")] + pub compiler_version: String, + /// applicable when codeformat=solidity-single-file + #[serde(rename = "optimizationUsed", skip_serializing_if = "Option::is_none")] + optimization_used: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub runs: Option, + /// NOTE: there is a typo in the etherscan API `constructorArguements` + #[serde( + rename = "constructorArguements", + skip_serializing_if = "Option::is_none" + )] + pub constructor_arguments: Option, + #[serde(rename = "evmversion")] + pub evm_version: Option, + #[serde(flatten)] + pub other: HashMap, +} + +impl VerifyContract { + pub fn new(address: Address, source: String, compiler_version: String) -> Self { + Self { + address, + source, + code_format: Default::default(), + contract_name: None, + compiler_version, + optimization_used: None, + runs: None, + constructor_arguments: None, + evm_version: None, + other: Default::default(), + } + } + + pub fn contract_name(mut self, name: impl Into) -> Self { + self.contract_name = Some(name.into()); + self + } + + pub fn runs(mut self, runs: u32) -> Self { + self.runs = Some(format!("{}", runs)); + self + } + + pub fn optimization(self, optimization: bool) -> Self { + if optimization { + self.optimized() + } else { + self.not_optimized() + } + } + + pub fn optimized(mut self) -> Self { + self.optimization_used = Some("1".to_string()); + self + } + + pub fn not_optimized(mut self) -> Self { + self.optimization_used = Some("0".to_string()); + self + } + + pub fn code_format(mut self, code_format: CodeFormat) -> Self { + self.code_format = code_format; + self + } + + pub fn evm_version(mut self, evm_version: impl Into) -> Self { + self.evm_version = Some(evm_version.into()); + self + } + + pub fn constructor_arguments( + mut self, + constructor_arguments: Option>, + ) -> Self { + self.constructor_arguments = constructor_arguments.map(|s| { + s.into() + // TODO is this correct? + .trim_start_matches("0x") + .to_string() + }); + self + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum CodeFormat { + #[serde(rename = "solidity-single-file")] + SingleFile, + #[serde(rename = "solidity-standard-json-input")] + StandardJsonInput, +} + +impl AsRef for CodeFormat { + fn as_ref(&self) -> &str { + match self { + CodeFormat::SingleFile => "solidity-single-file", + CodeFormat::StandardJsonInput => "solidity-standard-json-input", + } + } +} + +impl Default for CodeFormat { + fn default() -> Self { + CodeFormat::SingleFile + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ContractMetadata { + #[serde(flatten)] + 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 { + /// All ABI from all contracts in the source file + pub fn abis(&self) -> Result> { + let mut abis = Vec::with_capacity(self.items.len()); + for item in &self.items { + abis.push(serde_json::from_str(&item.abi)?); + } + Ok(abis) + } + + /// Combined source code of all contracts + pub fn source_code(&self) -> String { + self.items + .iter() + .map(|c| c.source_code.as_str()) + .collect::>() + .join("\n") + } +} + +/// Etherscan contract metadata +#[derive(Debug, Serialize, Deserialize)] +pub struct Metadata { + #[serde(rename = "SourceCode")] + pub source_code: String, + #[serde(rename = "ABI")] + pub abi: String, + #[serde(rename = "ContractName")] + pub contract_name: String, + #[serde(rename = "CompilerVersion")] + pub compiler_version: String, + #[serde(rename = "OptimizationUsed")] + pub optimization_used: String, + #[serde(rename = "Runs")] + pub runs: String, + #[serde(rename = "ConstructorArguments")] + pub constructor_arguments: String, + #[serde(rename = "EVMVersion")] + pub evm_version: String, + #[serde(rename = "Library")] + pub library: String, + #[serde(rename = "LicenseType")] + pub license_type: String, + #[serde(rename = "Proxy")] + pub proxy: String, + #[serde(rename = "Implementation")] + pub implementation: String, + #[serde(rename = "SwarmSource")] + pub swarm_source: String, +} + +impl Client { + /// Submit Source Code for Verification + pub async fn submit_contract_verification( + &self, + contract: &VerifyContract, + ) -> Result> { + let body = self.create_query("contract", "verifysourcecode", contract); + Ok(self.post_form(&body).await?) + } + + /// Check Source Code Verification Status with receipt received from + /// `[Self::submit_contract_verification]` + pub async fn check_contract_verification_status( + &self, + guid: impl AsRef, + ) -> Result> { + let mut map = HashMap::new(); + map.insert("guid", guid.as_ref()); + let body = self.create_query("contract", "checkverifystatus", map); + Ok(self.post_form(&body).await?) + } + + /// Returns the contract ABI of a verified contract + /// + /// ```no_run + /// # use ethers_etherscan::{Chain, Client}; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let client = Client::new(Chain::Mainnet, "API_KEY"); + /// 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 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}; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let client = Client::new(Chain::Mainnet, "API_KEY"); + /// 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 { + let mut map = HashMap::new(); + map.insert("address", address); + let query = self.create_query("contract", "getsourcecode", map); + let response: Response> = self.get_json(&query).await?; + Ok(ContractMetadata { + items: response.result, + }) + } +} + +#[cfg(test)] +mod tests { + use crate::contract::VerifyContract; + use crate::{Chain, Client}; + + #[tokio::test] + #[ignore] + async fn can_fetch_contract_abi() { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); + + let _abi = client + .contract_abi( + "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413" + .parse() + .unwrap(), + ) + .await + .unwrap(); + } + + #[tokio::test] + #[ignore] + async fn can_fetch_contract_source_code() { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); + + let _meta = client + .contract_source_code( + "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413" + .parse() + .unwrap(), + ) + .await + .unwrap(); + } + + #[tokio::test] + #[ignore] + async fn can_verify_contract() { + // TODO this needs further investigation + + // https://etherscan.io/address/0x9e744c9115b74834c0f33f4097f40c02a9ac5c33#code + let contract = include_str!("../resources/UniswapExchange.sol"); + let address = "0x9e744c9115b74834c0f33f4097f40c02a9ac5c33" + .parse() + .unwrap(); + let compiler_version = "v0.5.17+commit.d19bba13"; + let constructor_args = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000007596179537761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035941590000000000000000000000000000000000000000000000000000000000"; + + let client = Client::new_from_env(Chain::Mainnet).unwrap(); + + let contract = + VerifyContract::new(address, contract.to_string(), compiler_version.to_string()) + .constructor_arguments(Some(constructor_args)) + .optimization(true) + .runs(200); + + let _resp = client.submit_contract_verification(&contract).await; + } +} diff --git a/ethers-etherscan/src/errors.rs b/ethers-etherscan/src/errors.rs new file mode 100644 index 00000000..1ffa9080 --- /dev/null +++ b/ethers-etherscan/src/errors.rs @@ -0,0 +1,17 @@ +use std::env::VarError; + +#[derive(Debug, thiserror::Error)] +pub enum EtherscanError { + #[error("contract execution call failed: {0}")] + ExecutionFailed(String), + #[error("tx receipt failed")] + TransactionReceiptFailed, + #[error("bad status code {0}")] + BadStatusCode(String), + #[error(transparent)] + EnvVarNotFound(#[from] VarError), + #[error(transparent)] + Reqwest(#[from] reqwest::Error), + #[error(transparent)] + Serde(#[from] serde_json::Error), +} diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs index 86d45db8..eeb1ce55 100644 --- a/ethers-etherscan/src/lib.rs +++ b/ethers-etherscan/src/lib.rs @@ -1,55 +1,71 @@ //! Bindings for [etherscan.io web api](https://docs.etherscan.io/) -use ethers_core::abi::{Abi, Address}; +mod contract; +mod errors; +mod transaction; + +use errors::EtherscanError; +use ethers_core::abi::Address; use reqwest::{header, Url}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{borrow::Cow, collections::HashMap}; +use std::{borrow::Cow, fmt}; -/// The Etherscan.io api client. +pub type Result = std::result::Result; + +/// The Etherscan.io API client. #[derive(Clone)] pub struct Client { - /// The client that executes the http requests + /// Client that executes HTTP requests client: reqwest::Client, - /// The etherscan api key + /// Etherscan API key api_key: String, - /// API endpoint like https://api(-chain).etherscan.io/api + /// Etherscan API endpoint like https://api(-chain).etherscan.io/api etherscan_api_url: Url, - /// Base etherscan endpoint like https://etherscan.io/address + /// Etherscan base endpoint like https://etherscan.io 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. - /// - /// Supported chains are ethlive, mainnet,ropsten, kovan, rinkeby, goerli - // TODO make this an enum - pub fn new(chain: &str, api_key: impl Into) -> anyhow::Result { + /// 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 { let (etherscan_api_url, etherscan_url) = match chain { - "ethlive" | "mainnet" => { - ( - Url::parse("https://api.etherscan.io/api"), - Url::parse("https://etherscan.io/address"), - ) - }, - "ropsten"|"kovan"|"rinkeby"|"goerli" => { - ( - Url::parse(&format!("https://api-{}.etherscan.io/api", chain)), - Url::parse(&format!("https://{}.etherscan.io/address", chain)), - ) - } - s => { - return Err( - anyhow::anyhow!("Verification only works on mainnet, ropsten, kovan, rinkeby, and goerli, found `{}` chain", s) - ) - } + 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)), + ), }; - Ok(Self { + 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")?)) } pub fn etherscan_api_url(&self) -> &Url { @@ -60,16 +76,31 @@ impl Client { &self.etherscan_url } - /// Return the URL for the given address - pub fn address_url(&self, address: Address) -> String { - format!("{}/{}", self.etherscan_url, address) + /// Return the URL for the given block number + pub fn block_url(&self, block: u64) -> String { + format!("{}/block/{}", self.etherscan_url, block) } - /// Execute a api POST request with a form + /// Return the URL for the given address + pub fn address_url(&self, address: Address) -> String { + format!("{}/address/{}", self.etherscan_url, address) + } + + /// Return the URL for the given transaction hash + pub fn transaction_url(&self, tx_hash: impl AsRef) -> String { + format!("{}/tx/{}", self.etherscan_url, tx_hash.as_ref()) + } + + /// Return the URL for the given token hash + pub fn token_url(&self, token_hash: impl AsRef) -> String { + format!("{}/token/{}", self.etherscan_url, token_hash.as_ref()) + } + + /// Execute an API POST request with a form async fn post_form( &self, form: &Form, - ) -> anyhow::Result> { + ) -> Result> { Ok(self .client .post(self.etherscan_api_url.clone()) @@ -81,11 +112,8 @@ impl Client { .await?) } - /// Execute a api GET query - async fn get_json( - &self, - query: &Q, - ) -> anyhow::Result> { + /// Execute an API GET request with parameters + async fn get_json(&self, query: &Q) -> Result> { Ok(self .client .get(self.etherscan_api_url.clone()) @@ -110,74 +138,6 @@ impl Client { other, } } - - /// Submit Source Code for Verification - pub async fn submit_contract_verification( - &self, - contract: &VerifyContract, - ) -> anyhow::Result> { - let body = self.create_query("contract", "verifysourcecode", contract); - Ok(self.post_form(&body).await?) - } - - /// Check Source Code Verification Status with receipt received from - /// `[Self::submit_contract_verification]` - pub async fn check_verify_status( - &self, - guid: impl AsRef, - ) -> anyhow::Result> { - let mut map = HashMap::new(); - map.insert("guid", guid.as_ref()); - let body = self.create_query("contract", "checkverifystatus", map); - Ok(self.post_form(&body).await?) - } - - /// Returns the contract ABI of a verified contract - /// - /// ```no_run - /// # use ethers_etherscan::Client; - /// - /// # #[tokio::main] - /// # async fn main() -> anyhow::Result<()> { - /// let client = Client::new("mainnet", "API_KEY").unwrap(); - /// let abi = client - /// .contract_abi("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) - /// .await?; - /// - /// # Ok(()) - /// # } - /// ``` - pub async fn contract_abi(&self, address: Address) -> anyhow::Result { - let mut map = HashMap::new(); - map.insert("address", address); - let query = self.create_query("contract", "getabi", map); - 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::Client; - /// - /// # #[tokio::main] - /// # async fn main() -> anyhow::Result<()> { - /// let client = Client::new("mainnet", "API_KEY").unwrap(); - /// let meta = client - /// .contract_source_code("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse().unwrap()) - /// .await?; - /// let code = meta.source_code(); - /// # Ok(()) - /// # } - /// ``` - pub async fn contract_source_code(&self, address: Address) -> anyhow::Result { - let mut map = HashMap::new(); - map.insert("address", address); - let query = self.create_query("contract", "getsourcecode", map); - let response: Response> = self.get_json(&query).await?; - Ok(ContractMetadata { - items: response.result, - }) - } } /// The API response type @@ -197,256 +157,3 @@ struct Query<'a, T: Serialize> { #[serde(flatten)] other: T, } - -/// Arguments for verifying contracts -#[derive(Debug, Clone, Serialize)] -pub struct VerifyContract { - pub address: Address, - pub source: String, - #[serde(rename = "codeformat")] - pub code_format: CodeFormat, - /// if codeformat=solidity-standard-json-input, then expected as - /// `erc20.sol:erc20` - #[serde(rename = "contractname", skip_serializing_if = "Option::is_none")] - pub contract_name: Option, - #[serde(rename = "compilerversion")] - pub compiler_version: String, - /// applicable when codeformat=solidity-single-file - #[serde(rename = "optimizationUsed", skip_serializing_if = "Option::is_none")] - optimization_used: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub runs: Option, - /// NOTE: there is a typo in the etherscan API `constructorArguements` - #[serde( - rename = "constructorArguements", - skip_serializing_if = "Option::is_none" - )] - pub constructor_arguments: Option, - #[serde(rename = "evmversion")] - pub evm_version: Option, - #[serde(flatten)] - pub other: HashMap, -} - -impl VerifyContract { - pub fn new(address: Address, source: String, compilerversion: String) -> Self { - Self { - address, - source, - code_format: Default::default(), - contract_name: None, - compiler_version: compilerversion, - optimization_used: None, - runs: None, - constructor_arguments: None, - evm_version: None, - other: Default::default(), - } - } - - pub fn contract_name(mut self, name: impl Into) -> Self { - self.contract_name = Some(name.into()); - self - } - - pub fn runs(mut self, runs: u32) -> Self { - self.runs = Some(format!("{}", runs)); - self - } - - pub fn optimization(self, optimization: bool) -> Self { - if optimization { - self.optimized() - } else { - self.not_optimized() - } - } - - pub fn optimized(mut self) -> Self { - self.optimization_used = Some("1".to_string()); - self - } - - pub fn not_optimized(mut self) -> Self { - self.optimization_used = Some("0".to_string()); - self - } - - pub fn code_format(mut self, code_format: CodeFormat) -> Self { - self.code_format = code_format; - self - } - - pub fn evm_version(mut self, evm_version: impl Into) -> Self { - self.evm_version = Some(evm_version.into()); - self - } - - pub fn constructor_arguments( - mut self, - constructor_arguments: Option>, - ) -> Self { - self.constructor_arguments = constructor_arguments.map(|s| { - s.into() - // TODO is this correct? - .trim_start_matches("0x") - .to_string() - }); - self - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] -pub enum CodeFormat { - #[serde(rename = "solidity-single-file")] - SingleFile, - #[serde(rename = "solidity-standard-json-inpu")] - StandardJsonInput, -} - -impl AsRef for CodeFormat { - fn as_ref(&self) -> &str { - match self { - CodeFormat::SingleFile => "solidity-single-file", - CodeFormat::StandardJsonInput => "solidity-standard-json-input", - } - } -} - -impl Default for CodeFormat { - fn default() -> Self { - CodeFormat::SingleFile - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ContractMetadata { - #[serde(flatten)] - 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 { - /// All ABI from all contracts in the source file - pub fn abis(&self) -> anyhow::Result> { - let mut abis = Vec::with_capacity(self.items.len()); - for item in &self.items { - abis.push(serde_json::from_str(&item.abi)?); - } - Ok(abis) - } - - /// Combined source code of all contracts - pub fn source_code(&self) -> String { - self.items - .iter() - .map(|c| c.source_code.as_str()) - .collect::>() - .join("\n") - } -} - -/// Etherscan contract metadata -#[derive(Debug, Serialize, Deserialize)] -pub struct Metadata { - #[serde(rename = "SourceCode")] - pub source_code: String, - #[serde(rename = "ABI")] - pub abi: String, - #[serde(rename = "ContractName")] - pub contract_name: String, - #[serde(rename = "CompilerVersion")] - pub compiler_version: String, - #[serde(rename = "OptimizationUsed")] - pub optimization_used: String, - #[serde(rename = "Runs")] - pub runs: String, - #[serde(rename = "ConstructorArguments")] - pub constructor_arguments: String, - #[serde(rename = "EVMVersion")] - pub evm_version: String, - #[serde(rename = "Library")] - pub library: String, - #[serde(rename = "LicenseType")] - pub license_type: String, - #[serde(rename = "Proxy")] - pub proxy: String, - #[serde(rename = "Implementation")] - pub implementation: String, - #[serde(rename = "SwarmSource")] - pub swarm_source: String, -} - -#[cfg(test)] -mod tests { - use super::*; - - fn api_key() -> String { - std::env::var("ETHERSCAN_API_KEY").expect("ETHERSCAN_API_KEY not found") - } - - #[tokio::test] - #[ignore] - async fn can_fetch_contract_abi() { - let api = api_key(); - let client = Client::new("mainnet", api).unwrap(); - - let _abi = client - .contract_abi( - "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413" - .parse() - .unwrap(), - ) - .await - .unwrap(); - } - - #[tokio::test] - #[ignore] - async fn can_fetch_contract_source_code() { - let api = api_key(); - let client = Client::new("mainnet", api).unwrap(); - - let _meta = client - .contract_source_code( - "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413" - .parse() - .unwrap(), - ) - .await - .unwrap(); - } - - #[tokio::test] - #[ignore] - async fn can_verify_contract() { - // TODO this needs further investigation - - // https://etherscan.io/address/0x9e744c9115b74834c0f33f4097f40c02a9ac5c33#code - let contract = include_str!("../resources/UniswapExchange.sol"); - let address = "0x9e744c9115b74834c0f33f4097f40c02a9ac5c33" - .parse() - .unwrap(); - let compiler_version = "v0.5.17+commit.d19bba13"; - let constructor_args = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000007596179537761700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035941590000000000000000000000000000000000000000000000000000000000"; - - let api = api_key(); - let client = Client::new("mainnet", api).unwrap(); - - let contract = - VerifyContract::new(address, contract.to_string(), compiler_version.to_string()) - .constructor_arguments(Some(constructor_args)) - .optimization(true) - .runs(200); - - let _resp = client.submit_contract_verification(&contract).await; - } -} diff --git a/ethers-etherscan/src/transaction.rs b/ethers-etherscan/src/transaction.rs new file mode 100644 index 00000000..8b6a104e --- /dev/null +++ b/ethers-etherscan/src/transaction.rs @@ -0,0 +1,115 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +use crate::{Client, EtherscanError, Response, Result}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct ContractExecutionStatus { + is_error: String, + err_description: String, +} + +#[derive(Deserialize)] +struct TransactionReceiptStatus { + status: String, +} + +impl Client { + /// Returns the status of a contract execution + pub async fn check_contract_execution_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", "getstatus", map); + let response: Response = self.get_json(&query).await?; + + if response.result.is_error == "0" { + Ok(()) + } else { + Err(EtherscanError::ExecutionFailed( + response.result.err_description, + )) + } + } + + /// 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 response: Response = self.get_json(&query).await?; + + match response.result.status.as_str() { + "0" => Err(EtherscanError::TransactionReceiptFailed), + "1" => Ok(()), + err => Err(EtherscanError::BadStatusCode(err.to_string())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Chain; + + #[tokio::test] + async fn check_contract_execution_status_success() { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); + + let status = client + .check_contract_execution_status( + "0x16197e2a0eacc44c1ebdfddcfcfcafb3538de557c759a66e0ba95263b23d9007", + ) + .await; + + assert!(status.is_ok()); + } + + #[tokio::test] + async fn check_contract_execution_status_error() { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); + + let err = client + .check_contract_execution_status( + "0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a", + ) + .await + .unwrap_err(); + + assert!(matches!(err, EtherscanError::ExecutionFailed(_))); + assert_eq!( + err.to_string(), + "contract execution call failed: Bad jump destination" + ); + } + + #[tokio::test] + async fn check_transaction_receipt_status_success() { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); + + let success = client + .check_transaction_receipt_status( + "0x513c1ba0bebf66436b5fed86ab668452b7805593c05073eb2d51d3a52f480a76", + ) + .await; + + assert!(success.is_ok()); + } + + #[tokio::test] + async fn check_transaction_receipt_status_failed() { + let client = Client::new_from_env(Chain::Mainnet).unwrap(); + + let err = client + .check_transaction_receipt_status( + "0x21a29a497cb5d4bf514c0cca8d9235844bd0215c8fab8607217546a892fd0758", + ) + .await + .unwrap_err(); + + assert!(matches!(err, EtherscanError::TransactionReceiptFailed)); + } +}