alloy_network/ethereum/
builder.rs1use crate::{
2 BuildResult, Ethereum, Network, NetworkWallet, TransactionBuilder, TransactionBuilder7702,
3 TransactionBuilderError,
4};
5use alloy_consensus::{TxType, TypedTransaction};
6use alloy_primitives::{Address, Bytes, ChainId, TxKind, U256};
7use alloy_rpc_types_eth::{request::TransactionRequest, AccessList};
8
9impl TransactionBuilder<Ethereum> 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 input(&self) -> Option<&Bytes> {
27 self.input.input()
28 }
29
30 fn set_input<T: Into<Bytes>>(&mut self, input: T) {
31 self.input.input = Some(input.into());
32 }
33
34 fn from(&self) -> Option<Address> {
35 self.from
36 }
37
38 fn set_from(&mut self, from: Address) {
39 self.from = Some(from);
40 }
41
42 fn kind(&self) -> Option<TxKind> {
43 self.to
44 }
45
46 fn clear_kind(&mut self) {
47 self.to = None;
48 }
49
50 fn set_kind(&mut self, kind: TxKind) {
51 self.to = Some(kind);
52 }
53
54 fn value(&self) -> Option<U256> {
55 self.value
56 }
57
58 fn set_value(&mut self, value: U256) {
59 self.value = Some(value)
60 }
61
62 fn gas_price(&self) -> Option<u128> {
63 self.gas_price
64 }
65
66 fn set_gas_price(&mut self, gas_price: u128) {
67 self.gas_price = Some(gas_price);
68 }
69
70 fn max_fee_per_gas(&self) -> Option<u128> {
71 self.max_fee_per_gas
72 }
73
74 fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
75 self.max_fee_per_gas = Some(max_fee_per_gas);
76 }
77
78 fn max_priority_fee_per_gas(&self) -> Option<u128> {
79 self.max_priority_fee_per_gas
80 }
81
82 fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
83 self.max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
84 }
85
86 fn gas_limit(&self) -> Option<u64> {
87 self.gas
88 }
89
90 fn set_gas_limit(&mut self, gas_limit: u64) {
91 self.gas = Some(gas_limit);
92 }
93
94 fn access_list(&self) -> Option<&AccessList> {
95 self.access_list.as_ref()
96 }
97
98 fn set_access_list(&mut self, access_list: AccessList) {
99 self.access_list = Some(access_list);
100 }
101
102 fn complete_type(&self, ty: TxType) -> Result<(), Vec<&'static str>> {
103 match ty {
104 TxType::Legacy => self.complete_legacy(),
105 TxType::Eip2930 => self.complete_2930(),
106 TxType::Eip1559 => self.complete_1559(),
107 TxType::Eip4844 => self.complete_4844(),
108 TxType::Eip7702 => self.complete_7702(),
109 }
110 }
111
112 fn can_submit(&self) -> bool {
113 self.from.is_some()
117 }
118
119 fn can_build(&self) -> bool {
120 let common = self.gas.is_some() && self.nonce.is_some();
125
126 let legacy = self.gas_price.is_some();
127 let eip2930 = legacy && self.access_list().is_some();
128
129 let eip1559 = self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some();
130
131 let eip4844 = eip1559 && self.sidecar.is_some() && self.to.is_some();
132
133 let eip7702 = eip1559 && self.authorization_list().is_some();
134 common && (legacy || eip2930 || eip1559 || eip4844 || eip7702)
135 }
136
137 #[doc(alias = "output_transaction_type")]
138 fn output_tx_type(&self) -> TxType {
139 self.preferred_type()
140 }
141
142 #[doc(alias = "output_transaction_type_checked")]
143 fn output_tx_type_checked(&self) -> Option<TxType> {
144 self.buildable_type()
145 }
146
147 fn prep_for_submission(&mut self) {
148 self.transaction_type = Some(self.preferred_type() as u8);
149 self.trim_conflicting_keys();
150 self.populate_blob_hashes();
151 }
152
153 fn build_unsigned(self) -> BuildResult<TypedTransaction, Ethereum> {
154 if let Err((tx_type, missing)) = self.missing_keys() {
155 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
156 .into_unbuilt(self));
157 }
158 Ok(self.build_typed_tx().expect("checked by missing_keys"))
159 }
160
161 async fn build<W: NetworkWallet<Ethereum>>(
162 self,
163 wallet: &W,
164 ) -> Result<<Ethereum as Network>::TxEnvelope, TransactionBuilderError<Ethereum>> {
165 Ok(wallet.sign_request(self).await?)
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use crate::{
172 TransactionBuilder, TransactionBuilder4844, TransactionBuilder7702, TransactionBuilderError,
173 };
174 use alloy_consensus::{BlobTransactionSidecar, TxEip1559, TxType, TypedTransaction};
175 use alloy_eips::eip7702::Authorization;
176 use alloy_primitives::{Address, PrimitiveSignature as Signature, U256};
177 use alloy_rpc_types_eth::{AccessList, TransactionRequest};
178 use std::str::FromStr;
179
180 #[test]
181 fn from_eip1559_to_tx_req() {
182 let tx = TxEip1559 {
183 chain_id: 1,
184 nonce: 0,
185 gas_limit: 21_000,
186 to: Address::ZERO.into(),
187 max_priority_fee_per_gas: 20e9 as u128,
188 max_fee_per_gas: 20e9 as u128,
189 ..Default::default()
190 };
191 let tx_req: TransactionRequest = tx.into();
192 tx_req.build_unsigned().unwrap();
193 }
194
195 #[test]
196 fn test_4844_when_sidecar() {
197 let request = TransactionRequest::default()
198 .with_nonce(1)
199 .with_gas_limit(0)
200 .with_max_fee_per_gas(0)
201 .with_max_priority_fee_per_gas(0)
202 .with_to(Address::ZERO)
203 .with_blob_sidecar(BlobTransactionSidecar::default())
204 .with_max_fee_per_blob_gas(0);
205
206 let tx = request.clone().build_unsigned().unwrap();
207
208 assert!(matches!(tx, TypedTransaction::Eip4844(_)));
209
210 let tx = request.with_gas_price(0).build_unsigned().unwrap();
211
212 assert!(matches!(tx, TypedTransaction::Eip4844(_)));
213 }
214
215 #[test]
216 fn test_2930_when_access_list() {
217 let request = TransactionRequest::default()
218 .with_nonce(1)
219 .with_gas_limit(0)
220 .with_max_fee_per_gas(0)
221 .with_max_priority_fee_per_gas(0)
222 .with_to(Address::ZERO)
223 .with_gas_price(0)
224 .with_access_list(AccessList::default());
225
226 let tx = request.build_unsigned().unwrap();
227
228 assert!(matches!(tx, TypedTransaction::Eip2930(_)));
229 }
230
231 #[test]
232 fn test_7702_when_authorization_list() {
233 let request = TransactionRequest::default()
234 .with_nonce(1)
235 .with_gas_limit(0)
236 .with_max_fee_per_gas(0)
237 .with_max_priority_fee_per_gas(0)
238 .with_to(Address::ZERO)
239 .with_access_list(AccessList::default())
240 .with_authorization_list(vec![(Authorization {
241 chain_id: U256::from(1),
242 address: Address::left_padding_from(&[1]),
243 nonce: 1u64,
244 })
245 .into_signed(Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap())],);
246
247 let tx = request.build_unsigned().unwrap();
248
249 assert!(matches!(tx, TypedTransaction::Eip7702(_)));
250 }
251
252 #[test]
253 fn test_default_to_1559() {
254 let request = TransactionRequest::default()
255 .with_nonce(1)
256 .with_gas_limit(0)
257 .with_max_fee_per_gas(0)
258 .with_max_priority_fee_per_gas(0)
259 .with_to(Address::ZERO);
260
261 let tx = request.clone().build_unsigned().unwrap();
262
263 assert!(matches!(tx, TypedTransaction::Eip1559(_)));
264
265 let request = request.with_gas_price(0);
266 let tx = request.build_unsigned().unwrap();
267 assert!(matches!(tx, TypedTransaction::Legacy(_)));
268 }
269
270 #[test]
271 fn test_fail_when_sidecar_and_access_list() {
272 let request = TransactionRequest::default()
273 .with_blob_sidecar(BlobTransactionSidecar::default())
274 .with_access_list(AccessList::default());
275
276 let error = request.build_unsigned().unwrap_err();
277
278 assert!(matches!(error.error, TransactionBuilderError::InvalidTransactionRequest(_, _)));
279 }
280
281 #[test]
282 fn test_invalid_legacy_fields() {
283 let request = TransactionRequest::default().with_gas_price(0);
284
285 let error = request.build_unsigned().unwrap_err();
286
287 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
288 else {
289 panic!("wrong variant")
290 };
291
292 assert_eq!(tx_type, TxType::Legacy);
293 assert_eq!(errors.len(), 3);
294 assert!(errors.contains(&"to"));
295 assert!(errors.contains(&"nonce"));
296 assert!(errors.contains(&"gas_limit"));
297 }
298
299 #[test]
300 fn test_invalid_1559_fields() {
301 let request = TransactionRequest::default();
302
303 let error = request.build_unsigned().unwrap_err();
304
305 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
306 else {
307 panic!("wrong variant")
308 };
309
310 assert_eq!(tx_type, TxType::Eip1559);
311 assert_eq!(errors.len(), 5);
312 assert!(errors.contains(&"to"));
313 assert!(errors.contains(&"nonce"));
314 assert!(errors.contains(&"gas_limit"));
315 assert!(errors.contains(&"max_priority_fee_per_gas"));
316 assert!(errors.contains(&"max_fee_per_gas"));
317 }
318
319 #[test]
320 fn test_invalid_2930_fields() {
321 let request = TransactionRequest::default()
322 .with_access_list(AccessList::default())
323 .with_gas_price(Default::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::Eip2930);
333 assert_eq!(errors.len(), 3);
334 assert!(errors.contains(&"to"));
335 assert!(errors.contains(&"nonce"));
336 assert!(errors.contains(&"gas_limit"));
337 }
338
339 #[test]
340 fn test_invalid_4844_fields() {
341 let request =
342 TransactionRequest::default().with_blob_sidecar(BlobTransactionSidecar::default());
343
344 let error = request.build_unsigned().unwrap_err();
345
346 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
347 else {
348 panic!("wrong variant")
349 };
350
351 assert_eq!(tx_type, TxType::Eip4844);
352 assert_eq!(errors.len(), 7);
353 assert!(errors.contains(&"to"));
354 assert!(errors.contains(&"nonce"));
355 assert!(errors.contains(&"gas_limit"));
356 assert!(errors.contains(&"max_priority_fee_per_gas"));
357 assert!(errors.contains(&"max_fee_per_gas"));
358 assert!(errors.contains(&"to"));
359 assert!(errors.contains(&"max_fee_per_blob_gas"));
360 }
361
362 #[test]
363 fn test_invalid_7702_fields() {
364 let request = TransactionRequest::default().with_authorization_list(vec![]);
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::Eip7702);
374 assert_eq!(errors.len(), 5);
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 }
381}