Expose New `call_raw` API that permits MultiCalls without Detokenization (#915)

* Add `call_raw` method that forgoes detokenization for MultiCall. Have `call` wrap around `call_raw` permitting user to handle detokenization themselves if they wish

* Improve documentation: Add details to the documentation example that informs the user of their responsibility to detokenize results
This commit is contained in:
Jim 2022-02-15 12:38:01 -08:00 committed by GitHub
parent cd24022515
commit e3f0621d43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 79 additions and 23 deletions

View File

@ -187,14 +187,7 @@ impl<M: Middleware> Multicall<M> {
}
/// 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<D: Detokenize>(&mut self, call: ContractCall<M, D>) -> &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<M: Middleware> Multicall<M> {
/// # }
/// ```
///
/// # 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<M>`]: crate::ContractError<M>
pub async fn call<D: Detokenize>(&self) -> Result<D, ContractError<M>> {
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<M>`] if there is any error in the RPC call.
///
/// ```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?;
/// // The consumer of the API is responsible for detokenizing the results
/// // as the results will be a Vec<Token>
/// let tokens = multicall.call_raw().await?;
/// # Ok(())
/// # }
/// ```
///
/// Note: this method _does not_ send a transaction from your account
///
/// [`ContractError<M>`]: crate::ContractError<M>
pub async fn call_raw(&self) -> Result<Vec<Token>, ContractError<M>> {
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<Token> = call.function.decode_output(bytes.as_ref())?;
Ok(match tokens.len() {
0 => Token::Tuple(vec![]),
1 => tokens.remove(0),
@ -308,14 +336,7 @@ impl<M: Middleware> Multicall<M> {
})
})
.collect::<Result<Vec<Token>, ContractError<M>>>()?;
// 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.

View File

@ -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<String> = tokens
.iter()
.map(|token| {
// decode manually using Tokenizable method
String::from_token(token.to_owned()).unwrap()
})
.collect();
assert_eq!(results, ["many"; 17]);
}
#[tokio::test]