ethers_etherscan/
utils.rs

1use crate::{contract::SourceCodeMetadata, EtherscanError, Result};
2use ethers_core::types::Address;
3use semver::Version;
4use serde::{Deserialize, Deserializer};
5
6static SOLC_BIN_LIST_URL: &str = "https://binaries.soliditylang.org/bin/list.txt";
7
8/// Given a Solc [Version], lookup the build metadata and return the full SemVer.
9/// e.g. `0.8.13` -> `0.8.13+commit.abaa5c0e`
10pub async fn lookup_compiler_version(version: &Version) -> Result<Version> {
11    let response = reqwest::get(SOLC_BIN_LIST_URL).await?.text().await?;
12    // Ignore extra metadata (`pre` or `build`)
13    let version = format!("{}.{}.{}", version.major, version.minor, version.patch);
14    let v = response
15        .lines()
16        .find(|l| !l.contains("nightly") && l.contains(&version))
17        .map(|l| l.trim_start_matches("soljson-v").trim_end_matches(".js"))
18        .ok_or_else(|| EtherscanError::MissingSolcVersion(version))?;
19
20    Ok(v.parse().expect("failed to parse semver"))
21}
22
23/// Return None if empty, otherwise parse as [Address].
24pub fn deserialize_address_opt<'de, D: Deserializer<'de>>(
25    deserializer: D,
26) -> std::result::Result<Option<Address>, D::Error> {
27    match Option::<String>::deserialize(deserializer)? {
28        None => Ok(None),
29        Some(s) => match s.is_empty() {
30            true => Ok(None),
31            _ => Ok(Some(s.parse().map_err(serde::de::Error::custom)?)),
32        },
33    }
34}
35
36/// Deserializes as JSON either:
37///
38/// - Object: `{ "SourceCode": { language: "Solidity", .. }, ..}`
39/// - Stringified JSON object:
40///     - `{ "SourceCode": "{{\r\n  \"language\": \"Solidity\", ..}}", ..}`
41///     - `{ "SourceCode": "{ \"file.sol\": \"...\" }", ... }`
42/// - Normal source code string: `{ "SourceCode": "// SPDX-License-Identifier: ...", .. }`
43pub fn deserialize_source_code<'de, D: Deserializer<'de>>(
44    deserializer: D,
45) -> std::result::Result<SourceCodeMetadata, D::Error> {
46    #[derive(Deserialize)]
47    #[serde(untagged)]
48    enum SourceCode {
49        String(String), // this must come first
50        Obj(SourceCodeMetadata),
51    }
52    let s = SourceCode::deserialize(deserializer)?;
53    match s {
54        SourceCode::String(s) => {
55            if s.starts_with('{') && s.ends_with('}') {
56                let mut s = s.as_str();
57                // skip double braces
58                if s.starts_with("{{") && s.ends_with("}}") {
59                    s = &s[1..s.len() - 1];
60                }
61                serde_json::from_str(s).map_err(serde::de::Error::custom)
62            } else {
63                Ok(SourceCodeMetadata::SourceCode(s))
64            }
65        }
66        SourceCode::Obj(obj) => Ok(obj),
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use crate::contract::SourceCodeLanguage;
74
75    #[test]
76    fn can_deserialize_address_opt() {
77        #[derive(serde::Serialize, Deserialize)]
78        struct Test {
79            #[serde(deserialize_with = "deserialize_address_opt")]
80            address: Option<Address>,
81        }
82
83        // https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413
84        let json = r#"{"address":""}"#;
85        let de: Test = serde_json::from_str(json).unwrap();
86        assert_eq!(de.address, None);
87
88        // Round-trip the above
89        let json = serde_json::to_string(&de).unwrap();
90        let de: Test = serde_json::from_str(&json).unwrap();
91        assert_eq!(de.address, None);
92
93        // https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xDef1C0ded9bec7F1a1670819833240f027b25EfF
94        let json = r#"{"address":"0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01"}"#;
95        let de: Test = serde_json::from_str(json).unwrap();
96        let expected = "0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01".parse().unwrap();
97        assert_eq!(de.address, Some(expected));
98    }
99
100    #[test]
101    fn can_deserialize_source_code() {
102        #[derive(Deserialize)]
103        struct Test {
104            #[serde(deserialize_with = "deserialize_source_code")]
105            source_code: SourceCodeMetadata,
106        }
107
108        let src = "source code text";
109
110        // Normal JSON
111        let json = r#"{
112            "source_code": { "language": "Solidity", "sources": { "Contract": { "content": "source code text" } } }
113        }"#;
114        let de: Test = serde_json::from_str(json).unwrap();
115        assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));
116        assert_eq!(de.source_code.sources().len(), 1);
117        assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
118        #[cfg(feature = "ethers-solc")]
119        assert!(de.source_code.settings().unwrap().is_none());
120
121        // Stringified JSON
122        let json = r#"{
123            "source_code": "{{ \"language\": \"Solidity\", \"sources\": { \"Contract\": { \"content\": \"source code text\" } } }}"
124        }"#;
125        let de: Test = serde_json::from_str(json).unwrap();
126        assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));
127        assert_eq!(de.source_code.sources().len(), 1);
128        assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
129        #[cfg(feature = "ethers-solc")]
130        assert!(de.source_code.settings().unwrap().is_none());
131
132        let json = r#"{"source_code": "source code text"}"#;
133        let de: Test = serde_json::from_str(json).unwrap();
134        assert_eq!(de.source_code.source_code(), src);
135    }
136}