diff --git a/CHANGELOG.md b/CHANGELOG.md index d434c479..ce8d0fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -297,6 +297,8 @@ ### Unreleased +- Make `Contract` objects generic over the borrow trait, to allow non-arc mware + [#2082](https://github.com/gakonst/ethers-rs/pull/2082) - Return pending transaction from `Multicall::send` [#2044](https://github.com/gakonst/ethers-rs/pull/2044) - Add abigen to default features diff --git a/ethers-contract/src/base.rs b/ethers-contract/src/base.rs index 56792137..6f0c4395 100644 --- a/ethers-contract/src/base.rs +++ b/ethers-contract/src/base.rs @@ -1,4 +1,4 @@ -use crate::Contract; +use crate::contract::ContractInstance; pub use ethers_core::abi::AbiError; use ethers_core::{ @@ -8,10 +8,10 @@ use ethers_core::{ use ethers_providers::Middleware; use std::{ + borrow::Borrow, collections::{BTreeMap, HashMap}, fmt::Debug, hash::Hash, - sync::Arc, }; /// A reduced form of `Contract` which just takes the `abi` and produces @@ -195,12 +195,12 @@ impl BaseContract { } /// Upgrades a `BaseContract` into a full fledged contract with an address and middleware. - pub fn into_contract( - self, - address: Address, - client: impl Into>, - ) -> Contract { - Contract::new(address, self, client) + pub fn into_contract(self, address: Address, client: B) -> ContractInstance + where + B: Borrow, + M: Middleware, + { + ContractInstance::new(address, self, client) } } diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 676bd8d8..97923ebd 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -15,12 +15,11 @@ use ethers_providers::{ }; use std::{ - borrow::Cow, + borrow::{Borrow, Cow}, fmt::Debug, future::{Future, IntoFuture}, marker::PhantomData, pin::Pin, - sync::Arc, }; use thiserror::Error as ThisError; @@ -73,33 +72,49 @@ pub enum ContractError { ContractNotDeployed, } +/// `ContractCall` is a [`FunctionCall`] object with an [`std::sync::Arc`] middleware. +/// This type alias exists to preserve backwards compatibility with +/// less-abstract Contracts. +/// +/// For full usage docs, see [`FunctionCall`]. +pub type ContractCall = FunctionCall, M, D>; + #[derive(Debug)] #[must_use = "contract calls do nothing unless you `send` or `call` them"] /// Helper for managing a transaction before submitting it to a node -pub struct ContractCall { +pub struct FunctionCall { /// The raw transaction object pub tx: TypedTransaction, /// The ABI of the function being called pub function: Function, /// Optional block number to be used when calculating the transaction's gas and nonce pub block: Option, - pub(crate) client: Arc, + pub(crate) client: B, pub(crate) datatype: PhantomData, + pub(crate) _m: PhantomData, } -impl Clone for ContractCall { +impl Clone for FunctionCall +where + B: Clone, +{ fn clone(&self) -> Self { - ContractCall { + FunctionCall { tx: self.tx.clone(), function: self.function.clone(), block: self.block, client: self.client.clone(), datatype: self.datatype, + _m: self._m, } } } -impl ContractCall { +impl FunctionCall +where + B: Borrow, + D: Detokenize, +{ /// Sets the `from` field in the transaction to the provided value pub fn from>(mut self, from: T) -> Self { self.tx.set_from(from.into()); @@ -145,8 +160,9 @@ impl ContractCall { } } -impl ContractCall +impl FunctionCall where + B: Borrow, M: Middleware, D: Detokenize, { @@ -157,7 +173,11 @@ where /// Returns the estimated gas cost for the underlying transaction to be executed pub async fn estimate_gas(&self) -> Result> { - self.client.estimate_gas(&self.tx, self.block).await.map_err(ContractError::MiddlewareError) + self.client + .borrow() + .estimate_gas(&self.tx, self.block) + .await + .map_err(ContractError::MiddlewareError) } /// Queries the blockchain via an `eth_call` for the provided transaction. @@ -170,8 +190,12 @@ where /// /// Note: this function _does not_ send a transaction from your account pub async fn call(&self) -> Result> { - let bytes = - self.client.call(&self.tx, self.block).await.map_err(ContractError::MiddlewareError)?; + let bytes = self + .client + .borrow() + .call(&self.tx, self.block) + .await + .map_err(ContractError::MiddlewareError)?; // decode output let data = decode_function_data(&self.function, &bytes, false)?; @@ -202,7 +226,7 @@ where /// /// Note: this function _does not_ send a transaction from your account pub fn call_raw_bytes(&self) -> CallBuilder<'_, M::Provider> { - let call = self.client.provider().call_raw(&self.tx); + let call = self.client.borrow().provider().call_raw(&self.tx); if let Some(block) = self.block { call.block(block) } else { @@ -213,17 +237,19 @@ where /// Signs and broadcasts the provided transaction pub async fn send(&self) -> Result, ContractError> { self.client + .borrow() .send_transaction(self.tx.clone(), self.block) .await .map_err(ContractError::MiddlewareError) } } -/// [`ContractCall`] can be turned into [`Future`] automatically with `.await`. -/// Defaults to calling [`ContractCall::call`]. -impl IntoFuture for ContractCall +/// [`FunctionCall`] can be turned into [`Future`] automatically with `.await`. +/// Defaults to calling [`FunctionCall::call`]. +impl IntoFuture for FunctionCall where Self: 'static, + B: Borrow + Send + Sync, M: Middleware, D: Detokenize + Send + Sync, { diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index 09a2d713..c54418a9 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -1,6 +1,6 @@ use crate::{ base::{encode_function_data, AbiError, BaseContract}, - call::ContractCall, + call::FunctionCall, event::{EthEvent, Event}, EthLogDecode, }; @@ -9,13 +9,20 @@ use ethers_core::{ types::{Address, Filter, Selector, ValueOrArray}, }; use ethers_providers::Middleware; -use std::{marker::PhantomData, sync::Arc}; +use std::{borrow::Borrow, fmt::Debug, marker::PhantomData, sync::Arc}; #[cfg(not(feature = "legacy"))] use ethers_core::types::Eip1559TransactionRequest; #[cfg(feature = "legacy")] use ethers_core::types::TransactionRequest; +/// `Contract` is a [`ContractInstance`] object with an `Arc` middleware. +/// This type alias exists to preserve backwards compatibility with +/// less-abstract Contracts. +/// +/// For full usage docs, see [`ContractInstance`]. +pub type Contract = ContractInstance, M>; + /// A Contract is an abstraction of an executable program on the Ethereum Blockchain. /// It has code (called byte code) as well as allocated long-term memory /// (called storage). Every deployed Contract has an address, which is used to connect @@ -69,7 +76,7 @@ use ethers_core::types::TransactionRequest; /// use ethers_contract::Contract; /// use ethers_providers::{Provider, Http}; /// use ethers_signers::Wallet; -/// use std::convert::TryFrom; +/// use std::{convert::TryFrom, sync::Arc}; /// /// # async fn foo() -> Result<(), Box> { /// // this is a fake address used just for this example @@ -82,7 +89,7 @@ use ethers_core::types::TransactionRequest; /// let client = Provider::::try_from("http://localhost:8545").unwrap(); /// /// // create the contract object at the address -/// let contract = Contract::new(address, abi, client); +/// let contract = Contract::new(address, abi, Arc::new(client)); /// /// // Calling constant methods is done by calling `call()` on the method builder. /// // (if the function takes no arguments, then you must use `()` as the argument) @@ -116,13 +123,13 @@ use ethers_core::types::TransactionRequest; /// use ethers_contract::{Contract, EthEvent}; /// use ethers_providers::{Provider, Http, Middleware}; /// use ethers_signers::Wallet; -/// use std::convert::TryFrom; +/// use std::{convert::TryFrom, sync::Arc}; /// use ethers_core::abi::{Detokenize, Token, InvalidOutputType}; /// # // this is a fake address used just for this example /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; /// # let abi: Abi = serde_json::from_str(r#"[]"#)?; /// # let client = Provider::::try_from("http://localhost:8545").unwrap(); -/// # let contract = Contract::new(address, abi, client); +/// # let contract = Contract::new(address, abi, Arc::new(client)); /// /// #[derive(Clone, Debug, EthEvent)] /// struct ValueChanged { @@ -145,18 +152,40 @@ use ethers_core::types::TransactionRequest; /// /// _Disclaimer: these above docs have been adapted from the corresponding [ethers.js page](https://docs.ethers.io/ethers.js/html/api-contract.html)_ /// +/// # Usage Note +/// +/// `ContractInternal` accepts any client that implements `B: Borrow` where +/// `M :Middleware`. Previous `Contract` versions used only arcs, and relied +/// heavily on [`Arc`]. Due to constraints on the [`FunctionCall`] type, +/// calling contracts requires a `B: Borrow + Clone`. This is fine for most +/// middlware. However, when `B` is an owned middleware that is not Clone, we +/// cannot issue contract calls. Some notable exceptions: +/// +/// - `NonceManagerMiddleware` +/// - `SignerMiddleware` (when using a non-Clone Signer) +/// +/// When using non-Clone middlewares, instead of instantiating a contract that +/// OWNS the middlware, pass the contract a REFERENCE to the middleware. This +/// will fix the trait bounds issue (as `&M` is always `Clone`). +/// +/// We expect to fix this fully in a future version +/// /// [`abigen`]: macro.abigen.html /// [`Abigen` builder]: struct.Abigen.html -/// [`event`]: method@crate::Contract::event -/// [`method`]: method@crate::Contract::method +/// [`event`]: method@crate::ContractInstance::event +/// [`method`]: method@crate::ContractInstance::method #[derive(Debug)] -pub struct Contract { +pub struct ContractInstance { address: Address, base_contract: BaseContract, - client: Arc, + client: B, + _m: PhantomData, } -impl std::ops::Deref for Contract { +impl std::ops::Deref for ContractInstance +where + B: Borrow, +{ type Target = BaseContract; fn deref(&self) -> &Self::Target { @@ -164,18 +193,25 @@ impl std::ops::Deref for Contract { } } -impl Clone for Contract { +impl Clone for ContractInstance +where + B: Clone + Borrow, +{ fn clone(&self) -> Self { - Contract { + ContractInstance { base_contract: self.base_contract.clone(), client: self.client.clone(), address: self.address, + _m: self._m, } } } -impl Contract { - /// Returns the contract's address. +impl ContractInstance +where + B: Borrow, +{ + /// Returns the contract's address pub fn address(&self) -> Address { self.address } @@ -186,17 +222,24 @@ impl Contract { } /// Returns a pointer to the contract's client. - pub fn client(&self) -> Arc { - Arc::clone(&self.client) + pub fn client(&self) -> B + where + B: Clone, + { + self.client.clone() } /// Returns a reference to the contract's client. pub fn client_ref(&self) -> &M { - Arc::as_ref(&self.client) + self.client.borrow() } } -impl Contract { +impl ContractInstance +where + B: Borrow, + M: Middleware, +{ /// Returns an [`Event`](crate::builders::Event) builder for the provided event. /// This function operates in a static context, then it does not require a `self` /// to reference to instantiate an [`Event`](crate::builders::Event) builder. @@ -209,14 +252,14 @@ impl Contract { } } -impl Contract { +impl ContractInstance +where + B: Borrow, + M: Middleware, +{ /// Creates a new contract from the provided client, abi and address - pub fn new( - address: impl Into
, - abi: impl Into, - client: impl Into>, - ) -> Self { - Self { base_contract: abi.into(), client: client.into(), address: address.into() } + pub fn new(address: impl Into
, abi: impl Into, client: B) -> Self { + Self { base_contract: abi.into(), client, address: address.into(), _m: PhantomData } } /// Returns an [`Event`](crate::builders::Event) builder for the provided event. @@ -227,7 +270,7 @@ impl Contract { /// Returns an [`Event`](crate::builders::Event) builder with the provided filter. pub fn event_with_filter(&self, filter: Filter) -> Event { Event { - provider: &self.client, + provider: self.client.borrow(), filter: filter.address(ValueOrArray::Value(self.address)), datatype: PhantomData, } @@ -240,40 +283,49 @@ impl Contract { Ok(self.event_with_filter(Filter::new().event(&event.abi_signature()))) } - /// Returns a transaction builder for the provided function name. If there are - /// multiple functions with the same name due to overloading, consider using - /// the `method_hash` method instead, since this will use the first match. - pub fn method( - &self, - name: &str, - args: T, - ) -> Result, AbiError> { - // get the function - let function = self.base_contract.abi.function(name)?; - self.method_func(function, args) + /// Returns a new contract instance using the provided client + /// + /// Clones `self` internally + #[must_use] + pub fn connect(&self, client: Arc) -> ContractInstance, N> + where + N: Middleware, + { + ContractInstance { + base_contract: self.base_contract.clone(), + client, + address: self.address, + _m: PhantomData, + } } - /// Returns a transaction builder for the selected function signature. This should be - /// preferred if there are overloaded functions in your smart contract - pub fn method_hash( - &self, - signature: Selector, - args: T, - ) -> Result, AbiError> { - let function = self - .base_contract - .methods - .get(&signature) - .map(|(name, index)| &self.base_contract.abi.functions[name][*index]) - .ok_or_else(|| Error::InvalidName(hex::encode(signature)))?; - self.method_func(function, args) + /// Returns a new contract instance using the provided client + /// + /// Clones `self` internally + #[must_use] + pub fn connect_with(&self, client: C) -> ContractInstance + where + C: Borrow, + { + ContractInstance { + base_contract: self.base_contract.clone(), + client, + address: self.address, + _m: PhantomData, + } } +} +impl ContractInstance +where + B: Clone + Borrow, + M: Middleware, +{ fn method_func( &self, function: &Function, args: T, - ) -> Result, AbiError> { + ) -> Result, AbiError> { let data = encode_function_data(function, args)?; #[cfg(feature = "legacy")] @@ -291,15 +343,45 @@ impl Contract { let tx = tx.into(); - Ok(ContractCall { + Ok(FunctionCall { tx, - client: Arc::clone(&self.client), // cheap clone behind the Arc + client: self.client.clone(), block: None, function: function.to_owned(), datatype: PhantomData, + _m: self._m, }) } + /// Returns a transaction builder for the selected function signature. This should be + /// preferred if there are overloaded functions in your smart contract + pub fn method_hash( + &self, + signature: Selector, + args: T, + ) -> Result, AbiError> { + let function = self + .base_contract + .methods + .get(&signature) + .map(|(name, index)| &self.base_contract.abi.functions[name][*index]) + .ok_or_else(|| Error::InvalidName(hex::encode(signature)))?; + self.method_func(function, args) + } + + /// Returns a transaction builder for the provided function name. If there are + /// multiple functions with the same name due to overloading, consider using + /// the `method_hash` method instead, since this will use the first match. + pub fn method( + &self, + name: &str, + args: T, + ) -> Result, AbiError> { + // get the function + let function = self.base_contract.abi.function(name)?; + self.method_func(function, args) + } + /// Returns a new contract instance at `address`. /// /// Clones `self` internally @@ -309,12 +391,4 @@ impl Contract { this.address = address.into(); this } - - /// Returns a new contract instance using the provided client - /// - /// Clones `self` internally - #[must_use] - pub fn connect(&self, client: Arc) -> Contract { - Contract { base_contract: self.base_contract.clone(), client, address: self.address } - } } diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index f764616a..2d4fc645 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -3,13 +3,13 @@ #![deny(unsafe_code)] mod contract; -pub use contract::Contract; +pub use contract::{Contract, ContractInstance}; mod base; pub use base::{decode_function_data, encode_function_data, AbiError, BaseContract}; mod call; -pub use call::{ContractError, EthCall}; +pub use call::{ContractCall, ContractError, EthCall, FunctionCall}; mod error; pub use error::EthError; diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs index c4f58fba..0d3b7a64 100644 --- a/ethers-contract/tests/it/contract.rs +++ b/ethers-contract/tests/it/contract.rs @@ -6,10 +6,10 @@ use ethers_core::types::{Filter, ValueOrArray, H256}; #[cfg(not(feature = "celo"))] mod eth_tests { use super::*; - use ethers_contract::{EthEvent, LogMeta, Multicall, MulticallVersion}; + use ethers_contract::{ContractInstance, EthEvent, LogMeta, Multicall, MulticallVersion}; use ethers_core::{ abi::{encode, Detokenize, Token, Tokenizable}, - types::{transaction::eip712::Eip712, Address, BlockId, Bytes, I256, U256}, + types::{transaction::eip712::Eip712, Address, BlockId, Bytes, H160, I256, U256}, utils::{keccak256, Anvil}, }; use ethers_derive_eip712::*; @@ -17,6 +17,81 @@ mod eth_tests { use ethers_signers::{LocalWallet, Signer}; use std::{convert::TryFrom, iter::FromIterator, sync::Arc, time::Duration}; + #[derive(Debug)] + pub struct NonClone { + m: M, + } + + #[derive(Debug)] + pub struct MwErr(M::Error); + impl ethers_providers::FromErr for MwErr + where + M: Middleware, + { + fn from(src: M::Error) -> Self { + Self(src) + } + } + + impl std::fmt::Display for MwErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } + } + impl std::error::Error for MwErr {} + + impl Middleware for NonClone { + type Error = MwErr; + + type Provider = M::Provider; + + type Inner = M; + + fn inner(&self) -> &Self::Inner { + &self.m + } + } + + // this is not a test. It is a compile check. :) + // It exists to ensure that trait bounds on contract internal behave as + // expected. It should not be run + fn it_compiles() { + let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); + + // launch anvil + let anvil = Anvil::new().spawn(); + + let client = Provider::::try_from(anvil.endpoint()) + .unwrap() + .interval(Duration::from_millis(10u64)); + + // Works (B == M, M: Clone) + let c: ContractInstance<&Provider, Provider> = + ContractInstance::new(H160::default(), abi.clone(), &client); + + let _ = c.method::<(), ()>("notARealMethod", ()); + + // Works (B == &M, M: Clone) + let c: ContractInstance, Provider> = + ContractInstance::new(H160::default(), abi.clone(), client.clone()); + + let _ = c.method::<(), ()>("notARealMethod", ()); + + let non_clone_mware = NonClone { m: client }; + + // Works (B == &M, M: !Clone) + let c: ContractInstance<&NonClone>, NonClone>> = + ContractInstance::new(H160::default(), abi, &non_clone_mware); + + let _ = c.method::<(), ()>("notARealMethod", ()); + + // // Fails (B == M, M: !Clone) + // let c: ContractInternal>, NonClone>> = + // ContractInternal::new(H160::default(), abi, non_clone_mware); + + // let _ = c.method::<(), ()>("notARealMethod", ()); + } + #[tokio::test] async fn deploy_and_call_contract() { let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol"); @@ -265,7 +340,7 @@ mod eth_tests { // Also set up a subscription for the same thing let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); - let contract2 = ethers_contract::Contract::new(contract.address(), abi, ws); + let contract2 = ethers_contract::Contract::new(contract.address(), abi, ws.into()); let event2 = contract2.event::(); let mut subscription = event2.subscribe().await.unwrap();