Skip to main content

foundry_primitives/transaction/
request.rs

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