Skip to main content

foundry_primitives/transaction/
request.rs

1use super::{CIP64_TX_TYPE, FoundryTxEnvelope, FoundryTxType, FoundryTypedTx, TxCip64};
2use crate::FoundryNetwork;
3use alloy_consensus::{BlobTransactionSidecarVariant, EthereumTypedTransaction};
4use alloy_network::{
5    BuildResult, NetworkTransactionBuilder, NetworkWallet, TransactionBuilder,
6    TransactionBuilder4844, TransactionBuilderError,
7};
8use alloy_primitives::{Address, ChainId, TxKind, U256};
9use alloy_rpc_types::{AccessList, TransactionInputKind, TransactionRequest};
10use alloy_serde::WithOtherFields;
11use core::num::NonZeroU64;
12use serde::{Deserialize, Serialize};
13use tempo_primitives::{
14    SignatureType, TEMPO_TX_TYPE_ID, TempoTxType,
15    transaction::{Call, SignedKeyAuthorization, TempoSignedAuthorization},
16};
17
18#[cfg(all(feature = "base", not(feature = "optimism")))]
19use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit};
20
21#[cfg(any(feature = "base", feature = "optimism"))]
22use super::get_deposit_tx_parts;
23#[cfg(any(feature = "base", feature = "optimism"))]
24use op_revm::transaction::deposit::DepositTransactionParts;
25
26#[cfg(any(test, feature = "base", feature = "optimism"))]
27use alloy_serde::OtherFields;
28
29#[cfg(feature = "base")]
30use base_common_evm::EIP8130_TRANSACTION_TYPE;
31#[cfg(feature = "base")]
32use base_common_rpc_types::BaseTransactionRequest;
33
34#[cfg(feature = "optimism")]
35use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit};
36
37pub use tempo_alloy::rpc::TempoTransactionRequest;
38
39/// Foundry transaction request builder.
40///
41/// This is a union of different transaction request types, instantiated from a
42/// [`WithOtherFields<TransactionRequest>`]. The specific variant is determined by the transaction
43/// type field and/or the presence of certain fields:
44/// - **Ethereum**: Default variant when no special fields are present
45/// - **Op**: When `sourceHash`, `mint`, and `isSystemTx` fields are present, or transaction type is
46///   `DEPOSIT_TX_TYPE_ID`
47/// - **Celo**: When `feeCurrency` is non-null, or transaction type is `0x7b`
48/// - **Tempo**: When a Tempo-specific field is present, or transaction type is `TEMPO_TX_TYPE_ID`
49#[derive(Clone, Debug, PartialEq, Eq)]
50#[allow(clippy::large_enum_variant)]
51pub enum FoundryTransactionRequest {
52    Ethereum(TransactionRequest),
53    #[cfg(feature = "base")]
54    Base(BaseTransactionRequest),
55    #[cfg(any(feature = "base", feature = "optimism"))]
56    Op(WithOtherFields<TransactionRequest>),
57    Tempo(Box<TempoTransactionRequest>),
58    /// CIP-64 fields alongside the ordinary transaction request.
59    Celo(Cip64TransactionRequest),
60}
61
62/// Celo CIP-64 request fields. Anvil currently charges native fees.
63#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct Cip64TransactionRequest {
66    /// Ordinary transaction request fields.
67    #[serde(flatten)]
68    pub inner: TransactionRequest,
69    /// Fee currency retained in the signed envelope.
70    pub fee_currency: Option<Address>,
71}
72
73const TEMPO_REQUEST_FIELDS: &[&str] = &[
74    "feeToken",
75    "nonceKey",
76    "calls",
77    "keyType",
78    "keyData",
79    "keyId",
80    "aaAuthorizationList",
81    "keyAuthorization",
82    "validBefore",
83    "validAfter",
84    "feePayerSignature",
85];
86
87impl FoundryTransactionRequest {
88    /// Returns `true` if this is an Ethereum transaction request.
89    pub const fn is_ethereum(&self) -> bool {
90        matches!(self, Self::Ethereum(_))
91    }
92
93    /// Returns `true` if this is a Base EIP-8130 request.
94    #[cfg(feature = "base")]
95    pub const fn is_base(&self) -> bool {
96        matches!(self, Self::Base(_))
97    }
98
99    /// Returns the native Base request.
100    #[cfg(feature = "base")]
101    pub const fn as_base(&self) -> Option<&BaseTransactionRequest> {
102        match self {
103            Self::Base(request) => Some(request),
104            _ => None,
105        }
106    }
107
108    /// Returns `true` if this is an OP stack transaction request.
109    #[cfg(any(feature = "base", feature = "optimism"))]
110    pub const fn is_op(&self) -> bool {
111        matches!(self, Self::Op(_))
112    }
113
114    /// Returns `true` if this is a Tempo transaction request.
115    pub const fn is_tempo(&self) -> bool {
116        matches!(self, Self::Tempo(_))
117    }
118
119    /// Create a new [`FoundryTransactionRequest`] from given
120    /// [`WithOtherFields<TransactionRequest>`].
121    #[inline]
122    pub fn new(inner: WithOtherFields<TransactionRequest>) -> serde_json::Result<Self> {
123        inner.try_into()
124    }
125
126    /// Consume the [`FoundryTransactionRequest`] and return the inner transaction request.
127    pub fn into_inner(self) -> TransactionRequest {
128        match self {
129            Self::Ethereum(tx) => tx,
130            Self::Celo(tx) => tx.inner,
131            #[cfg(feature = "base")]
132            Self::Base(tx) => tx.into(),
133            #[cfg(any(feature = "base", feature = "optimism"))]
134            Self::Op(tx) => tx.inner,
135            Self::Tempo(tx) => tx.inner,
136        }
137    }
138
139    /// Get the deposit transaction parts from the request, calling [`get_deposit_tx_parts`] helper
140    /// with OtherFields.
141    ///
142    /// # Returns
143    /// - Ok(deposit_tx_parts) if all necessary keys are present to build a deposit transaction.
144    /// - Err(missing) if some keys are missing to build a deposit transaction.
145    #[cfg(any(feature = "base", feature = "optimism"))]
146    pub fn get_deposit_tx_parts(&self) -> Result<DepositTransactionParts, Vec<&'static str>> {
147        match self {
148            Self::Op(tx) => get_deposit_tx_parts(&tx.other),
149            // Not a deposit transaction request, so missing at least sourceHash, mint, and
150            // isSystemTx
151            _ => Err(vec!["sourceHash", "mint", "isSystemTx"]),
152        }
153    }
154
155    /// Returns the minimal transaction type this request can be converted into based on the fields
156    /// that are set. See [`TransactionRequest::preferred_type`].
157    pub fn preferred_type(&self) -> FoundryTxType {
158        match self {
159            Self::Ethereum(tx) => tx.preferred_type().into(),
160            #[cfg(feature = "base")]
161            Self::Base(_) => FoundryTxType::Eip8130,
162            #[cfg(feature = "optimism")]
163            Self::Op(tx) if tx.inner.transaction_type == Some(POST_EXEC_TX_TYPE_ID) => {
164                FoundryTxType::PostExec
165            }
166            #[cfg(any(feature = "base", feature = "optimism"))]
167            Self::Op(_) => FoundryTxType::Deposit,
168            Self::Tempo(_) => FoundryTxType::Tempo,
169            Self::Celo(_) => FoundryTxType::Celo,
170        }
171    }
172
173    /// Check if all necessary keys are present to build a 4844 transaction,
174    /// returning a list of keys that are missing.
175    ///
176    /// **NOTE:** Inner [`TransactionRequest::complete_4844`] method but "sidecar" key is filtered
177    /// from error.
178    pub fn complete_4844(&self) -> Result<(), Vec<&'static str>> {
179        match self.as_ref().complete_4844() {
180            Ok(()) => Ok(()),
181            Err(missing) => {
182                let filtered: Vec<_> =
183                    missing.into_iter().filter(|&key| key != "sidecar").collect();
184                if filtered.is_empty() { Ok(()) } else { Err(filtered) }
185            }
186        }
187    }
188
189    /// Check if all necessary keys are present to build a Deposit transaction, returning a list of
190    /// keys that are missing.
191    #[cfg(any(feature = "base", feature = "optimism"))]
192    pub fn complete_deposit(&self) -> Result<(), Vec<&'static str>> {
193        self.get_deposit_tx_parts().map(|_| ())
194    }
195
196    /// Check if all necessary keys are present to build a Tempo transaction, returning a list of
197    /// keys that are missing.
198    pub fn complete_tempo(&self) -> Result<(), Vec<&'static str>> {
199        match self {
200            Self::Tempo(tx) => tx.complete_type(TempoTxType::AA).map(|_| ()),
201            // Not a Tempo transaction request, so missing at least feeToken and nonceKey
202            _ => Err(vec!["feeToken", "nonceKey"]),
203        }
204    }
205
206    fn check_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
207        match ty {
208            FoundryTxType::Legacy => self.as_ref().complete_legacy(),
209            FoundryTxType::Eip2930 => self.as_ref().complete_2930(),
210            FoundryTxType::Eip1559 => self.as_ref().complete_1559(),
211            FoundryTxType::Eip4844 => self.as_ref().complete_4844(),
212            FoundryTxType::Eip7702 => self.as_ref().complete_7702(),
213            #[cfg(any(feature = "base", feature = "optimism"))]
214            FoundryTxType::Deposit => self.complete_deposit(),
215            #[cfg(feature = "optimism")]
216            FoundryTxType::PostExec => Err(vec!["not implemented for post-exec tx"]),
217            #[cfg(feature = "base")]
218            FoundryTxType::Eip8130 => {
219                Err(vec!["EIP-8130 requires a signed raw transaction envelope"])
220            }
221            FoundryTxType::Tempo => self.complete_tempo(),
222            FoundryTxType::Celo => self.as_ref().complete_1559(),
223        }
224    }
225
226    /// Check if all necessary keys are present to build a transaction.
227    ///
228    /// # Returns
229    ///
230    /// - Ok(type) if all necessary keys are present to build the preferred type.
231    /// - Err((type, missing)) if some keys are missing to build the preferred type.
232    pub fn missing_keys(&self) -> Result<FoundryTxType, (FoundryTxType, Vec<&'static str>)> {
233        let pref = self.preferred_type();
234        let result = if pref.is_eip4844() { self.complete_4844() } else { self.check_type(pref) };
235        if let Err(missing) = result { Err((pref, missing)) } else { Ok(pref) }
236    }
237
238    /// Build a typed transaction from this request.
239    ///
240    /// Converts the request into a `FoundryTypedTx`, handling all Ethereum and OP-stack transaction
241    /// types.
242    pub fn build_typed_tx(self) -> Result<FoundryTypedTx, Self> {
243        if let Self::Celo(request) = self {
244            let fee_currency = request.fee_currency;
245            return request
246                .inner
247                .clone()
248                .build_1559()
249                .map(|inner| FoundryTypedTx::Celo(TxCip64 { inner, fee_currency }))
250                .map_err(|_| Self::Celo(request));
251        }
252        #[cfg(feature = "base")]
253        if self.is_base() {
254            return Err(self);
255        }
256        #[cfg(any(feature = "base", feature = "optimism"))]
257        if let Ok(deposit_tx_parts) = self.get_deposit_tx_parts() {
258            // Build deposit transaction
259            return Ok(FoundryTypedTx::Deposit(TxDeposit {
260                from: self.from().unwrap_or_default(),
261                source_hash: deposit_tx_parts.source_hash,
262                to: self.kind().unwrap_or_default(),
263                mint: deposit_tx_parts.mint.unwrap_or_default(),
264                value: self.value().unwrap_or_default(),
265                gas_limit: self.gas_limit().unwrap_or_default(),
266                is_system_transaction: deposit_tx_parts.is_system_transaction,
267                input: self.input().cloned().unwrap_or_default(),
268            }));
269        }
270        if self.complete_tempo().is_ok()
271            && let Self::Tempo(tx_req) = self
272        {
273            // Build Tempo transaction
274            Ok(FoundryTypedTx::Tempo(
275                tx_req.build_aa().map_err(|e| Self::Tempo(Box::new(e.into_value())))?,
276            ))
277        } else if self.as_ref().has_eip4844_fields() && self.blob_sidecar().is_none() {
278            // if request has eip4844 fields but no blob sidecar (neither eip4844 nor eip7594
279            // format), try to build to eip4844 without sidecar
280            self.into_inner()
281                .build_4844_without_sidecar()
282                .map_err(|e| Self::Ethereum(e.into_value()))
283                .map(|tx| FoundryTypedTx::Eip4844(tx.into()))
284        } else {
285            // Use the inner transaction request to build EthereumTypedTransaction
286            let typed_tx = self.into_inner().build_typed_tx().map_err(Self::Ethereum)?;
287            // Convert EthereumTypedTransaction to FoundryTypedTx
288            Ok(match typed_tx {
289                EthereumTypedTransaction::Legacy(tx) => FoundryTypedTx::Legacy(tx),
290                EthereumTypedTransaction::Eip2930(tx) => FoundryTypedTx::Eip2930(tx),
291                EthereumTypedTransaction::Eip1559(tx) => FoundryTypedTx::Eip1559(tx),
292                EthereumTypedTransaction::Eip4844(tx) => FoundryTypedTx::Eip4844(tx),
293                EthereumTypedTransaction::Eip7702(tx) => FoundryTypedTx::Eip7702(tx),
294            })
295        }
296    }
297}
298
299impl Default for FoundryTransactionRequest {
300    fn default() -> Self {
301        Self::Ethereum(TransactionRequest::default())
302    }
303}
304
305impl Serialize for FoundryTransactionRequest {
306    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
307    where
308        S: serde::Serializer,
309    {
310        match self {
311            Self::Ethereum(tx) => tx.serialize(serializer),
312            #[cfg(feature = "base")]
313            Self::Base(tx) => tx.serialize(serializer),
314            #[cfg(any(feature = "base", feature = "optimism"))]
315            Self::Op(tx) => tx.serialize(serializer),
316            Self::Tempo(tx) => tx.serialize(serializer),
317            Self::Celo(tx) => tx.serialize(serializer),
318        }
319    }
320}
321
322impl<'de> Deserialize<'de> for FoundryTransactionRequest {
323    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324    where
325        D: serde::Deserializer<'de>,
326    {
327        Self::new(WithOtherFields::<TransactionRequest>::deserialize(deserializer)?)
328            .map_err(serde::de::Error::custom)
329    }
330}
331
332impl AsRef<TransactionRequest> for FoundryTransactionRequest {
333    fn as_ref(&self) -> &TransactionRequest {
334        match self {
335            Self::Ethereum(tx) => tx,
336            #[cfg(feature = "base")]
337            Self::Base(tx) => tx.as_ref(),
338            #[cfg(any(feature = "base", feature = "optimism"))]
339            Self::Op(tx) => tx,
340            Self::Tempo(tx) => tx.as_ref(),
341            Self::Celo(tx) => &tx.inner,
342        }
343    }
344}
345
346impl AsMut<TransactionRequest> for FoundryTransactionRequest {
347    fn as_mut(&mut self) -> &mut TransactionRequest {
348        match self {
349            Self::Ethereum(tx) => tx,
350            #[cfg(feature = "base")]
351            Self::Base(tx) => tx.as_mut(),
352            #[cfg(any(feature = "base", feature = "optimism"))]
353            Self::Op(tx) => tx,
354            Self::Tempo(tx) => tx.as_mut(),
355            Self::Celo(tx) => &mut tx.inner,
356        }
357    }
358}
359
360impl TryFrom<WithOtherFields<TransactionRequest>> for FoundryTransactionRequest {
361    type Error = serde_json::Error;
362
363    fn try_from(tx: WithOtherFields<TransactionRequest>) -> Result<Self, Self::Error> {
364        if tx.transaction_type == Some(CIP64_TX_TYPE)
365            || tx.other.get("feeCurrency").is_some_and(|value| !value.is_null())
366        {
367            if tx.transaction_type.is_some_and(|ty| ty != CIP64_TX_TYPE)
368                || tx.gas_price.is_some()
369                || tx.has_eip4844_fields()
370                || tx.authorization_list.is_some()
371                || TEMPO_REQUEST_FIELDS.iter().any(|field| tx.other.contains_key(*field))
372                || [
373                    "accountChanges",
374                    "metadata",
375                    "sender",
376                    "senderAuth",
377                    "payer",
378                    "payerAuth",
379                    "sourceHash",
380                    "mint",
381                    "isSystemTx",
382                ]
383                .iter()
384                .any(|field| tx.other.get(*field).is_some_and(|value| !value.is_null()))
385            {
386                return Err(serde::de::Error::custom("conflicting CIP-64 transaction fields"));
387            }
388            let fee_currency =
389                tx.other.get_deserialized::<Option<Address>>("feeCurrency").transpose()?.flatten();
390            let mut inner = tx.inner;
391            inner.transaction_type = Some(CIP64_TX_TYPE);
392            return Ok(Self::Celo(Cip64TransactionRequest { inner, fee_currency }));
393        }
394        #[derive(Deserialize)]
395        struct NonZeroQuantity(#[serde(with = "alloy_serde::quantity::opt")] Option<NonZeroU64>);
396
397        #[cfg(feature = "base")]
398        {
399            let present = |field: &str| tx.other.get(field).is_some_and(|value| !value.is_null());
400            let base_fields =
401                ["accountChanges", "metadata", "sender", "senderAuth", "payer", "payerAuth"]
402                    .iter()
403                    .any(|field| present(field));
404            let phased_calls = tx
405                .other
406                .get("calls")
407                .and_then(serde_json::Value::as_array)
408                .is_some_and(|calls| calls.first().is_some_and(serde_json::Value::is_array));
409            let explicit_base = tx.transaction_type == Some(EIP8130_TRANSACTION_TYPE);
410            if base_fields || phased_calls || explicit_base {
411                if tx.transaction_type.is_some() && !explicit_base {
412                    return Err(serde::de::Error::custom(
413                        "Base fields conflict with transaction type",
414                    ));
415                }
416                if TEMPO_REQUEST_FIELDS.iter().any(|field| {
417                    !matches!(*field, "nonceKey" | "calls" | "validBefore" | "validAfter")
418                        && present(field)
419                }) {
420                    return Err(serde::de::Error::custom(
421                        "conflicting Base and Tempo request fields",
422                    ));
423                }
424                let mut base =
425                    serde_json::from_value::<BaseTransactionRequest>(serde_json::to_value(&tx)?)?;
426                base.as_mut().transaction_type = Some(EIP8130_TRANSACTION_TYPE);
427                return Ok(Self::Base(base));
428            }
429        }
430
431        if tx.transaction_type == Some(TEMPO_TX_TYPE_ID)
432            || TEMPO_REQUEST_FIELDS.iter().any(|field| {
433                tx.other.get(*field).is_some_and(|value| {
434                    !value.is_null()
435                        && !matches!(
436                            (*field, value),
437                            (
438                                "calls" | "aaAuthorizationList",
439                                serde_json::Value::Array(values)
440                            ) if values.is_empty()
441                        )
442                })
443            })
444        {
445            let mut tempo_tx_req: TempoTransactionRequest = tx.inner.into();
446            tempo_tx_req.fee_token =
447                tx.other.get_deserialized::<Option<Address>>("feeToken").transpose()?.flatten();
448            tempo_tx_req.nonce_key =
449                tx.other.get_deserialized::<Option<U256>>("nonceKey").transpose()?.flatten();
450            tempo_tx_req.calls =
451                tx.other.get_deserialized::<Vec<Call>>("calls").transpose()?.unwrap_or_default();
452            tempo_tx_req.key_type = tx
453                .other
454                .get_deserialized::<Option<SignatureType>>("keyType")
455                .transpose()?
456                .flatten();
457            tempo_tx_req.key_data =
458                tx.other.get_deserialized::<Option<_>>("keyData").transpose()?.flatten();
459            tempo_tx_req.key_id =
460                tx.other.get_deserialized::<Option<_>>("keyId").transpose()?.flatten();
461            tempo_tx_req.tempo_authorization_list = tx
462                .other
463                .get_deserialized::<Vec<TempoSignedAuthorization>>("aaAuthorizationList")
464                .transpose()?
465                .unwrap_or_default();
466            tempo_tx_req.key_authorization = tx
467                .other
468                .get_deserialized::<Option<SignedKeyAuthorization>>("keyAuthorization")
469                .transpose()?
470                .flatten();
471            tempo_tx_req.valid_before = tx
472                .other
473                .get_deserialized::<NonZeroQuantity>("validBefore")
474                .transpose()?
475                .and_then(|value| value.0);
476            tempo_tx_req.valid_after = tx
477                .other
478                .get_deserialized::<NonZeroQuantity>("validAfter")
479                .transpose()?
480                .and_then(|value| value.0);
481            tempo_tx_req.fee_payer_signature =
482                tx.other.get_deserialized::<Option<_>>("feePayerSignature").transpose()?.flatten();
483            return Ok(Self::Tempo(Box::new(tempo_tx_req)));
484        }
485        #[cfg(all(feature = "base", not(feature = "optimism")))]
486        if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
487            || get_deposit_tx_parts(&tx.other).is_ok()
488        {
489            return Ok(Self::Op(tx));
490        }
491        #[cfg(feature = "optimism")]
492        if tx.transaction_type == Some(DEPOSIT_TX_TYPE_ID)
493            || tx.transaction_type == Some(POST_EXEC_TX_TYPE_ID)
494            || get_deposit_tx_parts(&tx.other).is_ok()
495        {
496            return Ok(Self::Op(tx));
497        }
498        Ok(Self::Ethereum(tx.into_inner()))
499    }
500}
501
502impl From<TransactionRequest> for FoundryTransactionRequest {
503    fn from(tx: TransactionRequest) -> Self {
504        Self::Ethereum(tx)
505    }
506}
507
508impl From<FoundryTypedTx> for FoundryTransactionRequest {
509    fn from(tx: FoundryTypedTx) -> Self {
510        match tx {
511            FoundryTypedTx::Legacy(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
512            FoundryTypedTx::Eip2930(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
513            FoundryTypedTx::Eip1559(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
514            FoundryTypedTx::Eip4844(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
515            FoundryTypedTx::Eip7702(tx) => Self::Ethereum(Into::<TransactionRequest>::into(tx)),
516            #[cfg(any(feature = "base", feature = "optimism"))]
517            FoundryTypedTx::Deposit(tx) => {
518                let other = OtherFields::from_iter([
519                    ("sourceHash", serde_json::to_value(tx.source_hash).unwrap()),
520                    ("mint", serde_json::to_value(U256::from(tx.mint)).unwrap()),
521                    ("isSystemTx", serde_json::to_value(tx.is_system_transaction).unwrap()),
522                ]);
523                WithOtherFields { inner: Into::<TransactionRequest>::into(tx), other }
524                    .try_into()
525                    .expect("valid deposit transaction request")
526            }
527            #[cfg(feature = "optimism")]
528            FoundryTypedTx::PostExec(tx) => Self::new(WithOtherFields {
529                inner: Into::<TransactionRequest>::into(tx),
530                other: OtherFields::default(),
531            })
532            .expect("valid OP post-exec transaction request"),
533            #[cfg(feature = "base")]
534            FoundryTypedTx::Eip8130(tx) => {
535                Self::Base(super::base::simulation_request(tx, None, None, None))
536            }
537            FoundryTypedTx::Tempo(tx) => Self::Tempo(Box::new(tx.into())),
538            FoundryTypedTx::Celo(tx) => {
539                let mut inner: TransactionRequest = tx.inner.into();
540                inner.transaction_type = Some(CIP64_TX_TYPE);
541                Self::Celo(Cip64TransactionRequest { inner, fee_currency: tx.fee_currency })
542            }
543        }
544    }
545}
546
547impl From<FoundryTxEnvelope> for FoundryTransactionRequest {
548    fn from(tx: FoundryTxEnvelope) -> Self {
549        #[cfg(feature = "base")]
550        if let FoundryTxEnvelope::Eip8130(tx) = tx {
551            let from = tx.recover_sender().ok();
552            let sender_auth = Some(tx.sender_auth().clone());
553            let payer_auth = Some(tx.payer_auth().clone());
554            return Self::Base(super::base::simulation_request(
555                tx.into_tx(),
556                from,
557                sender_auth,
558                payer_auth,
559            ));
560        }
561        FoundryTypedTx::from(tx).into()
562    }
563}
564
565#[cfg(not(feature = "optimism"))]
566impl From<alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>> for FoundryTransactionRequest {
567    fn from(tx: alloy_rpc_types_eth::Transaction<FoundryTxEnvelope>) -> Self {
568        tx.inner.into_inner().into()
569    }
570}
571
572// TransactionBuilder trait implementation for FoundryNetwork
573impl TransactionBuilder for FoundryTransactionRequest {
574    fn chain_id(&self) -> Option<ChainId> {
575        self.as_ref().chain_id
576    }
577
578    fn set_chain_id(&mut self, chain_id: ChainId) {
579        self.as_mut().chain_id = Some(chain_id);
580    }
581
582    fn nonce(&self) -> Option<u64> {
583        self.as_ref().nonce
584    }
585
586    fn set_nonce(&mut self, nonce: u64) {
587        self.as_mut().nonce = Some(nonce);
588    }
589
590    fn take_nonce(&mut self) -> Option<u64> {
591        self.as_mut().nonce.take()
592    }
593
594    fn input(&self) -> Option<&alloy_primitives::Bytes> {
595        self.as_ref().input.input()
596    }
597
598    fn set_input<T: Into<alloy_primitives::Bytes>>(&mut self, input: T) {
599        self.as_mut().input.input = Some(input.into());
600    }
601
602    fn set_input_kind<T: Into<alloy_primitives::Bytes>>(
603        &mut self,
604        input: T,
605        kind: TransactionInputKind,
606    ) {
607        let inner = self.as_mut();
608        match kind {
609            TransactionInputKind::Input => inner.input.input = Some(input.into()),
610            TransactionInputKind::Data => inner.input.data = Some(input.into()),
611            TransactionInputKind::Both => {
612                let bytes = input.into();
613                inner.input.input = Some(bytes.clone());
614                inner.input.data = Some(bytes);
615            }
616        }
617    }
618
619    fn from(&self) -> Option<Address> {
620        self.as_ref().from
621    }
622
623    fn set_from(&mut self, from: Address) {
624        self.as_mut().from = Some(from);
625    }
626
627    fn kind(&self) -> Option<TxKind> {
628        self.as_ref().to
629    }
630
631    fn clear_kind(&mut self) {
632        self.as_mut().to = None;
633    }
634
635    fn set_kind(&mut self, kind: TxKind) {
636        self.as_mut().to = Some(kind);
637    }
638
639    fn value(&self) -> Option<U256> {
640        self.as_ref().value
641    }
642
643    fn set_value(&mut self, value: U256) {
644        self.as_mut().value = Some(value);
645    }
646
647    fn gas_price(&self) -> Option<u128> {
648        self.as_ref().gas_price
649    }
650
651    fn set_gas_price(&mut self, gas_price: u128) {
652        self.as_mut().gas_price = Some(gas_price);
653    }
654
655    fn max_fee_per_gas(&self) -> Option<u128> {
656        self.as_ref().max_fee_per_gas
657    }
658
659    fn set_max_fee_per_gas(&mut self, max_fee_per_gas: u128) {
660        self.as_mut().max_fee_per_gas = Some(max_fee_per_gas);
661    }
662
663    fn max_priority_fee_per_gas(&self) -> Option<u128> {
664        self.as_ref().max_priority_fee_per_gas
665    }
666
667    fn set_max_priority_fee_per_gas(&mut self, max_priority_fee_per_gas: u128) {
668        self.as_mut().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
669    }
670
671    fn gas_limit(&self) -> Option<u64> {
672        self.as_ref().gas
673    }
674
675    fn set_gas_limit(&mut self, gas_limit: u64) {
676        self.as_mut().gas = Some(gas_limit);
677    }
678
679    fn access_list(&self) -> Option<&AccessList> {
680        self.as_ref().access_list.as_ref()
681    }
682
683    fn set_access_list(&mut self, access_list: AccessList) {
684        self.as_mut().access_list = Some(access_list);
685    }
686}
687
688impl NetworkTransactionBuilder<FoundryNetwork> for FoundryTransactionRequest {
689    fn complete_type(&self, ty: FoundryTxType) -> Result<(), Vec<&'static str>> {
690        self.check_type(ty)
691    }
692
693    fn can_submit(&self) -> bool {
694        self.from().is_some()
695    }
696
697    fn can_build(&self) -> bool {
698        #[cfg(feature = "base")]
699        if self.is_base() {
700            return false;
701        }
702        if let Self::Celo(_) = self {
703            return self.check_type(FoundryTxType::Celo).is_ok();
704        }
705        if self.as_ref().can_build() || self.complete_tempo().is_ok() {
706            return true;
707        }
708        #[cfg(any(feature = "base", feature = "optimism"))]
709        if self.complete_deposit().is_ok() {
710            return true;
711        }
712        false
713    }
714
715    fn output_tx_type(&self) -> FoundryTxType {
716        self.preferred_type()
717    }
718
719    fn output_tx_type_checked(&self) -> Option<FoundryTxType> {
720        let pref = self.preferred_type();
721        self.check_type(pref).ok()?;
722        Some(pref)
723    }
724
725    /// Prepares [`FoundryTransactionRequest`] by trimming conflicting fields, and filling with
726    /// default values the mandatory fields.
727    fn prep_for_submission(&mut self) {
728        #[cfg(feature = "base")]
729        if self.is_base() {
730            return;
731        }
732        let preferred_type = self.preferred_type();
733        let inner = self.as_mut();
734        inner.transaction_type = Some(preferred_type as u8);
735        inner.gas.is_none().then(|| inner.set_gas_limit(Default::default()));
736        let is_deposit = {
737            #[cfg(any(feature = "base", feature = "optimism"))]
738            {
739                preferred_type.is_deposit()
740            }
741            #[cfg(not(any(feature = "base", feature = "optimism")))]
742            {
743                false
744            }
745        };
746        if !is_deposit && !preferred_type.is_tempo() && !preferred_type.is_celo() {
747            inner.trim_conflicting_keys();
748            inner.populate_blob_hashes();
749        }
750        if !is_deposit {
751            inner.nonce.is_none().then(|| inner.set_nonce(Default::default()));
752        }
753        if preferred_type.is_legacy() || preferred_type.is_eip2930() {
754            inner.gas_price.is_none().then(|| inner.set_gas_price(Default::default()));
755        }
756        if preferred_type.is_eip2930() {
757            inner.access_list.is_none().then(|| inner.set_access_list(Default::default()));
758        }
759        if preferred_type.is_eip1559()
760            || preferred_type.is_eip4844()
761            || preferred_type.is_eip7702()
762            || preferred_type.is_tempo()
763            || preferred_type.is_celo()
764        {
765            inner
766                .max_priority_fee_per_gas
767                .is_none()
768                .then(|| inner.set_max_priority_fee_per_gas(Default::default()));
769            inner.max_fee_per_gas.is_none().then(|| inner.set_max_fee_per_gas(Default::default()));
770        }
771        if preferred_type.is_eip4844() {
772            inner
773                .as_ref()
774                .max_fee_per_blob_gas()
775                .is_none()
776                .then(|| inner.as_mut().set_max_fee_per_blob_gas(Default::default()));
777        }
778    }
779
780    fn build_unsigned(self) -> BuildResult<FoundryTypedTx, FoundryNetwork> {
781        if let Err((tx_type, missing)) = self.missing_keys() {
782            return Err(TransactionBuilderError::InvalidTransactionRequest(tx_type, missing)
783                .into_unbuilt(self));
784        }
785        Ok(self.build_typed_tx().expect("checked by missing_keys"))
786    }
787
788    async fn build<W: NetworkWallet<FoundryNetwork>>(
789        self,
790        wallet: &W,
791    ) -> Result<FoundryTxEnvelope, TransactionBuilderError<FoundryNetwork>> {
792        Ok(wallet.sign_request(self).await?)
793    }
794}
795
796impl TransactionBuilder4844 for FoundryTransactionRequest {
797    fn max_fee_per_blob_gas(&self) -> Option<u128> {
798        self.as_ref().max_fee_per_blob_gas()
799    }
800
801    fn set_max_fee_per_blob_gas(&mut self, max_fee_per_blob_gas: u128) {
802        self.as_mut().set_max_fee_per_blob_gas(max_fee_per_blob_gas);
803    }
804
805    fn blob_sidecar(&self) -> Option<&BlobTransactionSidecarVariant> {
806        self.as_ref().blob_sidecar()
807    }
808
809    fn set_blob_sidecar(&mut self, sidecar: BlobTransactionSidecarVariant) {
810        self.as_mut().set_blob_sidecar(sidecar);
811    }
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817    use alloy_primitives::{B256, Bytes, Signature};
818    use tempo_primitives::{
819        TempoSignature, TempoTransaction,
820        transaction::{Authorization, KeyAuthorization, PrimitiveSignature},
821    };
822
823    fn default_tx_req() -> TransactionRequest {
824        TransactionRequest::default()
825            .with_to(Address::random())
826            .with_nonce(1)
827            .with_value(U256::from(1000000))
828            .with_gas_limit(1000000)
829            .with_max_fee_per_gas(1000000)
830            .with_max_priority_fee_per_gas(1000000)
831    }
832
833    #[test]
834    fn request_predicates() {
835        let ethereum = FoundryTransactionRequest::default();
836        assert!(ethereum.is_ethereum());
837        assert!(!ethereum.is_tempo());
838
839        let tempo = FoundryTransactionRequest::Tempo(Box::default());
840        assert!(tempo.is_tempo());
841        assert!(!tempo.is_ethereum());
842
843        #[cfg(any(feature = "base", feature = "optimism"))]
844        {
845            let op = FoundryTransactionRequest::Op(WithOtherFields::default());
846            assert!(op.is_op());
847            assert!(!op.is_ethereum());
848            assert!(!op.is_tempo());
849        }
850    }
851
852    #[test]
853    fn test_routing_ethereum_default() {
854        let tx = default_tx_req();
855        let req: FoundryTransactionRequest = WithOtherFields::new(tx).try_into().unwrap();
856
857        assert!(req.is_ethereum());
858        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
859    }
860
861    #[test]
862    fn test_routing_tempo_by_fee_token() {
863        let tx = default_tx_req();
864        let mut other = OtherFields::default();
865        other.insert("feeToken".to_string(), serde_json::to_value(Address::random()).unwrap());
866
867        let req: FoundryTransactionRequest =
868            FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
869
870        assert!(req.is_tempo());
871        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Tempo(_))));
872    }
873
874    #[cfg(feature = "base")]
875    #[test]
876    fn base_and_tempo_request_routing() {
877        let call = serde_json::json!({ "to": Address::ZERO, "data": "0x", "value": "0x0" });
878        for (value, expected_type) in [
879            (serde_json::json!({"calls": [call]}), FoundryTxType::Tempo),
880            (serde_json::json!({"nonceKey": "0x1"}), FoundryTxType::Tempo),
881            (serde_json::json!({"validBefore": "0x123"}), FoundryTxType::Tempo),
882            (serde_json::json!({"calls": []}), FoundryTxType::Eip1559),
883            (serde_json::json!({"calls": [[call]], "validBefore": 123}), FoundryTxType::Eip8130),
884            (serde_json::json!({"type": "0x79"}), FoundryTxType::Eip8130),
885            (serde_json::json!({"type": "0x79", "nonceKey": "0x1"}), FoundryTxType::Eip8130),
886            (serde_json::json!({"type": "0x76", "calls": [call]}), FoundryTxType::Tempo),
887            (
888                serde_json::json!({"sender": Address::ZERO, "feeToken": null}),
889                FoundryTxType::Eip8130,
890            ),
891        ] {
892            let request: FoundryTransactionRequest = serde_json::from_value(value.clone())
893                .unwrap_or_else(|err| panic!("{value}: {err}"));
894            assert_eq!(request.preferred_type(), expected_type, "{value}");
895            if expected_type.is_eip8130() {
896                assert!(request.is_base(), "{value}");
897                assert!(!request.can_build(), "{value}");
898            }
899            let roundtrip: FoundryTransactionRequest =
900                serde_json::from_value(serde_json::to_value(&request).unwrap()).unwrap();
901            assert_eq!(request, roundtrip);
902        }
903        for value in [
904            serde_json::json!({"type": "0x76", "calls": [[call]]}),
905            serde_json::json!({"type": "0x79", "feeToken": Address::ZERO}),
906            serde_json::json!({"type": "0x2", "sender": Address::ZERO}),
907            serde_json::json!({"type": "0x79", "calls": [call]}),
908        ] {
909            assert!(
910                serde_json::from_value::<FoundryTransactionRequest>(value.clone()).is_err(),
911                "{value}"
912            );
913        }
914    }
915
916    #[test]
917    fn test_routing_serialized_non_aa_tempo_request_to_ethereum() {
918        let request = TempoTransactionRequest { inner: default_tx_req(), ..Default::default() };
919        let request = serde_json::from_value::<WithOtherFields<TransactionRequest>>(
920            serde_json::to_value(request).unwrap(),
921        )
922        .unwrap();
923
924        let request = FoundryTransactionRequest::try_from(request).unwrap();
925
926        assert!(request.is_ethereum());
927        assert!(matches!(request.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
928    }
929
930    #[test]
931    #[cfg(any(feature = "base", feature = "optimism"))]
932    fn test_routing_op_by_deposit_fields() {
933        let tx = default_tx_req();
934        let mut other = OtherFields::default();
935        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
936        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
937        other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
938
939        let req: FoundryTransactionRequest =
940            FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
941
942        assert!(req.is_op());
943        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Deposit(_))));
944    }
945
946    #[test]
947    fn test_op_incomplete_routes_to_ethereum() {
948        let tx = default_tx_req();
949        let mut other = OtherFields::default();
950        // Only provide 2 of 3 required Op fields
951        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
952        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
953
954        let req: FoundryTransactionRequest =
955            FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
956
957        assert!(req.is_ethereum());
958        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
959    }
960
961    #[test]
962    fn test_ethereum_with_unrelated_other_fields() {
963        let tx = default_tx_req();
964        let mut other = OtherFields::default();
965        other.insert("anotherField".to_string(), serde_json::to_value(123).unwrap());
966
967        let req: FoundryTransactionRequest =
968            FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
969
970        assert!(req.is_ethereum());
971        assert!(matches!(req.build_unsigned(), Ok(FoundryTypedTx::Eip1559(_))));
972    }
973
974    #[test]
975    fn test_serialization_ethereum() {
976        let tx = default_tx_req();
977        let original: FoundryTransactionRequest = WithOtherFields::new(tx).try_into().unwrap();
978
979        let serialized = serde_json::to_string(&original).unwrap();
980        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
981
982        assert!(deserialized.is_ethereum());
983    }
984
985    #[test]
986    #[cfg(any(feature = "base", feature = "optimism"))]
987    fn test_serialization_op() {
988        let tx = default_tx_req();
989        let mut other = OtherFields::default();
990        other.insert("sourceHash".to_string(), serde_json::to_value(B256::ZERO).unwrap());
991        other.insert("mint".to_string(), serde_json::to_value(U256::from(1000)).unwrap());
992        other.insert("isSystemTx".to_string(), serde_json::to_value(false).unwrap());
993
994        let original: FoundryTransactionRequest =
995            FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
996
997        let serialized = serde_json::to_string(&original).unwrap();
998        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
999
1000        assert!(deserialized.is_op());
1001    }
1002
1003    #[test]
1004    fn test_serialization_tempo() {
1005        let tx = default_tx_req();
1006        let mut other = OtherFields::default();
1007        other.insert("feeToken".to_string(), serde_json::to_value(Address::ZERO).unwrap());
1008        other.insert("nonceKey".to_string(), serde_json::to_value(U256::from(42)).unwrap());
1009
1010        let original: FoundryTransactionRequest =
1011            FoundryTransactionRequest::new(WithOtherFields { inner: tx, other }).unwrap();
1012
1013        let serialized = serde_json::to_string(&original).unwrap();
1014        let deserialized: FoundryTransactionRequest = serde_json::from_str(&serialized).unwrap();
1015
1016        assert!(deserialized.is_tempo());
1017    }
1018
1019    #[test]
1020    fn test_tempo_request_decodes_all_extension_fields() {
1021        let signature = Signature::test_signature();
1022        let expected = TempoTransactionRequest {
1023            inner: TransactionRequest {
1024                transaction_type: Some(TEMPO_TX_TYPE_ID),
1025                ..default_tx_req()
1026            },
1027            fee_token: Some(Address::repeat_byte(0x11)),
1028            nonce_key: Some(U256::from(42)),
1029            calls: vec![Call {
1030                to: TxKind::Call(Address::repeat_byte(0x22)),
1031                value: U256::from(7),
1032                input: Bytes::from_static(&[0xde, 0xad]),
1033            }],
1034            key_type: Some(SignatureType::WebAuthn),
1035            key_data: Some(Bytes::from_static(&[0xbe, 0xef])),
1036            key_id: Some(Address::repeat_byte(0x33)),
1037            tempo_authorization_list: vec![TempoSignedAuthorization::new_unchecked(
1038                Authorization {
1039                    chain_id: U256::from(4217),
1040                    address: Address::repeat_byte(0x44),
1041                    nonce: 3,
1042                },
1043                TempoSignature::default(),
1044            )],
1045            key_authorization: Some(
1046                KeyAuthorization::unrestricted(
1047                    4217,
1048                    SignatureType::Secp256k1,
1049                    Address::repeat_byte(0x55),
1050                )
1051                .into_signed(PrimitiveSignature::Secp256k1(signature)),
1052            ),
1053            valid_before: NonZeroU64::new(100),
1054            valid_after: NonZeroU64::new(10),
1055            fee_payer_signature: Some(signature),
1056        };
1057        let request = serde_json::from_value::<WithOtherFields<TransactionRequest>>(
1058            serde_json::to_value(&expected).unwrap(),
1059        )
1060        .unwrap();
1061
1062        let decoded = FoundryTransactionRequest::try_from(request).unwrap();
1063
1064        let FoundryTransactionRequest::Tempo(decoded) = decoded else { panic!() };
1065        assert_eq!(*decoded, expected);
1066    }
1067
1068    #[test]
1069    fn test_malformed_tempo_field_is_rejected() {
1070        let mut other = OtherFields::default();
1071        other.insert("nonceKey".to_string(), serde_json::json!("not a quantity"));
1072
1073        let result =
1074            FoundryTransactionRequest::new(WithOtherFields { inner: default_tx_req(), other });
1075
1076        assert!(result.is_err());
1077    }
1078
1079    #[test]
1080    fn test_tempo_validity_uses_rpc_quantities() {
1081        let request = serde_json::from_value::<FoundryTransactionRequest>(serde_json::json!({
1082            "type": "0x76",
1083            "validBefore": "0x64",
1084            "validAfter": "0xa",
1085        }))
1086        .unwrap();
1087
1088        let FoundryTransactionRequest::Tempo(request) = request else { panic!() };
1089        assert_eq!(request.valid_before, NonZeroU64::new(100));
1090        assert_eq!(request.valid_after, NonZeroU64::new(10));
1091    }
1092
1093    #[test]
1094    fn test_tempo_typed_request_roundtrip_preserves_fields() {
1095        let tx = TempoTransaction {
1096            chain_id: 4217,
1097            nonce: 7,
1098            fee_payer_signature: None,
1099            valid_before: NonZeroU64::new(100),
1100            valid_after: NonZeroU64::new(10),
1101            gas_limit: 100_000,
1102            max_fee_per_gas: 20,
1103            max_priority_fee_per_gas: 2,
1104            fee_token: Some(Address::random()),
1105            access_list: Default::default(),
1106            calls: vec![Call {
1107                to: TxKind::Call(Address::random()),
1108                value: U256::from(42),
1109                input: vec![1, 2, 3].into(),
1110            }],
1111            tempo_authorization_list: Vec::new(),
1112            nonce_key: U256::from(9),
1113            key_authorization: None,
1114        };
1115
1116        let request: FoundryTransactionRequest = FoundryTypedTx::Tempo(tx.clone()).into();
1117        let rebuilt = request.build_unsigned().unwrap();
1118
1119        assert_eq!(rebuilt, FoundryTypedTx::Tempo(tx));
1120    }
1121
1122    #[test]
1123    #[cfg(any(feature = "base", feature = "optimism"))]
1124    fn test_deposit_typed_tx_roundtrip() {
1125        let deposit_tx = TxDeposit {
1126            from: Address::random(),
1127            source_hash: B256::random(),
1128            to: TxKind::Call(Address::random()),
1129            mint: 1000u128,
1130            value: U256::from(500),
1131            gas_limit: 21000,
1132            is_system_transaction: true,
1133            input: Default::default(),
1134        };
1135
1136        let req: FoundryTransactionRequest = FoundryTypedTx::Deposit(deposit_tx.clone()).into();
1137
1138        assert!(req.is_op());
1139
1140        let parts = req.get_deposit_tx_parts().expect("should parse deposit parts");
1141        assert_eq!(parts.source_hash, deposit_tx.source_hash);
1142        assert_eq!(parts.mint, Some(deposit_tx.mint));
1143        assert_eq!(parts.is_system_transaction, deposit_tx.is_system_transaction);
1144    }
1145}