Repository: gakonst/ethers-rs
Pull request #1582 (2022-08-09 19:41:41)

feat: add etherscan urls function
<!--
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
move etherscan URLs to `Chain` impl so they can be used on its own.
required for https://github.com/foundry-rs/foundry/pull/2616
<!--
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 `Chain::etherscan_urls` function
<!--
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/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs
index b366bccd..653e9fe9 100644
--- a/ethers-core/src/types/chain.rs
+++ b/ethers-core/src/types/chain.rs
@@ -13,6 +13,12 @@ use thiserror::Error;
 #[error("Failed to parse chain: {0}")]
 pub struct ParseChainError(String);
 
+/// Enum for all known chains
+///
+/// When adding a new chain:
+///   1. add new variant
+///   2. update Display/FromStr impl
+///   3. add etherscan_keys if supported
 #[repr(u64)]
 #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Deserialize, EnumVariantNames)]
 #[serde(rename_all = "snake_case")]
@@ -92,6 +98,100 @@ impl Chain {
 
         Some(Duration::from_millis(ms))
     }
+
+    /// Returns the corresponding etherscan URLs
+    ///
+    /// Returns `(API URL, BASE_URL)`, like `("https://api(-chain).etherscan.io/api", "https://etherscan.io")`
+    pub fn etherscan_urls(&self) -> Option<(&'static str, &'static str)> {
+        let urls = match self {
+            Chain::Mainnet => ("https://api.etherscan.io/api", "https://etherscan.io"),
+            Chain::Ropsten => {
+                ("https://api-ropsten.etherscan.io/api", "https://ropsten.etherscan.io")
+            }
+            Chain::Kovan => ("https://api-kovan.etherscan.io/api", "https://kovan.etherscan.io"),
+            Chain::Rinkeby => {
+                ("https://api-rinkeby.etherscan.io/api", "https://rinkeby.etherscan.io")
+            }
+            Chain::Goerli => ("https://api-goerli.etherscan.io/api", "https://goerli.etherscan.io"),
+            Chain::Sepolia => {
+                ("https://api-sepolia.etherscan.io/api", "https://sepolia.etherscan.io")
+            }
+            Chain::Polygon => ("https://api.polygonscan.com/api", "https://polygonscan.com"),
+            Chain::PolygonMumbai => {
+                ("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com")
+            }
+            Chain::Avalanche => ("https://api.snowtrace.io/api", "https://snowtrace.io"),
+            Chain::AvalancheFuji => {
+                ("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io")
+            }
+            Chain::Optimism => {
+                ("https://api-optimistic.etherscan.io/api", "https://optimistic.etherscan.io")
+            }
+            Chain::OptimismKovan => (
+                "https://api-kovan-optimistic.etherscan.io/api",
+                "https://kovan-optimistic.etherscan.io",
+            ),
+            Chain::Fantom => ("https://api.ftmscan.com/api", "https://ftmscan.com"),
+            Chain::FantomTestnet => {
+                ("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com")
+            }
+            Chain::BinanceSmartChain => ("https://api.bscscan.com/api", "https://bscscan.com"),
+            Chain::BinanceSmartChainTestnet => {
+                ("https://api-testnet.bscscan.com/api", "https://testnet.bscscan.com")
+            }
+            Chain::Arbitrum => ("https://api.arbiscan.io/api", "https://arbiscan.io"),
+            Chain::ArbitrumTestnet => {
+                ("https://api-testnet.arbiscan.io/api", "https://testnet.arbiscan.io")
+            }
+            Chain::Cronos => ("https://api.cronoscan.com/api", "https://cronoscan.com"),
+            Chain::CronosTestnet => {
+                ("https://api-testnet.cronoscan.com/api", "https://testnet.cronoscan.com")
+            }
+            Chain::Moonbeam => {
+                ("https://api-moonbeam.moonscan.io/api", "https://moonbeam.moonscan.io/")
+            }
+            Chain::Moonbase => {
+                ("https://api-moonbase.moonscan.io/api", "https://moonbase.moonscan.io/")
+            }
+            Chain::Moonriver => {
+                ("https://api-moonriver.moonscan.io/api", "https://moonriver.moonscan.io")
+            }
+            // blockscout API is etherscan compatible
+            Chain::XDai => {
+                ("https://blockscout.com/xdai/mainnet/api", "https://blockscout.com/xdai/mainnet")
+            }
+            Chain::Sokol => {
+                ("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol")
+            }
+            Chain::Poa => {
+                ("https://blockscout.com/poa/core/api", "https://blockscout.com/poa/core")
+            }
+            Chain::Rsk => {
+                ("https://blockscout.com/rsk/mainnet/api", "https://blockscout.com/rsk/mainnet")
+            }
+            Chain::Oasis => ("https://scan.oasischain.io/api", "https://scan.oasischain.io/"),
+            Chain::Emerald => {
+                ("https://explorer.emerald.oasis.dev/api", "https://explorer.emerald.oasis.dev/")
+            }
+            Chain::EmeraldTestnet => (
+                "https://testnet.explorer.emerald.oasis.dev/api",
+                "https://testnet.explorer.emerald.oasis.dev/",
+            ),
+            Chain::Aurora => ("https://api.aurorascan.dev/api", "https://aurorascan.dev"),
+            Chain::AuroraTestnet => {
+                ("https://testnet.aurorascan.dev/api", "https://testnet.aurorascan.dev")
+            }
+            Chain::Evmos => ("https://evm.evmos.org/api", "https://evm.evmos.org/"),
+            Chain::EvmosTestnet => ("https://evm.evmos.dev/api", "https://evm.evmos.dev/"),
+            Chain::AnvilHardhat | Chain::Dev | Chain::Morden | Chain::MoonbeamDev => {
+                // this is explicitly exhaustive so we don't forget to add new urls when adding a
+                // new chain
+                return None
+            }
+        };
+
+        Some(urls)
+    }
 }
 
 impl fmt::Display for Chain {
diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs
index 6d4bb2a0..b73a4849 100644
--- a/ethers-etherscan/src/lib.rs
+++ b/ethers-etherscan/src/lib.rs
@@ -231,90 +231,10 @@ impl ClientBuilder {
         ) -> (reqwest::Result<Url>, reqwest::Result<Url>) {
             (api.into_url(), url.into_url())
         }
-
-        let (etherscan_api_url, etherscan_url) = match chain {
-            Chain::Mainnet => urls("https://api.etherscan.io/api", "https://etherscan.io"),
-            Chain::Ropsten | Chain::Kovan | Chain::Rinkeby | Chain::Goerli | Chain::Sepolia => {
-                let chain_name = chain.to_string().to_lowercase();
-                urls(
-                    format!("https://api-{}.etherscan.io/api", chain_name),
-                    format!("https://{}.etherscan.io", chain_name),
-                )
-            }
-            Chain::Polygon => urls("https://api.polygonscan.com/api", "https://polygonscan.com"),
-            Chain::PolygonMumbai => {
-                urls("https://api-testnet.polygonscan.com/api", "https://mumbai.polygonscan.com")
-            }
-            Chain::Avalanche => urls("https://api.snowtrace.io/api", "https://snowtrace.io"),
-            Chain::AvalancheFuji => {
-                urls("https://api-testnet.snowtrace.io/api", "https://testnet.snowtrace.io")
-            }
-            Chain::Optimism => {
-                urls("https://api-optimistic.etherscan.io/api", "https://optimistic.etherscan.io")
-            }
-            Chain::OptimismKovan => urls(
-                "https://api-kovan-optimistic.etherscan.io/api",
-                "https://kovan-optimistic.etherscan.io",
-            ),
-            Chain::Fantom => urls("https://api.ftmscan.com/api", "https://ftmscan.com"),
-            Chain::FantomTestnet => {
-                urls("https://api-testnet.ftmscan.com/api", "https://testnet.ftmscan.com")
-            }
-            Chain::BinanceSmartChain => urls("https://api.bscscan.com/api", "https://bscscan.com"),
-            Chain::BinanceSmartChainTestnet => {
-                urls("https://api-testnet.bscscan.com/api", "https://testnet.bscscan.com")
-            }
-            Chain::Arbitrum => urls("https://api.arbiscan.io/api", "https://arbiscan.io"),
-            Chain::ArbitrumTestnet => {
-                urls("https://api-testnet.arbiscan.io/api", "https://testnet.arbiscan.io")
-            }
-            Chain::Cronos => urls("https://api.cronoscan.com/api", "https://cronoscan.com"),
-            Chain::CronosTestnet => {
-                urls("https://api-testnet.cronoscan.com/api", "https://testnet.cronoscan.com")
-            }
-            Chain::Moonbeam => {
-                urls("https://api-moonbeam.moonscan.io/api", "https://moonbeam.moonscan.io/")
-            }
-            Chain::Moonbase => {
-                urls("https://api-moonbase.moonscan.io/api", "https://moonbase.moonscan.io/")
-            }
-            Chain::Moonriver => {
-                urls("https://api-moonriver.moonscan.io/api", "https://moonriver.moonscan.io")
-            }
-            // blockscout API is etherscan compatible
-            Chain::XDai => urls(
-                "https://blockscout.com/xdai/mainnet/api",
-                "https://blockscout.com/xdai/mainnet",
-            ),
-            Chain::Sokol => {
-                urls("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol")
-            }
-            Chain::Poa => {
-                urls("https://blockscout.com/poa/core/api", "https://blockscout.com/poa/core")
-            }
-            Chain::Rsk => {
-                urls("https://blockscout.com/rsk/mainnet/api", "https://blockscout.com/rsk/mainnet")
-            }
-            Chain::Oasis => urls("https://scan.oasischain.io/api", "https://scan.oasischain.io/"),
-            Chain::Emerald => urls(
-                "https://explorer.emerald.oasis.dev/api",
-                "https://explorer.emerald.oasis.dev/",
-            ),
-            Chain::EmeraldTestnet => urls(
-                "https://testnet.explorer.emerald.oasis.dev/api",
-                "https://testnet.explorer.emerald.oasis.dev/",
-            ),
-            Chain::Aurora => urls("https://api.aurorascan.dev/api", "https://aurorascan.dev"),
-            Chain::AuroraTestnet => {
-                urls("https://testnet.aurorascan.dev/api", "https://testnet.aurorascan.dev")
-            }
-            Chain::AnvilHardhat | Chain::Dev => {
-                return Err(EtherscanError::LocalNetworksNotSupported)
-            }
-            Chain::Evmos => urls("https://evm.evmos.org/api", "https://evm.evmos.org/"),
-            Chain::EvmosTestnet => urls("https://evm.evmos.dev/api", "https://evm.evmos.dev/"),
-            chain => return Err(EtherscanError::ChainNotSupported(chain)),
-        };
+        let (etherscan_api_url, etherscan_url) = chain
+            .etherscan_urls()
+            .map(|(api, base)| urls(api, base))
+            .ok_or(EtherscanError::ChainNotSupported(chain))?;
         self.with_api_url(etherscan_api_url?)?.with_url(etherscan_url?)
     }
 
diff --git a/ethers-solc/src/project_util/mod.rs b/ethers-solc/src/project_util/mod.rs
index 5b2b95d8..7d0c8b22 100644
--- a/ethers-solc/src/project_util/mod.rs
+++ b/ethers-solc/src/project_util/mod.rs
@@ -185,6 +185,7 @@ impl<T: ArtifactOutput> TempProject<T> {
         version: impl AsRef<str>,
     ) -> Result<PathBuf> {
         let name = name.as_ref();
+        let name = name.strip_suffix(".sol").unwrap_or(name);
         self.add_lib(
             name,
             format!(
@@ -213,6 +214,7 @@ contract {} {{}}
         version: impl AsRef<str>,
     ) -> Result<PathBuf> {
         let name = name.as_ref();
+        let name = name.strip_suffix(".sol").unwrap_or(name);
         self.add_source(
             name,
             format!(


Test patch:
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index cb3cbdfd..0a0a023b 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -2150,14 +2150,15 @@ fn can_add_basic_contract_and_library() {
 
     let src = project.add_basic_source("Foo.sol", "^0.8.0").unwrap();
 
-    let lib = project.add_basic_source("Bar.sol", "^0.8.0").unwrap();
+    let lib = project.add_basic_source("Bar", "^0.8.0").unwrap();
 
     let graph = Graph::resolve(project.paths()).unwrap();
     assert_eq!(graph.files().len(), 2);
-    assert_eq!(graph.files().clone(), HashMap::from([(src, 0), (lib, 1),]));
+    assert!(graph.files().contains_key(&src));
+    assert!(graph.files().contains_key(&lib));
 
     let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
     assert!(compiled.find_first("Foo").is_some());
     assert!(compiled.find_first("Bar").is_some());
-    assert!(!compiled.has_compiler_errors());
 }