foundry_block_explorers/
utils.rs

1use crate::{contract::SourceCodeMetadata, units::Units, EtherscanError, Result};
2use alloy_primitives::{Address, ParseSignedError, I256, U256};
3use semver::Version;
4use serde::{Deserialize, Deserializer};
5use std::{fmt, str::FromStr};
6use thiserror::Error;
7
8static SOLC_BIN_LIST_URL: &str = "https://binaries.soliditylang.org/bin/list.txt";
9
10/// Given a Solc [Version], lookup the build metadata and return the full SemVer.
11/// e.g. `0.8.13` -> `0.8.13+commit.abaa5c0e`
12pub async fn lookup_compiler_version(version: &Version) -> Result<Version> {
13    let response = reqwest::get(SOLC_BIN_LIST_URL).await?.text().await?;
14    // Ignore extra metadata (`pre` or `build`)
15    let version = format!("{}.{}.{}", version.major, version.minor, version.patch);
16    let v = response
17        .lines()
18        .find(|l| !l.contains("nightly") && l.contains(&version))
19        .map(|l| l.trim_start_matches("soljson-v").trim_end_matches(".js"))
20        .ok_or_else(|| EtherscanError::MissingSolcVersion(version))?;
21
22    Ok(v.parse().expect("failed to parse semver"))
23}
24
25/// Return None if empty, otherwise parse as [Address].
26pub fn deserialize_address_opt<'de, D: Deserializer<'de>>(
27    deserializer: D,
28) -> std::result::Result<Option<Address>, D::Error> {
29    match Option::<String>::deserialize(deserializer)? {
30        None => Ok(None),
31        Some(s) => match s.is_empty() {
32            true => Ok(None),
33            _ => Ok(Some(s.parse().map_err(serde::de::Error::custom)?)),
34        },
35    }
36}
37
38/// Deserializes as JSON either:
39///
40/// - Object: `{ "SourceCode": { language: "Solidity", .. }, ..}`
41/// - Stringified JSON object:
42///     - `{ "SourceCode": "{{\r\n  \"language\": \"Solidity\", ..}}", ..}`
43///     - `{ "SourceCode": "{ \"file.sol\": \"...\" }", ... }`
44/// - Normal source code string: `{ "SourceCode": "// SPDX-License-Identifier: ...", .. }`
45pub fn deserialize_source_code<'de, D: Deserializer<'de>>(
46    deserializer: D,
47) -> std::result::Result<SourceCodeMetadata, D::Error> {
48    #[derive(Deserialize)]
49    #[serde(untagged)]
50    enum SourceCode {
51        String(String), // this must come first
52        Obj(SourceCodeMetadata),
53    }
54    let s = SourceCode::deserialize(deserializer)?;
55    match s {
56        SourceCode::String(s) => {
57            if s.starts_with('{') && s.ends_with('}') {
58                let mut s = s.as_str();
59                // skip double braces
60                if s.starts_with("{{") && s.ends_with("}}") {
61                    s = &s[1..s.len() - 1];
62                }
63                serde_json::from_str(s).map_err(serde::de::Error::custom)
64            } else {
65                Ok(SourceCodeMetadata::SourceCode(s))
66            }
67        }
68        SourceCode::Obj(obj) => Ok(obj),
69    }
70}
71
72/// This enum holds the numeric types that a possible to be returned by `parse_units` and
73/// that are taken by `format_units`.
74#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
75pub enum ParseUnits {
76    U256(U256),
77    I256(I256),
78}
79
80impl From<ParseUnits> for U256 {
81    fn from(n: ParseUnits) -> Self {
82        match n {
83            ParseUnits::U256(n) => n,
84            ParseUnits::I256(n) => n.into_raw(),
85        }
86    }
87}
88
89impl From<ParseUnits> for I256 {
90    fn from(n: ParseUnits) -> Self {
91        match n {
92            ParseUnits::I256(n) => n,
93            ParseUnits::U256(n) => I256::from_raw(n),
94        }
95    }
96}
97
98impl From<alloy_primitives::Signed<256, 4>> for ParseUnits {
99    fn from(n: alloy_primitives::Signed<256, 4>) -> Self {
100        Self::I256(n)
101    }
102}
103
104impl fmt::Display for ParseUnits {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            ParseUnits::U256(val) => val.fmt(f),
108            ParseUnits::I256(val) => val.fmt(f),
109        }
110    }
111}
112
113macro_rules! construct_format_units_from {
114    ($( $t:ty[$convert:ident] ),*) => {
115        $(
116            impl From<$t> for ParseUnits {
117                fn from(num: $t) -> Self {
118                    Self::$convert(U256::from(num))
119                }
120            }
121        )*
122    }
123}
124
125macro_rules! construct_signed_format_units_from {
126    ($( $t:ty[$convert:ident] ),*) => {
127        $(
128            impl From<$t> for ParseUnits {
129                fn from(num: $t) -> Self {
130                    Self::$convert(I256::from_raw(U256::from(num)))
131                }
132            }
133        )*
134    }
135}
136
137// Generate the From<T> code for the given numeric types below.
138construct_format_units_from! {
139    u8[U256], u16[U256], u32[U256], u64[U256], u128[U256], U256[U256], usize[U256]
140}
141
142construct_signed_format_units_from! {
143    i8[I256], i16[I256], i32[I256], i64[I256], i128[I256], isize[I256]
144}
145
146#[derive(Error, Debug)]
147pub enum ConversionError {
148    #[error("Unknown units: {0}")]
149    UnrecognizedUnits(String),
150    #[error("bytes32 strings must not exceed 32 bytes in length")]
151    TextTooLong,
152    #[error(transparent)]
153    Utf8Error(#[from] std::str::Utf8Error),
154    #[error(transparent)]
155    InvalidFloat(#[from] std::num::ParseFloatError),
156    #[error("Invalid decimal string: {0}")]
157    FromDecStrError(String),
158    #[error("Overflow parsing string")]
159    ParseOverflow,
160    #[error("Parse Signed Error")]
161    ParseI256Error(#[from] ParseSignedError),
162    #[error("Invalid address checksum")]
163    InvalidAddressChecksum,
164    #[error(transparent)]
165    FromHexError(<Address as std::str::FromStr>::Err),
166}
167
168/// Multiplies the provided amount with 10^{units} provided.
169pub fn parse_units<K, S>(amount: S, units: K) -> Result<ParseUnits, ConversionError>
170where
171    S: ToString,
172    K: TryInto<Units, Error = ConversionError> + Copy,
173{
174    let exponent: u32 = units.try_into()?.as_num();
175    let mut amount_str = amount.to_string().replace('_', "");
176    let negative = amount_str.chars().next().unwrap_or_default() == '-';
177    let dec_len = if let Some(di) = amount_str.find('.') {
178        amount_str.remove(di);
179        amount_str[di..].len() as u32
180    } else {
181        0
182    };
183
184    if dec_len > exponent {
185        // Truncate the decimal part if it is longer than the exponent
186        let amount_str = &amount_str[..(amount_str.len() - (dec_len - exponent) as usize)];
187        if negative {
188            // Edge case: We have removed the entire number and only the negative sign is left.
189            //            Return 0 as a I256 given the input was signed.
190            if amount_str == "-" {
191                Ok(ParseUnits::I256(I256::ZERO))
192            } else {
193                Ok(ParseUnits::I256(
194                    I256::from_dec_str(amount_str)
195                        .map_err(|e| ConversionError::FromDecStrError(e.to_string()))?,
196                ))
197            }
198        } else {
199            Ok(ParseUnits::U256(
200                U256::from_str(amount_str)
201                    .map_err(|e| ConversionError::FromDecStrError(e.to_string()))?,
202            ))
203        }
204    } else if negative {
205        // Edge case: Only a negative sign was given, return 0 as a I256 given the input was signed.
206        if amount_str == "-" {
207            Ok(ParseUnits::I256(I256::ZERO))
208        } else {
209            let _fi = U256::from(10_i64);
210            let mut n = I256::from_str(&amount_str)?;
211            n *= I256::from_raw(U256::from(10))
212                .checked_pow(U256::from(exponent) - U256::from(dec_len))
213                .ok_or(ConversionError::ParseOverflow)?;
214            Ok(ParseUnits::I256(n))
215        }
216    } else {
217        let mut a_uint = U256::from_str(&amount_str)
218            .map_err(|e| ConversionError::FromDecStrError(e.to_string()))?;
219        a_uint *= U256::from(10)
220            .checked_pow(U256::from(exponent - dec_len))
221            .ok_or(ConversionError::ParseOverflow)?;
222        Ok(ParseUnits::U256(a_uint))
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use crate::contract::SourceCodeLanguage;
230
231    #[test]
232    fn can_deserialize_address_opt() {
233        #[derive(serde::Serialize, Deserialize)]
234        struct Test {
235            #[serde(deserialize_with = "deserialize_address_opt")]
236            address: Option<Address>,
237        }
238
239        // https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413
240        let json = r#"{"address":""}"#;
241        let de: Test = serde_json::from_str(json).unwrap();
242        assert_eq!(de.address, None);
243
244        // Round-trip the above
245        let json = serde_json::to_string(&de).unwrap();
246        let de: Test = serde_json::from_str(&json).unwrap();
247        assert_eq!(de.address, None);
248
249        // https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xDef1C0ded9bec7F1a1670819833240f027b25EfF
250        let json = r#"{"address":"0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01"}"#;
251        let de: Test = serde_json::from_str(json).unwrap();
252        let expected = "0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01".parse().unwrap();
253        assert_eq!(de.address, Some(expected));
254    }
255
256    #[test]
257    fn can_deserialize_source_code() {
258        #[derive(Deserialize)]
259        struct Test {
260            #[serde(deserialize_with = "deserialize_source_code")]
261            source_code: SourceCodeMetadata,
262        }
263
264        let src = "source code text";
265
266        // Normal JSON
267        let json = r#"{
268            "source_code": { "language": "Solidity", "sources": { "Contract": { "content": "source code text" } } }
269        }"#;
270        let de: Test = serde_json::from_str(json).unwrap();
271        assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));
272        assert_eq!(de.source_code.sources().len(), 1);
273        assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
274        #[cfg(feature = "foundry-compilers")]
275        assert!(de.source_code.settings().unwrap().is_none());
276
277        // Stringified JSON
278        let json = r#"{
279            "source_code": "{{ \"language\": \"Solidity\", \"sources\": { \"Contract\": { \"content\": \"source code text\" } } }}"
280        }"#;
281        let de: Test = serde_json::from_str(json).unwrap();
282        assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));
283        assert_eq!(de.source_code.sources().len(), 1);
284        assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
285        #[cfg(feature = "foundry-compilers")]
286        assert!(de.source_code.settings().unwrap().is_none());
287
288        let json = r#"{"source_code": "source code text"}"#;
289        let de: Test = serde_json::from_str(json).unwrap();
290        assert_eq!(de.source_code.source_code(), src);
291    }
292}