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() }
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SourceCodeMetadata {
Sources(HashMap<String, SourceCodeEntry>),
Metadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
language: Option<SourceCodeLanguage>,
#[serde(default)]
sources: HashMap<String, SourceCodeEntry>,
#[serde(default, skip_serializing_if = "Option::is_none")]
settings: Option<serde_json::Value>,
},
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::Sources(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::Sources(_) => None,
Self::SourceCode(_) => None,
}
}
pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
match self {
Self::Metadata { sources, .. } => sources.clone(),
Self::Sources(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::Sources(_) => 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::Sources(_) => None,
Self::SourceCode(_) => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Metadata {
#[serde(deserialize_with = "deserialize_source_code")]
pub source_code: SourceCodeMetadata,
#[serde(rename = "ABI")]
pub abi: String,
pub contract_name: String,
pub compiler_version: String,
#[serde(deserialize_with = "deserialize_stringified_u64")]
pub optimization_used: u64,
#[serde(deserialize_with = "deserialize_stringified_u64")]
pub runs: u64,
pub constructor_arguments: Bytes,
#[serde(rename = "EVMVersion")]
pub evm_version: String,
pub library: String,
pub license_type: String,
#[serde(deserialize_with = "deserialize_stringified_u64")]
pub proxy: u64,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_address_opt"
)]
pub implementation: Option<Address>,
pub swarm_source: String,
}
impl Metadata {
pub fn source_code(&self) -> String {
self.source_code.source_code()
}
pub fn language(&self) -> SourceCodeLanguage {
self.source_code.language().unwrap_or_else(|| {
if self.is_vyper() {
SourceCodeLanguage::Vyper
} else {
SourceCodeLanguage::Solidity
}
})
}
pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
self.source_code.sources()
}
pub fn raw_abi(&self) -> Result<RawAbi> {
Ok(serde_json::from_str(&self.abi)?)
}
pub fn abi(&self) -> Result<Abi> {
Ok(serde_json::from_str(&self.abi)?)
}
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),
}
}
pub fn is_vyper(&self) -> bool {
self.compiler_version.starts_with("vyper:")
}
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()
}
pub fn source_tree(&self) -> SourceTree {
SourceTree { entries: self.source_entries() }
}
#[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)
}
#[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))
}
#[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 {
pub fn abis(&self) -> Result<Vec<Abi>> {
self.items.iter().map(|c| c.abi()).collect()
}
pub fn raw_abis(&self) -> Result<Vec<RawAbi>> {
self.items.iter().map(|c| c.raw_abi()).collect()
}
pub fn source_code(&self) -> String {
self.items.iter().map(|c| c.source_code()).collect::<Vec<_>>().join("\n")
}
pub fn source_tree(&self) -> SourceTree {
SourceTree { entries: self.items.iter().flat_map(|item| item.source_entries()).collect() }
}
}
impl Client {
pub async fn contract_abi(&self, address: Address) -> Result<Abi> {
if let Some(ref cache) = self.cache {
if let Some(src) = cache.get_abi(address) {
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<Option<String>> = self.get_json(&query).await?;
let result = match resp.result {
Some(result) => result,
None => {
if resp.message.contains("Contract source code not verified") {
return Err(EtherscanError::ContractCodeNotVerified(address))
}
return Err(EtherscanError::EmptyResult {
message: resp.message,
status: resp.status,
})
}
};
if result.starts_with("Max rate limit reached") {
return Err(EtherscanError::RateLimitExceeded)
}
if result.starts_with("Contract source code not verified") ||
resp.message.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(&result)?;
if let Some(ref cache) = self.cache {
cache.set_abi(address, Some(&abi));
}
Ok(abi)
}
pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
if let Some(ref cache) = self.cache {
if let Some(src) = cache.get_source(address) {
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?;
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)
}
}