From 1861b9889645f070d0c78f5868aed70ff5138c6b Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 27 Feb 2023 21:14:36 +0100 Subject: [PATCH] chore: move etherscan api key env var matching to Chain enum (#2204) * fix: mainnet, optimism blocktimes * chore: move etherscan api key env var matching to Chain enum * docs: missing exhaustiveness comment --- .../src/source/online.rs | 3 +- ethers-core/src/types/chain.rs | 184 ++++++++++++++---- ethers-etherscan/src/lib.rs | 53 ++--- 3 files changed, 166 insertions(+), 74 deletions(-) diff --git a/ethers-contract/ethers-contract-abigen/src/source/online.rs b/ethers-contract/ethers-contract-abigen/src/source/online.rs index 05301481..3c3c70d8 100644 --- a/ethers-contract/ethers-contract-abigen/src/source/online.rs +++ b/ethers-contract/ethers-contract-abigen/src/source/online.rs @@ -67,7 +67,8 @@ impl Explorer { let chain = self.chain(); let client = match api_key { Some(api_key) => Client::new(chain, api_key), - None => Client::new_from_env(chain), + None => Client::new_from_env(chain) + .or_else(|_| Client::builder().chain(chain).and_then(|b| b.build())), }?; Ok(client) } diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs index 072daac3..05d8140d 100644 --- a/ethers-core/src/types/chain.rs +++ b/ethers-core/src/types/chain.rs @@ -20,6 +20,11 @@ pub type ParseChainError = TryFromPrimitiveError; // "main" must be present and will be used in `Display`, `Serialize` and `FromStr`, // while the aliases will be added only to `FromStr`. +// We don't derive Serialize because it is manually implemented using AsRef and it would +// break a lot of things since Serialize is `kebab-case` vs Deserialize `snake_case`. +// This means that the Chain type is not "round-trippable", because the Serialize and Deserialize +// implementations do not use the same case style. + /// An Ethereum EIP-155 chain. #[derive( Clone, @@ -30,11 +35,11 @@ pub type ParseChainError = TryFromPrimitiveError; PartialOrd, Ord, Hash, - AsRefStr, // also for fmt::Display and serde::Serialize - EnumVariantNames, // Self::VARIANTS + AsRefStr, // AsRef, fmt::Display and serde::Serialize + EnumVariantNames, // Chain::VARIANTS EnumString, // FromStr, TryFrom<&str> - EnumIter, - EnumCount, + EnumIter, // Chain::iter + EnumCount, // Chain::COUNT TryFromPrimitive, // TryFrom Deserialize, )] @@ -212,12 +217,25 @@ impl Chain { /// **Note:** this is not an accurate average, but is rather a sensible default derived from /// blocktime charts such as [Etherscan's](https://etherscan.com/chart/blocktime) /// or [Polygonscan's](https://polygonscan.com/chart/blocktime). + /// + /// # Examples + /// + /// ``` + /// use ethers_core::types::Chain; + /// use std::time::Duration; + /// + /// assert_eq!( + /// Chain::Mainnet.average_blocktime_hint(), + /// Some(Duration::from_millis(12_000)), + /// ); + /// assert_eq!(Chain::Optimism.average_blocktime_hint(), None); + /// ``` pub const fn average_blocktime_hint(&self) -> Option { use Chain::*; let ms = match self { + Mainnet => 12_000, Arbitrum | ArbitrumTestnet | ArbitrumGoerli | ArbitrumNova => 1_300, - Mainnet | Optimism => 13_000, Polygon | PolygonMumbai => 2_100, Moonbeam | Moonriver => 12_500, BinanceSmartChain | BinanceSmartChainTestnet => 3_000, @@ -230,20 +248,82 @@ impl Chain { Emerald => 6_000, Dev | AnvilHardhat => 200, Celo | CeloAlfajores | CeloBaklava => 5_000, - // Explictly handle all network to make it easier not to forget this match when new - // networks are added. + + // Explicitly exhaustive. See NB above. Morden | Ropsten | Rinkeby | Goerli | Kovan | XDai | Chiado | Sepolia | Moonbase | - MoonbeamDev | OptimismGoerli | OptimismKovan | Poa | Sokol | Rsk | EmeraldTestnet => { - return None - } + MoonbeamDev | Optimism | OptimismGoerli | OptimismKovan | Poa | Sokol | Rsk | + EmeraldTestnet => return None, }; Some(Duration::from_millis(ms)) } + /// Returns whether the chain implements EIP-1559 (with the type 2 EIP-2718 transaction type). + /// + /// # Examples + /// + /// ``` + /// use ethers_core::types::Chain; + /// + /// assert!(!Chain::Mainnet.is_legacy()); + /// assert!(Chain::Celo.is_legacy()); + /// ``` + #[allow(clippy::match_like_matches_macro)] + pub const fn is_legacy(&self) -> bool { + use Chain::*; + + match self { + // Known legacy chains / non EIP-1559 compliant + Optimism | + OptimismGoerli | + OptimismKovan | + Fantom | + FantomTestnet | + BinanceSmartChain | + BinanceSmartChainTestnet | + Arbitrum | + ArbitrumTestnet | + ArbitrumGoerli | + ArbitrumNova | + Rsk | + Oasis | + Emerald | + EmeraldTestnet | + Celo | + CeloAlfajores | + CeloBaklava => true, + + // Known EIP-1559 chains + Mainnet | Goerli | Sepolia | Polygon | PolygonMumbai | Avalanche | AvalancheFuji => { + false + } + + // Unknown / not applicable, default to false for backwards compatibility + Dev | AnvilHardhat | Morden | Ropsten | Rinkeby | Cronos | CronosTestnet | Kovan | + Sokol | Poa | XDai | Moonbeam | MoonbeamDev | Moonriver | Moonbase | Evmos | + EvmosTestnet | Chiado | Aurora | AuroraTestnet | Canto | CantoTestnet => false, + } + } + /// Returns the chain's blockchain explorer and its API (Etherscan and Etherscan-like) URLs. /// - /// Returns `(API URL, BASE_URL)`, like `("https://api(-chain).etherscan.io/api", "https://etherscan.io")` + /// Returns `(API_URL, BASE_URL)` + /// + /// # Examples + /// + /// ``` + /// use ethers_core::types::Chain; + /// + /// assert_eq!( + /// Chain::Mainnet.etherscan_urls(), + /// Some(("https://api.etherscan.io/api", "https://etherscan.io")) + /// ); + /// assert_eq!( + /// Chain::Avalanche.etherscan_urls(), + /// Some(("https://api.snowtrace.io/api", "https://snowtrace.io")) + /// ); + /// assert_eq!(Chain::AnvilHardhat.etherscan_urls(), None); + /// ``` pub const fn etherscan_urls(&self) -> Option<(&'static str, &'static str)> { use Chain::*; @@ -328,51 +408,83 @@ impl Chain { "https://testnet-explorer.canto.neobase.one/", "https://testnet-explorer.canto.neobase.one/api", ), - AnvilHardhat | Dev | Morden | MoonbeamDev => { - // this is explicitly exhaustive so we don't forget to add new urls when adding a - // new chain - return None - } + + // Explicitly exhaustive. See NB above. + AnvilHardhat | Dev | Morden | MoonbeamDev => return None, }; Some(urls) } - /// Returns whether the chain implements EIP-1559 (with the type 2 EIP-2718 transaction type). - pub const fn is_legacy(&self) -> bool { + /// Returns the chain's blockchain explorer's API key environment variable's default name. + /// + /// # Examples + /// + /// ``` + /// use ethers_core::types::Chain; + /// + /// assert_eq!(Chain::Mainnet.etherscan_api_key_name(), Some("ETHERSCAN_API_KEY")); + /// assert_eq!(Chain::AnvilHardhat.etherscan_api_key_name(), None); + /// ``` + pub const fn etherscan_api_key_name(&self) -> Option<&'static str> { use Chain::*; - match self { - // Known legacy chains / non EIP-1559 compliant + let api_key_name = match self { + Mainnet | + Morden | + Ropsten | + Kovan | + Rinkeby | + Goerli | Optimism | OptimismGoerli | OptimismKovan | - Fantom | - FantomTestnet | BinanceSmartChain | BinanceSmartChainTestnet | Arbitrum | ArbitrumTestnet | ArbitrumGoerli | ArbitrumNova | - Rsk | - Oasis | - Emerald | - EmeraldTestnet | + Cronos | + CronosTestnet | + Aurora | + AuroraTestnet | Celo | CeloAlfajores | - CeloBaklava => true, + CeloBaklava => "ETHERSCAN_API_KEY", - // Known EIP-1559 chains - Mainnet | Goerli | Sepolia | Polygon | PolygonMumbai | Avalanche | AvalancheFuji => { - false - } + Avalanche | AvalancheFuji => "SNOWTRACE_API_KEY", - // Unknown / not applicable, default to false for backwards compatibility - Dev | AnvilHardhat | Morden | Ropsten | Rinkeby | Cronos | CronosTestnet | Kovan | - Sokol | Poa | XDai | Moonbeam | MoonbeamDev | Moonriver | Moonbase | Evmos | - EvmosTestnet | Chiado | Aurora | AuroraTestnet | Canto | CantoTestnet => false, - } + Polygon | PolygonMumbai => "POLYGONSCAN_API_KEY", + + Fantom | FantomTestnet => "FTMSCAN_API_KEY", + + Moonbeam | Moonbase | MoonbeamDev | Moonriver => "MOONSCAN_API_KEY", + + Canto | CantoTestnet => "BLOCKSCOUT_API_KEY", + + // Explicitly exhaustive. See NB above. + XDai | Chiado | Sepolia | Rsk | Sokol | Poa | Oasis | Emerald | EmeraldTestnet | + Evmos | EvmosTestnet | AnvilHardhat | Dev => return None, + }; + + Some(api_key_name) + } + + /// Returns the chain's blockchain explorer's API key, from the environment variable with the + /// name specified in [`etherscan_api_key_name`](Chain::etherscan_api_key_name). + /// + /// # Examples + /// + /// ``` + /// use ethers_core::types::Chain; + /// + /// let chain = Chain::Mainnet; + /// std::env::set_var(chain.etherscan_api_key_name().unwrap(), "KEY"); + /// assert_eq!(chain.etherscan_api_key().as_deref(), Some("KEY")); + /// ``` + pub fn etherscan_api_key(&self) -> Option { + self.etherscan_api_key_name().and_then(|name| std::env::var(name).ok()) } } diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs index 7e629e2a..d6fd450c 100644 --- a/ethers-etherscan/src/lib.rs +++ b/ethers-etherscan/src/lib.rs @@ -46,6 +46,7 @@ pub struct Client { impl Client { /// Creates a `ClientBuilder` to configure a `Client`. + /// /// This is the same as `ClientBuilder::default()`. /// /// # Example @@ -77,36 +78,15 @@ impl Client { } /// Create a new client with the correct endpoints based on the chain and API key - /// from ETHERSCAN_API_KEY environment variable + /// from the default environment variable defined in [`Chain`]. pub fn new_from_env(chain: Chain) -> Result { let api_key = match chain { - Chain::Avalanche | Chain::AvalancheFuji => std::env::var("SNOWTRACE_API_KEY")?, - Chain::Polygon | Chain::PolygonMumbai => std::env::var("POLYGONSCAN_API_KEY")?, - Chain::Mainnet | - Chain::Morden | - Chain::Ropsten | - Chain::Kovan | - Chain::Rinkeby | - Chain::Goerli | - Chain::Optimism | - Chain::OptimismGoerli | - Chain::OptimismKovan | - Chain::BinanceSmartChain | - Chain::BinanceSmartChainTestnet | - Chain::Arbitrum | - Chain::ArbitrumTestnet | - Chain::ArbitrumGoerli | - Chain::ArbitrumNova | - Chain::Cronos | - Chain::CronosTestnet | - Chain::Aurora | - Chain::AuroraTestnet | - Chain::Celo | - Chain::CeloAlfajores | - Chain::CeloBaklava => std::env::var("ETHERSCAN_API_KEY")?, - Chain::Fantom | Chain::FantomTestnet => { - std::env::var("FTMSCAN_API_KEY").or_else(|_| std::env::var("FANTOMSCAN_API_KEY"))? - } + // Extra aliases + Chain::Fantom | Chain::FantomTestnet => std::env::var("FMTSCAN_API_KEY") + .or_else(|_| std::env::var("FANTOMSCAN_API_KEY")) + .map_err(Into::into), + + // Backwards compatibility, ideally these should return an error. Chain::XDai | Chain::Chiado | Chain::Sepolia | @@ -117,15 +97,14 @@ impl Client { Chain::Emerald | Chain::EmeraldTestnet | Chain::Evmos | - Chain::EvmosTestnet => String::default(), - Chain::Moonbeam | Chain::Moonbase | Chain::MoonbeamDev | Chain::Moonriver => { - std::env::var("MOONSCAN_API_KEY")? - } - Chain::Canto | Chain::CantoTestnet => std::env::var("BLOCKSCOUT_API_KEY")?, - Chain::AnvilHardhat | Chain::Dev => { - return Err(EtherscanError::LocalNetworksNotSupported) - } - }; + Chain::EvmosTestnet => Ok(String::new()), + Chain::AnvilHardhat | Chain::Dev => Err(EtherscanError::LocalNetworksNotSupported), + + _ => chain + .etherscan_api_key_name() + .ok_or_else(|| EtherscanError::ChainNotSupported(chain)) + .and_then(|key_name| std::env::var(key_name).map_err(Into::into)), + }?; Self::new(chain, api_key) }