Repository: gakonst/ethers-rs
Pull request #2014 (2023-01-05 10:56:07)

ci: fix nonce manager test
## Motivation
CI breaks on NonceManager test on the Goerli network.

## Solution
Switched the network from Goerli to Anvil to work in a sand-box.

## PR Checklist

-   [x] Added Tests
-   [ ] Added Documentation
-   [ ] Updated the changelog
-   [ ] Breaking changes


0xMelkor: Also `ethers-middleware/tests/signer.rs` fails on Goerli. 
I'll check it
0xMelkor: > at least one failing test less, testing against anvil will be more stable than using a live network.

Hi @mattsse , does Anvil support EIP1559? I get this error on send:

```bash
{ err: Error("invalid length 1, expected a (both 0x-prefixed or not) hex string or byte array containing 8 bytes", line: 1, column: 858), text: "{\"baseFeePerGas\":\"0x3b9aca00\",\"difficulty\":\"0x0\",\"extraData\":\"0x\",\"gasLimit\":\"0x1c9c380\",\"gasUsed\":\"0x5208\",\"hash\":\"0x946b047adbc3accea9304628094709e7f246f38d998b09469d01a0bdf3a9fbd9\",\"logsBloom\":\"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"miner\":\"0x0000000000000000000000000000000000000000\",\"mixHash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"nonce\":\"0x0\",\"number\":\"0x2\",\"parentHash\":\"0x9fd5237a990c0cccaed76e7f3957d23f0e6653c8fad17f3badaf0f31c4267785\",\"receiptsRoot\":\"0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2\",\"sealFields\":[\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"0x0000000000000000\"],\"sha3Uncles\":\"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347\",\"size\":\"0x260\",\"stateRoot\":\"0x3d2fc4451b81b998ab24989c32db79421b3ae7e7d39039ac902de26f05178edd\",\"timestamp\":\"0x63b6c185\",\"totalDifficulty\":null,\"transactions\":[\"0x6e631fa20f50ef664f83265c95489b460fcffcde08f3097f723b2f4d4a8359e8\"],\"transactionsRoot\":\"0x5aa9c0fa38efd734933a8d89577e06f1dfc3074474220c27988dd3ec0d270834\",\"uncles\":[]}" }
```


mattsse: this is a serde error,

but would need more context in order to debug this, likely U64 related or something.

please open a separate issue on foundry ideally with a repro, or if you're up to it, you can debug yourself. It'd start with an e2e test ref
https://github.com/foundry-rs/foundry/blob/master/anvil/tests/it/transaction.rs
mattsse: could you please describe what exactly is the issue, and why it happens?
0xMelkor: > could you please describe what exactly is the issue, and why it is happening?

Investigating..





0xMelkor: The current code is supposed to update the value of `inner.max_priority_fee_per_gas`.

```rust
  inner
    .max_priority_fee_per_gas
    .map(|tip| std::cmp::min(tip, *mfpg))
    .get_or_insert(max_priority_fee_per_gas);
```
It looks good, but for some reason `get_or_insert` doesn't update the value.
Please note that [get_or_insert](https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.get_or_insert) is marked as unstable.

Changing the code to this updates the value, and tests run correctly.

```rust
inner.max_priority_fee_per_gas = inner
    .max_priority_fee_per_gas
    .map(|tip| std::cmp::min(tip, *mfpg))
    .or(Some(max_priority_fee_per_gas));
```

Patch:
diff --git a/book/SUMMARY.md b/book/SUMMARY.md
index 732d33c7..d3461cd5 100644
--- a/book/SUMMARY.md
+++ b/book/SUMMARY.md
@@ -9,7 +9,7 @@
 - [Providers]()
   - [Http](./providers/http.md)
   - [IPC](./providers/ipc.md)
-  - [Mock]()
+  - [Mock](./providers/mock.md)
   - [Quorum](./providers/quorum.md)
   - [Retry](./providers/retry.md)
   - [RW](./providers/rw.md)
diff --git a/book/providers/mock.md b/book/providers/mock.md
new file mode 100644
index 00000000..2363bf71
--- /dev/null
+++ b/book/providers/mock.md
@@ -0,0 +1,5 @@
+# Mock provider
+
+```rust
+{{#include ../../examples/providers/examples/mock.rs}}
+```
\ No newline at end of file
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index 11083891..54fd646e 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -359,10 +359,10 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
                     //   - first: if set, set to the min(current value, MFPG)
                     //   - second, if still unset, use the RPC estimated amount
                     let mfpg = inner.max_fee_per_gas.get_or_insert(max_fee_per_gas);
-                    inner
+                    inner.max_priority_fee_per_gas = inner
                         .max_priority_fee_per_gas
                         .map(|tip| std::cmp::min(tip, *mfpg))
-                        .get_or_insert(max_priority_fee_per_gas);
+                        .or(Some(max_priority_fee_per_gas));
                 };
             }
         }
diff --git a/examples/README.md b/examples/README.md
index 8bc4d73d..bb25a09d 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -34,10 +34,10 @@
   - [x] Signer
   - [ ] Time lag
   - [ ] Transformer
-- [ ] Providers
+- [x] Providers
   - [x] Http
   - [x] IPC
-  - [ ] Mock 
+  - [x] Mock 
   - [x] Quorum
   - [x] Retry
   - [x] RW
diff --git a/examples/providers/examples/mock.rs b/examples/providers/examples/mock.rs
new file mode 100644
index 00000000..4d80fe57
--- /dev/null
+++ b/examples/providers/examples/mock.rs
@@ -0,0 +1,80 @@
+use ethers::prelude::*;
+
+/// `MockProvider` is a mock Ethereum provider that can be used for testing purposes.
+/// It allows to simulate Ethereum state and behavior, by explicitly instructing
+/// provider's responses on client requests.
+///
+/// This can be useful for testing code that relies on providers without the need to
+/// connect to a real network or spend real Ether. It also allows to test code in a
+/// deterministic manner, as you can control the state and behavior of the provider.
+///
+/// In these examples we use the common Arrange, Act, Assert (AAA) test approach.
+/// It is a useful pattern for well-structured, understandable and maintainable tests.
+#[tokio::main]
+async fn main() -> eyre::Result<()> {
+    mocked_block_number().await?;
+    mocked_provider_dependency().await?;
+    Ok(())
+}
+
+async fn mocked_block_number() -> eyre::Result<()> {
+    // Arrange
+    let mock = MockProvider::new();
+    let block_num_1 = U64::from(1);
+    let block_num_2 = U64::from(2);
+    let block_num_3 = U64::from(3);
+    // Mock responses are organized in a stack (LIFO)
+    mock.push(block_num_1)?;
+    mock.push(block_num_2)?;
+    mock.push(block_num_3)?;
+
+    // Act
+    let ret_block_3: U64 = JsonRpcClient::request(&mock, "eth_blockNumber", ()).await?;
+    let ret_block_2: U64 = JsonRpcClient::request(&mock, "eth_blockNumber", ()).await?;
+    let ret_block_1: U64 = JsonRpcClient::request(&mock, "eth_blockNumber", ()).await?;
+
+    // Assert
+    assert_eq!(block_num_1, ret_block_1);
+    assert_eq!(block_num_2, ret_block_2);
+    assert_eq!(block_num_3, ret_block_3);
+
+    Ok(())
+}
+
+/// Here we test the `OddBlockOracle` struct (defined below) that relies
+/// on a Provider to perform some logics.
+/// The Provider reference is expressed with trait bounds, enforcing lose coupling,
+/// maintainability and testability.
+async fn mocked_provider_dependency() -> eyre::Result<()> {
+    // Arrange
+    let (provider, mock) = crate::Provider::mocked();
+    mock.push(U64::from(2))?;
+
+    // Act
+    // Let's mock the provider dependency (we ❤️ DI!) then ask for the answer
+    let oracle = OddBlockOracle::new(provider);
+    let answer: bool = oracle.is_odd_block().await?;
+
+    // Assert
+    assert!(answer);
+    Ok(())
+}
+
+struct OddBlockOracle<P> {
+    provider: Provider<P>,
+}
+
+impl<P> OddBlockOracle<P>
+where
+    P: JsonRpcClient,
+{
+    fn new(provider: Provider<P>) -> Self {
+        Self { provider }
+    }
+
+    /// We want to test this!
+    async fn is_odd_block(&self) -> eyre::Result<bool> {
+        let block: U64 = self.provider.get_block_number().await?;
+        Ok(block % 2 == U64::zero())
+    }
+}
diff --git a/examples/providers/examples/ws.rs b/examples/providers/examples/ws.rs
index 9268ce38..e4aded8a 100644
--- a/examples/providers/examples/ws.rs
+++ b/examples/providers/examples/ws.rs
@@ -4,20 +4,18 @@ use ethers::prelude::*;
 
 const WSS_URL: &str = "wss://mainnet.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27";
 
-type BoxErr = Box<dyn std::error::Error>;
-
 /// The Ws transport allows you to send JSON-RPC requests and receive responses over WebSocket
 /// connections. It is useful for connecting to Ethereum nodes that support WebSockets.
 /// This allows to interact with the Ethereum network in real-time without the need for HTTP
 /// polling.
 #[tokio::main]
-async fn main() -> Result<(), BoxErr> {
+async fn main() -> eyre::Result<()> {
     create_instance().await?;
     watch_blocks().await?;
     Ok(())
 }
 
-async fn create_instance() -> Result<(), BoxErr> {
+async fn create_instance() -> eyre::Result<()> {
     // An Ws provider can be created from an ws(s) URI.
     // In case of wss you must add the "rustls" or "openssl" feature
     // to the ethers library dependency in `Cargo.toml`.
@@ -39,7 +37,7 @@ async fn create_instance() -> Result<(), BoxErr> {
 }
 
 /// Let's show how the Ws connection enables listening for blocks using a persistent TCP connection
-async fn watch_blocks() -> Result<(), BoxErr> {
+async fn watch_blocks() -> eyre::Result<()> {
     let provider = Provider::<Ws>::connect(WSS_URL).await?;
     let mut stream = provider.watch_blocks().await?.take(1);
 


Test patch:
diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs
index 1be27fec..85368aa2 100644
--- a/ethers-middleware/tests/nonce_manager.rs
+++ b/ethers-middleware/tests/nonce_manager.rs
@@ -1,28 +1,22 @@
 #![cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
 
-use ethers_core::types::*;
-use ethers_middleware::{nonce_manager::NonceManagerMiddleware, signer::SignerMiddleware};
-use ethers_providers::Middleware;
-use ethers_signers::{LocalWallet, Signer};
-use std::time::Duration;
+use ethers_core::{types::*, utils::Anvil};
+use ethers_middleware::MiddlewareBuilder;
+use ethers_providers::{Http, Middleware, Provider};
 
 #[tokio::test]
 async fn nonce_manager() {
-    let provider = ethers_providers::GOERLI.provider().interval(Duration::from_millis(2000));
-    let chain_id = provider.get_chainid().await.unwrap().as_u64();
+    let anvil = Anvil::new().spawn();
+    let endpoint = anvil.endpoint();
 
-    let wallet = std::env::var("GOERLI_PRIVATE_KEY")
-        .expect("GOERLI_PRIVATE_KEY is not defined")
-        .parse::<LocalWallet>()
-        .unwrap()
-        .with_chain_id(chain_id);
-    let address = wallet.address();
-
-    let provider = SignerMiddleware::new(provider, wallet);
+    let provider = Provider::<Http>::try_from(endpoint).unwrap();
+    let accounts = provider.get_accounts().await.unwrap();
+    let address = accounts[0];
+    let to = accounts[1];
 
     // the nonce manager must be over the Client so that it overrides the nonce
     // before the client gets it
-    let provider = NonceManagerMiddleware::new(provider, address);
+    let provider = provider.nonce_manager(address);
 
     let nonce = provider
         .get_transaction_count(address, Some(BlockNumber::Pending.into()))
@@ -34,22 +28,16 @@ async fn nonce_manager() {
     let mut tx_hashes = Vec::with_capacity(num_tx);
     for _ in 0..num_tx {
         let tx = provider
-            .send_transaction(
-                Eip1559TransactionRequest::new().to(address).value(100u64).chain_id(chain_id),
-                None,
-            )
+            .send_transaction(TransactionRequest::new().from(address).to(to).value(100u64), None)
             .await
             .unwrap();
         tx_hashes.push(*tx);
     }
 
-    // sleep a bit to ensure there's no flakiness in the test
-    tokio::time::sleep(Duration::from_secs(10)).await;
-
     let mut nonces = Vec::with_capacity(num_tx);
     for tx_hash in tx_hashes {
         nonces.push(provider.get_transaction(tx_hash).await.unwrap().unwrap().nonce.as_u64());
     }
 
-    assert_eq!(nonces, (nonce..nonce + num_tx as u64).collect::<Vec<_>>())
+    assert_eq!(nonces, (nonce..nonce + num_tx as u64).collect::<Vec<_>>());
 }