diff --git a/Cargo.lock b/Cargo.lock index 3d5770f6e..d2e7d6818 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1270,6 +1270,7 @@ dependencies = [ "ethers-core", "ethers-solc", "reqwest", + "semver", "serde", "serde-aux", "serde_json", diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml index 008a69b77..0687567dc 100644 --- a/ethers-etherscan/Cargo.toml +++ b/ethers-etherscan/Cargo.toml @@ -22,6 +22,7 @@ serde_json = { version = "1.0.64", default-features = false } serde-aux = { version = "3.0.1", default-features = false } thiserror = "1.0.31" tracing = "0.1.34" +semver = "1.0.9" [dev-dependencies] tempfile = "3.3.0" diff --git a/ethers-etherscan/src/errors.rs b/ethers-etherscan/src/errors.rs index c92e43e1e..c6780ab42 100644 --- a/ethers-etherscan/src/errors.rs +++ b/ethers-etherscan/src/errors.rs @@ -33,4 +33,6 @@ pub enum EtherscanError { Unknown(String), #[error("Missing field: {0}")] Builder(String), + #[error("Missing solc version: {0}")] + MissingSolcVersion(String), } diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs index 1515a8b27..053c160cd 100644 --- a/ethers-etherscan/src/lib.rs +++ b/ethers-etherscan/src/lib.rs @@ -24,6 +24,7 @@ pub mod errors; pub mod gas; pub mod source_tree; pub mod transaction; +pub mod utils; pub(crate) type Result = std::result::Result; diff --git a/ethers-etherscan/src/utils.rs b/ethers-etherscan/src/utils.rs new file mode 100644 index 000000000..dc78f8b98 --- /dev/null +++ b/ethers-etherscan/src/utils.rs @@ -0,0 +1,56 @@ +use semver::Version; + +use crate::{EtherscanError, Result}; + +static SOLC_BIN_LIST_URL: &str = + "https://raw.githubusercontent.com/ethereum/solc-bin/gh-pages/bin/list.txt"; + +/// Given the compiler version lookup the build metadata +/// and return full semver +/// i.e. `0.8.13` -> `0.8.13+commit.abaa5c0e` +pub async fn lookup_compiler_version(version: &Version) -> Result { + let response = reqwest::get(SOLC_BIN_LIST_URL).await?.text().await?; + let version = format!("{}", version); + let v = response + .lines() + .find(|l| !l.contains("nightly") && l.contains(&version)) + .map(|l| l.trim_start_matches("soljson-v").trim_end_matches(".js").to_owned()) + .ok_or(EtherscanError::MissingSolcVersion(version))?; + + Ok(v.parse().expect("failed to parse semver")) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::run_at_least_duration; + 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 + } +}