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