From 6bd3c41bd082df15b310f235617505ea2c2ca647 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Tue, 2 Jun 2020 02:15:33 +0300 Subject: [PATCH] contract: simplify errors and generics --- ethers-contract/Cargo.toml | 3 ++ ethers-contract/src/call.rs | 71 ++++++++++++++------------- ethers-contract/src/contract.rs | 22 +++++---- ethers-contract/src/event.rs | 26 +++++----- ethers-contract/src/factory.rs | 32 +++++------- ethers-contract/tests/contract.rs | 78 ++++++++++++++++++++++++++++++ ethers-contract/tests/contract.sol | 22 +++++++++ ethers/src/lib.rs | 4 -- 8 files changed, 176 insertions(+), 82 deletions(-) create mode 100644 ethers-contract/tests/contract.rs create mode 100644 ethers-contract/tests/contract.sol diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 77436aa7..5a9cd711 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -18,6 +18,9 @@ thiserror = { version = "1.0.19", default-features = false } once_cell = "1.4.0" tokio = { version = "0.2.21", default-features = false } +[dev-dependencies] +tokio = { version = "0.2.21", default-features = false, features = ["macros"] } + [features] default = ["abigen"] abigen = ["ethers-contract-abigen", "ethers-contract-derive"] diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 86c36267..ec8eac15 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -2,22 +2,43 @@ use ethers_core::{ abi::{Detokenize, Error as AbiError, Function, InvalidOutputType}, types::{Address, BlockNumber, TransactionRequest, H256, U256}, }; -use ethers_providers::{networks::Network, JsonRpcClient}; -use ethers_signers::{Client, Signer}; +use ethers_providers::{JsonRpcClient, ProviderError}; +use ethers_signers::{Client, ClientError, Signer}; use std::{fmt::Debug, marker::PhantomData}; use thiserror::Error as ThisError; -pub struct ContractCall<'a, P, N, S, D> { +#[derive(ThisError, Debug)] +pub enum ContractError { + #[error(transparent)] + DecodingError(#[from] AbiError), + + #[error(transparent)] + DetokenizationError(#[from] InvalidOutputType), + + #[error(transparent)] + ClientError(#[from] ClientError), + + #[error(transparent)] + ProviderError(#[from] ProviderError), + + #[error("constructor is not defined in the ABI")] + ConstructorError, + + #[error("Contract was not deployed")] + ContractNotDeployed, +} + +pub struct ContractCall<'a, P, S, D> { pub(crate) tx: TransactionRequest, pub(crate) function: Function, - pub(crate) client: &'a Client<'a, P, N, S>, + pub(crate) client: &'a Client, pub(crate) block: Option, pub(crate) datatype: PhantomData, } -impl<'a, P, N, S, D: Detokenize> ContractCall<'a, S, P, N, D> { +impl<'a, P, S, D: Detokenize> ContractCall<'a, P, S, 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()); @@ -41,32 +62,18 @@ impl<'a, P, N, S, D: Detokenize> ContractCall<'a, S, P, N, D> { self.tx.value = Some(value.into()); self } + + /// Sets the `block` field for sending the tx to the chain + pub fn block>(mut self, block: T) -> Self { + self.block = Some(block.into()); + self + } } -#[derive(ThisError, Debug)] -// TODO: Can we get rid of this static? -pub enum ContractError -where - P::Error: 'static, -{ - #[error(transparent)] - DecodingError(#[from] AbiError), - #[error(transparent)] - DetokenizationError(#[from] InvalidOutputType), - #[error(transparent)] - CallError(P::Error), - #[error("constructor is not defined in the ABI")] - ConstructorError, - #[error("Contract was not deployed")] - ContractNotDeployed, -} - -impl<'a, P, N, S, D> ContractCall<'a, P, N, S, D> +impl<'a, P, S, D> ContractCall<'a, P, S, D> where S: Signer, P: JsonRpcClient, - P::Error: 'static, - N: Network, D: Detokenize, { /// Queries the blockchain via an `eth_call` for the provided transaction. @@ -78,12 +85,8 @@ where /// 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)?; + pub async fn call(self) -> Result { + let bytes = self.client.call(self.tx, self.block).await?; let tokens = self.function.decode_output(&bytes.0)?; @@ -93,7 +96,7 @@ where } /// Signs and broadcasts the provided transaction - pub async fn send(self) -> Result { - self.client.send_transaction(self.tx, self.block).await + pub async fn send(self) -> Result { + Ok(self.client.send_transaction(self.tx, self.block).await?) } } diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index 44467b94..42b8ac44 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -4,7 +4,7 @@ use ethers_core::{ abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize}, types::{Address, Filter, NameOrAddress, Selector, TransactionRequest}, }; -use ethers_providers::{networks::Network, JsonRpcClient}; +use ethers_providers::JsonRpcClient; use ethers_signers::{Client, Signer}; use rustc_hex::ToHex; @@ -15,8 +15,8 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; // TODO: Should we separate the lifetimes for the two references? // https://stackoverflow.com/a/29862184 #[derive(Debug, Clone)] -pub struct Contract<'a, P, N, S> { - client: &'a Client<'a, P, N, S>, +pub struct Contract<'a, P, S> { + client: &'a Client, abi: &'a Abi, address: Address, @@ -27,9 +27,13 @@ pub struct Contract<'a, P, N, S> { methods: HashMap, } -impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> { +impl<'a, P, S> Contract<'a, P, S> +where + S: Signer, + P: JsonRpcClient, +{ /// Creates a new contract from the provided client, abi and address - pub fn new(client: &'a Client<'a, P, N, S>, abi: &'a Abi, address: Address) -> Self { + pub fn new(address: Address, abi: &'a Abi, client: &'a Client) -> Self { let methods = create_mapping(&abi.functions, |function| function.selector()); Self { @@ -43,7 +47,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> { /// 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> + pub fn event<'b, D: Detokenize>(&'a self, name: &str) -> Result, Error> where 'a: 'b, { @@ -64,7 +68,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> { &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 +80,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> { &self, signature: Selector, args: T, - ) -> Result, Error> { + ) -> Result, Error> { let function = self .methods .get(&signature) @@ -89,7 +93,7 @@ impl<'a, P: JsonRpcClient, N: Network, S: Signer> Contract<'a, P, N, S> { &self, function: &Function, args: T, - ) -> Result, Error> { + ) -> Result, Error> { // create the calldata let data = function.encode_input(&args.into_tokens())?; diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index f59caaf9..cbbed4fa 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -1,6 +1,6 @@ use crate::ContractError; -use ethers_providers::{networks::Network, JsonRpcClient, Provider}; +use ethers_providers::{JsonRpcClient, Provider}; use ethers_core::{ abi::{Detokenize, Event as AbiEvent, RawLog}, @@ -9,15 +9,15 @@ use ethers_core::{ use std::{collections::HashMap, marker::PhantomData}; -pub struct Event<'a, 'b, P, N, D> { +pub struct Event<'a, 'b, P, D> { pub filter: Filter, - pub(crate) provider: &'a Provider, + pub(crate) provider: &'a Provider

, pub(crate) event: &'b AbiEvent, pub(crate) datatype: PhantomData, } // TODO: Improve these functions -impl<'a, 'b, P, N, D: Detokenize> Event<'a, 'b, P, N, D> { +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()); @@ -41,26 +41,22 @@ impl<'a, 'b, P, N, D: Detokenize> Event<'a, 'b, P, N, D> { } } -// TODO: Can we get rid of the static? -impl<'a, 'b, P: JsonRpcClient, N: Network, D: Detokenize + Clone> Event<'a, 'b, P, N, D> +impl<'a, 'b, P, D> Event<'a, 'b, P, D> where - P::Error: 'static, + P: JsonRpcClient, + D: Detokenize + Clone, { /// Queries the blockchain for the selected filter and returns a vector of matching /// event logs - pub async fn query(self) -> Result, ContractError

> { + pub async fn query(self) -> Result, ContractError> { Ok(self.query_with_hashes().await?.values().cloned().collect()) } /// Queries the blockchain for the selected filter and returns a vector of matching /// event logs - pub async fn query_with_hashes(self) -> Result, ContractError

> { + pub async fn query_with_hashes(self) -> Result, ContractError> { // get the logs - let logs = self - .provider - .get_logs(&self.filter) - .await - .map_err(ContractError::CallError)?; + let logs = self.provider.get_logs(&self.filter).await?; let events = logs .into_iter() @@ -79,7 +75,7 @@ where .collect::>(); // convert the tokens to the requested datatype - Ok::<_, ContractError

>(( + Ok::<_, ContractError>(( log.transaction_hash.expect("should have tx hash"), D::from_tokens(tokens)?, )) diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index 1462cda5..ed2cfa5b 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -4,7 +4,7 @@ use ethers_core::{ abi::{Abi, Tokenize}, types::{Bytes, TransactionRequest}, }; -use ethers_providers::{networks::Network, JsonRpcClient}; +use ethers_providers::JsonRpcClient; use ethers_signers::{Client, Signer}; use std::time::Duration; @@ -15,20 +15,18 @@ use tokio::time; const POLL_INTERVAL: u64 = 7000; #[derive(Debug, Clone)] -pub struct Deployer<'a, P, N, S> { - client: &'a Client<'a, P, N, S>, +pub struct Deployer<'a, P, S> { + client: &'a Client, abi: &'a Abi, tx: TransactionRequest, confs: usize, poll_interval: Duration, } -impl<'a, P, N, S> Deployer<'a, P, N, S> +impl<'a, P, S> Deployer<'a, P, S> where S: Signer, P: JsonRpcClient, - P::Error: 'static, - N: Network, { pub fn poll_interval>(mut self, interval: T) -> Self { self.poll_interval = interval.into(); @@ -40,12 +38,8 @@ where self } - pub async fn send(self) -> Result, ContractError

> { - let tx_hash = self - .client - .send_transaction(self.tx, None) - .await - .map_err(ContractError::CallError)?; + pub async fn send(self) -> Result, ContractError> { + let tx_hash = self.client.send_transaction(self.tx, None).await?; // poll for the receipt let address; @@ -60,27 +54,25 @@ where time::delay_for(Duration::from_millis(POLL_INTERVAL)).await; } - let contract = Contract::new(self.client, self.abi, address); + let contract = Contract::new(address, self.abi, self.client); Ok(contract) } } #[derive(Debug, Clone)] -pub struct ContractFactory<'a, P, N, S> { - client: &'a Client<'a, P, N, S>, +pub struct ContractFactory<'a, P, S> { + client: &'a Client, abi: &'a Abi, bytecode: &'a Bytes, } -impl<'a, P, N, S> ContractFactory<'a, P, N, S> +impl<'a, P, S> ContractFactory<'a, P, S> where S: Signer, P: JsonRpcClient, - P::Error: 'static, - N: Network, { /// Instantiate a new contract factory - pub fn new(client: &'a Client<'a, P, N, S>, abi: &'a Abi, bytecode: &'a Bytes) -> Self { + pub fn new(client: &'a Client, abi: &'a Abi, bytecode: &'a Bytes) -> Self { Self { client, abi, @@ -93,7 +85,7 @@ where pub fn deploy( &self, constructor_args: T, - ) -> Result, ContractError

> { + ) -> Result, ContractError> { // Encode the constructor args & concatenate with the bytecode if necessary let params = constructor_args.into_tokens(); let data: Bytes = match (self.abi.constructor(), params.is_empty()) { diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs new file mode 100644 index 00000000..8eefba4e --- /dev/null +++ b/ethers-contract/tests/contract.rs @@ -0,0 +1,78 @@ +use ethers_contract::{Contract, ContractFactory}; +use ethers_core::{ + types::H256, + utils::{GanacheBuilder, Solc}, +}; +use ethers_providers::{Http, Provider}; +use ethers_signers::Wallet; +use std::convert::TryFrom; + +#[tokio::test] +async fn deploy_and_call_contract() { + // 1. compile the contract + let compiled = Solc::new("./tests/contract.sol").build().unwrap(); + let contract = compiled + .get("SimpleStorage") + .expect("could not find contract"); + + // 2. launch ganache + let port = 8546u64; + let url = format!("http://localhost:{}", port).to_string(); + let _ganache = GanacheBuilder::new().port(port) + .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") + .spawn(); + + // 3. instantiate our wallet + let wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" + .parse::() + .unwrap(); + + // 4. connect to the network + let provider = Provider::::try_from(url.as_str()).unwrap(); + + // 5. instantiate the client with the wallet + let client = wallet.connect(provider); + + // 6. create a factory which will be used to deploy instances of the contract + let factory = ContractFactory::new(&client, &contract.abi, &contract.bytecode); + + // 7. deploy it with the constructor arguments + let contract = factory + .deploy("initial value".to_string()) + .unwrap() + .send() + .await + .unwrap(); + + // 8. get the contract's address + let addr = contract.address(); + + // 9. instantiate the contract + let contract = Contract::new(*addr, contract.abi(), &client); + + // 10. the initial value must be the one set in the constructor + let value: String = contract + .method("getValue", ()) + .unwrap() + .call() + .await + .unwrap(); + assert_eq!(value, "initial value"); + + // 11. call the `setValue` method (ugly API here) + let _tx_hash = contract + .method::<_, H256>("setValue", "hi".to_owned()) + .unwrap() + .send() + .await + .unwrap(); + + // 12. get the new value + let value: String = contract + .method("getValue", ()) + .unwrap() + .call() + .await + .unwrap(); + assert_eq!(value, "hi"); +} diff --git a/ethers-contract/tests/contract.sol b/ethers-contract/tests/contract.sol new file mode 100644 index 00000000..9d04f2f4 --- /dev/null +++ b/ethers-contract/tests/contract.sol @@ -0,0 +1,22 @@ +pragma solidity >=0.4.24; + +contract SimpleStorage { + + event ValueChanged(address indexed author, string oldValue, string newValue); + + string _value; + + constructor(string memory value) public { + emit ValueChanged(msg.sender, _value, value); + _value = value; + } + + function getValue() view public returns (string memory) { + return _value; + } + + function setValue(string memory value) public { + emit ValueChanged(msg.sender, _value, value); + _value = value; + } +} diff --git a/ethers/src/lib.rs b/ethers/src/lib.rs index 5721e134..25eec3a3 100644 --- a/ethers/src/lib.rs +++ b/ethers/src/lib.rs @@ -187,10 +187,6 @@ pub mod core { #[cfg(feature = "core")] pub use ethers_core::utils; -// Re-export ethers_providers::networks -#[cfg(feature = "providers")] -pub use ethers_providers::networks; - /// Easy import of frequently used type definitions and traits pub mod prelude { #[cfg(feature = "contract")]