Skip to main content

foundry_primitives/transaction/
request.rs

1use alloy_consensus::{BlobTransactionSidecarVariant, EthereumTypedTransaction};
2use alloy_network::{
3    BuildResult, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder,
4    TransactionBuilder4844, TransactionBuilderError,
5};
6use alloy_primitives::{Address, ChainId, TxKind, U256};
7use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest};
8use alloy_serde::{OtherFields, WithOtherFields};
9#[cfg(feature = "optimism")]
10use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit};
11#[cfg(feature = "optimism")]
12use op_revm::transaction::deposit::DepositTransactionParts;
13use serde::{Deserialize, Serialize};
14use tempo_alloy::rpc::TempoTransactionRequest;
15use tempo_primitives::{TEMPO_TX_TYPE_ID, TempoTxType};
16
17#[cfg(feature = "optimism")]
18use super::optimism::get_deposit_tx_parts;
19use super::{FoundryTxEnvelope, FoundryTxType, FoundryTypedTx};
20use crate::FoundryNetwork;
21
22/// Foundry transaction request builder.
23///
24/// This is a union of different transaction request types, instantiated from a
25/// [`WithOtherFields<TransactionRequest>`]. The specific variant is determined by the transaction
26/// type field and/or the presence of certain fields:
27/// - **Ethereum**: Default variant when no special fields are present
28/// - **Op**: When `sourceHash`, `mint`, and `isSystemTx` fields are present, or transaction type is
29///   `DEPOSIT_TX_TYPE_ID`
30/// - **Tempo**: When `feeToken` or `nonceKey` fields are present, or transaction type is
31///   `TEMPO_TX_TYPE_ID`
32#[derive(Clone, Debug, PartialEq, Eq)]
33#[allow(clippy::large_enum_variant)]
34pub enum FoundryTransactionRequest {
35    Ethereum(TransactionRequest),
36    #[cfg(feature = "optimism")]
37    Op(WithOtherFields<TransactionRequest>),
38    Tempo(Box<TempoTransactionRequest>),
39}
40
41impl FoundryTransactionRequest {
42    /// Create a new [`FoundryTransactionRequest`] from given
43    /// [`WithOtherFields<TransactionRequest>`].
44    #[inline]
45    pub fn new(inner: WithOtherFields<TransactionRequest>) -> Self {
46        inner.into()
47    }
48
49    /// Consume the [`FoundryTransactionRequest`] and return the inner transaction request.
50    pub fn into_inner(self) -> TransactionRequest {
51        match self {
52            Self::Ethereum(tx) => tx,
53            #[cfg(feature = "optimism")]
54            Self::Op(tx) => tx.inner,
55            Self::Tempo(tx) => tx.inner,
56        }
57    }
58
59    /// Get the deposit transaction parts from the request, calling [`get_deposit_tx_parts`] helper
60    /// with OtherFields.
61    ///
62    /// # Returns
63    /// - Ok(deposit_tx_parts) if all necessary keys are present to build a deposit transaction.
64    /// - Err(missing) if some keys are missing to build a deposit transaction.
65    #[cfg(feature = "optimism")]
66    pub fn get_deposit_tx_parts(&self) -> Result<DepositTransactionParts, Vec<&'static str>> {
67        match self {
68            Self::Op(tx) => get_deposit_tx_parts(&tx.other),
69            // Not a deposit transaction request, so missing at least sourceHash, mint, and
70            // isSystemTx
71            _ => Err(vec!["sourceHash", "mint", "isSystemTx"]),
72        }
73    }
74
75    /// Returns the minimal transaction type this request can be converted into based on the fields
76    /// that are set. See [`TransactionRequest::preferred_type`].
77    pub fn preferred_type(&self) -> FoundryTxType {
78        match self {
79            Self::Ethereum(tx) => tx.preferred_type().into(),
80            #[cfg(feature = "optimism")]
81            Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => {
82                FoundryTxType::PostExec
83            }
84            #[cfg(feature = "optimism")]
85            Self::Op(_) => FoundryTxType::Deposit,
86            Self::Tempo(_) => FoundryTxType::Tempo,
87        }
88    }
89
90    /// Check if all necessary keys are present to build a 4844 transaction,
91    /// returning a list of keys that are missing.
92    ///
93    /// **NOTE:** Inner [`TransactionRequest::complete_4844`] method but "sidecar" key is filtered
94    /// from error.
95    pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
96        match self.as_ref().complete_4844() {
97            Ok(()) => Ok(()),
98            Err(missing) => {
99                let filtered: Vec<_> =
100                    missing.into_iter().filter(|&key| key != "sidecar").collect();
101                if filtered.is_empty() { Ok(()) } else { Err(filtered) }
102            }
103        }
104    }
105
106    /// Check if all necessary keys are present to build a Deposit transaction, returning a list of
107    /// keys that are missing.
108    #[cfg(feature = "optimism")]
109    pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
110        self.get_deposit_tx_parts().map(|_| ())
111    }
112
113    /// Check if all necessary keys are present to build a Tempo transaction, returning a list of
114    /// keys that are missing.
115    pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> {
116        match self {
117            Self::Tempo(tx) => tx.complete_type(TempoTxType::AA).map(|_| ()),
118            // Not a Tempo transaction request, so missing at least feeToken and nonceKey
119            _ => Err(vec!["feeToken", "nonceKey"]),
120        }
121    }
122
123    /// Check if all necessary keys are present to build a transaction.
124    ///
125    /// # Returns
126    ///
127    /// - Ok(type) if all necessary keys are present to build the preferred type.
128    /// - Err((type, missing)) if some keys are missing to build the preferred type.
129    pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
130        let pref = self.preferred_type();
131        if let Err(missing) = match pref {
132            FoundryTxType::Legacy => self.as_ref().complete_legacy(),
133            FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
134            FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
135            FoundryTxType::Eip4844 => self.complete_4844(),
136            FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
137            #[cfg(feature = "optimism")]
138            FoundryTxType::Deposit => self.complete_deposit(),
139            #[cfg(feature = "optimism")]
140            FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
141            FoundryTxType::Tempo => self.complete_tempo(),
142        } {
143            Err((pref, missing))
144        } else {
145            Ok(pref)
146        }
147    }
148
149    /// Build a typed transaction from this request.
150    ///
151    /// Converts the request into a `FoundryTypedTx`, handling all Ethereum and OP-stack transaction
152    /// types.
153    pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
154        #[cfg(feature = "optimism")]
155        if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() {
156            // Build deposit transaction
157            return Ok(FoundryTypedTx::Deposit(TxDeposit {
158                from: self.from().unwrap_or_default(),
159                source_hash: deposit_tx_parts.source_hash,
160                to: self.kind().unwrap_or_default(),
161                mint: deposit_tx_parts.mint.unwrap_or_default(),
162                value: self.value().unwrap_or_default(),
163                gas_limit: self.gas_limit().unwrap_or_default(),
164                is_system_transaction: deposit_tx_parts.is_system_transaction,
165                input: self.input().cloned().unwrap_or_default(),
166            }));
167        }
168        if self.complete_tempo().is_ok()
169            && let Self::Tempo(tx_req) = self
170        {
171            // Build Tempo transaction
172            Ok(FoundryTypedTx::Tempo(
173                tx_req.build_aa().map_err(|e| Self::Tempo(Box::new(e.into_value())))?,
174            ))
175        } else if self.as_ref().has_eip4844_fields() && self.blob_sidecar().is_none() {
176            // if request has eip4844 fields but no blob sidecar (neither eip4844 nor eip7594
177            // format), try to build to eip4844 without sidecar
178            self.into_inner()
179                .build_4844_without_sidecar()
180                .map_err(|e| Self::Ethereum(e.into_value()))
181                .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
182        } else {
183            // Use the inner transaction request to build EthereumTypedTransaction
184            let typed_tx = self.into_inner().build_typed_tx().map_err(Self::Ethereum)?;
185            // Convert EthereumTypedTransaction to FoundryTypedTx
186            Ok(match typed_tx {
187                EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
188                EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
189                EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
190                EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
191                EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
192            })
193        }
194    }
195}
196
197impl Default for FoundryTransactionRequest {
198    fn default() -> Self {
199        Self::Ethereum(TransactionRequest::default())
200    }
201}
202
203impl Serialize for FoundryTransactionRequest {
204    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
205    where
206        S: serde::Serializer,
207    {
208        match self {
209            Self::Ethereum(tx) => tx.serialize(serializer),
210            #[cfg(feature = "optimism")]
211            Self::Op(tx) => tx.serialize(serializer),
212            Self::Tempo(tx) => tx.serialize(serializer),
213        }
214    }
215}
216
217impl<'de> Deserialize<'de> for FoundryTransactionRequest {
218    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
219    where
220        D: serde::Deserializer<'de>,
221    {
222        WithOtherFields::<TransactionRequest>::deserialize(deserializer).map(Into::<Self>::into)
223    }
224}
225
226impl AsRef<TransactionRequest> for FoundryTransactionRequest {
227    fn as_ref(&self) -> &TransactionRequest {
228        match self {
229            Self::Ethereum(tx) => tx,
230            #[cfg(feature = "optimism")]
231            Self::Op(tx) => tx,
232            Self::Tempo(tx) => tx.as_ref(),
233        }
234    }
235}
236
237impl AsMut<TransactionRequest> for FoundryTransactionRequest {
238    fn as_mut(&mut self) -> &mut TransactionRequest {
239        match self {
240            Self::Ethereum(tx) => tx,
241            #[cfg(feature = "optimism")]
242            Self::Op(tx) => tx,
243            Self::Tempo(tx) => tx.as_mut(),
244        }
245    }
246}
247
248impl From<WithOtherFields<TransactionRequest>> for FoundryTransactionRequest {
249    fn from(tx: WithOtherFields<TransactionRequest>) -> Self {
250        if tx.transaction_type == Some(TEMPO_TX_TYPE_ID)
251            || tx.other.contains_key("feeToken")
252            || tx.other.contains_key("nonceKey")
253        {
254            let mut tempo_tx_req: TempoTransactionRequest = tx.inner.into();
255            if let Some(fee_token) =
256                tx.other.get_deserialized::<Address>("feeToken").transpose().ok().flatten()
257            {
258                tempo_tx_req.fee_token = Some(fee_token);
259            }
260            if let Some(nonce_key) =
261                tx.other.get_deserialized::<U256>("nonceKey").transpose().ok().flatten()
262            {
263                tempo_tx_req.set_nonce_key(nonce_key);
264            }
265            return Self::Tempo(Box::new(tempo_tx_req));
266        }
267        #[cfg(feature = "optimism")]
268        if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
269            || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID)
270            || get_deposit_tx_parts(&tx.other).is_ok()
271        {
272            return Self::Op(tx);
273        }
274        Self::Ethereum(tx.into_inner())
275    }
276}
277
278impl From<FoundryTypedTx> for FoundryTransactionRequest {
279    fn from(tx: FoundryTypedTx) -> Self {
280        match tx {
281            FoundryTypedTx::Legacy(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
282            FoundryTypedTx::Eip2930(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
283            FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
284            FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
285            FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
286            #[cfg(feature = "optimism")]
287            FoundryTypedTx::Deposit(tx) => {
288                let other = OtherFields::from_iter([
289                    ("sourceHash", tx.source_hash.to_string().into()),
290                    ("mint", tx.mint.to_string().into()),
291                    ("isSystemTx", tx.is_system_transaction.to_string().into()),
292                ]);
293                WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }.into()
294            }
295            #[cfg(feature = "optimism")]
296            FoundryTypedTx::PostExec(tx) => WithOtherFields {
297                inner: Into::<TransactionRequest>::into(tx),
298                other: OtherFields::default(),
299            }
300            .into(),
301            FoundryTypedTx::Tempo(tx) => {
302                let mut other = OtherFields::default();
303                if let Some(fee_token) = tx.fee_token {
304                    other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
305                }
306                other.insert("nonceKey".to_string(), serde_json::to_value(tx.nonce_key).unwrap());
307                let first_call = tx.calls.first();
308                let mut inner = TransactionRequest::default()
309                    .with_chain_id(tx.chain_id)
310                    .with_nonce(tx.nonce)
311                    .with_gas_limit(tx.gas_limit)
312                    .with_max_fee_per_gas(tx.max_fee_per_gas)
313                    .with_max_priority_fee_per_gas(tx.max_priority_fee_per_gas)
314                    .with_kind(first_call.map(|c| c.to).unwrap_or_default())
315                    .with_value(first_call.map(|c| c.value).unwrap_or_default())
316                    .with_input(first_call.map(|c| c.input.clone()).unwrap_or_default())
317                    .with_access_list(tx.access_list);
318                inner.transaction_type = Some(TEMPO_TX_TYPE_ID);
319                WithOtherFields { inner, other }.into()
320            }
321        }
322    }
323}
324
325impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
326    fn from(tx: FoundryTxEnvelope) -> Self {
327        FoundryTypedTx::from(tx).into()
328    }
329}
330
331#[cfg(not(feature = "optimism"))]
332impl From<alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>> for FoundryTransactionRequest {
333    fn from(tx: alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>) -> Self {
334        tx.inner.into_inner().into()
335    }
336}
337
338// TransactionBuilder trait implementation for FoundryNetwork
339impl TransactionBuilder for FoundryTransactionRequest {
340    fn chain_id(&self) -> Option<ChainId> {
341        self.as_ref().chain_id
342    }
343
344    fn set_chain_id(&mut self, chain_id: ChainId) {
345        self.as_mut().chain_id = Some(chain_id);
346    }
347
348    fn nonce(&self) -> Option<u64> {
349        self.as_ref().nonce
350    }
351
352    fn set_nonce(&mut self, nonce: u64) {
353        self.as_mut().nonce = Some(nonce);
354    }
355
356    fn take_nonce(&mut self) -> Option<u64> {
357        self.as_mut().nonce.take()
358    }
359
360    fn input(&self) -> Option<&alloy_primitives::Bytes> {
361        self.as_ref().input.input()
362    }
363
364    fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
365        self.as_mut().input.input = Some(input.into());
366    }
367
368    fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
369        &mut self,
370        input: T,
371        kind: TransactionInputKind,
372    ) {
373        let inner = self.as_mut();
374        match kind {
375            TransactionInputKind::Input => inner.input.input = Some(input.into()),
376            TransactionInputKind::Data => inner.input.data = Some(input.into()),
377            TransactionInputKind::Both => {
378                let bytes = input.into();
379                inner.input.input = Some(bytes.clone());
380                inner.input.data = Some(bytes);
381            }
382        }
383    }
384
385    fn from(&self) -> Option<Address> {
386        self.as_ref().from
387    }
388
389    fn set_from(&mut self, from: Address) {
390        self.as_mut().from = Some(from);
391    }
392
393    fn kind(&self) -> Option<TxKind> {
394        self.as_ref().to
395    }
396
397    fn clear_kind(&mut self) {
398        self.as_mut().to = None;
399    }
400
401    fn set_kind(&mut self, kind: TxKind) {
402        self.as_mut().to = Some(kind);
403    }
404
405    fn value(&self) -> Option<U256> {
406        self.as_ref().value
407    }
408
409    fn set_value(&mut self, value: U256) {
410        self.as_mut().value = Some(value);
411    }
412
413    fn gas_price(&self) -> Option<u128> {
414        self.as_ref().gas_price
415    }
416
417    fn set_gas_price(&mut self, gas_price: u128) {
418        self.as_mut().gas_price = Some(gas_price);
419    }
420
421    fn max_fee_per_gas(&self) -> Option<u128> {
422        self.as_ref().max_fee_per_gas
423    }
424
425    fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
426        self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
427    }
428
429    fn max_priority_fee_per_gas(&self) -> Option<u128> {
430        self.as_ref().max_priority_fee_per_gas
431    }
432
433    fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
434        self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
435    }
436
437    fn gas_limit(&self) -> Option<u64> {
438        self.as_ref().gas
439    }
440
441    fn set_gas_limit(&mut self, gas_limit: u64) {
442        self.as_mut().gas = Some(gas_limit);
443    }
444
445    fn access_list(&self) -> Option<&AccessList> {
446        self.as_ref().access_list.as_ref()
447    }
448
449    fn set_access_list(&mut self, access_list: AccessList) {
450        self.as_mut().access_list = Some(access_list);
451    }
452}
453
454impl NetworkTransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
455    fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
456        match ty {
457            FoundryTxType::Legacy => self.as_ref().complete_legacy(),
458            FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
459            FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
460            FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
461            FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
462            #[cfg(feature = "optimism")]
463            FoundryTxType::Deposit => self.complete_deposit(),
464            #[cfg(feature = "optimism")]
465            FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
466            FoundryTxType::Tempo => self.complete_tempo(),
467        }
468    }
469
470    fn can_submit(&self) -> bool {
471        self.from().is_some()
472    }
473
474    fn can_build(&self) -> bool {
475        if self.as_ref().can_build() || self.complete_tempo().is_ok() {
476            return true;
477        }
478        #[cfg(feature = "optimism")]
479        if self.complete_deposit().is_ok() {
480            return true;
481        }
482        false
483    }
484
485    fn output_tx_type(&self) -> FoundryTxType {
486        self.preferred_type()
487    }
488
489    fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
490        let pref = self.preferred_type();
491        match pref {
492            FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(),
493            FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(),
494            FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(),
495            FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(),
496            FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(),
497            #[cfg(feature = "optimism")]
498            FoundryTxType::Deposit => self.complete_deposit().ok(),
499            #[cfg(feature = "optimism")]
500            FoundryTxType::PostExec => self.complete_type(pref).ok(),
501            FoundryTxType::Tempo => self.complete_tempo().ok(),
502        }?;
503        Some(pref)
504    }
505
506    /// Prepares [`FoundryTransactionRequest`] by trimming conflicting fields, and filling with
507    /// default values the mandatory fields.
508    fn prep_for_submission(&mut self) {
509        let preferred_type = self.preferred_type();
510        let inner = self.as_mut();
511        inner.transaction_type = Some(preferred_type as u8);
512        inner.gas.is_none().then(|| inner.set_gas_limit(Default::default()));
513        let is_deposit = {
514            #[cfg(feature = "optimism")]
515            {
516                preferred_type == FoundryTxType::Deposit
517            }
518            #[cfg(not(feature = "optimism"))]
519            {
520                false
521            }
522        };
523        if !is_deposit && preferred_type != FoundryTxType::Tempo {
524            inner.trim_conflicting_keys();
525            inner.populate_blob_hashes();
526        }
527        if !is_deposit {
528            inner.nonce.is_none().then(|| inner.set_nonce(Default::default()));
529        }
530        if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) {
531            inner.gas_price.is_none().then(|| inner.set_gas_price(Default::default()));
532        }
533        if preferred_type == FoundryTxType::Eip2930 {
534            inner.access_list.is_none().then(|| inner.set_access_list(Default::default()));
535        }
536        if matches!(
537            preferred_type,
538            FoundryTxType::Eip1559
539                | FoundryTxType::Eip4844
540                | FoundryTxType::Eip7702
541                | FoundryTxType::Tempo
542        ) {
543            inner
544                .max_priority_fee_per_gas
545                .is_none()
546                .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
547            inner.max_fee_per_gas.is_none().then(|| inner.set_max_fee_per_gas(Default::default()));
548        }
549        if preferred_type == FoundryTxType::Eip4844 {
550            inner
551                .as_ref()
552                .max_fee_per_blob_gas()
553                .is_none()
554                .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
555        }
556    }
557
558    fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
559        if let Err((tx_type, missing)) = self.missing_keys() {
560            return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
561                .into_unbuilt(self));
562        }
563        Ok(self.build_typed_tx().expect("checked by missing_keys"))
564    }
565
566    async fn build<W: NetworkWallet<FoundryNetwork>>(
567        self,
568        wallet: &W,
569    ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
570        Ok(wallet.sign_request(self).await?)
571    }
572}
573
574impl TransactionBuilder4844 for FoundryTransactionRequest {
575    fn max_fee_per_blob_gas(&self) -> Option<u128> {
576        self.as_ref().max_fee_per_blob_gas()
577    }
578
579    fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
580        self.as_mut().set_max_fee_per_blob_gas(max_fee_per_blob_gas);
581    }
582
583    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
584        self.as_ref().blob_sidecar()
585    }
586
587    fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
588        self.as_mut().set_blob_sidecar(sidecar);
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use alloy_primitives::B256;
595
596    use super::*;
597
598    fn default_tx_req() -> TransactionRequest {
599        TransactionRequest::default()
600            .with_to(Address::random())
601            .with_nonce(1)
602            .with_value(U256::from(1000000))
603            .with_gas_limit(1000000)
604            .with_max_fee_per_gas(1000000)
605            .with_max_priority_fee_per_gas(1000000)
606    }
607
608    #[test]
609    fn test_routing_ethereum_default() {
610        let tx = default_tx_req();
611        let req: FoundryTransactionRequest = WithOtherFields::new(tx).into();
612
613        assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
614        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
615    }
616
617    #[test]
618    fn test_routing_tempo_by_fee_token() {
619        let tx = default_tx_req();
620        let mut other = OtherFields::default();
621        other.insert("feeToken".to_string(), serde_json::to_value(Address::random()).unwrap());
622
623        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
624
625        assert!(matches!(req, FoundryTransactionRequest::Tempo(_)));
626        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Tempo(_))));
627    }
628
629    #[test]
630    #[cfg(feature = "optimism")]
631    fn test_routing_op_by_deposit_fields() {
632        let tx = default_tx_req();
633        let mut other = OtherFields::default();
634        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
635        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
636        other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
637
638        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
639
640        assert!(matches!(req, FoundryTransactionRequest::Op(_)));
641        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Deposit(_))));
642    }
643
644    #[test]
645    fn test_op_incomplete_routes_to_ethereum() {
646        let tx = default_tx_req();
647        let mut other = OtherFields::default();
648        // Only provide 2 of 3 required Op fields
649        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
650        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
651
652        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
653
654        assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
655        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
656    }
657
658    #[test]
659    fn test_ethereum_with_unrelated_other_fields() {
660        let tx = default_tx_req();
661        let mut other = OtherFields::default();
662        other.insert("anotherField".to_string(), serde_json::to_value(123).unwrap());
663
664        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
665
666        assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
667        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
668    }
669
670    #[test]
671    fn test_serialization_ethereum() {
672        let tx = default_tx_req();
673        let original: FoundryTransactionRequest = WithOtherFields::new(tx).into();
674
675        let serialized = serde_json::to_string(&original).unwrap();
676        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
677
678        assert!(matches!(deserialized, FoundryTransactionRequest::Ethereum(_)));
679    }
680
681    #[test]
682    #[cfg(feature = "optimism")]
683    fn test_serialization_op() {
684        let tx = default_tx_req();
685        let mut other = OtherFields::default();
686        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
687        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
688        other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
689
690        let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
691
692        let serialized = serde_json::to_string(&original).unwrap();
693        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
694
695        assert!(matches!(deserialized, FoundryTransactionRequest::Op(_)));
696    }
697
698    #[test]
699    fn test_serialization_tempo() {
700        let tx = default_tx_req();
701        let mut other = OtherFields::default();
702        other.insert("feeToken".to_string(), serde_json::to_value(Address::ZERO).unwrap());
703        other.insert("nonceKey".to_string(), serde_json::to_value(U256::from(42)).unwrap());
704
705        let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
706
707        let serialized = serde_json::to_string(&original).unwrap();
708        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
709
710        assert!(matches!(deserialized, FoundryTransactionRequest::Tempo(_)));
711    }
712}