ic_web3/
confirm.rs

1//! Easy to use utilities for confirmations.
2
3use crate::{
4    api::{Eth, EthFilter, Namespace},
5    error,
6    types::{Bytes, TransactionReceipt, TransactionRequest, H256, U64},
7    Transport,
8};
9use futures::{Future, StreamExt};
10use std::time::Duration;
11
12/// Checks whether an event has been confirmed.
13pub trait ConfirmationCheck {
14    /// Future resolved when is known whether an event has been confirmed.
15    type Check: Future<Output = error::Result<Option<U64>>>;
16
17    /// Should be called to get future which resolves when confirmation state is known.
18    fn check(&self) -> Self::Check;
19}
20
21impl<F, T> ConfirmationCheck for F
22where
23    F: Fn() -> T,
24    T: Future<Output = error::Result<Option<U64>>>,
25{
26    type Check = T;
27
28    fn check(&self) -> Self::Check {
29        (*self)()
30    }
31}
32
33/// Should be used to wait for confirmations
34pub async fn wait_for_confirmations<T, V, F>(
35    eth: Eth<T>,
36    eth_filter: EthFilter<T>,
37    poll_interval: Duration,
38    confirmations: usize,
39    check: V,
40) -> error::Result<()>
41where
42    T: Transport,
43    V: ConfirmationCheck<Check = F>,
44    F: Future<Output = error::Result<Option<U64>>>,
45{
46    let filter = eth_filter.create_blocks_filter().await?;
47    // TODO #396: The stream should have additional checks.
48    // * We should not continue calling next on a stream that has completed (has returned None). We expect this to never
49    //   happen for the blocks filter but to be safe we should handle this case for example by `fuse`ing the stream or
50    //   erroring when it does complete.
51    // * We do not handle the case where the stream returns an error which means we are wrongly counting it as a
52    //   confirmation.
53    let filter_stream = filter.stream(poll_interval).skip(confirmations);
54    futures::pin_mut!(filter_stream);
55    loop {
56        let _ = filter_stream.next().await;
57        if let Some(confirmation_block_number) = check.check().await? {
58            let block_number = eth.block_number().await?;
59            if confirmation_block_number.low_u64() + confirmations as u64 <= block_number.low_u64() {
60                return Ok(());
61            }
62        }
63    }
64}
65
66async fn transaction_receipt_block_number_check<T: Transport>(eth: &Eth<T>, hash: H256) -> error::Result<Option<U64>> {
67    let receipt = eth.transaction_receipt(hash).await?;
68    Ok(receipt.and_then(|receipt| receipt.block_number))
69}
70
71async fn send_transaction_with_confirmation_<T: Transport>(
72    hash: H256,
73    transport: T,
74    poll_interval: Duration,
75    confirmations: usize,
76) -> error::Result<TransactionReceipt> {
77    let eth = Eth::new(transport.clone());
78    if confirmations > 0 {
79        let confirmation_check = || transaction_receipt_block_number_check(&eth, hash);
80        let eth_filter = EthFilter::new(transport.clone());
81        let eth = eth.clone();
82        wait_for_confirmations(eth, eth_filter, poll_interval, confirmations, confirmation_check).await?;
83    }
84    // TODO #397: We should remove this `expect`. No matter what happens inside the node, this shouldn't be a panic.
85    let receipt = eth
86        .transaction_receipt(hash)
87        .await?
88        .expect("receipt can't be null after wait for confirmations; qed");
89    Ok(receipt)
90}
91
92/// Sends transaction and returns future resolved after transaction is confirmed
93pub async fn send_transaction_with_confirmation<T>(
94    transport: T,
95    tx: TransactionRequest,
96    poll_interval: Duration,
97    confirmations: usize,
98) -> error::Result<TransactionReceipt>
99where
100    T: Transport,
101{
102    let hash = Eth::new(&transport).send_transaction(tx).await?;
103    send_transaction_with_confirmation_(hash, transport, poll_interval, confirmations).await
104}
105
106/// Sends raw transaction and returns future resolved after transaction is confirmed
107pub async fn send_raw_transaction_with_confirmation<T>(
108    transport: T,
109    tx: Bytes,
110    poll_interval: Duration,
111    confirmations: usize,
112) -> error::Result<TransactionReceipt>
113where
114    T: Transport,
115{
116    let hash = Eth::new(&transport).send_raw_transaction(tx).await?;
117    send_transaction_with_confirmation_(hash, transport, poll_interval, confirmations).await
118}
119
120#[cfg(test)]
121mod tests {
122    use super::send_transaction_with_confirmation;
123    use crate::{
124        rpc::Value,
125        transports::test::TestTransport,
126        types::{Address, TransactionReceipt, TransactionRequest, H256, U64},
127    };
128    use serde_json::json;
129    use std::time::Duration;
130
131    #[test]
132    fn test_send_transaction_with_confirmation() {
133        let mut transport = TestTransport::default();
134        let confirmations = 3;
135        let transaction_request = TransactionRequest {
136            from: Address::from_low_u64_be(0x123),
137            to: Some(Address::from_low_u64_be(0x123)),
138            gas: None,
139            gas_price: Some(1.into()),
140            value: Some(1.into()),
141            data: None,
142            nonce: None,
143            condition: None,
144            transaction_type: None,
145            access_list: None,
146            max_fee_per_gas: None,
147            max_priority_fee_per_gas: None,
148        };
149
150        let transaction_receipt = TransactionReceipt {
151            transaction_hash: H256::zero(),
152            transaction_index: U64::zero(),
153            block_hash: Some(H256::zero()),
154            block_number: Some(2.into()),
155            from: Address::from_low_u64_be(0x123),
156            to: Some(Address::from_low_u64_be(0x123)),
157            cumulative_gas_used: 0.into(),
158            gas_used: Some(0.into()),
159            contract_address: None,
160            logs: vec![],
161            status: Some(1.into()),
162            root: Some(H256::zero()),
163            logs_bloom: Default::default(),
164            transaction_type: None,
165            effective_gas_price: Default::default(),
166        };
167
168        let poll_interval = Duration::from_secs(0);
169        transport.add_response(Value::String(
170            r#"0x0000000000000000000000000000000000000000000000000000000000000111"#.into(),
171        ));
172        transport.add_response(Value::String("0x123".into()));
173        transport.add_response(Value::Array(vec![
174            Value::String(r#"0x0000000000000000000000000000000000000000000000000000000000000456"#.into()),
175            Value::String(r#"0x0000000000000000000000000000000000000000000000000000000000000457"#.into()),
176        ]));
177        transport.add_response(Value::Array(vec![Value::String(
178            r#"0x0000000000000000000000000000000000000000000000000000000000000458"#.into(),
179        )]));
180        transport.add_response(Value::Array(vec![Value::String(
181            r#"0x0000000000000000000000000000000000000000000000000000000000000459"#.into(),
182        )]));
183        transport.add_response(Value::Null);
184        transport.add_response(Value::Array(vec![
185            Value::String(r#"0x0000000000000000000000000000000000000000000000000000000000000460"#.into()),
186            Value::String(r#"0x0000000000000000000000000000000000000000000000000000000000000461"#.into()),
187        ]));
188        transport.add_response(Value::Null);
189        transport.add_response(json!(transaction_receipt));
190        transport.add_response(Value::String("0x6".into()));
191        transport.add_response(json!(transaction_receipt));
192        transport.add_response(Value::Bool(true));
193
194        let confirmation = {
195            let future =
196                send_transaction_with_confirmation(&transport, transaction_request, poll_interval, confirmations);
197            futures::executor::block_on(future)
198        };
199
200        transport.assert_request("eth_sendTransaction", &[r#"{"from":"0x0000000000000000000000000000000000000123","gasPrice":"0x1","to":"0x0000000000000000000000000000000000000123","value":"0x1"}"#.into()]);
201        transport.assert_request("eth_newBlockFilter", &[]);
202        transport.assert_request("eth_getFilterChanges", &[r#""0x123""#.into()]);
203        transport.assert_request("eth_getFilterChanges", &[r#""0x123""#.into()]);
204        transport.assert_request("eth_getFilterChanges", &[r#""0x123""#.into()]);
205        transport.assert_request(
206            "eth_getTransactionReceipt",
207            &[r#""0x0000000000000000000000000000000000000000000000000000000000000111""#.into()],
208        );
209        transport.assert_request("eth_getFilterChanges", &[r#""0x123""#.into()]);
210        transport.assert_request(
211            "eth_getTransactionReceipt",
212            &[r#""0x0000000000000000000000000000000000000000000000000000000000000111""#.into()],
213        );
214        transport.assert_request(
215            "eth_getTransactionReceipt",
216            &[r#""0x0000000000000000000000000000000000000000000000000000000000000111""#.into()],
217        );
218        transport.assert_request("eth_blockNumber", &[]);
219        transport.assert_request(
220            "eth_getTransactionReceipt",
221            &[r#""0x0000000000000000000000000000000000000000000000000000000000000111""#.into()],
222        );
223        transport.assert_no_more_requests();
224        assert_eq!(confirmation, Ok(transaction_receipt));
225    }
226}