feat(signers): join the transaction futures
This commit is contained in:
parent
8b5dac2866
commit
6156c4bf90
|
@ -372,6 +372,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ethers-core",
|
"ethers-core",
|
||||||
"ethers-providers",
|
"ethers-providers",
|
||||||
|
"futures-util",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue