Repository: gakonst/ethers-rs
Pull request #1877 (2022-11-22 02:14:03)

feat: retry client wasm support
Retry client builds under wasm32 now.

Curious what you think about the change to `maybe_connectivity`. `ReqwestErr` does not have `is_connect` in the wasm build, so we just don't check it. I don't think this should cause any major issues.

Also I added a new `TimerError` to `RetryClientError` which seemed a bit frivolous, but delays using `wasm_timer` can fail, so needed to cast it to some sort of error.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 992be8ad..59f25388 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Unreleased
 
+-   An `Event` builder can be instantiated specifying the event filter type, without the need to instantiate a contract.
 -   Add 'ethers_core::types::OpCode' and use in 'ethers_core::types::VMOperation' [1857](https://github.com/gakonst/ethers-rs/issues/1857)
 -   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)
@@ -348,4 +349,4 @@
 ### 0.5.3
 
 -   Added Time Lagged middleware
-    [#457](https://github.com/gakonst/ethers-rs/pull/457)
+    [#457](https://github.com/gakonst/ethers-rs/pull/457)
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index d5784d56..7a0587a6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -372,9 +372,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes"
-version = "1.2.1"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
 dependencies = [
  "serde",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index f6c40001..d2eea1dd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -118,7 +118,7 @@ serde = { version = "1.0.124", features = ["derive"] }
 serde_json = "1.0.64"
 tokio = { version = "1.18", features = ["macros", "rt-multi-thread"] }
 hex = "0.4.3"
-bytes = "1.2.1"
+bytes = "1.3.0"
 
 # profile for the wasm example
 [profile.release.package.ethers-wasm]
diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs
index 2a4e2c8c..7da01b7c 100644
--- a/ethers-contract/src/contract.rs
+++ b/ethers-contract/src/contract.rs
@@ -191,6 +191,19 @@ impl<M> Contract<M> {
     }
 }
 
+impl<M: Middleware> Contract<M> {
+    /// Returns an [`Event`](crate::builders::Event) builder for the provided event.
+    /// This function operates in a static context, then it does not require a `self`
+    /// to reference to instantiate an [`Event`](crate::builders::Event) builder.
+    pub fn event_of_type<'a, D: EthEvent>(client: &'a Arc<M>) -> Event<'a, M, D> {
+        Event {
+            provider: client,
+            filter: Filter::new().event(&D::abi_signature()),
+            datatype: PhantomData,
+        }
+    }
+}
+
 impl<M: Middleware> Contract<M> {
     /// Creates a new contract from the provided client, abi and address
     pub fn new(
diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs
index b4a922d4..23864128 100644
--- a/ethers-contract/src/event.rs
+++ b/ethers-contract/src/event.rs
@@ -2,8 +2,8 @@
 
 use crate::{log::LogMeta, stream::EventStream, ContractError, EthLogDecode};
 use ethers_core::{
-    abi::{Detokenize, RawLog},
-    types::{BlockNumber, Filter, Log, Topic, H256},
+    abi::{Address, Detokenize, RawLog},
+    types::{BlockNumber, Filter, Log, Topic, ValueOrArray, H256},
 };
 use ethers_providers::{FilterWatcher, Middleware, PubsubClient, SubscriptionStream};
 use std::{borrow::Cow, marker::PhantomData};
@@ -108,6 +108,12 @@ impl<M, D: EthLogDecode> Event<'_, M, D> {
         self.filter.topics[3] = Some(topic.into());
         self
     }
+
+    /// Sets the filter's address.
+    pub fn address(mut self, address: ValueOrArray<Address>) -> Self {
+        self.filter = self.filter.address(address);
+        self
+    }
 }
 
 impl<'a, M, D> Event<'a, M, D>
diff --git a/ethers-contract/src/multicall/multicall_contract.rs b/ethers-contract/src/multicall/multicall_contract.rs
index 661ac998..d3a15266 100644
--- a/ethers-contract/src/multicall/multicall_contract.rs
+++ b/ethers-contract/src/multicall/multicall_contract.rs
@@ -8,6 +8,22 @@ pub mod multicall_3 {
     mod ethers_contract {
         pub use crate::*;
     }
+
+    // This is a hack to guarantee all ethers-derive macros can find the types.
+    // See [`ethers_core::macros::determine_ethers_crates`]
+    #[doc(hidden)]
+    mod ethers {
+        pub mod core {
+            pub use ethers_core::*;
+        }
+        pub mod contract {
+            pub use crate::*;
+        }
+        pub mod providers {
+            pub use ethers_providers::*;
+        }
+    }
+
     use self::ethers_contract::{
         builders::{ContractCall, Event},
         Contract, Lazy,
@@ -17,6 +33,7 @@ pub mod multicall_3 {
         types::*,
     };
     use ethers_providers::Middleware;
+
     #[doc = "Multicall3 was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs"]
     use std::sync::Arc;
     # [rustfmt :: skip] const __ABI : & str = "[{\"type\":\"function\",\"name\":\"aggregate\",\"inputs\":[{\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"address\"},{\"type\":\"bytes\"}]}],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"aggregate3\",\"inputs\":[{\"internalType\":\"struct Multicall3.Call3[]\",\"name\":\"calls\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"address\"},{\"type\":\"bool\"},{\"type\":\"bytes\"}]}],\"outputs\":[{\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"bool\"},{\"type\":\"bytes\"}]}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"aggregate3Value\",\"inputs\":[{\"internalType\":\"struct Multicall3.Call3Value[]\",\"name\":\"calls\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"address\"},{\"type\":\"bool\"},{\"type\":\"uint256\"},{\"type\":\"bytes\"}]}],\"outputs\":[{\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"bool\"},{\"type\":\"bytes\"}]}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"blockAndAggregate\",\"inputs\":[{\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"address\"},{\"type\":\"bytes\"}]}],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"bool\"},{\"type\":\"bytes\"}]}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"getBasefee\",\"inputs\":[],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"basefee\",\"type\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getBlockHash\",\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getBlockNumber\",\"inputs\":[],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getChainId\",\"inputs\":[],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"chainid\",\"type\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getCurrentBlockCoinbase\",\"inputs\":[],\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getCurrentBlockDifficulty\",\"inputs\":[],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getCurrentBlockGasLimit\",\"inputs\":[],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getCurrentBlockTimestamp\",\"inputs\":[],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getEthBalance\",\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"getLastBlockHash\",\"inputs\":[],\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\"},{\"type\":\"function\",\"name\":\"tryAggregate\",\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"address\"},{\"type\":\"bytes\"}]}],\"outputs\":[{\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"bool\"},{\"type\":\"bytes\"}]}],\"stateMutability\":\"payable\"},{\"type\":\"function\",\"name\":\"tryBlockAndAggregate\",\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"internalType\":\"struct Multicall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"address\"},{\"type\":\"bytes\"}]}],\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"internalType\":\"struct Multicall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\",\"components\":[{\"type\":\"bool\"},{\"type\":\"bytes\"}]}],\"stateMutability\":\"payable\"}]" ;
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index eb64e1e3..889e287b 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -29,7 +29,7 @@ chrono = { version = "0.4", default-features = false }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
 thiserror = { version = "1.0", default-features = false }
-bytes = { version = "1.2.1", features = ["serde"] }
+bytes = { version = "1.3.0", features = ["serde"] }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
 once_cell = { version = "1.16.0", optional = true }
 unicode-xid = "0.2.4"
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index 32aa1f84..21da3047 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -35,7 +35,10 @@ use crate::types::{Address, Bytes, ParseI256Error, I256, U256};
 use elliptic_curve::sec1::ToEncodedPoint;
 use ethabi::ethereum_types::FromDecStrErr;
 use k256::{ecdsa::SigningKey, PublicKey as K256PublicKey};
-use std::convert::{TryFrom, TryInto};
+use std::{
+    convert::{TryFrom, TryInto},
+    fmt,
+};
 use thiserror::Error;
 
 /// Re-export of serde-json
@@ -80,7 +83,7 @@ pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200;
 
 /// This enum holds the numeric types that a possible to be returned by `parse_units` and
 /// that are taken by `format_units`.
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
 pub enum ParseUnits {
     U256(U256),
     I256(I256),
@@ -95,6 +98,15 @@ impl From<ParseUnits> for U256 {
     }
 }
 
+impl fmt::Display for ParseUnits {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            ParseUnits::U256(val) => val.fmt(f),
+            ParseUnits::I256(val) => val.fmt(f),
+        }
+    }
+}
+
 macro_rules! construct_format_units_from {
     ($( $t:ty[$convert:ident] ),*) => {
         $(
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index e4d189f2..7c25401e 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -39,7 +39,7 @@ pin-project = { version = "1.0.11", default-features = false }
 tracing = { version = "0.1.37", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] }
 
-bytes = { version = "1.2.1", default-features = false, optional = true }
+bytes = { version = "1.3.0", default-features = false, optional = true }
 once_cell = "1.16.0"
 hashers = "1.0.1"
 
diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/transports/mod.rs
index 2abf77c7..5623675d 100644
--- a/ethers-providers/src/transports/mod.rs
+++ b/ethers-providers/src/transports/mod.rs
@@ -38,9 +38,7 @@ pub use quorum::{JsonRpcClientWrapper, Quorum, QuorumError, QuorumProvider, Weig
 mod rw;
 pub use rw::{RwClient, RwClientError};
 
-#[cfg(not(target_arch = "wasm32"))]
 mod retry;
-#[cfg(not(target_arch = "wasm32"))]
 pub use retry::*;
 
 mod mock;
diff --git a/ethers-providers/src/transports/retry.rs b/ethers-providers/src/transports/retry.rs
index 947a0495..11b7386b 100644
--- a/ethers-providers/src/transports/retry.rs
+++ b/ethers-providers/src/transports/retry.rs
@@ -206,6 +206,7 @@ pub enum RetryClientError {
     TimeoutError,
     #[error(transparent)]
     SerdeJson(serde_json::Error),
+    TimerError,
 }
 
 impl std::fmt::Display for RetryClientError {
@@ -220,11 +221,13 @@ impl From<RetryClientError> for ProviderError {
             RetryClientError::ProviderError(err) => err,
             RetryClientError::TimeoutError => ProviderError::JsonRpcClientError(Box::new(src)),
             RetryClientError::SerdeJson(err) => err.into(),
+            RetryClientError::TimerError => ProviderError::JsonRpcClientError(Box::new(src)),
         }
     }
 }
 
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 impl<T> JsonRpcClient for RetryClient<T>
 where
     T: JsonRpcClient + 'static,
@@ -310,6 +313,13 @@ where
                 next_backoff += Duration::from_secs(seconds_to_wait_for_compute_budget);
 
                 trace!("retrying and backing off for {:?}", next_backoff);
+
+                #[cfg(target_arch = "wasm32")]
+                wasm_timer::Delay::new(next_backoff)
+                    .await
+                    .map_err(|_| RetryClientError::TimerError)?;
+
+                #[cfg(not(target_arch = "wasm32"))]
                 tokio::time::sleep(next_backoff).await;
             } else {
                 let err: ProviderError = err.into();
@@ -346,6 +356,12 @@ impl RetryPolicy<ClientError> for HttpRateLimitRetryPolicy {
                 if *code == 429 {
                     return true
                 }
+
+                // alternative alchemy error for specific IPs
+                if *code == -32016 && message.contains("rate limit") {
+                    return true
+                }
+
                 match message.as_str() {
                     // this is commonly thrown by infura and is apparently a load balancer issue, see also <https://github.com/MetaMask/metamask-extension/issues/7234>
                     "header not found" => true,
@@ -407,9 +423,15 @@ fn compute_unit_offset_in_secs(
 /// `request::Error::TimedOut`
 fn maybe_connectivity(err: &ProviderError) -> bool {
     if let ProviderError::HTTPError(reqwest_err) = err {
-        if reqwest_err.is_timeout() || reqwest_err.is_connect() {
+        if reqwest_err.is_timeout() {
+            return true
+        }
+
+        #[cfg(not(target_arch = "wasm32"))]
+        if reqwest_err.is_connect() {
             return true
         }
+
         // Error HTTP codes (5xx) are considered connectivity issues and will prompt retry
         if let Some(status) = reqwest_err.status() {
             let code = status.as_u16();
@@ -494,4 +516,14 @@ mod tests {
         let backoff = HttpRateLimitRetryPolicy.backoff_hint(&err);
         assert!(backoff.is_none());
     }
+
+    #[test]
+    fn test_alchemy_ip_rate_limit() {
+        let s = "{\"code\":-32016,\"message\":\"Your IP has exceeded its requests per second capacity. To increase your rate limits, please sign up for a free Alchemy account at https://www.alchemy.com/optimism.\"}";
+        let err: JsonRpcError = serde_json::from_str(s).unwrap();
+        let err = ClientError::JsonRpcError(err);
+
+        let should_retry = HttpRateLimitRetryPolicy::default().should_retry(&err);
+        assert!(should_retry);
+    }
 }
diff --git a/examples/subscribe_events_by_type.rs b/examples/subscribe_events_by_type.rs
new file mode 100644
index 00000000..9a3aa56f
--- /dev/null
+++ b/examples/subscribe_events_by_type.rs
@@ -0,0 +1,52 @@
+use ethers::{contract::Contract, prelude::*};
+use std::{error::Error, sync::Arc};
+abigen!(
+    AggregatorInterface,
+    r#"[
+        event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt)
+    ]"#,
+);
+
+const PRICE_FEED_1: &str = "0x7de93682b9b5d80d45cd371f7a14f74d49b0914c";
+const PRICE_FEED_2: &str = "0x0f00392fcb466c0e4e4310d81b941e07b4d5a079";
+const PRICE_FEED_3: &str = "0xebf67ab8cff336d3f609127e8bbf8bd6dd93cd81";
+
+/// Subscribe to a typed event stream without requiring a `Contract` instance.
+/// In this example we subscribe Chainlink price feeds and filter out them
+/// by address.
+/// -------------------------------------------------------------------------------
+/// In order to run this example you need to include Ws and TLS features
+/// Run this example with
+/// `cargo run -p ethers --example subscribe_events_by_type --features="ws","rustls"`
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn Error>> {
+    let client = get_client().await;
+    let client = Arc::new(client);
+
+    // Build an Event by type. We are not tied to a contract instance. We use builder functions to
+    // refine the event filter
+    let event = Contract::event_of_type::<AnswerUpdatedFilter>(&client)
+        .from_block(16022082)
+        .to_block(16022282)
+        .address(ValueOrArray::Array(vec![
+            PRICE_FEED_1.parse()?,
+            PRICE_FEED_2.parse()?,
+            PRICE_FEED_3.parse()?,
+        ]));
+
+    let mut stream = event.subscribe_with_meta().await?;
+
+    // Note that `log` has type AnswerUpdatedFilter
+    while let Some(Ok((log, meta))) = stream.next().await {
+        println!("{:?}", log);
+        println!("{:?}", meta)
+    }
+
+    Ok(())
+}
+
+async fn get_client() -> Provider<Ws> {
+    Provider::<Ws>::connect("wss://mainnet.infura.io/ws/v3/c60b0bb42f8a4c6481ecd229eddaca27")
+        .await
+        .unwrap()
+}


Test patch:
diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs
index 5968b19d..0c28bcde 100644
--- a/ethers-contract/tests/it/contract.rs
+++ b/ethers-contract/tests/it/contract.rs
@@ -6,7 +6,7 @@ use ethers_core::types::{Filter, ValueOrArray, H256};
 #[cfg(not(feature = "celo"))]
 mod eth_tests {
     use super::*;
-    use ethers_contract::{LogMeta, Multicall, MulticallVersion};
+    use ethers_contract::{EthEvent, LogMeta, Multicall, MulticallVersion};
     use ethers_core::{
         abi::{encode, Detokenize, Token, Tokenizable},
         types::{transaction::eip712::Eip712, Address, BlockId, Bytes, I256, U256},
@@ -324,6 +324,21 @@ mod eth_tests {
         assert_eq!(log_2.address, contract_2.address());
     }
 
+    #[tokio::test]
+    async fn build_event_of_type() {
+        abigen!(
+            AggregatorInterface,
+            r#"[
+                event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt)
+            ]"#,
+        );
+
+        let anvil = Anvil::new().spawn();
+        let client = connect(&anvil, 0);
+        let event = ethers_contract::Contract::event_of_type::<AnswerUpdatedFilter>(&client);
+        assert_eq!(event.filter, Filter::new().event(&AnswerUpdatedFilter::abi_signature()));
+    }
+
     #[tokio::test]
     async fn signer_on_node() {
         let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");