2022-09-29 15:07:50 +00:00
|
|
|
use crate::{contract::SourceCodeMetadata, EtherscanError, Result};
|
2022-09-29 18:15:04 +00:00
|
|
|
use ethers_core::types::Address;
|
2022-05-09 17:44:32 +00:00
|
|
|
use semver::Version;
|
2022-09-29 15:07:50 +00:00
|
|
|
use serde::{Deserialize, Deserializer};
|
2022-05-09 17:44:32 +00:00
|
|
|
|
|
|
|
static SOLC_BIN_LIST_URL: &str =
|
|
|
|
"https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/list.txt";
|
|
|
|
|
2022-09-29 15:07:50 +00:00
|
|
|
/// Given a Solc [Version], lookup the build metadata and return the full SemVer.
|
|
|
|
/// e.g. `0.8.13` -> `0.8.13+commit.abaa5c0e`
|
2022-05-09 17:44:32 +00:00
|
|
|
pub async fn lookup_compiler_version(version: &Version) -> Result<Version> {
|
|
|
|
let response = reqwest::get(SOLC_BIN_LIST_URL).await?.text().await?;
|
2022-09-29 15:07:50 +00:00
|
|
|
// Ignore extra metadata (`pre` or `build`)
|
|
|
|
let version = format!("{}.{}.{}", version.major, version.minor, version.patch);
|
2022-05-09 17:44:32 +00:00
|
|
|
let v = response
|
|
|
|
.lines()
|
|
|
|
.find(|l| !l.contains("nightly") && l.contains(&version))
|
2022-09-29 15:07:50 +00:00
|
|
|
.map(|l| l.trim_start_matches("soljson-v").trim_end_matches(".js"))
|
2022-10-11 17:49:05 +00:00
|
|
|
.ok_or_else(|| EtherscanError::MissingSolcVersion(version))?;
|
2022-05-09 17:44:32 +00:00
|
|
|
|
|
|
|
Ok(v.parse().expect("failed to parse semver"))
|
|
|
|
}
|
|
|
|
|
2022-09-29 15:07:50 +00:00
|
|
|
/// Return None if empty, otherwise parse as [Address].
|
|
|
|
pub fn deserialize_address_opt<'de, D: Deserializer<'de>>(
|
|
|
|
deserializer: D,
|
|
|
|
) -> std::result::Result<Option<Address>, D::Error> {
|
2023-01-22 21:08:43 +00:00
|
|
|
match Option::<String>::deserialize(deserializer)? {
|
|
|
|
None => Ok(None),
|
|
|
|
Some(s) => match s.is_empty() {
|
|
|
|
true => Ok(None),
|
|
|
|
_ => Ok(Some(s.parse().map_err(serde::de::Error::custom)?)),
|
|
|
|
},
|
2022-09-29 15:07:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Deserializes as JSON:
|
|
|
|
///
|
2022-12-22 18:58:35 +00:00
|
|
|
/// Object: `{ "SourceCode": { language: "Solidity", .. }, ..}`
|
2022-09-29 15:07:50 +00:00
|
|
|
///
|
|
|
|
/// or
|
|
|
|
///
|
2022-12-22 18:58:35 +00:00
|
|
|
/// Stringified JSON: `{ "SourceCode": "{{\r\n \"language\": \"Solidity\", ..}}", ..}`
|
|
|
|
///
|
|
|
|
/// or
|
|
|
|
///
|
|
|
|
/// Normal source code: `{ "SourceCode": "// SPDX-License-Identifier: ...", .. }`
|
|
|
|
pub fn deserialize_source_code<'de, D: Deserializer<'de>>(
|
2022-09-29 15:07:50 +00:00
|
|
|
deserializer: D,
|
|
|
|
) -> std::result::Result<SourceCodeMetadata, D::Error> {
|
2022-12-22 18:58:35 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[serde(untagged)]
|
|
|
|
enum SourceCode {
|
|
|
|
String(String), // this must come first
|
|
|
|
Obj(SourceCodeMetadata),
|
|
|
|
}
|
|
|
|
let s = SourceCode::deserialize(deserializer)?;
|
|
|
|
match s {
|
|
|
|
SourceCode::String(s) => {
|
|
|
|
if s.starts_with("{{") && s.ends_with("}}") {
|
|
|
|
let s = &s[1..s.len() - 1];
|
|
|
|
serde_json::from_str(s).map_err(serde::de::Error::custom)
|
|
|
|
} else {
|
|
|
|
Ok(SourceCodeMetadata::SourceCode(s))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
SourceCode::Obj(obj) => Ok(obj),
|
2022-09-29 15:07:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-09 17:44:32 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2022-09-29 15:07:50 +00:00
|
|
|
use crate::{contract::SourceCodeLanguage, tests::run_at_least_duration};
|
2022-05-09 17:44:32 +00:00
|
|
|
use semver::{BuildMetadata, Prerelease};
|
|
|
|
use serial_test::serial;
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[serial]
|
|
|
|
async fn can_lookup_compiler_version_build_metadata() {
|
|
|
|
run_at_least_duration(Duration::from_millis(250), async {
|
|
|
|
let v = Version::new(0, 8, 13);
|
|
|
|
let version = lookup_compiler_version(&v).await.unwrap();
|
|
|
|
assert_eq!(v.major, version.major);
|
|
|
|
assert_eq!(v.minor, version.minor);
|
|
|
|
assert_eq!(v.patch, version.patch);
|
|
|
|
assert_ne!(version.build, BuildMetadata::EMPTY);
|
|
|
|
assert_eq!(version.pre, Prerelease::EMPTY);
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[serial]
|
|
|
|
async fn errors_on_invalid_solc() {
|
|
|
|
run_at_least_duration(Duration::from_millis(250), async {
|
|
|
|
let v = Version::new(100, 0, 0);
|
|
|
|
let err = lookup_compiler_version(&v).await.unwrap_err();
|
|
|
|
assert!(matches!(err, EtherscanError::MissingSolcVersion(_)));
|
|
|
|
})
|
|
|
|
.await
|
|
|
|
}
|
2022-09-29 15:07:50 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn can_deserialize_address_opt() {
|
2023-01-22 21:08:43 +00:00
|
|
|
#[derive(serde::Serialize, Deserialize)]
|
2022-09-29 15:07:50 +00:00
|
|
|
struct Test {
|
|
|
|
#[serde(deserialize_with = "deserialize_address_opt")]
|
|
|
|
address: Option<Address>,
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413
|
|
|
|
let json = r#"{"address":""}"#;
|
|
|
|
let de: Test = serde_json::from_str(json).unwrap();
|
|
|
|
assert_eq!(de.address, None);
|
2023-01-22 21:08:43 +00:00
|
|
|
|
|
|
|
// Round-trip the above
|
|
|
|
let json = serde_json::to_string(&de).unwrap();
|
|
|
|
let de: Test = serde_json::from_str(&json).unwrap();
|
|
|
|
assert_eq!(de.address, None);
|
2022-09-29 15:07:50 +00:00
|
|
|
|
|
|
|
// https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xDef1C0ded9bec7F1a1670819833240f027b25EfF
|
|
|
|
let json = r#"{"address":"0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01"}"#;
|
|
|
|
let de: Test = serde_json::from_str(json).unwrap();
|
|
|
|
let expected = "0x4af649ffde640ceb34b1afaba3e0bb8e9698cb01".parse().unwrap();
|
|
|
|
assert_eq!(de.address, Some(expected));
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
2022-12-22 18:58:35 +00:00
|
|
|
fn can_deserialize_source_code() {
|
2022-09-29 15:07:50 +00:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Test {
|
2022-12-22 18:58:35 +00:00
|
|
|
#[serde(deserialize_with = "deserialize_source_code")]
|
2022-09-29 15:07:50 +00:00
|
|
|
source_code: SourceCodeMetadata,
|
|
|
|
}
|
|
|
|
|
|
|
|
let src = "source code text";
|
|
|
|
|
2022-12-22 18:58:35 +00:00
|
|
|
// Normal JSON
|
|
|
|
let json = r#"{
|
|
|
|
"source_code": { "language": "Solidity", "sources": { "Contract": { "content": "source code text" } } }
|
|
|
|
}"#;
|
|
|
|
let de: Test = serde_json::from_str(json).unwrap();
|
|
|
|
assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));
|
|
|
|
assert_eq!(de.source_code.sources().len(), 1);
|
|
|
|
assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
|
|
|
|
#[cfg(feature = "ethers-solc")]
|
|
|
|
assert!(matches!(de.source_code.settings().unwrap(), None));
|
|
|
|
|
|
|
|
// Stringified JSON
|
2022-09-29 15:07:50 +00:00
|
|
|
let json = r#"{
|
2022-12-22 18:58:35 +00:00
|
|
|
"source_code": "{{ \"language\": \"Solidity\", \"sources\": { \"Contract\": { \"content\": \"source code text\" } } }}"
|
2022-09-29 15:07:50 +00:00
|
|
|
}"#;
|
|
|
|
let de: Test = serde_json::from_str(json).unwrap();
|
|
|
|
assert!(matches!(de.source_code.language().unwrap(), SourceCodeLanguage::Solidity));
|
|
|
|
assert_eq!(de.source_code.sources().len(), 1);
|
|
|
|
assert_eq!(de.source_code.sources().get("Contract").unwrap().content, src);
|
|
|
|
#[cfg(feature = "ethers-solc")]
|
|
|
|
assert!(matches!(de.source_code.settings().unwrap(), None));
|
|
|
|
|
|
|
|
let json = r#"{"source_code": "source code text"}"#;
|
|
|
|
let de: Test = serde_json::from_str(json).unwrap();
|
|
|
|
assert_eq!(de.source_code.source_code(), src);
|
|
|
|
}
|
2022-05-09 17:44:32 +00:00
|
|
|
}
|