Repository: gakonst/ethers-rs
Pull request #1172 (2022-04-23 07:00:49)

fix(solc): adjust flatten format
<!--
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
<!--
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.
-->

Adjust flatten output format so that:
1. no newline at the start of the file, and
2. newline at the end of the last line.

## Solution
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

1. Move regex replace out of recursion (could have slight performance improvement).
2. `trim` and append `\n` at the end before returning result.
3. Adjust tests to reflext the new output formats.

## 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-core/src/types/log.rs b/ethers-core/src/types/log.rs
index dd70fa79..3815f158 100644
--- a/ethers-core/src/types/log.rs
+++ b/ethers-core/src/types/log.rs
@@ -9,7 +9,7 @@ use serde::{
 use std::ops::{Range, RangeFrom, RangeTo};
 
 /// A log produced by a transaction.
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
 pub struct Log {
     /// H160. the contract that emitted the log
     pub address: Address,
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/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs
index b25e962f..196f1d21 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -396,6 +396,7 @@ impl ExtraOutputValues {
 /// Determines what to emit as additional file
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
 pub struct ExtraOutputFiles {
+    pub abi: bool,
     pub metadata: bool,
     pub ir_optimized: bool,
     pub ewasm: bool,
@@ -420,6 +421,7 @@ impl ExtraOutputFiles {
     /// Returns an instance where all values are set to `true`
     pub fn all() -> Self {
         Self {
+            abi: true,
             metadata: true,
             ir_optimized: true,
             ewasm: true,
@@ -435,6 +437,9 @@ impl ExtraOutputFiles {
         let mut config = Self::default();
         for value in settings.into_iter() {
             match value {
+                ContractOutputSelection::Abi => {
+                    config.abi = true;
+                }
                 ContractOutputSelection::Metadata => {
                     config.metadata = true;
                 }
@@ -461,6 +466,14 @@ impl ExtraOutputFiles {
 
     /// Write the set values as separate files
     pub fn write_extras(&self, contract: &Contract, file: &Path) -> Result<(), SolcError> {
+        if self.abi {
+            if let Some(ref abi) = contract.abi {
+                let file = file.with_extension("abi.json");
+                fs::write(&file, serde_json::to_string_pretty(abi)?)
+                    .map_err(|err| SolcError::io(err, file))?
+            }
+        }
+
         if self.metadata {
             if let Some(ref metadata) = contract.metadata {
                 let file = file.with_extension("metadata.json");
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/config.rs b/ethers-solc/src/config.rs
index 3b67de83..261b5b38 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -250,7 +250,9 @@ impl ProjectPathsConfig {
     pub fn flatten(&self, target: &Path) -> Result<String> {
         tracing::trace!("flattening file");
         let graph = Graph::resolve(self)?;
-        self.flatten_node(target, &graph, &mut Default::default(), false, false, false)
+        self.flatten_node(target, &graph, &mut Default::default(), false, false, false).map(|x| {
+            format!("{}\n", utils::RE_THREE_OR_MORE_NEWLINES.replace_all(&x, "\n\n").trim())
+        })
     }
 
     /// Flattens a single node from the dependency graph
@@ -321,7 +323,6 @@ impl ProjectPathsConfig {
         let result = String::from_utf8(content).map_err(|err| {
             SolcError::msg(format!("failed to convert extended bytes to string: {}", err))
         })?;
-        let result = utils::RE_THREE_OR_MORE_NEWLINES.replace_all(&result, "\n\n").into_owned();
 
         Ok(result)
     }
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..2957c8cc 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;
@@ -490,8 +490,7 @@ contract C { }
 
     assert_eq!(
         result,
-        r#"
-pragma solidity ^0.8.10;
+        r#"pragma solidity ^0.8.10;
 
 contract C { }
 
@@ -547,8 +546,7 @@ contract C { }
 
     assert_eq!(
         result,
-        r#"
-pragma solidity ^0.8.10;
+        r#"pragma solidity ^0.8.10;
 pragma experimental ABIEncoderV2;
 
 contract C { }
@@ -656,7 +654,7 @@ contract C { }
 
     let result = project.flatten(&f).unwrap();
     assert_eq!(
-        result.trim(),
+        result,
         r#"pragma solidity ^0.8.10;
 
 contract C { }
@@ -664,7 +662,8 @@ contract C { }
 error IllegalArgument();
 error IllegalState();
 
-contract A { }"#
+contract A { }
+"#
     );
 }
 
@@ -707,7 +706,7 @@ contract C { }
 
     let result = project.flatten(&f).unwrap();
     assert_eq!(
-        result.trim(),
+        result,
         r#"pragma solidity ^0.8.10;
 
 contract C { }
@@ -716,7 +715,8 @@ contract C { }
 
 contract B { }
 
-contract A { }"#
+contract A { }
+"#
     );
 }
 
@@ -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