diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 61ca2701..3d5fe49d 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -187,14 +187,7 @@ impl Multicall { } /// Appends a `call` to the list of calls for the Multicall instance - /// - /// # Panics - /// - /// If more than the maximum number of supported calls are added. The maximum - /// limits is constrained due to tokenization/detokenization support for tuples pub fn add_call(&mut self, call: ContractCall) -> &mut Self { - assert!(self.calls.len() < 16, "Cannot support more than {} calls", 16); - match (call.tx.to(), call.tx.data()) { (Some(NameOrAddress::Address(target)), Some(data)) => { let call = Call { target: *target, data: data.clone(), function: call.function }; @@ -284,23 +277,58 @@ impl Multicall { /// # } /// ``` /// + /// # Panics + /// + /// If more than the maximum number of supported calls are added. The maximum + /// limits is constrained due to tokenization/detokenization support for tuples + /// /// Note: this method _does not_ send a transaction from your account /// /// [`ContractError`]: crate::ContractError pub async fn call(&self) -> Result> { - let contract_call = self.as_contract_call(); + assert!(self.calls.len() < 16, "Cannot decode more than {} calls", 16); + let tokens = self.call_raw().await?; + let tokens = vec![Token::Tuple(tokens)]; + let data = D::from_tokens(tokens)?; + Ok(data) + } + /// Queries the Ethereum blockchain via an `eth_call`, but via the Multicall contract and + /// without detokenization. + /// + /// It returns a [`ContractError`] if there is any error in the RPC call. + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers_core::types::{U256, Address}; + /// # use ethers_providers::{Provider, Http}; + /// # use ethers_contract::Multicall; + /// # use std::convert::TryFrom; + /// # + /// # let client = Provider::::try_from("http://localhost:8545")?; + /// # + /// # let multicall = Multicall::new(client, None).await?; + /// // The consumer of the API is responsible for detokenizing the results + /// // as the results will be a Vec + /// let tokens = multicall.call_raw().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// Note: this method _does not_ send a transaction from your account + /// + /// [`ContractError`]: crate::ContractError + + pub async fn call_raw(&self) -> Result, ContractError> { + let contract_call = self.as_contract_call(); // Fetch response from the Multicall contract let (_block_number, return_data) = contract_call.call().await?; - - // Decode return data into ABI tokens let tokens = self .calls .iter() .zip(&return_data) .map(|(call, bytes)| { let mut tokens: Vec = call.function.decode_output(bytes.as_ref())?; - Ok(match tokens.len() { 0 => Token::Tuple(vec![]), 1 => tokens.remove(0), @@ -308,14 +336,7 @@ impl Multicall { }) }) .collect::, ContractError>>()?; - - // Form tokens that represent tuples - let tokens = vec![Token::Tuple(tokens)]; - - // Detokenize from the tokens into the provided tuple D - let data = D::from_tokens(tokens)?; - - Ok(data) + Ok(tokens) } /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy. diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 09f82878..46e955db 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -9,6 +9,7 @@ mod eth_tests { use super::*; use ethers_contract::{LogMeta, Multicall}; use ethers_core::{ + abi::{Detokenize, Token, Tokenizable}, types::{transaction::eip712::Eip712, Address, BlockId, Bytes, I256, U256}, utils::{keccak256, Ganache}, }; @@ -374,7 +375,8 @@ mod eth_tests { let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); // launch ganache - let ganache = Ganache::new().spawn(); + // some tests expect 100 ether (-e === --wallet.defaultBalance in ether) + let ganache = Ganache::new().arg("-e").arg("100").spawn(); // Instantiate the clients. We assume that clients consume the provider and the wallet // (which makes sense), so for multi-client tests, you must clone the provider. @@ -425,6 +427,7 @@ mod eth_tests { .send() .await .unwrap(); + not_so_simple_contract .connect(client3.clone()) .method::<_, H256>("setValue", "reset second".to_owned()) @@ -498,10 +501,42 @@ mod eth_tests { .eth_balance_of(addrs[4]) .eth_balance_of(addrs[5]) .eth_balance_of(addrs[6]); + let balances: (U256, U256, U256) = multicall.call().await.unwrap(); - assert_eq!(balances.0, U256::from(100000000000000000000u128)); - assert_eq!(balances.1, U256::from(100000000000000000000u128)); - assert_eq!(balances.2, U256::from(100000000000000000000u128)); + assert_eq!(balances.0, U256::from(100_000_000_000_000_000_000u128)); + assert_eq!(balances.1, U256::from(100_000_000_000_000_000_000u128)); + assert_eq!(balances.2, U256::from(100_000_000_000_000_000_000u128)); + + // clear multicall so we can test `call_raw` w/ >16 calls + multicall.clear_calls(); + + // clear the current value + simple_contract + .connect(client2.clone()) + .method::<_, H256>("setValue", "many".to_owned()) + .unwrap() + .legacy() + .send() + .await + .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); + } + + // must use `call_raw` as `.calls` > 16 + let tokens = multicall.call_raw().await.unwrap(); + // if want to use, must detokenize manually + let results: Vec = tokens + .iter() + .map(|token| { + // decode manually using Tokenizable method + String::from_token(token.to_owned()).unwrap() + }) + .collect(); + assert_eq!(results, ["many"; 17]); } #[tokio::test]