Repository: gakonst/ethers-rs
Pull request #1818 (2022-10-31 09:36:58)

fix: get_logs_paginated fetches past latest block
<!--
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

`provider.get_logs_paginated` would try to fetch more blocks than are available, because it only considers `page_size` when fetching blocks.

## Solution

Only fetch up to `from_block + page_size` or the last block available, whichever is lower.

## PR Checklist

-   [x] Added Tests
-   [ ] Added Documentation
-   [x] Updated the changelog



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fffa6f22..06169b9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
 
 ### Unreleased
 
+-   Remove rust_decimals dependency for ethers-core
+-   Add support for numbers greater than 2^96 for `ethers_core::utils::parse_units` [#1822](https://github.com/gakonst/ethers-rs/issues/1822)
 -   Add comment about safety of u8 -> u64 cast in `ethers_core::types::Signature`
 -   Stop defaulting to the `"latest"` block in `eth_estimateGas` params [#1657](https://github.com/gakonst/ethers-rs/pull/1657)
 -   Fix geth trace types for debug_traceTransaction rpc
@@ -91,6 +93,7 @@
 -   [#1632](https://github.com/gakonst/ethers-rs/pull/1632) Re-export `H32` from `ethabi`.
 -   [#1634](https://github.com/gakonst/ethers-rs/pull/1634) Derive missing `Clone`, `Copy` and `Debug` impls in ethers-etherscan.
 -   Bytes debug format now displays hex literals [#1658](https://github.com/gakonst/ethers-rs/pull/1658)
+-   Fix `get_logs_paginated` trying to fetch beyond the latest block
 
 ## ethers-contract-abigen
 
diff --git a/Cargo.lock b/Cargo.lock
index 83356b62..3db56c4d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1316,7 +1316,6 @@ dependencies = [
  "rand 0.8.5",
  "rlp",
  "rlp-derive",
- "rust_decimal",
  "serde",
  "serde_json",
  "strum",
@@ -2388,9 +2387,9 @@ dependencies = [
 
 [[package]]
 name = "num_cpus"
-version = "1.13.1"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
 dependencies = [
  "hermit-abi",
  "libc",
@@ -3280,17 +3279,6 @@ dependencies = [
  "tokio",
 ]
 
-[[package]]
-name = "rust_decimal"
-version = "1.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee9164faf726e4f3ece4978b25ca877ddc6802fa77f38cdccb32c7f805ecd70c"
-dependencies = [
- "arrayvec 0.7.2",
- "num-traits",
- "serde",
-]
-
 [[package]]
 name = "rustc-hex"
 version = "2.1.0"
diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs
index 1f753414..c9a8ab63 100644
--- a/ethers-contract/src/call.rs
+++ b/ethers-contract/src/call.rs
@@ -14,7 +14,14 @@ use ethers_providers::{
     Middleware, PendingTransaction, ProviderError,
 };
 
-use std::{borrow::Cow, fmt::Debug, future::Future, marker::PhantomData, sync::Arc};
+use std::{
+    borrow::Cow,
+    fmt::Debug,
+    future::{Future, IntoFuture},
+    marker::PhantomData,
+    pin::Pin,
+    sync::Arc,
+};
 
 use thiserror::Error as ThisError;
 
@@ -211,3 +218,19 @@ where
             .map_err(ContractError::MiddlewareError)
     }
 }
+
+/// [`ContractCall`] can be turned into [`Future`] automatically with `.await`.
+/// Defaults to calling [`ContractCall::call`].
+impl<M, D> IntoFuture for ContractCall<M, D>
+where
+    Self: 'static,
+    M: Middleware,
+    D: Detokenize,
+{
+    type Output = Result<D, ContractError<M>>;
+    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        Box::pin(async move { self.call().await })
+    }
+}
diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs
index be81bc2d..99cde5ab 100644
--- a/ethers-contract/src/multicall/mod.rs
+++ b/ethers-contract/src/multicall/mod.rs
@@ -29,7 +29,7 @@ pub const MULTICALL_ADDRESS: Address = H160([
 
 /// The chain IDs that [`MULTICALL_ADDRESS`] has been deployed to.
 /// Taken from: https://github.com/mds1/multicall#multicall3-contract-addresses
-pub static MULTICALL_SUPPORTED_CHAIN_IDS: Lazy<[U256; 47]> = Lazy::new(|| {
+pub static MULTICALL_SUPPORTED_CHAIN_IDS: Lazy<[U256; 48]> = Lazy::new(|| {
     use Chain::*;
     [
         U256::from(Mainnet),                  // Mainnet
@@ -47,6 +47,7 @@ pub static MULTICALL_SUPPORTED_CHAIN_IDS: Lazy<[U256; 47]> = Lazy::new(|| {
         U256::from(Polygon),                  // Polygon
         U256::from(PolygonMumbai),            // PolygonMumbai
         U256::from(XDai),                     // XDai
+        U256::from(Chiado),                   // ChiadoTestnet
         U256::from(Avalanche),                // Avalanche
         U256::from(AvalancheFuji),            // AvalancheFuji
         U256::from(FantomTestnet),            // FantomTestnet
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index 281bc00c..da1eda32 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -43,7 +43,6 @@ cargo_metadata = { version = "0.15.0", optional = true }
 convert_case = { version = "0.6.0", optional = true }
 syn = { version = "1.0.103", optional = true }
 proc-macro2 = { version = "1.0.47", optional = true }
-rust_decimal = { version = "1.26.1", features = ["maths"] }
 
 [dev-dependencies]
 serde_json = { version = "1.0.64", default-features = false }
diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs
index 4645b890..0b127aa5 100644
--- a/ethers-core/src/types/chain.rs
+++ b/ethers-core/src/types/chain.rs
@@ -32,6 +32,7 @@ pub enum Chain {
     Kovan = 42,
     #[strum(serialize = "xdai")]
     XDai = 100,
+    Chiado = 10200,
     Polygon = 137,
     Fantom = 250,
     Dev = 1337,
@@ -103,6 +104,7 @@ impl Chain {
             Chain::Goerli |
             Chain::Kovan |
             Chain::XDai |
+            Chain::Chiado |
             Chain::Sepolia |
             Chain::Moonbase |
             Chain::MoonbeamDev |
@@ -186,6 +188,9 @@ impl Chain {
             Chain::XDai => {
                 ("https://blockscout.com/xdai/mainnet/api", "https://blockscout.com/xdai/mainnet")
             }
+            Chain::Chiado => {
+                ("https://blockscout.chiadochain.net/api", "https://blockscout.chiadochain.net")
+            }
             Chain::Sokol => {
                 ("https://blockscout.com/poa/sokol/api", "https://blockscout.com/poa/sokol")
             }
@@ -230,6 +235,7 @@ impl fmt::Display for Chain {
             Chain::Goerli => "goerli",
             Chain::Kovan => "kovan",
             Chain::XDai => "xdai",
+            Chain::Chiado => "chiado",
             Chain::Polygon => "polygon",
             Chain::PolygonMumbai => "mumbai",
             Chain::Avalanche => "avalanche",
@@ -299,6 +305,7 @@ impl TryFrom<u64> for Chain {
             5 => Chain::Goerli,
             42 => Chain::Kovan,
             100 => Chain::XDai,
+            10200 => Chain::Chiado,
             137 => Chain::Polygon,
             1337 => Chain::Dev,
             31337 => Chain::AnvilHardhat,
@@ -359,6 +366,7 @@ impl FromStr for Chain {
             "goerli" => Chain::Goerli,
             "kovan" => Chain::Kovan,
             "xdai" => Chain::XDai,
+            "chiado" => Chain::Chiado,
             "polygon" => Chain::Polygon,
             "mumbai" | "polygon-mumbai" => Chain::PolygonMumbai,
             "avalanche" => Chain::Avalanche,
diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs
index 68643dc2..3c2aff0e 100644
--- a/ethers-core/src/types/mod.rs
+++ b/ethers-core/src/types/mod.rs
@@ -6,7 +6,7 @@ pub type Selector = [u8; 4];
 pub use ethabi::ethereum_types::H256 as TxHash;
 
 pub use ethabi::ethereum_types::{
-    Address, BigEndianHash, Bloom, H128, H160, H256, H32, H512, H64, U128, U256, U64,
+    Address, BigEndianHash, Bloom, H128, H160, H256, H32, H512, H64, U128, U256, U64, U512,
 };
 
 pub mod transaction;
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index 5e3b1c2d..add0f031 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -56,8 +56,8 @@ pub enum ConversionError {
     InvalidFloat(#[from] std::num::ParseFloatError),
     #[error(transparent)]
     FromDecStrError(#[from] FromDecStrErr),
-    #[error(transparent)]
-    DecimalError(#[from] rust_decimal::Error),
+    #[error("Overflow parsing string")]
+    ParseOverflow,
 }
 
 /// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei
@@ -155,19 +155,27 @@ where
     S: ToString,
     K: TryInto<Units, Error = ConversionError> + Copy,
 {
-    use rust_decimal::{Decimal, MathematicalOps};
-
-    let num: Decimal = amount.to_string().parse()?;
-    let exponent = units.try_into()?.as_num();
-
-    let multiplier = Decimal::TEN
-        .checked_powu(exponent.into())
-        .ok_or(rust_decimal::Error::ExceedsMaximumPossibleValue)?;
+    let exponent: u32 = units.try_into()?.as_num();
+    let mut amount_str = amount.to_string().replace("_", "");
+    let dec_len = if let Some(di) = amount_str.find('.') {
+        amount_str.remove(di);
+        amount_str[di..].len() as u32
+    } else {
+        0
+    };
 
-    let val =
-        num.checked_mul(multiplier).ok_or(rust_decimal::Error::ExceedsMaximumPossibleValue)?;
-    let u256_n: U256 = U256::from_dec_str(&val.round().to_string())?;
-    Ok(u256_n)
+    if dec_len > exponent {
+        // Truncate the decimal part if it is longer than the exponent
+        let amount_str = &amount_str[..(amount_str.len() - (dec_len - exponent) as usize)];
+        let a_uint = U256::from_dec_str(amount_str)?;
+        Ok(a_uint)
+    } else {
+        let mut a_uint = U256::from_dec_str(&amount_str)?;
+        a_uint *= U256::from(10)
+            .checked_pow(U256::from(exponent - dec_len))
+            .ok_or(ConversionError::ParseOverflow)?;
+        Ok(a_uint)
+    }
 }
 /// The address for an Ethereum contract is deterministically computed from the
 /// address of its creator (sender) and how many transactions the creator has
@@ -474,6 +482,22 @@ mod tests {
 
         let val = parse_units("2.3", "ether").unwrap();
         assert_eq!(val, U256::from_dec_str("2300000000000000000").unwrap());
+
+        assert_eq!(parse_units(".2", 2).unwrap(), U256::from(20), "leading dot");
+        assert_eq!(parse_units("333.21", 2).unwrap(), U256::from(33321), "trailing dot");
+        assert_eq!(
+            parse_units("98766", 16).unwrap(),
+            U256::from_dec_str("987660000000000000000").unwrap(),
+            "no dot"
+        );
+        assert_eq!(parse_units("3_3_0", 3).unwrap(), U256::from(330000), "underscore");
+        assert_eq!(parse_units("330", 0).unwrap(), U256::from(330), "zero decimals");
+        assert_eq!(parse_units(".1234", 3).unwrap(), U256::from(123), "truncate too many decimals");
+        assert_eq!(parse_units("1", 80).is_err(), true, "overflow");
+        assert_eq!(parse_units("1", -1).is_err(), true, "neg units");
+        let two_e30 = U256::from(2) * U256([0x4674edea40000000, 0xc9f2c9cd0, 0x0, 0x0]);
+        assert_eq!(parse_units("2", 30).unwrap(), two_e30, "2e30");
+        assert_eq!(parse_units(".33_319_2", 0).unwrap(), U256::zero(), "mix");
     }
 
     #[test]
diff --git a/ethers-etherscan/src/lib.rs b/ethers-etherscan/src/lib.rs
index 4df3ebf2..9bd36992 100644
--- a/ethers-etherscan/src/lib.rs
+++ b/ethers-etherscan/src/lib.rs
@@ -103,6 +103,7 @@ impl Client {
                 std::env::var("FTMSCAN_API_KEY").or_else(|_| std::env::var("FANTOMSCAN_API_KEY"))?
             }
             Chain::XDai |
+            Chain::Chiado |
             Chain::Sepolia |
             Chain::Rsk |
             Chain::Sokol |
diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs
index b911a660..a6c79e15 100644
--- a/ethers-middleware/src/signer.rs
+++ b/ethers-middleware/src/signer.rs
@@ -1,9 +1,10 @@
 use ethers_core::types::{
     transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed},
-    Address, BlockId, Bytes, Signature, U256,
+    Address, BlockId, Bytes, Chain, Signature, TransactionRequest, U256,
 };
 use ethers_providers::{maybe, FromErr, Middleware, PendingTransaction};
 use ethers_signers::Signer;
+use std::convert::TryFrom;
 
 use async_trait::async_trait;
 use thiserror::Error;
@@ -251,6 +252,18 @@ where
             tx.set_chain_id(chain_id);
         }
 
+        // If a chain_id is matched to a known chain that doesn't support EIP-1559, automatically
+        // change transaction to be Legacy type.
+        if let Some(chain_id) = tx.chain_id() {
+            let chain = Chain::try_from(chain_id.as_u64());
+            if chain.unwrap_or_default().is_legacy() {
+                if let TypedTransaction::Eip1559(inner) = tx {
+                    let tx_req: TransactionRequest = inner.clone().into();
+                    *tx = TypedTransaction::Legacy(tx_req);
+                }
+            }
+        }
+
         let nonce = maybe(tx.nonce().cloned(), self.get_transaction_count(from, block)).await?;
         tx.set_nonce(nonce);
         self.inner()
@@ -338,7 +351,7 @@ where
 mod tests {
     use super::*;
     use ethers_core::{
-        types::TransactionRequest,
+        types::{Eip1559TransactionRequest, TransactionRequest},
         utils::{self, keccak256, Anvil},
     };
     use ethers_providers::Provider;
@@ -503,4 +516,72 @@ mod tests {
         let tx = client.get_transaction(hash).await.unwrap().unwrap();
         assert_eq!(tx.from, acc);
     }
+
+    #[tokio::test]
+    async fn converts_tx_to_legacy_to_match_chain() {
+        let eip1559 = Eip1559TransactionRequest {
+            from: None,
+            to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
+            value: Some(1_000_000_000.into()),
+            gas: Some(2_000_000.into()),
+            nonce: Some(U256::zero()),
+            access_list: Default::default(),
+            max_priority_fee_per_gas: None,
+            data: None,
+            chain_id: None,
+            max_fee_per_gas: None,
+        };
+        let mut tx = TypedTransaction::Eip1559(eip1559);
+
+        let chain_id = 10u64; // optimism does not support EIP-1559
+
+        // Signer middlewares now rely on a working provider which it can query the chain id from,
+        // so we make sure Anvil is started with the chain id that the expected tx was signed
+        // with
+        let anvil = Anvil::new().args(vec!["--chain-id".to_string(), chain_id.to_string()]).spawn();
+        let provider = Provider::try_from(anvil.endpoint()).unwrap();
+        let key = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
+            .parse::<LocalWallet>()
+            .unwrap()
+            .with_chain_id(chain_id);
+        let client = SignerMiddleware::new(provider, key);
+        client.fill_transaction(&mut tx, None).await.unwrap();
+
+        assert!(tx.as_eip1559_ref().is_none());
+        assert_eq!(tx, TypedTransaction::Legacy(tx.as_legacy_ref().unwrap().clone()));
+    }
+
+    #[tokio::test]
+    async fn does_not_convert_to_legacy_for_eip1559_chain() {
+        let eip1559 = Eip1559TransactionRequest {
+            from: None,
+            to: Some("F0109fC8DF283027b6285cc889F5aA624EaC1F55".parse::<Address>().unwrap().into()),
+            value: Some(1_000_000_000.into()),
+            gas: Some(2_000_000.into()),
+            nonce: Some(U256::zero()),
+            access_list: Default::default(),
+            max_priority_fee_per_gas: None,
+            data: None,
+            chain_id: None,
+            max_fee_per_gas: None,
+        };
+        let mut tx = TypedTransaction::Eip1559(eip1559);
+
+        let chain_id = 1u64; // eth main supports EIP-1559
+
+        // Signer middlewares now rely on a working provider which it can query the chain id from,
+        // so we make sure Anvil is started with the chain id that the expected tx was signed
+        // with
+        let anvil = Anvil::new().args(vec!["--chain-id".to_string(), chain_id.to_string()]).spawn();
+        let provider = Provider::try_from(anvil.endpoint()).unwrap();
+        let key = "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318"
+            .parse::<LocalWallet>()
+            .unwrap()
+            .with_chain_id(chain_id);
+        let client = SignerMiddleware::new(provider, key);
+        client.fill_transaction(&mut tx, None).await.unwrap();
+
+        assert!(tx.as_legacy_ref().is_none());
+        assert_eq!(tx, TypedTransaction::Eip1559(tx.as_eip1559_ref().unwrap().clone()));
+    }
 }
diff --git a/ethers-providers/src/log_query.rs b/ethers-providers/src/log_query.rs
index 43cf7086..ea8c145a 100644
--- a/ethers-providers/src/log_query.rs
+++ b/ethers-providers/src/log_query.rs
@@ -93,7 +93,7 @@ where
                         // this is okay because we will only enter this state when the filter is
                         // paginatable i.e. from block is set
                         let from_block = self.filter.get_from_block().unwrap();
-                        let to_block = from_block + self.page_size;
+                        let to_block = std::cmp::min(from_block + self.page_size, last_block);
                         self.from_block = Some(to_block + 1);
 
                         let filter = self.filter.clone().from_block(from_block).to_block(to_block);
@@ -122,7 +122,7 @@ where
                         // load new logs if there are still more pages to go through
                         // can safely assume this will always be set in this state
                         let from_block = self.from_block.unwrap();
-                        let to_block = from_block + self.page_size;
+                        let to_block = std::cmp::min(from_block + self.page_size, self.last_block.unwrap());
 
                         // no more pages to load, and everything is consumed
                         // can safely assume this will always be set in this state
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index f275cd72..1d247d58 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -31,7 +31,7 @@ hex = "0.4.3"
 yansi = "0.5.1"
 glob = "0.3.0"
 tracing = "0.1.37"
-num_cpus = "1.13.1"
+num_cpus = "1.14.0"
 tiny-keccak = { version = "2.0.2", default-features = false }
 tempfile = { version = "3.3.0", optional = true }
 fs_extra = { version = "1.2.0", optional = true }
diff --git a/examples/subscribe_contract_events.rs b/examples/subscribe_contract_events.rs
new file mode 100644
index 00000000..a6f769f9
--- /dev/null
+++ b/examples/subscribe_contract_events.rs
@@ -0,0 +1,38 @@
+use ethers::prelude::*;
+use eyre::Result;
+use std::sync::Arc;
+
+// Generate the type-safe contract bindings by providing the ABI
+// definition in human readable format
+abigen!(
+    ERC20,
+    r#"[
+        event  Transfer(address indexed src, address indexed dst, uint wad)
+    ]"#,
+);
+
+// In order to run this example you need to include Ws and TLS features
+// Run this example with
+// `cargo run -p ethers --example subscribe_contract_events --features="ws","rustls"`
+#[tokio::main]
+async fn main() -> Result<()> {
+    let client =
+        Provider::<Ws>::connect("wss://mainnet.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27")
+            .await?;
+
+    let client = Arc::new(client);
+
+    // WETH Token
+    let address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse::<Address>()?;
+    let weth = ERC20::new(address, Arc::clone(&client));
+
+    // Subscribe Transfer events
+    let events = weth.events();
+    let mut stream = events.stream().await?;
+
+    while let Some(Ok(event)) = stream.next().await {
+        println!("src: {:?}, dst: {:?}, wad: {:?}", event.src, event.dst, event.wad);
+    }
+
+    Ok(())
+}
diff --git a/examples/subscribe_contract_events_with_meta.rs b/examples/subscribe_contract_events_with_meta.rs
new file mode 100644
index 00000000..c81b4da8
--- /dev/null
+++ b/examples/subscribe_contract_events_with_meta.rs
@@ -0,0 +1,53 @@
+use ethers::prelude::*;
+use eyre::Result;
+use std::sync::Arc;
+
+// Generate the type-safe contract bindings by providing the ABI
+// definition in human readable format
+abigen!(
+    ERC20,
+    r#"[
+        event  Transfer(address indexed src, address indexed dst, uint wad)
+    ]"#,
+);
+
+// In order to run this example you need to include Ws and TLS features
+// Run this example with
+// `cargo run -p ethers --example subscribe_contract_events_with_meta --features="ws","rustls"`
+#[tokio::main]
+async fn main() -> Result<()> {
+    let client =
+        Provider::<Ws>::connect("wss://mainnet.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27")
+            .await?;
+
+    let client = Arc::new(client);
+
+    // WETH Token
+    let address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse::<Address>()?;
+    let weth = ERC20::new(address, Arc::clone(&client));
+
+    // Subscribe Transfer events
+    let events = weth.events();
+    let mut stream = events.stream().await?.with_meta();
+    while let Some(Ok((event, meta))) = stream.next().await {
+        println!("src: {:?}, dst: {:?}, wad: {:?}", event.src, event.dst, event.wad);
+
+        println!(
+            r#"address: {:?}, 
+               block_number: {:?}, 
+               block_hash: {:?}, 
+               transaction_hash: {:?}, 
+               transaction_index: {:?}, 
+               log_index: {:?}
+            "#,
+            meta.address,
+            meta.block_number,
+            meta.block_hash,
+            meta.transaction_hash,
+            meta.transaction_index,
+            meta.log_index
+        );
+    }
+
+    Ok(())
+}
diff --git a/examples/subscribe_logs.rs b/examples/subscribe_logs.rs
index 18216db9..0c74f78f 100644
--- a/examples/subscribe_logs.rs
+++ b/examples/subscribe_logs.rs
@@ -2,6 +2,8 @@ use ethers::{abi::AbiDecode, prelude::*, utils::keccak256};
 use eyre::Result;
 use std::sync::Arc;
 
+// In order to run this example you need to include Ws and TLS features
+// Run this example with `cargo run -p ethers --example subscribe_logs --features="ws","rustls"`
 #[tokio::main]
 async fn main() -> Result<()> {
     let client =


Test patch:
diff --git a/ethers-providers/tests/log_query.rs b/ethers-providers/tests/log_query.rs
new file mode 100644
index 00000000..641272c6
--- /dev/null
+++ b/ethers-providers/tests/log_query.rs
@@ -0,0 +1,21 @@
+#![cfg(not(target_arch = "wasm32"))]
+use ethers_core::{
+    types::{Filter, BlockNumber},
+    utils::Anvil,
+};
+use ethers_providers::{Http, Middleware, Provider, StreamExt};
+use std::convert::TryFrom;
+
+#[tokio::test]
+async fn get_logs_paginated() {
+    let geth = Anvil::new().block_time(20u64).spawn();
+    let provider = Provider::<Http>::try_from(geth.endpoint()).unwrap();
+
+    let filter = Filter::new().from_block(BlockNumber::Latest);
+    // try to get beyond the number of blocks available
+    let mut stream = provider.get_logs_paginated(&filter, 10);
+    let res = stream.next().await;
+    assert!(res.is_some());
+    let log = res.unwrap();
+    assert!(log.is_ok());
+}