1use crate::{
2 BuildResult, Ethereum, Network, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder,
3 TransactionBuilder7702, TransactionBuilderError,
4};
5use alloy_consensus::{TxType, TypedTransaction};
6use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256};
7use alloy_rpc_types_eth::{request::TransactionRequest, AccessList, TransactionInputKind};
8
9impl TransactionBuilder for TransactionRequest {
10 fn chain_id(&self) -> Option<ChainId> {
11 self.chain_id
12 }
13
14 fn set_chain_id(&mut self, chain_id: ChainId) {
15 self.chain_id = Some(chain_id);
16 }
17
18 fn nonce(&self) -> Option<u64> {
19 self.nonce
20 }
21
22 fn set_nonce(&mut self, nonce: u64) {
23 self.nonce = Some(nonce);
24 }
25
26 fn take_nonce(&mut self) -> Option<u64> {
27 self.nonce.take()
28 }
29
30 fn input(&self) -> Option<&Bytes> {
31 self.input.input()
32 }
33
34 fn set_input<T: Into<Bytes>>(&mut self, input: T) {
35 self.input.input = Some(input.into());
36 }
37
38 fn set_input_kind<T: Into<Bytes>>(&mut self, input: T, kind: TransactionInputKind) {
39 match kind {
40 TransactionInputKind::Input => self.input.input = Some(input.into()),
41 TransactionInputKind::Data => self.input.data = Some(input.into()),
42 TransactionInputKind::Both => {
43 let bytes = input.into();
44 self.input.input = Some(bytes.clone());
45 self.input.data = Some(bytes);
46 }
47 }
48 }
49
50 fn from(&self) -> Option<Address> {
51 self.from
52 }
53
54 fn set_from(&mut self, from: Address) {
55 self.from = Some(from);
56 }
57
58 fn kind(&self) -> Option<TxKind> {
59 self.to
60 }
61
62 fn clear_kind(&mut self) {
63 self.to = None;
64 }
65
66 fn set_kind(&mut self, kind: TxKind) {
67 self.to = Some(kind);
68 }
69
70 fn value(&self) -> Option<U256> {
71 self.value
72 }
73
74 fn set_value(&mut self, value: U256) {
75 self.value = Some(value)
76 }
77
78 fn gas_price(&self) -> Option<u128> {
79 self.gas_price
80 }
81
82 fn set_gas_price(&mut self, gas_price: u128) {
83 self.gas_price = Some(gas_price);
84 }
85
86 fn max_fee_per_gas(&self) -> Option<u128> {
87 self.max_fee_per_gas
88 }
89
90 fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
91 self.max_fee_per_gas = Some(max_fee_per_gas);
92 }
93
94 fn max_priority_fee_per_gas(&self) -> Option<u128> {
95 self.max_priority_fee_per_gas
96 }
97
98 fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
99 self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
100 }
101
102 fn gas_limit(&self) -> Option<u64> {
103 self.gas
104 }
105
106 fn set_gas_limit(&mut self, gas_limit: u64) {
107 self.gas = Some(gas_limit);
108 }
109
110 fn access_list(&self) -> Option<&AccessList> {
111 self.access_list.as_ref()
112 }
113
114 fn set_access_list(&mut self, access_list: AccessList) {
115 self.access_list = Some(access_list);
116 }
117}
118
119impl NetworkTransactionBuilder<Ethereum> for TransactionRequest {
120 fn can_submit(&self) -> bool {
121 self.from.is_some()
125 }
126
127 fn can_build(&self) -> bool {
128 let common = self.gas.is_some() && self.nonce.is_some();
133
134 let legacy = self.gas_price.is_some();
135 let eip2930 = legacy && self.access_list().is_some();
136
137 let eip1559 = self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some();
138
139 let eip4844 = eip1559 && self.sidecar.is_some() && self.to.is_some();
140
141 let eip7702 = eip1559 && self.authorization_list().is_some();
142 common && (legacy || eip2930 || eip1559 || eip4844 || eip7702)
143 }
144
145 fn complete_type(&self, ty: TxType) -> Result<(), Vec<&'static str>> {
146 match ty {
147 TxType::Legacy => self.complete_legacy(),
148 TxType::Eip2930 => self.complete_2930(),
149 TxType::Eip1559 => self.complete_1559(),
150 TxType::Eip4844 => self.complete_4844(),
151 TxType::Eip7702 => self.complete_7702(),
152 }
153 }
154
155 #[doc(alias = "output_transaction_type")]
156 fn output_tx_type(&self) -> TxType {
157 self.preferred_type()
158 }
159
160 #[doc(alias = "output_transaction_type_checked")]
161 fn output_tx_type_checked(&self) -> Option<TxType> {
162 self.buildable_type()
163 }
164
165 fn prep_for_submission(&mut self) {
166 self.transaction_type = Some(self.preferred_type() as u8);
167 self.trim_conflicting_keys();
168 self.populate_blob_hashes();
169 }
170
171 fn build_unsigned(self) -> BuildResult<TypedTransaction, Ethereum> {
172 if let Err((tx_type, missing)) = self.missing_keys() {
173 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
174 .into_unbuilt(self));
175 }
176 Ok(self.build_typed_tx().expect("checked by missing_keys"))
177 }
178
179 async fn build<W: NetworkWallet<Ethereum>>(
180 self,
181 wallet: &W,
182 ) -> Result<<Ethereum as Network>::TxEnvelope, TransactionBuilderError<Ethereum>> {
183 Ok(wallet.sign_request(self).await?)
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use crate::{
190 NetworkTransactionBuilder, TransactionBuilder, TransactionBuilder4844,
191 TransactionBuilder7702, TransactionBuilderError,
192 };
193 use alloy_consensus::{
194 transaction::Recovered, BlobTransactionSidecar, SignableTransaction, TxEip1559, TxEnvelope,
195 TxType, TypedTransaction,
196 };
197 use alloy_eips::eip7702::Authorization;
198 use alloy_primitives::{Address, Bytes, Signature, TxKind, B256, U160, U256};
199 use alloy_rpc_types_eth::{AccessList, TransactionRequest};
200 use std::str::FromStr;
201
202 #[test]
203 fn from_eip1559_to_tx_req() {
204 let tx = TxEip1559 {
205 chain_id: 1,
206 nonce: 0,
207 gas_limit: 21_000,
208 to: Address::ZERO.into(),
209 max_priority_fee_per_gas: 20e9 as u128,
210 max_fee_per_gas: 20e9 as u128,
211 ..Default::default()
212 };
213 let tx_req: TransactionRequest = tx.into();
214 tx_req.build_unsigned().unwrap();
215 }
216
217 #[test]
218 fn test_4844_when_sidecar() {
219 let request = TransactionRequest::default()
220 .with_nonce(1)
221 .with_gas_limit(0)
222 .with_max_fee_per_gas(0)
223 .with_max_priority_fee_per_gas(0)
224 .with_to(Address::ZERO)
225 .with_blob_sidecar_4844(BlobTransactionSidecar::default())
226 .with_max_fee_per_blob_gas(0);
227
228 let tx = request.clone().build_unsigned().unwrap();
229
230 assert!(matches!(tx, TypedTransaction::Eip4844(_)));
231
232 let tx = request.with_gas_price(0).build_unsigned().unwrap();
233
234 assert!(matches!(tx, TypedTransaction::Eip4844(_)));
235 }
236
237 #[test]
238 fn test_2930_when_access_list() {
239 let request = TransactionRequest::default()
240 .with_nonce(1)
241 .with_gas_limit(0)
242 .with_max_fee_per_gas(0)
243 .with_max_priority_fee_per_gas(0)
244 .with_to(Address::ZERO)
245 .with_gas_price(0)
246 .with_access_list(AccessList::default());
247
248 let tx = request.build_unsigned().unwrap();
249
250 assert!(matches!(tx, TypedTransaction::Eip2930(_)));
251 }
252
253 #[test]
254 fn test_7702_when_authorization_list() {
255 let request = TransactionRequest::default()
256 .with_nonce(1)
257 .with_gas_limit(0)
258 .with_max_fee_per_gas(0)
259 .with_max_priority_fee_per_gas(0)
260 .with_to(Address::ZERO)
261 .with_access_list(AccessList::default())
262 .with_authorization_list(vec![(Authorization {
263 chain_id: U256::from(1),
264 address: Address::left_padding_from(&[1]),
265 nonce: 1u64,
266 })
267 .into_signed(Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap())],);
268
269 let tx = request.build_unsigned().unwrap();
270
271 assert!(matches!(tx, TypedTransaction::Eip7702(_)));
272 }
273
274 #[test]
275 fn test_default_to_1559() {
276 let request = TransactionRequest::default()
277 .with_nonce(1)
278 .with_gas_limit(0)
279 .with_max_fee_per_gas(0)
280 .with_max_priority_fee_per_gas(0)
281 .with_to(Address::ZERO);
282
283 let tx = request.clone().build_unsigned().unwrap();
284
285 assert!(matches!(tx, TypedTransaction::Eip1559(_)));
286
287 let request = request.with_gas_price(0);
288 let tx = request.build_unsigned().unwrap();
289 assert!(matches!(tx, TypedTransaction::Legacy(_)));
290 }
291
292 #[test]
293 fn test_fail_when_sidecar_and_access_list() {
294 let request = TransactionRequest::default()
295 .with_blob_sidecar_4844(BlobTransactionSidecar::default())
296 .with_access_list(AccessList::default());
297
298 let error = request.build_unsigned().unwrap_err();
299
300 assert!(matches!(error.error, TransactionBuilderError::InvalidTransactionRequest(_, _)));
301 }
302
303 #[test]
304 fn test_invalid_legacy_fields() {
305 let request = TransactionRequest::default().with_gas_price(0);
306
307 let error = request.build_unsigned().unwrap_err();
308
309 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
310 else {
311 panic!("wrong variant")
312 };
313
314 assert_eq!(tx_type, TxType::Legacy);
315 assert_eq!(errors.len(), 3);
316 assert!(errors.contains(&"to"));
317 assert!(errors.contains(&"nonce"));
318 assert!(errors.contains(&"gas_limit"));
319 }
320
321 #[test]
322 fn test_invalid_1559_fields() {
323 let request = TransactionRequest::default();
324
325 let error = request.build_unsigned().unwrap_err();
326
327 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
328 else {
329 panic!("wrong variant")
330 };
331
332 assert_eq!(tx_type, TxType::Eip1559);
333 assert_eq!(errors.len(), 5);
334 assert!(errors.contains(&"to"));
335 assert!(errors.contains(&"nonce"));
336 assert!(errors.contains(&"gas_limit"));
337 assert!(errors.contains(&"max_priority_fee_per_gas"));
338 assert!(errors.contains(&"max_fee_per_gas"));
339 }
340
341 #[test]
342 fn test_invalid_2930_fields() {
343 let request = TransactionRequest::default()
344 .with_access_list(AccessList::default())
345 .with_gas_price(Default::default());
346
347 let error = request.build_unsigned().unwrap_err();
348
349 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
350 else {
351 panic!("wrong variant")
352 };
353
354 assert_eq!(tx_type, TxType::Eip2930);
355 assert_eq!(errors.len(), 3);
356 assert!(errors.contains(&"to"));
357 assert!(errors.contains(&"nonce"));
358 assert!(errors.contains(&"gas_limit"));
359 }
360
361 #[test]
362 fn test_invalid_4844_fields() {
363 let request =
364 TransactionRequest::default().with_blob_sidecar_4844(BlobTransactionSidecar::default());
365
366 let error = request.build_unsigned().unwrap_err();
367
368 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
369 else {
370 panic!("wrong variant")
371 };
372
373 assert_eq!(tx_type, TxType::Eip4844);
374 assert_eq!(errors.len(), 6);
375 assert!(errors.contains(&"to"));
376 assert!(errors.contains(&"nonce"));
377 assert!(errors.contains(&"gas_limit"));
378 assert!(errors.contains(&"max_priority_fee_per_gas"));
379 assert!(errors.contains(&"max_fee_per_gas"));
380 assert!(errors.contains(&"max_fee_per_blob_gas"));
381 }
382
383 #[test]
384 fn test_tx_response_into_req() {
385 let from = Address::from(U160::from(1));
386 let to = Address::from(U160::from(1));
387 let access_list_item = alloy_rpc_types_eth::AccessListItem {
388 address: Address::from(U160::from(3)),
389 storage_keys: vec![B256::from(U256::from(4)), B256::from(U256::from(5))],
390 };
391 let tx = TxEip1559 {
392 chain_id: 1337,
393 nonce: 12,
394 max_priority_fee_per_gas: 123,
395 max_fee_per_gas: 1234,
396 gas_limit: 21000,
397 to: TxKind::Call(to),
398 value: U256::from(111),
399 access_list: AccessList::from(vec![access_list_item.clone()]),
400 input: Bytes::new(),
401 };
402 let envelope =
403 TxEnvelope::Eip1559(tx.into_signed(Signature::new(U256::ZERO, U256::ZERO, false)));
404 let tx_response = alloy_rpc_types_eth::Transaction {
405 inner: Recovered::new_unchecked(envelope, from),
406 effective_gas_price: Some(1000),
407 block_hash: None,
408 block_number: None,
409 block_timestamp: None,
410 transaction_index: None,
411 };
412
413 let req: TransactionRequest = tx_response.into();
416
417 assert_eq!(TransactionBuilder::from(&req).unwrap(), from);
418 assert_eq!(TransactionBuilder::chain_id(&req).unwrap(), 1337);
419 assert_eq!(TransactionBuilder::nonce(&req).unwrap(), 12);
420 assert_eq!(TransactionBuilder::max_priority_fee_per_gas(&req).unwrap(), 123);
421 assert_eq!(TransactionBuilder::max_fee_per_gas(&req).unwrap(), 1234);
422 assert_eq!(TransactionBuilder::gas_limit(&req).unwrap(), 21000);
423 assert_eq!(TransactionBuilder::to(&req).unwrap(), to);
424 assert_eq!(TransactionBuilder::value(&req).unwrap(), 111);
425 assert_eq!(
426 *TransactionBuilder::access_list(&req).unwrap(),
427 AccessList::from(vec![access_list_item])
428 );
429 assert_eq!(*TransactionBuilder::input(&req).unwrap(), Bytes::new());
430 }
431
432 #[test]
433 fn test_invalid_7702_fields() {
434 let request = TransactionRequest::default().with_authorization_list(vec![]);
435
436 let error = request.build_unsigned().unwrap_err();
437
438 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
439 else {
440 panic!("wrong variant")
441 };
442
443 assert_eq!(tx_type, TxType::Eip7702);
444 assert_eq!(errors.len(), 5);
445 assert!(errors.contains(&"to"));
446 assert!(errors.contains(&"nonce"));
447 assert!(errors.contains(&"gas_limit"));
448 assert!(errors.contains(&"max_priority_fee_per_gas"));
449 assert!(errors.contains(&"max_fee_per_gas"));
450 }
451}