Repository: gakonst/ethers-rs
Pull request #1169 (2022-04-22 16:42:26)

feat: add standardjson compiler input type
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
supersedes #1151

this also makes sure all paths use unix slashes `/` via `path-slash` crate Ref https://github.com/foundry-rs/foundry/issues/1382

sorry to hijack this @pyk 🙈
but I needed this for some verify debugs,

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index cd255bce..042edee8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1393,6 +1393,7 @@ dependencies = [
  "md-5 0.10.1",
  "num_cpus",
  "once_cell",
+ "path-slash",
  "pretty_assertions",
  "rand 0.8.5",
  "rayon",
@@ -2466,6 +2467,12 @@ dependencies = [
  "subtle",
 ]
 
+[[package]]
+name = "path-slash"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cacbb3c4ff353b534a67fb8d7524d00229da4cb1dc8c79f4db96e375ab5b619"
+
 [[package]]
 name = "pbkdf2"
 version = "0.8.0"
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index fe62ee72..3a9d53cd 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -38,6 +38,7 @@ dunce = "1.0.2"
 solang-parser = { default-features = false, version = "0.1.12" }
 rayon = "1.5.2"
 rand = { version = "0.8.5", optional = true }
+path-slash = "0.1.4"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 home = "0.5.3"
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 5cbe8e61..74aba89b 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -46,6 +46,8 @@ pub type VersionedSources = BTreeMap<Solc, (Version, Sources)>;
 /// A set of different Solc installations with their version and the sources to be compiled
 pub type VersionedFilteredSources = BTreeMap<Solc, (Version, FilteredSources)>;
 
+const SOLIDITY: &str = "Solidity";
+
 /// Input type `solc` expects
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct CompilerInput {
@@ -77,7 +79,7 @@ impl CompilerInput {
         let mut res = Vec::new();
         if !solidity_sources.is_empty() {
             res.push(Self {
-                language: "Solidity".to_string(),
+                language: SOLIDITY.to_string(),
                 sources: solidity_sources,
                 settings: Default::default(),
             });
@@ -178,6 +180,52 @@ impl CompilerInput {
     }
 }
 
+/// A `CompilerInput` representation used for verify
+///
+/// This type is an alternative `CompilerInput` but uses non-alphabetic ordering of the `sources`
+/// and instead emits the (Path -> Source) path in the same order as the pairs in the `sources`
+/// `Vec`. This is used over a map, so we can determine the order in which etherscan will display
+/// the verified contracts
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct StandardJsonCompilerInput {
+    pub language: String,
+    #[serde(with = "serde_helpers::tuple_vec_map")]
+    pub sources: Vec<(PathBuf, Source)>,
+    pub settings: Settings,
+}
+
+// === impl StandardJsonCompilerInput ===
+
+impl StandardJsonCompilerInput {
+    pub fn new(sources: Vec<(PathBuf, Source)>, settings: Settings) -> Self {
+        Self { language: SOLIDITY.to_string(), sources, settings }
+    }
+
+    /// Normalizes the EVM version used in the settings to be up to the latest one
+    /// supported by the provided compiler version.
+    #[must_use]
+    pub fn normalize_evm_version(mut self, version: &Version) -> Self {
+        if let Some(ref mut evm_version) = self.settings.evm_version {
+            self.settings.evm_version = evm_version.normalize_version(version);
+        }
+        self
+    }
+}
+
+impl From<StandardJsonCompilerInput> for CompilerInput {
+    fn from(input: StandardJsonCompilerInput) -> Self {
+        let StandardJsonCompilerInput { language, sources, settings } = input;
+        CompilerInput { language, sources: sources.into_iter().collect(), settings }
+    }
+}
+
+impl From<CompilerInput> for StandardJsonCompilerInput {
+    fn from(input: CompilerInput) -> Self {
+        let CompilerInput { language, sources, settings } = input;
+        StandardJsonCompilerInput { language, sources: sources.into_iter().collect(), settings }
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct Settings {
@@ -1486,9 +1534,29 @@ mod tests {
 
         for path in fs::read_dir(dir).unwrap() {
             let path = path.unwrap().path();
-            let compiler_output = fs::read_to_string(&path).unwrap();
-            serde_json::from_str::<CompilerInput>(&compiler_output).unwrap_or_else(|err| {
-                panic!("Failed to read compiler output of {} {}", path.display(), err)
+            let compiler_input = fs::read_to_string(&path).unwrap();
+            serde_json::from_str::<CompilerInput>(&compiler_input).unwrap_or_else(|err| {
+                panic!("Failed to read compiler input of {} {}", path.display(), err)
+            });
+        }
+    }
+
+    #[test]
+    fn can_parse_standard_json_compiler_input() {
+        let mut dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+        dir.push("test-data/in");
+
+        for path in fs::read_dir(dir).unwrap() {
+            let path = path.unwrap().path();
+            let compiler_input = fs::read_to_string(&path).unwrap();
+            let val = serde_json::from_str::<StandardJsonCompilerInput>(&compiler_input)
+                .unwrap_or_else(|err| {
+                    panic!("Failed to read compiler output of {} {}", path.display(), err)
+                });
+
+            let pretty = serde_json::to_string_pretty(&val).unwrap();
+            serde_json::from_str::<CompilerInput>(&pretty).unwrap_or_else(|err| {
+                panic!("Failed to read converted compiler input of {} {}", path.display(), err)
             });
         }
     }
diff --git a/ethers-solc/src/artifacts/serde_helpers.rs b/ethers-solc/src/artifacts/serde_helpers.rs
index b44a1384..77338ae7 100644
--- a/ethers-solc/src/artifacts/serde_helpers.rs
+++ b/ethers-solc/src/artifacts/serde_helpers.rs
@@ -127,3 +127,71 @@ pub mod display_from_str_opt {
         }
     }
 }
+
+/// (De)serialize vec of tuples as map
+pub mod tuple_vec_map {
+    use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize, Serializer};
+
+    pub fn serialize<K, V, S>(data: &[(K, V)], serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+        K: Serialize,
+        V: Serialize,
+    {
+        serializer.collect_map(data.iter().map(|x| (&x.0, &x.1)))
+    }
+
+    pub fn deserialize<'de, K, V, D>(deserializer: D) -> Result<Vec<(K, V)>, D::Error>
+    where
+        D: Deserializer<'de>,
+        K: DeserializeOwned,
+        V: DeserializeOwned,
+    {
+        use serde::de::{MapAccess, Visitor};
+        use std::{fmt, marker::PhantomData};
+
+        struct TupleVecMapVisitor<K, V> {
+            marker: PhantomData<Vec<(K, V)>>,
+        }
+
+        impl<K, V> TupleVecMapVisitor<K, V> {
+            pub fn new() -> Self {
+                TupleVecMapVisitor { marker: PhantomData }
+            }
+        }
+
+        impl<'de, K, V> Visitor<'de> for TupleVecMapVisitor<K, V>
+        where
+            K: Deserialize<'de>,
+            V: Deserialize<'de>,
+        {
+            type Value = Vec<(K, V)>;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("a map")
+            }
+
+            #[inline]
+            fn visit_unit<E>(self) -> Result<Vec<(K, V)>, E> {
+                Ok(Vec::new())
+            }
+
+            #[inline]
+            fn visit_map<T>(self, mut access: T) -> Result<Vec<(K, V)>, T::Error>
+            where
+                T: MapAccess<'de>,
+            {
+                let mut values =
+                    Vec::with_capacity(std::cmp::min(access.size_hint().unwrap_or(0), 4096));
+
+                while let Some((key, value)) = access.next_entry()? {
+                    values.push((key, value));
+                }
+
+                Ok(values)
+            }
+        }
+
+        deserializer.deserialize_map(TupleVecMapVisitor::new())
+    }
+}
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index 0865ad95..dd879d6b 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -24,7 +24,7 @@ mod config;
 pub use config::{AllowedLibPaths, PathStyle, ProjectPathsConfig, SolcConfig};
 
 pub mod remappings;
-use crate::artifacts::{Source, SourceFile};
+use crate::artifacts::{Source, SourceFile, StandardJsonCompilerInput};
 
 pub mod error;
 mod filter;
@@ -428,7 +428,12 @@ impl<T: ArtifactOutput> Project<T> {
     }
 
     /// Returns standard-json-input to compile the target contract
-    pub fn standard_json_input(&self, target: impl AsRef<Path>) -> Result<CompilerInput> {
+    pub fn standard_json_input(
+        &self,
+        target: impl AsRef<Path>,
+    ) -> Result<StandardJsonCompilerInput> {
+        use path_slash::PathExt;
+
         let target = target.as_ref();
         tracing::trace!("Building standard-json-input for {:?}", target);
         let graph = Graph::resolve(&self.paths)?;
@@ -442,12 +447,22 @@ impl<T: ArtifactOutput> Project<T> {
             graph.all_imported_nodes(*target_index).map(|index| graph.node(index).unpack()),
         );
 
-        let compiler_inputs = CompilerInput::with_sources(
-            sources.into_iter().map(|(s, p)| (s.clone(), p.clone())).collect(),
-        );
-
+        let root = self.root();
+        let sources = sources
+            .into_iter()
+            .map(|(path, source)| {
+                let path: PathBuf = if let Ok(stripped) = path.strip_prefix(root) {
+                    stripped.to_slash_lossy().into()
+                } else {
+                    path.to_slash_lossy().into()
+                };
+                (path, source.clone())
+            })
+            .collect();
+
+        let mut settings = self.solc_config.settings.clone();
         // strip the path to the project root from all remappings
-        let remappings = self
+        settings.remappings = self
             .paths
             .remappings
             .clone()
@@ -455,15 +470,9 @@ impl<T: ArtifactOutput> Project<T> {
             .map(|r| r.into_relative(self.root()).to_relative_remapping())
             .collect::<Vec<_>>();
 
-        let compiler_input = compiler_inputs
-            .first()
-            .ok_or_else(|| SolcError::msg("cannot get the compiler input"))?
-            .clone()
-            .settings(self.solc_config.settings.clone())
-            .with_remappings(remappings)
-            .strip_prefix(self.root());
+        let input = StandardJsonCompilerInput::new(sources, settings);
 
-        Ok(compiler_input)
+        Ok(input)
     }
 }
 


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 2b360c9e..e53efe22 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -12,7 +12,7 @@ use ethers_solc::{
     cache::{SolFilesCache, SOLIDITY_FILES_CACHE_FILENAME},
     project_util::*,
     remappings::Remapping,
-    ConfigurableArtifacts, ExtraOutputValues, Graph, Project, ProjectCompileOutput,
+    CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project, ProjectCompileOutput,
     ProjectPathsConfig, Solc, TestFileFilter,
 };
 use pretty_assertions::assert_eq;
@@ -1023,11 +1023,11 @@ fn can_sanitize_bytecode_hash() {
 fn can_compile_std_json_input() {
     let tmp = TempProject::dapptools_init().unwrap();
     tmp.assert_no_errors();
-    let source =
-        tmp.list_source_files().into_iter().filter(|p| p.ends_with("Dapp.t.sol")).next().unwrap();
+    let source = tmp.list_source_files().into_iter().find(|p| p.ends_with("Dapp.t.sol")).unwrap();
     let input = tmp.project().standard_json_input(source).unwrap();
 
     assert!(input.settings.remappings.contains(&"ds-test/=lib/ds-test/src/".parse().unwrap()));
+    let input: CompilerInput = input.into();
     assert!(input.sources.contains_key(Path::new("lib/ds-test/src/test.sol")));
 
     // should be installed