From f42aaf25884306eeb15a57dc87f5c2538910fdf0 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 27 May 2020 11:46:16 +0300 Subject: [PATCH] refactor(ethers-contract): split to modules --- Cargo.lock | 1 + .../ethers-contract-abigen/Cargo.toml | 1 + .../src/contract/common.rs | 7 +- .../src/contract/events.rs | 2 +- .../src/contract/methods.rs | 11 +- crates/ethers-contract/src/call.rs | 89 ++++++++++ crates/ethers-contract/src/contract.rs | 164 ++---------------- crates/ethers-contract/src/event.rs | 82 +++++++++ crates/ethers-contract/src/lib.rs | 8 +- 9 files changed, 205 insertions(+), 160 deletions(-) create mode 100644 crates/ethers-contract/src/call.rs create mode 100644 crates/ethers-contract/src/event.rs diff --git a/Cargo.lock b/Cargo.lock index a8146c49..009dcc46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,6 +272,7 @@ dependencies = [ "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)", "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)", "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)", diff --git a/crates/ethers-contract/ethers-contract-abigen/Cargo.toml b/crates/ethers-contract/ethers-contract-abigen/Cargo.toml index 7aeb763a..34e9e18e 100644 --- a/crates/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/crates/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -19,3 +19,4 @@ syn = "1.0.12" url = "2.1" serde_json = "1.0.53" once_cell = "1.4.0" +rustc-hex = { version = "2.1.0", default-features = false } diff --git a/crates/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract/common.rs index 6bdc1ab1..467e7edd 100644 --- a/crates/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -6,12 +6,13 @@ use quote::quote; pub(crate) fn imports() -> TokenStream { quote! { + // TODO: Can we make this context aware so that it imports either ethers_contract + // or ethers::contract? use ethers_contract::{ - Sender, Event, 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 - signers::{Signer, Client}, providers::JsonRpcClient, }; } diff --git a/crates/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract/events.rs index c58a4a69..36b90ae2 100644 --- a/crates/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -48,7 +48,7 @@ fn expand_filter(event: &Event) -> Result { let ev_name = Literal::string(&event.name); 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! { #doc diff --git a/crates/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/crates/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index 7e05c4be..10d4eaba 100644 --- a/crates/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/crates/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Context as _, Result}; use inflector::Inflector; use proc_macro2::{Literal, TokenStream}; use quote::quote; +use rustc_hex::ToHex; use syn::Ident; /// Expands a context into a method struct containing all the generated bindings @@ -39,13 +40,17 @@ fn expand_function(function: &Function, alias: Option) -> Result } + quote! { ContractCall<'a, S, P, #outputs> } } else { - quote! { Sender<'a, S, P, H256> } + quote! { ContractCall<'a, S, P, H256> } }; 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::() + )); Ok(quote! { #doc diff --git a/crates/ethers-contract/src/call.rs b/crates/ethers-contract/src/call.rs new file mode 100644 index 00000000..160b6ba0 --- /dev/null +++ b/crates/ethers-contract/src/call.rs @@ -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, + pub(crate) datatype: PhantomData, +} + +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>(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>(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>(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>(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 +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> { + 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 { + self.client.send_transaction(self.tx, self.block).await + } +} diff --git a/crates/ethers-contract/src/contract.rs b/crates/ethers-contract/src/contract.rs index 9d0fd0f8..f2130fa4 100644 --- a/crates/ethers-contract/src/contract.rs +++ b/crates/ethers-contract/src/contract.rs @@ -1,19 +1,17 @@ -use ethers_abi::{ - Abi, Detokenize, Error, Event as AbiEvent, EventExt, Function, FunctionExt, RawLog, Tokenize, -}; -use ethers_providers::{JsonRpcClient, Provider}; +use crate::{ContractCall, Event}; + +use ethers_abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize}; +use ethers_providers::JsonRpcClient; use ethers_signers::{Client, Signer}; -use ethers_types::{ - Address, BlockNumber, Filter, Selector, TransactionRequest, ValueOrArray, H256, U256, -}; +use ethers_types::{Address, Filter, Selector, TransactionRequest}; use rustc_hex::ToHex; 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 /// contract interaction. +// TODO: Should we separate the lifetimes for the two references? +// https://stackoverflow.com/a/29862184 #[derive(Debug, Clone)] pub struct Contract<'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 /// the `method_hash` method instead, since this will use the first match. pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result, Error> @@ -64,7 +62,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> { &self, name: &str, args: T, - ) -> Result, Error> { + ) -> Result, Error> { // get the function let function = self.abi.function(name)?; self.method_func(function, args) @@ -76,7 +74,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> { &self, signature: Selector, args: T, - ) -> Result, Error> { + ) -> Result, Error> { let function = self .methods .get(&signature) @@ -89,7 +87,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> { &self, function: &Function, args: T, - ) -> Result, Error> { + ) -> Result, Error> { // create the calldata let data = function.encode_input(&args.into_tokens())?; @@ -100,7 +98,7 @@ impl<'a, S: Signer, P: JsonRpcClient> Contract<'a, S, P> { ..Default::default() }; - Ok(Sender { + Ok(ContractCall { tx, client: self.client, 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, - datatype: PhantomData, -} - -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>(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>(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>(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>(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 -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> { - 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 { - self.client.send_transaction(self.tx, self.block).await - } -} - -pub struct Event<'a, 'b, P, D> { - pub filter: Filter, - provider: &'a Provider

, - event: &'b AbiEvent, - datatype: PhantomData, -} - -impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> { - pub fn from_block>(mut self, block: T) -> Self { - self.filter.from_block = Some(block.into()); - self - } - - pub fn to_block>(mut self, block: T) -> Self { - self.filter.to_block = Some(block.into()); - self - } - - pub fn topic0>>(mut self, topic: T) -> Self { - self.filter.topics.push(topic.into()); - self - } - - pub fn topics(mut self, topics: &[ValueOrArray]) -> 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, ContractError

> { - // 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::>(); - - // convert the tokens to the requested datatype - Ok::<_, ContractError

>(D::from_tokens(tokens)?) - }) - .collect::, _>>()?; - - Ok(events) - } -} - /// Utility function for creating a mapping between a unique signature and a /// name-index pair for accessing contract ABI items. fn create_mapping( diff --git a/crates/ethers-contract/src/event.rs b/crates/ethers-contract/src/event.rs new file mode 100644 index 00000000..13982f6d --- /dev/null +++ b/crates/ethers-contract/src/event.rs @@ -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

, + pub(crate) event: &'b AbiEvent, + pub(crate) datatype: PhantomData, +} + +// TODO: Improve these functions +impl<'a, 'b, P, D: Detokenize> Event<'a, 'b, P, D> { + #[allow(clippy::wrong_self_convention)] + pub fn from_block>(mut self, block: T) -> Self { + self.filter.from_block = Some(block.into()); + self + } + + #[allow(clippy::wrong_self_convention)] + pub fn to_block>(mut self, block: T) -> Self { + self.filter.to_block = Some(block.into()); + self + } + + pub fn topic0>>(mut self, topic: T) -> Self { + self.filter.topics.push(topic.into()); + self + } + + pub fn topics(mut self, topics: &[ValueOrArray]) -> 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, ContractError

> { + // 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::>(); + + // convert the tokens to the requested datatype + Ok::<_, ContractError

>(D::from_tokens(tokens)?) + }) + .collect::, _>>()?; + + Ok(events) + } + + // TODO: Add filter watchers +} diff --git a/crates/ethers-contract/src/lib.rs b/crates/ethers-contract/src/lib.rs index 157109ed..c76e08a7 100644 --- a/crates/ethers-contract/src/lib.rs +++ b/crates/ethers-contract/src/lib.rs @@ -1,5 +1,11 @@ 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")] pub use ethers_contract_abigen::Builder;