1use 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#[derive(Clone, Debug)]
20#[must_use = "confirm parameters do nothing unless waited for"]
21pub struct ConfirmParams {
22 pub confirmations: usize,
29 pub poll_interval_min: Duration,
34 pub poll_interval_max: Duration,
36 pub poll_interval_factor: f32,
39 pub block_timeout: Option<usize>,
41}
42
43#[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#[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#[cfg(not(test))]
57const DEFAULT_POLL_INTERVAL_FACTOR: f32 = 1.7;
58#[cfg(test)]
59const DEFAULT_POLL_INTERVAL_FACTOR: f32 = 0.0;
60
61pub const DEFAULT_BLOCK_TIMEOUT: Option<usize> = Some(25);
63
64impl ConfirmParams {
65 pub fn mined() -> Self {
68 ConfirmParams::with_confirmations(0)
69 }
70
71 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 #[inline]
87 pub fn confirmations(mut self, confirmations: usize) -> Self {
88 self.confirmations = confirmations;
89 self
90 }
91
92 #[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 #[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 #[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 #[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 #[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
144pub 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#[derive(Debug)]
170struct ConfirmationContext<'a, T: Transport> {
171 web3: &'a Web3<T>,
172 tx: H256,
174 params: ConfirmParams,
177 starting_block: Option<U64>,
180}
181
182impl<T: Transport> ConfirmationContext<'_, T> {
183 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 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 (
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 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#[allow(clippy::large_enum_variant)]
257#[derive(Debug)]
258enum Check {
259 Confirmed(TransactionReceipt),
261 Pending(U64),
269}
270
271async 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 transport.add_response(json!("0x1"));
315 transport.add_response(json!(null));
316 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 transport.add_response(json!("0x1"));
361 transport.add_response(json!(null));
362 transport.add_response(json!("0x2"));
364 transport.add_response(json!(null));
366 transport.add_response(json!("0x3"));
368 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 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 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 transport.add_response(json!("0x1"));
436 transport.add_response(json!(null));
437 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 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 transport.add_response(json!("0x2"));
474 transport.add_response(generate_tx_receipt(hash, 1));
475 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 transport.add_response(json!("0x1"));
502 transport.add_response(json!(null));
503 transport.add_response(json!("0x4"));
505 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 transport.add_response(json!("0x1"));
529 transport.add_response(json!(null));
530 transport.add_response(json!("0x2"));
532 transport.add_response(json!("0x3"));
533 transport.add_response(generate_tx_receipt(hash, 3));
535 transport.add_response(json!("0x3"));
537 transport.add_response(json!("0x4"));
538 transport.add_response(generate_tx_receipt(hash, 4));
540 transport.add_response(json!("0x5"));
542 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 transport.add_response(json!("0x0"));
577 transport.add_response(json!(null));
578 transport.add_response(json!("0x4"));
580 transport.add_response(json!(null));
581 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}