foundry_block_explorers/
contract.rs

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