1mod 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#[derive(Clone, Debug)]
23pub enum Account {
24 Local(Address, Option<TransactionCondition>),
26 Locked(Address, Password, Option<TransactionCondition>),
28 Offline(PrivateKey, Option<u64>),
31 #[cfg(feature = "aws-kms")]
33 Kms(kms::Account, Option<u64>),
34}
35
36impl Account {
37 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#[derive(Clone, Debug)]
51pub enum ResolveCondition {
52 Pending,
56 Confirmed(ConfirmParams),
64}
65
66impl Default for ResolveCondition {
67 fn default() -> Self {
68 ResolveCondition::Confirmed(Default::default())
69 }
70}
71
72#[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 pub from: Option<Account>,
82 pub to: Option<Address>,
84 pub gas: Option<U256>,
86 pub gas_price: Option<GasPrice>,
88 pub value: Option<U256>,
90 pub data: Option<Bytes>,
92 pub nonce: Option<U256>,
95 pub resolve: Option<ResolveCondition>,
98 pub access_list: Option<AccessList>,
100}
101
102impl<T: Transport> TransactionBuilder<T> {
103 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 pub fn from(mut self, value: Account) -> Self {
122 self.from = Some(value);
123 self
124 }
125
126 pub fn to(mut self, value: Address) -> Self {
129 self.to = Some(value);
130 self
131 }
132
133 pub fn gas(mut self, value: U256) -> Self {
136 self.gas = Some(value);
137 self
138 }
139
140 pub fn gas_price(mut self, value: GasPrice) -> Self {
143 self.gas_price = Some(value);
144 self
145 }
146
147 pub fn value(mut self, value: U256) -> Self {
150 self.value = Some(value);
151 self
152 }
153
154 pub fn data(mut self, value: Bytes) -> Self {
157 self.data = Some(value);
158 self
159 }
160
161 pub fn nonce(mut self, value: U256) -> Self {
164 self.nonce = Some(value);
165 self
166 }
167
168 pub fn resolve(mut self, value: ResolveCondition) -> Self {
171 self.resolve = Some(value);
172 self
173 }
174
175 pub fn access_list(mut self, value: AccessList) -> Self {
177 self.access_list = Some(value);
178 self
179 }
180
181 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 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")); 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)); 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 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_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}