ethcontract/
transaction.rs

1//! Implementation for setting up, signing, estimating gas and sending
2//! transactions on the Ethereum network.
3
4mod build;
5pub mod confirm;
6pub mod gas_price;
7#[cfg(feature = "aws-kms")]
8pub mod kms;
9mod send;
10
11pub use self::build::Transaction;
12use self::confirm::ConfirmParams;
13pub use self::gas_price::GasPrice;
14pub use self::send::TransactionResult;
15use crate::errors::ExecutionError;
16use crate::secret::{Password, PrivateKey};
17use web3::api::Web3;
18use web3::types::{AccessList, Address, Bytes, CallRequest, TransactionCondition, U256};
19use web3::Transport;
20
21/// The account type used for signing the transaction.
22#[derive(Clone, Debug)]
23pub enum Account {
24    /// Let the node sign for a transaction with an unlocked account.
25    Local(Address, Option<TransactionCondition>),
26    /// Do online signing with a locked account with a password.
27    Locked(Address, Password, Option<TransactionCondition>),
28    /// Do offline signing with private key and optionally specify chain ID. If
29    /// no chain ID is specified, then it will default to the network ID.
30    Offline(PrivateKey, Option<u64>),
31    /// Sign using AWS KMS account and optionally specified chain ID.
32    #[cfg(feature = "aws-kms")]
33    Kms(kms::Account, Option<u64>),
34}
35
36impl Account {
37    /// Returns the public address of an account.
38    pub fn address(&self) -> Address {
39        match self {
40            Account::Local(address, _) => *address,
41            Account::Locked(address, _, _) => *address,
42            Account::Offline(key, _) => key.public_address(),
43            #[cfg(feature = "aws-kms")]
44            Account::Kms(kms, _) => kms.public_address(),
45        }
46    }
47}
48
49/// The condition on which a transaction's `SendFuture` gets resolved.
50#[derive(Clone, Debug)]
51pub enum ResolveCondition {
52    /// The transaction's `SendFuture` gets resolved immediately after it was
53    /// added to the pending transaction pool. This skips confirmation and
54    /// provides no guarantees that the transaction was mined or confirmed.
55    Pending,
56    /// Wait for confirmation with the specified `ConfirmParams`. A confirmed
57    /// transaction is always mined. There is a chance, however, that the block
58    /// in which the transaction was mined becomes an ommer block. Confirming
59    /// with a higher block count significantly decreases this probability.
60    ///
61    /// See `ConfirmParams` documentation for more details on the exact
62    /// semantics confirmation.
63    Confirmed(ConfirmParams),
64}
65
66impl Default for ResolveCondition {
67    fn default() -> Self {
68        ResolveCondition::Confirmed(Default::default())
69    }
70}
71
72/// Data used for building a transaction that modifies the blockchain. These
73/// transactions can either be sent to be signed locally by the node or can be
74/// signed offline.
75#[derive(Clone, Debug)]
76#[must_use = "transactions do nothing unless you `.build()` or `.send()` them"]
77pub struct TransactionBuilder<T: Transport> {
78    web3: Web3<T>,
79    /// The sender of the transaction with the signing strategy to use. Defaults
80    /// to locally signing on the node with the default acount.
81    pub from: Option<Account>,
82    /// The receiver of the transaction.
83    pub to: Option<Address>,
84    /// Optional gas amount to use for transaction. Defaults to estimated gas.
85    pub gas: Option<U256>,
86    /// Optional gas price to use for transaction. Defaults to None.
87    pub gas_price: Option<GasPrice>,
88    /// The ETH value to send with the transaction. Defaults to 0.
89    pub value: Option<U256>,
90    /// The data for the transaction. Defaults to empty data.
91    pub data: Option<Bytes>,
92    /// Optional nonce to use. Defaults to the signing account's current
93    /// transaction count.
94    pub nonce: Option<U256>,
95    /// Optional resolve conditions. Defaults to waiting the transaction to be
96    /// mined without any extra confirmation blocks.
97    pub resolve: Option<ResolveCondition>,
98    /// Access list
99    pub access_list: Option<AccessList>,
100}
101
102impl<T: Transport> TransactionBuilder<T> {
103    /// Creates a new builder for a transaction.
104    pub fn new(web3: Web3<T>) -> Self {
105        TransactionBuilder {
106            web3,
107            from: None,
108            to: None,
109            gas: None,
110            gas_price: None,
111            value: None,
112            data: None,
113            nonce: None,
114            resolve: None,
115            access_list: None,
116        }
117    }
118
119    /// Specify the signing method to use for the transaction, if not specified
120    /// the the transaction will be locally signed with the default user.
121    pub fn from(mut self, value: Account) -> Self {
122        self.from = Some(value);
123        self
124    }
125
126    /// Specify the recepient of the transaction, if not specified the
127    /// transaction will be sent to the 0 address (for deploying contracts).
128    pub fn to(mut self, value: Address) -> Self {
129        self.to = Some(value);
130        self
131    }
132
133    /// Secify amount of gas to use, if not specified then a gas estimate will
134    /// be used.
135    pub fn gas(mut self, value: U256) -> Self {
136        self.gas = Some(value);
137        self
138    }
139
140    /// Specify the gas price to use, if not specified then the estimated gas
141    /// price will be used.
142    pub fn gas_price(mut self, value: GasPrice) -> Self {
143        self.gas_price = Some(value);
144        self
145    }
146
147    /// Specify what how much ETH to transfer with the transaction, if not
148    /// specified then no ETH will be sent.
149    pub fn value(mut self, value: U256) -> Self {
150        self.value = Some(value);
151        self
152    }
153
154    /// Specify the data to use for the transaction, if not specified, then empty
155    /// data will be used.
156    pub fn data(mut self, value: Bytes) -> Self {
157        self.data = Some(value);
158        self
159    }
160
161    /// Specify the nonce for the transation, if not specified will use the
162    /// current transaction count for the signing account.
163    pub fn nonce(mut self, value: U256) -> Self {
164        self.nonce = Some(value);
165        self
166    }
167
168    /// Specify the resolve condition, if not specified will default to waiting
169    /// for the transaction to be mined (but not confirmed by any extra blocks).
170    pub fn resolve(mut self, value: ResolveCondition) -> Self {
171        self.resolve = Some(value);
172        self
173    }
174
175    /// Specify the access list for the transaction, if not specified no access list will be used.
176    pub fn access_list(mut self, value: AccessList) -> Self {
177        self.access_list = Some(value);
178        self
179    }
180
181    /// Specify the number of confirmations to use for the confirmation options.
182    /// This is a utility method for specifying the resolve condition.
183    pub fn confirmations(mut self, value: usize) -> Self {
184        self.resolve = match self.resolve {
185            Some(ResolveCondition::Confirmed(params)) => {
186                Some(ResolveCondition::Confirmed(ConfirmParams {
187                    confirmations: value,
188                    ..params
189                }))
190            }
191            _ => Some(ResolveCondition::Confirmed(
192                ConfirmParams::with_confirmations(value),
193            )),
194        };
195        self
196    }
197
198    /// Estimate the gas required for this transaction.
199    pub async fn estimate_gas(self) -> Result<U256, ExecutionError> {
200        let from = self.from.map(|account| account.address());
201        let resolved_gas_price = self
202            .gas_price
203            .map(|gas_price| gas_price.resolve_for_transaction())
204            .unwrap_or_default();
205        self.web3
206            .eth()
207            .estimate_gas(
208                CallRequest {
209                    from,
210                    to: self.to,
211                    gas: None,
212                    gas_price: resolved_gas_price.gas_price,
213                    value: self.value,
214                    data: self.data.clone(),
215                    transaction_type: resolved_gas_price.transaction_type,
216                    access_list: self.access_list,
217                    max_fee_per_gas: resolved_gas_price.max_fee_per_gas,
218                    max_priority_fee_per_gas: resolved_gas_price.max_priority_fee_per_gas,
219                },
220                None,
221            )
222            .await
223            .map_err(From::from)
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::test::prelude::*;
231    use hex_literal::hex;
232    use web3::types::{AccessListItem, H2048, H256};
233
234    #[test]
235    fn tx_builder_estimate_gas() {
236        let mut transport = TestTransport::new();
237        let web3 = Web3::new(transport.clone());
238
239        let to = addr!("0x0123456789012345678901234567890123456789");
240
241        transport.add_response(json!("0x42")); // estimate gas response
242        let estimate_gas = TransactionBuilder::new(web3)
243            .to(to)
244            .value(42.into())
245            .estimate_gas()
246            .immediate()
247            .expect("success");
248
249        assert_eq!(estimate_gas, 0x42.into());
250        transport.assert_request(
251            "eth_estimateGas",
252            &[json!({
253                "to": to,
254                "value": "0x2a",
255            })],
256        );
257        transport.assert_no_more_requests();
258    }
259
260    #[test]
261    fn tx_send_local() {
262        let mut transport = TestTransport::new();
263        let web3 = Web3::new(transport.clone());
264
265        let from = addr!("0x9876543210987654321098765432109876543210");
266        let to = addr!("0x0123456789012345678901234567890123456789");
267        let hash = hash!("0x4242424242424242424242424242424242424242424242424242424242424242");
268
269        transport.add_response(json!(hash)); // tansaction hash
270        let tx = TransactionBuilder::new(web3)
271            .from(Account::Local(from, Some(TransactionCondition::Block(100))))
272            .to(to)
273            .gas(1.into())
274            .gas_price(2.0.into())
275            .value(28.into())
276            .data(Bytes(vec![0x13, 0x37]))
277            .nonce(42.into())
278            .access_list(vec![AccessListItem::default()])
279            .resolve(ResolveCondition::Pending)
280            .send()
281            .immediate()
282            .expect("transaction success");
283
284        // assert that all the parameters are being used and that no extra
285        // request was being sent (since no extra data from the node is needed)
286        transport.assert_request(
287            "eth_sendTransaction",
288            &[json!({
289                "from": from,
290                "to": to,
291                "gas": "0x1",
292                "gasPrice": "0x2",
293                "value": "0x1c",
294                "data": "0x1337",
295                "nonce": "0x2a",
296                "accessList": [{
297                    "address": "0x0000000000000000000000000000000000000000",
298                    "storageKeys": [],
299                }],
300                "condition": { "block": 100 },
301            })],
302        );
303        transport.assert_no_more_requests();
304
305        // assert the tx hash is what we expect it to be
306        assert_eq!(tx.hash(), hash);
307    }
308
309    #[test]
310    fn tx_send_with_confirmations() {
311        let mut transport = TestTransport::new();
312        let web3 = Web3::new(transport.clone());
313
314        let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132");
315        let chain_id = 77777;
316        let tx_hash = H256(hex!(
317            "248988e44deaff5162c3f998a8b1f510862366a68ef4339dff6ec89e120a6c19"
318        ));
319
320        transport.add_response(json!(tx_hash));
321        transport.add_response(json!("0x1"));
322        transport.add_response(json!(null));
323        transport.add_response(json!("0x2"));
324        transport.add_response(json!("0x3"));
325        transport.add_response(json!({
326            "transactionHash": tx_hash,
327            "transactionIndex": "0x1",
328            "blockNumber": "0x2",
329            "blockHash": H256::repeat_byte(3),
330            "cumulativeGasUsed": "0x1337",
331            "gasUsed": "0x1337",
332            "logsBloom": H2048::zero(),
333            "logs": [],
334            "status": "0x1",
335            "effectiveGasPrice": "0x0",
336        }));
337
338        let builder = TransactionBuilder::new(web3)
339            .from(Account::Offline(key, Some(chain_id)))
340            .to(Address::zero())
341            .gas(0x1337.into())
342            .gas_price(f64::from(0x00ba_b10c).into())
343            .nonce(0x42.into())
344            .confirmations(1);
345        let tx_raw = builder
346            .clone()
347            .build()
348            .wait()
349            .expect("failed to sign transaction")
350            .raw()
351            .expect("offline transactions always build into raw transactions");
352        let tx_receipt = builder
353            .send()
354            .wait()
355            .expect("send with confirmations failed");
356
357        assert_eq!(tx_receipt.hash(), tx_hash);
358        transport.assert_request("eth_sendRawTransaction", &[json!(tx_raw)]);
359        transport.assert_request("eth_blockNumber", &[]);
360        transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]);
361        transport.assert_request("eth_blockNumber", &[]);
362        transport.assert_request("eth_blockNumber", &[]);
363        transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]);
364        transport.assert_no_more_requests();
365    }
366
367    #[test]
368    fn tx_failure() {
369        let mut transport = TestTransport::new();
370        let web3 = Web3::new(transport.clone());
371
372        let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132");
373        let chain_id = 77777;
374        let tx_hash = H256(hex!(
375            "248988e44deaff5162c3f998a8b1f510862366a68ef4339dff6ec89e120a6c19"
376        ));
377
378        transport.add_response(json!(tx_hash));
379        transport.add_response(json!("0x1"));
380        transport.add_response(json!({
381            "transactionHash": tx_hash,
382            "transactionIndex": "0x1",
383            "blockNumber": "0x1",
384            "blockHash": H256::repeat_byte(1),
385            "cumulativeGasUsed": "0x1337",
386            "gasUsed": "0x1337",
387            "logsBloom": H2048::zero(),
388            "logs": [],
389            "effectiveGasPrice": "0x0",
390        }));
391
392        let builder = TransactionBuilder::new(web3)
393            .from(Account::Offline(key, Some(chain_id)))
394            .to(Address::zero())
395            .gas(0x1337.into())
396            .gas_price(f64::from(0x00ba_b10c).into())
397            .nonce(0x42.into());
398        let tx_raw = builder
399            .clone()
400            .build()
401            .immediate()
402            .expect("failed to sign transaction")
403            .raw()
404            .expect("offline transactions always build into raw transactions");
405        let result = builder.send().immediate();
406
407        assert!(
408            matches!(
409                &result,
410                Err(ExecutionError::Failure(ref tx)) if tx.transaction_hash == tx_hash
411            ),
412            "expected transaction failure with hash {} but got {:?}",
413            tx_hash,
414            result
415        );
416        transport.assert_request("eth_sendRawTransaction", &[json!(tx_raw)]);
417        transport.assert_request("eth_blockNumber", &[]);
418        transport.assert_request("eth_getTransactionReceipt", &[json!(tx_hash)]);
419        transport.assert_no_more_requests();
420    }
421}