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:
parent
cd24022515
commit
e3f0621d43
|
@ -187,14 +187,7 @@ impl<M: Middleware> Multicall<M> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Appends a `call` to the list of calls for the Multicall instance
|
/// 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 {
|
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()) {
|
match (call.tx.to(), call.tx.data()) {
|
||||||
(Some(NameOrAddress::Address(target)), Some(data)) => {
|
(Some(NameOrAddress::Address(target)), Some(data)) => {
|
||||||
let call = Call { target: *target, data: data.clone(), function: call.function };
|
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
|
/// Note: this method _does not_ send a transaction from your account
|
||||||
///
|
///
|
||||||
/// [`ContractError<M>`]: crate::ContractError<M>
|
/// [`ContractError<M>`]: crate::ContractError<M>
|
||||||
pub async fn call<D: Detokenize>(&self) -> Result<D, 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
|
// Fetch response from the Multicall contract
|
||||||
let (_block_number, return_data) = contract_call.call().await?;
|
let (_block_number, return_data) = contract_call.call().await?;
|
||||||
|
|
||||||
// Decode return data into ABI tokens
|
|
||||||
let tokens = self
|
let tokens = self
|
||||||
.calls
|
.calls
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&return_data)
|
.zip(&return_data)
|
||||||
.map(|(call, bytes)| {
|
.map(|(call, bytes)| {
|
||||||
let mut tokens: Vec<Token> = call.function.decode_output(bytes.as_ref())?;
|
let mut tokens: Vec<Token> = call.function.decode_output(bytes.as_ref())?;
|
||||||
|
|
||||||
Ok(match tokens.len() {
|
Ok(match tokens.len() {
|
||||||
0 => Token::Tuple(vec![]),
|
0 => Token::Tuple(vec![]),
|
||||||
1 => tokens.remove(0),
|
1 => tokens.remove(0),
|
||||||
|
@ -308,14 +336,7 @@ impl<M: Middleware> Multicall<M> {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<Token>, ContractError<M>>>()?;
|
.collect::<Result<Vec<Token>, ContractError<M>>>()?;
|
||||||
|
Ok(tokens)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy.
|
/// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy.
|
||||||
|
|
|
@ -9,6 +9,7 @@ mod eth_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use ethers_contract::{LogMeta, Multicall};
|
use ethers_contract::{LogMeta, Multicall};
|
||||||
use ethers_core::{
|
use ethers_core::{
|
||||||
|
abi::{Detokenize, Token, Tokenizable},
|
||||||
types::{transaction::eip712::Eip712, Address, BlockId, Bytes, I256, U256},
|
types::{transaction::eip712::Eip712, Address, BlockId, Bytes, I256, U256},
|
||||||
utils::{keccak256, Ganache},
|
utils::{keccak256, Ganache},
|
||||||
};
|
};
|
||||||
|
@ -374,7 +375,8 @@ mod eth_tests {
|
||||||
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
||||||
|
|
||||||
// launch ganache
|
// 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
|
// 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.
|
// (which makes sense), so for multi-client tests, you must clone the provider.
|
||||||
|
@ -425,6 +427,7 @@ mod eth_tests {
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
not_so_simple_contract
|
not_so_simple_contract
|
||||||
.connect(client3.clone())
|
.connect(client3.clone())
|
||||||
.method::<_, H256>("setValue", "reset second".to_owned())
|
.method::<_, H256>("setValue", "reset second".to_owned())
|
||||||
|
@ -498,10 +501,42 @@ mod eth_tests {
|
||||||
.eth_balance_of(addrs[4])
|
.eth_balance_of(addrs[4])
|
||||||
.eth_balance_of(addrs[5])
|
.eth_balance_of(addrs[5])
|
||||||
.eth_balance_of(addrs[6]);
|
.eth_balance_of(addrs[6]);
|
||||||
|
|
||||||
let balances: (U256, U256, U256) = multicall.call().await.unwrap();
|
let balances: (U256, U256, U256) = multicall.call().await.unwrap();
|
||||||
assert_eq!(balances.0, U256::from(100000000000000000000u128));
|
assert_eq!(balances.0, U256::from(100_000_000_000_000_000_000u128));
|
||||||
assert_eq!(balances.1, U256::from(100000000000000000000u128));
|
assert_eq!(balances.1, U256::from(100_000_000_000_000_000_000u128));
|
||||||
assert_eq!(balances.2, U256::from(100000000000000000000u128));
|
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]
|
#[tokio::test]
|
||||||
|
|
Loading…
Reference in New Issue