feat(signers): join the transaction futures

This commit is contained in:
Georgios Konstantopoulos 2020-06-10 13:34:48 +03:00
parent 8b5dac2866
commit 6156c4bf90
No known key found for this signature in database
GPG Key ID: FA607837CD26EDBC
5 changed files with 87 additions and 29 deletions

1
Cargo.lock generated
View File

@ -372,6 +372,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"ethers-core", "ethers-core",
"ethers-providers", "ethers-providers",
"futures-util",
"thiserror", "thiserror",
"tokio", "tokio",
] ]

View File

@ -8,6 +8,7 @@ edition = "2018"
ethers-core = { version = "0.1.0", path = "../ethers-core" } ethers-core = { version = "0.1.0", path = "../ethers-core" }
ethers-providers = { version = "0.1.0", path = "../ethers-providers" } ethers-providers = { version = "0.1.0", path = "../ethers-providers" }
thiserror = { version = "1.0.19", default-features = false } thiserror = { version = "1.0.19", default-features = false }
futures-util = { version = "0.3.5", default-features = false }
[dev-dependencies] [dev-dependencies]
tokio = { version = "0.2.21", features = ["macros"] } tokio = { version = "0.2.21", features = ["macros"] }

View File

@ -5,13 +5,59 @@ use ethers_core::types::{
}; };
use ethers_providers::{JsonRpcClient, Provider, ProviderError}; 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; use thiserror::Error;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
/// A client provides an interface for signing and broadcasting locally signed transactions /// 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 /// It Derefs to [`Provider`], which allows interacting with the Ethereum JSON-RPC provider
/// via the same API. /// 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<dyn std::error::Error>> {
/// let mut client: Client<_, _> = Provider::<Http>::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::<Address>()?)
/// .value(200);
/// let tx_hash2 = client.send_transaction(tx2, None).await?;
///
/// # Ok(())
/// # }
///
/// ```
///
/// [`Provider`](../ethers_providers/struct.Provider.html)
pub struct Client<P, S> { pub struct Client<P, S> {
pub(crate) provider: Provider<P>, pub(crate) provider: Provider<P>,
pub(crate) signer: Option<S>, pub(crate) signer: Option<S>,
@ -97,36 +143,30 @@ where
Ok(signed_tx.hash) Ok(signed_tx.hash)
} }
// TODO: Convert to join'ed futures
async fn fill_transaction( async fn fill_transaction(
&self, &self,
tx: &mut TransactionRequest, tx: &mut TransactionRequest,
block: Option<BlockNumber>, block: Option<BlockNumber>,
) -> Result<(), ClientError> { ) -> Result<(), ClientError> {
// get the gas price
if tx.gas_price.is_none() {
tx.gas_price = Some(self.provider.get_gas_price().await?);
}
// estimate the gas
if tx.gas.is_none() {
tx.from = Some(self.address()); tx.from = Some(self.address());
tx.gas = Some(self.provider.estimate_gas(&tx, block).await?);
}
// set our nonce // will poll and await the futures concurrently
if tx.nonce.is_none() { let (gas_price, gas, nonce) = join!(
tx.nonce = Some( maybe(tx.gas_price, self.provider.get_gas_price()),
self.provider maybe(tx.gas, self.provider.estimate_gas(&tx, block)),
.get_transaction_count(self.address(), block) maybe(
.await?, 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(()) Ok(())
} }
/// Returns the client's address /// Returns the client's address (or `address(0)` if no signer is set)
pub fn address(&self) -> Address { pub fn address(&self) -> Address {
self.signer self.signer
.as_ref() .as_ref()
@ -139,7 +179,11 @@ where
&self.provider &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 { pub fn signer_unchecked(&self) -> &S {
self.signer.as_ref().expect("no signer is configured") self.signer.as_ref().expect("no signer is configured")
} }
@ -156,13 +200,25 @@ where
self 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 { pub fn from(&mut self, address: Address) -> &mut Self {
self.address = address; self.address = address;
self self
} }
} }
/// Calls the future if `item` is None, otherwise returns a `futures::ok`
async fn maybe<F, T, E>(item: Option<T>, f: F) -> Result<T, E>
where
F: Future<Output = Result<T, E>>,
{
if let Some(item) = item {
ok(item).await
} else {
f.await
}
}
// Abuse Deref to use the Provider's methods without re-writing everything. // 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 // This is an anti-pattern and should not be encouraged, but this improves the UX while
// keeping the LoC low // keeping the LoC low

View File

@ -5,7 +5,7 @@ mod client;
pub use client::{Client, ClientError}; pub use client::{Client, ClientError};
use ethers_core::types::{Address, Signature, Transaction, TransactionRequest}; use ethers_core::types::{Address, Signature, Transaction, TransactionRequest};
use ethers_providers::http::Provider; use ethers_providers::Http;
use std::error::Error; use std::error::Error;
/// Trait for signing transactions and messages /// 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 /// An HTTP client configured to work with ANY blockchain without replay protection
pub type HttpClient = Client<Provider, Wallet>; pub type HttpClient = Client<Http, Wallet>;

View File

@ -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_providers::{Http, Provider};
use ethers_signers::Wallet; use ethers_signers::Wallet;
use std::convert::TryFrom; use std::convert::TryFrom;
@ -7,7 +7,7 @@ use std::convert::TryFrom;
async fn send_eth() { async fn send_eth() {
let port = 8545u64; let port = 8545u64;
let url = format!("http://localhost:{}", port).to_string(); let url = format!("http://localhost:{}", port).to_string();
let _ganache = GanacheBuilder::new() let _ganache = Ganache::new()
.port(port) .port(port)
.mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle")
.spawn(); .spawn();