Repository: gakonst/ethers-rs
Pull request #774 (2022-01-07 16:52:17)

feat(solc): flatten
<!--
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.
-->
Implementation of flatten feature discussed in [foundry #312](https://github.com/gakonst/foundry/issues/312)

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->
Use the dependency graph to recursively resolve the file imports into a single string.

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [x] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23d8738a..c3be326d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,8 @@
 
 ### Unreleased
 
+- Add ability to flatten file imports
+  [#774](https://github.com/gakonst/ethers-rs/pull/774)
 - Add dependency graph and resolve all imported libraryfiles
   [#750](https://github.com/gakonst/ethers-rs/pull/750)
 - `Remapping::find_many` does not return a `Result` anymore
diff --git a/Cargo.lock b/Cargo.lock
index 196b1d18..44169a06 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -980,9 +980,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
 
 [[package]]
 name = "elliptic-curve"
-version = "0.11.6"
+version = "0.11.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "decb3a27ea454a5f23f96eb182af0671c12694d64ecc33dada74edd1301f6cfc"
+checksum = "5fcbfdf46fd1157b49be0c2170bab2784ca19233b35c2dc772d60247b72f2071"
 dependencies = [
  "crypto-bigint",
  "der",
@@ -3771,9 +3771,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-subscriber"
-version = "0.3.5"
+version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d81bfa81424cc98cb034b837c985b7a290f592e5b4322f353f94a0ab0f9f594"
+checksum = "77be66445c4eeebb934a7340f227bfe7b338173d3f8c00a60a5a58005c9faecf"
 dependencies = [
  "ansi_term",
  "sharded-slab",
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index 992f5098..23a2c1c4 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -17,7 +17,7 @@ rlp-derive = { version = "0.1.0", default-features = false }
 
 # crypto
 ecdsa = { version = "0.13.4", default-features = false, features = ["std"] }
-elliptic-curve = { version = "0.11.5", default-features = false }
+elliptic-curve = { version = "0.11.7", default-features = false }
 generic-array = { version = "0.14.5", default-features = false }
 k256 = { version = "0.10.1", default-features = false, features = ["keccak256", "ecdsa"] }
 rand = { version = "0.8.4", default-features = false }
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index 40bc5561..fd99ba35 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -21,7 +21,7 @@ coins-bip39 = "0.6.0"
 coins-ledger = { version = "0.6.0", default-features = false, optional = true }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
 async-trait = { version = "0.1.50", default-features = false }
-elliptic-curve = { version = "0.11.6", default-features = false }
+elliptic-curve = { version = "0.11.7", default-features = false }
 sha2 = { version = "0.9.8", default-features = false }
 rand = { version = "0.8.4", default-features = false }
 yubihsm = { version = "0.40.0", features = ["secp256k1", "http", "usb"], optional = true }
@@ -45,7 +45,7 @@ home = "0.5.3"
 ethers-contract = { version = "^0.6.0", path = "../ethers-contract", features = ["eip712", "abigen"]}
 ethers-derive-eip712 = { version = "0.2.0", path = "../ethers-core/ethers-derive-eip712" }
 serde_json = { version = "1.0.64" }
-tracing-subscriber = "0.3.5"
+tracing-subscriber = "0.3.6"
 yubihsm = { version = "0.40.0", features = ["secp256k1", "usb", "mockhsm"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs
index cb83625b..e671b9fc 100644
--- a/ethers-solc/src/artifacts.rs
+++ b/ethers-solc/src/artifacts.rs
@@ -474,7 +474,7 @@ impl Source {
 
     /// Returns all import statements of the file
     pub fn parse_imports(&self) -> Vec<&str> {
-        utils::find_import_paths(self.as_ref())
+        utils::find_import_paths(self.as_ref()).map(|m| m.as_str()).collect()
     }
 }
 
diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs
index 72b95c7a..4fd0074e 100644
--- a/ethers-solc/src/cache.rs
+++ b/ethers-solc/src/cache.rs
@@ -326,10 +326,10 @@ impl SolFilesCacheBuilder {
                 .map_err(|err| SolcError::solc(err.to_string()))?
                 .as_millis() as u64;
             let imports =
-                utils::find_import_paths(source.as_ref()).into_iter().map(str::to_string).collect();
+                utils::find_import_paths(source.as_ref()).map(|m| m.as_str().to_owned()).collect();
 
             let version_pragmas = utils::find_version_pragma(source.as_ref())
-                .map(|v| vec![v.to_string()])
+                .map(|v| vec![v.as_str().to_string()])
                 .unwrap_or_default();
 
             let entry = CacheEntry {
diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs
index 2e68cb59..ddf031ae 100644
--- a/ethers-solc/src/compile.rs
+++ b/ethers-solc/src/compile.rs
@@ -323,7 +323,7 @@ impl Solc {
     pub fn source_version_req(source: &Source) -> Result<VersionReq> {
         let version =
             utils::find_version_pragma(&source.content).ok_or(SolcError::PragmaNotFound)?;
-        Self::version_req(version)
+        Self::version_req(version.as_str())
     }
 
     /// Returns the corresponding SemVer version requirement for the solidity version
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index ee8dbd8a..8ed92257 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -4,6 +4,7 @@ use crate::{
     error::{Result, SolcError, SolcIoError},
     hh::HardhatArtifact,
     remappings::Remapping,
+    resolver::Graph,
     utils, CompilerOutput, Source, Sources,
 };
 use ethers_core::{abi::Abi, types::Bytes};
@@ -11,8 +12,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use std::{
     collections::BTreeMap,
     convert::TryFrom,
-    fmt,
-    fmt::Formatter,
+    fmt::{self, Formatter},
     fs, io,
     path::{Component, Path, PathBuf},
 };
@@ -171,6 +171,68 @@ impl ProjectPathsConfig {
     pub fn find_libs(root: impl AsRef<Path>) -> Vec<PathBuf> {
         vec![utils::find_fave_or_alt_path(root, "lib", "node_modules")]
     }
+
+    /// Flattens all file imports into a single string
+    pub fn flatten(&self, target: &Path) -> Result<String> {
+        tracing::trace!("flattening file");
+        let graph = Graph::resolve(self)?;
+        self.flatten_node(target, &graph, false, false)
+    }
+
+    /// Flattens a single node from the dependency graph
+    fn flatten_node(
+        &self,
+        target: &Path,
+        graph: &Graph,
+        strip_version_pragma: bool,
+        strip_license: bool,
+    ) -> Result<String> {
+        let target_dir = target.parent().ok_or_else(|| {
+            SolcError::msg(format!("failed to get parent directory for \"{:?}\"", target.display()))
+        })?;
+        let target_index = graph.files().get(target).ok_or_else(|| {
+            SolcError::msg(format!("cannot resolve file at \"{:?}\"", target.display()))
+        })?;
+        let target_node = graph.node(*target_index);
+
+        let mut imports = target_node.imports().clone();
+        imports.sort_by_key(|x| x.loc().0);
+
+        let mut content = target_node.content().as_bytes().to_vec();
+        let mut offset = 0_isize;
+
+        if strip_license {
+            if let Some(license) = target_node.license() {
+                let (start, end) = license.loc_by_offset(offset);
+                content.splice(start..end, std::iter::empty());
+                offset -= (end - start) as isize;
+            }
+        }
+
+        if strip_version_pragma {
+            if let Some(version) = target_node.version() {
+                let (start, end) = version.loc_by_offset(offset);
+                content.splice(start..end, std::iter::empty());
+                offset -= (end - start) as isize;
+            }
+        }
+
+        for import in imports.iter() {
+            let import_path = self.resolve_import(target_dir, import.data())?;
+            let import_content = self.flatten_node(&import_path, graph, true, true)?;
+            let import_content = import_content.trim().as_bytes().to_owned();
+            let import_content_len = import_content.len() as isize;
+            let (start, end) = import.loc_by_offset(offset);
+            content.splice(start..end, import_content);
+            offset += import_content_len - ((end - start) as isize);
+        }
+
+        let result = String::from_utf8(content).map_err(|err| {
+            SolcError::msg(format!("failed to convert extended bytes to string: {}", err))
+        })?;
+
+        Ok(result)
+    }
 }
 
 impl fmt::Display for ProjectPathsConfig {
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index d1fa2c21..d68e7cd8 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -37,8 +37,12 @@ use crate::{
 };
 use error::Result;
 use std::{
-    borrow::Cow, collections::BTreeMap, convert::TryInto, fmt, fs, marker::PhantomData,
-    path::PathBuf,
+    borrow::Cow,
+    collections::BTreeMap,
+    convert::TryInto,
+    fmt, fs,
+    marker::PhantomData,
+    path::{Path, PathBuf},
 };
 
 /// Utilities for creating, mocking and testing of (temporary) projects
@@ -507,6 +511,18 @@ impl<Artifacts: ArtifactOutput> Project<Artifacts> {
         }
         Ok(())
     }
+
+    /// Flattens the target file into a single string suitable for verification
+    ///
+    /// This method uses a dependency graph to resolve imported files and substitute
+    /// import directives with the contents of target files. It will strip the pragma
+    /// version directives and SDPX license identifiers from imported files.
+    ///
+    /// NOTE: the SDPX license identifier will be removed from the imported file
+    /// only if it is found at the beginning of the file.
+    pub fn flatten(&self, target: &Path) -> Result<String> {
+        self.paths.flatten(target)
+    }
 }
 
 enum PreprocessedJob<T: ArtifactOutput> {
diff --git a/ethers-solc/src/project_util.rs b/ethers-solc/src/project_util.rs
index 34c31412..a3effc74 100644
--- a/ethers-solc/src/project_util.rs
+++ b/ethers-solc/src/project_util.rs
@@ -59,6 +59,10 @@ impl<T: ArtifactOutput> TempProject<T> {
         self.project().compile()
     }
 
+    pub fn flatten(&self, target: &Path) -> Result<String> {
+        self.project().flatten(target)
+    }
+
     pub fn project_mut(&mut self) -> &mut Project<T> {
         &mut self.inner
     }
diff --git a/ethers-solc/src/resolver.rs b/ethers-solc/src/resolver.rs
index 3d93eff3..22f1677d 100644
--- a/ethers-solc/src/resolver.rs
+++ b/ethers-solc/src/resolver.rs
@@ -27,13 +27,14 @@
 //! which is defined on a per source file basis.
 
 use std::{
-    collections::{HashMap, VecDeque},
+    collections::{HashMap, HashSet, VecDeque},
     path::{Path, PathBuf},
 };
 
 use rayon::prelude::*;
+use regex::Match;
 use semver::VersionReq;
-use solang_parser::pt::{Import, SourceUnitPart};
+use solang_parser::pt::{Import, Loc, SourceUnitPart};
 
 use crate::{error::Result, utils, ProjectPathsConfig, Solc, Source, Sources};
 
@@ -72,6 +73,21 @@ impl Graph {
         &self.nodes[index]
     }
 
+    /// Returns an iterator that yields all nodes of the dependency tree that the given node id
+    /// spans, starting with the node itself.
+    ///
+    /// # Panics
+    ///
+    /// if the `start` node id is not included in the graph
+    pub fn node_ids(&self, start: usize) -> impl Iterator<Item = usize> + '_ {
+        NodesIter::new(start, self)
+    }
+
+    /// Same as `Self::node_ids` but returns the actual `Node`
+    pub fn nodes(&self, start: usize) -> impl Iterator<Item = &Node> + '_ {
+        self.node_ids(start).map(move |idx| self.node(idx))
+    }
+
     /// Returns all files together with their paths
     pub fn into_sources(self) -> Sources {
         self.nodes.into_iter().map(|node| (node.path, node.source)).collect()
@@ -138,14 +154,12 @@ impl Graph {
             };
 
             for import in node.data.imports.iter() {
-                match paths.resolve_import(cwd, import) {
+                match paths.resolve_import(cwd, import.data()) {
                     Ok(import) => {
                         add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
                     }
-                    Err(err) => {
-                        tracing::trace!("{}", err)
-                    }
-                }
+                    Err(err) => tracing::trace!("failed to resolve import component \"{:?}\"", err),
+                };
             }
             nodes.push(node);
             edges.push(resolved_imports);
@@ -241,32 +255,18 @@ impl Graph {
     }
 
     /// Filters incompatible versions from the `candidates`.
-    fn retain_compatible_versions(
-        &self,
-        idx: usize,
-        candidates: &mut Vec<&crate::SolcVersion>,
-        traversed: &mut std::collections::HashSet<(usize, usize)>,
-    ) -> std::result::Result<(), String> {
-        let node = self.node(idx);
-        if let Some(ref req) = node.data.version_req {
-            candidates.retain(|v| req.matches(v.as_ref()));
-        }
-        for dep in self.imported_nodes(idx).iter().copied() {
-            // check for circular deps which would result in endless recursion SO here
-            // a circular dependency exists, if there was already a `dependency imports current
-            // node` relationship in the traversed path we skip this node
-            traversed.insert((idx, dep));
-            if traversed.contains(&(dep, idx)) {
-                tracing::warn!(
-                    "Detected cyclic imports {} <-> {}",
-                    utils::source_name(&self.nodes[idx].path, &self.root).display(),
-                    utils::source_name(&self.nodes[dep].path, &self.root).display()
-                );
-                continue
+    fn retain_compatible_versions(&self, idx: usize, candidates: &mut Vec<&crate::SolcVersion>) {
+        let nodes: HashSet<_> = self.node_ids(idx).collect();
+        for node in nodes {
+            let node = self.node(node);
+            if let Some(ref req) = node.data.version_req {
+                candidates.retain(|v| req.matches(v.as_ref()));
+            }
+            if candidates.is_empty() {
+                // nothing to filter anymore
+                return
             }
-            self.retain_compatible_versions(dep, candidates, traversed)?;
         }
-        Ok(())
     }
 
     /// Ensures that all files are compatible with all of their imports.
@@ -304,11 +304,7 @@ impl Graph {
         // walking through the node's dep tree and filtering the versions along the way
         for idx in 0..self.num_input_files {
             let mut candidates = all_versions.iter().collect::<Vec<_>>();
-            let mut traveresd = std::collections::HashSet::new();
-            if let Err(msg) = self.retain_compatible_versions(idx, &mut candidates, &mut traveresd)
-            {
-                errors.push(msg);
-            }
+            self.retain_compatible_versions(idx, &mut candidates);
 
             if candidates.is_empty() && !erroneous_nodes.contains(&idx) {
                 let mut msg = String::new();
@@ -344,6 +340,34 @@ impl Graph {
     }
 }
 
+/// An iterator over a node and its dependencies
+#[derive(Debug)]
+pub struct NodesIter<'a> {
+    /// stack of nodes
+    stack: VecDeque<usize>,
+    visited: HashSet<usize>,
+    graph: &'a Graph,
+}
+
+impl<'a> NodesIter<'a> {
+    fn new(start: usize, graph: &'a Graph) -> Self {
+        Self { stack: VecDeque::from([start]), visited: Default::default(), graph }
+    }
+}
+
+impl<'a> Iterator for NodesIter<'a> {
+    type Item = usize;
+    fn next(&mut self) -> Option<Self::Item> {
+        let node = self.stack.pop_front()?;
+
+        if self.visited.insert(node) {
+            // push the node's direct dependencies to the stack if we haven't visited it already
+            self.stack.extend(self.graph.imported_nodes(node).iter().copied());
+        }
+        Some(node)
+    }
+}
+
 /// Container type for solc versions and their compatible sources
 #[cfg(all(feature = "svm", feature = "async"))]
 #[derive(Debug)]
@@ -397,12 +421,81 @@ pub struct Node {
     data: SolData,
 }
 
+impl Node {
+    pub fn content(&self) -> &str {
+        &self.source.content
+    }
+
+    pub fn imports(&self) -> &Vec<SolDataUnit<PathBuf>> {
+        &self.data.imports
+    }
+
+    pub fn version(&self) -> &Option<SolDataUnit<String>> {
+        &self.data.version
+    }
+
+    pub fn license(&self) -> &Option<SolDataUnit<String>> {
+        &self.data.license
+    }
+}
+
 #[derive(Debug, Clone)]
 #[allow(unused)]
 struct SolData {
-    version: Option<String>,
+    license: Option<SolDataUnit<String>>,
+    version: Option<SolDataUnit<String>>,
+    imports: Vec<SolDataUnit<PathBuf>>,
     version_req: Option<VersionReq>,
-    imports: Vec<PathBuf>,
+}
+
+#[derive(Debug, Clone)]
+pub struct SolDataUnit<T> {
+    loc: Location,
+    data: T,
+}
+#[derive(Debug, Clone)]
+pub struct Location {
+    pub start: usize,
+    pub end: usize,
+}
+
+/// Solidity Data Unit decorated with its location within the file
+impl<T> SolDataUnit<T> {
+    pub fn new(data: T, loc: Location) -> Self {
+        Self { data, loc }
+    }
+
+    /// Returns the underlying data for the unit
+    pub fn data(&self) -> &T {
+        &self.data
+    }
+
+    /// Returns the location of the given data unit
+    pub fn loc(&self) -> (usize, usize) {
+        (self.loc.start, self.loc.end)
+    }
+
+    /// Returns the location of the given data unit adjusted by an offset.
+    /// Used to determine new position of the unit within the file after
+    /// content manipulation.
+    pub fn loc_by_offset(&self, offset: isize) -> (usize, usize) {
+        (
+            offset.saturating_add(self.loc.start as isize) as usize,
+            offset.saturating_add(self.loc.end as isize) as usize,
+        )
+    }
+}
+
+impl From<Match<'_>> for Location {
+    fn from(src: Match) -> Self {
+        Location { start: src.start(), end: src.end() }
+    }
+}
+
+impl From<Loc> for Location {
+    fn from(src: Loc) -> Self {
+        Location { start: src.1, end: src.2 }
+    }
 }
 
 fn read_node(file: impl AsRef<Path>) -> Result<Node> {
@@ -418,24 +511,24 @@ fn read_node(file: impl AsRef<Path>) -> Result<Node> {
 /// parsing fails, we'll fall back to extract that info via regex
 fn parse_data(content: &str) -> SolData {
     let mut version = None;
-    let mut imports = Vec::new();
+    let mut imports = Vec::<SolDataUnit<PathBuf>>::new();
     match solang_parser::parse(content, 0) {
         Ok(units) => {
             for unit in units.0 {
                 match unit {
-                    SourceUnitPart::PragmaDirective(_, _, pragma, value) => {
+                    SourceUnitPart::PragmaDirective(loc, _, pragma, value) => {
                         if pragma.name == "solidity" {
                             // we're only interested in the solidity version pragma
-                            version = Some(value.string);
+                            version = Some(SolDataUnit::new(value.string, loc.into()));
                         }
                     }
                     SourceUnitPart::ImportDirective(_, import) => {
-                        let import = match import {
-                            Import::Plain(s, _) => s,
-                            Import::GlobalSymbol(s, _, _) => s,
-                            Import::Rename(s, _, _) => s,
+                        let (import, loc) = match import {
+                            Import::Plain(s, l) => (s, l),
+                            Import::GlobalSymbol(s, _, l) => (s, l),
+                            Import::Rename(s, _, l) => (s, l),
                         };
-                        imports.push(PathBuf::from(import.string));
+                        imports.push(SolDataUnit::new(PathBuf::from(import.string), loc.into()));
                     }
                     _ => {}
                 }
@@ -446,21 +539,50 @@ fn parse_data(content: &str) -> SolData {
                 "failed to parse solidity ast: \"{:?}\". Falling back to regex to extract data",
                 err
             );
-            version = utils::find_version_pragma(content).map(str::to_string);
-            imports = utils::find_import_paths(content)
-                .into_iter()
-                .map(|p| Path::new(p).to_path_buf())
-                .collect()
+            version = capture_outer_and_inner(content, &utils::RE_SOL_PRAGMA_VERSION, &["version"])
+                .first()
+                .map(|(cap, name)| {
+                    SolDataUnit::new(name.as_str().to_owned(), cap.to_owned().into())
+                });
+            imports = capture_outer_and_inner(content, &utils::RE_SOL_IMPORT, &["p1", "p2", "p3"])
+                .iter()
+                .map(|(cap, m)| SolDataUnit::new(PathBuf::from(m.as_str()), cap.to_owned().into()))
+                .collect();
         }
     };
-    let version_req = if let Some(ref v) = version { Solc::version_req(v).ok() } else { None };
-    SolData { version_req, version, imports }
+    let license = content.lines().next().and_then(|line| {
+        capture_outer_and_inner(line, &utils::RE_SOL_SDPX_LICENSE_IDENTIFIER, &["license"])
+            .first()
+            .map(|(cap, l)| SolDataUnit::new(l.as_str().to_owned(), cap.to_owned().into()))
+    });
+    let version_req = version.as_ref().and_then(|v| Solc::version_req(v.data()).ok());
+    SolData { version_req, version, imports, license }
+}
+
+/// Given the regex and the target string, find all occurrences
+/// of named groups within the string. This method returns
+/// the tuple of matches `(a, b)` where `a` is the match for the
+/// entire regex and `b` is the match for the first named group.
+///
+/// NOTE: This method will return the match for the first named
+/// group, so the order of passed named groups matters.
+fn capture_outer_and_inner<'a>(
+    content: &'a str,
+    regex: &regex::Regex,
+    names: &[&str],
+) -> Vec<(regex::Match<'a>, regex::Match<'a>)> {
+    regex
+        .captures_iter(content)
+        .filter_map(|cap| {
+            let cap_match = names.iter().find_map(|name| cap.name(name));
+            cap_match.and_then(|m| cap.get(0).map(|outer| (outer.to_owned(), m)))
+        })
+        .collect()
 }
 
 #[cfg(test)]
 mod tests {
     use super::*;
-    use std::path::Path;
 
     #[test]
     fn can_resolve_hardhat_dependency_graph() {
@@ -502,11 +624,8 @@ mod tests {
         let dapp_test = graph.node(1);
         assert_eq!(dapp_test.path, paths.sources.join("Dapp.t.sol"));
         assert_eq!(
-            dapp_test.data.imports,
-            vec![
-                Path::new("ds-test/test.sol").to_path_buf(),
-                Path::new("./Dapp.sol").to_path_buf()
-            ]
+            dapp_test.data.imports.iter().map(|i| i.data()).collect::<Vec<&PathBuf>>(),
+            vec![&PathBuf::from("ds-test/test.sol"), &PathBuf::from("./Dapp.sol")]
         );
         assert_eq!(graph.imported_nodes(1).to_vec(), vec![2, 0]);
     }
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index df1d1abc..fc1960b4 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -2,43 +2,49 @@
 
 use std::path::{Component, Path, PathBuf};
 
-use crate::{error::SolcError, SolcIoError};
+use crate::{
+    error::{self, SolcError},
+    ProjectPathsConfig, SolcIoError,
+};
 use once_cell::sync::Lazy;
-use regex::Regex;
+use regex::{Match, Regex};
 use semver::Version;
 use tiny_keccak::{Hasher, Keccak};
 use walkdir::WalkDir;
 
 /// A regex that matches the import path and identifier of a solidity import
 /// statement with the named groups "path", "id".
+// Adapted from https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L100
 pub static RE_SOL_IMPORT: Lazy<Regex> = Lazy::new(|| {
-    // Adapted from https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L100
     Regex::new(r#"import\s+(?:(?:"(?P<p1>[^;]*)"|'([^;]*)')(?:;|\s+as\s+(?P<id>[^;]*);)|.+from\s+(?:"(?P<p2>.*)"|'(?P<p3>.*)');)"#).unwrap()
 });
 
 /// A regex that matches the version part of a solidity pragma
 /// as follows: `pragma solidity ^0.5.2;` => `^0.5.2`
-/// statement with the named groups "path", "id".
+/// statement with the named group "version".
 // Adapted from https://github.com/nomiclabs/hardhat/blob/cced766c65b25d3d0beb39ef847246ac9618bdd9/packages/hardhat-core/src/internal/solidity/parse.ts#L119
 pub static RE_SOL_PRAGMA_VERSION: Lazy<Regex> =
     Lazy::new(|| Regex::new(r"pragma\s+solidity\s+(?P<version>.+?);").unwrap());
 
+/// A regex that matches the SDPX license identifier
+/// statement with the named group "license".
+pub static RE_SOL_SDPX_LICENSE_IDENTIFIER: Lazy<Regex> =
+    Lazy::new(|| Regex::new(r"///?\s*SPDX-License-Identifier:\s*(?P<license>.+)").unwrap());
+
 /// Returns all path parts from any solidity import statement in a string,
 /// `import "./contracts/Contract.sol";` -> `"./contracts/Contract.sol"`.
 ///
 /// See also https://docs.soliditylang.org/en/v0.8.9/grammar.html
-pub fn find_import_paths(contract: &str) -> Vec<&str> {
+pub fn find_import_paths(contract: &str) -> impl Iterator<Item = Match> {
     RE_SOL_IMPORT
         .captures_iter(contract)
         .filter_map(|cap| cap.name("p1").or_else(|| cap.name("p2")).or_else(|| cap.name("p3")))
-        .map(|m| m.as_str())
-        .collect()
 }
 
 /// Returns the solidity version pragma from the given input:
 /// `pragma solidity ^0.5.2;` => `^0.5.2`
-pub fn find_version_pragma(contract: &str) -> Option<&str> {
-    RE_SOL_PRAGMA_VERSION.captures(contract)?.name("version").map(|m| m.as_str())
+pub fn find_version_pragma(contract: &str) -> Option<Match> {
+    RE_SOL_PRAGMA_VERSION.captures(contract)?.name("version")
 }
 
 /// Returns a list of absolute paths to all the solidity files under the root, or the file itself,
@@ -79,6 +85,37 @@ pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
     dunce::canonicalize(&path).map_err(|err| SolcIoError::new(err, path))
 }
 
+/// Try to resolve import to a local file or library path
+pub fn resolve_import_component(
+    import: &Path,
+    node_dir: &Path,
+    paths: &ProjectPathsConfig,
+) -> error::Result<PathBuf> {
+    let component = match import.components().next() {
+        Some(inner) => inner,
+        None => {
+            return Err(SolcError::msg(format!(
+                "failed to resolve import at \"{:?}\"",
+                import.display()
+            )))
+        }
+    };
+
+    if component == Component::CurDir || component == Component::ParentDir {
+        // if the import is relative we assume it's already part of the processed input file set
+        canonicalize(node_dir.join(import)).map_err(|err| err.into())
+    } else {
+        // resolve library file
+        match paths.resolve_library_import(import.as_ref()) {
+            Some(lib) => Ok(lib),
+            None => Err(SolcError::msg(format!(
+                "failed to resolve library import \"{:?}\"",
+                import.display()
+            ))),
+        }
+    }
+}
+
 /// Returns the path to the library if the source path is in fact determined to be a library path,
 /// and it exists.
 /// Note: this does not handle relative imports or remappings.
@@ -305,7 +342,7 @@ import { T } from '../Test2.sol';
 "##;
         assert_eq!(
             vec!["hardhat/console.sol", "../contract/Contract.sol", "../Test.sol", "../Test2.sol"],
-            find_import_paths(s)
+            find_import_paths(s).map(|m| m.as_str()).collect::<Vec<&str>>()
         );
     }
     #[test]
@@ -313,7 +350,7 @@ import { T } from '../Test2.sol';
         let s = r##"//SPDX-License-Identifier: Unlicense
 pragma solidity ^0.8.0;
 "##;
-        assert_eq!(Some("^0.8.0"), find_version_pragma(s));
+        assert_eq!(Some("^0.8.0"), find_version_pragma(s).map(|s| s.as_str()));
     }
 
     #[test]
diff --git a/examples/ethers-wasm/yarn.lock b/examples/ethers-wasm/yarn.lock
index a0ee1543..745a37ff 100644
--- a/examples/ethers-wasm/yarn.lock
+++ b/examples/ethers-wasm/yarn.lock
@@ -1605,9 +1605,9 @@ flush-write-stream@^1.0.0:
     readable-stream "^2.3.6"
 
 follow-redirects@^1.0.0:
-  version "1.14.2"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.2.tgz#cecb825047c00f5e66b142f90fed4f515dec789b"
-  integrity sha512-yLR6WaE2lbF0x4K2qE2p9PEXKLDjUjnR/xmjS3wHAYxtlsI9MLLBJUZirAHKzUZDGLxje7w/cXR49WOUo4rbsA==
+  version "1.14.7"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.7.tgz#2004c02eb9436eee9a21446a6477debf17e81685"
+  integrity sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==
 
 for-in@^1.0.2:
   version "1.0.2"
diff --git a/examples/uniswapv2.rs b/examples/uniswapv2.rs
new file mode 100644
index 00000000..91ae6b2b
--- /dev/null
+++ b/examples/uniswapv2.rs
@@ -0,0 +1,32 @@
+use anyhow::Result;
+use ethers::prelude::*;
+use std::sync::Arc;
+
+// Generate the type-safe contract bindings by providing the ABI
+// definition in human readable format
+abigen!(
+    IUniswapV2Pair,
+    r#"[
+        function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)
+    ]"#,
+);
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let client = Provider::<Http>::try_from(
+        "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
+    )?;
+    let client = Arc::new(client);
+
+    // ETH/USDT pair on Uniswap V2
+    let address = "0x0d4a11d5EEaaC28EC3F61d100daF4d40471f1852".parse::<Address>()?;
+    let pair = IUniswapV2Pair::new(address, Arc::clone(&client));
+
+    // getReserves -> get_reserves
+    let (reserve0, reserve1, _timestamp) = pair.get_reserves().call().await?;
+    println!("Reserves (ETH, USDT): ({}, {})", reserve0, reserve1);
+
+    let mid_price = f64::powi(10.0, 18 - 6) * reserve1 as f64 / reserve0 as f64;
+    println!("ETH/USDT price: {:.2}", mid_price);
+    Ok(())
+}


Test patch:
diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs
index a35e107c..44a55869 100644
--- a/ethers-providers/tests/provider.rs
+++ b/ethers-providers/tests/provider.rs
@@ -112,19 +112,19 @@ mod celo_tests {
     use futures_util::stream::StreamExt;
 
     #[tokio::test]
-    // https://alfajores-blockscout.celo-testnet.org/tx/0x544ea96cddb16aeeaedaf90885c1e02be4905f3eb43d6db3f28cac4dbe76a625/internal_transactions
+    // https://alfajores-blockscout.celo-testnet.org/tx/0xd3d27aa4517124d9ff3ac6f1d8f248e0fe47b6f841b625722546162672ac24c7/internal-transactions
     async fn get_transaction() {
         let provider =
             Provider::<Http>::try_from("https://alfajores-forno.celo-testnet.org").unwrap();
 
-        let tx_hash = "c8496681d0ade783322980cce00c89419fce4b484635d9e09c79787a0f75d450"
+        let tx_hash = "d3d27aa4517124d9ff3ac6f1d8f248e0fe47b6f841b625722546162672ac24c7"
             .parse::<H256>()
             .unwrap();
         let tx = provider.get_transaction(tx_hash).await.unwrap().unwrap();
         assert!(tx.gateway_fee_recipient.is_none());
         assert_eq!(tx.gateway_fee.unwrap(), 0.into());
         assert_eq!(tx.hash, tx_hash);
-        assert_eq!(tx.block_number.unwrap(), 447181.into())
+        assert_eq!(tx.block_number.unwrap(), 9401421.into())
     }
 
     #[tokio::test]
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 7f91c983..2a9a91c8 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -307,3 +307,56 @@ fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()>
     }
     Ok(())
 }
+
+#[test]
+fn can_flatten_file() {
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/test-contract-libs");
+    let target = root.join("src").join("Foo.sol");
+    let paths = ProjectPathsConfig::builder()
+        .sources(root.join("src"))
+        .lib(root.join("lib1"))
+        .lib(root.join("lib2"));
+    let project = TempProject::<MinimalCombinedArtifacts>::new(paths).unwrap();
+
+    let result = project.flatten(&target);
+    assert!(result.is_ok());
+
+    let result = result.unwrap();
+    assert!(result.find("contract Foo").is_some());
+    assert!(result.find("contract Bar").is_some());
+}
+
+#[test]
+fn can_flatten_file_with_external_lib() {
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample");
+    let paths = ProjectPathsConfig::builder()
+        .sources(root.join("contracts"))
+        .lib(root.join("node_modules"));
+    let project = TempProject::<MinimalCombinedArtifacts>::new(paths).unwrap();
+
+    let target = root.join("contracts").join("Greeter.sol");
+
+    let result = project.flatten(&target);
+    assert!(result.is_ok());
+
+    let result = result.unwrap();
+    assert!(result.find("library console").is_some());
+    assert!(result.find("contract Greeter").is_some());
+}
+
+#[test]
+fn can_flatten_file_in_dapp_sample() {
+    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
+    let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib"));
+    let project = TempProject::<MinimalCombinedArtifacts>::new(paths).unwrap();
+
+    let target = root.join("src/Dapp.t.sol");
+
+    let result = project.flatten(&target);
+    assert!(result.is_ok());
+
+    let result = result.unwrap();
+    assert!(result.find("contract DSTest").is_some());
+    assert!(result.find("contract Dapp").is_some());
+    assert!(result.find("contract DappTest").is_some());
+}