Repository: gakonst/ethers-rs
Pull request #2044 (2023-01-11 13:48:01)

feat(contract): return multicall pending transaction
<!--
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

2 year-old TODO comment from #107

```txt
TODO: Can we make this return a PendingTransaction directly instead?
Seems hard due to `returns a value referencing data owned by the current function`
```

The only data that `PendingTransaction` has to hold a reference to is `M::Provider`. which we can get with `Arc::as_ref` but this is currently not exposed

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

add `client_ref` method that returns `Arc::as_ref(self.client)` for `Contract`
return `PendingTransaction` in `Multicall::send`

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

-   [ ] Added Tests
-   [x] Added Documentation
-   [ ] Updated the changelog
-   [x] Breaking changes



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 608599c7..3208a490 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Unreleased
 
+- Avoid unnecessary allocations in `utils` [#2046](https://github.com/gakonst/ethers-rs/pull/2046)
 - Add abigen support for hardhat generated bytecode json format [#2012](https://github.com/gakonst/ethers-rs/pull/2012)
 - Fix typo in `RwClient` docs for `write_client` method.
 - Add support for Geth `debug_traceCall` [#1949](https://github.com/gakonst/ethers-rs/pull/1949)
@@ -290,6 +291,8 @@
 
 ### Unreleased
 
+- Return pending transaction from `Multicall::send`
+  [#2044](https://github.com/gakonst/ethers-rs/pull/2044)
 - Add abigen to default features
   [#1684](https://github.com/gakonst/ethers-rs/pull/1684)
 - Add extra Multicall helper methods
diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs
index 5f417611..09a2d713 100644
--- a/ethers-contract/src/contract.rs
+++ b/ethers-contract/src/contract.rs
@@ -4,21 +4,18 @@ use crate::{
     event::{EthEvent, Event},
     EthLogDecode,
 };
-
 use ethers_core::{
     abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize},
     types::{Address, Filter, Selector, ValueOrArray},
 };
+use ethers_providers::Middleware;
+use std::{marker::PhantomData, sync::Arc};
 
 #[cfg(not(feature = "legacy"))]
 use ethers_core::types::Eip1559TransactionRequest;
 #[cfg(feature = "legacy")]
 use ethers_core::types::TransactionRequest;
 
-use ethers_providers::Middleware;
-
-use std::{fmt::Debug, marker::PhantomData, sync::Arc};
-
 /// A Contract is an abstraction of an executable program on the Ethereum Blockchain.
 /// It has code (called byte code) as well as allocated long-term memory
 /// (called storage). Every deployed Contract has an address, which is used to connect
@@ -161,6 +158,7 @@ pub struct Contract<M> {
 
 impl<M> std::ops::Deref for Contract<M> {
     type Target = BaseContract;
+
     fn deref(&self) -> &Self::Target {
         &self.base_contract
     }
@@ -177,19 +175,24 @@ impl<M> Clone for Contract<M> {
 }
 
 impl<M> Contract<M> {
-    /// Returns the contract's address
+    /// Returns the contract's address.
     pub fn address(&self) -> Address {
         self.address
     }
 
-    /// Returns a reference to the contract's ABI
+    /// Returns a reference to the contract's ABI.
     pub fn abi(&self) -> &Abi {
         &self.base_contract.abi
     }
 
-    /// Returns a pointer to the contract's client
+    /// Returns a pointer to the contract's client.
     pub fn client(&self) -> Arc<M> {
-        self.client.clone()
+        Arc::clone(&self.client)
+    }
+
+    /// Returns a reference to the contract's client.
+    pub fn client_ref(&self) -> &M {
+        Arc::as_ref(&self.client)
     }
 }
 
@@ -301,10 +304,7 @@ impl<M: Middleware> Contract<M> {
     ///
     /// Clones `self` internally
     #[must_use]
-    pub fn at<T: Into<Address>>(&self, address: T) -> Self
-    where
-        M: Clone,
-    {
+    pub fn at<T: Into<Address>>(&self, address: T) -> Self {
         let mut this = self.clone();
         this.address = address.into();
         this
@@ -314,10 +314,7 @@ impl<M: Middleware> Contract<M> {
     ///
     /// Clones `self` internally
     #[must_use]
-    pub fn connect<N>(&self, client: Arc<N>) -> Contract<N>
-    where
-        N: Clone,
-    {
+    pub fn connect<N>(&self, client: Arc<N>) -> Contract<N> {
         Contract { base_contract: self.base_contract.clone(), client, address: self.address }
     }
 }
diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs
index 0b5bc56f..73c67a39 100644
--- a/ethers-contract/src/multicall/mod.rs
+++ b/ethers-contract/src/multicall/mod.rs
@@ -4,9 +4,9 @@ use crate::{
 };
 use ethers_core::{
     abi::{AbiDecode, Detokenize, Function, Token},
-    types::{Address, BlockNumber, Bytes, Chain, NameOrAddress, TxHash, H160, U256},
+    types::{Address, BlockNumber, Bytes, Chain, NameOrAddress, H160, U256},
 };
-use ethers_providers::Middleware;
+use ethers_providers::{Middleware, PendingTransaction};
 use std::{convert::TryFrom, sync::Arc};
 
 pub mod multicall_contract;
@@ -223,8 +223,7 @@ impl TryFrom<u8> for MulticallVersion {
 ///
 /// // `await`ing the `send` method waits for the transaction to be broadcast, which also
 /// // returns the transaction hash
-/// let tx_hash = multicall.send().await?;
-/// let _tx_receipt = PendingTransaction::new(tx_hash, &client).await?;
+/// let _tx_receipt = multicall.send().await?.await.expect("tx dropped");
 ///
 /// // you can also query ETH balances of multiple addresses
 /// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::<Address>()?;
@@ -569,7 +568,7 @@ impl<M: Middleware> Multicall<M> {
     ///     .add_call(broadcast_1, false)
     ///     .add_call(broadcast_2, false);
     ///
-    /// let _tx_hash = multicall.send().await?;
+    /// let _tx_receipt = multicall.send().await?.await.expect("tx dropped");
     ///
     /// # let call_1 = contract.method::<_, String>("getValue", ())?;
     /// # let call_2 = contract.method::<_, Address>("lastSender", ())?;
@@ -735,7 +734,7 @@ impl<M: Middleware> Multicall<M> {
             v @ (MulticallVersion::Multicall2 | MulticallVersion::Multicall3) => {
                 let is_v2 = v == MulticallVersion::Multicall2;
                 let call = if is_v2 { self.as_try_aggregate() } else { self.as_aggregate_3() };
-                let return_data = call.call().await?;
+                let return_data = ContractCall::call(&call).await?;
                 self.calls
                     .iter()
                     .zip(return_data.into_iter())
@@ -789,7 +788,7 @@ impl<M: Middleware> Multicall<M> {
     }
 
     /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy,
-    /// returning the transaction hash once the transaction confirms.
+    /// returning the pending transaction.
     ///
     /// Note: this method will broadcast a transaction from an account, meaning it must have
     /// sufficient funds for gas and transaction value.
@@ -811,32 +810,18 @@ impl<M: Middleware> Multicall<M> {
     /// # Ok(())
     /// # }
     /// ```
-    pub async fn send(&self) -> Result<TxHash, M> {
-        // Broadcast transaction and return the transaction hash
-        // TODO: Can we make this return a PendingTransaction directly instead?
-        // Seems hard due to `returns a value referencing data owned by the current function`
-
-        // running clippy --fix on this throws E0597
-        #[allow(clippy::let_and_return)]
-        let tx_hash = match self.version {
-            MulticallVersion::Multicall => {
-                let call = self.as_aggregate();
-                let hash = *call.send().await?;
-                hash
-            }
-            MulticallVersion::Multicall2 => {
-                let call = self.as_try_aggregate();
-                let hash = *call.send().await?;
-                hash
-            }
-            MulticallVersion::Multicall3 => {
-                let call = self.as_aggregate_3_value();
-                let hash = *call.send().await?;
-                hash
-            }
+    pub async fn send(&self) -> Result<PendingTransaction<'_, M::Provider>, M> {
+        let tx = match self.version {
+            MulticallVersion::Multicall => self.as_aggregate().tx,
+            MulticallVersion::Multicall2 => self.as_try_aggregate().tx,
+            MulticallVersion::Multicall3 => self.as_aggregate_3_value().tx,
         };
 
-        Ok(tx_hash)
+        self.contract
+            .client_ref()
+            .send_transaction(tx, self.block.map(Into::into))
+            .await
+            .map_err(|e| MulticallError::ContractError(ContractError::MiddlewareError(e)))
     }
 
     /// v1
diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs
index f5f2f760..e423b1a6 100644
--- a/ethers-core/src/types/chain.rs
+++ b/ethers-core/src/types/chain.rs
@@ -5,7 +5,7 @@ use std::{
     fmt,
     time::Duration,
 };
-use strum::{AsRefStr, EnumString, EnumVariantNames};
+use strum::{AsRefStr, EnumCount, EnumIter, EnumString, EnumVariantNames};
 
 // compatibility re-export
 #[doc(hidden)]
@@ -33,6 +33,8 @@ pub type ParseChainError = TryFromPrimitiveError<Chain>;
     AsRefStr,         // also for fmt::Display and serde::Serialize
     EnumVariantNames, // Self::VARIANTS
     EnumString,       // FromStr, TryFrom<&str>
+    EnumIter,
+    EnumCount,
     TryFromPrimitive, // TryFrom<u64>
     Deserialize,
 )]
@@ -372,9 +374,15 @@ impl Chain {
 #[cfg(test)]
 mod tests {
     use super::*;
+    use strum::IntoEnumIterator;
 
     #[test]
     fn test_default_chain() {
         assert_eq!(serde_json::to_string(&Chain::default()).unwrap(), "\"mainnet\"");
     }
+
+    #[test]
+    fn test_enum_iter() {
+        assert_eq!(Chain::COUNT, Chain::iter().size_hint().0);
+    }
 }
diff --git a/ethers-core/src/utils/hash.rs b/ethers-core/src/utils/hash.rs
index 2ddb4c43..18552590 100644
--- a/ethers-core/src/utils/hash.rs
+++ b/ethers-core/src/utils/hash.rs
@@ -1,36 +1,42 @@
-//! Various utilities for manipulating Ethereum related dat
+//! Various utilities for manipulating Ethereum related data.
+
 use ethabi::ethereum_types::H256;
 use tiny_keccak::{Hasher, Keccak};
 
-const PREFIX: &str = "\x19Ethereum Signed Message:\n";
-
-/// Hash a message according to EIP-191.
+/// Hash a message according to [EIP-191] (version `0x01`).
+///
+/// The final message is a UTF-8 string, encoded as follows:
+/// `"\x19Ethereum Signed Message:\n" + message.length + message`
+///
+/// This message is then hashed using [Keccak-256](keccak256).
 ///
-/// The data is a UTF-8 encoded string and will enveloped as follows:
-/// `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed
-/// using keccak256.
-pub fn hash_message<S>(message: S) -> H256
-where
-    S: AsRef<[u8]>,
-{
+/// [EIP-191]: https://eips.ethereum.org/EIPS/eip-191
+pub fn hash_message<T: AsRef<[u8]>>(message: T) -> H256 {
+    const PREFIX: &str = "\x19Ethereum Signed Message:\n";
+
     let message = message.as_ref();
+    let len = message.len();
+    let len_string = len.to_string();
 
-    let mut eth_message = format!("{PREFIX}{}", message.len()).into_bytes();
+    let mut eth_message = Vec::with_capacity(PREFIX.len() + len_string.len() + len);
+    eth_message.extend_from_slice(PREFIX.as_bytes());
+    eth_message.extend_from_slice(len_string.as_bytes());
     eth_message.extend_from_slice(message);
 
-    keccak256(&eth_message).into()
+    H256(keccak256(&eth_message))
 }
 
 /// Compute the Keccak-256 hash of input bytes.
+///
+/// Note that strings are interpreted as UTF-8 bytes,
 // TODO: Add Solidity Keccak256 packing support
-pub fn keccak256<S>(bytes: S) -> [u8; 32]
-where
-    S: AsRef<[u8]>,
-{
+pub fn keccak256<T: AsRef<[u8]>>(bytes: T) -> [u8; 32] {
     let mut output = [0u8; 32];
+
     let mut hasher = Keccak::v256();
     hasher.update(bytes.as_ref());
     hasher.finalize(&mut output);
+
     output
 }
 
@@ -53,7 +59,7 @@ pub fn id<S: AsRef<str>>(signature: S) -> [u8; 4] {
 ///
 /// If the type returns an error during serialization.
 pub fn serialize<T: serde::Serialize>(t: &T) -> serde_json::Value {
-    serde_json::to_value(t).expect("Types never fail to serialize.")
+    serde_json::to_value(t).expect("Failed to serialize value")
 }
 
 #[cfg(test)]
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index ff5ca22a..9528fa01 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -36,7 +36,7 @@ pub use rlp;
 /// Re-export hex
 pub use hex;
 
-use crate::types::{Address, Bytes, ParseI256Error, I256, U256};
+use crate::types::{Address, ParseI256Error, I256, U256};
 use elliptic_curve::sec1::ToEncodedPoint;
 use ethabi::ethereum_types::FromDecStrErr;
 use k256::{ecdsa::SigningKey, PublicKey as K256PublicKey};
@@ -212,10 +212,7 @@ where
 /// assert_eq!(eth, parse_ether(1usize).unwrap());
 /// assert_eq!(eth, parse_ether("1").unwrap());
 /// ```
-pub fn parse_ether<S>(eth: S) -> Result<U256, ConversionError>
-where
-    S: ToString,
-{
+pub fn parse_ether<S: ToString>(eth: S) -> Result<U256, ConversionError> {
     Ok(parse_units(eth, "ether")?.into())
 }
 
@@ -308,10 +305,11 @@ pub fn get_contract_address(sender: impl Into<Address>, nonce: impl Into<U256>)
 /// keccak256( 0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12..]
 pub fn get_create2_address(
     from: impl Into<Address>,
-    salt: impl Into<Bytes>,
-    init_code: impl Into<Bytes>,
+    salt: impl AsRef<[u8]>,
+    init_code: impl AsRef<[u8]>,
 ) -> Address {
-    get_create2_address_from_hash(from, salt, keccak256(init_code.into().as_ref()).to_vec())
+    let init_code_hash = keccak256(init_code.as_ref());
+    get_create2_address_from_hash(from, salt, init_code_hash)
 }
 
 /// Returns the CREATE2 address of a smart contract as specified in
@@ -332,9 +330,7 @@ pub fn get_create2_address(
 ///     utils::{get_create2_address_from_hash, keccak256},
 /// };
 ///
-/// let UNISWAP_V3_POOL_INIT_CODE_HASH = Bytes::from(
-///     hex::decode("e34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54").unwrap(),
-/// );
+/// let init_code_hash = hex::decode("e34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54").unwrap();
 /// let factory: Address = "0x1F98431c8aD98523631AE4a59f267346ea31F984"
 ///     .parse()
 ///     .unwrap();
@@ -344,19 +340,18 @@ pub fn get_create2_address(
 /// let token1: Address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
 ///     .parse()
 ///     .unwrap();
-/// let fee = 500;
+/// let fee = U256::from(500_u64);
 ///
 /// // abi.encode(token0 as address, token1 as address, fee as uint256)
-/// let input = abi::encode(&vec![
+/// let input = abi::encode(&[
 ///     Token::Address(token0),
 ///     Token::Address(token1),
-///     Token::Uint(U256::from(fee)),
+///     Token::Uint(fee),
 /// ]);
 ///
 /// // keccak256(abi.encode(token0, token1, fee))
 /// let salt = keccak256(&input);
-/// let pool_address =
-///     get_create2_address_from_hash(factory, salt.to_vec(), UNISWAP_V3_POOL_INIT_CODE_HASH);
+/// let pool_address = get_create2_address_from_hash(factory, salt, init_code_hash);
 ///
 /// assert_eq!(
 ///     pool_address,
@@ -367,12 +362,18 @@ pub fn get_create2_address(
 /// ```
 pub fn get_create2_address_from_hash(
     from: impl Into<Address>,
-    salt: impl Into<Bytes>,
-    init_code_hash: impl Into<Bytes>,
+    salt: impl AsRef<[u8]>,
+    init_code_hash: impl AsRef<[u8]>,
 ) -> Address {
-    let bytes =
-        [&[0xff], from.into().as_bytes(), salt.into().as_ref(), init_code_hash.into().as_ref()]
-            .concat();
+    let from = from.into();
+    let salt = salt.as_ref();
+    let init_code_hash = init_code_hash.as_ref();
+
+    let mut bytes = Vec::with_capacity(1 + 20 + salt.len() + init_code_hash.len());
+    bytes.push(0xff);
+    bytes.extend_from_slice(from.as_bytes());
+    bytes.extend_from_slice(salt);
+    bytes.extend_from_slice(init_code_hash);
 
     let hash = keccak256(bytes);
 
@@ -388,11 +389,20 @@ pub fn secret_key_to_address(secret_key: &SigningKey) -> Address {
     let public_key = public_key.as_bytes();
     debug_assert_eq!(public_key[0], 0x04);
     let hash = keccak256(&public_key[1..]);
-    Address::from_slice(&hash[12..])
+
+    let mut bytes = [0u8; 20];
+    bytes.copy_from_slice(&hash[12..]);
+    Address::from(bytes)
 }
 
-/// Converts an Ethereum address to the checksum encoding
-/// Ref: <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md>
+/// Encodes an Ethereum address to its [EIP-55] checksum.
+///
+/// You can optionally specify an [EIP-155 chain ID] to encode the address using the [EIP-1191]
+/// extension.
+///
+/// [EIP-55]: https://eips.ethereum.org/EIPS/eip-55
+/// [EIP-155 chain ID]: https://eips.ethereum.org/EIPS/eip-155
+/// [EIP-1191]: https://eips.ethereum.org/EIPS/eip-1191
 pub fn to_checksum(addr: &Address, chain_id: Option<u8>) -> String {
     let prefixed_addr = match chain_id {
         Some(chain_id) => format!("{chain_id}0x{addr:x}"),
diff --git a/ethers-providers/src/transports/common.rs b/ethers-providers/src/transports/common.rs
index 747e1405..a23b68d5 100644
--- a/ethers-providers/src/transports/common.rs
+++ b/ethers-providers/src/transports/common.rs
@@ -1,5 +1,6 @@
 // Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
 
+use base64::{engine::general_purpose, Engine};
 use ethers_core::types::U256;
 use serde::{
     de::{self, MapAccess, Unexpected, Visitor},
@@ -190,8 +191,10 @@ pub enum Authorization {
 }
 
 impl Authorization {
-    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
-        let auth_secret = base64::encode(username.into() + ":" + &password.into());
+    pub fn basic(username: impl AsRef<str>, password: impl AsRef<str>) -> Self {
+        let username = username.as_ref();
+        let password = password.as_ref();
+        let auth_secret = general_purpose::STANDARD.encode(format!("{username}:{password}"));
         Self::Basic(auth_secret)
     }
 
diff --git a/ethers-solc/src/sourcemap.rs b/ethers-solc/src/sourcemap.rs
index 5e722368..f9b75844 100644
--- a/ethers-solc/src/sourcemap.rs
+++ b/ethers-solc/src/sourcemap.rs
@@ -512,7 +512,7 @@ mod tests {
         let source_maps = include_str!("../test-data/out-source-maps.txt");
 
         for (line, s) in source_maps.lines().enumerate() {
-            parse(s).unwrap_or_else(|_| panic!("Failed to parse line {}", line));
+            parse(s).unwrap_or_else(|_| panic!("Failed to parse line {line}"));
         }
     }
 
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index bb4d0a3b..aba0a883 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -506,7 +506,7 @@ mod tests {
         let existing = path.join("Test.sol");
         let non_existing = path.join("test.sol");
         std::fs::write(
-            &existing,
+            existing,
             r#"
 pragma solidity ^0.8.10;
 contract A {}
diff --git a/examples/providers/examples/retry.rs b/examples/providers/examples/retry.rs
index 9146bd84..8f78b689 100644
--- a/examples/providers/examples/retry.rs
+++ b/examples/providers/examples/retry.rs
@@ -19,7 +19,7 @@ async fn main() -> eyre::Result<()> {
         .rate_limit_retries(10)
         .timeout_retries(3)
         .initial_backoff(Duration::from_millis(500))
-        .build(provider, Box::new(HttpRateLimitRetryPolicy::default()));
+        .build(provider, Box::<ethers::providers::HttpRateLimitRetryPolicy>::default());
 
     // Send a JSON-RPC request for the latest block
     let block_num = "latest".to_string();


Test patch:
diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs
index 8fdd0469..c4f58fba 100644
--- a/ethers-contract/tests/it/contract.rs
+++ b/ethers-contract/tests/it/contract.rs
@@ -491,8 +491,7 @@ mod eth_tests {
         multicall_send.clear_calls().add_call(broadcast, false).add_call(broadcast2, false);
 
         // broadcast the transaction and wait for it to be mined
-        let tx_hash = multicall_send.legacy().send().await.unwrap();
-        let _tx_receipt = PendingTransaction::new(tx_hash, client.provider()).await.unwrap();
+        let _tx_receipt = multicall_send.legacy().send().await.unwrap().await.unwrap();
 
         // Do another multicall to check the updated return values
         // The `getValue` calls should return the last value we set in the batched broadcast