Returning a `PendingTransaction` after sending a tx (#107)

* feat(providers): return a PendingTransaction from send_tx calls

* feat(providers): expose the internal provider to all middlewares

* fix(middleware): use the returned PendingTx instead of using a hash

* fix(contract): use the pending tx returned value

Note1: To support that, we need to clone the tx when sending in order to make lifetimes work out
Note2: Multicall does not support that feature

* fix(ethers): adjust examples

* chore: fix provider test

* chore: fix celo test

BREAKING CHANGE
This commit is contained in:
Georgios Konstantopoulos 2020-12-17 13:26:01 +02:00 committed by GitHub
parent 5b7578296b
commit 3a2fd3e814
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 114 additions and 121 deletions

View File

@ -1,9 +1,9 @@
use super::base::{decode_fn, AbiError};
use ethers_core::{
abi::{Detokenize, Function, InvalidOutputType},
types::{Address, BlockNumber, Bytes, TransactionRequest, TxHash, U256},
types::{Address, BlockNumber, Bytes, TransactionRequest, U256},
};
use ethers_providers::Middleware;
use ethers_providers::{Middleware, PendingTransaction};
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
@ -126,9 +126,9 @@ where
}
/// Signs and broadcasts the provided transaction
pub async fn send(self) -> Result<TxHash, ContractError<M>> {
pub async fn send(&self) -> Result<PendingTransaction<'_, M::Provider>, ContractError<M>> {
self.client
.send_transaction(self.tx, self.block)
.send_transaction(self.tx.clone(), self.block)
.await
.map_err(ContractError::MiddlewareError)
}

View File

@ -6,9 +6,9 @@ use super::{
use ethers_core::{
abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize},
types::{Address, Filter, NameOrAddress, Selector, TransactionRequest, TxHash},
types::{Address, Filter, NameOrAddress, Selector, TransactionRequest},
};
use ethers_providers::{Middleware, PendingTransaction};
use ethers_providers::Middleware;
use rustc_hex::ToHex;
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
@ -90,11 +90,12 @@ use std::{fmt::Debug, marker::PhantomData, sync::Arc};
/// .await?;
///
/// // 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?;
/// let call = contract
/// .method::<_, H256>("setValue", "hi".to_owned())?;
/// let pending_tx = call.send().await?;
///
/// // `await`ing on the pending transaction resolves to a transaction receipt
/// let receipt = contract.pending_transaction(tx_hash).confirmations(6).await?;
/// let receipt = pending_tx.confirmations(6).await?;
///
/// # Ok(())
/// # }
@ -283,8 +284,4 @@ impl<M: Middleware> Contract<M> {
pub fn client(&self) -> &M {
&self.client
}
pub fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, M::Provider> {
self.client.pending_transaction(tx_hash)
}
}

View File

@ -35,16 +35,14 @@ impl<M: Middleware> Deployer<M> {
/// 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<M>, ContractError<M>> {
let tx_hash = self
let pending_tx = self
.client
.send_transaction(self.tx, Some(self.block))
.await
.map_err(ContractError::MiddlewareError)?;
// TODO: Should this be calculated "optimistically" by address/nonce?
let receipt = self
.client
.pending_transaction(tx_hash)
let receipt = pending_tx
.confirmations(self.confs)
.await
.map_err(|_| ContractError::ContractNotDeployed)?;

View File

@ -61,7 +61,7 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
/// use ethers::{
/// abi::Abi,
/// contract::{Contract, Multicall},
/// providers::{Middleware, Http, Provider},
/// providers::{Middleware, Http, Provider, PendingTransaction},
/// types::{Address, H256, U256},
/// };
/// use std::{convert::TryFrom, sync::Arc};
@ -110,7 +110,7 @@ pub static ADDRESS_BOOK: Lazy<HashMap<U256, Address>> = Lazy::new(|| {
/// // `await`ing the `send` method waits for the transaction to be broadcast, which also
/// // returns the transaction hash
/// let tx_hash = multicall.send().await?;
/// let _tx_receipt = client.pending_transaction(tx_hash).await?;
/// let _tx_receipt = PendingTransaction::new(tx_hash, &client).await?;
///
/// // you can also query ETH balances of multiple addresses
/// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::<Address>()?;
@ -345,7 +345,9 @@ impl<M: Middleware> Multicall<M> {
let contract_call = self.as_contract_call();
// Broadcast transaction and return the transaction hash
let tx_hash = contract_call.send().await?;
// TODO: Can we make this return a PendingTransaction directly instead?
// Seems hard due to `returns a value referencing data owned by the current function`
let tx_hash = *contract_call.send().await?;
Ok(tx_hash)
}

View File

@ -8,7 +8,7 @@ mod eth_tests {
use super::*;
use ethers::{
contract::Multicall,
providers::{Http, Middleware, Provider, StreamExt},
providers::{Http, Middleware, PendingTransaction, Provider, StreamExt},
types::{Address, U256},
utils::Ganache,
};
@ -51,13 +51,9 @@ mod eth_tests {
.unwrap();
let calldata = contract_call.calldata().unwrap();
let gas_estimate = contract_call.estimate_gas().await.unwrap();
let tx_hash = contract_call.send().await.unwrap();
let tx = client.get_transaction(tx_hash).await.unwrap().unwrap();
let tx_receipt = client
.get_transaction_receipt(tx_hash)
.await
.unwrap()
.unwrap();
let pending_tx = contract_call.send().await.unwrap();
let tx = client.get_transaction(*pending_tx).await.unwrap().unwrap();
let tx_receipt = pending_tx.await.unwrap();
assert_eq!(last_sender.clone().call().await.unwrap(), client2.address());
assert_eq!(get_value.clone().call().await.unwrap(), "hi");
assert_eq!(tx.input, calldata);
@ -88,6 +84,8 @@ mod eth_tests {
.unwrap()
.send()
.await
.unwrap()
.await
.unwrap();
}
@ -99,7 +97,7 @@ mod eth_tests {
let contract = deploy(client.clone(), abi, bytecode).await;
// make a call with `client2`
let _tx_hash = contract
let _tx_hash = *contract
.method::<_, H256>("setValue", "hi".to_owned())
.unwrap()
.send()
@ -143,13 +141,11 @@ mod eth_tests {
// and we make a few calls
for i in 0..num_calls {
let tx_hash = contract
let call = contract
.method::<_, H256>("setValue", i.to_string())
.unwrap()
.send()
.await
.unwrap();
let _receipt = contract.pending_transaction(tx_hash).await.unwrap();
let pending_tx = call.send().await.unwrap();
let _receipt = pending_tx.await.unwrap();
}
for i in 0..num_calls {
@ -180,13 +176,14 @@ mod eth_tests {
let contract = deploy(client, abi, bytecode).await;
// make a call without the signer
let tx_hash = contract
let _receipt = 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()
@ -313,7 +310,9 @@ mod eth_tests {
// broadcast the transaction and wait for it to be mined
let tx_hash = multicall_send.send().await.unwrap();
let _tx_receipt = client4.pending_transaction(tx_hash).await.unwrap();
let _tx_receipt = PendingTransaction::new(tx_hash, client.provider())
.await
.unwrap();
// Do another multicall to check the updated return values
// The `getValue` calls should return the last value we set in the batched broadcast
@ -385,14 +384,14 @@ mod celo_tests {
assert_eq!(value, "initial value");
// make a state mutating transaction
let tx_hash = contract
let call = contract
.method::<_, H256>("setValue", "hi".to_owned())
.unwrap()
.unwrap();
let pending_tx = call
.send()
.await
.unwrap();
let receipt = contract.pending_transaction(tx_hash).await.unwrap();
assert_eq!(receipt.status.unwrap(), 1.into());
let _receipt = pending_tx.await.unwrap();
let value: String = contract
.method("getValue", ())

View File

@ -6,7 +6,7 @@ pub use linear::LinearGasPrice;
use async_trait::async_trait;
use ethers_core::types::{BlockNumber, TransactionRequest, TxHash, U256};
use ethers_providers::{interval, FromErr, Middleware, StreamExt};
use ethers_providers::{interval, FromErr, Middleware, PendingTransaction, StreamExt};
use futures_util::lock::Mutex;
use std::sync::Arc;
use std::{pin::Pin, time::Instant};
@ -97,8 +97,8 @@ where
&self,
tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, Self::Error> {
let tx_hash = self
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
let pending_tx = self
.inner()
.send_transaction(tx.clone(), block)
.await
@ -106,9 +106,9 @@ where
// insert the tx in the pending txs
let mut lock = self.txs.lock().await;
lock.push((tx_hash, tx, Instant::now(), block));
lock.push((*pending_tx, tx, Instant::now(), block));
Ok(tx_hash)
Ok(pending_tx)
}
}
@ -188,7 +188,7 @@ where
.send_transaction(replacement_tx.clone(), priority)
.await
{
Ok(tx_hash) => tx_hash,
Ok(tx_hash) => *tx_hash,
Err(err) => {
if err.to_string().contains("nonce too low") {
// ignore "nonce too low" errors because they

View File

@ -1,7 +1,7 @@
use super::{GasOracle, GasOracleError};
use async_trait::async_trait;
use ethers_core::types::*;
use ethers_providers::{FromErr, Middleware};
use ethers_providers::{FromErr, Middleware, PendingTransaction};
use thiserror::Error;
#[derive(Debug)]
@ -60,7 +60,7 @@ where
&self,
mut tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, Self::Error> {
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
if tx.gas_price.is_none() {
tx.gas_price = Some(self.get_gas_price().await?);
}

View File

@ -1,6 +1,6 @@
use async_trait::async_trait;
use ethers_core::types::*;
use ethers_providers::{FromErr, Middleware};
use ethers_providers::{FromErr, Middleware, PendingTransaction};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use thiserror::Error;
@ -87,7 +87,7 @@ where
&self,
mut tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, Self::Error> {
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
if tx.nonce.is_none() {
tx.nonce = Some(self.get_transaction_count_with_manager(block).await?);
}

View File

@ -1,11 +1,11 @@
use ethers_core::{
types::{
Address, BlockNumber, Bytes, NameOrAddress, Signature, Transaction, TransactionRequest,
TxHash, U256,
U256,
},
utils::keccak256,
};
use ethers_providers::{FromErr, Middleware};
use ethers_providers::{FromErr, Middleware, PendingTransaction};
use ethers_signers::Signer;
use async_trait::async_trait;
@ -44,11 +44,11 @@ use thiserror::Error;
///
/// // ...and sign transactions
/// let tx = TransactionRequest::pay("vitalik.eth", 100);
/// let tx_hash = client.send_transaction(tx, None).await?;
/// let pending_tx = client.send_transaction(tx, None).await?;
///
/// // You can `await` on the pending transaction to get the receipt with a pre-specified
/// // number of confirmations
/// let receipt = client.pending_transaction(tx_hash).confirmations(6).await?;
/// let receipt = pending_tx.confirmations(6).await?;
///
/// // You can connect with other wallets at runtime via the `with_signer` function
/// let wallet2: LocalWallet = "cd8c407233c0560f6de24bb2dc60a8b02335c959a1a17f749ce6c1ccf63d74a7"
@ -242,7 +242,7 @@ where
&self,
mut tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, Self::Error> {
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
if let Some(ref to) = tx.to {
if let NameOrAddress::Name(ens_name) = to {
let addr = self

View File

@ -27,7 +27,7 @@ async fn using_gas_oracle() {
.value(10000);
let tx_hash = provider.send_transaction(tx, None).await.unwrap();
let tx = provider.get_transaction(tx_hash).await.unwrap().unwrap();
let tx = provider.get_transaction(*tx_hash).await.unwrap().unwrap();
assert_eq!(tx.gas_price, expected_gas_price);
}

View File

@ -36,7 +36,7 @@ async fn nonce_manager() {
.send_transaction(TransactionRequest::pay(address, 100u64), None)
.await
.unwrap();
tx_hashes.push(tx);
tx_hashes.push(*tx);
}
// sleep a bit to ensure there's no flakiness in the test

View File

@ -58,9 +58,10 @@ async fn test_send_transaction() {
let balance_before = client.get_balance(client.address(), None).await.unwrap();
let tx = TransactionRequest::pay(client.address(), 100);
let tx_hash = client.send_transaction(tx, None).await.unwrap();
let _receipt = client
.pending_transaction(tx_hash)
.send_transaction(tx, None)
.await
.unwrap()
.confirmations(3)
.await
.unwrap();

View File

@ -58,7 +58,6 @@ mod tests {
// the base provider
let provider = Provider::<Http>::try_from(ganache.endpoint()).unwrap();
let provider_clone = provider.clone();
// the Gas Price escalator middleware is the first middleware above the provider,
// so that it receives the transaction last, after all the other middleware
@ -77,24 +76,21 @@ mod tests {
let provider = NonceManagerMiddleware::new(provider, address);
let tx = TransactionRequest::new();
let mut tx_hash = None;
let mut pending_txs = Vec::new();
for _ in 0..10 {
tx_hash = Some(provider.send_transaction(tx.clone(), None).await.unwrap());
dbg!(
provider
.get_transaction(tx_hash.unwrap())
.await
.unwrap()
.unwrap()
.gas_price
);
let pending = provider.send_transaction(tx.clone(), None).await.unwrap();
let hash = *pending;
let gas_price = provider
.get_transaction(hash)
.await
.unwrap()
.unwrap()
.gas_price;
dbg!(gas_price);
pending_txs.push(pending);
}
let receipt = provider_clone
.pending_transaction(tx_hash.unwrap())
.await
.unwrap();
dbg!(receipt);
let receipts = futures_util::future::join_all(pending_txs);
dbg!(receipts.await);
}
}

View File

@ -1,4 +1,5 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(broken_intra_doc_links)]
//! # Clients for interacting with Ethereum nodes
//!
//! This crate provides asynchronous [Ethereum JSON-RPC](https://github.com/ethereum/wiki/wiki/JSON-RPC)
@ -153,6 +154,10 @@ pub trait Middleware: Sync + Send + Debug {
fn inner(&self) -> &Self::Inner;
fn provider(&self) -> &Provider<Self::Provider> {
self.inner().provider()
}
async fn get_block_number(&self) -> Result<U64, Self::Error> {
self.inner().get_block_number().await.map_err(FromErr::from)
}
@ -161,7 +166,7 @@ pub trait Middleware: Sync + Send + Debug {
&self,
tx: TransactionRequest,
block: Option<BlockNumber>,
) -> Result<TxHash, Self::Error> {
) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
self.inner()
.send_transaction(tx, block)
.await
@ -268,7 +273,10 @@ pub trait Middleware: Sync + Send + Debug {
self.inner().get_accounts().await.map_err(FromErr::from)
}
async fn send_raw_transaction(&self, tx: &Transaction) -> Result<TxHash, Self::Error> {
async fn send_raw_transaction<'a>(
&'a self,
tx: &Transaction,
) -> Result<PendingTransaction<'a, Self::Provider>, Self::Error> {
self.inner()
.send_raw_transaction(tx)
.await
@ -357,10 +365,6 @@ pub trait Middleware: Sync + Send + Debug {
.map_err(FromErr::from)
}
fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, Self::Provider> {
self.inner().pending_transaction(tx_hash)
}
// Mempool inspection for Geth's API
async fn txpool_content(&self) -> Result<TxpoolContent, Self::Error> {

View File

@ -133,6 +133,10 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
unreachable!("There is no inner provider here")
}
fn provider(&self) -> &Provider<Self::Provider> {
self
}
////// Blockchain Status
//
// Functions for querying the state of the blockchain
@ -299,7 +303,7 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
&self,
mut tx: TransactionRequest,
_: Option<BlockNumber>,
) -> Result<TxHash, ProviderError> {
) -> Result<PendingTransaction<'_, P>, ProviderError> {
if tx.from.is_none() {
tx.from = self.3;
}
@ -318,22 +322,28 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
}
}
Ok(self
let tx_hash = self
.0
.request("eth_sendTransaction", [tx])
.await
.map_err(Into::into)?)
.map_err(Into::into)?;
Ok(PendingTransaction::new(tx_hash, self).interval(self.get_interval()))
}
/// 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.
async fn send_raw_transaction(&self, tx: &Transaction) -> Result<TxHash, ProviderError> {
async fn send_raw_transaction<'a>(
&'a self,
tx: &Transaction,
) -> Result<PendingTransaction<'a, P>, ProviderError> {
let rlp = utils::serialize(&tx.rlp());
Ok(self
let tx_hash = self
.0
.request("eth_sendRawTransaction", [rlp])
.await
.map_err(Into::into)?)
.map_err(Into::into)?;
Ok(PendingTransaction::new(tx_hash, self).interval(self.get_interval()))
}
/// Signs data using a specific account. This account needs to be unlocked.
@ -510,12 +520,6 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
.await
}
/// Helper which creates a pending transaction object from a transaction hash
/// using the provider's polling interval
fn pending_transaction(&self, tx_hash: TxHash) -> PendingTransaction<'_, P> {
PendingTransaction::new(tx_hash, self).interval(self.get_interval())
}
/// Returns the details of all transactions currently pending for inclusion in the next
/// block(s), as well as the ones that are being scheduled for future execution only.
/// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content)
@ -979,22 +983,17 @@ mod tests {
let accounts = provider.get_accounts().await.unwrap();
let tx = TransactionRequest::pay(accounts[0], parse_ether(1u64).unwrap()).from(accounts[0]);
let tx_hash = provider.send_transaction(tx, None).await.unwrap();
let pending_tx = provider.send_transaction(tx, None).await.unwrap();
assert!(provider
.get_transaction_receipt(tx_hash)
.get_transaction_receipt(*pending_tx)
.await
.unwrap()
.is_none());
// couple of seconds pass
std::thread::sleep(std::time::Duration::new(3, 0));
assert!(provider
.get_transaction_receipt(tx_hash)
.await
.unwrap()
.is_some());
let hash = *pending_tx;
let receipt = pending_tx.await.unwrap();
assert_eq!(receipt.transaction_hash, hash);
}
#[tokio::test]

View File

@ -136,8 +136,8 @@ mod eth_tests {
async fn generic_pending_txs_test<M: Middleware>(provider: M, who: ethers::types::Address) {
let tx = TransactionRequest::new().to(who).from(who);
let tx_hash = provider.send_transaction(tx, None).await.unwrap();
let pending_tx = provider.pending_transaction(tx_hash);
let pending_tx = provider.send_transaction(tx, None).await.unwrap();
let tx_hash = *pending_tx;
let receipt = pending_tx.confirmations(3).await.unwrap();
// got the correct receipt
assert_eq!(receipt.transaction_hash, tx_hash);

View File

@ -56,8 +56,8 @@ async fn main() -> Result<()> {
let contract = SimpleContract::new(addr, client.clone());
// 10. call the `setValue` method
let tx_hash = contract.set_value("hi".to_owned()).send().await?;
let _receipt = client.pending_transaction(tx_hash).await?;
// (first `await` returns a PendingTransaction, second one waits for it to be mined)
let _receipt = contract.set_value("hi".to_owned()).send().await?.await?;
// 11. get all events
let logs = contract

View File

@ -18,9 +18,7 @@ async fn main() -> Result<()> {
let tx = TransactionRequest::new().to("vitalik.eth").value(100_000);
// send it!
let tx_hash = client.send_transaction(tx, None).await?;
let receipt = client.pending_transaction(tx_hash).await?;
let receipt = client.send_transaction(tx, None).await?.await?;
let tx = client.get_transaction(receipt.transaction_hash).await?;
println!("{}", serde_json::to_string(&tx)?);

View File

@ -17,10 +17,10 @@ async fn main() -> anyhow::Result<()> {
let tx = TransactionRequest::new()
.to("vitalik.eth")
.value(parse_ether(10)?);
let tx_hash = client.send_transaction(tx, None).await?;
let pending_tx = client.send_transaction(tx, None).await?;
// Get the receipt
let _receipt = client.pending_transaction(tx_hash).confirmations(3).await?;
let _receipt = pending_tx.confirmations(3).await?;
Ok(())
}

View File

@ -19,10 +19,10 @@ async fn main() -> Result<()> {
let tx = TransactionRequest::new().to(wallet2.address()).value(10000);
// send it!
let tx_hash = client.send_transaction(tx, None).await?;
let pending_tx = client.send_transaction(tx, None).await?;
// get the mined tx
let receipt = client.pending_transaction(tx_hash).await?;
let receipt = pending_tx.await?;
let tx = client.get_transaction(receipt.transaction_hash).await?;
println!("Sent tx: {}\n", serde_json::to_string(&tx)?);

View File

@ -18,9 +18,7 @@ async fn main() -> Result<()> {
let balance_before = provider.get_balance(from, None).await?;
// broadcast it via the eth_sendTransaction API
let tx_hash = provider.send_transaction(tx, None).await?;
let tx = provider.pending_transaction(tx_hash).await?;
let tx = provider.send_transaction(tx, None).await?.await?;
println!("{}", serde_json::to_string(&tx)?);

View File

@ -20,10 +20,10 @@ async fn main() -> anyhow::Result<()> {
let tx = TransactionRequest::new()
.to("vitalik.eth")
.value(parse_ether(10)?);
let tx_hash = client.send_transaction(tx, None).await?;
let pending_tx = client.send_transaction(tx, None).await?;
// Get the receipt
let _receipt = client.pending_transaction(tx_hash).confirmations(3).await?;
let _receipt = pending_tx.confirmations(3).await?;
Ok(())
}

View File

@ -4,6 +4,7 @@
rust_2018_idioms,
unreachable_pub
)]
#![deny(broken_intra_doc_links)]
#![doc(test(
no_crate_inject,
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))