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