Skip to main content

alloy_network/ethereum/
builder.rs

1use crate::{
2    BuildResult, Ethereum, Network, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder,
3    TransactionBuilder7702, 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 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
119impl NetworkTransactionBuilder<Ethereum> for TransactionRequest {
120    fn can_submit(&self) -> bool {
121        // value and data may be None. If they are, they will be set to default.
122        // gas fields and nonce may be None, if they are, they will be populated
123        // with default values by the RPC server
124        self.from.is_some()
125    }
126
127    fn can_build(&self) -> bool {
128        // value and data may be none. If they are, they will be set to default
129        // values.
130
131        // chain_id and from may be none.
132        let common = self.gas.is_some() && self.nonce.is_some();
133
134        let legacy = self.gas_price.is_some();
135        let eip2930 = legacy && self.access_list().is_some();
136
137        let eip1559 = self.max_fee_per_gas.is_some() && self.max_priority_fee_per_gas.is_some();
138
139        let eip4844 = eip1559 && self.sidecar.is_some() && self.to.is_some();
140
141        let eip7702 = eip1559 && self.authorization_list().is_some();
142        common && (legacy || eip2930 || eip1559 || eip4844 || eip7702)
143    }
144
145    fn complete_type(&self, ty: TxType) -> Result<(), Vec<&'static str>> {
146        match ty {
147            TxType::Legacy => self.complete_legacy(),
148            TxType::Eip2930 => self.complete_2930(),
149            TxType::Eip1559 => self.complete_1559(),
150            TxType::Eip4844 => self.complete_4844(),
151            TxType::Eip7702 => self.complete_7702(),
152        }
153    }
154
155    #[doc(alias = "output_transaction_type")]
156    fn output_tx_type(&self) -> TxType {
157        self.preferred_type()
158    }
159
160    #[doc(alias = "output_transaction_type_checked")]
161    fn output_tx_type_checked(&self) -> Option<TxType> {
162        self.buildable_type()
163    }
164
165    fn prep_for_submission(&mut self) {
166        self.transaction_type = Some(self.preferred_type() as u8);
167        self.trim_conflicting_keys();
168        self.populate_blob_hashes();
169    }
170
171    fn build_unsigned(self) -> BuildResult<TypedTransaction, Ethereum> {
172        if let Err((tx_type, missing)) = self.missing_keys() {
173            return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
174                .into_unbuilt(self));
175        }
176        Ok(self.build_typed_tx().expect("checked by missing_keys"))
177    }
178
179    async fn build<W: NetworkWallet<Ethereum>>(
180        self,
181        wallet: &W,
182    ) -> Result<<Ethereum as Network>::TxEnvelope, TransactionBuilderError<Ethereum>> {
183        Ok(wallet.sign_request(self).await?)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use crate::{
190        NetworkTransactionBuilder, TransactionBuilder, TransactionBuilder4844,
191        TransactionBuilder7702, TransactionBuilderError,
192    };
193    use alloy_consensus::{
194        transaction::Recovered, BlobTransactionSidecar, SignableTransaction, TxEip1559, TxEnvelope,
195        TxType, TypedTransaction,
196    };
197    use alloy_eips::eip7702::Authorization;
198    use alloy_primitives::{Address, Bytes, Signature, TxKind, B256, U160, U256};
199    use alloy_rpc_types_eth::{AccessList, TransactionRequest};
200    use std::str::FromStr;
201
202    #[test]
203    fn from_eip1559_to_tx_req() {
204        let tx = TxEip1559 {
205            chain_id: 1,
206            nonce: 0,
207            gas_limit: 21_000,
208            to: Address::ZERO.into(),
209            max_priority_fee_per_gas: 20e9 as u128,
210            max_fee_per_gas: 20e9 as u128,
211            ..Default::default()
212        };
213        let tx_req: TransactionRequest = tx.into();
214        tx_req.build_unsigned().unwrap();
215    }
216
217    #[test]
218    fn test_4844_when_sidecar() {
219        let request = TransactionRequest::default()
220            .with_nonce(1)
221            .with_gas_limit(0)
222            .with_max_fee_per_gas(0)
223            .with_max_priority_fee_per_gas(0)
224            .with_to(Address::ZERO)
225            .with_blob_sidecar_4844(BlobTransactionSidecar::default())
226            .with_max_fee_per_blob_gas(0);
227
228        let tx = request.clone().build_unsigned().unwrap();
229
230        assert!(matches!(tx, TypedTransaction::Eip4844(_)));
231
232        let tx = request.with_gas_price(0).build_unsigned().unwrap();
233
234        assert!(matches!(tx, TypedTransaction::Eip4844(_)));
235    }
236
237    #[test]
238    fn test_2930_when_access_list() {
239        let request = TransactionRequest::default()
240            .with_nonce(1)
241            .with_gas_limit(0)
242            .with_max_fee_per_gas(0)
243            .with_max_priority_fee_per_gas(0)
244            .with_to(Address::ZERO)
245            .with_gas_price(0)
246            .with_access_list(AccessList::default());
247
248        let tx = request.build_unsigned().unwrap();
249
250        assert!(matches!(tx, TypedTransaction::Eip2930(_)));
251    }
252
253    #[test]
254    fn test_7702_when_authorization_list() {
255        let request = TransactionRequest::default()
256            .with_nonce(1)
257            .with_gas_limit(0)
258            .with_max_fee_per_gas(0)
259            .with_max_priority_fee_per_gas(0)
260            .with_to(Address::ZERO)
261            .with_access_list(AccessList::default())
262            .with_authorization_list(vec![(Authorization {
263                chain_id: U256::from(1),
264                address: Address::left_padding_from(&[1]),
265                nonce: 1u64,
266            })
267            .into_signed(Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap())],);
268
269        let tx = request.build_unsigned().unwrap();
270
271        assert!(matches!(tx, TypedTransaction::Eip7702(_)));
272    }
273
274    #[test]
275    fn test_default_to_1559() {
276        let request = TransactionRequest::default()
277            .with_nonce(1)
278            .with_gas_limit(0)
279            .with_max_fee_per_gas(0)
280            .with_max_priority_fee_per_gas(0)
281            .with_to(Address::ZERO);
282
283        let tx = request.clone().build_unsigned().unwrap();
284
285        assert!(matches!(tx, TypedTransaction::Eip1559(_)));
286
287        let request = request.with_gas_price(0);
288        let tx = request.build_unsigned().unwrap();
289        assert!(matches!(tx, TypedTransaction::Legacy(_)));
290    }
291
292    #[test]
293    fn test_fail_when_sidecar_and_access_list() {
294        let request = TransactionRequest::default()
295            .with_blob_sidecar_4844(BlobTransactionSidecar::default())
296            .with_access_list(AccessList::default());
297
298        let error = request.build_unsigned().unwrap_err();
299
300        assert!(matches!(error.error, TransactionBuilderError::InvalidTransactionRequest(_, _)));
301    }
302
303    #[test]
304    fn test_invalid_legacy_fields() {
305        let request = TransactionRequest::default().with_gas_price(0);
306
307        let error = request.build_unsigned().unwrap_err();
308
309        let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
310        else {
311            panic!("wrong variant")
312        };
313
314        assert_eq!(tx_type, TxType::Legacy);
315        assert_eq!(errors.len(), 3);
316        assert!(errors.contains(&"to"));
317        assert!(errors.contains(&"nonce"));
318        assert!(errors.contains(&"gas_limit"));
319    }
320
321    #[test]
322    fn test_invalid_1559_fields() {
323        let request = TransactionRequest::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::Eip1559);
333        assert_eq!(errors.len(), 5);
334        assert!(errors.contains(&"to"));
335        assert!(errors.contains(&"nonce"));
336        assert!(errors.contains(&"gas_limit"));
337        assert!(errors.contains(&"max_priority_fee_per_gas"));
338        assert!(errors.contains(&"max_fee_per_gas"));
339    }
340
341    #[test]
342    fn test_invalid_2930_fields() {
343        let request = TransactionRequest::default()
344            .with_access_list(AccessList::default())
345            .with_gas_price(Default::default());
346
347        let error = request.build_unsigned().unwrap_err();
348
349        let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
350        else {
351            panic!("wrong variant")
352        };
353
354        assert_eq!(tx_type, TxType::Eip2930);
355        assert_eq!(errors.len(), 3);
356        assert!(errors.contains(&"to"));
357        assert!(errors.contains(&"nonce"));
358        assert!(errors.contains(&"gas_limit"));
359    }
360
361    #[test]
362    fn test_invalid_4844_fields() {
363        let request =
364            TransactionRequest::default().with_blob_sidecar_4844(BlobTransactionSidecar::default());
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::Eip4844);
374        assert_eq!(errors.len(), 6);
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        assert!(errors.contains(&"max_fee_per_blob_gas"));
381    }
382
383    #[test]
384    fn test_tx_response_into_req() {
385        let from = Address::from(U160::from(1));
386        let to = Address::from(U160::from(1));
387        let access_list_item = alloy_rpc_types_eth::AccessListItem {
388            address: Address::from(U160::from(3)),
389            storage_keys: vec![B256::from(U256::from(4)), B256::from(U256::from(5))],
390        };
391        let tx = TxEip1559 {
392            chain_id: 1337,
393            nonce: 12,
394            max_priority_fee_per_gas: 123,
395            max_fee_per_gas: 1234,
396            gas_limit: 21000,
397            to: TxKind::Call(to),
398            value: U256::from(111),
399            access_list: AccessList::from(vec![access_list_item.clone()]),
400            input: Bytes::new(),
401        };
402        let envelope =
403            TxEnvelope::Eip1559(tx.into_signed(Signature::new(U256::ZERO, U256::ZERO, false)));
404        let tx_response = alloy_rpc_types_eth::Transaction {
405            inner: Recovered::new_unchecked(envelope, from),
406            effective_gas_price: Some(1000),
407            block_hash: None,
408            block_number: None,
409            block_timestamp: None,
410            transaction_index: None,
411        };
412
413        // Convert the transaction response into a transaction request via
414        // From<TransactionResponse>, and check that the fields are correctly populated.
415        let req: TransactionRequest = tx_response.into();
416
417        assert_eq!(TransactionBuilder::from(&req).unwrap(), from);
418        assert_eq!(TransactionBuilder::chain_id(&req).unwrap(), 1337);
419        assert_eq!(TransactionBuilder::nonce(&req).unwrap(), 12);
420        assert_eq!(TransactionBuilder::max_priority_fee_per_gas(&req).unwrap(), 123);
421        assert_eq!(TransactionBuilder::max_fee_per_gas(&req).unwrap(), 1234);
422        assert_eq!(TransactionBuilder::gas_limit(&req).unwrap(), 21000);
423        assert_eq!(TransactionBuilder::to(&req).unwrap(), to);
424        assert_eq!(TransactionBuilder::value(&req).unwrap(), 111);
425        assert_eq!(
426            *TransactionBuilder::access_list(&req).unwrap(),
427            AccessList::from(vec![access_list_item])
428        );
429        assert_eq!(*TransactionBuilder::input(&req).unwrap(), Bytes::new());
430    }
431
432    #[test]
433    fn test_invalid_7702_fields() {
434        let request = TransactionRequest::default().with_authorization_list(vec![]);
435
436        let error = request.build_unsigned().unwrap_err();
437
438        let TransactionBuilderError::InvalidTransactionRequest(tx_type, errors) = error.error
439        else {
440            panic!("wrong variant")
441        };
442
443        assert_eq!(tx_type, TxType::Eip7702);
444        assert_eq!(errors.len(), 5);
445        assert!(errors.contains(&"to"));
446        assert!(errors.contains(&"nonce"));
447        assert!(errors.contains(&"gas_limit"));
448        assert!(errors.contains(&"max_priority_fee_per_gas"));
449        assert!(errors.contains(&"max_fee_per_gas"));
450    }
451}