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, 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        // value and data may be None. If they are, they will be set to default.
126        // gas fields and nonce may be None, if they are, they will be populated
127        // with default values by the RPC server
128        self.from.is_some()
129    }
130
131    fn can_build(&self) -> bool {
132        // value and data may be none. If they are, they will be set to default
133        // values.
134
135        // chain_id and from may be none.
136        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}