From 1cfbc7b3c347eb2d2f256f5823101e7f41f39591 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Mon, 22 Jun 2020 11:44:08 +0300 Subject: [PATCH] Replace contract client references with Arc (#35) * feat(provider): allow specifying a default polling interval param This parameter is going to be used for all subsequent client calls by default. It can still be overriden with the internal `interval` calls * feat(contract): replace reference to Client with Arc * feat(abigen): adjusts codegen to use Arcs * fix(ethers): adjust examples to new apis * fix(provider): return TxHash instead of PendingTransaction on tx submission Returning a PendingTransaction allowed us to have nice ethers.js-like syntax where you submit a transaction and then can immediately await it. Unfortunately, now that we use Arcs and not lifetimes this meant that we would need to bind the function call in a variable, and then await on it, which is pretty bad UX. To fix this, we revert back to returning a TxHash and introduce a convenience method on the provider and the contract which takes a tx_hash and returns a PendingTransaction object. The syntax ends up being slightly more verbose (although more explicit), but the issue is fixed. --- .../ethers-contract-abigen/src/contract.rs | 8 +-- .../src/contract/common.rs | 19 ++--- .../src/contract/methods.rs | 4 +- ethers-contract/src/call.rs | 16 ++--- ethers-contract/src/contract.rs | 43 ++++++----- ethers-contract/src/factory.rs | 47 +++++++----- ethers-contract/tests/common/mod.rs | 16 +++-- ethers-contract/tests/contract.rs | 44 +++++++----- ethers-providers/src/lib.rs | 2 +- ethers-providers/src/pending_transaction.rs | 8 +-- ethers-providers/src/provider.rs | 72 +++++++++++-------- ethers-providers/src/stream.rs | 16 +++-- ethers-providers/tests/provider.rs | 35 ++++----- ethers-signers/src/client.rs | 37 +++++++--- ethers-signers/src/lib.rs | 4 +- ethers-signers/tests/signer.rs | 33 ++++++--- ethers/examples/contract.rs | 16 +++-- ethers/examples/ens.rs | 4 +- ethers/examples/local_signer.rs | 4 +- ethers/examples/transfer_eth.rs | 4 +- ethers/examples/watch_blocks.rs | 5 +- 21 files changed, 257 insertions(+), 180 deletions(-) diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index 5ad5f828..4f89938c 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -44,7 +44,7 @@ impl Context { let abi_name = super::util::safe_ident(&format!("{}_ABI", name.to_string().to_uppercase())); // 0. Imports - let imports = common::imports(); + let imports = common::imports(&name.to_string()); // 1. Declare Contract struct let struct_decl = common::struct_declaration(&cx, &abi_name); @@ -67,12 +67,12 @@ impl Context { #struct_decl - impl<'a, P: JsonRpcClient, S: Signer> #name<'a, P, S> { + impl<'a, P: JsonRpcClient, S: Signer> #name { /// Creates a new contract instance with the specified `ethers` /// client at the given `Address`. The contract derefs to a `ethers::Contract` /// object - pub fn new>(address: T, client: &'a Client) -> Self { - let contract = Contract::new(address.into(), #abi_name.clone(), client); + pub fn new, C: Into>>>(address: T, client: C) -> Self { + let contract = Contract::new(address.into(), #abi_name.clone(), client.into()); Self(contract) } diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs index 4b2f1677..2e969254 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -1,15 +1,18 @@ -use super::Context; +use super::{util, Context}; use ethers_core::types::Address; use proc_macro2::{Literal, TokenStream}; use quote::quote; -pub(crate) fn imports() -> TokenStream { +pub(crate) fn imports(name: &str) -> TokenStream { + let doc = util::expand_doc(&format!("{} was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs", name)); + quote! { #![allow(dead_code)] #![allow(unused_imports)] - // TODO: Can we make this context aware so that it imports either ethers_contract - // or ethers::contract? + #doc + + use std::sync::Arc; use ethers::{ core::{ abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable}, @@ -33,17 +36,17 @@ pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> // Struct declaration #[derive(Clone)] - pub struct #name<'a, P, S>(Contract<'a, P, S>); + pub struct #name(Contract); // Deref to the inner contract in order to access more specific functions functions - impl<'a, P, S> std::ops::Deref for #name<'a, P, S> { - type Target = Contract<'a, P, S>; + impl std::ops::Deref for #name { + type Target = Contract; fn deref(&self) -> &Self::Target { &self.0 } } - impl<'a, P: JsonRpcClient, S: Signer> std::fmt::Debug for #name<'a, P, S> { + impl std::fmt::Debug for #name { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_tuple(stringify!(#name)) .field(&self.address()) diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index 4494b747..bf9c4df0 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -45,9 +45,9 @@ fn expand_function(function: &Function, alias: Option) -> Result } + quote! { ContractCall } } else { - quote! { ContractCall<'a, P, S, H256> } + quote! { ContractCall } }; let arg = expand_inputs_call_arg(&function.inputs); diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 17cde911..e8dc4234 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -1,11 +1,11 @@ use ethers_core::{ abi::{Detokenize, Error as AbiError, Function, InvalidOutputType}, - types::{Address, BlockNumber, TransactionRequest, U256}, + types::{Address, BlockNumber, TransactionRequest, TxHash, U256}, }; -use ethers_providers::{JsonRpcClient, PendingTransaction, ProviderError}; +use ethers_providers::{JsonRpcClient, ProviderError}; use ethers_signers::{Client, ClientError, Signer}; -use std::{fmt::Debug, marker::PhantomData}; +use std::{fmt::Debug, marker::PhantomData, sync::Arc}; use thiserror::Error as ThisError; @@ -42,18 +42,18 @@ pub enum ContractError { #[derive(Debug, Clone)] #[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<'a, P, S, D> { +pub struct ContractCall { /// The raw transaction object pub tx: TransactionRequest, /// 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, - pub(crate) client: &'a Client, + pub(crate) client: Arc>, pub(crate) datatype: PhantomData, } -impl<'a, P, S, D: Detokenize> ContractCall<'a, P, S, D> { +impl ContractCall { /// 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()); @@ -85,7 +85,7 @@ impl<'a, P, S, D: Detokenize> ContractCall<'a, P, S, D> { } } -impl<'a, P, S, D> ContractCall<'a, P, S, D> +impl ContractCall where S: Signer, P: JsonRpcClient, @@ -111,7 +111,7 @@ where } /// Signs and broadcasts the provided transaction - pub async fn send(self) -> Result, ContractError> { + 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 002ed8b3..cf7bb3cc 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -2,13 +2,13 @@ use super::{call::ContractCall, event::Event}; use ethers_core::{ abi::{Abi, Detokenize, Error, EventExt, Function, FunctionExt, Tokenize}, - types::{Address, Filter, NameOrAddress, Selector, TransactionRequest}, + types::{Address, Filter, NameOrAddress, Selector, TransactionRequest, TxHash}, }; -use ethers_providers::JsonRpcClient; +use ethers_providers::{JsonRpcClient, PendingTransaction}; use ethers_signers::{Client, Signer}; use rustc_hex::ToHex; -use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; +use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData, sync::Arc}; /// 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 @@ -79,7 +79,7 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; /// .parse::()?.connect(provider); /// /// // create the contract object at the address -/// let contract = Contract::new(address, abi, &client); +/// let contract = Contract::new(address, abi, 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) @@ -90,9 +90,10 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; /// /// // Non-constant methods are executed via the `send()` call on the method builder. /// let tx_hash = contract -/// .method::<_, H256>("setValue", "hi".to_owned())? -/// .send() -/// .await?; +/// .method::<_, H256>("setValue", "hi".to_owned())?.send().await?; +/// +/// // `await`ing on the pending transaction resolves to a transaction receipt +/// let receipt = contract.pending_transaction(tx_hash).confirmations(6).await?; /// /// # Ok(()) /// # } @@ -116,7 +117,7 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; /// # let abi: Abi = serde_json::from_str(r#"[]"#)?; /// # let provider = Provider::::try_from("http://localhost:8545").unwrap(); /// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::()?.connect(provider); -/// # let contract = Contract::new(address, abi, &client); +/// # let contract = Contract::new(address, abi, client); /// /// #[derive(Clone, Debug)] /// struct ValueChanged { @@ -162,8 +163,8 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, marker::PhantomData}; /// [`event`]: method@crate::Contract::event /// [`method`]: method@crate::Contract::method #[derive(Debug, Clone)] -pub struct Contract<'a, P, S> { - client: &'a Client, +pub struct Contract { + client: Arc>, abi: Abi, address: Address, @@ -174,17 +175,17 @@ pub struct Contract<'a, P, S> { methods: HashMap, } -impl<'a, P, S> Contract<'a, P, S> +impl Contract where S: Signer, P: JsonRpcClient, { /// Creates a new contract from the provided client, abi and address - pub fn new(address: Address, abi: Abi, client: &'a Client) -> Self { + pub fn new(address: Address, abi: Abi, client: impl Into>>) -> Self { let methods = create_mapping(&abi.functions, |function| function.selector()); Self { - client, + client: client.into(), abi, address, methods, @@ -212,7 +213,7 @@ where &self, name: &str, args: T, - ) -> Result, Error> { + ) -> Result, Error> { // get the function let function = self.abi.function(name)?; self.method_func(function, args) @@ -224,7 +225,7 @@ where &self, signature: Selector, args: T, - ) -> Result, Error> { + ) -> Result, Error> { let function = self .methods .get(&signature) @@ -237,7 +238,7 @@ where &self, function: &Function, args: T, - ) -> Result, Error> { + ) -> Result, Error> { // create the calldata let data = function.encode_input(&args.into_tokens())?; @@ -250,7 +251,7 @@ where Ok(ContractCall { tx, - client: self.client, + client: Arc::clone(&self.client), // cheap clone behind the Arc block: None, function: function.to_owned(), datatype: PhantomData, @@ -272,7 +273,7 @@ where /// Returns a new contract instance using the provided client /// /// Clones `self` internally - pub fn connect(&self, client: &'a Client) -> Self + pub fn connect(&self, client: Arc>) -> Self where P: Clone, { @@ -295,6 +296,12 @@ where pub fn client(&self) -> &Client { &self.client } + + /// Helper which creates a pending transaction object from a transaction hash + /// using the provider's polling interval + pub fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> { + self.client.provider().pending_transaction(tx_hash) + } } /// Utility function for creating a mapping between a unique signature and a diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index 1a0fee65..c8d8f4b2 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -7,18 +7,21 @@ use ethers_core::{ use ethers_providers::JsonRpcClient; use ethers_signers::{Client, Signer}; +use std::{sync::Arc, time::Duration}; + #[derive(Debug, Clone)] /// Helper which manages the deployment transaction of a smart contract -pub struct Deployer<'a, P, S> { +pub struct Deployer { /// The deployer's transaction, exposed for overriding the defaults pub tx: TransactionRequest, abi: Abi, - client: &'a Client, + client: Arc>, confs: usize, block: BlockNumber, + interval: Duration, } -impl<'a, P, S> Deployer<'a, P, S> +impl Deployer where S: Signer, P: JsonRpcClient, @@ -29,6 +32,12 @@ where self } + /// Sets the poll interval for the pending deployment transaction's inclusion + pub fn interval>(mut self, interval: T) -> Self { + self.interval = interval.into(); + self + } + pub fn block>(mut self, block: T) -> Self { self.block = block.into(); self @@ -37,13 +46,17 @@ where /// Broadcasts the contract deployment transaction and after waiting for it to /// be sufficiently confirmed (default: 1), it returns a [`Contract`](crate::Contract) /// struct at the deployed contract's address. - pub async fn send(self) -> Result, ContractError> { - let pending_tx = self + pub async fn send(self) -> Result, ContractError> { + let tx_hash = self .client .send_transaction(self.tx, Some(self.block)) .await?; - - let receipt = pending_tx.confirmations(self.confs).await?; + let receipt = self + .client + .pending_transaction(tx_hash) + .interval(self.interval) + .confirmations(self.confs) + .await?; let address = receipt .contract_address @@ -97,7 +110,7 @@ where /// .parse::()?.connect(provider); /// /// // create a factory which will be used to deploy instances of the contract -/// let factory = ContractFactory::new(contract.abi.clone(), contract.bytecode.clone(), &client); +/// let factory = ContractFactory::new(contract.abi.clone(), contract.bytecode.clone(), client); /// /// // The deployer created by the `deploy` call exposes a builder which gets consumed /// // by the async `send` call @@ -109,13 +122,13 @@ where /// println!("{}", contract.address()); /// # Ok(()) /// # } -pub struct ContractFactory<'a, P, S> { - client: &'a Client, +pub struct ContractFactory { + client: Arc>, abi: Abi, bytecode: Bytes, } -impl<'a, P, S> ContractFactory<'a, P, S> +impl ContractFactory where S: Signer, P: JsonRpcClient, @@ -123,9 +136,9 @@ where /// Creates a factory for deployment of the Contract with bytecode, and the /// constructor defined in the abi. The client will be used to send any deployment /// transaction. - pub fn new(abi: Abi, bytecode: Bytes, client: &'a Client) -> Self { + pub fn new(abi: Abi, bytecode: Bytes, client: impl Into>>) -> Self { Self { - client, + client: client.into(), abi, bytecode, } @@ -139,10 +152,7 @@ where /// 1. If there are no constructor arguments, you should pass `()` as the argument. /// 1. The default poll duration is 7 seconds. /// 1. The default number of confirmations is 1 block. - pub fn deploy( - self, - constructor_args: T, - ) -> Result, ContractError> { + pub fn deploy(self, constructor_args: T) -> 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()) { @@ -164,11 +174,12 @@ where }; Ok(Deployer { - client: self.client, + client: Arc::clone(&self.client), // cheap clone behind the arc abi: self.abi, tx, confs: 1, block: BlockNumber::Latest, + interval: self.client.get_interval(), }) } } diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index 3e9c7890..0cb16c66 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -7,7 +7,7 @@ use ethers_contract::{Contract, ContractFactory}; use ethers_core::utils::{Ganache, GanacheInstance, Solc}; use ethers_providers::{Http, Provider}; use ethers_signers::{Client, Wallet}; -use std::convert::TryFrom; +use std::{convert::TryFrom, sync::Arc, time::Duration}; // Note: We also provide the `abigen` macro for generating these bindings automatically #[derive(Clone, Debug)] @@ -44,17 +44,19 @@ pub fn compile() -> (Abi, Bytes) { } /// connects the private key to http://localhost:8545 -pub fn connect(private_key: &str) -> Client { - let provider = Provider::::try_from("http://localhost:8545").unwrap(); - private_key.parse::().unwrap().connect(provider) +pub fn connect(private_key: &str) -> Arc> { + let provider = Provider::::try_from("http://localhost:8545") + .unwrap() + .interval(Duration::from_millis(10u64)); + Arc::new(private_key.parse::().unwrap().connect(provider)) } /// Launches a ganache instance and deploys the SimpleStorage contract -pub async fn deploy<'a>( - client: &'a Client, +pub async fn deploy( + client: Arc>, abi: Abi, bytecode: Bytes, -) -> (GanacheInstance, Contract<'a, Http, Wallet>) { +) -> (GanacheInstance, Contract) { let ganache = Ganache::new() .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") .spawn(); diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index af1ed40b..bea466a6 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -13,7 +13,7 @@ mod eth_tests { utils::Ganache, }; use serial_test::serial; - use std::convert::TryFrom; + use std::{convert::TryFrom, sync::Arc, time::Duration}; #[tokio::test] #[serial] @@ -31,7 +31,7 @@ mod eth_tests { let client2 = connect("cc96601bc52293b53c4736a12af9130abf347669b3813f9ec4cafdf6991b087e"); // create a factory which will be used to deploy instances of the contract - let factory = ContractFactory::new(abi, bytecode, &client); + let factory = ContractFactory::new(abi, bytecode, client.clone()); // `send` consumes the deployer so it must be cloned for later re-use // (practically it's not expected that you'll need to deploy multiple instances of @@ -46,9 +46,11 @@ mod eth_tests { let value = get_value.clone().call().await.unwrap(); assert_eq!(value, "initial value"); - // make a call with `client2` + // need to declare the method first, and only then send it + // this is because it internally clones an Arc which would otherwise + // get immediately dropped let _tx_hash = contract - .connect(&client2) + .connect(client2.clone()) .method::<_, H256>("setValue", "hi".to_owned()) .unwrap() .send() @@ -59,7 +61,7 @@ mod eth_tests { // we can also call contract methods at other addresses with the `at` call // (useful when interacting with multiple ERC20s for example) - let contract2_addr = deployer.clone().send().await.unwrap().address(); + let contract2_addr = deployer.send().await.unwrap().address(); let contract2 = contract.at(contract2_addr); let init_value: String = contract2 .method::<_, String>("getValue", ()) @@ -82,7 +84,7 @@ mod eth_tests { async fn get_past_events() { let (abi, bytecode) = compile(); let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let (_ganache, contract) = deploy(&client, abi, bytecode).await; + let (_ganache, contract) = deploy(client.clone(), abi, bytecode).await; // make a call with `client2` let _tx_hash = contract @@ -111,7 +113,7 @@ mod eth_tests { async fn watch_events() { let (abi, bytecode) = compile(); let client = connect("380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc"); - let (_ganache, contract) = deploy(&client, abi, bytecode).await; + let (_ganache, contract) = deploy(client, abi, bytecode).await; // We spawn the event listener: let mut stream = contract @@ -125,12 +127,13 @@ mod eth_tests { // and we make a few calls for i in 0..num_calls { - let _tx_hash = contract + let tx_hash = contract .method::<_, H256>("setValue", i.to_string()) .unwrap() .send() .await .unwrap(); + let _receipt = contract.pending_transaction(tx_hash).await.unwrap(); } for i in 0..num_calls { @@ -144,22 +147,23 @@ mod eth_tests { #[serial] async fn signer_on_node() { let (abi, bytecode) = compile(); - let provider = Provider::::try_from("http://localhost:8545").unwrap(); + let provider = Provider::::try_from("http://localhost:8545") + .unwrap() + .interval(Duration::from_millis(10u64)); let deployer = "3cDB3d9e1B74692Bb1E3bb5fc81938151cA64b02" .parse::
() .unwrap(); - let client = Client::from(provider).with_sender(deployer); - let (_ganache, contract) = deploy(&client, abi, bytecode).await; + let client = Arc::new(Client::from(provider).with_sender(deployer)); + let (_ganache, contract) = deploy(client, abi, bytecode).await; // make a call without the signer - let _tx = contract + let tx_hash = contract .method::<_, H256>("setValue", "hi".to_owned()) .unwrap() .send() .await - .unwrap() - .await .unwrap(); + let _receipt = contract.pending_transaction(tx_hash).await.unwrap(); let value: String = contract .method::<_, String>("getValue", ()) .unwrap() @@ -178,7 +182,7 @@ mod celo_tests { signers::Wallet, types::BlockNumber, }; - use std::convert::TryFrom; + use std::{convert::TryFrom, sync::Arc, time::Duration}; #[tokio::test] async fn deploy_and_call_contract() { @@ -192,9 +196,11 @@ mod celo_tests { let client = "d652abb81e8c686edba621a895531b1f291289b63b5ef09a94f686a5ecdd5db1" .parse::() .unwrap() - .connect(provider); + .connect(provider) + .interval(Duration::from_millis(6000)); + let client = Arc::new(client); - let factory = ContractFactory::new(abi, bytecode, &client); + let factory = ContractFactory::new(abi, bytecode, client); let deployer = factory.deploy("initial value".to_string()).unwrap(); let contract = deployer.block(BlockNumber::Pending).send().await.unwrap(); @@ -207,13 +213,13 @@ mod celo_tests { assert_eq!(value, "initial value"); // make a state mutating transaction - let pending_tx = contract + let tx_hash = contract .method::<_, H256>("setValue", "hi".to_owned()) .unwrap() .send() .await .unwrap(); - let _receipt = pending_tx.await.unwrap(); + let _receipt = contract.pending_transaction(tx_hash).await.unwrap(); let value: String = contract .method("getValue", ()) diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index e95036c3..e625b493 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -111,7 +111,7 @@ mod pending_transaction; pub use pending_transaction::PendingTransaction; mod stream; -pub use stream::FilterStream; +pub use stream::{FilterStream, DEFAULT_POLL_INTERVAL}; // re-export `StreamExt` so that consumers can call `next()` on the `FilterStream` // without having to import futures themselves pub use futures_util::StreamExt; diff --git a/ethers-providers/src/pending_transaction.rs b/ethers-providers/src/pending_transaction.rs index 2867bcfa..a8b72697 100644 --- a/ethers-providers/src/pending_transaction.rs +++ b/ethers-providers/src/pending_transaction.rs @@ -1,5 +1,5 @@ use crate::{ - stream::{interval, DEFAULT_POLL_DURATION}, + stream::{interval, DEFAULT_POLL_INTERVAL}, JsonRpcClient, Provider, ProviderError, }; use ethers_core::types::{TransactionReceipt, TxHash, U64}; @@ -38,7 +38,7 @@ impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> { confirmations: 1, provider, state: PendingTxState::GettingReceipt(fut), - interval: Box::new(interval(DEFAULT_POLL_DURATION)), + interval: Box::new(interval(DEFAULT_POLL_INTERVAL)), } } @@ -50,8 +50,8 @@ impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> { } /// Sets the polling interval - pub fn interval>(mut self, duration: T) -> Self { - self.interval = Box::new(interval(Duration::from_millis(duration.into()))); + pub fn interval>(mut self, duration: T) -> Self { + self.interval = Box::new(interval(duration.into())); self } } diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index bafed72c..cbe020db 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -1,6 +1,6 @@ use crate::{ ens, - stream::{FilterStream, FilterWatcher}, + stream::{FilterStream, FilterWatcher, DEFAULT_POLL_INTERVAL}, Http as HttpProvider, JsonRpcClient, PendingTransaction, }; @@ -17,7 +17,7 @@ use serde::Deserialize; use thiserror::Error; use url::{ParseError, Url}; -use std::{convert::TryFrom, fmt::Debug}; +use std::{convert::TryFrom, fmt::Debug, time::Duration}; /// An abstract provider for interacting with the [Ethereum JSON RPC /// API](https://github.com/ethereum/wiki/wiki/JSON-RPC). Must be instantiated @@ -41,7 +41,7 @@ use std::{convert::TryFrom, fmt::Debug}; /// # } /// ``` #[derive(Clone, Debug)] -pub struct Provider

(P, Option

); +pub struct Provider

(P, Option

, Option); #[derive(Debug, Error)] /// An error thrown when making a call to the provider @@ -72,7 +72,7 @@ pub enum FilterKind<'a> { impl Provider

{ /// Instantiate a new provider with a backend. pub fn new(provider: P) -> Self { - Self(provider, None) + Self(provider, None, None) } ////// Blockchain Status @@ -265,7 +265,7 @@ impl Provider

{ pub async fn send_transaction( &self, mut tx: TransactionRequest, - ) -> Result, ProviderError> { + ) -> Result { if let Some(ref to) = tx.to { if let NameOrAddress::Name(ens_name) = to { // resolve to an address @@ -276,27 +276,22 @@ impl Provider

{ } } - let tx_hash = self + Ok(self .0 .request("eth_sendTransaction", [tx]) .await - .map_err(Into::into)?; - Ok(PendingTransaction::new(tx_hash, self)) + .map_err(Into::into)?) } /// Send the raw RLP encoded transaction to the entire Ethereum network and returns the transaction's hash /// This will consume gas from the account that signed the transaction. - pub async fn send_raw_transaction( - &self, - tx: &Transaction, - ) -> Result, ProviderError> { + pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result { let rlp = utils::serialize(&tx.rlp()); - let tx_hash = self + Ok(self .0 .request("eth_sendRawTransaction", [rlp]) .await - .map_err(Into::into)?; - Ok(PendingTransaction::new(tx_hash, self)) + .map_err(Into::into)?) } /// Signs data using a specific account. This account needs to be unlocked. @@ -332,14 +327,17 @@ impl Provider

{ ) -> Result + '_, ProviderError> { let id = self.new_filter(FilterKind::Logs(filter)).await?; let fut = move || Box::pin(self.get_filter_changes(id)); - Ok(FilterWatcher::new(id, fut)) + let filter = FilterWatcher::new(id, fut).interval(self.get_interval()); + + Ok(filter) } /// Streams new block hashes pub async fn watch_blocks(&self) -> Result + '_, ProviderError> { let id = self.new_filter(FilterKind::NewBlocks).await?; let fut = move || Box::pin(self.get_filter_changes(id)); - Ok(FilterWatcher::new(id, fut)) + let filter = FilterWatcher::new(id, fut).interval(self.get_interval()); + Ok(filter) } /// Streams pending transactions @@ -348,7 +346,8 @@ impl Provider

{ ) -> Result + '_, ProviderError> { let id = self.new_filter(FilterKind::PendingTransactions).await?; let fut = move || Box::pin(self.get_filter_changes(id)); - Ok(FilterWatcher::new(id, fut)) + let filter = FilterWatcher::new(id, fut).interval(self.get_interval()); + Ok(filter) } /// Creates a filter object, based on filter options, to notify when the state changes (logs). @@ -495,6 +494,25 @@ impl Provider

{ self.1 = Some(ens.into()); self } + + /// Sets the default polling interval for event filters and pending transactions + /// (default: 7 seconds) + pub fn interval>(mut self, interval: T) -> Self { + self.2 = Some(interval.into()); + self + } + + /// Gets the polling interval which the provider currently uses for event filters + /// and pending transactions (default: 7 seconds) + pub fn get_interval(&self) -> Duration { + self.2.unwrap_or(DEFAULT_POLL_INTERVAL) + } + + /// Helper which creates a pending transaction object from a transaction hash + /// using the provider's polling interval + pub fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> { + PendingTransaction::new(tx_hash, self).interval(self.get_interval()) + } } /// infallbile conversion of Bytes to Address/String @@ -512,7 +530,7 @@ impl TryFrom<&str> for Provider { type Error = ParseError; fn try_from(src: &str) -> Result { - Ok(Provider(HttpProvider::new(Url::parse(src)?), None)) + Ok(Provider(HttpProvider::new(Url::parse(src)?), None, None)) } } @@ -578,15 +596,12 @@ mod tests { async fn test_new_block_filter() { let num_blocks = 3; - let provider = Provider::::try_from("http://localhost:8545").unwrap(); + let provider = Provider::::try_from("http://localhost:8545") + .unwrap() + .interval(Duration::from_millis(1000)); let start_block = provider.get_block_number().await.unwrap(); - let stream = provider - .watch_blocks() - .await - .unwrap() - .interval(1000u64) - .stream(); + let stream = provider.watch_blocks().await.unwrap().stream(); let hashes: Vec = stream.take(num_blocks).collect::>().await; for (i, hash) in hashes.iter().enumerate() { @@ -606,14 +621,15 @@ mod tests { async fn test_new_pending_txs_filter() { let num_txs = 5; - let provider = Provider::::try_from("http://localhost:8545").unwrap(); + let provider = Provider::::try_from("http://localhost:8545") + .unwrap() + .interval(Duration::from_millis(1000)); let accounts = provider.get_accounts().await.unwrap(); let stream = provider .watch_pending_transactions() .await .unwrap() - .interval(1000u64) .stream(); let mut tx_hashes = Vec::new(); diff --git a/ethers-providers/src/stream.rs b/ethers-providers/src/stream.rs index 6f5e3afb..25ac1098 100644 --- a/ethers-providers/src/stream.rs +++ b/ethers-providers/src/stream.rs @@ -20,7 +20,8 @@ pub fn interval(duration: Duration) -> impl Stream + Send + Unpin { stream::unfold((), move |_| Delay::new(duration).map(|_| Some(((), ())))).map(drop) } -pub const DEFAULT_POLL_DURATION: Duration = Duration::from_millis(7000); +/// The default polling interval for filters and pending transactions +pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(7000); /// Trait for streaming filters. pub trait FilterStream: StreamExt + Stream @@ -31,7 +32,7 @@ where fn id(&self) -> U256; /// Sets the stream's polling interval - fn interval>(self, duration: T) -> Self; + fn interval(self, duration: Duration) -> Self; /// Alias for Box::pin, must be called in order to pin the stream and be able /// to call `next` on it. @@ -73,7 +74,7 @@ where pub fn new>(id: T, factory: F) -> Self { Self { id: id.into(), - interval: Box::new(interval(DEFAULT_POLL_DURATION)), + interval: Box::new(interval(DEFAULT_POLL_INTERVAL)), state: FilterWatcherState::WaitForInterval, factory, } @@ -90,8 +91,8 @@ where self.id } - fn interval>(mut self, duration: T) -> Self { - self.interval = Box::new(interval(Duration::from_millis(duration.into()))); + fn interval(mut self, duration: Duration) -> Self { + self.interval = Box::new(interval(duration)); self } } @@ -191,7 +192,10 @@ mod watch { let filter = FilterWatcher::<_, u64>::new(1, factory); // stream combinator calls are still doable since FilterStream extends // Stream and StreamExt - let mut stream = filter.interval(100u64).stream().map(|x| 2 * x); + let mut stream = filter + .interval(Duration::from_millis(100u64)) + .stream() + .map(|x| 2 * x); assert_eq!(stream.next().await.unwrap(), 2); assert_eq!(stream.next().await.unwrap(), 4); assert_eq!(stream.next().await.unwrap(), 6); diff --git a/ethers-providers/tests/provider.rs b/ethers-providers/tests/provider.rs index 19c873f2..58d5ee93 100644 --- a/ethers-providers/tests/provider.rs +++ b/ethers-providers/tests/provider.rs @@ -1,6 +1,6 @@ #![allow(unused_braces)] use ethers::providers::{Http, Provider}; -use std::convert::TryFrom; +use std::{convert::TryFrom, time::Duration}; #[cfg(not(feature = "celo"))] mod eth_tests { @@ -48,14 +48,9 @@ mod eth_tests { let (ws, _) = async_tungstenite::tokio::connect_async("ws://localhost:8545") .await .unwrap(); - let provider = Provider::new(Ws::new(ws)); + let provider = Provider::new(Ws::new(ws)).interval(Duration::from_millis(500u64)); - let stream = provider - .watch_blocks() - .await - .unwrap() - .interval(2000u64) - .stream(); + let stream = provider.watch_blocks().await.unwrap().stream(); let _blocks = stream.take(3usize).collect::>().await; let _number = provider.get_block_number().await.unwrap(); @@ -65,7 +60,9 @@ mod eth_tests { #[serial] async fn pending_txs_with_confirmations_ganache() { let _ganache = Ganache::new().block_time(2u64).spawn(); - let provider = Provider::::try_from("http://localhost:8545").unwrap(); + let provider = Provider::::try_from("http://localhost:8545") + .unwrap() + .interval(Duration::from_millis(500u64)); generic_pending_txs_test(provider).await; } @@ -84,12 +81,12 @@ mod eth_tests { let accounts = provider.get_accounts().await.unwrap(); let tx = TransactionRequest::pay(accounts[0], parse_ether(1u64).unwrap()).from(accounts[0]); - let pending_tx = provider.send_transaction(tx).await.unwrap(); - let hash = *pending_tx; - let receipt = pending_tx.interval(500u64).confirmations(5).await.unwrap(); + let tx_hash = provider.send_transaction(tx).await.unwrap(); + let pending_tx = provider.pending_transaction(tx_hash); + let receipt = pending_tx.confirmations(5).await.unwrap(); // got the correct receipt - assert_eq!(receipt.transaction_hash, hash); + assert_eq!(receipt.transaction_hash, tx_hash); } } @@ -117,15 +114,11 @@ mod celo_tests { #[tokio::test] async fn watch_blocks() { - let provider = - Provider::::try_from("https://alfajores-forno.celo-testnet.org").unwrap(); - - let stream = provider - .watch_blocks() - .await + let provider = Provider::::try_from("https://alfajores-forno.celo-testnet.org") .unwrap() - .interval(2000u64) - .stream(); + .interval(Duration::from_millis(2000u64)); + + let stream = provider.watch_blocks().await.unwrap().stream(); let _blocks = stream.take(3usize).collect::>().await; } diff --git a/ethers-signers/src/client.rs b/ethers-signers/src/client.rs index b6278347..a6858a69 100644 --- a/ethers-signers/src/client.rs +++ b/ethers-signers/src/client.rs @@ -1,12 +1,12 @@ use crate::Signer; use ethers_core::types::{ - Address, BlockNumber, Bytes, NameOrAddress, Signature, TransactionRequest, + Address, BlockNumber, Bytes, NameOrAddress, Signature, TransactionRequest, TxHash, }; -use ethers_providers::{JsonRpcClient, PendingTransaction, Provider, ProviderError}; +use ethers_providers::{JsonRpcClient, Provider, ProviderError}; use futures_util::{future::ok, join}; -use std::{future::Future, ops::Deref}; +use std::{future::Future, ops::Deref, time::Duration}; use thiserror::Error; #[derive(Clone, Debug)] @@ -42,14 +42,11 @@ use thiserror::Error; /// let signed_msg = client.provider().sign(b"hello".to_vec(), &client.address()).await?; /// /// let tx = TransactionRequest::pay("vitalik.eth", 100); -/// let pending_tx = client.send_transaction(tx, None).await?; +/// let tx_hash = client.send_transaction(tx, None).await?; /// -/// // You can get the transaction hash by dereferencing it -/// let tx_hash = *pending_tx; -/// -/// // Or you can `await` on the pending transaction to get the receipt with a pre-specified +/// // You can `await` on the pending transaction to get the receipt with a pre-specified /// // number of confirmations -/// let receipt = pending_tx.confirmations(6).await?; +/// let receipt = client.pending_transaction(tx_hash).confirmations(6).await?; /// /// // You can connect with other wallets at runtime via the `with_signer` function /// let wallet2: Wallet = "cd8c407233c0560f6de24bb2dc60a8b02335c959a1a17f749ce6c1ccf63d74a7" @@ -124,7 +121,7 @@ where &self, mut tx: TransactionRequest, block: Option, - ) -> Result, ClientError> { + ) -> Result { if let Some(ref to) = tx.to { if let NameOrAddress::Name(ens_name) = to { let addr = self.resolve_name(&ens_name).await?; @@ -215,10 +212,30 @@ where /// Sets the address which will be used for interacting with the blockchain. /// Useful if no signer is set and you want to specify a default sender for /// your transactions + /// + /// # Panics + /// + /// If the signer is Some. It is forbidden to switch the sender if a private + /// key is already specified. pub fn with_sender>(mut self, address: T) -> Self { + if self.signer.is_some() { + panic!( + "It is forbidden to switch the sender if a signer is specified. + Consider using the `with_signer` method if you want to specify a + different signer" + ) + } + self.address = address.into(); self } + + /// Sets the default polling interval for event filters and pending transactions + pub fn interval>(mut self, interval: T) -> Self { + let provider = self.provider.interval(interval.into()); + self.provider = provider; + self + } } /// Calls the future if `item` is None, otherwise returns a `futures::ok` diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 8a74a3a3..186e8533 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -24,10 +24,10 @@ //! .value(10000); //! //! // send it! (this will resolve the ENS name to an address under the hood) -//! let pending_tx = client.send_transaction(tx, None).await?; +//! let tx_hash = client.send_transaction(tx, None).await?; //! //! // get the receipt -//! let receipt = pending_tx.await?; +//! let receipt = client.pending_transaction(tx_hash).await?; //! //! // get the mined tx //! let tx = client.get_transaction(receipt.transaction_hash).await?; diff --git a/ethers-signers/tests/signer.rs b/ethers-signers/tests/signer.rs index 3c87804a..62a9735d 100644 --- a/ethers-signers/tests/signer.rs +++ b/ethers-signers/tests/signer.rs @@ -3,7 +3,7 @@ use ethers::{ signers::Wallet, types::TransactionRequest, }; -use std::convert::TryFrom; +use std::{convert::TryFrom, time::Duration}; #[cfg(not(feature = "celo"))] mod eth_tests { @@ -18,7 +18,8 @@ mod eth_tests { let provider = Provider::::try_from( "https://rinkeby.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27", ) - .unwrap(); + .unwrap() + .interval(Duration::from_millis(2000u64)); // pls do not drain this key :) // note: this works even if there's no EIP-155 configured! @@ -28,15 +29,18 @@ mod eth_tests { .connect(provider); let tx = TransactionRequest::pay(client.address(), parse_ether(1u64).unwrap()); - let pending_tx = client + let tx_hash = client .send_transaction(tx, Some(BlockNumber::Pending)) .await .unwrap(); - let hash = *pending_tx; - let receipt = pending_tx.confirmations(3).await.unwrap(); + let receipt = client + .pending_transaction(tx_hash) + .confirmations(3) + .await + .unwrap(); // got the correct receipt - assert_eq!(receipt.transaction_hash, hash); + assert_eq!(receipt.transaction_hash, tx_hash); } #[tokio::test] @@ -54,7 +58,9 @@ mod eth_tests { .unwrap(); // connect to the network - let provider = Provider::::try_from(url.as_str()).unwrap(); + let provider = Provider::::try_from(url.as_str()) + .unwrap() + .interval(Duration::from_millis(10u64)); // connect the wallet to the provider let client = wallet.connect(provider); @@ -83,8 +89,9 @@ mod celo_tests { #[tokio::test] async fn test_send_transaction() { // Celo testnet - let provider = - Provider::::try_from("https://alfajores-forno.celo-testnet.org").unwrap(); + let provider = Provider::::try_from("https://alfajores-forno.celo-testnet.org") + .unwrap() + .interval(Duration::from_millis(3000u64)); // Funded with https://celo.org/developers/faucet // Please do not drain this account :) @@ -95,8 +102,12 @@ mod celo_tests { let balance_before = client.get_balance(client.address(), None).await.unwrap(); let tx = TransactionRequest::pay(client.address(), 100); - let pending_tx = client.send_transaction(tx, None).await.unwrap(); - let _receipt = pending_tx.confirmations(3).await.unwrap(); + let tx_hash = client.send_transaction(tx, None).await.unwrap(); + let _receipt = client + .pending_transaction(tx_hash) + .confirmations(3) + .await + .unwrap(); let balance_after = client.get_balance(client.address(), None).await.unwrap(); assert!(balance_before > balance_after); } diff --git a/ethers/examples/contract.rs b/ethers/examples/contract.rs index 8dde1a29..f08e875f 100644 --- a/ethers/examples/contract.rs +++ b/ethers/examples/contract.rs @@ -3,7 +3,7 @@ use ethers::{ prelude::*, utils::{Ganache, Solc}, }; -use std::convert::TryFrom; +use std::{convert::TryFrom, sync::Arc, time::Duration}; // Generate the type-safe contract bindings by providing the ABI abigen!( @@ -32,13 +32,18 @@ async fn main() -> Result<()> { "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::()?; // 4. connect to the network - let provider = Provider::::try_from(url.as_str())?; + let provider = Provider::::try_from(url.as_str())?.interval(Duration::from_millis(10u64)); // 5. instantiate the client with the wallet let client = wallet.connect(provider); + let client = Arc::new(client); // 6. create a factory which will be used to deploy instances of the contract - let factory = ContractFactory::new(contract.abi.clone(), contract.bytecode.clone(), &client); + let factory = ContractFactory::new( + contract.abi.clone(), + contract.bytecode.clone(), + client.clone(), + ); // 7. deploy it with the constructor arguments let contract = factory.deploy("initial value".to_string())?.send().await?; @@ -47,10 +52,11 @@ async fn main() -> Result<()> { let addr = contract.address(); // 9. instantiate the contract - let contract = SimpleContract::new(addr, &client); + let contract = SimpleContract::new(addr, client.clone()); // 10. call the `setValue` method - let _tx_hash = contract.set_value("hi".to_owned()).send().await?; + let tx_hash = contract.set_value("hi".to_owned()).send().await?; + let _receipt = client.pending_transaction(tx_hash).await?; // 11. get all events let logs = contract diff --git a/ethers/examples/ens.rs b/ethers/examples/ens.rs index f721c520..9ee7e192 100644 --- a/ethers/examples/ens.rs +++ b/ethers/examples/ens.rs @@ -18,9 +18,9 @@ async fn main() -> Result<()> { let tx = TransactionRequest::new().to("vitalik.eth").value(100_000); // send it! - let pending_tx = client.send_transaction(tx, None).await?; + let tx_hash = client.send_transaction(tx, None).await?; - let receipt = pending_tx.await?; + let receipt = client.pending_transaction(tx_hash).await?; let tx = client.get_transaction(receipt.transaction_hash).await?; println!("{}", serde_json::to_string(&tx)?); diff --git a/ethers/examples/local_signer.rs b/ethers/examples/local_signer.rs index 43ce7b7b..ba583a05 100644 --- a/ethers/examples/local_signer.rs +++ b/ethers/examples/local_signer.rs @@ -27,10 +27,10 @@ async fn main() -> Result<()> { .value(10000); // send it! - let pending_tx = client.send_transaction(tx, None).await?; + let tx_hash = client.send_transaction(tx, None).await?; // get the mined tx - let receipt = pending_tx.await?; + let receipt = client.pending_transaction(tx_hash).await?; let tx = client.get_transaction(receipt.transaction_hash).await?; println!("Sent tx: {}\n", serde_json::to_string(&tx)?); diff --git a/ethers/examples/transfer_eth.rs b/ethers/examples/transfer_eth.rs index e1316dc3..b02dc414 100644 --- a/ethers/examples/transfer_eth.rs +++ b/ethers/examples/transfer_eth.rs @@ -22,9 +22,9 @@ async fn main() -> Result<()> { let balance_before = provider.get_balance(from, None).await?; // broadcast it via the eth_sendTransaction API - let pending_tx = provider.send_transaction(tx).await?; + let tx_hash = provider.send_transaction(tx).await?; - let tx = pending_tx.await?; + let tx = provider.pending_transaction(tx_hash).await?; println!("{}", serde_json::to_string(&tx)?); diff --git a/ethers/examples/watch_blocks.rs b/ethers/examples/watch_blocks.rs index bc65b138..97c2e008 100644 --- a/ethers/examples/watch_blocks.rs +++ b/ethers/examples/watch_blocks.rs @@ -1,10 +1,11 @@ use ethers::prelude::*; +use std::time::Duration; #[tokio::main] async fn main() -> anyhow::Result<()> { let ws = Ws::connect("ws://localhost:8546").await?; - let provider = Provider::new(ws); - let mut stream = provider.watch_blocks().await?.interval(2000u64).stream(); + let provider = Provider::new(ws).interval(Duration::from_millis(2000)); + let mut stream = provider.watch_blocks().await?.stream(); while let Some(block) = stream.next().await { dbg!(block); }