use crate::errors::ExecutionError;
use crate::transaction::TransactionResult;
use futures_timer::Delay;
use std::cmp::min;
use std::time::Duration;
use web3::api::Web3;
use web3::types::{TransactionReceipt, H256, U64};
use web3::Transport;
#[derive(Clone, Debug)]
#[must_use = "confirm parameters do nothing unless waited for"]
pub struct ConfirmParams {
pub confirmations: usize,
pub poll_interval_min: Duration,
pub poll_interval_max: Duration,
pub poll_interval_factor: f32,
pub block_timeout: Option<usize>,
}
#[cfg(not(test))]
const DEFAULT_POLL_INTERVAL_MIN: Duration = Duration::from_millis(250);
#[cfg(test)]
const DEFAULT_POLL_INTERVAL_MIN: Duration = Duration::from_millis(0);
#[cfg(not(test))]
const DEFAULT_POLL_INTERVAL_MAX: Duration = Duration::from_millis(7000);
#[cfg(test)]
const DEFAULT_POLL_INTERVAL_MAX: Duration = Duration::from_millis(0);
#[cfg(not(test))]
const DEFAULT_POLL_INTERVAL_FACTOR: f32 = 1.7;
#[cfg(test)]
const DEFAULT_POLL_INTERVAL_FACTOR: f32 = 0.0;
pub const DEFAULT_BLOCK_TIMEOUT: Option<usize> = Some(25);
impl ConfirmParams {
pub fn mined() -> Self {
ConfirmParams::with_confirmations(0)
}
pub fn with_confirmations(count: usize) -> Self {
ConfirmParams {
confirmations: count,
poll_interval_min: DEFAULT_POLL_INTERVAL_MIN,
poll_interval_max: DEFAULT_POLL_INTERVAL_MAX,
poll_interval_factor: DEFAULT_POLL_INTERVAL_FACTOR,
block_timeout: DEFAULT_BLOCK_TIMEOUT,
}
}
#[inline]
pub fn confirmations(mut self, confirmations: usize) -> Self {
self.confirmations = confirmations;
self
}
#[inline]
pub fn poll_interval(mut self, min: Duration, max: Duration, factor: f32) -> Self {
self.poll_interval_min = min;
self.poll_interval_max = max;
self.poll_interval_factor = factor;
self
}
#[inline]
pub fn poll_interval_min(mut self, poll_interval_min: Duration) -> Self {
self.poll_interval_min = poll_interval_min;
self
}
#[inline]
pub fn poll_interval_max(mut self, poll_interval_max: Duration) -> Self {
self.poll_interval_max = poll_interval_max;
self
}
#[inline]
pub fn poll_interval_factor(mut self, poll_interval_factor: f32) -> Self {
self.poll_interval_factor = poll_interval_factor;
self
}
#[inline]
pub fn block_timeout(mut self, block_timeout: Option<usize>) -> Self {
self.block_timeout = block_timeout;
self
}
}
impl Default for ConfirmParams {
fn default() -> Self {
ConfirmParams::mined()
}
}
pub async fn wait_for_confirmation<T: Transport>(
web3: &Web3<T>,
tx: H256,
params: ConfirmParams,
) -> Result<TransactionReceipt, ExecutionError> {
let mut latest_block = None;
let mut context = ConfirmationContext {
web3,
tx,
params,
starting_block: None,
};
loop {
let target_block = match context.check(latest_block).await? {
Check::Confirmed(tx) => return Ok(tx),
Check::Pending(target_block) => target_block,
};
latest_block = Some(context.wait_for_blocks(target_block).await?);
}
}
#[derive(Debug)]
struct ConfirmationContext<'a, T: Transport> {
web3: &'a Web3<T>,
tx: H256,
params: ConfirmParams,
starting_block: Option<U64>,
}
impl<T: Transport> ConfirmationContext<'_, T> {
async fn check(&mut self, latest_block: Option<U64>) -> Result<Check, ExecutionError> {
let latest_block = match latest_block {
Some(value) => value,
None => self.web3.eth().block_number().await?,
};
let tx = self.web3.eth().transaction_receipt(self.tx).await?;
let (target_block, tx_result) = match tx.and_then(|tx| Some((tx.block_number?, tx))) {
Some((tx_block, tx)) => {
let target_block = tx_block + self.params.confirmations;
if latest_block >= target_block || self.params.confirmations == 0 {
return Ok(Check::Confirmed(tx));
}
(target_block, TransactionResult::Receipt(tx))
}
None => {
(
latest_block + self.params.confirmations + 1,
TransactionResult::Hash(self.tx),
)
}
};
if let Some(block_timeout) = self.params.block_timeout {
let starting_block = *self.starting_block.get_or_insert(latest_block);
let remaining_blocks = target_block.saturating_sub(starting_block);
if remaining_blocks > U64::from(block_timeout) {
return Err(ExecutionError::ConfirmTimeout(Box::new(tx_result)));
}
}
Ok(Check::Pending(target_block))
}
async fn wait_for_blocks(&self, target_block: U64) -> Result<U64, ExecutionError> {
let mut cur_delay = self.params.poll_interval_min;
loop {
delay(cur_delay).await;
let latest_block = self.web3.eth().block_number().await?;
if target_block <= latest_block {
break Ok(latest_block);
}
cur_delay = min(
cur_delay.mul_f32(self.params.poll_interval_factor),
self.params.poll_interval_max,
);
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
enum Check {
Confirmed(TransactionReceipt),
Pending(U64),
}
async fn delay(duration: Duration) {
const ZERO_DURATION: Duration = Duration::from_secs(0);
if duration != ZERO_DURATION {
Delay::new(duration).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::prelude::*;
use serde_json::Value;
use web3::types::H2048;
fn generate_tx_receipt<U: Into<U64>>(hash: H256, block_num: U) -> Value {
json!({
"transactionHash": hash,
"transactionIndex": "0x1",
"blockNumber": block_num.into(),
"blockHash": H256::zero(),
"cumulativeGasUsed": "0x1337",
"gasUsed": "0x1337",
"logsBloom": H2048::zero(),
"logs": [],
"effectiveGasPrice": "0x0",
})
}
#[test]
fn confirm_mined_transaction() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x1"));
transport.add_response(json!(null));
transport.add_response(json!("0x2"));
transport.add_response(generate_tx_receipt(hash, 2));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
.wait()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirm_auto_mined_transaction() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x1"));
transport.add_response(generate_tx_receipt(hash, 1));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
.immediate()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirm_mined_transaction_when_mining_is_delayed() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x1"));
transport.add_response(json!(null));
transport.add_response(json!("0x2"));
transport.add_response(json!(null));
transport.add_response(json!("0x3"));
transport.add_response(generate_tx_receipt(hash, 2));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
.wait()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirm_mined_transaction_when_mining_is_ahead_of_us() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x2"));
transport.add_response(generate_tx_receipt(hash, 1));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
.immediate()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirmations_when_mining_is_way_ahead_of_us() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x3"));
transport.add_response(generate_tx_receipt(hash, 1));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(2))
.immediate()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirmations_with_polling() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x1"));
transport.add_response(json!(null));
transport.add_response(json!("0x1"));
transport.add_response(json!("0x1"));
transport.add_response(json!("0x2"));
transport.add_response(json!("0x2"));
transport.add_response(json!("0x2"));
transport.add_response(json!("0x3"));
transport.add_response(generate_tx_receipt(hash, 2));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(1))
.wait()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirmations_with_polling_when_mining_is_slightly_ahead_of_us() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x2"));
transport.add_response(generate_tx_receipt(hash, 1));
transport.add_response(json!("0x2"));
transport.add_response(json!("0x3"));
transport.add_response(generate_tx_receipt(hash, 1));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(2))
.immediate()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirmations_with_polling_and_skipped_blocks() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x1"));
transport.add_response(json!(null));
transport.add_response(json!("0x4"));
transport.add_response(generate_tx_receipt(hash, 2));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(1))
.immediate()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirmations_with_polling_reorg_tx_receipt() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
transport.add_response(json!("0x1"));
transport.add_response(json!(null));
transport.add_response(json!("0x2"));
transport.add_response(json!("0x3"));
transport.add_response(generate_tx_receipt(hash, 3));
transport.add_response(json!("0x3"));
transport.add_response(json!("0x4"));
transport.add_response(generate_tx_receipt(hash, 4));
transport.add_response(json!("0x5"));
transport.add_response(generate_tx_receipt(hash, 4));
let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(1))
.wait()
.expect("transaction confirmation failed");
assert_eq!(confirm.transaction_hash, hash);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
#[test]
fn confirmation_timeout() {
let mut transport = TestTransport::new();
let web3 = Web3::new(transport.clone());
let hash = H256::repeat_byte(0xff);
let params = ConfirmParams {
confirmations: 3,
block_timeout: Some(10),
..Default::default()
};
transport.add_response(json!("0x0"));
transport.add_response(json!(null));
transport.add_response(json!("0x4"));
transport.add_response(json!(null));
transport.add_response(json!("0x8"));
transport.add_response(json!(null));
let confirm = wait_for_confirmation(&web3, hash, params).wait();
assert!(
match &confirm {
Err(ExecutionError::ConfirmTimeout(tx)) => tx.is_hash(),
_ => false,
},
"expected confirmation to time out but got {:?}",
confirm
);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_request("eth_blockNumber", &[]);
transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
transport.assert_no_more_requests();
}
}