refactor(ethers-contract): split to modules

This commit is contained in:
Georgios Konstantopoulos 2020-05-27 11:46:16 +03:00
parent 099fa5d7ef
commit f42aaf2588
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
9 changed files with 205 additions and 160 deletions

1
Cargo.lock generated
View File

@ -272,6 +272,7 @@ dependencies = [
"once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-hex 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.53 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)",
"url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -19,3 +19,4 @@ syn = "1.0.12"
url = "2.1" url = "2.1"
serde_json = "1.0.53" serde_json = "1.0.53"
once_cell = "1.4.0" once_cell = "1.4.0"
rustc-hex = { version = "2.1.0", default-features = false }

View File

@ -6,12 +6,13 @@ use quote::quote;
pub(crate) fn imports() -> TokenStream { pub(crate) fn imports() -> TokenStream {
quote! { quote! {
// TODO: Can we make this context aware so that it imports either ethers_contract
// or ethers::contract?
use ethers_contract::{ use ethers_contract::{
Sender, Event,
abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable}, abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable},
Contract, Lazy, Contract, ContractCall, Event, Lazy,
signers::{Client, Signer},
types::*, // import all the types so that we can codegen for everything types::*, // import all the types so that we can codegen for everything
signers::{Signer, Client},
providers::JsonRpcClient, providers::JsonRpcClient,
}; };
} }

View File

@ -48,7 +48,7 @@ fn expand_filter(event: &Event) -> Result<TokenStream> {
let ev_name = Literal::string(&event.name); let ev_name = Literal::string(&event.name);
let result = util::ident(&event.name.to_pascal_case()); let result = util::ident(&event.name.to_pascal_case());
let doc = util::expand_doc(&format!("Gets the {} event", event.name)); let doc = util::expand_doc(&format!("Gets the contract's `{}` event", event.name));
Ok(quote! { Ok(quote! {
#doc #doc

View File

@ -7,6 +7,7 @@ use anyhow::{anyhow, Context as _, Result};
use inflector::Inflector; use inflector::Inflector;
use proc_macro2::{Literal, TokenStream}; use proc_macro2::{Literal, TokenStream};
use quote::quote; use quote::quote;
use rustc_hex::ToHex;
use syn::Ident; use syn::Ident;
/// Expands a context into a method struct containing all the generated bindings /// Expands a context into a method struct containing all the generated bindings
@ -39,13 +40,17 @@ 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 result = if function.constant { let result = if function.constant {
quote! { Sender<'a, S, P, #outputs> } quote! { ContractCall<'a, S, P, #outputs> }
} else { } else {
quote! { Sender<'a, S, P, H256> } quote! { ContractCall<'a, S, P, H256> }
}; };
let arg = expand_inputs_call_arg(&function.inputs); let arg = expand_inputs_call_arg(&function.inputs);
let doc = util::expand_doc(&format!("Calls the contract's {} function", function.name)); let doc = util::expand_doc(&format!(
"Calls the contract's `{}` (0x{}) function",
function.name,
function.selector().to_hex::<String>()
));
Ok(quote! { Ok(quote! {
#doc #doc

View File

@ -0,0 +1,89 @@
use ethers_abi::{Detokenize, Function};
use ethers_providers::JsonRpcClient;
use ethers_signers::{Client, Signer};
use ethers_types::{Address, BlockNumber, TransactionRequest, H256, U256};
use std::{fmt::Debug, marker::PhantomData};
use thiserror::Error as ThisError;
pub struct ContractCall<'a, S, P, D> {
pub(crate) tx: TransactionRequest,
pub(crate) function: Function,
pub(crate) client: &'a Client<'a, S, P>,
pub(crate) block: Option<BlockNumber>,
pub(crate) datatype: PhantomData<D>,
}
impl<'a, S, P, D: Detokenize> ContractCall<'a, S, P, D> {
/// Sets the `from` field in the transaction to the provided value
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
self.tx.from = Some(from.into());
self
}
/// Sets the `gas` field in the transaction to the provided value
pub fn gas<T: Into<U256>>(mut self, gas: T) -> Self {
self.tx.gas = Some(gas.into());
self
}
/// Sets the `gas_price` field in the transaction to the provided value
pub fn gas_price<T: Into<U256>>(mut self, gas_price: T) -> Self {
self.tx.gas_price = Some(gas_price.into());
self
}
/// Sets the `value` field in the transaction to the provided value
pub fn value<T: Into<U256>>(mut self, value: T) -> Self {
self.tx.value = Some(value.into());
self
}
}
#[derive(ThisError, Debug)]
// TODO: Can we get rid of this static?
pub enum ContractError<P: JsonRpcClient>
where
P::Error: 'static,
{
#[error(transparent)]
DecodingError(#[from] ethers_abi::Error),
#[error(transparent)]
DetokenizationError(#[from] ethers_abi::InvalidOutputType),
#[error(transparent)]
CallError(P::Error),
}
impl<'a, S: Signer, P: JsonRpcClient, D: Detokenize> ContractCall<'a, S, P, D>
where
P::Error: 'static,
{
/// Queries the blockchain via an `eth_call` for the provided transaction.
///
/// If executed on a non-state mutating smart contract function (i.e. `view`, `pure`)
/// then it will return the raw data from the chain.
///
/// If executed on a mutating smart contract function, it will do a "dry run" of the call
/// and return the return type of the transaction without mutating the state
///
/// Note: this function _does not_ send a transaction from your account
pub async fn call(self) -> Result<D, ContractError<P>> {
let bytes = self
.client
.call(self.tx, self.block)
.await
.map_err(ContractError::CallError)?;
let tokens = self.function.decode_output(&bytes.0)?;
let data = D::from_tokens(tokens)?;
Ok(data)
}
/// Signs and broadcasts the provided transaction
pub async fn send(self) -> Result<H256, P::Error> {
self.client.send_transaction(self.tx, self.block).await
}
}

View File

@ -1,19 +1,17 @@
use ethers_abi::{ use crate::{ContractCall, Event};
Abi, Detokenize, Error, Event as AbiEvent, EventExt, Function, FunctionExt, RawLog, Tokenize,
}; use ethers_abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize};
use ethers_providers::{JsonRpcClient, Provider}; use ethers_providers::JsonRpcClient;
use ethers_signers::{Client, Signer}; use ethers_signers::{Client, Signer};
use ethers_types::{ use ethers_types::{Address, Filter, Selector, TransactionRequest};
Address, BlockNumber, Filter, Selector, TransactionRequest, ValueOrArray, H256, U256,
};
use rustc_hex::ToHex; use rustc_hex::ToHex;
use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData};
use thiserror::Error as ThisError;
/// Represents a contract instance at an address. Provides methods for /// Represents a contract instance at an address. Provides methods for
/// contract interaction. /// contract interaction.
// TODO: Should we separate the lifetimes for the two references?
// https://stackoverflow.com/a/29862184
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Contract<'a, S, P> { pub struct Contract<'a, S, P> {
client: &'a Client<'a, S, P>, client: &'a Client<'a, S, P>,
@ -40,7 +38,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
} }
} }
/// Returns a transaction builder for the provided function name. If there are /// Returns an `Event` builder for the provided event name. If there are
/// multiple functions with the same name due to overloading, consider using /// multiple functions with the same name due to overloading, consider using
/// the `method_hash` method instead, since this will use the first match. /// the `method_hash` method instead, since this will use the first match.
pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result<Event<'a, 'b, P, D>, Error> pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result<Event<'a, 'b, P, D>, Error>
@ -64,7 +62,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
&self, &self,
name: &str, name: &str,
args: T, args: T,
) -> Result<Sender<'a, S, P, D>, Error> { ) -> Result<ContractCall<'a, S, P, D>, Error> {
// get the function // get the function
let function = self.abi.function(name)?; let function = self.abi.function(name)?;
self.method_func(function, args) self.method_func(function, args)
@ -76,7 +74,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
&self, &self,
signature: Selector, signature: Selector,
args: T, args: T,
) -> Result<Sender<'a, S, P, D>, Error> { ) -> Result<ContractCall<'a, S, P, D>, Error> {
let function = self let function = self
.methods .methods
.get(&signature) .get(&signature)
@ -89,7 +87,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
&self, &self,
function: &Function, function: &Function,
args: T, args: T,
) -> Result<Sender<'a, S, P, D>, Error> { ) -> Result<ContractCall<'a, S, P, D>, Error> {
// create the calldata // create the calldata
let data = function.encode_input(&args.into_tokens())?; let data = function.encode_input(&args.into_tokens())?;
@ -100,7 +98,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
..Default::default() ..Default::default()
}; };
Ok(Sender { Ok(ContractCall {
tx, tx,
client: self.client, client: self.client,
block: None, block: None,
@ -118,144 +116,6 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> {
} }
} }
pub struct Sender<'a, S, P, D> {
tx: TransactionRequest,
function: Function,
client: &'a Client<'a, S, P>,
block: Option<BlockNumber>,
datatype: PhantomData<D>,
}
impl<'a, S, P, D: Detokenize> Sender<'a, S, P, D> {
/// Sets the `from` field in the transaction to the provided value
pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
self.tx.from = Some(from.into());
self
}
/// Sets the `gas` field in the transaction to the provided value
pub fn gas<T: Into<U256>>(mut self, gas: T) -> Self {
self.tx.gas = Some(gas.into());
self
}
/// Sets the `gas_price` field in the transaction to the provided value
pub fn gas_price<T: Into<U256>>(mut self, gas_price: T) -> Self {
self.tx.gas_price = Some(gas_price.into());
self
}
/// Sets the `value` field in the transaction to the provided value
pub fn value<T: Into<U256>>(mut self, value: T) -> Self {
self.tx.value = Some(value.into());
self
}
}
#[derive(ThisError, Debug)]
// TODO: Can we get rid of this static?
pub enum ContractError<P: JsonRpcClient>
where
P::Error: 'static,
{
#[error(transparent)]
DecodingError(#[from] ethers_abi::Error),
#[error(transparent)]
DetokenizationError(#[from] ethers_abi::InvalidOutputType),
#[error(transparent)]
CallError(P::Error),
}
impl<'a, S: Signer, P: JsonRpcClient, D: Detokenize> Sender<'a, S, P, D>
where
P::Error: 'static,
{
pub async fn call(self) -> Result<D, ContractError<P>> {
let bytes = self
.client
.call(self.tx, self.block)
.await
.map_err(ContractError::CallError)?;
let tokens = self.function.decode_output(&bytes.0)?;
let data = D::from_tokens(tokens)?;
Ok(data)
}
pub async fn send(self) -> Result<H256, P::Error> {
self.client.send_transaction(self.tx, self.block).await
}
}
pub struct Event<'a, 'b, P, D> {
pub filter: Filter,
provider: &'a Provider<P>,
event: &'b AbiEvent,
datatype: PhantomData<D>,
}
impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> {
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.from_block = Some(block.into());
self
}
pub fn to_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.to_block = Some(block.into());
self
}
pub fn topic0<T: Into<ValueOrArray<H256>>>(mut self, topic: T) -> Self {
self.filter.topics.push(topic.into());
self
}
pub fn topics(mut self, topics: &[ValueOrArray<H256>]) -> Self {
self.filter.topics.extend_from_slice(topics);
self
}
}
// TODO: Can we get rid of the static?
impl<'a, 'b, P: JsonRpcClient, D: Detokenize> Event<'a, 'b, P, D>
where
P::Error: 'static,
{
pub async fn query(self) -> Result<Vec<D>, ContractError<P>> {
// get the logs
let logs = self
.provider
.get_logs(&self.filter)
.await
.map_err(ContractError::CallError)?;
let events = logs
.into_iter()
.map(|log| {
// ethabi parses the unindexed and indexed logs together to a
// vector of tokens
let tokens = self
.event
.parse_log(RawLog {
topics: log.topics,
data: log.data.0,
})?
.params
.into_iter()
.map(|param| param.value)
.collect::<Vec<_>>();
// convert the tokens to the requested datatype
Ok::<_, ContractError<P>>(D::from_tokens(tokens)?)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(events)
}
}
/// Utility function for creating a mapping between a unique signature and a /// Utility function for creating a mapping between a unique signature and a
/// name-index pair for accessing contract ABI items. /// name-index pair for accessing contract ABI items.
fn create_mapping<T, S, F>( fn create_mapping<T, S, F>(

View File

@ -0,0 +1,82 @@
use crate::ContractError;
use ethers_abi::{Detokenize, Event as AbiEvent, RawLog};
use ethers_providers::{JsonRpcClient, Provider};
use ethers_types::{BlockNumber, Filter, ValueOrArray, H256};
use std::marker::PhantomData;
pub struct Event<'a, 'b, P, D> {
pub filter: Filter,
pub(crate) provider: &'a Provider<P>,
pub(crate) event: &'b AbiEvent,
pub(crate) datatype: PhantomData<D>,
}
// TODO: Improve these functions
impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> {
#[allow(clippy::wrong_self_convention)]
pub fn from_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.from_block = Some(block.into());
self
}
#[allow(clippy::wrong_self_convention)]
pub fn to_block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.filter.to_block = Some(block.into());
self
}
pub fn topic0<T: Into<ValueOrArray<H256>>>(mut self, topic: T) -> Self {
self.filter.topics.push(topic.into());
self
}
pub fn topics(mut self, topics: &[ValueOrArray<H256>]) -> Self {
self.filter.topics.extend_from_slice(topics);
self
}
}
// TODO: Can we get rid of the static?
impl<'a, 'b, P: JsonRpcClient, D: Detokenize> Event<'a, 'b, P, D>
where
P::Error: 'static,
{
/// Queries the blockchain for the selected filter and returns a vector of matching
/// event logs
pub async fn query(self) -> Result<Vec<D>, ContractError<P>> {
// get the logs
let logs = self
.provider
.get_logs(&self.filter)
.await
.map_err(ContractError::CallError)?;
let events = logs
.into_iter()
.map(|log| {
// ethabi parses the unindexed and indexed logs together to a
// vector of tokens
let tokens = self
.event
.parse_log(RawLog {
topics: log.topics,
data: log.data.0,
})?
.params
.into_iter()
.map(|param| param.value)
.collect::<Vec<_>>();
// convert the tokens to the requested datatype
Ok::<_, ContractError<P>>(D::from_tokens(tokens)?)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(events)
}
// TODO: Add filter watchers
}

View File

@ -1,5 +1,11 @@
mod contract; mod contract;
pub use contract::*; pub use contract::Contract;
mod event;
pub use event::Event;
mod call;
pub use call::{ContractCall, ContractError};
#[cfg(feature = "abigen")] #[cfg(feature = "abigen")]
pub use ethers_contract_abigen::Builder; pub use ethers_contract_abigen::Builder;