From 6156c4bf901468f01cad475663e13d183223827f Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Wed, 10 Jun 2020 13:34:48 +0300 Subject: [PATCH] feat(signers): join the transaction futures --- Cargo.lock | 1 + ethers-signers/Cargo.toml | 1 + ethers-signers/src/client.rs | 106 +++++++++++++++++++++++-------- ethers-signers/src/lib.rs | 4 +- ethers-signers/tests/send_eth.rs | 4 +- 5 files changed, 87 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5be676e8..3703b239 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,6 +372,7 @@ version = "0.1.0" dependencies = [ "ethers-core", "ethers-providers", + "futures-util", "thiserror", "tokio", ] diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index f8bd8a52..f7af48f5 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" ethers-core = { version = "0.1.0", path = "../ethers-core" } ethers-providers = { version = "0.1.0", path = "../ethers-providers" } thiserror = { version = "1.0.19", default-features = false } +futures-util = { version = "0.3.5", default-features = false } [dev-dependencies] tokio = { version = "0.2.21", features = ["macros"] } diff --git a/ethers-signers/src/client.rs b/ethers-signers/src/client.rs index 8cf3a6de..06b45520 100644 --- a/ethers-signers/src/client.rs +++ b/ethers-signers/src/client.rs @@ -5,13 +5,59 @@ use ethers_core::types::{ }; use ethers_providers::{JsonRpcClient, Provider, ProviderError}; -use std::ops::Deref; +use futures_util::{future::ok, join}; +use std::{future::Future, ops::Deref}; use thiserror::Error; #[derive(Clone, Debug)] /// A client provides an interface for signing and broadcasting locally signed transactions -/// It Derefs to `Provider`, which allows interacting with the Ethereum JSON-RPC provider -/// via the same API. +/// It Derefs to [`Provider`], which allows interacting with the Ethereum JSON-RPC provider +/// via the same API. Sending transactions also supports using ENS as a receiver. If you will +/// not be using a local signer, it is recommended to use a [`Provider`] instead. +/// +/// # Example +/// +/// ```no_run +/// use ethers_providers::{Provider, Http}; +/// use ethers_signers::{Client, ClientError, Wallet}; +/// use ethers_core::types::{Address, TransactionRequest}; +/// use std::convert::TryFrom; +/// +/// # async fn foo() -> Result<(), Box> { +/// let mut client: Client<_, _> = Provider::::try_from("http://localhost:8545") +/// .expect("could not instantiate HTTP Provider").into(); +/// +/// // since it derefs to `Provider`, we can just call any of the JSON-RPC API methods +/// let block = client.get_block(100u64).await?; +/// +/// // calling `sign_message` and `send_transaction` will use the unlocked accounts +/// // on the node. +/// let signed_msg = client.sign_message(b"hello".to_vec()).await?; +/// +/// let tx = TransactionRequest::pay("vitalik.eth", 100); +/// let tx_hash = client.send_transaction(tx, None).await?; +/// +/// // if we set a signer, signing of messages and transactions will be done locally +/// // (transactions will be broadcast via the eth_sendRawTransaction API) +/// let wallet: Wallet = "380eb0f3d505f087e438eca80bc4df9a7faa24f868e69fc0440261a0fc0567dc" +/// .parse() +/// .unwrap(); +/// +/// let client = client.with_signer(wallet); +/// +/// let signed_msg2 = client.sign_message(b"hello".to_vec()).await?; +/// +/// let tx2 = TransactionRequest::new() +/// .to("0xd8da6bf26964af9d7eed9e03e53415d37aa96045".parse::
()?) +/// .value(200); +/// let tx_hash2 = client.send_transaction(tx2, None).await?; +/// +/// # Ok(()) +/// # } +/// +/// ``` +/// +/// [`Provider`](../ethers_providers/struct.Provider.html) pub struct Client { pub(crate) provider: Provider

, pub(crate) signer: Option, @@ -97,36 +143,30 @@ where Ok(signed_tx.hash) } - // TODO: Convert to join'ed futures async fn fill_transaction( &self, tx: &mut TransactionRequest, block: Option, ) -> Result<(), ClientError> { - // get the gas price - if tx.gas_price.is_none() { - tx.gas_price = Some(self.provider.get_gas_price().await?); - } + tx.from = Some(self.address()); - // estimate the gas - if tx.gas.is_none() { - tx.from = Some(self.address()); - tx.gas = Some(self.provider.estimate_gas(&tx, block).await?); - } - - // set our nonce - if tx.nonce.is_none() { - tx.nonce = Some( - self.provider - .get_transaction_count(self.address(), block) - .await?, - ); - } + // will poll and await the futures concurrently + let (gas_price, gas, nonce) = join!( + maybe(tx.gas_price, self.provider.get_gas_price()), + maybe(tx.gas, self.provider.estimate_gas(&tx, block)), + maybe( + tx.nonce, + self.provider.get_transaction_count(self.address(), block) + ), + ); + tx.gas_price = Some(gas_price?); + tx.gas = Some(gas?); + tx.nonce = Some(nonce?); Ok(()) } - /// Returns the client's address + /// Returns the client's address (or `address(0)` if no signer is set) pub fn address(&self) -> Address { self.signer .as_ref() @@ -139,7 +179,11 @@ where &self.provider } - /// Returns a reference to the client's signer, will panic if no signer is set + /// Returns a reference to the client's signer + /// + /// # Panics + /// + /// If `self.signer` is `None` pub fn signer_unchecked(&self) -> &S { self.signer.as_ref().expect("no signer is configured") } @@ -156,13 +200,25 @@ where self } - /// Sets the account to be used with the `eth_sign` API calls + /// Sets the default account to be used with the `eth_sign` API calls pub fn from(&mut self, address: Address) -> &mut Self { self.address = address; self } } +/// Calls the future if `item` is None, otherwise returns a `futures::ok` +async fn maybe(item: Option, f: F) -> Result +where + F: Future>, +{ + if let Some(item) = item { + ok(item).await + } else { + f.await + } +} + // Abuse Deref to use the Provider's methods without re-writing everything. // This is an anti-pattern and should not be encouraged, but this improves the UX while // keeping the LoC low diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 124faaa5..77b6b5a3 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -5,7 +5,7 @@ mod client; pub use client::{Client, ClientError}; use ethers_core::types::{Address, Signature, Transaction, TransactionRequest}; -use ethers_providers::http::Provider; +use ethers_providers::Http; use std::error::Error; /// Trait for signing transactions and messages @@ -25,4 +25,4 @@ pub trait Signer: Clone { } /// An HTTP client configured to work with ANY blockchain without replay protection -pub type HttpClient = Client; +pub type HttpClient = Client; diff --git a/ethers-signers/tests/send_eth.rs b/ethers-signers/tests/send_eth.rs index 4d249b63..43d7d53d 100644 --- a/ethers-signers/tests/send_eth.rs +++ b/ethers-signers/tests/send_eth.rs @@ -1,4 +1,4 @@ -use ethers_core::{types::TransactionRequest, utils::GanacheBuilder}; +use ethers_core::{types::TransactionRequest, utils::Ganache}; use ethers_providers::{Http, Provider}; use ethers_signers::Wallet; use std::convert::TryFrom; @@ -7,7 +7,7 @@ use std::convert::TryFrom; async fn send_eth() { let port = 8545u64; let url = format!("http://localhost:{}", port).to_string(); - let _ganache = GanacheBuilder::new() + let _ganache = Ganache::new() .port(port) .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") .spawn();