Repository: gakonst/ethers-rs
Pull request #1017 (2022-03-13 15:58:47)

feat: rotating infura keys
Instead of us getting rate limited on 1 key, introduce a lazy static struct which rotates through a list of API keys to avoid rate limiting during unit testing


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2173af74..cf3431f7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1322,6 +1322,7 @@ dependencies = [
  "futures-util",
  "hex",
  "http",
+ "once_cell",
  "parking_lot",
  "pin-project",
  "reqwest",
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index 0d921325..56090340 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -39,6 +39,7 @@ tracing = { version = "0.1.32", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] }
 
 bytes = { version  = "1.1.0", default-features = false, optional = true }
+once_cell = "1.10.0"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 # tokio
diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs
index d169bcc2..00e90d9c 100644
--- a/ethers-providers/src/lib.rs
+++ b/ethers-providers/src/lib.rs
@@ -657,3 +657,53 @@ pub trait CeloMiddleware: Middleware {
         self.provider().get_validators_bls_public_keys(block_id).await.map_err(FromErr::from)
     }
 }
+
+pub use test_provider::{GOERLI, MAINNET, RINKEBY, ROPSTEN};
+
+/// Pre-instantiated Infura HTTP clients which rotate through multiple API keys
+/// to prevent rate limits
+pub mod test_provider {
+    use super::*;
+    use crate::Http;
+    use once_cell::sync::Lazy;
+    use std::{convert::TryFrom, iter::Cycle, slice::Iter, sync::Mutex};
+
+    // List of infura keys to rotate through so we don't get rate limited
+    const INFURA_KEYS: &[&str] = &[
+        "6770454bc6ea42c58aac12978531b93f",
+        "7a8769b798b642f6933f2ed52042bd70",
+        "631fd9a6539644088297dc605d35fff3",
+        "16a8be88795540b9b3903d8de0f7baa5",
+        "f4a0bdad42674adab5fc0ac077ffab2b",
+        "5c812e02193c4ba793f8c214317582bd",
+    ];
+
+    pub static RINKEBY: Lazy<TestProvider> =
+        Lazy::new(|| TestProvider::new(INFURA_KEYS, "rinkeby"));
+    pub static MAINNET: Lazy<TestProvider> =
+        Lazy::new(|| TestProvider::new(INFURA_KEYS, "mainnet"));
+    pub static GOERLI: Lazy<TestProvider> = Lazy::new(|| TestProvider::new(INFURA_KEYS, "goerli"));
+    pub static ROPSTEN: Lazy<TestProvider> =
+        Lazy::new(|| TestProvider::new(INFURA_KEYS, "ropsten"));
+
+    #[derive(Debug)]
+    pub struct TestProvider {
+        network: String,
+        keys: Mutex<Cycle<Iter<'static, &'static str>>>,
+    }
+
+    impl TestProvider {
+        pub fn new(keys: &'static [&'static str], network: &str) -> Self {
+            Self { keys: Mutex::new(keys.iter().cycle()), network: network.to_owned() }
+        }
+
+        pub fn provider(&self) -> Provider<Http> {
+            let url = format!(
+                "https://{}.infura.io/v3/{}",
+                self.network,
+                self.keys.lock().unwrap().next().unwrap()
+            );
+            Provider::try_from(url.as_str()).unwrap()
+        }
+    }
+}
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index dee01796..6f6c7ea8 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -1488,12 +1488,10 @@ mod tests {
     };
     use futures_util::StreamExt;
 
-    const INFURA: &str = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
-
     #[tokio::test]
     // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
     async fn mainnet_resolve_name() {
-        let provider = Provider::<HttpProvider>::try_from(INFURA).unwrap();
+        let provider = crate::test_provider::MAINNET.provider();
 
         let addr = provider.resolve_name("registrar.firefly.eth").await.unwrap();
         assert_eq!(addr, "6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap());
@@ -1508,7 +1506,7 @@ mod tests {
     #[tokio::test]
     // Test vector from: https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#id2
     async fn mainnet_lookup_address() {
-        let provider = Provider::<HttpProvider>::try_from(INFURA).unwrap();
+        let provider = crate::MAINNET.provider();
 
         let name = provider
             .lookup_address("6fC21092DA55B392b045eD78F4732bff3C580e2c".parse().unwrap())
@@ -1525,7 +1523,7 @@ mod tests {
 
     #[tokio::test]
     async fn mainnet_resolve_avatar() {
-        let provider = Provider::<HttpProvider>::try_from(INFURA).unwrap();
+        let provider = crate::MAINNET.provider();
 
         for (ens_name, res) in &[
             // HTTPS


Test patch:
diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs
index 30b3fa03..5e0a23dc 100644
--- a/ethers-middleware/tests/nonce_manager.rs
+++ b/ethers-middleware/tests/nonce_manager.rs
@@ -4,14 +4,11 @@
 async fn nonce_manager() {
     use ethers_core::types::*;
     use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
-    use ethers_providers::{Http, Middleware, Provider};
+    use ethers_providers::Middleware;
     use ethers_signers::{LocalWallet, Signer};
-    use std::{convert::TryFrom, time::Duration};
+    use std::time::Duration;
 
-    let provider =
-        Provider::<Http>::try_from("https://rinkeby.infura.io/v3/fd8b88b56aa84f6da87b60f5441d6778")
-            .unwrap()
-            .interval(Duration::from_millis(2000u64));
+    let provider = ethers_providers::RINKEBY.provider().interval(Duration::from_millis(2000u64));
     let chain_id = provider.get_chainid().await.unwrap().as_u64();
 
     let wallet = std::env::var("RINKEBY_PRIVATE_KEY")
diff --git a/ethers-middleware/tests/signer.rs b/ethers-middleware/tests/signer.rs
index e5cf3570..6ec3fea6 100644
--- a/ethers-middleware/tests/signer.rs
+++ b/ethers-middleware/tests/signer.rs
@@ -1,5 +1,5 @@
 #![allow(unused)]
-use ethers_providers::{Http, JsonRpcClient, Middleware, Provider};
+use ethers_providers::{Http, JsonRpcClient, Middleware, Provider, RINKEBY};
 
 use ethers_core::{
     types::{BlockNumber, TransactionRequest},
@@ -8,7 +8,7 @@ use ethers_core::{
 use ethers_middleware::signer::SignerMiddleware;
 use ethers_signers::{coins_bip39::English, LocalWallet, MnemonicBuilder, Signer};
 use once_cell::sync::Lazy;
-use std::{convert::TryFrom, sync::atomic::AtomicU8, time::Duration};
+use std::{convert::TryFrom, iter::Cycle, sync::atomic::AtomicU8, time::Duration};
 
 static WALLETS: Lazy<TestWallets> = Lazy::new(|| {
     TestWallets {
@@ -54,10 +54,7 @@ async fn send_eth() {
 #[tokio::test]
 #[cfg(not(feature = "celo"))]
 async fn pending_txs_with_confirmations_testnet() {
-    let provider =
-        Provider::<Http>::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")
-            .unwrap()
-            .interval(Duration::from_millis(3000));
+    let provider = RINKEBY.provider().interval(Duration::from_millis(3000));
     let chain_id = provider.get_chainid().await.unwrap();
     let wallet = WALLETS.next().with_chain_id(chain_id.as_u64());
     let address = wallet.address();
@@ -97,9 +94,7 @@ async fn generic_pending_txs_test<M: Middleware>(provider: M, who: Address) {
 #[tokio::test]
 #[cfg(not(feature = "celo"))]
 async fn typed_txs() {
-    let provider =
-        Provider::<Http>::try_from("https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")
-            .unwrap();
+    let provider = RINKEBY.provider();
 
     let chain_id = provider.get_chainid().await.unwrap();
     let wallet = WALLETS.next().with_chain_id(chain_id.as_u64());
diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs
index 3caec3cf..a6ffa2b8 100644
--- a/ethers-providers/tests/provider.rs
+++ b/ethers-providers/tests/provider.rs
@@ -1,5 +1,5 @@
 #![cfg(not(target_arch = "wasm32"))]
-use ethers_providers::{Http, Middleware, Provider};
+use ethers_providers::{Http, Middleware, Provider, RINKEBY};
 use std::{convert::TryFrom, time::Duration};
 
 #[cfg(not(feature = "celo"))]
@@ -12,10 +12,7 @@ mod eth_tests {
 
     #[tokio::test]
     async fn non_existing_data_works() {
-        let provider = Provider::<Http>::try_from(
-            "https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
-        )
-        .unwrap();
+        let provider = RINKEBY.provider();
 
         assert!(provider.get_transaction(H256::zero()).await.unwrap().is_none());
         assert!(provider.get_transaction_receipt(H256::zero()).await.unwrap().is_none());
@@ -25,10 +22,7 @@ mod eth_tests {
 
     #[tokio::test]
     async fn client_version() {
-        let provider = Provider::<Http>::try_from(
-            "https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
-        )
-        .unwrap();
+        let provider = RINKEBY.provider();
 
         // e.g., Geth/v1.10.6-omnibus-1af33248/linux-amd64/go1.16.6
         assert!(provider
@@ -95,10 +89,7 @@ mod eth_tests {
 
     #[tokio::test]
     async fn eip1559_fee_estimation() {
-        let provider = Provider::<Http>::try_from(
-            "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27",
-        )
-        .unwrap();
+        let provider = ethers_providers::MAINNET.provider();
 
         let (_max_fee_per_gas, _max_priority_fee_per_gas) =
             provider.estimate_eip1559_fees(None).await.unwrap();