Skip to main content

foundry_primitives/transaction/
request.rs

1use alloy_consensus::{BlobTransactionSidecar, 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()
156            && self.blob_sidecar().is_none()
157            && alloy_network::TransactionBuilder7594::blob_sidecar_7594(self.as_ref()).is_none()
158        {
159            // if request has eip4844 fields but no blob sidecar (neither eip4844 nor eip7594
160            // format), try to build to eip4844 without sidecar
161            self.into_inner()
162                .build_4844_without_sidecar()
163                .map_err(|e| Self::Ethereum(e.into_value()))
164                .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
165        } else {
166            // Use the inner transaction request to build EthereumTypedTransaction
167            let typed_tx = self.into_inner().build_typed_tx().map_err(Self::Ethereum)?;
168            // Convert EthereumTypedTransaction to FoundryTypedTx
169            Ok(match typed_tx {
170                EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
171                EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
172                EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
173                EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
174                EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
175            })
176        }
177    }
178}
179
180impl Default for FoundryTransactionRequest {
181    fn default() -> Self {
182        Self::Ethereum(TransactionRequest::default())
183    }
184}
185
186impl Serialize for FoundryTransactionRequest {
187    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188    where
189        S: serde::Serializer,
190    {
191        match self {
192            Self::Ethereum(tx) => tx.serialize(serializer),
193            Self::Op(tx) => tx.serialize(serializer),
194            Self::Tempo(tx) => tx.serialize(serializer),
195        }
196    }
197}
198
199impl<'de> Deserialize<'de> for FoundryTransactionRequest {
200    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
201    where
202        D: serde::Deserializer<'de>,
203    {
204        WithOtherFields::<TransactionRequest>::deserialize(deserializer).map(Into::<Self>::into)
205    }
206}
207
208impl AsRef<TransactionRequest> for FoundryTransactionRequest {
209    fn as_ref(&self) -> &TransactionRequest {
210        match self {
211            Self::Ethereum(tx) => tx,
212            Self::Op(tx) => tx,
213            Self::Tempo(tx) => tx,
214        }
215    }
216}
217
218impl AsMut<TransactionRequest> for FoundryTransactionRequest {
219    fn as_mut(&mut self) -> &mut TransactionRequest {
220        match self {
221            Self::Ethereum(tx) => tx,
222            Self::Op(tx) => tx,
223            Self::Tempo(tx) => tx,
224        }
225    }
226}
227
228impl From<WithOtherFields<TransactionRequest>> for FoundryTransactionRequest {
229    fn from(tx: WithOtherFields<TransactionRequest>) -> Self {
230        if tx.transaction_type == Some(TEMPO_TX_TYPE_ID)
231            || tx.other.contains_key("feeToken")
232            || tx.other.contains_key("nonceKey")
233        {
234            let mut tempo_tx_req: TempoTransactionRequest = tx.inner.into();
235            if let Some(fee_token) =
236                tx.other.get_deserialized::<Address>("feeToken").transpose().ok().flatten()
237            {
238                tempo_tx_req.fee_token = Some(fee_token);
239            }
240            if let Some(nonce_key) =
241                tx.other.get_deserialized::<U256>("nonceKey").transpose().ok().flatten()
242            {
243                tempo_tx_req.set_nonce_key(nonce_key);
244            }
245            Self::Tempo(Box::new(tempo_tx_req))
246        } else if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
247            || get_deposit_tx_parts(&tx.other).is_ok()
248        {
249            Self::Op(tx)
250        } else {
251            Self::Ethereum(tx.into_inner())
252        }
253    }
254}
255
256impl From<FoundryTypedTx> for FoundryTransactionRequest {
257    fn from(tx: FoundryTypedTx) -> Self {
258        match tx {
259            FoundryTypedTx::Legacy(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
260            FoundryTypedTx::Eip2930(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
261            FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
262            FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
263            FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
264            FoundryTypedTx::Deposit(tx) => {
265                let other = OtherFields::from_iter([
266                    ("sourceHash", tx.source_hash.to_string().into()),
267                    ("mint", tx.mint.to_string().into()),
268                    ("isSystemTx", tx.is_system_transaction.to_string().into()),
269                ]);
270                WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }.into()
271            }
272            FoundryTypedTx::Tempo(tx) => {
273                let mut other = OtherFields::default();
274                if let Some(fee_token) = tx.fee_token {
275                    other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
276                }
277                other.insert("nonceKey".to_string(), serde_json::to_value(tx.nonce_key).unwrap());
278                let first_call = tx.calls.first();
279                let mut inner = TransactionRequest::default()
280                    .with_chain_id(tx.chain_id)
281                    .with_nonce(tx.nonce)
282                    .with_gas_limit(tx.gas_limit)
283                    .with_max_fee_per_gas(tx.max_fee_per_gas)
284                    .with_max_priority_fee_per_gas(tx.max_priority_fee_per_gas)
285                    .with_kind(first_call.map(|c| c.to).unwrap_or_default())
286                    .with_value(first_call.map(|c| c.value).unwrap_or_default())
287                    .with_input(first_call.map(|c| c.input.clone()).unwrap_or_default())
288                    .with_access_list(tx.access_list);
289                inner.transaction_type = Some(TEMPO_TX_TYPE_ID);
290                WithOtherFields { inner, other }.into()
291            }
292        }
293    }
294}
295
296impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
297    fn from(tx: FoundryTxEnvelope) -> Self {
298        FoundryTypedTx::from(tx).into()
299    }
300}
301
302// TransactionBuilder trait implementation for FoundryNetwork
303impl TransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
304    fn chain_id(&self) -> Option<ChainId> {
305        self.as_ref().chain_id
306    }
307
308    fn set_chain_id(&mut self, chain_id: ChainId) {
309        self.as_mut().chain_id = Some(chain_id);
310    }
311
312    fn nonce(&self) -> Option<u64> {
313        self.as_ref().nonce
314    }
315
316    fn set_nonce(&mut self, nonce: u64) {
317        self.as_mut().nonce = Some(nonce);
318    }
319
320    fn take_nonce(&mut self) -> Option<u64> {
321        self.as_mut().nonce.take()
322    }
323
324    fn input(&self) -> Option<&alloy_primitives::Bytes> {
325        self.as_ref().input.input()
326    }
327
328    fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
329        self.as_mut().input.input = Some(input.into());
330    }
331
332    fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
333        &mut self,
334        input: T,
335        kind: TransactionInputKind,
336    ) {
337        let inner = self.as_mut();
338        match kind {
339            TransactionInputKind::Input => inner.input.input = Some(input.into()),
340            TransactionInputKind::Data => inner.input.data = Some(input.into()),
341            TransactionInputKind::Both => {
342                let bytes = input.into();
343                inner.input.input = Some(bytes.clone());
344                inner.input.data = Some(bytes);
345            }
346        }
347    }
348
349    fn from(&self) -> Option<Address> {
350        self.as_ref().from
351    }
352
353    fn set_from(&mut self, from: Address) {
354        self.as_mut().from = Some(from);
355    }
356
357    fn kind(&self) -> Option<TxKind> {
358        self.as_ref().to
359    }
360
361    fn clear_kind(&mut self) {
362        self.as_mut().to = None;
363    }
364
365    fn set_kind(&mut self, kind: TxKind) {
366        self.as_mut().to = Some(kind);
367    }
368
369    fn value(&self) -> Option<U256> {
370        self.as_ref().value
371    }
372
373    fn set_value(&mut self, value: U256) {
374        self.as_mut().value = Some(value);
375    }
376
377    fn gas_price(&self) -> Option<u128> {
378        self.as_ref().gas_price
379    }
380
381    fn set_gas_price(&mut self, gas_price: u128) {
382        self.as_mut().gas_price = Some(gas_price);
383    }
384
385    fn max_fee_per_gas(&self) -> Option<u128> {
386        self.as_ref().max_fee_per_gas
387    }
388
389    fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
390        self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
391    }
392
393    fn max_priority_fee_per_gas(&self) -> Option<u128> {
394        self.as_ref().max_priority_fee_per_gas
395    }
396
397    fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
398        self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
399    }
400
401    fn gas_limit(&self) -> Option<u64> {
402        self.as_ref().gas
403    }
404
405    fn set_gas_limit(&mut self, gas_limit: u64) {
406        self.as_mut().gas = Some(gas_limit);
407    }
408
409    fn access_list(&self) -> Option<&AccessList> {
410        self.as_ref().access_list.as_ref()
411    }
412
413    fn set_access_list(&mut self, access_list: AccessList) {
414        self.as_mut().access_list = Some(access_list);
415    }
416
417    fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
418        match ty {
419            FoundryTxType::Legacy => self.as_ref().complete_legacy(),
420            FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
421            FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
422            FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
423            FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
424            FoundryTxType::Deposit => self.complete_deposit(),
425            FoundryTxType::Tempo => self.complete_tempo(),
426        }
427    }
428
429    fn can_submit(&self) -> bool {
430        self.from().is_some()
431    }
432
433    fn can_build(&self) -> bool {
434        self.as_ref().can_build()
435            || self.complete_deposit().is_ok()
436            || self.complete_tempo().is_ok()
437    }
438
439    fn output_tx_type(&self) -> FoundryTxType {
440        self.preferred_type()
441    }
442
443    fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
444        let pref = self.preferred_type();
445        match pref {
446            FoundryTxType::Legacy => self.as_ref().complete_legacy().ok(),
447            FoundryTxType::Eip2930 => self.as_ref().complete_2930().ok(),
448            FoundryTxType::Eip1559 => self.as_ref().complete_1559().ok(),
449            FoundryTxType::Eip4844 => self.as_ref().complete_4844().ok(),
450            FoundryTxType::Eip7702 => self.as_ref().complete_7702().ok(),
451            FoundryTxType::Deposit => self.complete_deposit().ok(),
452            FoundryTxType::Tempo => self.complete_tempo().ok(),
453        }?;
454        Some(pref)
455    }
456
457    /// Prepares [`FoundryTransactionRequest`] by trimming conflicting fields, and filling with
458    /// default values the mandatory fields.
459    fn prep_for_submission(&mut self) {
460        let preferred_type = self.preferred_type();
461        let inner = self.as_mut();
462        inner.transaction_type = Some(preferred_type as u8);
463        inner.gas.is_none().then(|| inner.set_gas_limit(Default::default()));
464        if !matches!(preferred_type, FoundryTxType::Deposit | FoundryTxType::Tempo) {
465            inner.trim_conflicting_keys();
466            inner.populate_blob_hashes();
467        }
468        if preferred_type != FoundryTxType::Deposit {
469            inner.nonce.is_none().then(|| inner.set_nonce(Default::default()));
470        }
471        if matches!(preferred_type, FoundryTxType::Legacy | FoundryTxType::Eip2930) {
472            inner.gas_price.is_none().then(|| inner.set_gas_price(Default::default()));
473        }
474        if preferred_type == FoundryTxType::Eip2930 {
475            inner.access_list.is_none().then(|| inner.set_access_list(Default::default()));
476        }
477        if matches!(
478            preferred_type,
479            FoundryTxType::Eip1559
480                | FoundryTxType::Eip4844
481                | FoundryTxType::Eip7702
482                | FoundryTxType::Tempo
483        ) {
484            inner
485                .max_priority_fee_per_gas
486                .is_none()
487                .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
488            inner.max_fee_per_gas.is_none().then(|| inner.set_max_fee_per_gas(Default::default()));
489        }
490        if preferred_type == FoundryTxType::Eip4844 {
491            inner
492                .as_ref()
493                .max_fee_per_blob_gas()
494                .is_none()
495                .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
496        }
497    }
498
499    fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
500        if let Err((tx_type, missing)) = self.missing_keys() {
501            return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
502                .into_unbuilt(self));
503        }
504        Ok(self.build_typed_tx().expect("checked by missing_keys"))
505    }
506
507    async fn build<W: NetworkWallet<FoundryNetwork>>(
508        self,
509        wallet: &W,
510    ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
511        Ok(wallet.sign_request(self).await?)
512    }
513}
514
515impl TransactionBuilder4844 for FoundryTransactionRequest {
516    fn max_fee_per_blob_gas(&self) -> Option<u128> {
517        self.as_ref().max_fee_per_blob_gas()
518    }
519
520    fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
521        self.as_mut().set_max_fee_per_blob_gas(max_fee_per_blob_gas);
522    }
523
524    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecar> {
525        self.as_ref().blob_sidecar()
526    }
527
528    fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecar) {
529        self.as_mut().set_blob_sidecar(sidecar);
530    }
531}
532
533/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields
534pub fn get_deposit_tx_parts(
535    other: &OtherFields,
536) -> Result<DepositTransactionParts, Vec<&'static str>> {
537    let mut missing = Vec::new();
538    let source_hash =
539        other.get_deserialized::<B256>("sourceHash").transpose().ok().flatten().unwrap_or_else(
540            || {
541                missing.push("sourceHash");
542                Default::default()
543            },
544        );
545    let mint = other
546        .get_deserialized::<U256>("mint")
547        .transpose()
548        .unwrap_or_else(|_| {
549            missing.push("mint");
550            Default::default()
551        })
552        .map(|value| value.to::<u128>());
553    let is_system_transaction =
554        other.get_deserialized::<bool>("isSystemTx").transpose().ok().flatten().unwrap_or_else(
555            || {
556                missing.push("isSystemTx");
557                Default::default()
558            },
559        );
560    if missing.is_empty() {
561        Ok(DepositTransactionParts { source_hash, mint, is_system_transaction })
562    } else {
563        Err(missing)
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570
571    fn default_tx_req() -> TransactionRequest {
572        TransactionRequest::default()
573            .with_to(Address::random())
574            .with_nonce(1)
575            .with_value(U256::from(1000000))
576            .with_gas_limit(1000000)
577            .with_max_fee_per_gas(1000000)
578            .with_max_priority_fee_per_gas(1000000)
579    }
580
581    #[test]
582    fn test_routing_ethereum_default() {
583        let tx = default_tx_req();
584        let req: FoundryTransactionRequest = WithOtherFields::new(tx).into();
585
586        assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
587        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
588    }
589
590    #[test]
591    fn test_routing_tempo_by_fee_token() {
592        let tx = default_tx_req();
593        let mut other = OtherFields::default();
594        other.insert("feeToken".to_string(), serde_json::to_value(Address::random()).unwrap());
595
596        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
597
598        assert!(matches!(req, FoundryTransactionRequest::Tempo(_)));
599        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Tempo(_))));
600    }
601
602    #[test]
603    fn test_routing_op_by_deposit_fields() {
604        let tx = default_tx_req();
605        let mut other = OtherFields::default();
606        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
607        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
608        other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
609
610        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
611
612        assert!(matches!(req, FoundryTransactionRequest::Op(_)));
613        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Deposit(_))));
614    }
615
616    #[test]
617    fn test_op_incomplete_routes_to_ethereum() {
618        let tx = default_tx_req();
619        let mut other = OtherFields::default();
620        // Only provide 2 of 3 required Op fields
621        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
622        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
623
624        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
625
626        assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
627        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
628    }
629
630    #[test]
631    fn test_ethereum_with_unrelated_other_fields() {
632        let tx = default_tx_req();
633        let mut other = OtherFields::default();
634        other.insert("anotherField".to_string(), serde_json::to_value(123).unwrap());
635
636        let req: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
637
638        assert!(matches!(req, FoundryTransactionRequest::Ethereum(_)));
639        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
640    }
641
642    #[test]
643    fn test_serialization_ethereum() {
644        let tx = default_tx_req();
645        let original: FoundryTransactionRequest = WithOtherFields::new(tx).into();
646
647        let serialized = serde_json::to_string(&original).unwrap();
648        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
649
650        assert!(matches!(deserialized, FoundryTransactionRequest::Ethereum(_)));
651    }
652
653    #[test]
654    fn test_serialization_op() {
655        let tx = default_tx_req();
656        let mut other = OtherFields::default();
657        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
658        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
659        other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
660
661        let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
662
663        let serialized = serde_json::to_string(&original).unwrap();
664        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
665
666        assert!(matches!(deserialized, FoundryTransactionRequest::Op(_)));
667    }
668
669    #[test]
670    fn test_serialization_tempo() {
671        let tx = default_tx_req();
672        let mut other = OtherFields::default();
673        other.insert("feeToken".to_string(), serde_json::to_value(Address::ZERO).unwrap());
674        other.insert("nonceKey".to_string(), serde_json::to_value(U256::from(42)).unwrap());
675
676        let original: FoundryTransactionRequest = WithOtherFields { inner: tx, other }.into();
677
678        let serialized = serde_json::to_string(&original).unwrap();
679        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
680
681        assert!(matches!(deserialized, FoundryTransactionRequest::Tempo(_)));
682    }
683}