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