refactor: extract minimal features of Contract into BaseContract (#88)

* refactor: extract minimal features of Contract into BaseContract

* refactor: move BaseContract to own file

* chore: ensure celo test passes

* chore: fix clippy

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Pawan Dhananjay 2020-10-27 16:27:01 +05:30 committed by GitHub
parent a22f1f9aa0
commit 35e24ed412
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 46 deletions

View File

@ -0,0 +1,74 @@
use crate::Contract;
use ethers_core::{
abi::{Abi, FunctionExt},
types::{Address, Selector},
};
use ethers_providers::Middleware;
use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc};
/// A reduced form of `Contract` which just takes the `abi` and produces
/// ABI encoded data for its functions.
#[derive(Debug, Clone)]
pub struct BaseContract {
pub(crate) abi: Abi,
/// A mapping from method signature to a name-index pair for accessing
/// functions in the contract ABI. This is used to avoid allocation when
/// searching for matching functions by signature.
// Adapted from: https://github.com/gnosis/ethcontract-rs/blob/master/src/contract.rs
pub(crate) methods: HashMap<Selector, (String, usize)>,
}
impl From<Abi> for BaseContract {
/// Creates a new `BaseContract` from the abi.
fn from(abi: Abi) -> Self {
let methods = create_mapping(&abi.functions, |function| function.selector());
Self { abi, methods }
}
}
impl BaseContract {
/// Returns a reference to the contract's ABI
pub fn abi(&self) -> &Abi {
&self.abi
}
/// 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)
}
}
impl AsRef<Abi> for BaseContract {
fn as_ref(&self) -> &Abi {
self.abi()
}
}
/// Utility function for creating a mapping between a unique signature and a
/// name-index pair for accessing contract ABI items.
fn create_mapping<T, S, F>(
elements: &HashMap<String, Vec<T>>,
signature: F,
) -> HashMap<S, (String, usize)>
where
S: Hash + Eq,
F: Fn(&T) -> S,
{
let signature = &signature;
elements
.iter()
.flat_map(|(name, sub_elements)| {
sub_elements
.iter()
.enumerate()
.map(move |(index, element)| (signature(element), (name.to_owned(), index)))
})
.collect()
}

View File

@ -1,13 +1,13 @@
use super::{call::ContractCall, event::Event}; use super::{base::BaseContract, call::ContractCall, event::Event};
use ethers_core::{ use ethers_core::{
abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize}, abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize},
types::{Address, Filter, NameOrAddress, Selector, TransactionRequest, TxHash}, types::{Address, Filter, NameOrAddress, Selector, TransactionRequest, TxHash},
}; };
use ethers_providers::{Middleware, PendingTransaction}; use ethers_providers::{Middleware, PendingTransaction};
use rustc_hex::ToHex; use rustc_hex::ToHex;
use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData, sync::Arc}; use std::{fmt::Debug, marker::PhantomData, sync::Arc};
/// A Contract is an abstraction of an executable program on the Ethereum Blockchain. /// 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 /// It has code (called byte code) as well as allocated long-term memory
@ -160,34 +160,25 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData, syn
/// [`method`]: method@crate::Contract::method /// [`method`]: method@crate::Contract::method
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Contract<M> { pub struct Contract<M> {
base_contract: BaseContract,
client: Arc<M>, client: Arc<M>,
abi: Abi,
address: Address, address: Address,
/// A mapping from method signature to a name-index pair for accessing
/// functions in the contract ABI. This is used to avoid allocation when
/// searching for matching functions by signature.
// Adapted from: https://github.com/gnosis/ethcontract-rs/blob/master/src/contract.rs
methods: HashMap<Selector, (String, usize)>,
} }
impl<M: Middleware> Contract<M> { impl<M: Middleware> Contract<M> {
/// Creates a new contract from the provided client, abi and address /// Creates a new contract from the provided client, abi and address
pub fn new(address: Address, abi: Abi, client: impl Into<Arc<M>>) -> Self { pub fn new(address: Address, abi: impl Into<BaseContract>, client: impl Into<Arc<M>>) -> Self {
let methods = create_mapping(&abi.functions, |function| function.selector());
Self { Self {
base_contract: abi.into(),
client: client.into(), client: client.into(),
abi,
address, address,
methods,
} }
} }
/// Returns an [`Event`](crate::builders::Event) builder for the provided event name. /// Returns an [`Event`](crate::builders::Event) builder for the provided event name.
pub fn event<D: Detokenize>(&self, name: &str) -> Result<Event<M, D>, Error> { pub fn event<D: Detokenize>(&self, name: &str) -> Result<Event<M, D>, Error> {
// get the event's full name // get the event's full name
let event = self.abi.event(name)?; let event = self.base_contract.abi.event(name)?;
Ok(Event { Ok(Event {
provider: &self.client, provider: &self.client,
filter: Filter::new() filter: Filter::new()
@ -207,7 +198,7 @@ impl<M: Middleware> Contract<M> {
args: T, args: T,
) -> Result<ContractCall<M, D>, Error> { ) -> Result<ContractCall<M, D>, Error> {
// get the function // get the function
let function = self.abi.function(name)?; let function = self.base_contract.abi.function(name)?;
self.method_func(function, args) self.method_func(function, args)
} }
@ -219,9 +210,10 @@ impl<M: Middleware> Contract<M> {
args: T, args: T,
) -> Result<ContractCall<M, D>, Error> { ) -> Result<ContractCall<M, D>, Error> {
let function = self let function = self
.base_contract
.methods .methods
.get(&signature) .get(&signature)
.map(|(name, index)| &self.abi.functions[name][*index]) .map(|(name, index)| &self.base_contract.abi.functions[name][*index])
.ok_or_else(|| Error::InvalidName(signature.to_hex::<String>()))?; .ok_or_else(|| Error::InvalidName(signature.to_hex::<String>()))?;
self.method_func(function, args) self.method_func(function, args)
} }
@ -283,7 +275,7 @@ impl<M: Middleware> Contract<M> {
/// Returns a reference to the contract's ABI /// Returns a reference to the contract's ABI
pub fn abi(&self) -> &Abi { pub fn abi(&self) -> &Abi {
&self.abi &self.base_contract.abi
} }
/// Returns a reference to the contract's client /// Returns a reference to the contract's client
@ -295,25 +287,3 @@ impl<M: Middleware> Contract<M> {
self.client.pending_transaction(tx_hash) self.client.pending_transaction(tx_hash)
} }
} }
/// Utility function for creating a mapping between a unique signature and a
/// name-index pair for accessing contract ABI items.
fn create_mapping<T, S, F>(
elements: &HashMap<String, Vec<T>>,
signature: F,
) -> HashMap<S, (String, usize)>
where
S: Hash + Eq,
F: Fn(&T) -> S,
{
let signature = &signature;
elements
.iter()
.flat_map(|(name, sub_elements)| {
sub_elements
.iter()
.enumerate()
.map(move |(index, element)| (signature(element), (name.to_owned(), index)))
})
.collect()
}

View File

@ -16,6 +16,9 @@
mod contract; mod contract;
pub use contract::Contract; pub use contract::Contract;
mod base;
pub use base::BaseContract;
mod call; mod call;
pub use call::ContractError; pub use call::ContractError;

View File

@ -234,13 +234,13 @@ impl<M: Middleware> Multicall<M> {
/// ///
/// ```no_run /// ```no_run
/// # async fn foo() -> Result<(), Box<dyn std::error::Error>> { /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
/// # use ethers::prelude::*; /// # use ethers::{abi::Abi, prelude::*};
/// # use std::{sync::Arc, convert::TryFrom}; /// # use std::{sync::Arc, convert::TryFrom};
/// # /// #
/// # let client = Provider::<Http>::try_from("http://localhost:8545")?; /// # let client = Provider::<Http>::try_from("http://localhost:8545")?;
/// # let client = Arc::new(client); /// # let client = Arc::new(client);
/// # /// #
/// # let abi = serde_json::from_str("")?; /// # let abi: Abi = serde_json::from_str("")?;
/// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?; /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::<Address>()?;
/// # let contract = Contract::<Provider<Http>>::new(address, abi, client.clone()); /// # let contract = Contract::<Provider<Http>>::new(address, abi, client.clone());
/// # /// #

View File

@ -385,7 +385,8 @@ mod celo_tests {
.send() .send()
.await .await
.unwrap(); .unwrap();
let _receipt = contract.pending_transaction(tx_hash).await.unwrap(); let receipt = contract.pending_transaction(tx_hash).await.unwrap();
assert_eq!(receipt.status.unwrap(), 1.into());
let value: String = contract let value: String = contract
.method("getValue", ()) .method("getValue", ())

View File

@ -1,5 +1,4 @@
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(intra_doc_link_resolution_failure)]
//! # Clients for interacting with Ethereum nodes //! # Clients for interacting with Ethereum nodes
//! //!
//! This crate provides asynchronous [Ethereum JSON-RPC](https://github.com/ethereum/wiki/wiki/JSON-RPC) //! This crate provides asynchronous [Ethereum JSON-RPC](https://github.com/ethereum/wiki/wiki/JSON-RPC)

View File

@ -4,7 +4,6 @@
rust_2018_idioms, rust_2018_idioms,
unreachable_pub unreachable_pub
)] )]
#![deny(intra_doc_link_resolution_failure)]
#![doc(test( #![doc(test(
no_crate_inject, no_crate_inject,
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))