alloy_network/ethereum/
builder.rs

1use 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        // value and data may be None. If they are, they will be set to default.
114        // gas fields and nonce may be None, if they are, they will be populated
115        // with default values by the RPC server
116        self.from.is_some()
117    }
118
119    fn can_build(&self) -> bool {
120        // value and data may be none. If they are, they will be set to default
121        // values.
122
123        // chain_id and from may be none.
124        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}