ethers_etherscan/
contract.rs

1use crate::{
2    source_tree::{SourceTree, SourceTreeEntry},
3    utils::{deserialize_address_opt, deserialize_source_code},
4    Client, EtherscanError, Response, Result,
5};
6use ethers_core::{
7    abi::{Abi, Address, RawAbi},
8    types::{serde_helpers::deserialize_stringified_u64, Bytes},
9};
10use semver::Version;
11use serde::{Deserialize, Serialize};
12use std::{collections::HashMap, path::Path};
13
14#[cfg(feature = "ethers-solc")]
15use ethers_solc::{artifacts::Settings, EvmVersion, Project, ProjectBuilder, SolcConfig};
16
17#[derive(Clone, Debug, Default, Serialize, Deserialize)]
18pub enum SourceCodeLanguage {
19    #[default]
20    Solidity,
21    Vyper,
22}
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct SourceCodeEntry {
26    pub content: String,
27}
28
29impl<T: Into<String>> From<T> for SourceCodeEntry {
30    fn from(s: T) -> Self {
31        Self { content: s.into() }
32    }
33}
34
35/// The contract metadata's SourceCode field.
36#[derive(Clone, Debug, Serialize, Deserialize)]
37#[serde(untagged)]
38pub enum SourceCodeMetadata {
39    /// Contains just mapped source code.
40    // NOTE: this must come before `Metadata`
41    Sources(HashMap<String, SourceCodeEntry>),
42    /// Contains metadata and path mapped source code.
43    Metadata {
44        /// Programming language of the sources.
45        #[serde(default, skip_serializing_if = "Option::is_none")]
46        language: Option<SourceCodeLanguage>,
47        /// Source path => source code
48        #[serde(default)]
49        sources: HashMap<String, SourceCodeEntry>,
50        /// Compiler settings, None if the language is not Solidity.
51        #[serde(default, skip_serializing_if = "Option::is_none")]
52        settings: Option<serde_json::Value>,
53    },
54    /// Contains only the source code.
55    SourceCode(String),
56}
57
58impl SourceCodeMetadata {
59    pub fn source_code(&self) -> String {
60        match self {
61            Self::Metadata { sources, .. } => {
62                sources.values().map(|s| s.content.clone()).collect::<Vec<_>>().join("\n")
63            }
64            Self::Sources(sources) => {
65                sources.values().map(|s| s.content.clone()).collect::<Vec<_>>().join("\n")
66            }
67            Self::SourceCode(s) => s.clone(),
68        }
69    }
70
71    pub fn language(&self) -> Option<SourceCodeLanguage> {
72        match self {
73            Self::Metadata { language, .. } => language.clone(),
74            Self::Sources(_) => None,
75            Self::SourceCode(_) => None,
76        }
77    }
78
79    pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
80        match self {
81            Self::Metadata { sources, .. } => sources.clone(),
82            Self::Sources(sources) => sources.clone(),
83            Self::SourceCode(s) => HashMap::from([("Contract".into(), s.into())]),
84        }
85    }
86
87    #[cfg(feature = "ethers-solc")]
88    pub fn settings(&self) -> Result<Option<Settings>> {
89        match self {
90            Self::Metadata { settings, .. } => match settings {
91                Some(value) => {
92                    if value.is_null() {
93                        Ok(None)
94                    } else {
95                        Ok(Some(serde_json::from_value(value.to_owned())?))
96                    }
97                }
98                None => Ok(None),
99            },
100            Self::Sources(_) => Ok(None),
101            Self::SourceCode(_) => Ok(None),
102        }
103    }
104
105    #[cfg(not(feature = "ethers-solc"))]
106    pub fn settings(&self) -> Option<&serde_json::Value> {
107        match self {
108            Self::Metadata { settings, .. } => settings.as_ref(),
109            Self::Sources(_) => None,
110            Self::SourceCode(_) => None,
111        }
112    }
113}
114
115/// Etherscan contract metadata.
116#[derive(Clone, Debug, Serialize, Deserialize)]
117#[serde(rename_all = "PascalCase")]
118pub struct Metadata {
119    /// Includes metadata for compiler settings and language.
120    #[serde(deserialize_with = "deserialize_source_code")]
121    pub source_code: SourceCodeMetadata,
122
123    /// The ABI of the contract.
124    #[serde(rename = "ABI")]
125    pub abi: String,
126
127    /// The name of the contract.
128    pub contract_name: String,
129
130    /// The version that this contract was compiled with. If it is a Vyper contract, it will start
131    /// with "vyper:".
132    pub compiler_version: String,
133
134    /// Whether the optimizer was used. This value should only be 0 or 1.
135    #[serde(deserialize_with = "deserialize_stringified_u64")]
136    pub optimization_used: u64,
137
138    /// The number of optimizations performed.
139    #[serde(deserialize_with = "deserialize_stringified_u64")]
140    pub runs: u64,
141
142    /// The constructor arguments the contract was deployed with.
143    pub constructor_arguments: Bytes,
144
145    /// The version of the EVM the contract was deployed in. Can be either a variant of EvmVersion
146    /// or "Default" which indicates the compiler's default.
147    #[serde(rename = "EVMVersion")]
148    pub evm_version: String,
149
150    // ?
151    pub library: String,
152
153    /// The license of the contract.
154    pub license_type: String,
155
156    /// Whether this contract is a proxy. This value should only be 0 or 1.
157    #[serde(deserialize_with = "deserialize_stringified_u64")]
158    pub proxy: u64,
159
160    /// If this contract is a proxy, the address of its implementation.
161    #[serde(
162        default,
163        skip_serializing_if = "Option::is_none",
164        deserialize_with = "deserialize_address_opt"
165    )]
166    pub implementation: Option<Address>,
167
168    /// The swarm source of the contract.
169    pub swarm_source: String,
170}
171
172impl Metadata {
173    /// Returns the contract's source code.
174    pub fn source_code(&self) -> String {
175        self.source_code.source_code()
176    }
177
178    /// Returns the contract's programming language.
179    pub fn language(&self) -> SourceCodeLanguage {
180        self.source_code.language().unwrap_or_else(|| {
181            if self.is_vyper() {
182                SourceCodeLanguage::Vyper
183            } else {
184                SourceCodeLanguage::Solidity
185            }
186        })
187    }
188
189    /// Returns the contract's path mapped source code.
190    pub fn sources(&self) -> HashMap<String, SourceCodeEntry> {
191        self.source_code.sources()
192    }
193
194    /// Parses the Abi String as an [RawAbi] struct.
195    pub fn raw_abi(&self) -> Result<RawAbi> {
196        Ok(serde_json::from_str(&self.abi)?)
197    }
198
199    /// Parses the Abi String as an [Abi] struct.
200    pub fn abi(&self) -> Result<Abi> {
201        Ok(serde_json::from_str(&self.abi)?)
202    }
203
204    /// Parses the compiler version.
205    pub fn compiler_version(&self) -> Result<Version> {
206        let v = &self.compiler_version;
207        let v = v.strip_prefix("vyper:").unwrap_or(v);
208        let v = v.strip_prefix('v').unwrap_or(v);
209        match v.parse() {
210            Err(e) => {
211                let v = v.replace('a', "-alpha.");
212                let v = v.replace('b', "-beta.");
213                v.parse().map_err(|_| EtherscanError::Unknown(format!("bad compiler version: {e}")))
214            }
215            Ok(v) => Ok(v),
216        }
217    }
218
219    /// Returns whether this contract is a Vyper or a Solidity contract.
220    pub fn is_vyper(&self) -> bool {
221        self.compiler_version.starts_with("vyper:")
222    }
223
224    /// Maps this contract's sources to a [SourceTreeEntry] vector.
225    pub fn source_entries(&self) -> Vec<SourceTreeEntry> {
226        let root = Path::new(&self.contract_name);
227        self.sources()
228            .into_iter()
229            .map(|(path, entry)| {
230                let path = root.join(path);
231                SourceTreeEntry { path, contents: entry.content }
232            })
233            .collect()
234    }
235
236    /// Returns the source tree of this contract's sources.
237    pub fn source_tree(&self) -> SourceTree {
238        SourceTree { entries: self.source_entries() }
239    }
240
241    /// Returns the contract's compiler settings.
242    #[cfg(feature = "ethers-solc")]
243    pub fn settings(&self) -> Result<Settings> {
244        let mut settings = self.source_code.settings()?.unwrap_or_default();
245
246        if self.optimization_used == 1 && !settings.optimizer.enabled.unwrap_or_default() {
247            settings.optimizer.enable();
248            settings.optimizer.runs(self.runs as usize);
249        }
250
251        settings.evm_version = self.evm_version()?;
252
253        Ok(settings)
254    }
255
256    /// Creates a Solc [ProjectBuilder] with this contract's settings.
257    #[cfg(feature = "ethers-solc")]
258    pub fn project_builder(&self) -> Result<ProjectBuilder> {
259        let solc_config = SolcConfig::builder().settings(self.settings()?).build();
260
261        Ok(Project::builder().solc_config(solc_config))
262    }
263
264    /// Parses the EVM version.
265    #[cfg(feature = "ethers-solc")]
266    pub fn evm_version(&self) -> Result<Option<EvmVersion>> {
267        match self.evm_version.as_str() {
268            "" | "Default" => {
269                Ok(EvmVersion::default().normalize_version(&self.compiler_version()?))
270            }
271            _ => {
272                let evm_version = self
273                    .evm_version
274                    .parse()
275                    .map_err(|e| EtherscanError::Unknown(format!("bad evm version: {e}")))?;
276                Ok(Some(evm_version))
277            }
278        }
279    }
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize)]
283#[serde(transparent)]
284pub struct ContractMetadata {
285    pub items: Vec<Metadata>,
286}
287
288impl IntoIterator for ContractMetadata {
289    type Item = Metadata;
290    type IntoIter = std::vec::IntoIter<Metadata>;
291
292    fn into_iter(self) -> Self::IntoIter {
293        self.items.into_iter()
294    }
295}
296
297impl ContractMetadata {
298    /// Returns the ABI of all contracts.
299    pub fn abis(&self) -> Result<Vec<Abi>> {
300        self.items.iter().map(|c| c.abi()).collect()
301    }
302
303    /// Returns the raw ABI of all contracts.
304    pub fn raw_abis(&self) -> Result<Vec<RawAbi>> {
305        self.items.iter().map(|c| c.raw_abi()).collect()
306    }
307
308    /// Returns the combined source code of all contracts.
309    pub fn source_code(&self) -> String {
310        self.items.iter().map(|c| c.source_code()).collect::<Vec<_>>().join("\n")
311    }
312
313    /// Returns the combined [SourceTree] of all contracts.
314    pub fn source_tree(&self) -> SourceTree {
315        SourceTree { entries: self.items.iter().flat_map(|item| item.source_entries()).collect() }
316    }
317}
318
319impl Client {
320    /// Fetches a verified contract's ABI.
321    ///
322    /// # Example
323    ///
324    /// ```no_run
325    /// # async fn foo(client: ethers_etherscan::Client) -> Result<(), Box<dyn std::error::Error>> {
326    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
327    /// let abi = client.contract_abi(address).await?;
328    /// # Ok(()) }
329    /// ```
330    pub async fn contract_abi(&self, address: Address) -> Result<Abi> {
331        // apply caching
332        if let Some(ref cache) = self.cache {
333            // If this is None, then we have a cache miss
334            if let Some(src) = cache.get_abi(address) {
335                // If this is None, then the contract is not verified
336                return match src {
337                    Some(src) => Ok(src),
338                    None => Err(EtherscanError::ContractCodeNotVerified(address)),
339                }
340            }
341        }
342
343        let query = self.create_query("contract", "getabi", HashMap::from([("address", address)]));
344        let resp: Response<Option<String>> = self.get_json(&query).await?;
345
346        let result = match resp.result {
347            Some(result) => result,
348            None => {
349                if resp.message.contains("Contract source code not verified") {
350                    return Err(EtherscanError::ContractCodeNotVerified(address))
351                }
352                return Err(EtherscanError::EmptyResult {
353                    message: resp.message,
354                    status: resp.status,
355                })
356            }
357        };
358
359        if result.starts_with("Max rate limit reached") {
360            return Err(EtherscanError::RateLimitExceeded)
361        }
362
363        if result.starts_with("Contract source code not verified") ||
364            resp.message.starts_with("Contract source code not verified")
365        {
366            if let Some(ref cache) = self.cache {
367                cache.set_abi(address, None);
368            }
369            return Err(EtherscanError::ContractCodeNotVerified(address))
370        }
371        let abi = serde_json::from_str(&result)?;
372
373        if let Some(ref cache) = self.cache {
374            cache.set_abi(address, Some(&abi));
375        }
376
377        Ok(abi)
378    }
379
380    /// Fetches a contract's verified source code and its metadata.
381    ///
382    /// # Example
383    ///
384    /// ```no_run
385    /// # async fn foo(client: ethers_etherscan::Client) -> Result<(), Box<dyn std::error::Error>> {
386    /// let address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".parse()?;
387    /// let metadata = client.contract_source_code(address).await?;
388    /// assert_eq!(metadata.items[0].contract_name, "DAO");
389    /// # Ok(()) }
390    /// ```
391    pub async fn contract_source_code(&self, address: Address) -> Result<ContractMetadata> {
392        // apply caching
393        if let Some(ref cache) = self.cache {
394            // If this is None, then we have a cache miss
395            if let Some(src) = cache.get_source(address) {
396                // If this is None, then the contract is not verified
397                return match src {
398                    Some(src) => Ok(src),
399                    None => Err(EtherscanError::ContractCodeNotVerified(address)),
400                }
401            }
402        }
403
404        let query =
405            self.create_query("contract", "getsourcecode", HashMap::from([("address", address)]));
406        let response = self.get(&query).await?;
407
408        // Source code is not verified
409        if response.contains("Contract source code not verified") {
410            if let Some(ref cache) = self.cache {
411                cache.set_source(address, None);
412            }
413            return Err(EtherscanError::ContractCodeNotVerified(address))
414        }
415
416        let response: Response<ContractMetadata> = self.sanitize_response(response)?;
417        let result = response.result;
418
419        if let Some(ref cache) = self.cache {
420            cache.set_source(address, Some(&result));
421        }
422
423        Ok(result)
424    }
425}