diff --git a/Cargo.toml b/Cargo.toml index 62544858..0ce29694 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ ethcontract-common = { version = "0.2.0", path = "./common" } ethcontract-derive = { version = "0.2.0", path = "./derive" } ethsign = "0.7" futures = { version = "0.3", features = ["compat"] } +futures-timer = "2.0" jsonrpc-core = "11.0" lazy_static = "1.4" rlp = "0.4" diff --git a/examples/async.rs b/examples/async.rs index 97e49da6..aa47cb12 100644 --- a/examples/async.rs +++ b/examples/async.rs @@ -24,7 +24,6 @@ async fn run() { let instance = RustCoin::builder(&web3) .gas(4_712_388.into()) - .confirmations(0) .deploy() .await .expect("deployment failed"); diff --git a/examples/generate/src/main.rs b/examples/generate/src/main.rs index b5d2d027..7adfa735 100644 --- a/examples/generate/src/main.rs +++ b/examples/generate/src/main.rs @@ -14,7 +14,6 @@ async fn run() { let instance = RustCoin::builder(&web3) .gas(4_712_388.into()) - .confirmations(0) .deploy() .await .expect("deployment failed"); diff --git a/examples/linked.rs b/examples/linked.rs index a98c23e2..69a45a4c 100644 --- a/examples/linked.rs +++ b/examples/linked.rs @@ -15,13 +15,11 @@ async fn run() { let library = SimpleLibrary::builder(&web3) .gas(4_712_388.into()) - .confirmations(0) .deploy() .await .expect("library deployment failure"); let instance = LinkedContract::builder(&web3, library.address(), 1337.into()) .gas(4_712_388.into()) - .confirmations(0) .deploy() .await .expect("contract deployment failure"); diff --git a/examples/rinkeby.rs b/examples/rinkeby.rs index f7eabbdd..f3b6740a 100644 --- a/examples/rinkeby.rs +++ b/examples/rinkeby.rs @@ -2,7 +2,6 @@ use ethcontract::web3::api::Web3; use ethcontract::web3::transports::WebSocket; use ethcontract::{Account, SecretKey, H256}; use std::env; -use std::time::Duration; ethcontract::contract!("examples/truffle/build/contracts/DeployedContract.json"); @@ -24,7 +23,9 @@ async fn run() { format!("wss://rinkeby.infura.io/ws/v3/{}", project_id) }; - // use a WebSocket transport to support confirmations + // NOTE: Use a WebSocket transport for `eth_newBlockFilter` support on + // Infura, filters are disabled over HTTPS. Filters are needed for + // confirmation support. let (eloop, ws) = WebSocket::new(&infura_url).expect("transport failed"); eloop.into_remote(); let web3 = Web3::new(ws); @@ -46,7 +47,8 @@ async fn run() { println!(" incrementing (this may take a while)..."); instance .increment() - .send_and_confirm(Duration::new(5, 0), 1) + .confirmations(1) // wait for 1 block confirmation + .send() .await .expect("increment failed"); println!( diff --git a/src/contract.rs b/src/contract.rs index 6ac96d0d..2e0dacb6 100644 --- a/src/contract.rs +++ b/src/contract.rs @@ -15,8 +15,7 @@ use web3::Transport; pub use self::deploy::{Deploy, DeployBuilder, DeployFuture, DeployedFuture}; pub use self::method::{ - CallFuture, MethodBuilder, MethodDefaults, MethodFuture, MethodSendAndConfirmFuture, - MethodSendFuture, ViewMethodBuilder, + CallFuture, MethodBuilder, MethodDefaults, MethodFuture, MethodSendFuture, ViewMethodBuilder, }; /// Represents a contract instance at an address. Provides methods for diff --git a/src/contract/deploy.rs b/src/contract/deploy.rs index 632f08a9..1a7d7fc6 100644 --- a/src/contract/deploy.rs +++ b/src/contract/deploy.rs @@ -2,9 +2,9 @@ //! new contracts. use crate::contract::Instance; -use crate::errors::DeployError; +use crate::errors::{DeployError, ExecutionError}; use crate::future::{CompatCallFuture, Web3Unpin}; -use crate::transaction::{Account, SendAndConfirmFuture, TransactionBuilder}; +use crate::transaction::{Account, SendFuture, TransactionBuilder, TransactionResult}; use crate::truffle::abi::ErrorKind as AbiErrorKind; use crate::truffle::{Abi, Artifact}; use futures::compat::Future01CompatExt; @@ -13,7 +13,6 @@ use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; use std::task::{Context, Poll}; -use std::time::Duration; use web3::api::Web3; use web3::contract::tokens::Tokenize; use web3::types::{Address, Bytes, U256}; @@ -116,10 +115,6 @@ where abi: Abi, /// The underlying transaction used t tx: TransactionBuilder, - /// The poll interval for confirming the contract deployed. - pub poll_interval: Option, - /// The number of confirmations to wait for. - pub confirmations: Option, _deploy: PhantomData>, } @@ -157,9 +152,7 @@ where Ok(DeployBuilder { web3: web3.clone(), abi: artifact.abi, - tx: TransactionBuilder::new(web3).data(data), - poll_interval: None, - confirmations: None, + tx: TransactionBuilder::new(web3).data(data).confirmations(0), _deploy: Default::default(), }) } @@ -199,17 +192,11 @@ where self } - /// Specify the poll interval to use for confirming the deployment, if not - /// specified will use a period of 7 seconds. - pub fn poll_interval(mut self, value: Duration) -> DeployBuilder { - self.poll_interval = Some(value); - self - } - /// Specify the number of confirmations to wait for when confirming the - /// transaction, if not specified will wait for 1 confirmation. + /// transaction, if not specified will wait for the transaction to be mined + /// without any extra confirmations. pub fn confirmations(mut self, value: usize) -> DeployBuilder { - self.confirmations = Some(value); + self.tx = self.tx.confirmations(value); self } @@ -236,7 +223,7 @@ where /// The deployment args args: Option<(Web3Unpin, Abi)>, /// The future resolved when the deploy transaction is complete. - tx: Result, Option>, + send: SendFuture, _deploy: PhantomData>, } @@ -247,13 +234,9 @@ where { /// Create an instance from a `DeployBuilder`. pub fn from_builder(builder: DeployBuilder) -> DeployFuture { - // NOTE(nlordell): arbitrary default values taken from `rust-web3` - let poll_interval = builder.poll_interval.unwrap_or(Duration::from_secs(7)); - let confirmations = builder.confirmations.unwrap_or(1); - DeployFuture { args: Some((builder.web3.into(), builder.abi)), - tx: Ok(builder.tx.send_and_confirm(poll_interval, confirmations)), + send: builder.tx.send(), _deploy: Default::default(), } } @@ -268,26 +251,25 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { let unpinned = self.get_mut(); - match unpinned.tx { - Ok(ref mut tx) => { - let tx = ready!(Pin::new(tx).poll(cx).map_err(DeployError::from)); - let tx = match tx { - Ok(tx) => tx, - Err(err) => return Poll::Ready(Err(err)), - }; - let address = match tx.contract_address { - Some(address) => address, - None => { - return Poll::Ready(Err(DeployError::Failure(tx.transaction_hash))); - } - }; - - let (web3, abi) = unpinned.args.take().expect("called once"); - - Poll::Ready(Ok(D::deployed_at(web3.into(), abi, address))) + + let tx = match ready!(Pin::new(&mut unpinned.send).poll(cx)) { + Ok(TransactionResult::Receipt(tx)) => tx, + Ok(TransactionResult::Hash(tx)) => return Poll::Ready(Err(DeployError::Pending(tx))), + Err(err) => return Poll::Ready(Err(err.into())), + }; + + let address = match tx.contract_address { + Some(address) => address, + None => { + return Poll::Ready(Err(DeployError::Tx(ExecutionError::Failure( + tx.transaction_hash, + )))); } - Err(ref mut err) => Poll::Ready(Err(err.take().expect("called once"))), - } + }; + + let (web3, abi) = unpinned.args.take().expect("called more than once"); + + Poll::Ready(Ok(D::deployed_at(web3.into(), abi, address))) } } diff --git a/src/contract/method.rs b/src/contract/method.rs index d3fcf4bd..b17c5838 100644 --- a/src/contract/method.rs +++ b/src/contract/method.rs @@ -5,7 +5,7 @@ use crate::errors::{ExecutionError, MethodError}; use crate::future::CompatCallFuture; use crate::hash; -use crate::transaction::{Account, SendAndConfirmFuture, SendFuture, TransactionBuilder}; +use crate::transaction::{Account, SendFuture, TransactionBuilder}; use crate::truffle::abi::{self, Function, ParamType}; use futures::compat::Future01CompatExt; use futures::ready; @@ -14,7 +14,6 @@ use std::future::Future; use std::marker::PhantomData; use std::pin::Pin; use std::task::{Context, Poll}; -use std::time::Duration; use web3::api::Web3; use web3::contract::tokens::Detokenize; use web3::types::{Address, BlockNumber, Bytes, CallRequest, U256}; @@ -103,6 +102,14 @@ impl MethodBuilder { self } + /// Specify the number of confirmations to wait for when confirming the + /// transaction, if not specified will wait for the transaction to be mined + /// without any extra confirmations. + pub fn confirmations(mut self, value: usize) -> MethodBuilder { + self.tx = self.tx.confirmations(value); + self + } + /// Extract inner `TransactionBuilder` from this `SendBuilder`. This exposes /// `TransactionBuilder` only APIs. pub fn into_inner(self) -> TransactionBuilder { @@ -113,19 +120,6 @@ impl MethodBuilder { pub fn send(self) -> MethodSendFuture { MethodFuture::new(self.function, self.tx.send()) } - - /// Send a transaction for the method call and wait for confirmation. - /// Returns the transaction receipt for inspection. - pub fn send_and_confirm( - self, - poll_interval: Duration, - confirmations: usize, - ) -> MethodSendAndConfirmFuture { - MethodFuture::new( - self.function, - self.tx.send_and_confirm(poll_interval, confirmations), - ) - } } /// Future that wraps an inner transaction execution future to add method @@ -161,9 +155,6 @@ where /// A type alias for a `MethodFuture` wrapped `SendFuture`. pub type MethodSendFuture = MethodFuture>; -/// A type alias for a `MethodFuture` wrapped `SendAndConfirmFuture`. -pub type MethodSendAndConfirmFuture = MethodFuture>; - impl MethodBuilder { /// Demotes a `MethodBuilder` into a `ViewMethodBuilder` which has a more /// restricted API and cannot actually send transactions. diff --git a/src/errors.rs b/src/errors.rs index 4027c95b..aac08f35 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -40,9 +40,10 @@ pub enum DeployError { #[error("error executing contract deployment transaction: {0}")] Tx(#[from] ExecutionError), - /// Transaction failure (e.g. out of gas). - #[error("contract deployment transaction failed: {0}")] - Failure(H256), + /// Transaction was unable to confirm and is still pending. The contract + /// address cannot be determined. + #[error("contract deployment transaction pending: {0}")] + Pending(H256), } impl From for DeployError { @@ -78,6 +79,14 @@ pub enum ExecutionError { /// A contract call executed an invalid opcode. #[error("contract call executed an invalid opcode")] InvalidOpcode, + + /// A contract transaction failed to confirm within the block timeout limit. + #[error("transaction confirmation timed-out")] + ConfirmTimeout, + + /// Transaction failure (e.g. out of gas or revert). + #[error("transaction failed: {0}")] + Failure(H256), } impl From for ExecutionError { diff --git a/src/future.rs b/src/future.rs index 2434b8f4..695f025a 100644 --- a/src/future.rs +++ b/src/future.rs @@ -5,7 +5,6 @@ use std::ops::Deref; use std::pin::Pin; use std::task::{Context, Poll}; use web3::api::Web3; -use web3::confirm::SendTransactionWithConfirmation; use web3::helpers::CallFuture; use web3::Transport; @@ -69,6 +68,3 @@ impl Unpin for Web3Unpin {} /// Type alias for Compat01As03> since it is used a lot. pub type CompatCallFuture = Compat01As03::Out>>; - -/// Type alias for Compat01As03>. -pub type CompatSendTxWithConfirmation = Compat01As03>; diff --git a/src/transaction.rs b/src/transaction.rs index dce9a3a1..eb1b5b11 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,23 +1,25 @@ //! Implementation for setting up, signing, estimating gas and sending //! transactions on the Ethereum network. +pub mod confirm; + use crate::errors::ExecutionError; -use crate::future::{CompatCallFuture, CompatSendTxWithConfirmation, MaybeReady, Web3Unpin}; +use crate::future::{CompatCallFuture, MaybeReady, Web3Unpin}; use crate::sign::TransactionData; +use crate::transaction::confirm::{ConfirmFuture, ConfirmParams}; use ethsign::{Protected, SecretKey}; use futures::compat::Future01CompatExt; -use futures::future::{self, TryFuture, TryJoin4}; +use futures::future::{self, TryJoin4}; use futures::ready; use std::future::Future; use std::pin::Pin; use std::str; use std::task::{Context, Poll}; -use std::time::Duration; use web3::api::{Eth, Namespace, Web3}; use web3::helpers::{self, CallFuture}; use web3::types::{ Address, Bytes, CallRequest, RawTransaction, TransactionCondition, TransactionReceipt, - TransactionRequest, H256, U256, + TransactionRequest, H256, U256, U64, }; use web3::Transport; @@ -44,6 +46,83 @@ impl Account { } } +/// The condition on which a transaction's `SendFuture` gets resolved. +#[derive(Clone, Debug)] +pub enum ResolveCondition { + /// The transaction's `SendFuture` gets resolved immediately after it was + /// added to the pending transaction pool. This skips confirmation and + /// provides no guarantees that the transaction was mined or confirmed. + Pending, + /// Wait for confirmation with the specified `ConfirmParams`. A confirmed + /// transaction is always mined. There is a chance, however, that the block + /// in which the transaction was mined becomes an ommer block. Confirming + /// with a higher block count significantly decreases this probability. + /// + /// See `ConfirmParams` documentation for more details on the exact + /// semantics confirmation. + Confirmed(ConfirmParams), +} + +impl Default for ResolveCondition { + fn default() -> Self { + ResolveCondition::Confirmed(Default::default()) + } +} + +/// Represents the result of a sent transaction that can either be a transaction +/// hash, in the case the transaction was not confirmed, or a full transaction +/// receipt if the `TransactionBuilder` was configured to wait for confirmation +/// blocks. +/// +/// Note that the result will always be a `TransactionResult::Hash` if +/// `Confirm::Skip` was used and `TransactionResult::Receipt` if +/// `Confirm::Blocks` was used. +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +pub enum TransactionResult { + /// A transaction hash, this variant happens if and only if confirmation was + /// skipped. + Hash(H256), + /// A transaction receipt, this variant happens if and only if the + /// transaction was configured to wait for confirmations. + Receipt(TransactionReceipt), +} + +impl TransactionResult { + /// Returns true if the `TransactionResult` is a `Hash` variant, i.e. it is + /// only a hash and does not contain the transaction receipt. + pub fn is_hash(&self) -> bool { + match self { + TransactionResult::Hash(_) => true, + _ => false, + } + } + + /// Get the transaction hash. + pub fn hash(&self) -> H256 { + match self { + TransactionResult::Hash(hash) => *hash, + TransactionResult::Receipt(tx) => tx.transaction_hash, + } + } + + /// Returns true if the `TransactionResult` is a `Receipt` variant, i.e. the + /// transaction was confirmed and the full transaction receipt is available. + pub fn is_receipt(&self) -> bool { + self.as_receipt().is_some() + } + + /// Extract a `TransactionReceipt` from the result. This will return `None` + /// if the result is only a hash and the transaction receipt is not + /// available. + pub fn as_receipt(&self) -> Option<&TransactionReceipt> { + match self { + TransactionResult::Receipt(ref tx) => Some(tx), + _ => None, + } + } +} + /// Represents a prepared and optionally signed transaction that is ready for /// sending created by a `TransactionBuilder`. #[derive(Clone, Debug, PartialEq)] @@ -99,6 +178,9 @@ pub struct TransactionBuilder { /// Optional nonce to use. Defaults to the signing account's current /// transaction count. pub nonce: Option, + /// Optional resolve conditions. Defaults to waiting the transaction to be + /// mined without any extra confirmation blocks. + pub resolve: Option, } impl TransactionBuilder { @@ -113,6 +195,7 @@ impl TransactionBuilder { value: None, data: None, nonce: None, + resolve: None, } } @@ -165,6 +248,30 @@ impl TransactionBuilder { self } + /// Specify the resolve condition, if not specified will default to waiting + /// for the transaction to be mined (but not confirmed by any extra blocks). + pub fn resolve(mut self, value: ResolveCondition) -> TransactionBuilder { + self.resolve = Some(value); + self + } + + /// Specify the number of confirmations to use for the confirmation options. + /// This is a utility method for specifying the resolve condition. + pub fn confirmations(mut self, value: usize) -> TransactionBuilder { + self.resolve = match self.resolve { + Some(ResolveCondition::Confirmed(params)) => { + Some(ResolveCondition::Confirmed(ConfirmParams { + confirmations: value, + ..params + })) + } + _ => Some(ResolveCondition::Confirmed( + ConfirmParams::with_confirmations(value), + )), + }; + self + } + /// Estimate the gas required for this transaction. pub fn estimate_gas(self) -> EstimateGasFuture { EstimateGasFuture::from_builder(self) @@ -180,16 +287,6 @@ impl TransactionBuilder { pub fn send(self) -> SendFuture { SendFuture::from_builder(self) } - - /// Send a transaction and wait for confirmation. Returns the transaction - /// receipt for inspection. - pub fn send_and_confirm( - self, - poll_interval: Duration, - confirmations: usize, - ) -> SendAndConfirmFuture { - SendAndConfirmFuture::from_builder_with_confirm(self, poll_interval, confirmations) - } } /// Future for estimating gas for a transaction. @@ -247,27 +344,6 @@ pub struct BuildFuture { state: BuildState, } -impl BuildFuture { - /// Create an instance from a `TransactionBuilder`. - pub fn from_builder(builder: TransactionBuilder) -> BuildFuture { - BuildFuture { - state: BuildState::from_builder(builder), - } - } - - fn state(self: Pin<&mut Self>) -> &mut BuildState { - &mut self.get_mut().state - } -} - -impl Future for BuildFuture { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - self.state().poll_unpinned(cx) - } -} - /// Type alias for a call future that might already be resolved. type MaybeCallFuture = MaybeReady>; @@ -321,10 +397,10 @@ enum BuildState { }, } -impl BuildState { - /// Create a `BuildState` from a `TransactionBuilder` - fn from_builder(builder: TransactionBuilder) -> BuildState { - match builder.from { +impl BuildFuture { + /// Create an instance from a `TransactionBuilder`. + pub fn from_builder(builder: TransactionBuilder) -> Self { + let state = match builder.from { None => BuildState::DefaultAccount { request: Some(TransactionRequest { from: Address::zero(), @@ -421,11 +497,17 @@ impl BuildState { params: future::try_join4(gas, gas_price, nonce, chain_id), } } - } + }; + + BuildFuture { state } } +} - fn poll_unpinned(&mut self, cx: &mut Context) -> Poll> { - match self { +impl Future for BuildFuture { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match &mut self.get_mut().state { BuildState::DefaultAccount { request, inner } => { Pin::new(inner).poll(cx).map(|accounts| { let accounts = accounts?; @@ -473,137 +555,90 @@ impl BuildState { /// Future for optionally signing and then sending a transaction. #[must_use = "futures do nothing unless you `.await` or poll them"] pub struct SendFuture { + web3: Web3Unpin, + /// The confirmation options to use for the transaction once it has been + /// sent. Stored as an option as we require transfer of ownership. + resolve: Option, /// Internal execution state. - state: ExecutionState, CompatCallFuture>, + state: SendState, +} + +/// The state of the send future. +enum SendState { + /// The transaction is being built into a request or a signed raw + /// transaction. + Building(BuildFuture), + /// The transaction is being sent to the node. + Sending(CompatCallFuture), + /// The transaction is being confirmed. + Confirming(ConfirmFuture), } impl SendFuture { /// Creates a new future from a `TransactionBuilder` - pub fn from_builder(builder: TransactionBuilder) -> SendFuture { + pub fn from_builder(mut builder: TransactionBuilder) -> SendFuture { let web3 = builder.web3.clone().into(); - let state = ExecutionState::from_builder_with_data(builder, web3); + let resolve = Some(builder.resolve.take().unwrap_or_default()); + let state = SendState::Building(BuildFuture::from_builder(builder)); - SendFuture { state } - } - - fn state( - self: Pin<&mut Self>, - ) -> &mut ExecutionState, CompatCallFuture> { - &mut self.get_mut().state + SendFuture { + web3, + resolve, + state, + } } } impl Future for SendFuture { - type Output = Result; + type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - self.state().poll_unpinned(cx, |web3, tx| match tx { - Transaction::Request(tx) => web3.eth().send_transaction(tx).compat(), - Transaction::Raw(tx) => web3.eth().send_raw_transaction(tx).compat(), - }) - } -} - -/// Future for optinally signing and then sending a transaction with -/// confirmation. -#[must_use = "futures do nothing unless you `.await` or poll them"] -pub struct SendAndConfirmFuture { - /// Internal execution state. - state: ExecutionState, Duration, usize), CompatSendTxWithConfirmation>, -} - -impl SendAndConfirmFuture { - /// Creates a new future from a `TransactionBuilder` - pub fn from_builder_with_confirm( - builder: TransactionBuilder, - poll_interval: Duration, - confirmations: usize, - ) -> SendAndConfirmFuture { - let web3 = builder.web3.clone().into(); - let state = - ExecutionState::from_builder_with_data(builder, (web3, poll_interval, confirmations)); - - SendAndConfirmFuture { state } - } - - fn state( - self: Pin<&mut Self>, - ) -> &mut ExecutionState, Duration, usize), CompatSendTxWithConfirmation> - { - &mut self.get_mut().state - } -} - -impl Future for SendAndConfirmFuture { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - self.as_mut() - .state() - .poll_unpinned(cx, |(web3, poll_interval, confirmations), tx| match tx { - Transaction::Request(tx) => web3 - .send_transaction_with_confirmation(tx, poll_interval, confirmations) - .compat(), - Transaction::Raw(tx) => web3 - .send_raw_transaction_with_confirmation(tx, poll_interval, confirmations) - .compat(), - }) - } -} - -/// Internal execution state for preparing and executing transactions. -enum ExecutionState -where - T: Transport, - F: TryFuture + Unpin, - F::Error: Into, -{ - /// Waiting for the transaction to be prepared to be sent. - Building(BuildFuture, Option), - /// Sending the request and waiting for the future to resolve. - Sending(F), -} - -impl ExecutionState -where - T: Transport, - F: TryFuture + Unpin, - F::Error: Into, -{ - /// Create a `ExecutionState` from a `TransactionBuilder` - fn from_builder_with_data(builder: TransactionBuilder, data: D) -> ExecutionState { - let build = BuildFuture::from_builder(builder); - let data = Some(data); - - ExecutionState::Building(build, data) - } - - /// Poll the state to drive the execution of its inner futures. - fn poll_unpinned( - &mut self, - cx: &mut Context, - mut send_fn: S, - ) -> Poll> - where - S: FnMut(D, Transaction) -> F, - { + let unpinned = self.get_mut(); loop { - match self { - ExecutionState::Building(build, data) => { - let tx = ready!(Pin::new(build).poll(cx).map_err(ExecutionError::from)); - let tx = match tx { + unpinned.state = match &mut unpinned.state { + SendState::Building(ref mut build) => { + let tx = match ready!(Pin::new(build).poll(cx)) { Ok(tx) => tx, Err(err) => return Poll::Ready(Err(err)), }; - let data = data.take().expect("called once"); - let send = send_fn(data, tx); - *self = ExecutionState::Sending(send); + let eth = unpinned.web3.eth(); + let send = match tx { + Transaction::Request(tx) => eth.send_transaction(tx).compat(), + Transaction::Raw(tx) => eth.send_raw_transaction(tx).compat(), + }; + + SendState::Sending(send) } - ExecutionState::Sending(ref mut send) => { - return Pin::new(send) - .try_poll(cx) - .map_err(Into::::into) + SendState::Sending(ref mut send) => { + let tx_hash = match ready!(Pin::new(send).poll(cx)) { + Ok(tx_hash) => tx_hash, + Err(err) => return Poll::Ready(Err(err.into())), + }; + + let confirm = match unpinned + .resolve + .take() + .expect("confirmation called more than once") + { + ResolveCondition::Pending => { + return Poll::Ready(Ok(TransactionResult::Hash(tx_hash))) + } + ResolveCondition::Confirmed(params) => { + ConfirmFuture::new(&unpinned.web3, tx_hash, params) + } + }; + + SendState::Confirming(confirm) + } + SendState::Confirming(ref mut confirm) => { + return Pin::new(confirm).poll(cx).map(|result| { + let tx = result?; + match tx.status { + Some(U64([1])) => Ok(TransactionResult::Receipt(tx)), + _ => Err(ExecutionError::Failure(tx.transaction_hash)), + } + }) } } } @@ -614,6 +649,7 @@ where mod tests { use super::*; use crate::test::prelude::*; + use web3::types::H2048; #[test] fn tx_builder_estimate_gas() { @@ -642,50 +678,7 @@ mod tests { } #[test] - fn tx_send_local() { - let mut transport = TestTransport::new(); - let web3 = Web3::new(transport.clone()); - - let from = addr!("0x9876543210987654321098765432109876543210"); - let to = addr!("0x0123456789012345678901234567890123456789"); - let hash = hash!("0x4242424242424242424242424242424242424242424242424242424242424242"); - - transport.add_response(json!(hash)); // tansaction hash - let tx = TransactionBuilder::new(web3) - .from(Account::Local(from, Some(TransactionCondition::Block(100)))) - .to(to) - .gas(1.into()) - .gas_price(2.into()) - .value(28.into()) - .data(Bytes(vec![0x13, 0x37])) - .nonce(42.into()) - .send() - .wait() - .expect("transaction success"); - - // assert that all the parameters are being used and that no extra - // request was being sent (since no extra data from the node is needed) - transport.assert_request( - "eth_sendTransaction", - &[json!({ - "from": from, - "to": to, - "gas": "0x1", - "gasPrice": "0x2", - "value": "0x1c", - "data": "0x1337", - "nonce": "0x2a", - "condition": { "block": 100 }, - })], - ); - transport.assert_no_more_requests(); - - // assert the tx hash is what we expect it to be - assert_eq!(tx, hash); - } - - #[test] - fn tx_build_default_account() { + fn tx_build_local_default_account() { let mut transport = TestTransport::new(); let web3 = Web3::new(transport.clone()); @@ -815,4 +808,157 @@ mod tests { // check that if we sign with same values we get same results assert_eq!(tx1, tx2); } + + #[test] + fn tx_send_local() { + let mut transport = TestTransport::new(); + let web3 = Web3::new(transport.clone()); + + let from = addr!("0x9876543210987654321098765432109876543210"); + let to = addr!("0x0123456789012345678901234567890123456789"); + let hash = hash!("0x4242424242424242424242424242424242424242424242424242424242424242"); + + transport.add_response(json!(hash)); // tansaction hash + let tx = TransactionBuilder::new(web3) + .from(Account::Local(from, Some(TransactionCondition::Block(100)))) + .to(to) + .gas(1.into()) + .gas_price(2.into()) + .value(28.into()) + .data(Bytes(vec![0x13, 0x37])) + .nonce(42.into()) + .resolve(ResolveCondition::Pending) + .send() + .wait() + .expect("transaction success"); + + // assert that all the parameters are being used and that no extra + // request was being sent (since no extra data from the node is needed) + transport.assert_request( + "eth_sendTransaction", + &[json!({ + "from": from, + "to": to, + "gas": "0x1", + "gasPrice": "0x2", + "value": "0x1c", + "data": "0x1337", + "nonce": "0x2a", + "condition": { "block": 100 }, + })], + ); + transport.assert_no_more_requests(); + + // assert the tx hash is what we expect it to be + assert_eq!(tx.hash(), hash); + } + + #[test] + fn tx_send_with_confirmations() { + let mut transport = TestTransport::new(); + let web3 = Web3::new(transport.clone()); + + let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132"); + let chain_id = 77777; + let tx_hash = H256::repeat_byte(0xff); + + transport.add_response(json!(tx_hash)); + transport.add_response(json!("0x1")); + transport.add_response(json!(null)); + transport.add_response(json!("0xf0")); + transport.add_response(json!([H256::repeat_byte(2), H256::repeat_byte(3)])); + transport.add_response(json!("0x3")); + transport.add_response(json!({ + "transactionHash": tx_hash, + "transactionIndex": "0x1", + "blockNumber": "0x2", + "blockHash": H256::repeat_byte(3), + "cumulativeGasUsed": "0x1337", + "gasUsed": "0x1337", + "logsBloom": H2048::zero(), + "logs": [], + "status": "0x1", + })); + + let builder = TransactionBuilder::new(web3) + .from(Account::Offline(key, Some(chain_id))) + .to(Address::zero()) + .gas(0x1337.into()) + .gas_price(0x00ba_b10c.into()) + .nonce(0x42.into()) + .confirmations(1); + let tx_raw = builder + .clone() + .build() + .wait() + .expect("failed to sign transaction") + .raw() + .expect("offline transactions always build into raw transactions"); + let tx_receipt = builder + .send() + .wait() + .expect("send with confirmations failed"); + + assert_eq!(tx_receipt.hash(), tx_hash); + transport.assert_request("eth_sendRawTransaction", &[json!(tx_raw)]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]); + transport.assert_request("eth_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]); + transport.assert_no_more_requests(); + } + + #[test] + fn tx_failure() { + let mut transport = TestTransport::new(); + let web3 = Web3::new(transport.clone()); + + let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132"); + let chain_id = 77777; + let tx_hash = H256::repeat_byte(0xff); + + transport.add_response(json!(tx_hash)); + transport.add_response(json!("0x1")); + transport.add_response(json!({ + "transactionHash": tx_hash, + "transactionIndex": "0x1", + "blockNumber": "0x1", + "blockHash": H256::repeat_byte(1), + "cumulativeGasUsed": "0x1337", + "gasUsed": "0x1337", + "logsBloom": H2048::zero(), + "logs": [], + })); + + let builder = TransactionBuilder::new(web3) + .from(Account::Offline(key, Some(chain_id))) + .to(Address::zero()) + .gas(0x1337.into()) + .gas_price(0x00ba_b10c.into()) + .nonce(0x42.into()); + let tx_raw = builder + .clone() + .build() + .wait() + .expect("failed to sign transaction") + .raw() + .expect("offline transactions always build into raw transactions"); + let result = builder.send().wait(); + + assert!( + match &result { + Err(ExecutionError::Failure(ref hash)) if *hash == tx_hash => true, + _ => false, + }, + "expected transaction failure with hash {} but got {:?}", + tx_hash, + result + ); + transport.assert_request("eth_sendRawTransaction", &[json!(tx_raw)]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]); + transport.assert_no_more_requests(); + } } diff --git a/src/transaction/confirm.rs b/src/transaction/confirm.rs new file mode 100644 index 00000000..35983309 --- /dev/null +++ b/src/transaction/confirm.rs @@ -0,0 +1,609 @@ +//! Transaction confirmation implementation. This is a re-implementation of +//! `web3` confirmation future to fix issues with development nodes like Ganache +//! where the transaction gets mined right away, so waiting for 1 confirmation +//! would require another transaction to be sent so a new block could mine. +//! Additionally, waiting for 0 confirmations in `web3` means that the tx is +//! just sent to the mem-pool but does not wait for it to get mined. Hopefully +//! some of this can move upstream into the `web3` crate. + +use crate::errors::ExecutionError; +use crate::future::{CompatCallFuture, MaybeReady, Web3Unpin}; +use futures::compat::{Compat01As03, Future01CompatExt}; +use futures::future::{self, TryJoin}; +use futures::ready; +use futures_timer::Delay; +use std::fmt::{self, Debug, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::Duration; +use web3::api::{CreateFilter, FilterStream, Web3}; +use web3::futures::stream::{Skip as Skip01, StreamFuture as StreamFuture01}; +use web3::futures::Stream as Stream01; +use web3::types::{TransactionReceipt, H256, U256}; +use web3::Transport; + +/// A struct with the confirmation parameters. +#[derive(Clone, Debug)] +pub struct ConfirmParams { + /// The number of blocks to confirm the transaction with. This is the number + /// of blocks mined on top of the block where the transaction was mined. + /// This means that, for example, to just wait for the transaction to be + /// mined, then the number of confirmations should be 0. Positive non-zero + /// values indicate that extra blocks should be waited for on top of the + /// block where the transaction was mined. + pub confirmations: usize, + /// The polling interval. This is used as the interval between consecutive + /// `eth_getFilterChanges` calls to get filter updates, or the interval to + /// wait between confirmation checks in case filters are not supported by + /// the node (for example when using Infura over HTTP(S)). + pub poll_interval: Duration, + /// The maximum number of blocks to wait for a transaction to get confirmed. + pub block_timeout: Option, +} + +/// The default poll interval to use for confirming transactions. +/// +/// Note that this is currently 7 seconds as this is what was chosen in `web3` +/// crate. +#[cfg(not(test))] +pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(7); +#[cfg(test)] +pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(0); + +/// The default block timeout to use for confirming transactions. +pub const DEFAULT_BLOCK_TIMEOUT: Option = Some(25); + +impl ConfirmParams { + /// Create new confirmation parameters for just confirming that the + /// transaction was mined but not confirmed with any extra blocks. + pub fn mined() -> Self { + ConfirmParams::with_confirmations(0) + } + + /// Create new confirmation parameters from the specified number of extra + /// blocks to wait for with the default poll interval. + pub fn with_confirmations(count: usize) -> Self { + ConfirmParams { + confirmations: count, + poll_interval: DEFAULT_POLL_INTERVAL, + block_timeout: DEFAULT_BLOCK_TIMEOUT, + } + } +} + +impl Default for ConfirmParams { + fn default() -> Self { + ConfirmParams::mined() + } +} + +/// A future that resolves once a transaction is confirmed. +#[derive(Debug)] +pub struct ConfirmFuture { + web3: Web3Unpin, + /// The transaction hash that is being confirmed. + tx: H256, + /// The confirmation parameters (like number of confirming blocks to wait + /// for and polling interval). + params: ConfirmParams, + /// The current block number when confirmation started. This is used for + /// timeouts. + starting_block_num: Option, + /// The current state of the confirmation. + state: ConfirmState, +} + +/// The state of the confirmation future. +enum ConfirmState { + /// The future is in the state where it needs to setup the checking future + /// to see if the confirmation is complete. This is used as a intermediate + /// state that doesn't actually wait for anything and immediately proceeds + /// to the `Checking` state. + Check, + /// The future is waiting for the block number and transaction receipt to + /// make sure that enough blocks have passed since the transaction was + /// mined. Note that the transaction receipt is retrieved everytime in case + /// of ommered blocks. + Checking(CheckFuture), + /// The future is waiting for the block filter to be created so that it can + /// wait for blocks to go by. + CreatingFilter(CompatCreateFilter, U256, u64), + /// The future is waiting for new blocks to be mined and added to the chain + /// so that the transaction can be confirmed the desired number of blocks. + WaitingForBlocks(CompatFilterFuture), + /// The future is waiting for a poll timeout. This state happens when the + /// node does not support block filters for the given transport (like Infura + /// over HTTPS) so we need to fallback to polling. + PollDelay(Delay, U256), + /// The future is checking that the current block number has reached a + /// certain target after waiting the poll delay. + PollCheckingBlockNumber(CompatCallFuture, U256), +} + +impl ConfirmFuture { + /// Create a new `ConfirmFuture` with a `web3` provider for the specified + /// transaction hash and with the specified parameters. + pub fn new(web3: &Web3, tx: H256, params: ConfirmParams) -> Self { + ConfirmFuture { + web3: web3.clone().into(), + tx, + params, + starting_block_num: None, + state: ConfirmState::Check, + } + } +} + +impl Future for ConfirmFuture { + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + let unpinned = self.get_mut(); + loop { + unpinned.state = match &mut unpinned.state { + ConfirmState::Check => ConfirmState::Checking(future::try_join( + MaybeReady::future(unpinned.web3.eth().block_number().compat()), + unpinned + .web3 + .eth() + .transaction_receipt(unpinned.tx) + .compat(), + )), + ConfirmState::Checking(ref mut check) => { + let (block_num, tx) = match ready!(Pin::new(check).poll(cx)) { + Ok(result) => result, + Err(err) => return Poll::Ready(Err(err.into())), + }; + + // NOTE: If the transaction hasn't been mined, then assume + // it will be picked up in the next block. + let tx_block_num = tx + .as_ref() + .and_then(|tx| tx.block_number) + .unwrap_or(block_num + 1); + + let target_block_num = tx_block_num + unpinned.params.confirmations; + let remaining_confirmations = target_block_num.saturating_sub(block_num); + + if remaining_confirmations.is_zero() { + // NOTE: It is safe to unwrap here since if tx is `None` + // then the `remaining_confirmations` will always be + // positive since `tx_block_num` will be a future + // block. + return Poll::Ready(Ok(tx.unwrap())); + } + + if let Some(block_timeout) = unpinned.params.block_timeout { + let starting_block_num = + *unpinned.starting_block_num.get_or_insert(block_num); + let elapsed_blocks = block_num.saturating_sub(starting_block_num); + + if elapsed_blocks > U256::from(block_timeout) { + return Poll::Ready(Err(ExecutionError::ConfirmTimeout)); + } + } + + ConfirmState::CreatingFilter( + unpinned.web3.eth_filter().create_blocks_filter().compat(), + target_block_num, + remaining_confirmations.as_u64(), + ) + } + ConfirmState::CreatingFilter(ref mut create_filter, target_block_num, count) => { + match ready!(Pin::new(create_filter).poll(cx)) { + Ok(filter) => ConfirmState::WaitingForBlocks( + filter + .stream(unpinned.params.poll_interval) + .skip(*count - 1) + .into_future() + .compat(), + ), + Err(_) => { + // NOTE: In the case we fail to create a filter + // (usually because the node doesn't support + // filters like Infura over HTTPS) then fall back + // to polling. + ConfirmState::PollDelay( + Delay::new(unpinned.params.poll_interval), + *target_block_num, + ) + } + } + } + ConfirmState::WaitingForBlocks(ref mut wait) => { + match ready!(Pin::new(wait).poll(cx)) { + Ok(_) => ConfirmState::Check, + Err((err, _)) => return Poll::Ready(Err(err.into())), + } + } + ConfirmState::PollDelay(ref mut delay, target_block_num) => { + ready!(Pin::new(delay).poll(cx)); + ConfirmState::PollCheckingBlockNumber( + unpinned.web3.eth().block_number().compat(), + *target_block_num, + ) + } + ConfirmState::PollCheckingBlockNumber(ref mut block_num, target_block_num) => { + let block_num = match ready!(Pin::new(block_num).poll(cx)) { + Ok(block_num) => block_num, + Err(err) => return Poll::Ready(Err(err.into())), + }; + + if block_num == *target_block_num { + ConfirmState::Checking(future::try_join( + MaybeReady::ready(Ok(block_num)), + unpinned + .web3 + .eth() + .transaction_receipt(unpinned.tx) + .compat(), + )) + } else { + ConfirmState::PollDelay( + Delay::new(unpinned.params.poll_interval), + *target_block_num, + ) + } + } + } + } + } +} + +impl Debug for ConfirmState { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + ConfirmState::Check => f.debug_tuple("Check").finish(), + ConfirmState::Checking(_) => f.debug_tuple("Checking").finish(), + ConfirmState::CreatingFilter(_, t, c) => { + f.debug_tuple("CreatingFilter").field(t).field(c).finish() + } + ConfirmState::WaitingForBlocks(_) => f.debug_tuple("WaitingForBlocks").finish(), + ConfirmState::PollDelay(d, t) => f.debug_tuple("PollDelay").field(d).field(t).finish(), + ConfirmState::PollCheckingBlockNumber(_, t) => { + f.debug_tuple("PollCheckingBlockNumber").field(t).finish() + } + } + } +} + +/// A type alias for a joined `eth_blockNumber` and `eth_getTransactionReceipt` +/// calls. Used when checking that the transaction has been confirmed by enough +/// blocks. +type CheckFuture = + TryJoin>, CompatCallFuture>>; + +/// A type alias for a future creating a `eth_newBlockFilter` filter. +type CompatCreateFilter = Compat01As03>; + +/// A type alias for a future that resolves once the block filter has received +/// a certain number of blocks. +type CompatFilterFuture = Compat01As03>>>; + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::prelude::*; + use serde_json::Value; + use web3::types::H2048; + + fn generate_tx_receipt>(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": [], + }) + } + + #[test] + fn confirm_mined_transaction() { + let mut transport = TestTransport::new(); + let web3 = Web3::new(transport.clone()); + + let hash = H256::repeat_byte(0xff); + + // transaction pending + transport.add_response(json!("0x1")); + transport.add_response(json!(null)); + // filter created + transport.add_response(json!("0xf0")); + // polled block filter for 1 new block + transport.add_response(json!([])); + transport.add_response(json!([])); + transport.add_response(json!([H256::repeat_byte(2)])); + // check transaction was mined + transport.add_response(json!("0x2")); + transport.add_response(generate_tx_receipt(hash, 2)); + + let confirm = ConfirmFuture::new(&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_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + 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 = ConfirmFuture::new(&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_no_more_requests(); + } + + #[test] + fn confirmations_with_filter() { + let mut transport = TestTransport::new(); + let web3 = Web3::new(transport.clone()); + + let hash = H256::repeat_byte(0xff); + + // transaction pending + transport.add_response(json!("0x1")); + transport.add_response(json!(null)); + // filter created + transport.add_response(json!("0xf0")); + // polled block filter 4 times + transport.add_response(json!([H256::repeat_byte(2), H256::repeat_byte(3)])); + transport.add_response(json!([])); + transport.add_response(json!([H256::repeat_byte(4)])); + transport.add_response(json!([H256::repeat_byte(5)])); + // check confirmation again - transaction mined on block 3 instead of 2 + transport.add_response(json!("0x5")); + transport.add_response(generate_tx_receipt(hash, 3)); + // needs to wait 1 more block - creating filter again and polling + transport.add_response(json!("0xf1")); + transport.add_response(json!([H256::repeat_byte(6)])); + // check confirmation one last time + transport.add_response(json!("0x6")); + transport.add_response(generate_tx_receipt(hash, 3)); + + let confirm = ConfirmFuture::new(&web3, hash, ConfirmParams::with_confirmations(3)) + .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_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_request("eth_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf1")]); + 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); + + // transaction pending + transport.add_response(json!("0x1")); + transport.add_response(json!(null)); + // filter created not supported + transport.add_response(json!({ "error": "eth_newBlockFilter not supported" })); + // poll block number until new block is found + 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")); + // check transaction was mined - note that the block number doesn't get + // re-queried and is re-used from the polling loop. + transport.add_response(generate_tx_receipt(hash, 2)); + + let confirm = ConfirmFuture::new(&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_newBlockFilter", &[]); + 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_reorg_tx_receipt() { + let mut transport = TestTransport::new(); + let web3 = Web3::new(transport.clone()); + + let hash = H256::repeat_byte(0xff); + + // transaction pending + transport.add_response(json!("0x1")); + transport.add_response(json!(null)); + // filter created - poll for 2 blocks + transport.add_response(json!("0xf0")); + transport.add_response(json!([H256::repeat_byte(2)])); + transport.add_response(json!([H256::repeat_byte(3)])); + // check confirmation again - transaction mined on block 3 + transport.add_response(json!("0x3")); + transport.add_response(generate_tx_receipt(hash, 3)); + // needs to wait 1 more block - creating filter again and polling + transport.add_response(json!("0xf1")); + transport.add_response(json!([H256::repeat_byte(4)])); + // check confirmation - reorg happened, tx mined on block 4! + transport.add_response(json!("0x4")); + transport.add_response(generate_tx_receipt(hash, 4)); + // wait for another block + transport.add_response(json!("0xf2")); + transport.add_response(json!([H256::repeat_byte(5)])); + // check confirmation - and we are satisfied. + transport.add_response(json!("0x5")); + transport.add_response(generate_tx_receipt(hash, 4)); + + let confirm = ConfirmFuture::new(&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_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_request("eth_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf1")]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_request("eth_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf2")]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_no_more_requests(); + } + + #[test] + fn confirmations_with_reorg_blocks() { + let mut transport = TestTransport::new(); + let web3 = Web3::new(transport.clone()); + + let hash = H256::repeat_byte(0xff); + + // transaction pending + transport.add_response(json!("0x1")); + transport.add_response(json!(null)); + // filter created - poll for 2 blocks + transport.add_response(json!("0xf0")); + transport.add_response(json!([H256::repeat_byte(2)])); + transport.add_response(json!([H256::repeat_byte(3)])); + transport.add_response(json!([H256::repeat_byte(4)])); + // check confirmation again - transaction mined on block 3 + transport.add_response(json!("0x4")); + transport.add_response(generate_tx_receipt(hash, 3)); + // needs to wait 1 more block - creating filter again and polling + transport.add_response(json!("0xf1")); + transport.add_response(json!([H256::repeat_byte(5)])); + // check confirmation - reorg happened and block 4 was replaced + transport.add_response(json!("0x4")); + transport.add_response(generate_tx_receipt(hash, 3)); + // wait for another block + transport.add_response(json!("0xf2")); + transport.add_response(json!([H256::repeat_byte(6)])); + // check confirmation - and we are satisfied. + transport.add_response(json!("0x5")); + transport.add_response(generate_tx_receipt(hash, 3)); + + let confirm = ConfirmFuture::new(&web3, hash, ConfirmParams::with_confirmations(2)) + .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_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf0")]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_request("eth_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf1")]); + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_request("eth_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!("0xf2")]); + 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::mined(); + let timeout = params + .block_timeout + .expect("default confirm parameters have a block timeout") + + 1; + + // wait for the transaction a total of block timeout + 1 times + for i in 0..timeout { + let block_num = format!("0x{:x}", i + 1); + let filter_id = format!("0xf{:x}", i); + + // transaction is pending + transport.add_response(json!(block_num)); + transport.add_response(json!(null)); + transport.add_response(json!(filter_id)); + transport.add_response(json!([H256::repeat_byte(2)])); + } + + let block_num = format!("0x{:x}", timeout + 1); + transport.add_response(json!(block_num)); + transport.add_response(json!(null)); + + let confirm = ConfirmFuture::new(&web3, hash, params).wait(); + + assert!( + match &confirm { + Err(ExecutionError::ConfirmTimeout) => true, + _ => false, + }, + "expected confirmation to time out but got {:?}", + confirm + ); + + for i in 0..timeout { + let filter_id = format!("0xf{:x}", i); + + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_request("eth_newBlockFilter", &[]); + transport.assert_request("eth_getFilterChanges", &[json!(filter_id)]); + } + + transport.assert_request("eth_blockNumber", &[]); + transport.assert_request("eth_getTransactionReceipt", &[json!(hash)]); + transport.assert_no_more_requests(); + } +}