Repository: gakonst/ethers-rs
Pull request #1945 (2022-12-16 23:41:17)

fix(msrv): bump MSRV in .clippy.toml and add a comment in root Cargo.toml
<!--
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

.clippy.toml msrv was not bumped with the others.

.clippy.toml is for backwards compatibility since the rust-version key in the Cargo manifest is [ignored before 1.56.0](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) and clippy will display a warning if it differs from the manifests

<!--
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



<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

-   [ ] Added Tests
-   [ ] Added Documentation
-   [ ] Updated the changelog
-   [ ] Breaking changes



Patch:
diff --git a/.clippy.toml b/.clippy.toml
index 6bfeada7..ad0ea4e7 100644
--- a/.clippy.toml
+++ b/.clippy.toml
@@ -1,1 +1,1 @@
-msrv = "1.62"
+msrv = "1.64"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1b26a77..7b157dc5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -103,6 +103,7 @@
 - [#842](https://github.com/gakonst/ethers-rs/issues/842) Add support for I256 types in `parse_units` and `format_units`.
   Added `twos_complement` function for I256.
 - [#1934](https://github.com/gakonst/ethers-rs/pull/1934) Allow 16 calls in multicall.
+- [#1941](https://github.com/gakonst/ethers-rs/pull/1941) Add `add_calls` and `call_array` for `Multicall`.
 
 ## ethers-contract-abigen
 
diff --git a/Cargo.toml b/Cargo.toml
index eff02c36..3205a175 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,7 +2,7 @@
 name = "ethers"
 version = "1.0.2"
 edition = "2021"
-rust-version = "1.64"
+rust-version = "1.64" # must also be changed in **/Cargo.toml and .clippy.toml
 authors = ["Georgios Konstantopoulos <me@gakonst.com>"]
 license = "MIT OR Apache-2.0"
 readme = "README.md"
diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs
index 8dd24542..56dca427 100644
--- a/ethers-contract/src/multicall/mod.rs
+++ b/ethers-contract/src/multicall/mod.rs
@@ -437,6 +437,27 @@ impl<M: Middleware> Multicall<M> {
         }
     }
 
+    /// Appends multiple `call`s to the list of calls of the Multicall instance.
+    ///
+    /// Version specific details:
+    /// - 1: `allow_failure` is ignored.
+    /// - >=2: `allow_failure` specifies whether or not this call is allowed to revert in the
+    ///   multicall.
+    /// - 3: Transaction values are used when broadcasting transactions with [`send`], otherwise
+    ///   they are always ignored.
+    ///
+    /// [`send`]: #method.send
+    pub fn add_calls<D: Detokenize>(
+        &mut self,
+        allow_failure: bool,
+        calls: impl IntoIterator<Item = ContractCall<M, D>>,
+    ) -> &mut Self {
+        for call in calls {
+            self.add_call(call, allow_failure);
+        }
+        self
+    }
+
     /// Appends a `call` to the list of calls of the Multicall instance for querying the block hash
     /// of a given block number.
     ///
@@ -615,6 +636,45 @@ impl<M: Middleware> Multicall<M> {
         Ok(data)
     }
 
+    /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract, assuming
+    /// that every call returns same data type.
+    ///
+    /// Note: this method _does not_ send a transaction from your account.
+    ///
+    /// # Errors
+    ///
+    /// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
+    /// the tokens back to the expected return type.
+    ///
+    /// # Examples
+    ///
+    /// The return type must be annotated while calling this method:
+    ///
+    /// ```no_run
+    /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
+    /// # use ethers_core::types::{U256, Address};
+    /// # use ethers_providers::{Provider, Http};
+    /// # use ethers_contract::Multicall;
+    /// # use std::convert::TryFrom;
+    /// #
+    /// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
+    /// #
+    /// # let multicall = Multicall::new(client, None).await?;
+    /// // If the all Solidity function calls `returns (uint256)`:
+    /// let result: Vec<U256> = multicall.call().await?;
+    /// # Ok(())
+    /// # }
+    /// ```
+    pub async fn call_array<D: Detokenize>(&self) -> Result<Vec<D>, M> {
+        let tokens = self.call_raw().await?;
+        let res: std::result::Result<Vec<D>, ContractError<M>> = tokens
+            .into_iter()
+            .map(|token| D::from_tokens(vec![token]).map_err(ContractError::DetokenizationError))
+            .collect();
+
+        Ok(res?)
+    }
+
     /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract and
     /// without detokenization.
     ///
diff --git a/ethers-core/src/types/transaction/eip1559.rs b/ethers-core/src/types/transaction/eip1559.rs
index ba080e19..ba937fe6 100644
--- a/ethers-core/src/types/transaction/eip1559.rs
+++ b/ethers-core/src/types/transaction/eip1559.rs
@@ -208,7 +208,7 @@ impl Eip1559TransactionRequest {
         *offset += 1;
         tx.gas = Some(rlp.val_at(*offset)?);
         *offset += 1;
-        tx.to = decode_to(rlp, offset)?;
+        tx.to = decode_to(rlp, offset)?.map(NameOrAddress::Address);
         tx.value = Some(rlp.val_at(*offset)?);
         *offset += 1;
         let data = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs
index efe70780..816f4539 100644
--- a/ethers-core/src/types/transaction/mod.rs
+++ b/ethers-core/src/types/transaction/mod.rs
@@ -75,7 +75,7 @@ fn decode_signature(
 fn decode_to(
     rlp: &rlp::Rlp,
     offset: &mut usize,
-) -> Result<Option<super::NameOrAddress>, rlp::DecoderError> {
+) -> Result<Option<super::Address>, rlp::DecoderError> {
     let to = {
         let to = rlp.at(*offset)?;
         if to.is_empty() {
diff --git a/ethers-core/src/types/transaction/request.rs b/ethers-core/src/types/transaction/request.rs
index f8be2627..d31d91bf 100644
--- a/ethers-core/src/types/transaction/request.rs
+++ b/ethers-core/src/types/transaction/request.rs
@@ -233,7 +233,7 @@ impl TransactionRequest {
             *offset += 1;
         }
 
-        txn.to = decode_to(rlp, offset)?;
+        txn.to = decode_to(rlp, offset)?.map(NameOrAddress::Address);
         txn.value = Some(rlp.at(*offset)?.as_val()?);
         *offset += 1;
 
diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs
index 314caf0b..b29d6f99 100644
--- a/ethers-core/src/types/transaction/response.rs
+++ b/ethers-core/src/types/transaction/response.rs
@@ -1,7 +1,7 @@
 //! Transaction types
 use super::{
-    decode_signature, eip2718::TypedTransaction, eip2930::AccessList, normalize_v, rlp_opt,
-    rlp_opt_list,
+    decode_signature, decode_to, eip2718::TypedTransaction, eip2930::AccessList, normalize_v,
+    rlp_opt, rlp_opt_list,
 };
 use crate::{
     types::{
@@ -248,8 +248,7 @@ impl Transaction {
         *offset += 1;
         self.gas = rlp.val_at(*offset)?;
         *offset += 1;
-        self.to = Some(rlp.val_at(*offset)?);
-        *offset += 1;
+        self.to = decode_to(rlp, offset)?;
         self.value = rlp.val_at(*offset)?;
         *offset += 1;
         let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@@ -279,8 +278,7 @@ impl Transaction {
         #[cfg(feature = "celo")]
         self.decode_celo_metadata(rlp, offset)?;
 
-        self.to = Some(rlp.val_at(*offset)?);
-        *offset += 1;
+        self.to = decode_to(rlp, offset)?;
         self.value = rlp.val_at(*offset)?;
         *offset += 1;
         let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@@ -310,8 +308,7 @@ impl Transaction {
         #[cfg(feature = "celo")]
         self.decode_celo_metadata(rlp, offset)?;
 
-        self.to = Some(rlp.val_at(*offset)?);
-        *offset += 1;
+        self.to = decode_to(rlp, offset)?;
         self.value = rlp.val_at(*offset)?;
         *offset += 1;
         let input = rlp::Rlp::new(rlp.at(*offset)?.as_raw()).data()?;
@@ -473,7 +470,7 @@ impl PartialOrd<Self> for TransactionReceipt {
 #[cfg(test)]
 #[cfg(not(feature = "celo"))]
 mod tests {
-    use rlp::Encodable;
+    use rlp::{Encodable, Rlp};
 
     use crate::types::transaction::eip2930::AccessListItem;
 
@@ -1093,4 +1090,24 @@ mod tests {
         let r = rlp::Rlp::new(b.as_slice());
         Transaction::decode(&r).unwrap();
     }
+
+    #[test]
+    fn test_rlp_decoding_create_roundtrip() {
+        let tx = Transaction {
+            block_hash: None,
+            block_number: None,
+            from: Address::from_str("c26ad91f4e7a0cad84c4b9315f420ca9217e315d").unwrap(),
+            gas: U256::from_str_radix("0x10e2b", 16).unwrap(),
+            gas_price: Some(U256::from_str_radix("0x12ec276caf", 16).unwrap()),
+            hash: H256::from_str("929ff27a5c7833953df23103c4eb55ebdfb698678139d751c51932163877fada").unwrap(),
+            input: Bytes::from(
+                hex::decode("a9059cbb000000000000000000000000fdae129ecc2c27d166a3131098bc05d143fa258e0000000000000000000000000000000000000000000000000000000002faf080").unwrap()
+            ),
+            nonce: U256::zero(),
+            transaction_index: None,
+            value: U256::zero(),
+            ..Default::default()
+        };
+        Transaction::decode(&Rlp::new(&tx.rlp())).unwrap();
+    }
 }
diff --git a/ethers-core/src/utils/geth.rs b/ethers-core/src/utils/geth.rs
index 8657c888..8c13d175 100644
--- a/ethers-core/src/utils/geth.rs
+++ b/ethers-core/src/utils/geth.rs
@@ -164,6 +164,7 @@ impl Default for PrivateNetOptions {
 /// ```
 #[derive(Clone, Default)]
 pub struct Geth {
+    program: Option<PathBuf>,
     port: Option<u16>,
     authrpc_port: Option<u16>,
     ipc_path: Option<PathBuf>,
@@ -180,6 +181,32 @@ impl Geth {
         Self::default()
     }
 
+    /// Creates a Geth builder which will execute `geth` at the given path.
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use ethers_core::utils::Geth;
+    /// # fn a() {
+    ///  let geth = Geth::at("../go-ethereum/build/bin/geth").spawn();
+    ///
+    ///  println!("Geth running at `{}`", geth.endpoint());
+    /// # }
+    /// ```
+    pub fn at(path: impl Into<PathBuf>) -> Self {
+        Self::new().path(path)
+    }
+
+    /// Sets the `path` to the `geth` executable
+    ///
+    /// By default, it's expected that `geth` is in `$PATH`, see also
+    /// [`std::process::Command::new()`]
+    #[must_use]
+    pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
+        self.program = Some(path.into());
+        self
+    }
+
     /// Sets the port which will be used when the `geth-cli` instance is launched.
     #[must_use]
     pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
@@ -274,9 +301,10 @@ impl Geth {
     /// Consumes the builder and spawns `geth` with stdout redirected
     /// to /dev/null.
     pub fn spawn(self) -> GethInstance {
-        let mut cmd = Command::new(GETH);
+        let mut cmd =
+            if let Some(ref prg) = self.program { Command::new(prg) } else { Command::new(GETH) };
         // geth uses stderr for its logs
-        cmd.stderr(std::process::Stdio::piped());
+        cmd.stderr(Stdio::piped());
         let port = if let Some(port) = self.port { port } else { unused_port() };
         let authrpc_port = if let Some(port) = self.authrpc_port { port } else { unused_port() };
 


Test patch:
diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs
index e15005aa..8fdd0469 100644
--- a/ethers-contract/tests/it/contract.rs
+++ b/ethers-contract/tests/it/contract.rs
@@ -15,7 +15,7 @@ mod eth_tests {
     use ethers_derive_eip712::*;
     use ethers_providers::{Http, Middleware, PendingTransaction, Provider, StreamExt};
     use ethers_signers::{LocalWallet, Signer};
-    use std::{convert::TryFrom, sync::Arc, time::Duration};
+    use std::{convert::TryFrom, iter::FromIterator, sync::Arc, time::Duration};
 
     #[tokio::test]
     async fn deploy_and_call_contract() {
@@ -517,10 +517,26 @@ mod eth_tests {
             .add_get_eth_balance(addrs[5], false)
             .add_get_eth_balance(addrs[6], false);
 
+        let valid_balances = [
+            U256::from(10_000_000_000_000_000_000_000u128),
+            U256::from(10_000_000_000_000_000_000_000u128),
+            U256::from(10_000_000_000_000_000_000_000u128),
+        ];
+
         let balances: (U256, U256, U256) = multicall.call().await.unwrap();
-        assert_eq!(balances.0, U256::from(10_000_000_000_000_000_000_000u128));
-        assert_eq!(balances.1, U256::from(10_000_000_000_000_000_000_000u128));
-        assert_eq!(balances.2, U256::from(10_000_000_000_000_000_000_000u128));
+        assert_eq!(balances.0, valid_balances[0]);
+        assert_eq!(balances.1, valid_balances[1]);
+        assert_eq!(balances.2, valid_balances[2]);
+
+        // call_array
+        multicall
+            .clear_calls()
+            .add_get_eth_balance(addrs[4], false)
+            .add_get_eth_balance(addrs[5], false)
+            .add_get_eth_balance(addrs[6], false);
+
+        let balances: Vec<U256> = multicall.call_array().await.unwrap();
+        assert_eq!(balances, Vec::from_iter(valid_balances.iter().copied()));
 
         // clear multicall so we can test `call_raw` w/ >16 calls
         multicall.clear_calls();
@@ -536,10 +552,11 @@ mod eth_tests {
             .unwrap();
 
         // build up a list of calls greater than the 16 max restriction
-        for i in 0..=16 {
-            let call = simple_contract.method::<_, String>("getValue", ()).unwrap();
-            multicall.add_call(call, false);
-        }
+        multicall.add_calls(
+            false,
+            std::iter::repeat(simple_contract.method::<_, String>("getValue", ()).unwrap())
+                .take(17), // .collect(),
+        );
 
         // must use `call_raw` as `.calls` > 16
         let tokens = multicall.call_raw().await.unwrap();