ethers-rs/ethers-etherscan/src/contract.rs

409 lines
13 KiB
Rust

use crate::{
source_tree::{SourceTree, SourceTreeEntry},
utils::{deserialize_address_opt, deserialize_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<T: Into<String>> From<T> 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<SourceCodeLanguage>,
/// Source path => source code
#[serde(default)]
sources: HashMap<String, SourceCodeEntry>,
/// Compiler settings, None if the language is not Solidity.
#[serde(default, skip_serializing_if = "Option::is_none")]
settings: Option<serde_json::Value>,
},
/// 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::<Vec<_>>().join("\n")
}
Self::SourceCode(s) => s.clone(),
}
}
pub fn language(&self) -> Option<SourceCodeLanguage> {
match self {
Self::Metadata { language, .. } => language.clone(),
Self::SourceCode(_) => None,
}
}
pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
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<Option<Settings>> {
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_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(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_address_opt"
)]
pub implementation: Option<Address>,
/// 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<String, SourceCodeEntry> {
self.source_code.sources()
}
/// Parses the Abi String as an [RawAbi] struct.
pub fn raw_abi(&self) -> Result<RawAbi> {
Ok(serde_json::from_str(&self.abi)?)
}
/// Parses the Abi String as an [Abi] struct.
pub fn abi(&self) -> Result<Abi> {
Ok(serde_json::from_str(&self.abi)?)
}
/// Parses the compiler version.
pub fn compiler_version(&self) -> Result<Version> {
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<SourceTreeEntry> {
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<Settings> {
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<ProjectBuilder> {
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<Option<EvmVersion>> {
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<Metadata>,
}
impl IntoIterator for ContractMetadata {
type Item = Metadata;
type IntoIter = std::vec::IntoIter<Metadata>;
fn into_iter(self) -> Self::IntoIter {
self.items.into_iter()
}
}
impl ContractMetadata {
/// Returns the ABI of all contracts.
pub fn abis(&self) -> Result<Vec<Abi>> {
self.items.iter().map(|c| c.abi()).collect()
}
/// Returns the raw ABI of all contracts.
pub fn raw_abis(&self) -> Result<Vec<RawAbi>> {
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::<Vec<_>>().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<Abi> {
// 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<String> = 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;
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new(Chain::Mainnet, "<your_api_key>")?;
/// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
/// let metadata = client.contract_source_code(address).await?;
/// assert_eq!(metadata.items[0].contract_name, "DAO");
/// # Ok(())
/// # }
/// ```
pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
// 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<ContractMetadata> = self.sanitize_response(response)?;
let result = response.result;
if let Some(ref cache) = self.cache {
cache.set_source(address, Some(&result));
}
Ok(result)
}
}