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.
This commit is contained in:
Georgios Konstantopoulos 2020-06-22 11:44:08 +03:00 committed by GitHub
parent 7a17e54fbd
commit 1cfbc7b3c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 257 additions and 180 deletions

View File

@ -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<P, S> {
/// 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<T: Into<Address>>(address: T, client: &'a Client<P, S>) -> Self {
let contract = Contract::new(address.into(), #abi_name.clone(), client);
pub fn new<T: Into<Address>, C: Into<Arc<Client<P, S>>>>(address: T, client: C) -> Self {
let contract = Contract::new(address.into(), #abi_name.clone(), client.into());
Self(contract)
}

View File

@ -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<P, S>(Contract<P, S>);
// 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<P, S> std::ops::Deref for #name<P, S> {
type Target = Contract<P, S>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<'a, P: JsonRpcClient, S: Signer> std::fmt::Debug for #name<'a, P, S> {
impl<P: JsonRpcClient, S: Signer> std::fmt::Debug for #name<P, S> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple(stringify!(#name))
.field(&self.address())

View File

@ -45,9 +45,9 @@ fn expand_function(function: &Function, alias: Option<Ident>) -> Result<TokenStr
StateMutability::Nonpayable | StateMutability::Payable
);
let result = if !is_mutable {
quote! { ContractCall<'a, P, S, #outputs> }
quote! { ContractCall<P, S, #outputs> }
} else {
quote! { ContractCall<'a, P, S, H256> }
quote! { ContractCall<P, S, H256> }
};
let arg = expand_inputs_call_arg(&function.inputs);

View File

@ -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<P, S, D> {
/// 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<BlockNumber>,
pub(crate) client: &'a Client<P, S>,
pub(crate) client: Arc<Client<P, S>>,
pub(crate) datatype: PhantomData<D>,
}
impl<'a, P, S, D: Detokenize> ContractCall<'a, P, S, D> {
impl<P, S, D: Detokenize> ContractCall<P, S, 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());
@ -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<P, S, D> ContractCall<P, S, D>
where
S: Signer,
P: JsonRpcClient,
@ -111,7 +111,7 @@ where
}
/// Signs and broadcasts the provided transaction
pub async fn send(self) -> Result<PendingTransaction<'a, P>, ContractError> {
pub async fn send(self) -> Result<TxHash, ContractError> {
Ok(self.client.send_transaction(self.tx, self.block).await?)
}
}

View File

@ -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::<Wallet>()?.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::<Http>::try_from("http://localhost:8545").unwrap();
/// # let client = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc".parse::<Wallet>()?.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<P, S>,
pub struct Contract<P, S> {
client: Arc<Client<P, S>>,
abi: Abi,
address: Address,
@ -174,17 +175,17 @@ pub struct Contract<'a, P, S> {
methods: HashMap<Selector, (String, usize)>,
}
impl<'a, P, S> Contract<'a, P, S>
impl<P, S> Contract<P, S>
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<P, S>) -> Self {
pub fn new(address: Address, abi: Abi, client: impl Into<Arc<Client<P, S>>>) -> 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<ContractCall<'a, P, S, D>, Error> {
) -> Result<ContractCall<P, S, D>, 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<ContractCall<'a, P, S, D>, Error> {
) -> Result<ContractCall<P, S, D>, Error> {
let function = self
.methods
.get(&signature)
@ -237,7 +238,7 @@ where
&self,
function: &Function,
args: T,
) -> Result<ContractCall<'a, P, S, D>, Error> {
) -> Result<ContractCall<P, S, D>, 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<P, S>) -> Self
pub fn connect(&self, client: Arc<Client<P, S>>) -> Self
where
P: Clone,
{
@ -295,6 +296,12 @@ where
pub fn client(&self) -> &Client<P, S> {
&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

View File

@ -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<P, S> {
/// The deployer's transaction, exposed for overriding the defaults
pub tx: TransactionRequest,
abi: Abi,
client: &'a Client<P, S>,
client: Arc<Client<P, S>>,
confs: usize,
block: BlockNumber,
interval: Duration,
}
impl<'a, P, S> Deployer<'a, P, S>
impl<P, S> Deployer<P, S>
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<T: Into<Duration>>(mut self, interval: T) -> Self {
self.interval = interval.into();
self
}
pub fn block<T: Into<BlockNumber>>(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<Contract<'a, P, S>, ContractError> {
let pending_tx = self
pub async fn send(self) -> Result<Contract<P, S>, 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::<Wallet>()?.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<P, S>,
pub struct ContractFactory<P, S> {
client: Arc<Client<P, S>>,
abi: Abi,
bytecode: Bytes,
}
impl<'a, P, S> ContractFactory<'a, P, S>
impl<P, S> ContractFactory<P, S>
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<P, S>) -> Self {
pub fn new(abi: Abi, bytecode: Bytes, client: impl Into<Arc<Client<P, S>>>) -> 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<T: Tokenize>(
self,
constructor_args: T,
) -> Result<Deployer<'a, P, S>, ContractError> {
pub fn deploy<T: Tokenize>(self, constructor_args: T) -> Result<Deployer<P, S>, 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(),
})
}
}

View File

@ -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<Http, Wallet> {
let provider = Provider::<Http>::try_from("http://localhost:8545").unwrap();
private_key.parse::<Wallet>().unwrap().connect(provider)
pub fn connect(private_key: &str) -> Arc<Client<Http, Wallet>> {
let provider = Provider::<Http>::try_from("http://localhost:8545")
.unwrap()
.interval(Duration::from_millis(10u64));
Arc::new(private_key.parse::<Wallet>().unwrap().connect(provider))
}
/// Launches a ganache instance and deploys the SimpleStorage contract
pub async fn deploy<'a>(
client: &'a Client<Http, Wallet>,
pub async fn deploy(
client: Arc<Client<Http, Wallet>>,
abi: Abi,
bytecode: Bytes,
) -> (GanacheInstance, Contract<'a, Http, Wallet>) {
) -> (GanacheInstance, Contract<Http, Wallet>) {
let ganache = Ganache::new()
.mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
.spawn();

View File

@ -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::<Http>::try_from("http://localhost:8545").unwrap();
let provider = Provider::<Http>::try_from("http://localhost:8545")
.unwrap()
.interval(Duration::from_millis(10u64));
let deployer = "3cDB3d9e1B74692Bb1E3bb5fc81938151cA64b02"
.parse::<Address>()
.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::<Wallet>()
.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", ())

View File

@ -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;

View File

@ -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<T: Into<u64>>(mut self, duration: T) -> Self {
self.interval = Box::new(interval(Duration::from_millis(duration.into())));
pub fn interval<T: Into<Duration>>(mut self, duration: T) -> Self {
self.interval = Box::new(interval(duration.into()));
self
}
}

View File

@ -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>(P, Option<Address>);
pub struct Provider<P>(P, Option<Address>, Option<Duration>);
#[derive(Debug, Error)]
/// An error thrown when making a call to the provider
@ -72,7 +72,7 @@ pub enum FilterKind<'a> {
impl<P: JsonRpcClient> Provider<P> {
/// 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<P: JsonRpcClient> Provider<P> {
pub async fn send_transaction(
&self,
mut tx: TransactionRequest,
) -> Result<PendingTransaction<'_, P>, ProviderError> {
) -> Result<TxHash, ProviderError> {
if let Some(ref to) = tx.to {
if let NameOrAddress::Name(ens_name) = to {
// resolve to an address
@ -276,27 +276,22 @@ impl<P: JsonRpcClient> Provider<P> {
}
}
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<PendingTransaction<'_, P>, ProviderError> {
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<TxHash, ProviderError> {
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<P: JsonRpcClient> Provider<P> {
) -> Result<impl FilterStream<Log> + '_, 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<impl FilterStream<H256> + '_, 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<P: JsonRpcClient> Provider<P> {
) -> Result<impl FilterStream<H256> + '_, 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<P: JsonRpcClient> Provider<P> {
self.1 = Some(ens.into());
self
}
/// Sets the default polling interval for event filters and pending transactions
/// (default: 7 seconds)
pub fn interval<T: Into<Duration>>(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<HttpProvider> {
type Error = ParseError;
fn try_from(src: &str) -> Result<Self, Self::Error> {
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::<HttpProvider>::try_from("http://localhost:8545").unwrap();
let provider = Provider::<HttpProvider>::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<H256> = stream.take(num_blocks).collect::<Vec<H256>>().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::<HttpProvider>::try_from("http://localhost:8545").unwrap();
let provider = Provider::<HttpProvider>::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();

View File

@ -20,7 +20,8 @@ pub fn interval(duration: Duration) -> impl Stream<Item = ()> + 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<R>: StreamExt + Stream<Item = R>
@ -31,7 +32,7 @@ where
fn id(&self) -> U256;
/// Sets the stream's polling interval
fn interval<T: Into<u64>>(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<T: Into<U256>>(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<T: Into<u64>>(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);

View File

@ -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::<Vec<H256>>().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::<Http>::try_from("http://localhost:8545").unwrap();
let provider = Provider::<Http>::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::<Http>::try_from("https://alfajores-forno.celo-testnet.org").unwrap();
let stream = provider
.watch_blocks()
.await
let provider = Provider::<Http>::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::<Vec<H256>>().await;
}

View File

@ -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<BlockNumber>,
) -> Result<PendingTransaction<'_, P>, ClientError> {
) -> Result<TxHash, ClientError> {
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<T: Into<Address>>(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<T: Into<Duration>>(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`

View File

@ -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?;

View File

@ -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::<Http>::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::<Http>::try_from(url.as_str()).unwrap();
let provider = Provider::<Http>::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::<Http>::try_from("https://alfajores-forno.celo-testnet.org").unwrap();
let provider = Provider::<Http>::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);
}

View File

@ -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::<Wallet>()?;
// 4. connect to the network
let provider = Provider::<Http>::try_from(url.as_str())?;
let provider = Provider::<Http>::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

View File

@ -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)?);

View File

@ -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)?);

View File

@ -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)?);

View File

@ -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);
}