1use crate::errors::ExecutionError;
8use crate::secret::{Password, PrivateKey};
9use crate::transaction::gas_price::GasPrice;
10#[cfg(feature = "aws-kms")]
11use crate::transaction::kms;
12use crate::transaction::{Account, TransactionBuilder};
13use web3::api::Web3;
14use web3::types::{
15 AccessList, Address, Bytes, CallRequest, RawTransaction, SignedTransaction,
16 TransactionCondition, TransactionParameters, TransactionRequest, H256, U256,
17};
18use web3::Transport;
19
20impl<T: Transport> TransactionBuilder<T> {
21 pub async fn build(self) -> Result<Transaction, ExecutionError> {
27 let options = TransactionOptions {
28 to: self.to,
29 gas: self.gas,
30 gas_price: self.gas_price,
31 value: self.value,
32 data: self.data,
33 nonce: self.nonce,
34 access_list: self.access_list,
35 };
36
37 let tx = match self.from {
38 None => Transaction::Request(
39 build_transaction_request_for_local_signing(
40 self.web3,
41 None,
42 TransactionRequestOptions(options, None),
43 )
44 .await?,
45 ),
46 Some(Account::Local(from, condition)) => Transaction::Request(
47 build_transaction_request_for_local_signing(
48 self.web3,
49 Some(from),
50 TransactionRequestOptions(options, condition),
51 )
52 .await?,
53 ),
54 Some(Account::Locked(from, password, condition)) => {
55 build_transaction_signed_with_locked_account(
56 self.web3,
57 from,
58 password,
59 TransactionRequestOptions(options, condition),
60 )
61 .await
62 .map(|signed| Transaction::Raw {
63 bytes: signed.raw,
64 hash: signed.tx.hash,
65 })?
66 }
67 Some(Account::Offline(key, chain_id)) => {
68 build_offline_signed_transaction(self.web3, key, chain_id, options)
69 .await
70 .map(|signed| Transaction::Raw {
71 bytes: signed.raw_transaction,
72 hash: signed.transaction_hash,
73 })?
74 }
75 #[cfg(feature = "aws-kms")]
76 Some(Account::Kms(account, chain_id)) => {
77 build_kms_signed_transaction(self.web3, account, chain_id, options)
78 .await
79 .map(|signed| Transaction::Raw {
80 bytes: signed.raw_transaction,
81 hash: signed.transaction_hash,
82 })?
83 }
84 };
85
86 Ok(tx)
87 }
88}
89
90#[derive(Clone, Debug, PartialEq)]
93#[allow(clippy::large_enum_variant)]
94pub enum Transaction {
95 Request(TransactionRequest),
97 Raw {
99 bytes: Bytes,
101 hash: H256,
103 },
104}
105
106impl Transaction {
107 pub fn request(self) -> Option<TransactionRequest> {
110 match self {
111 Transaction::Request(tx) => Some(tx),
112 _ => None,
113 }
114 }
115
116 pub fn raw(self) -> Option<Bytes> {
119 match self {
120 Transaction::Raw { bytes, .. } => Some(bytes),
121 _ => None,
122 }
123 }
124}
125
126#[derive(Clone, Debug, Default)]
129struct TransactionOptions {
130 pub to: Option<Address>,
132 pub gas: Option<U256>,
134 pub gas_price: Option<GasPrice>,
136 pub value: Option<U256>,
138 pub data: Option<Bytes>,
140 pub nonce: Option<U256>,
142 pub access_list: Option<AccessList>,
144}
145
146#[derive(Clone, Debug, Default)]
150struct TransactionRequestOptions(TransactionOptions, Option<TransactionCondition>);
151
152impl TransactionRequestOptions {
153 fn build_request(self, from: Address, gas: Option<U256>) -> TransactionRequest {
156 let resolved_gas_price = self
157 .0
158 .gas_price
159 .map(|gas_price| gas_price.resolve_for_transaction())
160 .unwrap_or_default();
161 TransactionRequest {
162 from,
163 to: self.0.to,
164 gas,
165 gas_price: resolved_gas_price.gas_price,
166 value: self.0.value,
167 data: self.0.data,
168 nonce: self.0.nonce,
169 condition: self.1,
170 transaction_type: resolved_gas_price.transaction_type,
171 access_list: self.0.access_list,
172 max_fee_per_gas: resolved_gas_price.max_fee_per_gas,
173 max_priority_fee_per_gas: resolved_gas_price.max_priority_fee_per_gas,
174 }
175 }
176}
177
178async fn build_transaction_request_for_local_signing<T: Transport>(
180 web3: Web3<T>,
181 from: Option<Address>,
182 options: TransactionRequestOptions,
183) -> Result<TransactionRequest, ExecutionError> {
184 let from = match from {
185 Some(address) => address,
186 None => *web3
187 .eth()
188 .accounts()
189 .await?
190 .first()
191 .ok_or(ExecutionError::NoLocalAccounts)?,
192 };
193 let gas = resolve_gas_limit(&web3, from, &options.0).await?;
194 let request = options.build_request(from, Some(gas));
195
196 Ok(request)
197}
198
199async fn build_transaction_signed_with_locked_account<T: Transport>(
201 web3: Web3<T>,
202 from: Address,
203 password: Password,
204 options: TransactionRequestOptions,
205) -> Result<RawTransaction, ExecutionError> {
206 let gas = resolve_gas_limit(&web3, from, &options.0).await?;
207 let request = options.build_request(from, Some(gas));
208 let signed_tx = web3.personal().sign_transaction(request, &password).await?;
209
210 Ok(signed_tx)
211}
212
213async fn build_offline_signed_transaction<T: Transport>(
219 web3: Web3<T>,
220 key: PrivateKey,
221 chain_id: Option<u64>,
222 options: TransactionOptions,
223) -> Result<SignedTransaction, ExecutionError> {
224 let gas = resolve_gas_limit(&web3, key.public_address(), &options).await?;
225 let resolved_gas_price = options
226 .gas_price
227 .map(|gas_price| gas_price.resolve_for_transaction())
228 .unwrap_or_default();
229 let signed = web3
230 .accounts()
231 .sign_transaction(
232 TransactionParameters {
233 nonce: options.nonce,
234 gas_price: resolved_gas_price.gas_price,
235 gas,
236 to: options.to,
237 value: options.value.unwrap_or_default(),
238 data: options.data.unwrap_or_default(),
239 chain_id,
240 transaction_type: resolved_gas_price.transaction_type,
241 access_list: options.access_list,
242 max_fee_per_gas: resolved_gas_price.max_fee_per_gas,
243 max_priority_fee_per_gas: resolved_gas_price.max_priority_fee_per_gas,
244 },
245 &key,
246 )
247 .await?;
248
249 Ok(signed)
250}
251
252#[cfg(feature = "aws-kms")]
258async fn build_kms_signed_transaction<T: Transport>(
259 web3: Web3<T>,
260 account: kms::Account,
261 chain_id: Option<u64>,
262 options: TransactionOptions,
263) -> Result<SignedTransaction, ExecutionError> {
264 let gas = resolve_gas_limit(&web3, account.public_address(), &options).await?;
265 let resolved_gas_price = options
266 .gas_price
267 .map(|gas_price| gas_price.resolve_for_transaction())
268 .unwrap_or_default();
269 let signed = account
270 .sign_transaction(
271 web3,
272 TransactionParameters {
273 nonce: options.nonce,
274 gas_price: resolved_gas_price.gas_price,
275 gas,
276 to: options.to,
277 value: options.value.unwrap_or_default(),
278 data: options.data.unwrap_or_default(),
279 chain_id,
280 transaction_type: resolved_gas_price.transaction_type,
281 access_list: options.access_list,
282 max_fee_per_gas: resolved_gas_price.max_fee_per_gas,
283 max_priority_fee_per_gas: resolved_gas_price.max_priority_fee_per_gas,
284 },
285 )
286 .await?;
287
288 Ok(signed)
289}
290
291async fn resolve_gas_limit<T: Transport>(
292 web3: &Web3<T>,
293 from: Address,
294 options: &TransactionOptions,
295) -> Result<U256, ExecutionError> {
296 let resolved_gas_price = options
297 .gas_price
298 .map(|gas_price| gas_price.resolve_for_transaction())
299 .unwrap_or_default();
300 match options.gas {
301 Some(value) => Ok(value),
302 None => Ok(web3
303 .eth()
304 .estimate_gas(
305 CallRequest {
306 from: Some(from),
307 to: options.to,
308 gas: None,
309 gas_price: resolved_gas_price.gas_price,
310 value: options.value,
311 data: options.data.clone(),
312 transaction_type: resolved_gas_price.transaction_type,
313 access_list: options.access_list.clone(),
314 max_fee_per_gas: resolved_gas_price.max_fee_per_gas,
315 max_priority_fee_per_gas: resolved_gas_price.max_priority_fee_per_gas,
316 },
317 None,
318 )
319 .await?),
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::test::prelude::*;
327
328 #[test]
329 fn tx_build_local() {
330 let mut transport = TestTransport::new();
331 let web3 = Web3::new(transport.clone());
332
333 let from = addr!("0x9876543210987654321098765432109876543210");
334
335 transport.add_response(json!("0x9a5")); let tx = build_transaction_request_for_local_signing(
338 web3,
339 Some(from),
340 TransactionRequestOptions::default(),
341 )
342 .immediate()
343 .expect("failed to build local transaction");
344
345 transport.assert_request(
346 "eth_estimateGas",
347 &[json!({"from": "0x9876543210987654321098765432109876543210"})],
348 );
349 transport.assert_no_more_requests();
350 assert_eq!(tx.from, from);
351 }
352
353 #[test]
354 fn tx_build_local_default_account() {
355 let mut transport = TestTransport::new();
356 let web3 = Web3::new(transport.clone());
357
358 let accounts = [
359 addr!("0x9876543210987654321098765432109876543210"),
360 addr!("0x1111111111111111111111111111111111111111"),
361 addr!("0x2222222222222222222222222222222222222222"),
362 ];
363
364 transport.add_response(json!(accounts)); transport.add_response(json!("0x9a5")); let tx = build_transaction_request_for_local_signing(
367 web3,
368 None,
369 TransactionRequestOptions::default(),
370 )
371 .immediate()
372 .expect("failed to build local transaction");
373
374 transport.assert_request("eth_accounts", &[]);
375 transport.assert_request(
376 "eth_estimateGas",
377 &[json!({"from": "0x9876543210987654321098765432109876543210"})],
378 );
379 transport.assert_no_more_requests();
380
381 assert_eq!(tx.from, accounts[0]);
382 assert_eq!(tx.gas_price, None);
383 }
384
385 #[test]
386 fn tx_build_local_default_account_with_extra_gas_price() {
387 let mut transport = TestTransport::new();
388 let web3 = Web3::new(transport.clone());
389
390 let accounts = [
391 addr!("0x9876543210987654321098765432109876543210"),
392 addr!("0x1111111111111111111111111111111111111111"),
393 addr!("0x2222222222222222222222222222222222222222"),
394 ];
395
396 transport.add_response(json!(accounts)); transport.add_response(json!("0x9a5")); let tx = build_transaction_request_for_local_signing(
399 web3,
400 None,
401 TransactionRequestOptions {
402 0: TransactionOptions {
403 gas_price: Some(66.0.into()),
404 ..Default::default()
405 },
406 ..Default::default()
407 },
408 )
409 .immediate()
410 .expect("failed to build local transaction");
411
412 transport.assert_request("eth_accounts", &[]);
413 transport.assert_request(
414 "eth_estimateGas",
415 &[json!({ "from": json!(accounts[0]), "gasPrice": format!("{:#x}", 66), })],
416 );
417 transport.assert_no_more_requests();
418
419 assert_eq!(tx.from, accounts[0]);
420 assert_eq!(tx.gas_price, Some(U256::from(0x42)));
421 assert_eq!(tx.gas, Some(U256::from(0x9a5)));
422 }
423
424 #[test]
425 fn tx_build_local_with_explicit_gas_price() {
426 let mut transport = TestTransport::new();
427 let web3 = Web3::new(transport.clone());
428
429 let from = addr!("0xffffffffffffffffffffffffffffffffffffffff");
430
431 transport.add_response(json!("0x9a5")); let options = TransactionRequestOptions {
434 0: TransactionOptions {
435 gas_price: Some(1337.0.into()),
436 ..Default::default()
437 },
438 ..Default::default()
439 };
440
441 let tx = build_transaction_request_for_local_signing(web3, Some(from), options)
442 .immediate()
443 .expect("failed to build local transaction");
444
445 transport.assert_request(
446 "eth_estimateGas",
447 &[json!({ "from": json!(from) , "gasPrice": format!("{:#x}", 1337)})],
448 );
449 transport.assert_no_more_requests();
450
451 assert_eq!(tx.from, from);
452 assert_eq!(tx.gas_price, Some(1337.into()));
453 }
454
455 #[test]
456 fn tx_build_local_no_local_accounts() {
457 let mut transport = TestTransport::new();
458 let web3 = Web3::new(transport.clone());
459
460 transport.add_response(json!([])); let err = build_transaction_request_for_local_signing(
462 web3,
463 None,
464 TransactionRequestOptions::default(),
465 )
466 .immediate()
467 .expect_err("unexpected success building transaction");
468
469 transport.assert_request("eth_accounts", &[]);
470 transport.assert_no_more_requests();
471
472 assert!(
473 matches!(err, ExecutionError::NoLocalAccounts),
474 "expected no local accounts error but got '{:?}'",
475 err
476 );
477 }
478
479 #[test]
480 fn tx_build_locked() {
481 let mut transport = TestTransport::new();
482 let web3 = Web3::new(transport.clone());
483
484 let from = addr!("0x9876543210987654321098765432109876543210");
485 let pw = "foobar";
486 let to = addr!("0x0000000000000000000000000000000000000000");
487 let signed = bytes!("0x0123456789"); let hash = H256::from_low_u64_be(1);
489 let gas = json!("0x9a5");
490
491 transport.add_response(gas.clone());
492 transport.add_response(json!({
493 "raw": signed,
494 "tx": {
495 "hash": "0x0000000000000000000000000000000000000000000000000000000000000001",
496 "nonce": "0x0",
497 "from": from,
498 "value": "0x0",
499 "gas": "0x0",
500 "gasPrice": "0x0",
501 "input": "0x",
502 }
503 })); let tx = build_transaction_signed_with_locked_account(
505 web3,
506 from,
507 pw.into(),
508 TransactionRequestOptions(
509 TransactionOptions {
510 to: Some(to),
511 ..Default::default()
512 },
513 None,
514 ),
515 )
516 .immediate()
517 .expect("failed to build locked transaction");
518
519 transport.assert_request(
520 "eth_estimateGas",
521 &[json!({ "from": json!(from) , "to": json!(to)})],
522 );
523 transport.assert_request(
524 "personal_signTransaction",
525 &[
526 json!({
527 "from": from,
528 "to": to,
529 "gas": gas
530 }),
531 json!(pw),
532 ],
533 );
534 transport.assert_no_more_requests();
535
536 assert_eq!(tx.raw, signed);
537 assert_eq!(tx.tx.hash, hash);
538 }
539
540 #[test]
541 fn tx_build_offline() {
542 let mut transport = TestTransport::new();
543 let web3 = Web3::new(transport.clone());
544
545 let key = key!("0x0102030405060708091011121314151617181920212223242526272829303132");
546 let from: Address = key.public_address();
547 let to = addr!("0x0000000000000000000000000000000000000000");
548
549 let gas = uint!("0x9a5");
550 let gas_price = uint!("0x1ce");
551 let nonce = uint!("0x42");
552 let chain_id = 77777;
553
554 transport.add_response(json!(gas));
555 transport.add_response(json!(nonce));
556 transport.add_response(json!(format!("{:#x}", chain_id)));
557
558 let tx1 = build_offline_signed_transaction(
559 web3.clone(),
560 key.clone(),
561 None,
562 TransactionOptions {
563 to: Some(to),
564 gas_price: Some(gas_price.into()),
565 ..Default::default()
566 },
567 )
568 .immediate()
569 .expect("failed to build offline transaction");
570
571 transport.assert_request(
573 "eth_estimateGas",
574 &[json!({
575 "from": from,
576 "to": to,
577 "gasPrice": gas_price,
578 })],
579 );
580 transport.assert_request("eth_getTransactionCount", &[json!(from), json!("latest")]);
581 transport.assert_request("eth_chainId", &[]);
582 transport.assert_no_more_requests();
583
584 let tx2 = build_offline_signed_transaction(
585 web3,
586 key,
587 Some(chain_id),
588 TransactionOptions {
589 to: Some(to),
590 gas: Some(gas),
591 gas_price: Some(gas_price.into()),
592 nonce: Some(nonce),
593 ..Default::default()
594 },
595 )
596 .immediate()
597 .expect("failed to build offline transaction");
598
599 transport.assert_no_more_requests();
601
602 assert_eq!(tx1, tx2);
604 }
605}