feature: PendingTransaction returns Option (#327)
* feature: PendingTransaction returns Option * chore: expand safety reasoning in pending tx expect * chore: clippy lints * bug: check readiness of future before taking receipt option
This commit is contained in:
parent
dd98a593e2
commit
d722c1a6af
|
@ -45,7 +45,8 @@ impl<M: Middleware> Deployer<M> {
|
||||||
let receipt = pending_tx
|
let receipt = pending_tx
|
||||||
.confirmations(self.confs)
|
.confirmations(self.confs)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| ContractError::ContractNotDeployed)?;
|
.map_err(|_| ContractError::ContractNotDeployed)?
|
||||||
|
.ok_or(ContractError::ContractNotDeployed)?;
|
||||||
let address = receipt
|
let address = receipt
|
||||||
.contract_address
|
.contract_address
|
||||||
.ok_or(ContractError::ContractNotDeployed)?;
|
.ok_or(ContractError::ContractNotDeployed)?;
|
||||||
|
|
|
@ -53,7 +53,7 @@ mod eth_tests {
|
||||||
let gas_estimate = contract_call.estimate_gas().await.unwrap();
|
let gas_estimate = contract_call.estimate_gas().await.unwrap();
|
||||||
let pending_tx = contract_call.send().await.unwrap();
|
let pending_tx = contract_call.send().await.unwrap();
|
||||||
let tx = client.get_transaction(*pending_tx).await.unwrap().unwrap();
|
let tx = client.get_transaction(*pending_tx).await.unwrap().unwrap();
|
||||||
let tx_receipt = pending_tx.await.unwrap();
|
let tx_receipt = pending_tx.await.unwrap().unwrap();
|
||||||
assert_eq!(last_sender.clone().call().await.unwrap(), client2.address());
|
assert_eq!(last_sender.clone().call().await.unwrap(), client2.address());
|
||||||
assert_eq!(get_value.clone().call().await.unwrap(), "hi");
|
assert_eq!(get_value.clone().call().await.unwrap(), "hi");
|
||||||
assert_eq!(tx.input, calldata);
|
assert_eq!(tx.input, calldata);
|
||||||
|
|
|
@ -110,7 +110,8 @@ impl DsProxy {
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.await
|
.await
|
||||||
.map_err(ContractError::ProviderError)?;
|
.map_err(ContractError::ProviderError)?
|
||||||
|
.ok_or(ContractError::ContractNotDeployed)?;
|
||||||
|
|
||||||
// decode the event log to get the address of the deployed contract.
|
// decode the event log to get the address of the deployed contract.
|
||||||
if tx_receipt.status == Some(U64::from(1u64)) {
|
if tx_receipt.status == Some(U64::from(1u64)) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
stream::{interval, DEFAULT_POLL_INTERVAL},
|
stream::{interval, DEFAULT_POLL_INTERVAL},
|
||||||
JsonRpcClient, PinBoxFut, Provider, ProviderError,
|
JsonRpcClient, PinBoxFut, Provider, ProviderError,
|
||||||
};
|
};
|
||||||
use ethers_core::types::{TransactionReceipt, TxHash, U64};
|
use ethers_core::types::{Transaction, TransactionReceipt, TxHash, U64};
|
||||||
use futures_core::stream::Stream;
|
use futures_core::stream::Stream;
|
||||||
use futures_util::stream::StreamExt;
|
use futures_util::stream::StreamExt;
|
||||||
use pin_project::pin_project;
|
use pin_project::pin_project;
|
||||||
|
@ -33,12 +33,12 @@ pub struct PendingTransaction<'a, P> {
|
||||||
impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> {
|
impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> {
|
||||||
/// Creates a new pending transaction poller from a hash and a provider
|
/// Creates a new pending transaction poller from a hash and a provider
|
||||||
pub fn new(tx_hash: TxHash, provider: &'a Provider<P>) -> Self {
|
pub fn new(tx_hash: TxHash, provider: &'a Provider<P>) -> Self {
|
||||||
let fut = Box::pin(provider.get_transaction_receipt(tx_hash));
|
let fut = Box::pin(provider.get_transaction(tx_hash));
|
||||||
Self {
|
Self {
|
||||||
tx_hash,
|
tx_hash,
|
||||||
confirmations: 1,
|
confirmations: 1,
|
||||||
provider,
|
provider,
|
||||||
state: PendingTxState::GettingReceipt(fut),
|
state: PendingTxState::GettingTx(fut),
|
||||||
interval: Box::new(interval(DEFAULT_POLL_INTERVAL)),
|
interval: Box::new(interval(DEFAULT_POLL_INTERVAL)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,13 +57,68 @@ impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! rewake_with_new_state {
|
||||||
|
($ctx:ident, $this:ident, $new_state:expr) => {
|
||||||
|
*$this.state = $new_state;
|
||||||
|
$ctx.waker().wake_by_ref();
|
||||||
|
return Poll::Pending;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! rewake_with_new_state_if {
|
||||||
|
($condition:expr, $ctx:ident, $this:ident, $new_state:expr) => {
|
||||||
|
if $condition {
|
||||||
|
rewake_with_new_state!($ctx, $this, $new_state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> {
|
impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> {
|
||||||
type Output = Result<TransactionReceipt, ProviderError>;
|
type Output = Result<Option<TransactionReceipt>, ProviderError>;
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
|
||||||
let this = self.project();
|
let this = self.project();
|
||||||
|
|
||||||
match this.state {
|
match this.state {
|
||||||
|
PendingTxState::PausedGettingTx => {
|
||||||
|
// Wait the polling period so that we do not spam the chain when no
|
||||||
|
// new block has been mined
|
||||||
|
let _ready = futures_util::ready!(this.interval.poll_next_unpin(ctx));
|
||||||
|
let fut = Box::pin(this.provider.get_transaction(*this.tx_hash));
|
||||||
|
*this.state = PendingTxState::GettingTx(fut);
|
||||||
|
ctx.waker().wake_by_ref();
|
||||||
|
}
|
||||||
|
PendingTxState::GettingTx(fut) => {
|
||||||
|
let tx_res = futures_util::ready!(fut.as_mut().poll(ctx));
|
||||||
|
// If the provider errors, just try again after the interval.
|
||||||
|
// nbd.
|
||||||
|
rewake_with_new_state_if!(
|
||||||
|
tx_res.is_err(),
|
||||||
|
ctx,
|
||||||
|
this,
|
||||||
|
PendingTxState::PausedGettingTx
|
||||||
|
);
|
||||||
|
|
||||||
|
let tx_opt = tx_res.unwrap();
|
||||||
|
// If the tx is no longer in the mempool, return Ok(None)
|
||||||
|
if tx_opt.is_none() {
|
||||||
|
*this.state = PendingTxState::Completed;
|
||||||
|
return Poll::Ready(Ok(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it hasn't confirmed yet, poll again later
|
||||||
|
let tx = tx_opt.unwrap();
|
||||||
|
rewake_with_new_state_if!(
|
||||||
|
tx.block_number.is_none(),
|
||||||
|
ctx,
|
||||||
|
this,
|
||||||
|
PendingTxState::PausedGettingTx
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start polling for the receipt now
|
||||||
|
let fut = Box::pin(this.provider.get_transaction_receipt(*this.tx_hash));
|
||||||
|
*this.state = PendingTxState::GettingReceipt(fut);
|
||||||
|
}
|
||||||
PendingTxState::PausedGettingReceipt => {
|
PendingTxState::PausedGettingReceipt => {
|
||||||
// Wait the polling period so that we do not spam the chain when no
|
// Wait the polling period so that we do not spam the chain when no
|
||||||
// new block has been mined
|
// new block has been mined
|
||||||
|
@ -73,25 +128,31 @@ impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> {
|
||||||
ctx.waker().wake_by_ref();
|
ctx.waker().wake_by_ref();
|
||||||
}
|
}
|
||||||
PendingTxState::GettingReceipt(fut) => {
|
PendingTxState::GettingReceipt(fut) => {
|
||||||
if let Ok(Some(receipt)) = futures_util::ready!(fut.as_mut().poll(ctx)) {
|
if let Ok(receipt) = futures_util::ready!(fut.as_mut().poll(ctx)) {
|
||||||
*this.state = PendingTxState::CheckingReceipt(Box::new(receipt))
|
*this.state = PendingTxState::CheckingReceipt(receipt)
|
||||||
} else {
|
} else {
|
||||||
*this.state = PendingTxState::PausedGettingReceipt
|
*this.state = PendingTxState::PausedGettingReceipt
|
||||||
}
|
}
|
||||||
ctx.waker().wake_by_ref();
|
ctx.waker().wake_by_ref();
|
||||||
}
|
}
|
||||||
PendingTxState::CheckingReceipt(receipt) => {
|
PendingTxState::CheckingReceipt(receipt) => {
|
||||||
|
rewake_with_new_state_if!(
|
||||||
|
receipt.is_none(),
|
||||||
|
ctx,
|
||||||
|
this,
|
||||||
|
PendingTxState::PausedGettingReceipt
|
||||||
|
);
|
||||||
|
|
||||||
// If we requested more than 1 confirmation, we need to compare the receipt's
|
// If we requested more than 1 confirmation, we need to compare the receipt's
|
||||||
// block number and the current block
|
// block number and the current block
|
||||||
if *this.confirmations > 1 {
|
if *this.confirmations > 1 {
|
||||||
let fut = Box::pin(this.provider.get_block_number());
|
let fut = Box::pin(this.provider.get_block_number());
|
||||||
*this.state =
|
*this.state = PendingTxState::GettingBlockNumber(fut, receipt.take());
|
||||||
PendingTxState::GettingBlockNumber(fut, Box::new(*receipt.clone()));
|
|
||||||
|
|
||||||
// Schedule the waker to poll again
|
// Schedule the waker to poll again
|
||||||
ctx.waker().wake_by_ref();
|
ctx.waker().wake_by_ref();
|
||||||
} else {
|
} else {
|
||||||
let receipt = *receipt.clone();
|
let receipt = receipt.take();
|
||||||
*this.state = PendingTxState::Completed;
|
*this.state = PendingTxState::Completed;
|
||||||
return Poll::Ready(Ok(receipt));
|
return Poll::Ready(Ok(receipt));
|
||||||
}
|
}
|
||||||
|
@ -104,26 +165,30 @@ impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> {
|
||||||
// we need to re-instantiate the get_block_number future so that
|
// we need to re-instantiate the get_block_number future so that
|
||||||
// we poll again
|
// we poll again
|
||||||
let fut = Box::pin(this.provider.get_block_number());
|
let fut = Box::pin(this.provider.get_block_number());
|
||||||
*this.state = PendingTxState::GettingBlockNumber(fut, receipt.clone());
|
*this.state = PendingTxState::GettingBlockNumber(fut, receipt.take());
|
||||||
ctx.waker().wake_by_ref();
|
ctx.waker().wake_by_ref();
|
||||||
}
|
}
|
||||||
PendingTxState::GettingBlockNumber(fut, receipt) => {
|
PendingTxState::GettingBlockNumber(fut, receipt) => {
|
||||||
|
let current_block = futures_util::ready!(fut.as_mut().poll(ctx))?;
|
||||||
|
|
||||||
|
// This is safe so long as we only enter the `GettingBlock`
|
||||||
|
// loop from `CheckingReceipt`, which contains an explicit
|
||||||
|
// `is_none` check
|
||||||
|
let receipt = receipt.take().expect("GettingBlockNumber without receipt");
|
||||||
|
|
||||||
// Wait for the interval
|
// Wait for the interval
|
||||||
let inclusion_block = receipt
|
let inclusion_block = receipt
|
||||||
.block_number
|
.block_number
|
||||||
.expect("Receipt did not have a block number. This should never happen");
|
.expect("Receipt did not have a block number. This should never happen");
|
||||||
|
|
||||||
let current_block = futures_util::ready!(fut.as_mut().poll(ctx))?;
|
|
||||||
|
|
||||||
// if the transaction has at least K confirmations, return the receipt
|
// if the transaction has at least K confirmations, return the receipt
|
||||||
// (subtract 1 since the tx already has 1 conf when it's mined)
|
// (subtract 1 since the tx already has 1 conf when it's mined)
|
||||||
if current_block > inclusion_block + *this.confirmations - 1 {
|
if current_block > inclusion_block + *this.confirmations - 1 {
|
||||||
let receipt = *receipt.clone();
|
let receipt = Some(receipt);
|
||||||
*this.state = PendingTxState::Completed;
|
*this.state = PendingTxState::Completed;
|
||||||
return Poll::Ready(Ok(receipt));
|
return Poll::Ready(Ok(receipt));
|
||||||
} else {
|
} else {
|
||||||
tracing::trace!(tx_hash = ?this.tx_hash, "confirmations {}/{}", current_block - inclusion_block + 1, this.confirmations);
|
tracing::trace!(tx_hash = ?this.tx_hash, "confirmations {}/{}", current_block - inclusion_block + 1, this.confirmations);
|
||||||
*this.state = PendingTxState::PausedGettingBlockNumber(receipt.clone());
|
*this.state = PendingTxState::PausedGettingBlockNumber(Some(receipt));
|
||||||
ctx.waker().wake_by_ref();
|
ctx.waker().wake_by_ref();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,22 +235,28 @@ impl<'a, P> Deref for PendingTransaction<'a, P> {
|
||||||
|
|
||||||
// We box the TransactionReceipts to keep the enum small.
|
// We box the TransactionReceipts to keep the enum small.
|
||||||
enum PendingTxState<'a> {
|
enum PendingTxState<'a> {
|
||||||
|
/// Waiting for interval to elapse before calling API again
|
||||||
|
PausedGettingTx,
|
||||||
|
|
||||||
|
/// Polling The blockchain to see if the Tx has confirmed or dropped
|
||||||
|
GettingTx(PinBoxFut<'a, Option<Transaction>>),
|
||||||
|
|
||||||
/// Waiting for interval to elapse before calling API again
|
/// Waiting for interval to elapse before calling API again
|
||||||
PausedGettingReceipt,
|
PausedGettingReceipt,
|
||||||
|
|
||||||
/// Polling the blockchain for the receipt
|
/// Polling the blockchain for the receipt
|
||||||
GettingReceipt(PinBoxFut<'a, Option<TransactionReceipt>>),
|
GettingReceipt(PinBoxFut<'a, Option<TransactionReceipt>>),
|
||||||
|
|
||||||
/// Waiting for interval to elapse before calling API again
|
|
||||||
PausedGettingBlockNumber(Box<TransactionReceipt>),
|
|
||||||
|
|
||||||
/// Polling the blockchain for the current block number
|
|
||||||
GettingBlockNumber(PinBoxFut<'a, U64>, Box<TransactionReceipt>),
|
|
||||||
|
|
||||||
/// If the pending tx required only 1 conf, it will return early. Otherwise it will
|
/// If the pending tx required only 1 conf, it will return early. Otherwise it will
|
||||||
/// proceed to the next state which will poll the block number until there have been
|
/// proceed to the next state which will poll the block number until there have been
|
||||||
/// enough confirmations
|
/// enough confirmations
|
||||||
CheckingReceipt(Box<TransactionReceipt>),
|
CheckingReceipt(Option<TransactionReceipt>),
|
||||||
|
|
||||||
|
/// Waiting for interval to elapse before calling API again
|
||||||
|
PausedGettingBlockNumber(Option<TransactionReceipt>),
|
||||||
|
|
||||||
|
/// Polling the blockchain for the current block number
|
||||||
|
GettingBlockNumber(PinBoxFut<'a, U64>, Option<TransactionReceipt>),
|
||||||
|
|
||||||
/// Future has completed and should panic if polled again
|
/// Future has completed and should panic if polled again
|
||||||
Completed,
|
Completed,
|
||||||
|
@ -194,8 +265,10 @@ enum PendingTxState<'a> {
|
||||||
impl<'a> fmt::Debug for PendingTxState<'a> {
|
impl<'a> fmt::Debug for PendingTxState<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let state = match self {
|
let state = match self {
|
||||||
PendingTxState::GettingReceipt(_) => "GettingReceipt",
|
PendingTxState::PausedGettingTx => "PausedGettingTx",
|
||||||
|
PendingTxState::GettingTx(_) => "GettingTx",
|
||||||
PendingTxState::PausedGettingReceipt => "PausedGettingReceipt",
|
PendingTxState::PausedGettingReceipt => "PausedGettingReceipt",
|
||||||
|
PendingTxState::GettingReceipt(_) => "GettingReceipt",
|
||||||
PendingTxState::GettingBlockNumber(_, _) => "GettingBlockNumber",
|
PendingTxState::GettingBlockNumber(_, _) => "GettingBlockNumber",
|
||||||
PendingTxState::PausedGettingBlockNumber(_) => "PausedGettingBlockNumber",
|
PendingTxState::PausedGettingBlockNumber(_) => "PausedGettingBlockNumber",
|
||||||
PendingTxState::CheckingReceipt(_) => "CheckingReceipt",
|
PendingTxState::CheckingReceipt(_) => "CheckingReceipt",
|
||||||
|
|
|
@ -964,7 +964,7 @@ mod tests {
|
||||||
.is_none());
|
.is_none());
|
||||||
|
|
||||||
let hash = *pending_tx;
|
let hash = *pending_tx;
|
||||||
let receipt = pending_tx.await.unwrap();
|
let receipt = pending_tx.await.unwrap().unwrap();
|
||||||
assert_eq!(receipt.transaction_hash, hash);
|
assert_eq!(receipt.transaction_hash, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -289,6 +289,7 @@ mod tests {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
.unwrap()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.fuse();
|
.fuse();
|
||||||
|
@ -361,7 +362,7 @@ mod tests {
|
||||||
|
|
||||||
let stream = TransactionStream::new(
|
let stream = TransactionStream::new(
|
||||||
&provider,
|
&provider,
|
||||||
stream::iter(txs.iter().map(|tx| tx.transaction_hash)),
|
stream::iter(txs.iter().cloned().map(|tx| tx.unwrap().transaction_hash)),
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
let res = stream
|
let res = stream
|
||||||
|
@ -374,7 +375,9 @@ mod tests {
|
||||||
assert_eq!(res.len(), txs.len());
|
assert_eq!(res.len(), txs.len());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
res.into_iter().map(|tx| tx.hash).collect::<HashSet<_>>(),
|
res.into_iter().map(|tx| tx.hash).collect::<HashSet<_>>(),
|
||||||
txs.into_iter().map(|tx| tx.transaction_hash).collect()
|
txs.into_iter()
|
||||||
|
.map(|tx| tx.unwrap().transaction_hash)
|
||||||
|
.collect()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,7 +125,7 @@ mod eth_tests {
|
||||||
let tx = TransactionRequest::new().to(who).from(who);
|
let tx = TransactionRequest::new().to(who).from(who);
|
||||||
let pending_tx = provider.send_transaction(tx, None).await.unwrap();
|
let pending_tx = provider.send_transaction(tx, None).await.unwrap();
|
||||||
let tx_hash = *pending_tx;
|
let tx_hash = *pending_tx;
|
||||||
let receipt = pending_tx.confirmations(3).await.unwrap();
|
let receipt = pending_tx.confirmations(3).await.unwrap().unwrap();
|
||||||
// got the correct receipt
|
// got the correct receipt
|
||||||
assert_eq!(receipt.transaction_hash, tx_hash);
|
assert_eq!(receipt.transaction_hash, tx_hash);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,11 @@ async fn main() -> Result<()> {
|
||||||
let tx = TransactionRequest::new().to("vitalik.eth").value(100_000);
|
let tx = TransactionRequest::new().to("vitalik.eth").value(100_000);
|
||||||
|
|
||||||
// send it!
|
// send it!
|
||||||
let receipt = client.send_transaction(tx, None).await?.await?;
|
let receipt = client
|
||||||
|
.send_transaction(tx, None)
|
||||||
|
.await?
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::format_err!("tx dropped from mempool"))?;
|
||||||
let tx = client.get_transaction(receipt.transaction_hash).await?;
|
let tx = client.get_transaction(receipt.transaction_hash).await?;
|
||||||
|
|
||||||
println!("{}", serde_json::to_string(&tx)?);
|
println!("{}", serde_json::to_string(&tx)?);
|
||||||
|
|
|
@ -22,7 +22,9 @@ async fn main() -> Result<()> {
|
||||||
let pending_tx = client.send_transaction(tx, None).await?;
|
let pending_tx = client.send_transaction(tx, None).await?;
|
||||||
|
|
||||||
// get the mined tx
|
// get the mined tx
|
||||||
let receipt = pending_tx.await?;
|
let receipt = pending_tx
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow::format_err!("tx dropped from mempool"))?;
|
||||||
let tx = client.get_transaction(receipt.transaction_hash).await?;
|
let tx = client.get_transaction(receipt.transaction_hash).await?;
|
||||||
|
|
||||||
println!("Sent tx: {}\n", serde_json::to_string(&tx)?);
|
println!("Sent tx: {}\n", serde_json::to_string(&tx)?);
|
||||||
|
|
Loading…
Reference in New Issue