Skip to main content

ethcontract/transaction/
confirm.rs

1//! Transaction confirmation implementation. This is a re-implementation of
2//! `web3` confirmation future to fix issues with development nodes like Ganache
3//! where the transaction gets mined right away, so waiting for 1 confirmation
4//! would require another transaction to be sent so a new block could mine.
5//! Additionally, waiting for 0 confirmations in `web3` means that the tx is
6//! just sent to the mem-pool but does not wait for it to get mined. Hopefully
7//! some of this can move upstream into the `web3` crate.
8
9use crate::errors::ExecutionError;
10use crate::transaction::TransactionResult;
11use futures_timer::Delay;
12use std::cmp::min;
13use std::time::Duration;
14use web3::api::Web3;
15use web3::types::{TransactionReceipt, H256, U64};
16use web3::Transport;
17
18/// A struct with the confirmation parameters.
19#[derive(Clone, Debug)]
20#[must_use = "confirm parameters do nothing unless waited for"]
21pub struct ConfirmParams {
22    /// The number of blocks to confirm the transaction with. This is the number
23    /// of blocks mined on top of the block where the transaction was mined.
24    /// This means that, for example, to just wait for the transaction to be
25    /// mined, then the number of confirmations should be 0. Positive non-zero
26    /// values indicate that extra blocks should be waited for on top of the
27    /// block where the transaction was mined.
28    pub confirmations: usize,
29    /// Minimal delay between consecutive `eth_blockNumber` calls.
30    /// We wait for transaction confirmation by polling node for latest
31    /// block number. We use exponential backoff to control how often
32    /// we poll the node.
33    pub poll_interval_min: Duration,
34    /// Maximal delay between consecutive `eth_blockNumber` calls.
35    pub poll_interval_max: Duration,
36    /// Factor, by which the delay between consecutive `eth_blockNumber`
37    /// calls is multiplied after each call.
38    pub poll_interval_factor: f32,
39    /// The maximum number of blocks to wait for a transaction to get confirmed.
40    pub block_timeout: Option<usize>,
41}
42
43/// Default minimal delay between polling the node for transaction confirmation.
44#[cfg(not(test))]
45const DEFAULT_POLL_INTERVAL_MIN: Duration = Duration::from_millis(250);
46#[cfg(test)]
47const DEFAULT_POLL_INTERVAL_MIN: Duration = Duration::from_millis(0);
48
49/// Default maximal delay between polling the node for transaction confirmation.
50#[cfg(not(test))]
51const DEFAULT_POLL_INTERVAL_MAX: Duration = Duration::from_millis(7000);
52#[cfg(test)]
53const DEFAULT_POLL_INTERVAL_MAX: Duration = Duration::from_millis(0);
54
55/// Default factor for increasing delays between node polls.
56#[cfg(not(test))]
57const DEFAULT_POLL_INTERVAL_FACTOR: f32 = 1.7;
58#[cfg(test)]
59const DEFAULT_POLL_INTERVAL_FACTOR: f32 = 0.0;
60
61/// The default block timeout to use for confirming transactions.
62pub const DEFAULT_BLOCK_TIMEOUT: Option<usize> = Some(25);
63
64impl ConfirmParams {
65    /// Create new confirmation parameters for just confirming that the
66    /// transaction was mined but not confirmed with any extra blocks.
67    pub fn mined() -> Self {
68        ConfirmParams::with_confirmations(0)
69    }
70
71    /// Create new confirmation parameters from the specified number of extra
72    /// blocks to wait for with the default poll interval.
73    pub fn with_confirmations(count: usize) -> Self {
74        ConfirmParams {
75            confirmations: count,
76            poll_interval_min: DEFAULT_POLL_INTERVAL_MIN,
77            poll_interval_max: DEFAULT_POLL_INTERVAL_MAX,
78            poll_interval_factor: DEFAULT_POLL_INTERVAL_FACTOR,
79            block_timeout: DEFAULT_BLOCK_TIMEOUT,
80        }
81    }
82
83    /// Set new value for [`confirmations`].
84    ///
85    /// [`confirmations`]: #structfield.confirmations
86    #[inline]
87    pub fn confirmations(mut self, confirmations: usize) -> Self {
88        self.confirmations = confirmations;
89        self
90    }
91
92    /// Set new values for exponential backoff settings.
93    #[inline]
94    pub fn poll_interval(mut self, min: Duration, max: Duration, factor: f32) -> Self {
95        self.poll_interval_min = min;
96        self.poll_interval_max = max;
97        self.poll_interval_factor = factor;
98        self
99    }
100
101    /// Set new value for [`poll_interval_min`].
102    ///
103    /// [`poll_interval_min`]: #structfield.poll_interval_min
104    #[inline]
105    pub fn poll_interval_min(mut self, poll_interval_min: Duration) -> Self {
106        self.poll_interval_min = poll_interval_min;
107        self
108    }
109
110    /// Set new value for [`poll_interval_max`].
111    ///
112    /// [`poll_interval_max`]: #structfield.poll_interval_max
113    #[inline]
114    pub fn poll_interval_max(mut self, poll_interval_max: Duration) -> Self {
115        self.poll_interval_max = poll_interval_max;
116        self
117    }
118
119    /// Set new value for [`poll_interval_factor`].
120    ///
121    /// [`poll_interval_factor`]: #structfield.poll_interval_factor
122    #[inline]
123    pub fn poll_interval_factor(mut self, poll_interval_factor: f32) -> Self {
124        self.poll_interval_factor = poll_interval_factor;
125        self
126    }
127
128    /// Set new value for [`block_timeout`].
129    ///
130    /// [`block_timeout`]: #structfield.block_timeout
131    #[inline]
132    pub fn block_timeout(mut self, block_timeout: Option<usize>) -> Self {
133        self.block_timeout = block_timeout;
134        self
135    }
136}
137
138impl Default for ConfirmParams {
139    fn default() -> Self {
140        ConfirmParams::mined()
141    }
142}
143
144/// Waits for a transaction to be confirmed.
145pub async fn wait_for_confirmation<T: Transport>(
146    web3: &Web3<T>,
147    tx: H256,
148    params: ConfirmParams,
149) -> Result<TransactionReceipt, ExecutionError> {
150    let mut latest_block = None;
151    let mut context = ConfirmationContext {
152        web3,
153        tx,
154        params,
155        starting_block: None,
156    };
157
158    loop {
159        let target_block = match context.check(latest_block).await? {
160            Check::Confirmed(tx) => return Ok(tx),
161            Check::Pending(target_block) => target_block,
162        };
163
164        latest_block = Some(context.wait_for_blocks(target_block).await?);
165    }
166}
167
168/// The state used for waiting for a transaction confirmation.
169#[derive(Debug)]
170struct ConfirmationContext<'a, T: Transport> {
171    web3: &'a Web3<T>,
172    /// The transaction hash that is being confirmed.
173    tx: H256,
174    /// The confirmation parameters (like number of confirming blocks to wait
175    /// for and polling interval).
176    params: ConfirmParams,
177    /// The current block number when confirmation started. This is used for
178    /// timeouts.
179    starting_block: Option<U64>,
180}
181
182impl<T: Transport> ConfirmationContext<'_, T> {
183    /// Checks if the transaction is confirmed.
184    ///
185    /// Accepts an optional block number parameter to avoid re-querying the
186    /// current block if it is already known.
187    async fn check(&mut self, latest_block: Option<U64>) -> Result<Check, ExecutionError> {
188        let latest_block = match latest_block {
189            Some(value) => value,
190            None => self.web3.eth().block_number().await?,
191        };
192        let tx = self.web3.eth().transaction_receipt(self.tx).await?;
193
194        let (target_block, tx_result) = match tx.and_then(|tx| Some((tx.block_number?, tx))) {
195            Some((tx_block, tx)) => {
196                let target_block = tx_block + self.params.confirmations;
197
198                // This happens in two cases:
199                // - we don't need additional confirmation, transaction receipt is enough,
200                // - the transaction was mined before we queried `latest_block`, thus
201                //   `latest_block >= tx_block`.
202                if latest_block >= target_block || self.params.confirmations == 0 {
203                    return Ok(Check::Confirmed(tx));
204                }
205
206                (target_block, TransactionResult::Receipt(tx))
207            }
208            None => {
209                // We know that transaction was not mined at block `latest_block` because
210                // we've fetched `latest_block` before we've fetched transaction receipt.
211                // Thus, we need to wait at least one block after the `latest_block`,
212                // and then `self.params.confirmations` blocks on top of that.
213                (
214                    latest_block + self.params.confirmations + 1,
215                    TransactionResult::Hash(self.tx),
216                )
217            }
218        };
219
220        if let Some(block_timeout) = self.params.block_timeout {
221            let starting_block = *self.starting_block.get_or_insert(latest_block);
222            let remaining_blocks = target_block.saturating_sub(starting_block);
223
224            if remaining_blocks > U64::from(block_timeout) {
225                return Err(ExecutionError::ConfirmTimeout(Box::new(tx_result)));
226            }
227        }
228
229        Ok(Check::Pending(target_block))
230    }
231
232    /// Waits for blocks to be mined. This method polls the latest block number
233    /// and waits till the target block number is reached.
234    ///
235    /// This method returns the latest block number if it is known.
236    async fn wait_for_blocks(&self, target_block: U64) -> Result<U64, ExecutionError> {
237        let mut cur_delay = self.params.poll_interval_min;
238
239        loop {
240            delay(cur_delay).await;
241
242            let latest_block = self.web3.eth().block_number().await?;
243            if target_block <= latest_block {
244                break Ok(latest_block);
245            }
246
247            cur_delay = min(
248                cur_delay.mul_f32(self.params.poll_interval_factor),
249                self.params.poll_interval_max,
250            );
251        }
252    }
253}
254
255/// The result of checking a transaction confirmation.
256#[allow(clippy::large_enum_variant)]
257#[derive(Debug)]
258enum Check {
259    /// The transaction is confirmed with a transaction receipt.
260    Confirmed(TransactionReceipt),
261    /// The transaction is not yet confirmed, and requires additional block
262    /// confirmations.
263    ///
264    /// Contains estimated target block after which the transaction
265    /// should be mined and confirmed. Note that waiting for that block does
266    /// not guarantee that the transaction is confirmed. An additional
267    /// check is required.
268    Pending(U64),
269}
270
271/// Create a new delay that may resolve immediately when delayed for a zero
272/// duration.
273///
274/// This method is used so that unit tests resolve immediately, as the `Delay`
275/// future always returns `Poll::Pending` at least once, even with a delay or
276/// zero.
277async fn delay(duration: Duration) {
278    const ZERO_DURATION: Duration = Duration::from_secs(0);
279
280    if duration != ZERO_DURATION {
281        Delay::new(duration).await;
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::test::prelude::*;
289    use serde_json::Value;
290    use web3::types::H2048;
291
292    fn generate_tx_receipt<U: Into<U64>>(hash: H256, block_num: U) -> Value {
293        json!({
294            "transactionHash": hash,
295            "transactionIndex": "0x1",
296            "blockNumber": block_num.into(),
297            "blockHash": H256::zero(),
298            "cumulativeGasUsed": "0x1337",
299            "gasUsed": "0x1337",
300            "logsBloom": H2048::zero(),
301            "logs": [],
302            "effectiveGasPrice": "0x0",
303        })
304    }
305
306    #[test]
307    fn confirm_mined_transaction() {
308        let mut transport = TestTransport::new();
309        let web3 = Web3::new(transport.clone());
310
311        let hash = H256::repeat_byte(0xff);
312
313        // transaction pending
314        transport.add_response(json!("0x1"));
315        transport.add_response(json!(null));
316        // poll for one block
317        transport.add_response(json!("0x2"));
318        transport.add_response(generate_tx_receipt(hash, 2));
319
320        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
321            .wait()
322            .expect("transaction confirmation failed");
323
324        assert_eq!(confirm.transaction_hash, hash);
325        transport.assert_request("eth_blockNumber", &[]);
326        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
327        transport.assert_request("eth_blockNumber", &[]);
328        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
329        transport.assert_no_more_requests();
330    }
331
332    #[test]
333    fn confirm_auto_mined_transaction() {
334        let mut transport = TestTransport::new();
335        let web3 = Web3::new(transport.clone());
336
337        let hash = H256::repeat_byte(0xff);
338
339        transport.add_response(json!("0x1"));
340        transport.add_response(generate_tx_receipt(hash, 1));
341
342        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
343            .immediate()
344            .expect("transaction confirmation failed");
345
346        assert_eq!(confirm.transaction_hash, hash);
347        transport.assert_request("eth_blockNumber", &[]);
348        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
349        transport.assert_no_more_requests();
350    }
351
352    #[test]
353    fn confirm_mined_transaction_when_mining_is_delayed() {
354        let mut transport = TestTransport::new();
355        let web3 = Web3::new(transport.clone());
356
357        let hash = H256::repeat_byte(0xff);
358
359        // transaction pending
360        transport.add_response(json!("0x1"));
361        transport.add_response(json!(null));
362        // poll for one block
363        transport.add_response(json!("0x2"));
364        // transaction still not mined
365        transport.add_response(json!(null));
366        // poll for one more block
367        transport.add_response(json!("0x3"));
368        // now it's mined
369        transport.add_response(generate_tx_receipt(hash, 2));
370
371        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
372            .wait()
373            .expect("transaction confirmation failed");
374
375        assert_eq!(confirm.transaction_hash, hash);
376        transport.assert_request("eth_blockNumber", &[]);
377        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
378        transport.assert_request("eth_blockNumber", &[]);
379        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
380        transport.assert_request("eth_blockNumber", &[]);
381        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
382        transport.assert_no_more_requests();
383    }
384
385    #[test]
386    fn confirm_mined_transaction_when_mining_is_ahead_of_us() {
387        let mut transport = TestTransport::new();
388        let web3 = Web3::new(transport.clone());
389
390        let hash = H256::repeat_byte(0xff);
391
392        // current block is 2, tx was mined on block 1
393        transport.add_response(json!("0x2"));
394        transport.add_response(generate_tx_receipt(hash, 1));
395
396        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::mined())
397            .immediate()
398            .expect("transaction confirmation failed");
399
400        assert_eq!(confirm.transaction_hash, hash);
401        transport.assert_request("eth_blockNumber", &[]);
402        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
403        transport.assert_no_more_requests();
404    }
405
406    #[test]
407    fn confirmations_when_mining_is_way_ahead_of_us() {
408        let mut transport = TestTransport::new();
409        let web3 = Web3::new(transport.clone());
410
411        let hash = H256::repeat_byte(0xff);
412
413        // current block is 3, tx was mined on block 1, so we can confirm it
414        transport.add_response(json!("0x3"));
415        transport.add_response(generate_tx_receipt(hash, 1));
416
417        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(2))
418            .immediate()
419            .expect("transaction confirmation failed");
420
421        assert_eq!(confirm.transaction_hash, hash);
422        transport.assert_request("eth_blockNumber", &[]);
423        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
424        transport.assert_no_more_requests();
425    }
426
427    #[test]
428    fn confirmations_with_polling() {
429        let mut transport = TestTransport::new();
430        let web3 = Web3::new(transport.clone());
431
432        let hash = H256::repeat_byte(0xff);
433
434        // transaction pending
435        transport.add_response(json!("0x1"));
436        transport.add_response(json!(null));
437        // poll block number until new block is found
438        transport.add_response(json!("0x1"));
439        transport.add_response(json!("0x1"));
440        transport.add_response(json!("0x2"));
441        transport.add_response(json!("0x2"));
442        transport.add_response(json!("0x2"));
443        transport.add_response(json!("0x3"));
444        // check transaction was mined - note that the block number doesn't get
445        // re-queried and is re-used from the polling loop.
446        transport.add_response(generate_tx_receipt(hash, 2));
447
448        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(1))
449            .wait()
450            .expect("transaction confirmation failed");
451
452        assert_eq!(confirm.transaction_hash, hash);
453        transport.assert_request("eth_blockNumber", &[]);
454        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
455        transport.assert_request("eth_blockNumber", &[]);
456        transport.assert_request("eth_blockNumber", &[]);
457        transport.assert_request("eth_blockNumber", &[]);
458        transport.assert_request("eth_blockNumber", &[]);
459        transport.assert_request("eth_blockNumber", &[]);
460        transport.assert_request("eth_blockNumber", &[]);
461        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
462        transport.assert_no_more_requests();
463    }
464
465    #[test]
466    fn confirmations_with_polling_when_mining_is_slightly_ahead_of_us() {
467        let mut transport = TestTransport::new();
468        let web3 = Web3::new(transport.clone());
469
470        let hash = H256::repeat_byte(0xff);
471
472        // current block is 2, tx was mined on block 1
473        transport.add_response(json!("0x2"));
474        transport.add_response(generate_tx_receipt(hash, 1));
475        // still waiting for one more block
476        transport.add_response(json!("0x2"));
477        transport.add_response(json!("0x3"));
478        transport.add_response(generate_tx_receipt(hash, 1));
479
480        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(2))
481            .immediate()
482            .expect("transaction confirmation failed");
483
484        assert_eq!(confirm.transaction_hash, hash);
485        transport.assert_request("eth_blockNumber", &[]);
486        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
487        transport.assert_request("eth_blockNumber", &[]);
488        transport.assert_request("eth_blockNumber", &[]);
489        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
490        transport.assert_no_more_requests();
491    }
492
493    #[test]
494    fn confirmations_with_polling_and_skipped_blocks() {
495        let mut transport = TestTransport::new();
496        let web3 = Web3::new(transport.clone());
497
498        let hash = H256::repeat_byte(0xff);
499
500        // transaction pending
501        transport.add_response(json!("0x1"));
502        transport.add_response(json!(null));
503        // poll block number which skipped 2
504        transport.add_response(json!("0x4"));
505        // check transaction was mined (`eth_blockNumber` request is reused)
506        transport.add_response(generate_tx_receipt(hash, 2));
507
508        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(1))
509            .immediate()
510            .expect("transaction confirmation failed");
511
512        assert_eq!(confirm.transaction_hash, hash);
513        transport.assert_request("eth_blockNumber", &[]);
514        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
515        transport.assert_request("eth_blockNumber", &[]);
516        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
517        transport.assert_no_more_requests();
518    }
519
520    #[test]
521    fn confirmations_with_polling_reorg_tx_receipt() {
522        let mut transport = TestTransport::new();
523        let web3 = Web3::new(transport.clone());
524
525        let hash = H256::repeat_byte(0xff);
526
527        // transaction pending
528        transport.add_response(json!("0x1"));
529        transport.add_response(json!(null));
530        // poll for 2 blocks
531        transport.add_response(json!("0x2"));
532        transport.add_response(json!("0x3"));
533        // check confirmation again - transaction mined on block 3
534        transport.add_response(generate_tx_receipt(hash, 3));
535        // needs to wait 1 more block
536        transport.add_response(json!("0x3"));
537        transport.add_response(json!("0x4"));
538        // check confirmation - reorg happened, tx mined on block 4!
539        transport.add_response(generate_tx_receipt(hash, 4));
540        // wait for another block
541        transport.add_response(json!("0x5"));
542        // check confirmation - and we are satisfied.
543        transport.add_response(generate_tx_receipt(hash, 4));
544
545        let confirm = wait_for_confirmation(&web3, hash, ConfirmParams::with_confirmations(1))
546            .wait()
547            .expect("transaction confirmation failed");
548
549        assert_eq!(confirm.transaction_hash, hash);
550        transport.assert_request("eth_blockNumber", &[]);
551        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
552        transport.assert_request("eth_blockNumber", &[]);
553        transport.assert_request("eth_blockNumber", &[]);
554        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
555        transport.assert_request("eth_blockNumber", &[]);
556        transport.assert_request("eth_blockNumber", &[]);
557        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
558        transport.assert_request("eth_blockNumber", &[]);
559        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
560        transport.assert_no_more_requests();
561    }
562
563    #[test]
564    fn confirmation_timeout() {
565        let mut transport = TestTransport::new();
566        let web3 = Web3::new(transport.clone());
567
568        let hash = H256::repeat_byte(0xff);
569        let params = ConfirmParams {
570            confirmations: 3,
571            block_timeout: Some(10),
572            ..Default::default()
573        };
574
575        // Initial check
576        transport.add_response(json!("0x0"));
577        transport.add_response(json!(null));
578        // Check again, at block 4
579        transport.add_response(json!("0x4"));
580        transport.add_response(json!(null));
581        // Wait for more blocks
582        // Final check at block 8, since the earliest the transaction can be
583        // confirmed is at block 12 which is past the block timeout.
584        transport.add_response(json!("0x8"));
585        transport.add_response(json!(null));
586
587        let confirm = wait_for_confirmation(&web3, hash, params).wait();
588
589        assert!(
590            match &confirm {
591                Err(ExecutionError::ConfirmTimeout(tx)) => tx.is_hash(),
592                _ => false,
593            },
594            "expected confirmation to time out but got {:?}",
595            confirm
596        );
597
598        transport.assert_request("eth_blockNumber", &[]);
599        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
600        transport.assert_request("eth_blockNumber", &[]);
601        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
602        transport.assert_request("eth_blockNumber", &[]);
603        transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]);
604        transport.assert_no_more_requests();
605    }
606}