diff --git a/ethers-contract/src/base.rs b/ethers-contract/src/base.rs index 1374b8b56..3fe572ff3 100644 --- a/ethers-contract/src/base.rs +++ b/ethers-contract/src/base.rs @@ -200,7 +200,8 @@ impl BaseContract { decode_function_data_raw(function, bytes, false) } - fn get_fn_from_input(&self, input: &[u8]) -> Result<&Function, AbiError> { + /// Returns the function from the input bytes (useful for going from tx.data to function) + pub fn get_fn_from_input(&self, input: &[u8]) -> Result<&Function, AbiError> { let sig: [u8; 4] = input .get(0..4) .ok_or(AbiError::WrongSelector)? diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 701207561..891e6abfe 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -176,6 +176,30 @@ pub struct FunctionCall { pub(crate) _m: PhantomData, } +impl FunctionCall +where + B: Borrow, + M: Middleware, + D: Detokenize, +{ + /// Creates a new function call object + pub fn new( + tx: TypedTransaction, + function: Function, + client: B, + block: Option, + ) -> Self { + Self { + tx, + function, + client, + block, + datatype: PhantomData, + _m: PhantomData, + } + } +} + impl Clone for FunctionCall where B: Clone, diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 859b2102b..846b75335 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -30,7 +30,7 @@ pub mod stream; #[cfg(feature = "abigen")] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] -mod multicall; +pub mod multicall; #[cfg(feature = "abigen")] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] pub use multicall::{ diff --git a/ethers-contract/src/multicall/constants.rs b/ethers-contract/src/multicall/constants.rs index ac0fdbbd6..9c175704c 100644 --- a/ethers-contract/src/multicall/constants.rs +++ b/ethers-contract/src/multicall/constants.rs @@ -1,5 +1,14 @@ use ethers_core::types::{Chain, H160}; +/// presigned tx that will deploy multicall3 +pub const SIGNED_DEPLOY_MULTICALL_TX: &str = "0xf90f538085174876e800830f42408080b90f00608060405234801561001057600080fd5b50610ee0806100206000396000f3fe6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c00331ca0edce47092c0f398cebf3ffc267f05c8e7076e3b89445e0fe50f6332273d4569ba01b0b9d000e19b24c5869b0fc3b22b0d6fa47cd63316875cbbd577d76e6fde086"; + +/// address to be funded for `SIGNED_DEPLOY_MULTICALL_TX` to succeed +pub const DEPLOYER_ADDRESS: H160 = H160([ + 0x05, 0xf3, 0x2B, 0x3c, 0xC3, 0x88, 0x84, 0x53, 0xff, 0x71, 0xB0, 0x11, 0x35, 0xB3, 0x4F, 0xF8, + 0xe4, 0x12, 0x63, 0xF2, +]); + /// The Multicall3 contract address that is deployed in [`MULTICALL_SUPPORTED_CHAIN_IDS`]: /// [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11) pub const MULTICALL_ADDRESS: H160 = H160([ diff --git a/ethers-contract/src/multicall/middleware.rs b/ethers-contract/src/multicall/middleware.rs index 98474755d..36c0711e6 100644 --- a/ethers-contract/src/multicall/middleware.rs +++ b/ethers-contract/src/multicall/middleware.rs @@ -202,6 +202,7 @@ impl Multicall { let address: Address = match address { Some(addr) => addr, None => { + // TODO: replace with eth_getCode? let chain_id = client .get_chainid() .await diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 681890073..69b5e9cba 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -1,14 +1,18 @@ +//! Contains the `Multicall` contract which enables batching `eth_call` RPC requests + use std::result::Result as StdResult; /// The Multicall contract bindings. Auto-generated with `abigen`. pub mod contract; +/// vendored constants from [Multicall repo](https://github.com/mds1/multicall#new-deployments) pub mod constants; if_providers! { mod middleware; pub use middleware::{Call, Multicall, MulticallContract}; + /// The Multicall error types. pub mod error; } diff --git a/ethers-middleware/README.md b/ethers-middleware/README.md index 19c415d5f..ddd3e0787 100644 --- a/ethers-middleware/README.md +++ b/ethers-middleware/README.md @@ -18,6 +18,8 @@ For more information, please refer to the [book](https://gakonst.com/ethers-rs). - [`Transformer`](./transformer/trait.Transformer.html): Allows intercepting and transforming a transaction to be broadcasted via a proxy wallet, e.g. [`DSProxy`](./transformer/struct.DsProxy.html). +- [`MultiCall`](./multicall/struct.MulticallMiddleware.html): transparently batches multiple `eth_call` requests into a single + `eth_call` request, reducing the number of round trips to the RPC and saving API CU usage. ## Examples diff --git a/ethers-middleware/src/lib.rs b/ethers-middleware/src/lib.rs index 9c7518835..8490394b0 100644 --- a/ethers-middleware/src/lib.rs +++ b/ethers-middleware/src/lib.rs @@ -41,6 +41,11 @@ pub use policy::{ pub mod timelag; pub use timelag::TimeLag; +/// The [MulticallMiddleware] provides a way to batch multiple calls into a single call +pub mod multicall; +#[cfg(not(target_arch = "wasm32"))] +pub use multicall::MulticallMiddleware; + /// [MiddlewareBuilder] provides a way to compose many [`Middleware`]s in a concise way. pub mod builder; pub use builder::MiddlewareBuilder; diff --git a/ethers-middleware/src/multicall.rs b/ethers-middleware/src/multicall.rs new file mode 100644 index 000000000..c480683fb --- /dev/null +++ b/ethers-middleware/src/multicall.rs @@ -0,0 +1,390 @@ +#![cfg(not(target_arch = "wasm32"))] + +use async_trait::async_trait; +use ethers_contract::{ + multicall::{ + contract::{ + GetBlockNumberCall, GetBlockNumberReturn, GetEthBalanceCall, GetEthBalanceReturn, + MULTICALL3_ABI, + }, + Multicall, + }, + BaseContract, ContractCall, ContractError, MulticallError, +}; +use ethers_core::{ + abi::{encode, Abi, AbiDecode, AbiEncode, Token, Tokenizable}, + types::{ + transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, NameOrAddress, + TransactionRequest, + }, +}; +use ethers_providers::{Middleware, MiddlewareError}; +use std::{ops::Deref, sync::Arc}; +use thiserror::Error; +use tokio::sync::{mpsc, oneshot}; + +type MulticallResult = Result>>; +type MulticallRequest = (ContractCall, oneshot::Sender>); + +/// Processor for multicall middleware requests +#[derive(Debug)] +pub struct MulticallProcessor { + inner: Arc, + multicall_address: Option
, + max_batch_size: usize, + rx: mpsc::UnboundedReceiver>, +} + +/// Middleware used for transparently leveraging multicall functionality +#[derive(Debug, Clone)] +pub struct MulticallMiddleware { + inner: Arc, + contracts: Arc>, + tx: mpsc::UnboundedSender>, +} + +impl MiddlewareError for MulticallMiddlewareError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { + MulticallMiddlewareError::MiddlewareError(src) + } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + MulticallMiddlewareError::MiddlewareError(e) => Some(e), + MulticallMiddlewareError::MulticallError(e) => e.as_middleware_error(), + MulticallMiddlewareError::ProcessorNotRunning => None, + } + } +} + +/// Thrown when an error happens at the Multicall middleware +#[derive(Error, Debug)] +pub enum MulticallMiddlewareError { + /// Thrown when the internal middleware errors + #[error("{0}")] + MiddlewareError(M::Error), + /// Thrown when the internal multicall errors + #[error(transparent)] + MulticallError(#[from] Arc>), + /// Thrown when the processor isn't running + #[error("Processor is not running")] + ProcessorNotRunning, +} + +impl MulticallProcessor +where + M: Middleware, +{ + /// Should be run in a separate task to process requests + pub async fn run(mut self) -> Result<(), MulticallError> { + let mut multicall: Multicall = + Multicall::new(self.inner.clone(), self.multicall_address).await?; + + loop { + let mut requests = Vec::new(); + + // wait for the first request + match self.rx.recv().await { + Some(request) => requests.push(request), + None => break, + } + + // attempt to batch more requests, up to the max batch size + while requests.len() < self.max_batch_size { + match self.rx.try_recv() { + Ok(request) => requests.push(request), + + // For both errors (Disconnected and Empty), the correct action + // is to process the items. If the error was Disconnected, on + // the next iteration rx.recv().await will be None and we'll + // break from the outer loop anyway. + Err(_) => break, + } + } + + let (calls, callbacks): (Vec<_>, Vec<_>) = requests.into_iter().unzip(); + + multicall.clear_calls(); + for mut call in calls.into_iter() { + // use `to: None` as sentinel for system calls to get block number, etc + if call.tx.to().is_none() { + call.tx.set_to(multicall.contract.address()); + // do not allow reverts for system calls + multicall.add_call(call, false); + } else { + // allow reverts for user calls + multicall.add_call(call, true); + } + } + let results = multicall.call_raw().await; + + let responses = match results { + Ok(results) => results + .into_iter() + .map(|result| { + result.map_err(|e| { + Arc::new(MulticallError::ContractError(ContractError::Revert(e))) + }) + }) + .collect(), + Err(e) => vec![Err(Arc::new(e)); callbacks.len()], + }; + + for (callback, response) in callbacks.into_iter().zip(responses) { + // ignore errors, the receiver may have dropped + let _ = callback.send(response); + } + } + + Ok(()) + } +} + +impl MulticallMiddleware +where + M: Middleware, +{ + /// Instantiates the multicall middleware to recognize the given `match_abis` + /// # Panics + /// Panics if `max_batch_size` is less than 2 + pub fn new( + inner: M, + match_abis: Vec, + max_batch_size: usize, + multicall_address: Option
, + ) -> (Self, MulticallProcessor) { + if max_batch_size < 2 { + panic!("batches must be at least 2 calls to justify the overhead of multicall"); + } + + let (tx, rx) = mpsc::unbounded_channel(); + let client = Arc::new(inner); + + let contracts = Arc::new( + match_abis + .iter() + .map(|abi| abi.to_owned().into()) + .chain(vec![MULTICALL3_ABI.to_owned().into()]) + .collect(), + ); + + ( + Self { inner: client.clone(), tx, contracts }, + MulticallProcessor { inner: client, rx, multicall_address, max_batch_size }, + ) + } + + fn call_from_tx( + &self, + tx: &TypedTransaction, + block: Option, + ) -> Option> { + if let Some(data) = tx.data() { + for contract in self.contracts.iter() { + if let Ok(function) = contract.get_fn_from_input(data) { + return Some(ContractCall::new( + tx.clone(), + function.clone(), + self.inner.clone(), + block, + )); + } + } + } + None + } + + async fn batch_call( + &self, + call: ContractCall, + ) -> Result> { + let (tx, rx) = oneshot::channel(); + + if self.tx.send((call, tx)).is_err() { + return Err(MulticallMiddlewareError::ProcessorNotRunning); + }; + + match rx.await { + Err(_) => Err(MulticallMiddlewareError::ProcessorNotRunning), + Ok(response) => response + .map(|token| encode(&[token]).into()) + .map_err(MulticallMiddlewareError::MulticallError), + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl Middleware for MulticallMiddleware +where + M: Middleware, +{ + type Error = MulticallMiddlewareError; + type Provider = M::Provider; + type Inner = M; + + fn inner(&self) -> &M { + &self.inner + } + + async fn call( + &self, + tx: &TypedTransaction, + block: Option, + ) -> Result { + if let Some(call) = self.call_from_tx(tx, block) { + return self.batch_call(call).await; + } + + return self.inner.call(tx, block).await.map_err(MulticallMiddlewareError::from_err); + } + + async fn get_block_number(&self) -> Result { + let data = (GetBlockNumberCall {}).encode(); + let tx = TypedTransaction::Legacy(TransactionRequest::new().data(data.clone())); + self.call(&tx, None) + .await + .map(|b| GetBlockNumberReturn::decode(b.deref()).unwrap().block_number.as_u64().into()) + } + + async fn get_balance + Send + Sync>( + &self, + address: T, + block: Option, + ) -> Result { + let address_or_name = address.into(); + if address_or_name.as_name().is_some() { + return self + .inner + .get_balance(address_or_name, block) + .await + .map_err(MulticallMiddlewareError::from_err); + } + + let address = *address_or_name.as_address().unwrap(); + let data = (GetEthBalanceCall { addr: address }).encode(); + let tx = TypedTransaction::Legacy(TransactionRequest::new().data(data.clone())); + self.call(&tx, block).await.map(|b| GetEthBalanceReturn::decode(b.deref()).unwrap().balance) + } + + // TODO: implement more middleware functions? +} + +#[cfg(test)] +mod tests { + use super::*; + use ethers_contract::abigen; + use ethers_providers::{MockProvider, Provider}; + + abigen!(Test, r#"[read(string) view returns (bytes4)]"#); + + #[tokio::test] + #[should_panic( + expected = "batches must be at least 2 calls to justify the overhead of multicall" + )] + async fn needs_min_batch() { + // will panic if batch size is less than 2 + let _ = MulticallMiddleware::new(Provider::new(MockProvider::new()), vec![], 1, None); + } + + #[tokio::test] + async fn needs_processor() { + let (provider, _) = + MulticallMiddleware::new(Provider::new(MockProvider::new()), vec![], 2, None); + let e = provider.get_block_number().await.unwrap_err(); + assert!(matches!(e, MulticallMiddlewareError::ProcessorNotRunning)); + } + + #[tokio::test] + async fn matches_multicall_signatures() { + let (provider1, mock1) = Provider::mocked(); + let (provider2, mock2) = Provider::mocked(); + let mock_multicall = Address::random(); + + let (provider, processor) = + MulticallMiddleware::new(provider1.clone(), vec![], 2, Some(mock_multicall)); + + let mut multicall = Multicall::new(provider2.clone(), Some(mock_multicall)).await.unwrap(); + + tokio::spawn(async move { + let _ = processor.run().await; + }); + + let address = Address::zero(); + + let _ = tokio::join!(provider.get_block_number(), provider.get_balance(address, None)); + + let _ = + multicall.add_get_block_number().add_get_eth_balance(address, false).call_raw().await; + + assert!(mock1.requests_match(&mock2)); + } + + #[tokio::test] + async fn uses_batch_size() { + let (provider1, mock1) = Provider::mocked(); + let (provider2, mock2) = Provider::mocked(); + let mock_multicall = Address::random(); + + let (provider, processor) = + MulticallMiddleware::new(provider1.clone(), vec![], 2, Some(mock_multicall)); + + let mut multicall = Multicall::new(provider2.clone(), Some(mock_multicall)).await.unwrap(); + + tokio::spawn(async move { + let _ = processor.run().await; + }); + + let _ = tokio::join!( + provider.get_block_number(), + provider.get_block_number(), + provider.get_block_number() + ); + + let _ = multicall.add_get_block_number().add_get_block_number().call_raw().await; + multicall.clear_calls(); + + let _ = multicall.add_get_block_number().call_raw().await; + + assert!(mock1.requests_match(&mock2)); + } + + #[tokio::test] + async fn matches_provided_signatures() { + let (provider1, mock1) = Provider::mocked(); + let (provider2, mock2) = Provider::mocked(); + let mock_multicall = Address::random(); + + let (provider, processor) = MulticallMiddleware::new( + provider1.clone(), + vec![TEST_ABI.clone()], + 2, + Some(mock_multicall), + ); + + let mut multicall = Multicall::new(provider2.clone(), Some(mock_multicall)).await.unwrap(); + + let mock_test = Address::random(); + let test1 = Test::new(mock_test, Arc::new(provider.clone())); + let test2 = Test::new(mock_test, Arc::new(provider2.clone())); + + tokio::spawn(async move { + let _ = processor.run().await; + }); + + let call1 = test1.read("call1".to_string()); + let call2 = test1.read("call2".to_string()); + + let _ = tokio::join!(call1.call(), call2.call()); + + let _ = multicall + .add_call(test2.read("call1".to_string()), true) + .add_call(test2.read("call2".to_string()), true) + .call_raw() + .await; + + assert!(mock1.requests_match(&mock2)); + } +} diff --git a/ethers-middleware/tests/it/main.rs b/ethers-middleware/tests/it/main.rs index bf2dc441d..02e01a203 100644 --- a/ethers-middleware/tests/it/main.rs +++ b/ethers-middleware/tests/it/main.rs @@ -15,6 +15,8 @@ mod gas_oracle; #[cfg(not(feature = "celo"))] mod signer; +mod multicall; + #[cfg(not(feature = "celo"))] mod nonce_manager; diff --git a/ethers-middleware/tests/it/multicall.rs b/ethers-middleware/tests/it/multicall.rs new file mode 100644 index 000000000..862e7ee23 --- /dev/null +++ b/ethers-middleware/tests/it/multicall.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use crate::spawn_anvil; +use ethers_core::types::*; +use ethers_middleware::{MulticallMiddleware, SignerMiddleware}; + +use ethers_contract::{ + abigen, + multicall::constants::{DEPLOYER_ADDRESS, MULTICALL_ADDRESS, SIGNED_DEPLOY_MULTICALL_TX}, +}; +use ethers_providers::Middleware; +use ethers_signers::{LocalWallet, Signer}; + +abigen!( + SimpleRevertingStorage, + "../ethers-contract/tests/solidity-contracts/SimpleRevertingStorage.json" +); +abigen!(SimpleStorage, "../ethers-contract/tests/solidity-contracts/SimpleStorage.json"); + +#[cfg(not(feature = "celo"))] +#[tokio::test] +async fn multicall() { + let (provider, anvil) = spawn_anvil(); + let wallet: LocalWallet = anvil.keys()[0].clone().into(); + let client = + Arc::new(SignerMiddleware::new(provider.clone(), wallet.with_chain_id(anvil.chain_id()))); + + // 1. deploy multicall contract (if not already) + provider + .request::<(H160, U256), ()>( + "anvil_setBalance", + (DEPLOYER_ADDRESS, U256::from(1_000_000_000_000_000_000u64)), + ) + .await + .unwrap(); + provider + .request::<[serde_json::Value; 1], H256>( + "eth_sendRawTransaction", + [SIGNED_DEPLOY_MULTICALL_TX.into()], + ) + .await + .unwrap(); + + // 2. instantiate the multicall middleware + let (multicall_provider, multicall_processor) = MulticallMiddleware::new( + client, + vec![SIMPLEREVERTINGSTORAGE_ABI.to_owned(), SIMPLESTORAGE_ABI.to_owned()], + 10, + Some(MULTICALL_ADDRESS), + ); + let multicall_client = Arc::new(multicall_provider); + + // 3. deploy a contract to interact with + let value = "multicall!".to_string(); + let simple = SimpleStorage::deploy(multicall_client.clone(), value.clone()) + .unwrap() + .send() + .await + .unwrap(); + let simple_reverting = SimpleRevertingStorage::deploy(multicall_client.clone(), value.clone()) + .unwrap() + .send() + .await + .unwrap(); + + // 4. spawn the multicall processor + tokio::spawn(async move { + let _ = multicall_processor.run().await; + }); + + // 5. make some calls in parallel + tokio::join!( + async { + let val: String = simple.get_value().call().await.unwrap(); + assert_eq!(val, value); + }, + async { + let e = simple_reverting.get_value(true).call().await.unwrap_err(); + assert!(e.to_string().contains("call reverted")); + }, + async { + let bal = multicall_client.get_balance(DEPLOYER_ADDRESS, None).await.unwrap(); + assert!(bal > U256::zero()); + }, + async { + let block = multicall_client.get_block_number().await.unwrap(); + assert!(block > U64::zero()); + } + ); +} diff --git a/ethers-providers/src/rpc/transports/mock.rs b/ethers-providers/src/rpc/transports/mock.rs index c00d1455d..8637dc88e 100644 --- a/ethers-providers/src/rpc/transports/mock.rs +++ b/ethers-providers/src/rpc/transports/mock.rs @@ -5,6 +5,7 @@ use serde_json::Value; use std::{ borrow::Borrow, collections::VecDeque, + ops::Deref, sync::{Arc, Mutex}, }; use thiserror::Error; @@ -12,7 +13,7 @@ use thiserror::Error; /// Helper type that can be used to pass through the `params` value. /// This is necessary because the wrapper provider is supposed to skip the `params` if it's of /// size 0, see `crate::transports::common::Request` -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] enum MockParams { Value(Value), Zst, @@ -73,6 +74,11 @@ impl JsonRpcClient for MockProvider { } impl MockProvider { + /// Checks that the provided requests match + pub fn requests_match(&self, other: &Self) -> bool { + self.requests.lock().unwrap().deref() == other.requests.lock().unwrap().deref() + } + /// Checks that the provided request was submitted by the client pub fn assert_request( &self,