Implement Multicall functionality for batched calls (#43)
* Implement Multicall functionality for batched calls * Documentation, some modifications as suggested in the review * (Abigen) handle single input arg and set output irrespective of mutability * implement send functionality and allow clearing calls * Fix detokenization, dont require pre-processing anymore * panic when more than supported number of calls are pushed * add doc for panics in case of add_call * (multicall) eth_balance support, update bindings * refactor: move multicall to its own directory * fix: add infura api key * ci: ensure CI runs on PRs from forks * test(multicall): re-use aggregate call * contract: make multicall docs compile and remove redundant clones * ci: add public etherscan API key so that forks don't get rate limited * chore: adjust test contract naming Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
parent
41998d6d2d
commit
a9bb98b5a7
|
@ -1,10 +1,12 @@
|
||||||
on: push
|
on: pull_request
|
||||||
|
|
||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
# set for fetching ABIs for abigen from etherscan
|
# Yeah I know it's bad practice to have API keys, this is a read-only API key
|
||||||
|
# so that we do not get rate limited by Etherscan (and it's free to generate as
|
||||||
|
# many as you want)
|
||||||
env:
|
env:
|
||||||
ETHERSCAN_API_KEY: ${{ secrets.ETHERSCAN_API_KEY }}
|
ETHERSCAN_API_KEY: 76XKCZ4QKZYTJS8PBFUDZ292JBKEKS4974
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
|
|
|
@ -18,6 +18,7 @@ ethers-signers = { version = "0.1.3", path = "../ethers-signers" }
|
||||||
ethers-core = { version = "0.1.3", path = "../ethers-core" }
|
ethers-core = { version = "0.1.3", path = "../ethers-core" }
|
||||||
|
|
||||||
serde = { version = "1.0.110", default-features = false }
|
serde = { version = "1.0.110", default-features = false }
|
||||||
|
serde_json = "1.0.55"
|
||||||
rustc-hex = { version = "2.1.0", default-features = false }
|
rustc-hex = { version = "2.1.0", default-features = false }
|
||||||
thiserror = { version = "1.0.15", default-features = false }
|
thiserror = { version = "1.0.15", default-features = false }
|
||||||
once_cell = { version = "1.3.1", default-features = false }
|
once_cell = { version = "1.3.1", default-features = false }
|
||||||
|
|
|
@ -40,15 +40,7 @@ fn expand_function(function: &Function, alias: Option<Ident>) -> Result<TokenStr
|
||||||
|
|
||||||
let outputs = expand_fn_outputs(&function.outputs)?;
|
let outputs = expand_fn_outputs(&function.outputs)?;
|
||||||
|
|
||||||
let is_mutable = matches!(
|
let result = quote! { ContractCall<P, S, #outputs> };
|
||||||
function.state_mutability,
|
|
||||||
StateMutability::Nonpayable | StateMutability::Payable
|
|
||||||
);
|
|
||||||
let result = if !is_mutable {
|
|
||||||
quote! { ContractCall<P, S, #outputs> }
|
|
||||||
} else {
|
|
||||||
quote! { ContractCall<P, S, H256> }
|
|
||||||
};
|
|
||||||
|
|
||||||
let arg = expand_inputs_call_arg(&function.inputs);
|
let arg = expand_inputs_call_arg(&function.inputs);
|
||||||
let doc = util::expand_doc(&format!(
|
let doc = util::expand_doc(&format!(
|
||||||
|
@ -85,8 +77,13 @@ pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream {
|
||||||
let names = inputs
|
let names = inputs
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, param)| util::expand_input_name(i, ¶m.name));
|
.map(|(i, param)| util::expand_input_name(i, ¶m.name))
|
||||||
quote! { ( #( #names ,)* ) }
|
.collect::<Vec<TokenStream>>();
|
||||||
|
match names.len() {
|
||||||
|
0 => quote! { () },
|
||||||
|
1 => quote! { #( #names )* },
|
||||||
|
_ => quote! { ( #(#names, )* ) },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
|
fn expand_fn_outputs(outputs: &[Param]) -> Result<TokenStream> {
|
||||||
|
@ -113,6 +110,54 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use ethers_core::abi::ParamType;
|
use ethers_core::abi::ParamType;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_expand_inputs_call_arg() {
|
||||||
|
// no inputs
|
||||||
|
let params = vec![];
|
||||||
|
let token_stream = expand_inputs_call_arg(¶ms);
|
||||||
|
assert_eq!(token_stream.to_string(), "( )");
|
||||||
|
|
||||||
|
// single input
|
||||||
|
let params = vec![Param {
|
||||||
|
name: "arg_a".to_string(),
|
||||||
|
kind: ParamType::Address,
|
||||||
|
}];
|
||||||
|
let token_stream = expand_inputs_call_arg(¶ms);
|
||||||
|
assert_eq!(token_stream.to_string(), "arg_a");
|
||||||
|
|
||||||
|
// two inputs
|
||||||
|
let params = vec![
|
||||||
|
Param {
|
||||||
|
name: "arg_a".to_string(),
|
||||||
|
kind: ParamType::Address,
|
||||||
|
},
|
||||||
|
Param {
|
||||||
|
name: "arg_b".to_string(),
|
||||||
|
kind: ParamType::Uint(256usize),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let token_stream = expand_inputs_call_arg(¶ms);
|
||||||
|
assert_eq!(token_stream.to_string(), "( arg_a , arg_b , )");
|
||||||
|
|
||||||
|
// three inputs
|
||||||
|
let params = vec![
|
||||||
|
Param {
|
||||||
|
name: "arg_a".to_string(),
|
||||||
|
kind: ParamType::Address,
|
||||||
|
},
|
||||||
|
Param {
|
||||||
|
name: "arg_b".to_string(),
|
||||||
|
kind: ParamType::Uint(128usize),
|
||||||
|
},
|
||||||
|
Param {
|
||||||
|
name: "arg_c".to_string(),
|
||||||
|
kind: ParamType::Bool,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let token_stream = expand_inputs_call_arg(¶ms);
|
||||||
|
assert_eq!(token_stream.to_string(), "( arg_a , arg_b , arg_c , )");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn expand_inputs_empty() {
|
fn expand_inputs_empty() {
|
||||||
assert_quote!(expand_inputs(&[]).unwrap().to_string(), {},);
|
assert_quote!(expand_inputs(&[]).unwrap().to_string(), {},);
|
||||||
|
@ -157,7 +202,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn expand_fn_outputs_muliple() {
|
fn expand_fn_outputs_multiple() {
|
||||||
assert_quote!(
|
assert_quote!(
|
||||||
expand_fn_outputs(&[
|
expand_fn_outputs(&[
|
||||||
Param {
|
Param {
|
||||||
|
|
|
@ -24,6 +24,9 @@ pub use factory::ContractFactory;
|
||||||
|
|
||||||
mod event;
|
mod event;
|
||||||
|
|
||||||
|
mod multicall;
|
||||||
|
pub use multicall::Multicall;
|
||||||
|
|
||||||
/// This module exposes low lever builder structures which are only consumed by the
|
/// This module exposes low lever builder structures which are only consumed by the
|
||||||
/// type-safe ABI bindings generators.
|
/// type-safe ABI bindings generators.
|
||||||
pub mod builders {
|
pub mod builders {
|
||||||
|
|
|
@ -0,0 +1,379 @@
|
||||||
|
use ethers_core::{
|
||||||
|
abi::{Detokenize, Function, Token},
|
||||||
|
types::{Address, BlockNumber, NameOrAddress, TxHash, U256},
|
||||||
|
};
|
||||||
|
use ethers_providers::JsonRpcClient;
|
||||||
|
use ethers_signers::{Client, Signer};
|
||||||
|
|
||||||
|
use std::{collections::HashMap, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
call::{ContractCall, ContractError},
|
||||||
|
Lazy,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod multicall_contract;
|
||||||
|
use multicall_contract::MulticallContract;
|
||||||
|
|
||||||
|
/// A lazily computed hash map with the Ethereum network IDs as keys and the corresponding
|
||||||
|
/// Multicall smart contract addresses as values
|
||||||
|
pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
|
||||||
|
// mainnet
|
||||||
|
let addr =
|
||||||
|
Address::from_str("eefba1e63905ef1d7acba5a8513c70307c1ce441").expect("Decoding failed");
|
||||||
|
m.insert(U256::from(1u8), addr);
|
||||||
|
|
||||||
|
// rinkeby
|
||||||
|
let addr =
|
||||||
|
Address::from_str("42ad527de7d4e9d9d011ac45b31d8551f8fe9821").expect("Decoding failed");
|
||||||
|
m.insert(U256::from(4u8), addr);
|
||||||
|
|
||||||
|
// goerli
|
||||||
|
let addr =
|
||||||
|
Address::from_str("77dca2c955b15e9de4dbbcf1246b4b85b651e50e").expect("Decoding failed");
|
||||||
|
m.insert(U256::from(5u8), addr);
|
||||||
|
|
||||||
|
// kovan
|
||||||
|
let addr =
|
||||||
|
Address::from_str("2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a").expect("Decoding failed");
|
||||||
|
m.insert(U256::from(42u8), addr);
|
||||||
|
|
||||||
|
m
|
||||||
|
});
|
||||||
|
|
||||||
|
/// A Multicall is an abstraction for sending batched calls/transactions to the Ethereum blockchain.
|
||||||
|
/// It stores an instance of the [`Multicall` smart contract](https://etherscan.io/address/0xeefba1e63905ef1d7acba5a8513c70307c1ce441#code)
|
||||||
|
/// and the user provided list of transactions to be made.
|
||||||
|
///
|
||||||
|
/// `Multicall` can instantiate the Multicall contract instance from the chain ID of the client
|
||||||
|
/// supplied to [`new`]. It supports the Ethereum mainnet, as well as testnets
|
||||||
|
/// [Rinkeby](https://rinkeby.etherscan.io/address/0x42ad527de7d4e9d9d011ac45b31d8551f8fe9821#code),
|
||||||
|
/// [Goerli](https://goerli.etherscan.io/address/0x77dca2c955b15e9de4dbbcf1246b4b85b651e50e) and
|
||||||
|
/// [Kovan](https://kovan.etherscan.io/address/0x2cc8688c5f75e365aaeeb4ea8d6a480405a48d2a#code).
|
||||||
|
///
|
||||||
|
/// Additionally, the `block` number can be provided for the call by using the [`block`] method.
|
||||||
|
/// Build on the `Multicall` instance by adding calls using the [`add_call`] method.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use ethers::{
|
||||||
|
/// abi::Abi,
|
||||||
|
/// contract::{Contract, Multicall},
|
||||||
|
/// providers::{Http, Provider},
|
||||||
|
/// signers::{Client, Wallet},
|
||||||
|
/// types::{Address, H256, U256},
|
||||||
|
/// };
|
||||||
|
/// use std::{convert::TryFrom, sync::Arc};
|
||||||
|
///
|
||||||
|
/// # async fn bar() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// // this is a dummy address used for illustration purpose
|
||||||
|
/// let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
|
||||||
|
///
|
||||||
|
/// // (ugly way to write the ABI inline, you can otherwise read it from a file)
|
||||||
|
/// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?;
|
||||||
|
///
|
||||||
|
/// // connect to the network
|
||||||
|
/// let provider = Provider::<Http>::try_from("https://kovan.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27")?;
|
||||||
|
/// let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
|
||||||
|
/// .parse::<Wallet>()?.connect(provider);
|
||||||
|
///
|
||||||
|
/// // create the contract object. This will be used to construct the calls for multicall
|
||||||
|
/// let contract = Contract::new(address, abi, client.clone());
|
||||||
|
///
|
||||||
|
/// // note that these [`ContractCall`]s are futures, and need to be `.await`ed to resolve.
|
||||||
|
/// // But we will let `Multicall` to take care of that for us
|
||||||
|
/// let first_call = contract.method::<_, String>("getValue", ())?;
|
||||||
|
/// let second_call = contract.method::<_, Address>("lastSender", ())?;
|
||||||
|
///
|
||||||
|
/// // since this example connects to the Kovan testnet, we need not provide an address for
|
||||||
|
/// // the Multicall contract and we set that to `None`. If you wish to provide the address
|
||||||
|
/// // for the Multicall contract, you can pass the `Some(multicall_addr)` argument.
|
||||||
|
/// // Construction of the `Multicall` instance follows the builder pattern
|
||||||
|
/// let multicall = Multicall::new(client.clone(), None)
|
||||||
|
/// .await?
|
||||||
|
/// .add_call(first_call)
|
||||||
|
/// .add_call(second_call);
|
||||||
|
///
|
||||||
|
/// // `await`ing on the `call` method lets us fetch the return values of both the above calls
|
||||||
|
/// // in one single RPC call
|
||||||
|
/// let _return_data: (String, Address) = multicall.call().await?;
|
||||||
|
///
|
||||||
|
/// // the same `Multicall` instance can be re-used to do a different batch of transactions.
|
||||||
|
/// // Say we wish to broadcast (send) a couple of transactions via the Multicall contract.
|
||||||
|
/// let first_broadcast = contract.method::<_, H256>("setValue", "some value".to_owned())?;
|
||||||
|
/// let second_broadcast = contract.method::<_, H256>("setValue", "new value".to_owned())?;
|
||||||
|
/// let multicall = multicall
|
||||||
|
/// .clear_calls()
|
||||||
|
/// .add_call(first_broadcast)
|
||||||
|
/// .add_call(second_broadcast);
|
||||||
|
///
|
||||||
|
/// // `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 = client.provider().pending_transaction(tx_hash).await?;
|
||||||
|
///
|
||||||
|
/// // you can also query ETH balances of multiple addresses
|
||||||
|
/// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::<Address>()?;
|
||||||
|
/// let address_2 = "ffffffffffffffffffffffffffffffffffffffff".parse::<Address>()?;
|
||||||
|
/// let multicall = multicall
|
||||||
|
/// .clear_calls()
|
||||||
|
/// .eth_balance_of(address_1)
|
||||||
|
/// .eth_balance_of(address_2);
|
||||||
|
/// let _balances: (U256, U256) = multicall.call().await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// [`new`]: method@crate::Multicall::new
|
||||||
|
/// [`block`]: method@crate::Multicall::block
|
||||||
|
/// [`add_call`]: methond@crate::Multicall::add_call
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Multicall<P, S> {
|
||||||
|
calls: Vec<Call>,
|
||||||
|
block: Option<BlockNumber>,
|
||||||
|
contract: MulticallContract<P, S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
/// Helper struct for managing calls to be made to the `function` in smart contract `target`
|
||||||
|
/// with `data`
|
||||||
|
pub struct Call {
|
||||||
|
target: Address,
|
||||||
|
data: Vec<u8>,
|
||||||
|
function: Function,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P, S> Multicall<P, S>
|
||||||
|
where
|
||||||
|
P: JsonRpcClient,
|
||||||
|
S: Signer,
|
||||||
|
{
|
||||||
|
/// Creates a new Multicall instance from the provided client. If provided with an `address`,
|
||||||
|
/// it instantiates the Multicall contract with that address. Otherwise it fetches the address
|
||||||
|
/// from the address book.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If a `None` address is provided, and the provided client also does not belong to one of
|
||||||
|
/// the supported network IDs (mainnet, kovan, rinkeby and goerli)
|
||||||
|
pub async fn new<C: Into<Arc<Client<P, S>>>>(
|
||||||
|
client: C,
|
||||||
|
address: Option<Address>,
|
||||||
|
) -> Result<Self, ContractError> {
|
||||||
|
let client = client.into();
|
||||||
|
|
||||||
|
// Fetch chain id and the corresponding address of Multicall contract
|
||||||
|
// preference is given to Multicall contract's address if provided
|
||||||
|
// otherwise check the address book for the client's chain ID
|
||||||
|
let address: Address = match address {
|
||||||
|
Some(addr) => addr,
|
||||||
|
None => {
|
||||||
|
let chain_id = client.get_chainid().await?;
|
||||||
|
match ADDRESS_BOOK.get(&chain_id) {
|
||||||
|
Some(addr) => *addr,
|
||||||
|
None => panic!(
|
||||||
|
"Must either be a supported Network ID or provide Multicall contract address"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Instantiate the multicall contract
|
||||||
|
let contract = MulticallContract::new(address, client);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
calls: vec![],
|
||||||
|
block: None,
|
||||||
|
contract,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `block` field for the multicall aggregate call
|
||||||
|
pub fn block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
|
||||||
|
self.block = Some(block.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<P, S, D>) -> Self {
|
||||||
|
if self.calls.len() >= 16 {
|
||||||
|
panic!("Cannot support more than {} calls", 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
match (call.tx.to, call.tx.data) {
|
||||||
|
(Some(NameOrAddress::Address(target)), Some(data)) => {
|
||||||
|
let call = Call {
|
||||||
|
target,
|
||||||
|
data: data.0,
|
||||||
|
function: call.function,
|
||||||
|
};
|
||||||
|
self.calls.push(call);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
_ => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends a `call` to the list of calls for the Multicall instance for querying
|
||||||
|
/// the ETH balance of an address
|
||||||
|
///
|
||||||
|
/// # 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 eth_balance_of(self, addr: Address) -> Self {
|
||||||
|
let call = self.contract.get_eth_balance(addr);
|
||||||
|
self.add_call(call)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the batch of calls from the Multicall instance. Re-use the already instantiated
|
||||||
|
/// Multicall, to send a different batch of transactions or do another aggregate query
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// # use ethers::prelude::*;
|
||||||
|
/// # use std::{sync::Arc, convert::TryFrom};
|
||||||
|
/// #
|
||||||
|
/// # let provider = Provider::<Http>::try_from("http://localhost:8545")?;
|
||||||
|
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
|
||||||
|
/// # .parse::<Wallet>()?.connect(provider);
|
||||||
|
/// # let client = Arc::new(client);
|
||||||
|
/// #
|
||||||
|
/// # let abi = serde_json::from_str("")?;
|
||||||
|
/// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
|
||||||
|
/// # let contract = Contract::new(address, abi, client.clone());
|
||||||
|
/// #
|
||||||
|
/// # let broadcast_1 = contract.method::<_, H256>("setValue", "some value".to_owned())?;
|
||||||
|
/// # let broadcast_2 = contract.method::<_, H256>("setValue", "new value".to_owned())?;
|
||||||
|
/// #
|
||||||
|
/// let multicall = Multicall::new(client, None)
|
||||||
|
/// .await?
|
||||||
|
/// .add_call(broadcast_1)
|
||||||
|
/// .add_call(broadcast_2);
|
||||||
|
///
|
||||||
|
/// let _tx_hash = multicall.send().await?;
|
||||||
|
///
|
||||||
|
/// # let call_1 = contract.method::<_, String>("getValue", ())?;
|
||||||
|
/// # let call_2 = contract.method::<_, Address>("lastSender", ())?;
|
||||||
|
/// let multicall = multicall
|
||||||
|
/// .clear_calls()
|
||||||
|
/// .add_call(call_1)
|
||||||
|
/// .add_call(call_2);
|
||||||
|
/// let return_data: (String, Address) = multicall.call().await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub fn clear_calls(mut self) -> Self {
|
||||||
|
self.calls.clear();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the Ethereum blockchain via an `eth_call`, but via the Multicall contract.
|
||||||
|
///
|
||||||
|
/// It returns a [`ContractError`] if there is any error in the RPC call or while
|
||||||
|
/// detokenizing the tokens back to the expected return type. The return type must be
|
||||||
|
/// annonated while calling this method.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// # use ethers::prelude::*;
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// #
|
||||||
|
/// # let provider = Provider::<Http>::try_from("http://localhost:8545")?;
|
||||||
|
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
|
||||||
|
/// # .parse::<Wallet>()?.connect(provider);
|
||||||
|
/// #
|
||||||
|
/// # let multicall = Multicall::new(client, None).await?;
|
||||||
|
/// // If the Solidity function calls has the following return types:
|
||||||
|
/// // 1. `returns (uint256)`
|
||||||
|
/// // 2. `returns (string, address)`
|
||||||
|
/// // 3. `returns (bool)`
|
||||||
|
/// let result: (U256, (String, Address), bool) = multicall.call().await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note: this method _does not_ send a transaction from your account
|
||||||
|
///
|
||||||
|
/// [`ContractError`]: crate::ContractError
|
||||||
|
pub async fn call<D: Detokenize>(&self) -> Result<D, 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 tokens: Vec<Token> = call.function.decode_output(&bytes)?;
|
||||||
|
|
||||||
|
Ok(match tokens.len() {
|
||||||
|
0 => Token::Tuple(vec![]),
|
||||||
|
1 => tokens[0].clone(),
|
||||||
|
_ => Token::Tuple(tokens),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<Token>, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy.
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// # use ethers::prelude::*;
|
||||||
|
/// # use std::convert::TryFrom;
|
||||||
|
/// # let provider = Provider::<Http>::try_from("http://localhost:8545")?;
|
||||||
|
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"
|
||||||
|
/// # .parse::<Wallet>()?.connect(provider);
|
||||||
|
/// # let multicall = Multicall::new(client, None).await?;
|
||||||
|
/// let tx_hash = multicall.send().await?;
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Note: this method sends a transaction from your account, and will return an error
|
||||||
|
/// if you do not have sufficient funds to pay for gas
|
||||||
|
pub async fn send(&self) -> Result<TxHash, ContractError> {
|
||||||
|
let contract_call = self.as_contract_call();
|
||||||
|
|
||||||
|
// Broadcast transaction and return the transaction hash
|
||||||
|
let tx_hash = contract_call.send().await?;
|
||||||
|
|
||||||
|
Ok(tx_hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_contract_call(&self) -> ContractCall<P, S, (U256, Vec<Vec<u8>>)> {
|
||||||
|
// Map the Multicall struct into appropriate types for `aggregate` function
|
||||||
|
let calls: Vec<(Address, Vec<u8>)> = self
|
||||||
|
.calls
|
||||||
|
.iter()
|
||||||
|
.map(|call| (call.target, call.data.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Construct the ContractCall for `aggregate` function to broadcast the transaction
|
||||||
|
let contract_call = self.contract.aggregate(calls);
|
||||||
|
if let Some(block) = self.block {
|
||||||
|
contract_call.block(block)
|
||||||
|
} else {
|
||||||
|
contract_call
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
pub use multicallcontract_mod::*;
|
||||||
|
mod multicallcontract_mod {
|
||||||
|
#![allow(dead_code)]
|
||||||
|
#![allow(unused_imports)]
|
||||||
|
use crate::{
|
||||||
|
builders::{ContractCall, Event},
|
||||||
|
Contract, Lazy,
|
||||||
|
};
|
||||||
|
use ethers_core::{
|
||||||
|
abi::{Abi, Detokenize, InvalidOutputType, Token, Tokenizable},
|
||||||
|
types::*,
|
||||||
|
};
|
||||||
|
use ethers_providers::JsonRpcClient;
|
||||||
|
use ethers_signers::{Client, Signer};
|
||||||
|
#[doc = "MulticallContract was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs"]
|
||||||
|
use std::sync::Arc;
|
||||||
|
pub static MULTICALLCONTRACT_ABI: Lazy<Abi> = Lazy::new(|| {
|
||||||
|
serde_json :: from_str ( "[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"struct MulticallContract.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"name\":\"getBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockCoinbase\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockDifficulty\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockTimestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"getEthBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]" ) . expect ( "invalid abi" )
|
||||||
|
});
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MulticallContract<P, S>(Contract<P, S>);
|
||||||
|
impl<P, S> std::ops::Deref for MulticallContract<P, S> {
|
||||||
|
type Target = Contract<P, S>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<P: JsonRpcClient, S: Signer> std::fmt::Debug for MulticallContract<P, S> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
f.debug_tuple(stringify!(MulticallContract))
|
||||||
|
.field(&self.address())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a, P: JsonRpcClient, S: Signer> MulticallContract<P, S> {
|
||||||
|
#[doc = r" Creates a new contract instance with the specified `ethers`"]
|
||||||
|
#[doc = r" client at the given `Address`. The contract derefs to a `ethers::Contract`"]
|
||||||
|
#[doc = r" object"]
|
||||||
|
pub fn new<T: Into<Address>, C: Into<Arc<Client<P, S>>>>(address: T, client: C) -> Self {
|
||||||
|
let contract =
|
||||||
|
Contract::new(address.into(), MULTICALLCONTRACT_ABI.clone(), client.into());
|
||||||
|
Self(contract)
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `aggregate` (0x252dba42) function"]
|
||||||
|
pub fn aggregate(
|
||||||
|
&self,
|
||||||
|
calls: Vec<(Address, Vec<u8>)>,
|
||||||
|
) -> ContractCall<P, S, (U256, Vec<Vec<u8>>)> {
|
||||||
|
self.0
|
||||||
|
.method_hash([37, 45, 186, 66], calls)
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `getCurrentBlockDifficulty` (0x72425d9d) function"]
|
||||||
|
pub fn get_current_block_difficulty(&self) -> ContractCall<P, S, U256> {
|
||||||
|
self.0
|
||||||
|
.method_hash([114, 66, 93, 157], ())
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `getCurrentBlockGasLimit` (0x86d516e8) function"]
|
||||||
|
pub fn get_current_block_gas_limit(&self) -> ContractCall<P, S, U256> {
|
||||||
|
self.0
|
||||||
|
.method_hash([134, 213, 22, 232], ())
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `getCurrentBlockTimestamp` (0x0f28c97d) function"]
|
||||||
|
pub fn get_current_block_timestamp(&self) -> ContractCall<P, S, U256> {
|
||||||
|
self.0
|
||||||
|
.method_hash([15, 40, 201, 125], ())
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `getCurrentBlockCoinbase` (0xa8b0574e) function"]
|
||||||
|
pub fn get_current_block_coinbase(&self) -> ContractCall<P, S, Address> {
|
||||||
|
self.0
|
||||||
|
.method_hash([168, 176, 87, 78], ())
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `getBlockHash` (0xee82ac5e) function"]
|
||||||
|
pub fn get_block_hash(&self, block_number: U256) -> ContractCall<P, S, [u8; 32]> {
|
||||||
|
self.0
|
||||||
|
.method_hash([238, 130, 172, 94], block_number)
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `getEthBalance` (0x4d2301cc) function"]
|
||||||
|
pub fn get_eth_balance(&self, addr: Address) -> ContractCall<P, S, U256> {
|
||||||
|
self.0
|
||||||
|
.method_hash([77, 35, 1, 204], addr)
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
#[doc = "Calls the contract's `getLastBlockHash` (0x27e86d6e) function"]
|
||||||
|
pub fn get_last_block_hash(&self) -> ContractCall<P, S, [u8; 32]> {
|
||||||
|
self.0
|
||||||
|
.method_hash([39, 232, 109, 110], ())
|
||||||
|
.expect("method not found (this should never happen)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,12 +34,12 @@ impl Detokenize for ValueChanged {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// compiles the test contract
|
/// compiles the given contract and returns the ABI and Bytecode
|
||||||
pub fn compile() -> (Abi, Bytes) {
|
pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) {
|
||||||
let compiled = Solc::new("./tests/contract.sol").build().unwrap();
|
let compiled = Solc::new(&format!("./tests/solidity-contracts/{}", filename))
|
||||||
let contract = compiled
|
.build()
|
||||||
.get("SimpleStorage")
|
.unwrap();
|
||||||
.expect("could not find contract");
|
let contract = compiled.get(name).expect("could not find contract");
|
||||||
(contract.abi.clone(), contract.bytecode.clone())
|
(contract.abi.clone(), contract.bytecode.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,17 @@ pub use common::*;
|
||||||
mod eth_tests {
|
mod eth_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use ethers::{
|
use ethers::{
|
||||||
|
contract::Multicall,
|
||||||
providers::{Http, Provider, StreamExt},
|
providers::{Http, Provider, StreamExt},
|
||||||
signers::Client,
|
signers::Client,
|
||||||
types::Address,
|
types::{Address, U256},
|
||||||
utils::Ganache,
|
utils::Ganache,
|
||||||
};
|
};
|
||||||
use std::{convert::TryFrom, sync::Arc};
|
use std::{convert::TryFrom, sync::Arc};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn deploy_and_call_contract() {
|
async fn deploy_and_call_contract() {
|
||||||
let (abi, bytecode) = compile();
|
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
||||||
|
|
||||||
// launch ganache
|
// launch ganache
|
||||||
let ganache = Ganache::new().spawn();
|
let ganache = Ganache::new().spawn();
|
||||||
|
@ -85,7 +86,7 @@ mod eth_tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_past_events() {
|
async fn get_past_events() {
|
||||||
let (abi, bytecode) = compile();
|
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
||||||
let ganache = Ganache::new().spawn();
|
let ganache = Ganache::new().spawn();
|
||||||
let client = connect(&ganache, 0);
|
let client = connect(&ganache, 0);
|
||||||
let contract = deploy(client.clone(), abi, bytecode).await;
|
let contract = deploy(client.clone(), abi, bytecode).await;
|
||||||
|
@ -114,7 +115,7 @@ mod eth_tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn watch_events() {
|
async fn watch_events() {
|
||||||
let (abi, bytecode) = compile();
|
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
||||||
let ganache = Ganache::new().spawn();
|
let ganache = Ganache::new().spawn();
|
||||||
let client = connect(&ganache, 0);
|
let client = connect(&ganache, 0);
|
||||||
let contract = deploy(client, abi, bytecode).await;
|
let contract = deploy(client, abi, bytecode).await;
|
||||||
|
@ -149,7 +150,7 @@ mod eth_tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn signer_on_node() {
|
async fn signer_on_node() {
|
||||||
let (abi, bytecode) = compile();
|
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
||||||
// spawn ganache
|
// spawn ganache
|
||||||
let ganache = Ganache::new().spawn();
|
let ganache = Ganache::new().spawn();
|
||||||
|
|
||||||
|
@ -180,6 +181,152 @@ mod eth_tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(value, "hi");
|
assert_eq!(value, "hi");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multicall_aggregate() {
|
||||||
|
// get ABI and bytecode for the Multcall contract
|
||||||
|
let (multicall_abi, multicall_bytecode) = compile_contract("Multicall", "Multicall.sol");
|
||||||
|
|
||||||
|
// get ABI and bytecode for the NotSoSimpleStorage contract
|
||||||
|
let (not_so_simple_abi, not_so_simple_bytecode) =
|
||||||
|
compile_contract("NotSoSimpleStorage", "NotSoSimpleStorage.sol");
|
||||||
|
|
||||||
|
// get ABI and bytecode for the SimpleStorage contract
|
||||||
|
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
||||||
|
|
||||||
|
// launch ganache
|
||||||
|
let ganache = Ganache::new().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.
|
||||||
|
// `client` is used to deploy the Multicall contract
|
||||||
|
// `client2` is used to deploy the first SimpleStorage contract
|
||||||
|
// `client3` is used to deploy the second SimpleStorage contract
|
||||||
|
// `client4` is used to make the aggregate call
|
||||||
|
let client = connect(&ganache, 0);
|
||||||
|
let client2 = connect(&ganache, 1);
|
||||||
|
let client3 = connect(&ganache, 2);
|
||||||
|
let client4 = connect(&ganache, 3);
|
||||||
|
|
||||||
|
// create a factory which will be used to deploy instances of the contract
|
||||||
|
let multicall_factory =
|
||||||
|
ContractFactory::new(multicall_abi, multicall_bytecode, client.clone());
|
||||||
|
let simple_factory = ContractFactory::new(abi.clone(), bytecode.clone(), client2.clone());
|
||||||
|
let not_so_simple_factory =
|
||||||
|
ContractFactory::new(not_so_simple_abi, not_so_simple_bytecode, client3.clone());
|
||||||
|
|
||||||
|
let multicall_contract = multicall_factory.deploy(()).unwrap().send().await.unwrap();
|
||||||
|
let addr = multicall_contract.address();
|
||||||
|
|
||||||
|
let simple_contract = simple_factory
|
||||||
|
.deploy("the first one".to_string())
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let not_so_simple_contract = not_so_simple_factory
|
||||||
|
.deploy("the second one".to_string())
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Client2 and Client3 broadcast txs to set the values for both contracts
|
||||||
|
simple_contract
|
||||||
|
.connect(client2.clone())
|
||||||
|
.method::<_, H256>("setValue", "reset first".to_owned())
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
not_so_simple_contract
|
||||||
|
.connect(client3.clone())
|
||||||
|
.method::<_, H256>("setValue", "reset second".to_owned())
|
||||||
|
.unwrap()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// get the calls for `value` and `last_sender` for both SimpleStorage contracts
|
||||||
|
let value = simple_contract.method::<_, String>("getValue", ()).unwrap();
|
||||||
|
let value2 = not_so_simple_contract
|
||||||
|
.method::<_, (String, Address)>("getValues", ())
|
||||||
|
.unwrap();
|
||||||
|
let last_sender = simple_contract
|
||||||
|
.method::<_, Address>("lastSender", ())
|
||||||
|
.unwrap();
|
||||||
|
let last_sender2 = not_so_simple_contract
|
||||||
|
.method::<_, Address>("lastSender", ())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// initiate the Multicall instance and add calls one by one in builder style
|
||||||
|
let multicall = Multicall::new(client4.clone(), Some(addr))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.add_call(value)
|
||||||
|
.add_call(value2)
|
||||||
|
.add_call(last_sender)
|
||||||
|
.add_call(last_sender2);
|
||||||
|
|
||||||
|
let return_data: (String, (String, Address), Address, Address) =
|
||||||
|
multicall.call().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(return_data.0, "reset first");
|
||||||
|
assert_eq!((return_data.1).0, "reset second");
|
||||||
|
assert_eq!((return_data.1).1, client3.address());
|
||||||
|
assert_eq!(return_data.2, client2.address());
|
||||||
|
assert_eq!(return_data.3, client3.address());
|
||||||
|
|
||||||
|
// construct broadcast transactions that will be batched and broadcast via Multicall
|
||||||
|
let broadcast = simple_contract
|
||||||
|
.connect(client4.clone())
|
||||||
|
.method::<_, H256>("setValue", "first reset again".to_owned())
|
||||||
|
.unwrap();
|
||||||
|
let broadcast2 = not_so_simple_contract
|
||||||
|
.connect(client4.clone())
|
||||||
|
.method::<_, H256>("setValue", "second reset again".to_owned())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// use the already initialised Multicall instance, clearing the previous calls and adding
|
||||||
|
// new calls. Previously we used the `.call()` functionality to do a batch of calls in one
|
||||||
|
// go. Now we will use the `.send()` functionality to broadcast a batch of transactions
|
||||||
|
// in one go
|
||||||
|
let multicall_send = multicall
|
||||||
|
.clone()
|
||||||
|
.clear_calls()
|
||||||
|
.add_call(broadcast)
|
||||||
|
.add_call(broadcast2);
|
||||||
|
|
||||||
|
// broadcast the transaction and wait for it to be mined
|
||||||
|
let tx_hash = multicall_send.send().await.unwrap();
|
||||||
|
let _tx_receipt = client4.pending_transaction(tx_hash).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
|
||||||
|
// The `lastSender` calls should return the address of the Multicall contract, as it is
|
||||||
|
// the one acting as proxy and calling our SimpleStorage contracts (msg.sender)
|
||||||
|
let return_data: (String, (String, Address), Address, Address) =
|
||||||
|
multicall.call().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(return_data.0, "first reset again");
|
||||||
|
assert_eq!((return_data.1).0, "second reset again");
|
||||||
|
assert_eq!((return_data.1).1, multicall_contract.address());
|
||||||
|
assert_eq!(return_data.2, multicall_contract.address());
|
||||||
|
assert_eq!(return_data.3, multicall_contract.address());
|
||||||
|
|
||||||
|
// query ETH balances of multiple addresses
|
||||||
|
// these keys haven't been used to do any tx
|
||||||
|
// so should have 100 ETH
|
||||||
|
let multicall = multicall
|
||||||
|
.clear_calls()
|
||||||
|
.eth_balance_of(Address::from(&ganache.keys()[4]))
|
||||||
|
.eth_balance_of(Address::from(&ganache.keys()[5]))
|
||||||
|
.eth_balance_of(Address::from(&ganache.keys()[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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "celo")]
|
#[cfg(feature = "celo")]
|
||||||
|
@ -194,7 +341,7 @@ mod celo_tests {
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn deploy_and_call_contract() {
|
async fn deploy_and_call_contract() {
|
||||||
let (abi, bytecode) = compile();
|
let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
|
||||||
|
|
||||||
// Celo testnet
|
// Celo testnet
|
||||||
let provider =
|
let provider =
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
pragma solidity >=0.5.0;
|
||||||
|
pragma experimental ABIEncoderV2;
|
||||||
|
|
||||||
|
/// @title Multicall - Aggregate results from multiple read-only function calls
|
||||||
|
/// @author Michael Elliot <mike@makerdao.com>
|
||||||
|
/// @author Joshua Levine <joshua@makerdao.com>
|
||||||
|
/// @author Nick Johnson <arachnid@notdot.net>
|
||||||
|
|
||||||
|
contract Multicall {
|
||||||
|
struct Call {
|
||||||
|
address target;
|
||||||
|
bytes callData;
|
||||||
|
}
|
||||||
|
function aggregate(Call[] memory calls) public returns (uint256 blockNumber, bytes[] memory returnData) {
|
||||||
|
blockNumber = block.number;
|
||||||
|
returnData = new bytes[](calls.length);
|
||||||
|
for(uint256 i = 0; i < calls.length; i++) {
|
||||||
|
(bool success, bytes memory ret) = calls[i].target.call(calls[i].callData);
|
||||||
|
require(success);
|
||||||
|
returnData[i] = ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Helper functions
|
||||||
|
function getEthBalance(address addr) public view returns (uint256 balance) {
|
||||||
|
balance = addr.balance;
|
||||||
|
}
|
||||||
|
function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) {
|
||||||
|
blockHash = blockhash(blockNumber);
|
||||||
|
}
|
||||||
|
function getLastBlockHash() public view returns (bytes32 blockHash) {
|
||||||
|
blockHash = blockhash(block.number - 1);
|
||||||
|
}
|
||||||
|
function getCurrentBlockTimestamp() public view returns (uint256 timestamp) {
|
||||||
|
timestamp = block.timestamp;
|
||||||
|
}
|
||||||
|
function getCurrentBlockDifficulty() public view returns (uint256 difficulty) {
|
||||||
|
difficulty = block.difficulty;
|
||||||
|
}
|
||||||
|
function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) {
|
||||||
|
gaslimit = block.gaslimit;
|
||||||
|
}
|
||||||
|
function getCurrentBlockCoinbase() public view returns (address coinbase) {
|
||||||
|
coinbase = block.coinbase;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
pragma solidity >=0.4.24;
|
||||||
|
|
||||||
|
contract NotSoSimpleStorage {
|
||||||
|
|
||||||
|
event ValueChanged(address indexed author, address indexed oldAuthor, string oldValue, string newValue);
|
||||||
|
|
||||||
|
address public lastSender;
|
||||||
|
string _value;
|
||||||
|
|
||||||
|
constructor(string memory value) public {
|
||||||
|
emit ValueChanged(msg.sender, address(0), _value, value);
|
||||||
|
_value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue() view public returns (string memory) {
|
||||||
|
return _value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValues() view public returns (string memory, address) {
|
||||||
|
return (_value, lastSender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValue(string memory value) public {
|
||||||
|
emit ValueChanged(msg.sender, lastSender, _value, value);
|
||||||
|
_value = value;
|
||||||
|
lastSender = msg.sender;
|
||||||
|
}
|
||||||
|
}
|
|
@ -566,4 +566,41 @@ mod tests {
|
||||||
assert_eq!((-4i64).into_token(), Token::Int(U256::MAX - 3));
|
assert_eq!((-4i64).into_token(), Token::Int(U256::MAX - 3));
|
||||||
assert_eq!((-5i128).into_token(), Token::Int(U256::MAX - 4));
|
assert_eq!((-5i128).into_token(), Token::Int(U256::MAX - 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_detokenize() {
|
||||||
|
// handle tuple of one element
|
||||||
|
let tokens = vec![Token::FixedBytes(vec![1, 2, 3, 4]), Token::Bool(true)];
|
||||||
|
let tokens = vec![Token::Tuple(tokens)];
|
||||||
|
let data: ([u8; 4], bool) = Detokenize::from_tokens(tokens).unwrap();
|
||||||
|
assert_eq!(data.0[0], 1);
|
||||||
|
assert_eq!(data.0[1], 2);
|
||||||
|
assert_eq!(data.0[2], 3);
|
||||||
|
assert_eq!(data.0[3], 4);
|
||||||
|
assert_eq!(data.1, true);
|
||||||
|
|
||||||
|
// handle vector of more than one elements
|
||||||
|
let tokens = vec![Token::Bool(false), Token::Uint(U256::from(13u8))];
|
||||||
|
let data: (bool, u8) = Detokenize::from_tokens(tokens).unwrap();
|
||||||
|
assert_eq!(data.0, false);
|
||||||
|
assert_eq!(data.1, 13u8);
|
||||||
|
|
||||||
|
// handle more than two tuples
|
||||||
|
let tokens1 = vec![Token::FixedBytes(vec![1, 2, 3, 4]), Token::Bool(true)];
|
||||||
|
let tokens2 = vec![Token::Bool(false), Token::Uint(U256::from(13u8))];
|
||||||
|
let tokens = vec![Token::Tuple(tokens1), Token::Tuple(tokens2)];
|
||||||
|
let data: (([u8; 4], bool), (bool, u8)) = Detokenize::from_tokens(tokens).unwrap();
|
||||||
|
assert_eq!((data.0).0[0], 1);
|
||||||
|
assert_eq!((data.0).0[1], 2);
|
||||||
|
assert_eq!((data.0).0[2], 3);
|
||||||
|
assert_eq!((data.0).0[3], 4);
|
||||||
|
assert_eq!((data.0).1, true);
|
||||||
|
assert_eq!((data.1).0, false);
|
||||||
|
assert_eq!((data.1).1, 13u8);
|
||||||
|
|
||||||
|
// error if no tokens in the vector
|
||||||
|
let tokens = vec![];
|
||||||
|
let data: Result<U256, InvalidOutputType> = Detokenize::from_tokens(tokens);
|
||||||
|
assert!(data.is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue