refactor: make contract abstract over Borrow (#2082)

* refactor: contract abstract over Borrow

* refactor: preserve old connect usage

* nit: remove commented out modules

* chore: changelog

* test: add compile check to tests

* docs: add usage note to doc and make sure contractcall is visible

* fix: test compilation

* refactor: ContractCallInternal -> FunctionCall, ContractInternal -> ContractInstance

* fix: Send IntoFuture

* nit: must-use on connect

* docs: remove deprecation warning in docstrings for type aliases

* Update ethers-contract/src/call.rs

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>

---------

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
James Prestwich 2023-02-06 16:27:01 -05:00 committed by GitHub
parent 3323641311
commit 0236de8d2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 268 additions and 91 deletions

View File

@ -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

View File

@ -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<M: Middleware>(
self,
address: Address,
client: impl Into<Arc<M>>,
) -> Contract<M> {
Contract::new(address, self, client)
pub fn into_contract<B, M>(self, address: Address, client: B) -> ContractInstance<B, M>
where
B: Borrow<M>,
M: Middleware,
{
ContractInstance::new(address, self, client)
}
}

View File

@ -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<M: Middleware> {
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<M, D> = FunctionCall<std::sync::Arc<M>, 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<M, D> {
pub struct FunctionCall<B, M, D> {
/// 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<BlockId>,
pub(crate) client: Arc<M>,
pub(crate) client: B,
pub(crate) datatype: PhantomData<D>,
pub(crate) _m: PhantomData<M>,
}
impl<M, D> Clone for ContractCall<M, D> {
impl<B, M, D> Clone for FunctionCall<B, M, D>
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<M, D: Detokenize> ContractCall<M, D> {
impl<B, M, D> FunctionCall<B, M, D>
where
B: Borrow<M>,
D: Detokenize,
{
/// Sets the `from` field in the transaction to the provided value
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
self.tx.set_from(from.into());
@ -145,8 +160,9 @@ impl<M, D: Detokenize> ContractCall<M, D> {
}
}
impl<M, D> ContractCall<M, D>
impl<B, M, D> FunctionCall<B, M, D>
where
B: Borrow<M>,
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<U256, ContractError<M>> {
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<D, ContractError<M>> {
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<PendingTransaction<'_, M::Provider>, ContractError<M>> {
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<M, D> IntoFuture for ContractCall<M, D>
/// [`FunctionCall`] can be turned into [`Future`] automatically with `.await`.
/// Defaults to calling [`FunctionCall::call`].
impl<B, M, D> IntoFuture for FunctionCall<B, M, D>
where
Self: 'static,
B: Borrow<M> + Send + Sync,
M: Middleware,
D: Detokenize + Send + Sync,
{

View File

@ -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<M> = ContractInstance<std::sync::Arc<M>, 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<dyn std::error::Error>> {
/// // this is a fake address used just for this example
@ -82,7 +89,7 @@ use ethers_core::types::TransactionRequest;
/// let client = Provider::<Http>::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::<Address>()?;
/// # let abi: Abi = serde_json::from_str(r#"[]"#)?;
/// # let client = Provider::<Http>::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<M>` 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<M> + 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<M> {
pub struct ContractInstance<B, M> {
address: Address,
base_contract: BaseContract,
client: Arc<M>,
client: B,
_m: PhantomData<M>,
}
impl<M> std::ops::Deref for Contract<M> {
impl<B, M> std::ops::Deref for ContractInstance<B, M>
where
B: Borrow<M>,
{
type Target = BaseContract;
fn deref(&self) -> &Self::Target {
@ -164,18 +193,25 @@ impl<M> std::ops::Deref for Contract<M> {
}
}
impl<M> Clone for Contract<M> {
impl<B, M> Clone for ContractInstance<B, M>
where
B: Clone + Borrow<M>,
{
fn clone(&self) -> Self {
Contract {
ContractInstance {
base_contract: self.base_contract.clone(),
client: self.client.clone(),
address: self.address,
_m: self._m,
}
}
}
impl<M> Contract<M> {
/// Returns the contract's address.
impl<B, M> ContractInstance<B, M>
where
B: Borrow<M>,
{
/// Returns the contract's address
pub fn address(&self) -> Address {
self.address
}
@ -186,17 +222,24 @@ impl<M> Contract<M> {
}
/// Returns a pointer to the contract's client.
pub fn client(&self) -> Arc<M> {
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<M: Middleware> Contract<M> {
impl<B, M> ContractInstance<B, M>
where
B: Borrow<M>,
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<M: Middleware> Contract<M> {
}
}
impl<M: Middleware> Contract<M> {
impl<B, M> ContractInstance<B, M>
where
B: Borrow<M>,
M: Middleware,
{
/// Creates a new contract from the provided client, abi and address
pub fn new(
address: impl Into<Address>,
abi: impl Into<BaseContract>,
client: impl Into<Arc<M>>,
) -> Self {
Self { base_contract: abi.into(), client: client.into(), address: address.into() }
pub fn new(address: impl Into<Address>, abi: impl Into<BaseContract>, 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<M: Middleware> Contract<M> {
/// Returns an [`Event`](crate::builders::Event) builder with the provided filter.
pub fn event_with_filter<D: EthLogDecode>(&self, filter: Filter) -> Event<M, D> {
Event {
provider: &self.client,
provider: self.client.borrow(),
filter: filter.address(ValueOrArray::Value(self.address)),
datatype: PhantomData,
}
@ -240,40 +283,49 @@ impl<M: Middleware> Contract<M> {
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<T: Tokenize, D: Detokenize>(
&self,
name: &str,
args: T,
) -> Result<ContractCall<M, D>, 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<N>(&self, client: Arc<N>) -> ContractInstance<Arc<N>, 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<T: Tokenize, D: Detokenize>(
&self,
signature: Selector,
args: T,
) -> Result<ContractCall<M, D>, 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<C, N>(&self, client: C) -> ContractInstance<C, N>
where
C: Borrow<N>,
{
ContractInstance {
base_contract: self.base_contract.clone(),
client,
address: self.address,
_m: PhantomData,
}
}
}
impl<B, M> ContractInstance<B, M>
where
B: Clone + Borrow<M>,
M: Middleware,
{
fn method_func<T: Tokenize, D: Detokenize>(
&self,
function: &Function,
args: T,
) -> Result<ContractCall<M, D>, AbiError> {
) -> Result<FunctionCall<B, M, D>, AbiError> {
let data = encode_function_data(function, args)?;
#[cfg(feature = "legacy")]
@ -291,15 +343,45 @@ impl<M: Middleware> Contract<M> {
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<T: Tokenize, D: Detokenize>(
&self,
signature: Selector,
args: T,
) -> Result<FunctionCall<B, M, D>, 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<T: Tokenize, D: Detokenize>(
&self,
name: &str,
args: T,
) -> Result<FunctionCall<B, M, D>, 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<M: Middleware> Contract<M> {
this.address = address.into();
this
}
/// Returns a new contract instance using the provided client
///
/// Clones `self` internally
#[must_use]
pub fn connect<N>(&self, client: Arc<N>) -> Contract<N> {
Contract { base_contract: self.base_contract.clone(), client, address: self.address }
}
}

View File

@ -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;

View File

@ -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: M,
}
#[derive(Debug)]
pub struct MwErr<M: Middleware>(M::Error);
impl<M> ethers_providers::FromErr<M::Error> for MwErr<M>
where
M: Middleware,
{
fn from(src: M::Error) -> Self {
Self(src)
}
}
impl<M: Middleware> std::fmt::Display for MwErr<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Ok(())
}
}
impl<M: Middleware> std::error::Error for MwErr<M> {}
impl<M: Middleware> Middleware for NonClone<M> {
type Error = MwErr<M>;
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::<Http>::try_from(anvil.endpoint())
.unwrap()
.interval(Duration::from_millis(10u64));
// Works (B == M, M: Clone)
let c: ContractInstance<&Provider<Http>, Provider<Http>> =
ContractInstance::new(H160::default(), abi.clone(), &client);
let _ = c.method::<(), ()>("notARealMethod", ());
// Works (B == &M, M: Clone)
let c: ContractInstance<Provider<Http>, Provider<Http>> =
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<Provider<Http>>, NonClone<Provider<Http>>> =
ContractInstance::new(H160::default(), abi, &non_clone_mware);
let _ = c.method::<(), ()>("notARealMethod", ());
// // Fails (B == M, M: !Clone)
// let c: ContractInternal<NonClone<Provider<Http>>, NonClone<Provider<Http>>> =
// 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::<ValueChanged>();
let mut subscription = event2.subscribe().await.unwrap();