Repository: gakonst/ethers-rs
Pull request #1165 (2022-04-21 21:22:16)

feat: function debug data in extra output
<!--
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

https://github.com/foundry-rs/foundry/issues/1381

<!--
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

Add it

Also, it seems like the AST is always present and is not configurable - it's not present in `OutputSelection` and is enabled by default.

`ExtraOutputValues::compact_format` also seems to be unused?



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index cd255bce..34500ddb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1277,6 +1277,8 @@ dependencies = [
  "tempfile",
  "thiserror",
  "tokio",
+ "tracing",
+ "tracing-subscriber",
 ]
 
 [[package]]
@@ -1393,6 +1395,7 @@ dependencies = [
  "md-5 0.10.1",
  "num_cpus",
  "once_cell",
+ "path-slash",
  "pretty_assertions",
  "rand 0.8.5",
  "rayon",
@@ -2466,6 +2469,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-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml
index 354ff64b..2d9e7985 100644
--- a/ethers-etherscan/Cargo.toml
+++ b/ethers-etherscan/Cargo.toml
@@ -21,11 +21,13 @@ serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
 serde-aux = { version = "3.0.1", default-features = false }
 thiserror = "1.0.29"
+tracing = "0.1.34"
 
 [dev-dependencies]
 tempfile = "3.3.0"
 tokio = { version = "1.5", features = ["macros", "rt-multi-thread", "time"] }
 serial_test = "0.6.0"
+tracing-subscriber = { version = "0.3", default-features = false, features = ["env-filter", "fmt"] }
 
 [package.metadata.docs.rs]
 all-features = true
diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs
index 7ccb5d42..a2d497d9 100644
--- a/ethers-etherscan/src/contract.rs
+++ b/ethers-etherscan/src/contract.rs
@@ -364,14 +364,34 @@ impl Client {
 
 #[cfg(test)]
 mod tests {
+    use crate::{contract::VerifyContract, tests::run_at_least_duration, Client, EtherscanError};
+    use ethers_core::types::Chain;
+    use ethers_solc::{Project, ProjectPathsConfig};
+    use serial_test::serial;
     use std::{path::PathBuf, time::Duration};
 
-    use serial_test::serial;
+    #[allow(unused)]
+    fn init_tracing() {
+        tracing_subscriber::fmt()
+            .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
+            .init();
+    }
 
-    use ethers_core::types::Chain;
-    use ethers_solc::{Project, ProjectPathsConfig};
+    #[tokio::test]
+    #[serial]
+    #[ignore]
+    async fn can_fetch_ftm_contract_abi() {
+        init_tracing();
+        run_at_least_duration(Duration::from_millis(250), async {
+            let client = Client::new_from_env(Chain::Fantom).unwrap();
 
-    use crate::{contract::VerifyContract, tests::run_at_least_duration, Client, EtherscanError};
+            let _abi = client
+                .contract_abi("0x80AA7cb0006d5DDD91cce684229Ac6e398864606".parse().unwrap())
+                .await
+                .unwrap();
+        })
+        .await;
+    }
 
     #[tokio::test]
     #[serial]
diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs
index 2a6213eb..e5b6f1a1 100644
--- a/ethers-etherscan/src/lib.rs
+++ b/ethers-etherscan/src/lib.rs
@@ -10,6 +10,7 @@ use std::{
 use contract::ContractMetadata;
 use reqwest::{header, Url};
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use tracing::trace;
 
 use errors::EtherscanError;
 use ethers_core::{
@@ -166,10 +167,10 @@ impl Client {
                 Url::parse("https://kovan-optimistic.etherscan.io"),
             ),
             Chain::Fantom => {
-                (Url::parse("https://api.ftmscan.com"), Url::parse("https://ftmscan.com"))
+                (Url::parse("https://api.ftmscan.com/api"), Url::parse("https://ftmscan.com"))
             }
             Chain::FantomTestnet => (
-                Url::parse("https://api-testnet.ftmscan.com"),
+                Url::parse("https://api-testnet.ftmscan.com/api"),
                 Url::parse("https://testnet.ftmscan.com"),
             ),
             Chain::BinanceSmartChain => {
@@ -215,19 +216,20 @@ impl Client {
             Chain::Goerli |
             Chain::Optimism |
             Chain::OptimismKovan |
-            Chain::Fantom |
-            Chain::FantomTestnet |
             Chain::BinanceSmartChain |
             Chain::BinanceSmartChainTestnet |
             Chain::Arbitrum |
             Chain::ArbitrumTestnet |
             Chain::Cronos => std::env::var("ETHERSCAN_API_KEY")?,
+            Chain::Fantom | Chain::FantomTestnet => {
+                std::env::var("FTMSCAN_API_KEY").or_else(|_| std::env::var("FANTOMSCAN_API_KEY"))?
+            }
 
             Chain::XDai | Chain::Sepolia | Chain::CronosTestnet => String::default(),
             Chain::Moonbeam | Chain::MoonbeamDev | Chain::Moonriver => {
                 std::env::var("MOONSCAN_API_KEY")?
             }
-            Chain::Dev => return Err(errors::EtherscanError::LocalNetworksNotSupported),
+            Chain::Dev => return Err(EtherscanError::LocalNetworksNotSupported),
         };
         Self::new(chain, api_key)
     }
@@ -265,6 +267,7 @@ impl Client {
         &self,
         form: &Form,
     ) -> Result<Response<T>> {
+        trace!(target: "etherscan", "POST FORM {}", self.etherscan_api_url);
         Ok(self
             .client
             .post(self.etherscan_api_url.clone())
@@ -278,6 +281,7 @@ impl Client {
 
     /// Execute an API GET request with parameters
     async fn get_json<T: DeserializeOwned, Q: Serialize>(&self, query: &Q) -> Result<Response<T>> {
+        trace!(target: "etherscan", "GET JSON {}", self.etherscan_api_url);
         let res: ResponseData<T> = self
             .client
             .get(self.etherscan_api_url.clone())
@@ -342,15 +346,13 @@ struct Query<'a, T: Serialize> {
 
 #[cfg(test)]
 mod tests {
+    use crate::{Client, EtherscanError};
+    use ethers_core::types::{Address, Chain, H256};
     use std::{
         future::Future,
         time::{Duration, SystemTime},
     };
 
-    use ethers_core::types::{Address, Chain, H256};
-
-    use crate::{Client, EtherscanError};
-
     #[test]
     fn chain_not_supported() {
         let err = Client::new_from_env(Chain::XDai).unwrap_err();
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..7acd788f 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -4,9 +4,12 @@ use crate::{
     artifacts::{
         bytecode::{CompactBytecode, CompactDeployedBytecode},
         contract::{CompactContract, CompactContractBytecode, Contract},
-        output_selection::{ContractOutputSelection, EvmOutputSelection, EwasmOutputSelection},
-        CompactContractBytecodeCow, CompactEvm, DevDoc, Ewasm, GasEstimates, LosslessAbi, Metadata,
-        Offsets, Settings, StorageLayout, UserDoc,
+        output_selection::{
+            BytecodeOutputSelection, ContractOutputSelection, EvmOutputSelection,
+            EwasmOutputSelection,
+        },
+        CompactContractBytecodeCow, DevDoc, Evm, Ewasm, FunctionDebugData, GasEstimates,
+        LosslessAbi, Metadata, Offsets, Settings, StorageLayout, UserDoc,
     },
     ArtifactOutput, SolcConfig, SolcError, SourceFile,
 };
@@ -32,6 +35,8 @@ pub struct ConfigurableContractArtifact {
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub method_identifiers: Option<BTreeMap<String, String>>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub function_debug_data: Option<BTreeMap<String, FunctionDebugData>>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub gas_estimates: Option<GasEstimates>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub metadata: Option<Metadata>,
@@ -154,6 +159,7 @@ impl ConfigurableArtifacts {
     /// Returns the output selection corresponding to this configuration
     pub fn output_selection(&self) -> Vec<ContractOutputSelection> {
         let mut selection = ContractOutputSelection::basic();
+
         if self.additional_values.ir {
             selection.push(ContractOutputSelection::Ir);
         }
@@ -181,6 +187,12 @@ impl ConfigurableArtifacts {
         if self.additional_values.ewasm || self.additional_files.ewasm {
             selection.push(EwasmOutputSelection::All.into());
         }
+        if self.additional_values.function_debug_data {
+            selection.push(BytecodeOutputSelection::FunctionDebugData.into());
+        }
+        if self.additional_values.method_identifiers {
+            selection.push(EvmOutputSelection::MethodIdentifiers.into());
+        }
         selection
     }
 }
@@ -208,6 +220,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
         let mut artifact_bytecode = None;
         let mut artifact_deployed_bytecode = None;
         let mut artifact_gas_estimates = None;
+        let mut artifact_function_debug_data = None;
         let mut artifact_method_identifiers = None;
         let mut artifact_assembly = None;
         let mut artifact_storage_layout = None;
@@ -247,17 +260,22 @@ impl ArtifactOutput for ConfigurableArtifacts {
         }
 
         if let Some(evm) = evm {
-            let CompactEvm {
+            let Evm {
                 assembly,
                 bytecode,
                 deployed_bytecode,
                 method_identifiers,
                 gas_estimates,
                 ..
-            } = evm.into_compact();
+            } = evm;
+
+            if self.additional_values.function_debug_data {
+                artifact_function_debug_data =
+                    bytecode.as_ref().map(|b| b.function_debug_data.clone());
+            }
 
-            artifact_bytecode = bytecode;
-            artifact_deployed_bytecode = deployed_bytecode;
+            artifact_bytecode = bytecode.map(Into::into);
+            artifact_deployed_bytecode = deployed_bytecode.map(Into::into);
 
             if self.additional_values.gas_estimates {
                 artifact_gas_estimates = gas_estimates;
@@ -275,6 +293,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
             bytecode: artifact_bytecode,
             deployed_bytecode: artifact_deployed_bytecode,
             assembly: artifact_assembly,
+            function_debug_data: artifact_function_debug_data,
             method_identifiers: artifact_method_identifiers,
             gas_estimates: artifact_gas_estimates,
             metadata: artifact_metadata,
@@ -304,6 +323,7 @@ pub struct ExtraOutputValues {
     pub ir: bool,
     pub ir_optimized: bool,
     pub ewasm: bool,
+    pub function_debug_data: bool,
 
     /// PRIVATE: This structure may grow, As such, constructing this structure should
     /// _always_ be done using a public constructor or update syntax:
@@ -336,6 +356,7 @@ impl ExtraOutputValues {
             ir: true,
             ir_optimized: true,
             ewasm: true,
+            function_debug_data: true,
             __non_exhaustive: (),
         }
     }
@@ -380,6 +401,9 @@ impl ExtraOutputValues {
                     EvmOutputSelection::GasEstimates => {
                         config.gas_estimates = true;
                     }
+                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::FunctionDebugData) => {
+                        config.function_debug_data = true;
+                    }
                     _ => {}
                 },
                 ContractOutputSelection::Ewasm(_) => {
@@ -396,6 +420,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 +445,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 +461,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 +490,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..c15b78ce 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 {
@@ -1233,8 +1281,18 @@ impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         if let Some(msg) = &self.formatted_message {
             match self.severity {
-                Severity::Error => msg.as_str().red().fmt(f),
-                Severity::Warning | Severity::Info => msg.as_str().yellow().fmt(f),
+                Severity::Error => {
+                    if let Some(code) = self.error_code {
+                        format!("error[{}]: ", code).as_str().red().fmt(f)?;
+                    }
+                    msg.as_str().red().fmt(f)
+                }
+                Severity::Warning | Severity::Info => {
+                    if let Some(code) = self.error_code {
+                        format!("warning[{}]: ", code).as_str().yellow().fmt(f)?;
+                    }
+                    msg.as_str().yellow().fmt(f)
+                }
             }
         } else {
             self.severity.fmt(f)?;
@@ -1486,9 +1544,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/compile/output.rs b/ethers-solc/src/compile/output.rs
index 2ff3e08c..2ee3eddb 100644
--- a/ethers-solc/src/compile/output.rs
+++ b/ethers-solc/src/compile/output.rs
@@ -354,7 +354,9 @@ impl AggregatedCompilerOutput {
 /// Helper type to implement display for solc errors
 #[derive(Clone, Debug)]
 pub struct OutputDiagnostics<'a> {
+    /// output of the compiled project
     compiler_output: &'a AggregatedCompilerOutput,
+    /// the error codes to ignore
     ignored_error_codes: &'a [u64],
 }
 
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