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, TransactionInputKind};
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 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 fn complete_type(&self, ty: TxType) -> Result<(), Vec<&'static str>> {
119 match ty {
120 TxType::Legacy => self.complete_legacy(),
121 TxType::Eip2930 => self.complete_2930(),
122 TxType::Eip1559 => self.complete_1559(),
123 TxType::Eip4844 => self.complete_4844(),
124 TxType::Eip7702 => self.complete_7702(),
125 }
126 }
127
128 fn can_submit(&self) -> bool {
129 self.from.is_some()
133 }
134
135 fn can_build(&self) -> bool {
136 let common = self.gas.is_some() && self.nonce.is_some();
141
142 let legacy = self.gas_price.is_some();
143 let eip2930 = legacy && self.access_list().is_some();
144
145 let eip1559 = self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some();
146
147 let eip4844 = eip1559 && self.sidecar.is_some() && self.to.is_some();
148
149 let eip7702 = eip1559 && self.authorization_list().is_some();
150 common && (legacy || eip2930 || eip1559 || eip4844 || eip7702)
151 }
152
153 #[doc(alias = "output_transaction_type")]
154 fn output_tx_type(&self) -> TxType {
155 self.preferred_type()
156 }
157
158 #[doc(alias = "output_transaction_type_checked")]
159 fn output_tx_type_checked(&self) -> Option<TxType> {
160 self.buildable_type()
161 }
162
163 fn prep_for_submission(&mut self) {
164 self.transaction_type = Some(self.preferred_type() as u8);
165 self.trim_conflicting_keys();
166 self.populate_blob_hashes();
167 }
168
169 fn build_unsigned(self) -> BuildResult<TypedTransaction, Ethereum> {
170 if let Err((tx_type, missing)) = self.missing_keys() {
171 return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
172 .into_unbuilt(self));
173 }
174 Ok(self.build_typed_tx().expect("checked by missing_keys"))
175 }
176
177 async fn build<W: NetworkWallet<Ethereum>>(
178 self,
179 wallet: &W,
180 ) -> Result<<Ethereum as Network>::TxEnvelope, TransactionBuilderError<Ethereum>> {
181 Ok(wallet.sign_request(self).await?)
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use crate::{
188 TransactionBuilder, TransactionBuilder4844, TransactionBuilder7702, TransactionBuilderError,
189 };
190 use alloy_consensus::{BlobTransactionSidecar, TxEip1559, TxType, TypedTransaction};
191 use alloy_eips::eip7702::Authorization;
192 use alloy_primitives::{Address, Signature, U256};
193 use alloy_rpc_types_eth::{AccessList, TransactionRequest};
194 use std::str::FromStr;
195
196 #[test]
197 fn from_eip1559_to_tx_req() {
198 let tx = TxEip1559 {
199 chain_id: 1,
200 nonce: 0,
201 gas_limit: 21_000,
202 to: Address::ZERO.into(),
203 max_priority_fee_per_gas: 20e9 as u128,
204 max_fee_per_gas: 20e9 as u128,
205 ..Default::default()
206 };
207 let tx_req: TransactionRequest = tx.into();
208 tx_req.build_unsigned().unwrap();
209 }
210
211 #[test]
212 fn test_4844_when_sidecar() {
213 let request = TransactionRequest::default()
214 .with_nonce(1)
215 .with_gas_limit(0)
216 .with_max_fee_per_gas(0)
217 .with_max_priority_fee_per_gas(0)
218 .with_to(Address::ZERO)
219 .with_blob_sidecar(BlobTransactionSidecar::default())
220 .with_max_fee_per_blob_gas(0);
221
222 let tx = request.clone().build_unsigned().unwrap();
223
224 assert!(matches!(tx, TypedTransaction::Eip4844(_)));
225
226 let tx = request.with_gas_price(0).build_unsigned().unwrap();
227
228 assert!(matches!(tx, TypedTransaction::Eip4844(_)));
229 }
230
231 #[test]
232 fn test_2930_when_access_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_gas_price(0)
240 .with_access_list(AccessList::default());
241
242 let tx = request.build_unsigned().unwrap();
243
244 assert!(matches!(tx, TypedTransaction::Eip2930(_)));
245 }
246
247 #[test]
248 fn test_7702_when_authorization_list() {
249 let request = TransactionRequest::default()
250 .with_nonce(1)
251 .with_gas_limit(0)
252 .with_max_fee_per_gas(0)
253 .with_max_priority_fee_per_gas(0)
254 .with_to(Address::ZERO)
255 .with_access_list(AccessList::default())
256 .with_authorization_list(vec![(Authorization {
257 chain_id: U256::from(1),
258 address: Address::left_padding_from(&[1]),
259 nonce: 1u64,
260 })
261 .into_signed(Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap())],);
262
263 let tx = request.build_unsigned().unwrap();
264
265 assert!(matches!(tx, TypedTransaction::Eip7702(_)));
266 }
267
268 #[test]
269 fn test_default_to_1559() {
270 let request = TransactionRequest::default()
271 .with_nonce(1)
272 .with_gas_limit(0)
273 .with_max_fee_per_gas(0)
274 .with_max_priority_fee_per_gas(0)
275 .with_to(Address::ZERO);
276
277 let tx = request.clone().build_unsigned().unwrap();
278
279 assert!(matches!(tx, TypedTransaction::Eip1559(_)));
280
281 let request = request.with_gas_price(0);
282 let tx = request.build_unsigned().unwrap();
283 assert!(matches!(tx, TypedTransaction::Legacy(_)));
284 }
285
286 #[test]
287 fn test_fail_when_sidecar_and_access_list() {
288 let request = TransactionRequest::default()
289 .with_blob_sidecar(BlobTransactionSidecar::default())
290 .with_access_list(AccessList::default());
291
292 let error = request.build_unsigned().unwrap_err();
293
294 assert!(matches!(error.error, TransactionBuilderError::InvalidTransactionRequest(_, _)));
295 }
296
297 #[test]
298 fn test_invalid_legacy_fields() {
299 let request = TransactionRequest::default().with_gas_price(0);
300
301 let error = request.build_unsigned().unwrap_err();
302
303 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
304 else {
305 panic!("wrong variant")
306 };
307
308 assert_eq!(tx_type, TxType::Legacy);
309 assert_eq!(errors.len(), 3);
310 assert!(errors.contains(&"to"));
311 assert!(errors.contains(&"nonce"));
312 assert!(errors.contains(&"gas_limit"));
313 }
314
315 #[test]
316 fn test_invalid_1559_fields() {
317 let request = TransactionRequest::default();
318
319 let error = request.build_unsigned().unwrap_err();
320
321 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
322 else {
323 panic!("wrong variant")
324 };
325
326 assert_eq!(tx_type, TxType::Eip1559);
327 assert_eq!(errors.len(), 5);
328 assert!(errors.contains(&"to"));
329 assert!(errors.contains(&"nonce"));
330 assert!(errors.contains(&"gas_limit"));
331 assert!(errors.contains(&"max_priority_fee_per_gas"));
332 assert!(errors.contains(&"max_fee_per_gas"));
333 }
334
335 #[test]
336 fn test_invalid_2930_fields() {
337 let request = TransactionRequest::default()
338 .with_access_list(AccessList::default())
339 .with_gas_price(Default::default());
340
341 let error = request.build_unsigned().unwrap_err();
342
343 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
344 else {
345 panic!("wrong variant")
346 };
347
348 assert_eq!(tx_type, TxType::Eip2930);
349 assert_eq!(errors.len(), 3);
350 assert!(errors.contains(&"to"));
351 assert!(errors.contains(&"nonce"));
352 assert!(errors.contains(&"gas_limit"));
353 }
354
355 #[test]
356 fn test_invalid_4844_fields() {
357 let request =
358 TransactionRequest::default().with_blob_sidecar(BlobTransactionSidecar::default());
359
360 let error = request.build_unsigned().unwrap_err();
361
362 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
363 else {
364 panic!("wrong variant")
365 };
366
367 assert_eq!(tx_type, TxType::Eip4844);
368 assert_eq!(errors.len(), 6);
369 assert!(errors.contains(&"to"));
370 assert!(errors.contains(&"nonce"));
371 assert!(errors.contains(&"gas_limit"));
372 assert!(errors.contains(&"max_priority_fee_per_gas"));
373 assert!(errors.contains(&"max_fee_per_gas"));
374 assert!(errors.contains(&"max_fee_per_blob_gas"));
375 }
376
377 #[test]
378 fn test_invalid_7702_fields() {
379 let request = TransactionRequest::default().with_authorization_list(vec![]);
380
381 let error = request.build_unsigned().unwrap_err();
382
383 let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
384 else {
385 panic!("wrong variant")
386 };
387
388 assert_eq!(tx_type, TxType::Eip7702);
389 assert_eq!(errors.len(), 5);
390 assert!(errors.contains(&"to"));
391 assert!(errors.contains(&"nonce"));
392 assert!(errors.contains(&"gas_limit"));
393 assert!(errors.contains(&"max_priority_fee_per_gas"));
394 assert!(errors.contains(&"max_fee_per_gas"));
395 }
396}