foundry_primitives/transaction/
request.rs

1use std::ops::{Deref, DerefMut};
2
3use alloy_consensus::constants::{
4    EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID,
5    LEGACY_TX_TYPE_ID,
6};
7use alloy_network::{BuildResult, NetworkWallet, TransactionBuilder, TransactionBuilderError};
8use alloy_primitives::{Address, ChainId, TxKind, U256};
9use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest};
10use alloy_serde::WithOtherFields;
11use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpTypedTransaction};
12use op_alloy_rpc_types::OpTransactionRequest;
13use serde::{Deserialize, Serialize};
14
15use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx};
16use crate::FoundryNetwork;
17
18/// Foundry transaction request builder.
19///
20/// This is implemented as a transparent wrapper around [`OpTransactionRequest`],
21/// which provides handling of deposit transactions.
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
23pub struct FoundryTransactionRequest(OpTransactionRequest);
24
25impl Deref for FoundryTransactionRequest {
26    type Target = OpTransactionRequest;
27
28    fn deref(&self) -> &Self::Target {
29        &self.0
30    }
31}
32
33impl DerefMut for FoundryTransactionRequest {
34    fn deref_mut(&mut self) -> &mut Self::Target {
35        &mut self.0
36    }
37}
38
39impl Serialize for FoundryTransactionRequest {
40    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
41    where
42        S: serde::Serializer,
43    {
44        self.0.serialize(serializer)
45    }
46}
47
48impl<'de> Deserialize<'de> for FoundryTransactionRequest {
49    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50    where
51        D: serde::Deserializer<'de>,
52    {
53        OpTransactionRequest::deserialize(deserializer).map(Self)
54    }
55}
56
57impl From<OpTransactionRequest> for FoundryTransactionRequest {
58    fn from(req: OpTransactionRequest) -> Self {
59        Self(req)
60    }
61}
62
63impl From<FoundryTransactionRequest> for OpTransactionRequest {
64    fn from(req: FoundryTransactionRequest) -> Self {
65        req.0
66    }
67}
68
69impl From<TransactionRequest> for FoundryTransactionRequest {
70    fn from(req: TransactionRequest) -> Self {
71        Self(req.into())
72    }
73}
74
75impl From<FoundryTransactionRequest> for TransactionRequest {
76    fn from(req: FoundryTransactionRequest) -> Self {
77        req.0.into()
78    }
79}
80
81impl From<WithOtherFields<TransactionRequest>> for FoundryTransactionRequest {
82    fn from(req: WithOtherFields<TransactionRequest>) -> Self {
83        Self(req.inner.into())
84    }
85}
86
87impl FoundryTransactionRequest {
88    /// Create a new empty transaction request.
89    #[inline]
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Check if this is a deposit transaction.
95    #[inline]
96    pub fn is_deposit(&self) -> bool {
97        self.as_ref().transaction_type == Some(0x7E)
98    }
99
100    /// Mark this as a deposit transaction.
101    #[inline]
102    pub fn set_deposit_type(&mut self) {
103        self.as_mut().transaction_type = Some(0x7E);
104    }
105
106    /// Builder-pattern method to mark as deposit transaction.
107    #[inline]
108    pub fn as_deposit(mut self) -> Self {
109        self.set_deposit_type();
110        self
111    }
112
113    /// Build a typed transaction from this request.
114    ///
115    /// Converts the request into a `FoundryTypedTx`, handling all Ethereum
116    /// and OP-stack transaction types.
117    pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
118        // Use OpTransactionRequest's build_typed_tx
119        let op_typed = self.0.build_typed_tx().map_err(Self)?;
120
121        // Convert OpTypedTransaction → FoundryTypedTx
122        Ok(match op_typed {
123            OpTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
124            OpTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
125            OpTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
126            OpTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
127            OpTypedTransaction::Deposit(tx) => FoundryTypedTx::Deposit(tx),
128        })
129    }
130}
131
132impl From<FoundryTypedTx> for FoundryTransactionRequest {
133    fn from(tx: FoundryTypedTx) -> Self {
134        match tx {
135            FoundryTypedTx::Legacy(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
136            FoundryTypedTx::Eip2930(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
137            FoundryTypedTx::Eip1559(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
138            FoundryTypedTx::Eip4844(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
139            FoundryTypedTx::Eip7702(tx) => Self(Into::<TransactionRequest>::into(tx).into()),
140            FoundryTypedTx::Deposit(tx) => Self(tx.into()),
141        }
142    }
143}
144
145impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
146    fn from(tx: FoundryTxEnvelope) -> Self {
147        FoundryTypedTx::from(tx).into()
148    }
149}
150
151// TransactionBuilder trait implementation for FoundryNetwork
152impl TransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
153    fn chain_id(&self) -> Option<ChainId> {
154        self.as_ref().chain_id
155    }
156
157    fn set_chain_id(&mut self, chain_id: ChainId) {
158        self.as_mut().chain_id = Some(chain_id);
159    }
160
161    fn nonce(&self) -> Option<u64> {
162        self.as_ref().nonce
163    }
164
165    fn set_nonce(&mut self, nonce: u64) {
166        self.as_mut().nonce = Some(nonce);
167    }
168
169    fn take_nonce(&mut self) -> Option<u64> {
170        self.as_mut().nonce.take()
171    }
172
173    fn input(&self) -> Option<&alloy_primitives::Bytes> {
174        self.as_ref().input.input()
175    }
176
177    fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
178        self.as_mut().input.input = Some(input.into());
179    }
180
181    fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
182        &mut self,
183        input: T,
184        kind: TransactionInputKind,
185    ) {
186        let inner = self.as_mut();
187        match kind {
188            TransactionInputKind::Input => inner.input.input = Some(input.into()),
189            TransactionInputKind::Data => inner.input.data = Some(input.into()),
190            TransactionInputKind::Both => {
191                let bytes = input.into();
192                inner.input.input = Some(bytes.clone());
193                inner.input.data = Some(bytes);
194            }
195        }
196    }
197
198    fn from(&self) -> Option<Address> {
199        self.as_ref().from
200    }
201
202    fn set_from(&mut self, from: Address) {
203        self.as_mut().from = Some(from);
204    }
205
206    fn kind(&self) -> Option<TxKind> {
207        self.as_ref().to
208    }
209
210    fn clear_kind(&mut self) {
211        self.as_mut().to = None;
212    }
213
214    fn set_kind(&mut self, kind: TxKind) {
215        self.as_mut().to = Some(kind);
216    }
217
218    fn value(&self) -> Option<U256> {
219        self.as_ref().value
220    }
221
222    fn set_value(&mut self, value: U256) {
223        self.as_mut().value = Some(value);
224    }
225
226    fn gas_price(&self) -> Option<u128> {
227        self.as_ref().gas_price
228    }
229
230    fn set_gas_price(&mut self, gas_price: u128) {
231        self.as_mut().gas_price = Some(gas_price);
232    }
233
234    fn max_fee_per_gas(&self) -> Option<u128> {
235        self.as_ref().max_fee_per_gas
236    }
237
238    fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
239        self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
240    }
241
242    fn max_priority_fee_per_gas(&self) -> Option<u128> {
243        self.as_ref().max_priority_fee_per_gas
244    }
245
246    fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
247        self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
248    }
249
250    fn gas_limit(&self) -> Option<u64> {
251        self.as_ref().gas
252    }
253
254    fn set_gas_limit(&mut self, gas_limit: u64) {
255        self.as_mut().gas = Some(gas_limit);
256    }
257
258    fn access_list(&self) -> Option<&AccessList> {
259        self.as_ref().access_list.as_ref()
260    }
261
262    fn set_access_list(&mut self, access_list: AccessList) {
263        self.as_mut().access_list = Some(access_list);
264    }
265
266    fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
267        match ty {
268            FoundryTxType::Legacy => {
269                let mut missing = Vec::new();
270                if self.from().is_none() {
271                    missing.push("from");
272                }
273                if self.gas_limit().is_none() {
274                    missing.push("gas");
275                }
276                if self.gas_price().is_none() {
277                    missing.push("gasPrice");
278                }
279                if missing.is_empty() { Ok(()) } else { Err(missing) }
280            }
281            FoundryTxType::Eip2930 => {
282                let mut missing = Vec::new();
283                if self.from().is_none() {
284                    missing.push("from");
285                }
286                if self.gas_limit().is_none() {
287                    missing.push("gas");
288                }
289                if self.gas_price().is_none() {
290                    missing.push("gasPrice");
291                }
292                if self.access_list().is_none() {
293                    missing.push("accessList");
294                }
295                if missing.is_empty() { Ok(()) } else { Err(missing) }
296            }
297            FoundryTxType::Eip1559 => {
298                let mut missing = Vec::new();
299                if self.from().is_none() {
300                    missing.push("from");
301                }
302                if self.gas_limit().is_none() {
303                    missing.push("gas");
304                }
305                if self.max_fee_per_gas().is_none() {
306                    missing.push("maxFeePerGas");
307                }
308                if self.max_priority_fee_per_gas().is_none() {
309                    missing.push("maxPriorityFeePerGas");
310                }
311                if missing.is_empty() { Ok(()) } else { Err(missing) }
312            }
313            FoundryTxType::Eip4844 => {
314                let mut missing = Vec::new();
315                if self.from().is_none() {
316                    missing.push("from");
317                }
318                if self.kind().is_none() {
319                    missing.push("to");
320                }
321                if self.gas_limit().is_none() {
322                    missing.push("gas");
323                }
324                if self.max_fee_per_gas().is_none() {
325                    missing.push("maxFeePerGas");
326                }
327                if self.max_priority_fee_per_gas().is_none() {
328                    missing.push("maxPriorityFeePerGas");
329                }
330                if self.as_ref().sidecar.is_none() {
331                    missing.push("blobVersionedHashes or sidecar");
332                }
333                if missing.is_empty() { Ok(()) } else { Err(missing) }
334            }
335            FoundryTxType::Eip7702 => {
336                let mut missing = Vec::new();
337                if self.from().is_none() {
338                    missing.push("from");
339                }
340                if self.kind().is_none() {
341                    missing.push("to");
342                }
343                if self.gas_limit().is_none() {
344                    missing.push("gas");
345                }
346                if self.max_fee_per_gas().is_none() {
347                    missing.push("maxFeePerGas");
348                }
349                if self.max_priority_fee_per_gas().is_none() {
350                    missing.push("maxPriorityFeePerGas");
351                }
352                if self.as_ref().authorization_list.is_none() {
353                    missing.push("authorizationList");
354                }
355                if missing.is_empty() { Ok(()) } else { Err(missing) }
356            }
357            FoundryTxType::Deposit => {
358                let mut missing = Vec::new();
359                if self.from().is_none() {
360                    missing.push("from");
361                }
362                if self.kind().is_none() {
363                    missing.push("to");
364                }
365                if missing.is_empty() { Ok(()) } else { Err(missing) }
366            }
367        }
368    }
369
370    fn can_submit(&self) -> bool {
371        self.from().is_some()
372    }
373
374    fn can_build(&self) -> bool {
375        let inner = self.as_ref();
376        let common = inner.gas.is_some() && inner.nonce.is_some();
377
378        let legacy = inner.gas_price.is_some();
379        let eip2930 = legacy && inner.access_list.is_some();
380        let eip1559 = inner.max_fee_per_gas.is_some() && inner.max_priority_fee_per_gas.is_some();
381        let eip4844 = eip1559 && inner.sidecar.is_some() && inner.to.is_some();
382        let eip7702 = eip1559 && inner.authorization_list.is_some();
383
384        let deposit =
385            inner.transaction_type == Some(0x7E) && inner.from.is_some() && inner.to.is_some();
386
387        (common && (legacy || eip2930 || eip1559 || eip4844 || eip7702)) || deposit
388    }
389
390    fn output_tx_type(&self) -> FoundryTxType {
391        if self.as_ref().transaction_type == Some(DEPOSIT_TX_TYPE_ID) {
392            return FoundryTxType::Deposit;
393        }
394
395        // Default to EIP1559 if the transaction type is not explicitly set
396        if self.as_ref().transaction_type.is_some() {
397            match self.as_ref().transaction_type.unwrap() {
398                LEGACY_TX_TYPE_ID => FoundryTxType::Legacy,
399                EIP2930_TX_TYPE_ID => FoundryTxType::Eip2930,
400                EIP1559_TX_TYPE_ID => FoundryTxType::Eip1559,
401                EIP4844_TX_TYPE_ID => FoundryTxType::Eip4844,
402                EIP7702_TX_TYPE_ID => FoundryTxType::Eip7702,
403                _ => FoundryTxType::Eip1559,
404            }
405        } else if self.max_fee_per_gas().is_some() {
406            FoundryTxType::Eip1559
407        } else if self.gas_price().is_some() {
408            FoundryTxType::Legacy
409        } else {
410            FoundryTxType::Eip1559
411        }
412    }
413
414    fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
415        if self.can_build() { Some(self.output_tx_type()) } else { None }
416    }
417
418    fn prep_for_submission(&mut self) {
419        // Set transaction type if not already set
420        if self.as_ref().transaction_type.is_none() {
421            self.as_mut().transaction_type = Some(match self.output_tx_type() {
422                FoundryTxType::Legacy => LEGACY_TX_TYPE_ID,
423                FoundryTxType::Eip2930 => EIP2930_TX_TYPE_ID,
424                FoundryTxType::Eip1559 => EIP1559_TX_TYPE_ID,
425                FoundryTxType::Eip4844 => EIP4844_TX_TYPE_ID,
426                FoundryTxType::Eip7702 => EIP7702_TX_TYPE_ID,
427                FoundryTxType::Deposit => DEPOSIT_TX_TYPE_ID,
428            });
429        }
430    }
431
432    fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
433        // Try to build the transaction
434        match self.build_typed_tx() {
435            Ok(tx) => Ok(tx),
436            Err(err_self) => {
437                // If build_typed_tx fails, it's likely missing required fields
438                let tx_type = err_self.output_tx_type();
439                let mut missing = Vec::new();
440                if let Err(m) = err_self.complete_type(tx_type) {
441                    missing.extend(m);
442                }
443                // Return a generic error with missing field information
444                Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
445                    .into_unbuilt(err_self))
446            }
447        }
448    }
449
450    async fn build<W: NetworkWallet<FoundryNetwork>>(
451        self,
452        wallet: &W,
453    ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
454        Ok(wallet.sign_request(self).await?)
455    }
456}