Skip to main content

foundry_primitives/transaction/
envelope.rs

1use super::{CIP64_TX_TYPE, TxCip64};
2use alloy_consensus::{
3    SignableTransaction, Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy,
4    TxType, Typed2718,
5    crypto::RecoveryError,
6    transaction::{
7        SignerRecoverable, TxEip7702, TxHashRef,
8        eip4844::{TxEip4844Variant, TxEip4844WithSidecar},
9    },
10};
11use alloy_evm::{FromRecoveredTx, FromTxWithEncoded};
12use alloy_network::{
13    AnyRpcTransaction, AnyTxEnvelope, TransactionResponse, eip2718::Encodable2718,
14};
15use alloy_primitives::{Address, B256, Bytes, Signature, TxHash};
16use alloy_rpc_types::ConversionError;
17use revm::context::TxEnv;
18use tempo_primitives::{AASigned, TEMPO_TX_TYPE_ID, TempoSignature, TempoTransaction};
19use tempo_revm::TempoTxEnv;
20
21#[cfg(all(feature = "base", not(feature = "optimism")))]
22use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit};
23
24#[cfg(any(feature = "base", feature = "optimism"))]
25use alloy_consensus::Sealed;
26
27#[cfg(feature = "base")]
28use base_common_consensus::{BaseTxEnvelope, Eip8130Signed, TxEip8130};
29#[cfg(feature = "base")]
30use base_common_evm::EIP8130_TRANSACTION_TYPE;
31#[cfg(feature = "base")]
32use base_common_rpc_types::Transaction;
33
34#[cfg(feature = "optimism")]
35use alloy_consensus::Transaction as _;
36#[cfg(feature = "optimism")]
37use op_alloy_consensus::{
38    DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, PostExecPayload, TxDeposit, TxPostExec,
39};
40
41//
42/// Container type for signed, typed transactions.
43// NOTE(onbjerg): Boxing `Tempo(AASigned)` breaks `TransactionEnvelope` derive macro trait bounds.
44#[allow(clippy::large_enum_variant)]
45#[derive(Clone, Debug, TransactionEnvelope)]
46#[envelope(
47    tx_type_name = FoundryTxType,
48    typed = FoundryTypedTx,
49)]
50pub enum FoundryTxEnvelope {
51    /// Legacy transaction type
52    #[envelope(ty = 0)]
53    Legacy(Signed<TxLegacy>),
54    /// [EIP-2930] transaction.
55    ///
56    /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930
57    #[envelope(ty = 1)]
58    Eip2930(Signed<TxEip2930>),
59    /// [EIP-1559] transaction.
60    ///
61    /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559
62    #[envelope(ty = 2)]
63    Eip1559(Signed<TxEip1559>),
64    /// [EIP-4844] transaction.
65    ///
66    /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844
67    #[envelope(ty = 3)]
68    Eip4844(Signed<TxEip4844Variant>),
69    /// [EIP-7702] transaction.
70    ///
71    /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
72    #[envelope(ty = 4)]
73    Eip7702(Signed<TxEip7702>),
74    /// OP stack deposit transaction.
75    ///
76    /// See <https://docs.optimism.io/op-stack/bridging/deposit-flow>.
77    #[cfg(any(feature = "base", feature = "optimism"))]
78    #[envelope(ty = 126)]
79    Deposit(Sealed<TxDeposit>),
80    /// OP stack post-execution synthetic transaction.
81    #[cfg(feature = "optimism")]
82    #[envelope(ty = 0x7D)]
83    PostExec(Sealed<TxPostExec>),
84    /// Base EIP-8130 account-abstraction transaction.
85    #[cfg(feature = "base")]
86    #[envelope(ty = 0x79, typed = TxEip8130)]
87    Eip8130(Eip8130Signed),
88    /// Tempo transaction type.
89    ///
90    /// See <https://docs.tempo.xyz/protocol/transactions>.
91    #[envelope(ty = 0x76, typed = TempoTransaction)]
92    Tempo(AASigned),
93    /// Celo CIP-64 dynamic fee transaction.
94    #[envelope(ty = 0x7b)]
95    Celo(Signed<TxCip64>),
96}
97
98impl FoundryTxEnvelope {
99    /// Returns `true` if this is a legacy transaction.
100    #[inline]
101    pub const fn is_legacy(&self) -> bool {
102        matches!(self, Self::Legacy(_))
103    }
104
105    /// Returns `true` if this is an EIP-2930 transaction.
106    #[inline]
107    pub const fn is_eip2930(&self) -> bool {
108        matches!(self, Self::Eip2930(_))
109    }
110
111    /// Returns `true` if this is an EIP-1559 transaction.
112    #[inline]
113    pub const fn is_eip1559(&self) -> bool {
114        matches!(self, Self::Eip1559(_))
115    }
116
117    /// Returns `true` if this is an EIP-4844 transaction.
118    #[inline]
119    pub const fn is_eip4844(&self) -> bool {
120        matches!(self, Self::Eip4844(_))
121    }
122
123    /// Returns `true` if this is an EIP-7702 transaction.
124    #[inline]
125    pub const fn is_eip7702(&self) -> bool {
126        matches!(self, Self::Eip7702(_))
127    }
128
129    /// Returns `true` if this is an OP stack deposit transaction.
130    #[cfg(any(feature = "base", feature = "optimism"))]
131    #[inline]
132    pub const fn is_deposit(&self) -> bool {
133        matches!(self, Self::Deposit(_))
134    }
135
136    /// Returns `true` if this is an OP stack post-execution synthetic transaction.
137    #[cfg(feature = "optimism")]
138    #[inline]
139    pub const fn is_post_exec(&self) -> bool {
140        matches!(self, Self::PostExec(_))
141    }
142
143    /// Returns `true` if this is a Base EIP-8130 transaction.
144    #[cfg(feature = "base")]
145    #[inline]
146    pub const fn is_eip8130(&self) -> bool {
147        matches!(self, Self::Eip8130(_))
148    }
149
150    /// Converts the transaction into an Ethereum [`TxEnvelope`].
151    ///
152    /// Returns an error if the transaction is not part of the standard Ethereum transaction types.
153    pub fn try_into_eth(self) -> Result<TxEnvelope, Self> {
154        match self {
155            Self::Legacy(tx) => Ok(TxEnvelope::Legacy(tx)),
156            Self::Eip2930(tx) => Ok(TxEnvelope::Eip2930(tx)),
157            Self::Eip1559(tx) => Ok(TxEnvelope::Eip1559(tx)),
158            Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)),
159            Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)),
160            #[cfg(any(feature = "base", feature = "optimism"))]
161            Self::Deposit(_) => Err(self),
162            #[cfg(feature = "optimism")]
163            Self::PostExec(_) => Err(self),
164            #[cfg(feature = "base")]
165            Self::Eip8130(_) => Err(self),
166            Self::Tempo(_) | Self::Celo(_) => Err(self),
167        }
168    }
169
170    pub const fn sidecar(&self) -> Option<&TxEip4844WithSidecar> {
171        match self {
172            Self::Eip4844(signed_variant) => match signed_variant.tx() {
173                TxEip4844Variant::TxEip4844WithSidecar(with_sidecar) => Some(with_sidecar),
174                _ => None,
175            },
176            _ => None,
177        }
178    }
179
180    /// Drops pooled sidecars so the transaction uses its canonical block-body representation.
181    pub fn into_canonical(self) -> Self {
182        match self {
183            Self::Eip4844(tx) => Self::Eip4844(tx.map(TxEip4844Variant::drop_sidecar)),
184            tx => tx,
185        }
186    }
187
188    /// Returns the hash of the transaction.
189    ///
190    /// # Note
191    ///
192    /// If this transaction has the Impersonated signature then this returns a modified unique
193    /// hash. This allows us to treat impersonated transactions as unique.
194    pub fn hash(&self) -> B256 {
195        *self.tx_hash()
196    }
197
198    /// Returns `true` if this is a Tempo transaction.
199    pub const fn is_tempo(&self) -> bool {
200        matches!(self, Self::Tempo(_))
201    }
202
203    /// Returns `true` if this is a Tempo transaction with a nonzero nonce key.
204    pub fn has_nonzero_tempo_nonce_key(&self) -> bool {
205        matches!(self, Self::Tempo(tx) if !tx.tx().nonce_key.is_zero())
206    }
207
208    /// Recovers the Ethereum address which was used to sign the transaction.
209    pub fn recover(&self) -> Result<Address, RecoveryError> {
210        Ok(match self {
211            Self::Legacy(tx) => tx.recover_signer()?,
212            Self::Eip2930(tx) => tx.recover_signer()?,
213            Self::Eip1559(tx) => tx.recover_signer()?,
214            Self::Celo(tx) => tx.recover_signer()?,
215            Self::Eip4844(tx) => tx.recover_signer()?,
216            Self::Eip7702(tx) => tx.recover_signer()?,
217            #[cfg(any(feature = "base", feature = "optimism"))]
218            Self::Deposit(tx) => tx.from,
219            #[cfg(feature = "optimism")]
220            Self::PostExec(tx) => tx.inner().signer_address(),
221            #[cfg(feature = "base")]
222            Self::Eip8130(tx) => tx.recover_sender()?,
223            Self::Tempo(tx) => tx.signature().recover_signer(&tx.signature_hash())?,
224        })
225    }
226
227    /// EIP-2718 encodes a transaction held in its JSON-RPC form.
228    ///
229    /// [`AnyTxEnvelope`] panics rather than encode a transaction type alloy does not model, so
230    /// anything that is not plain Ethereum is routed through [`Self`], which knows the types
231    /// Foundry supports. Chains that can be forked but not executed, such as Arbitrum and its
232    /// Orbit rollups, mint types with no Foundry envelope; only their RPC representation is ever
233    /// available, which is not enough to reconstruct their consensus encoding.
234    pub fn encode_rpc_2718(transaction: &AnyRpcTransaction) -> Result<Bytes, ConversionError> {
235        if let AnyTxEnvelope::Ethereum(envelope) = &*transaction.inner.inner {
236            return Ok(envelope.encoded_2718().into());
237        }
238
239        Ok(Self::try_from(transaction.clone())?.encoded_2718().into())
240    }
241}
242
243impl FoundryTxType {
244    /// Returns `true` if this is a legacy transaction type.
245    pub const fn is_legacy(&self) -> bool {
246        matches!(self, Self::Legacy)
247    }
248
249    /// Returns `true` if this is an EIP-2930 transaction type.
250    pub const fn is_eip2930(&self) -> bool {
251        matches!(self, Self::Eip2930)
252    }
253
254    /// Returns `true` if this is an EIP-1559 transaction type.
255    pub const fn is_eip1559(&self) -> bool {
256        matches!(self, Self::Eip1559)
257    }
258
259    /// Returns `true` if this is an EIP-4844 transaction type.
260    pub const fn is_eip4844(&self) -> bool {
261        matches!(self, Self::Eip4844)
262    }
263
264    /// Returns `true` if this is an EIP-7702 transaction type.
265    pub const fn is_eip7702(&self) -> bool {
266        matches!(self, Self::Eip7702)
267    }
268
269    /// Returns `true` if this is an OP stack deposit transaction type.
270    #[cfg(any(feature = "base", feature = "optimism"))]
271    pub const fn is_deposit(&self) -> bool {
272        matches!(self, Self::Deposit)
273    }
274
275    /// Returns `true` if this is an OP stack post-execution synthetic transaction type.
276    #[cfg(feature = "optimism")]
277    pub const fn is_post_exec(&self) -> bool {
278        matches!(self, Self::PostExec)
279    }
280
281    /// Returns `true` if this is a Base EIP-8130 transaction type.
282    #[cfg(feature = "base")]
283    pub const fn is_eip8130(&self) -> bool {
284        matches!(self, Self::Eip8130)
285    }
286
287    /// Returns `true` if this is a Tempo transaction type.
288    pub const fn is_tempo(&self) -> bool {
289        matches!(self, Self::Tempo)
290    }
291
292    /// Returns `true` if this is a Celo CIP-64 transaction type.
293    pub const fn is_celo(&self) -> bool {
294        matches!(self, Self::Celo)
295    }
296}
297
298impl FoundryTypedTx {
299    /// Builds an envelope with a dummy signature for an impersonated account.
300    ///
301    /// The signature uses `r = 1` and `s = 1` because clients reject zero scalar values.
302    pub fn into_impersonated(self) -> FoundryTxEnvelope {
303        let signature = Signature::from_scalars_and_parity(
304            B256::with_last_byte(1),
305            B256::with_last_byte(1),
306            false,
307        );
308        match self {
309            Self::Legacy(tx) => FoundryTxEnvelope::Legacy(tx.into_signed(signature)),
310            Self::Eip2930(tx) => FoundryTxEnvelope::Eip2930(tx.into_signed(signature)),
311            Self::Eip1559(tx) => FoundryTxEnvelope::Eip1559(tx.into_signed(signature)),
312            Self::Celo(tx) => FoundryTxEnvelope::Celo(tx.into_signed(signature)),
313            Self::Eip7702(tx) => FoundryTxEnvelope::Eip7702(tx.into_signed(signature)),
314            Self::Eip4844(tx) => FoundryTxEnvelope::Eip4844(tx.into_signed(signature)),
315            #[cfg(any(feature = "base", feature = "optimism"))]
316            Self::Deposit(tx) => FoundryTxEnvelope::Deposit(Sealed::new(tx)),
317            #[cfg(feature = "optimism")]
318            Self::PostExec(_) => {
319                unreachable!("op post-exec txs should not be impersonated")
320            }
321            #[cfg(feature = "base")]
322            Self::Eip8130(_) => {
323                unreachable!("EIP-8130 requires a signed raw transaction envelope")
324            }
325            Self::Tempo(tx) => {
326                let tempo_sig: TempoSignature = signature.into();
327                FoundryTxEnvelope::Tempo(tx.into_signed(tempo_sig))
328            }
329        }
330    }
331
332    /// Returns `true` if this is an OP stack deposit transaction.
333    #[cfg(any(feature = "base", feature = "optimism"))]
334    pub const fn is_deposit(&self) -> bool {
335        matches!(self, Self::Deposit(_))
336    }
337
338    /// Returns `true` if this is an OP stack post-execution synthetic transaction.
339    #[cfg(feature = "optimism")]
340    pub const fn is_post_exec(&self) -> bool {
341        matches!(self, Self::PostExec(_))
342    }
343
344    /// Returns `true` if this is a Base EIP-8130 transaction.
345    #[cfg(feature = "base")]
346    pub const fn is_eip8130(&self) -> bool {
347        matches!(self, Self::Eip8130(_))
348    }
349
350    /// Returns `true` if this is a Tempo transaction.
351    pub const fn is_tempo(&self) -> bool {
352        matches!(self, Self::Tempo(_))
353    }
354}
355
356impl TxHashRef for FoundryTxEnvelope {
357    fn tx_hash(&self) -> &TxHash {
358        match self {
359            Self::Legacy(t) => t.hash(),
360            Self::Eip2930(t) => t.hash(),
361            Self::Eip1559(t) => t.hash(),
362            Self::Celo(t) => t.hash(),
363            Self::Eip4844(t) => t.hash(),
364            Self::Eip7702(t) => t.hash(),
365            #[cfg(any(feature = "base", feature = "optimism"))]
366            Self::Deposit(t) => t.hash_ref(),
367            #[cfg(feature = "optimism")]
368            Self::PostExec(t) => t.hash_ref(),
369            #[cfg(feature = "base")]
370            Self::Eip8130(t) => t.hash(),
371            Self::Tempo(t) => t.hash(),
372        }
373    }
374}
375
376impl SignerRecoverable for FoundryTxEnvelope {
377    fn recover_signer(&self) -> Result<Address, RecoveryError> {
378        self.recover()
379    }
380
381    fn recover_signer_unchecked(&self) -> Result<Address, RecoveryError> {
382        self.recover()
383    }
384}
385
386impl TryFrom<FoundryTxEnvelope> for TxEnvelope {
387    type Error = FoundryTxEnvelope;
388
389    fn try_from(envelope: FoundryTxEnvelope) -> Result<Self, Self::Error> {
390        envelope.try_into_eth()
391    }
392}
393
394impl From<TxEnvelope> for FoundryTxEnvelope {
395    fn from(tx: TxEnvelope) -> Self {
396        match tx {
397            TxEnvelope::Legacy(tx) => Self::Legacy(tx),
398            TxEnvelope::Eip2930(tx) => Self::Eip2930(tx),
399            TxEnvelope::Eip1559(tx) => Self::Eip1559(tx),
400            TxEnvelope::Eip4844(tx) => Self::Eip4844(tx),
401            TxEnvelope::Eip7702(tx) => Self::Eip7702(tx),
402        }
403    }
404}
405
406impl From<tempo_primitives::TempoTxEnvelope> for FoundryTxEnvelope {
407    fn from(tx: tempo_primitives::TempoTxEnvelope) -> Self {
408        match tx {
409            tempo_primitives::TempoTxEnvelope::Legacy(tx) => Self::Legacy(tx),
410            tempo_primitives::TempoTxEnvelope::Eip2930(tx) => Self::Eip2930(tx),
411            tempo_primitives::TempoTxEnvelope::Eip1559(tx) => Self::Eip1559(tx),
412            tempo_primitives::TempoTxEnvelope::Eip7702(tx) => Self::Eip7702(tx),
413            tempo_primitives::TempoTxEnvelope::AA(tx) => Self::Tempo(tx),
414        }
415    }
416}
417
418impl TryFrom<AnyRpcTransaction> for FoundryTxEnvelope {
419    type Error = ConversionError;
420
421    fn try_from(value: AnyRpcTransaction) -> Result<Self, Self::Error> {
422        #[cfg(feature = "base")]
423        if value.ty() == EIP8130_TRANSACTION_TYPE {
424            let rpc = serde_json::from_value::<Transaction>(
425                serde_json::to_value(&value)
426                    .map_err(|err| ConversionError::Custom(err.to_string()))?,
427            )
428            .map_err(|err| ConversionError::Custom(err.to_string()))?;
429            return match rpc.inner.into_inner() {
430                BaseTxEnvelope::Eip8130(tx) => Ok(Self::Eip8130(tx)),
431                _ => Err(ConversionError::Custom("expected Base EIP-8130 transaction".to_string())),
432            };
433        }
434        let transaction = value.into_inner();
435        let from = transaction.from();
436        match transaction.into_inner() {
437            AnyTxEnvelope::Ethereum(tx) => match tx {
438                TxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
439                TxEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
440                TxEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
441                TxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
442                TxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
443            },
444            AnyTxEnvelope::Unknown(tx) => {
445                if tx.ty() == CIP64_TX_TYPE {
446                    let mut fields = tx.inner.fields;
447                    fields.insert("hash".into(), serde_json::to_value(tx.hash).unwrap());
448                    return fields
449                        .deserialize_into::<Signed<TxCip64>>()
450                        .map(Self::Celo)
451                        .map_err(|err| ConversionError::Custom(err.to_string()));
452                }
453                if tx.ty() == TEMPO_TX_TYPE_ID {
454                    let tempo_tx = tx.inner.fields.deserialize_into::<AASigned>().map_err(|e| {
455                        ConversionError::Custom(format!("Failed to deserialize tempo tx: {e}"))
456                    })?;
457                    return Ok(Self::Tempo(tempo_tx));
458                }
459
460                #[cfg(all(feature = "base", not(feature = "optimism")))]
461                {
462                    let mut tx = tx;
463                    if tx.ty() == DEPOSIT_TX_TYPE_ID {
464                        tx.inner
465                            .fields
466                            .insert("from".to_string(), serde_json::to_value(from).unwrap());
467                        let deposit =
468                            tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|err| {
469                                ConversionError::Custom(format!(
470                                    "Failed to deserialize deposit transaction: {err}"
471                                ))
472                            })?;
473                        return Ok(Self::Deposit(Sealed::new(deposit)));
474                    }
475                    let tx_type = tx.ty();
476                    Err(ConversionError::Custom(format!(
477                        "Unknown transaction type: 0x{tx_type:02X}"
478                    )))
479                }
480                #[cfg(feature = "optimism")]
481                {
482                    let mut tx = tx;
483                    let _ = from;
484                    // Try to convert to deposit transaction
485                    if tx.ty() == DEPOSIT_TX_TYPE_ID {
486                        tx.inner
487                            .fields
488                            .insert("from".to_string(), serde_json::to_value(from).unwrap());
489                        let deposit_tx =
490                            tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|e| {
491                                ConversionError::Custom(format!(
492                                    "Failed to deserialize deposit tx: {e}"
493                                ))
494                            })?;
495
496                        return Ok(Self::Deposit(Sealed::new(deposit_tx)));
497                    }
498
499                    if tx.ty() == POST_EXEC_TX_TYPE_ID {
500                        // The RPC form carries the RLP-encoded `PostExecPayload` in `input`;
501                        // `TxPostExec`'s own serde form expects the payload fields instead.
502                        let input = tx
503                            .inner
504                            .fields
505                            .get_deserialized::<Bytes>("input")
506                            .ok_or_else(|| {
507                                ConversionError::Custom(
508                                    "Post-exec tx is missing `input`".to_string(),
509                                )
510                            })?
511                            .map_err(|e| {
512                                ConversionError::Custom(format!(
513                                    "Failed to deserialize post-exec tx `input`: {e}"
514                                ))
515                            })?;
516                        let payload =
517                            PostExecPayload::from_rlp_bytes(input.as_ref()).map_err(|e| {
518                                ConversionError::Custom(format!(
519                                    "Failed to decode post-exec tx payload: {e}"
520                                ))
521                            })?;
522
523                        return Ok(Self::PostExec(Sealed::new(TxPostExec::new(payload))));
524                    }
525
526                    let tx_type = tx.ty();
527                    Err(ConversionError::Custom(format!(
528                        "Unknown transaction type: 0x{tx_type:02X}"
529                    )))
530                }
531                #[cfg(not(any(feature = "base", feature = "optimism")))]
532                {
533                    let _ = from;
534                    let tx_type = tx.ty();
535                    Err(ConversionError::Custom(format!(
536                        "Unknown transaction type: 0x{tx_type:02X}"
537                    )))
538                }
539            }
540        }
541    }
542}
543
544impl FromRecoveredTx<FoundryTxEnvelope> for TxEnv {
545    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
546        match tx {
547            FoundryTxEnvelope::Legacy(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
548            FoundryTxEnvelope::Eip2930(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
549            FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
550            // Anvil preserves the CIP-64 envelope but executes with native EIP-1559 fees.
551            FoundryTxEnvelope::Celo(signed_tx) => {
552                Self::from_recovered_tx(&signed_tx.tx().inner, caller)
553            }
554            FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
555            FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
556            #[cfg(any(feature = "base", feature = "optimism"))]
557            FoundryTxEnvelope::Deposit(sealed_tx) => {
558                let tx = sealed_tx.inner();
559                Self {
560                    tx_type: tx.ty(),
561                    caller,
562                    gas_limit: tx.gas_limit,
563                    kind: tx.to,
564                    value: tx.value,
565                    data: tx.input.clone(),
566                    ..Default::default()
567                }
568            }
569            #[cfg(feature = "optimism")]
570            FoundryTxEnvelope::PostExec(sealed_tx) => {
571                let tx = sealed_tx.inner();
572                Self {
573                    tx_type: tx.ty(),
574                    caller,
575                    kind: tx.kind(),
576                    data: tx.input.clone(),
577                    ..Default::default()
578                }
579            }
580            #[cfg(feature = "base")]
581            FoundryTxEnvelope::Eip8130(_) => {
582                unreachable!("EIP-8130 transaction in Ethereum context")
583            }
584            FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Ethereum context"),
585        }
586    }
587}
588
589impl FromTxWithEncoded<FoundryTxEnvelope> for TxEnv {
590    fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
591        Self::from_recovered_tx(tx, sender)
592    }
593}
594
595impl FromRecoveredTx<FoundryTxEnvelope> for TempoTxEnv {
596    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
597        match tx {
598            FoundryTxEnvelope::Legacy(signed_tx) => {
599                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
600            }
601            FoundryTxEnvelope::Eip2930(signed_tx) => {
602                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
603            }
604            FoundryTxEnvelope::Eip1559(signed_tx) => {
605                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
606            }
607            FoundryTxEnvelope::Eip4844(signed_tx) => {
608                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
609            }
610            FoundryTxEnvelope::Eip7702(signed_tx) => {
611                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
612            }
613            #[cfg(any(feature = "base", feature = "optimism"))]
614            FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"),
615            #[cfg(feature = "optimism")]
616            FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"),
617            #[cfg(feature = "base")]
618            FoundryTxEnvelope::Eip8130(_) => {
619                unreachable!("EIP-8130 transaction in Tempo context")
620            }
621            FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller),
622            FoundryTxEnvelope::Celo(_) => unreachable!("CIP-64 transaction in Tempo context"),
623        }
624    }
625}
626
627impl FromTxWithEncoded<FoundryTxEnvelope> for TempoTxEnv {
628    fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
629        Self::from_recovered_tx(tx, sender)
630    }
631}
632
633impl std::fmt::Display for FoundryTxType {
634    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
635        match self {
636            Self::Legacy => write!(f, "legacy"),
637            Self::Eip2930 => write!(f, "eip2930"),
638            Self::Eip1559 => write!(f, "eip1559"),
639            Self::Eip4844 => write!(f, "eip4844"),
640            Self::Eip7702 => write!(f, "eip7702"),
641            #[cfg(any(feature = "base", feature = "optimism"))]
642            Self::Deposit => write!(f, "deposit"),
643            #[cfg(feature = "optimism")]
644            Self::PostExec => write!(f, "post-exec"),
645            #[cfg(feature = "base")]
646            Self::Eip8130 => write!(f, "eip8130"),
647            Self::Tempo => write!(f, "tempo"),
648            Self::Celo => write!(f, "celo"),
649        }
650    }
651}
652
653impl From<TxType> for FoundryTxType {
654    fn from(tx: TxType) -> Self {
655        match tx {
656            TxType::Legacy => Self::Legacy,
657            TxType::Eip2930 => Self::Eip2930,
658            TxType::Eip1559 => Self::Eip1559,
659            TxType::Eip4844 => Self::Eip4844,
660            TxType::Eip7702 => Self::Eip7702,
661        }
662    }
663}
664
665impl From<FoundryTxEnvelope> for FoundryTypedTx {
666    fn from(envelope: FoundryTxEnvelope) -> Self {
667        match envelope {
668            FoundryTxEnvelope::Legacy(signed_tx) => Self::Legacy(signed_tx.strip_signature()),
669            FoundryTxEnvelope::Eip2930(signed_tx) => Self::Eip2930(signed_tx.strip_signature()),
670            FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()),
671            FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()),
672            FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()),
673            #[cfg(any(feature = "base", feature = "optimism"))]
674            FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.unseal()),
675            #[cfg(feature = "optimism")]
676            FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.unseal()),
677            #[cfg(feature = "base")]
678            FoundryTxEnvelope::Eip8130(signed_tx) => Self::Eip8130(signed_tx.into_tx()),
679            FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()),
680            FoundryTxEnvelope::Celo(signed_tx) => Self::Celo(signed_tx.strip_signature()),
681        }
682    }
683}
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688    use alloy_primitives::{TxKind, U256, address, b256, hex};
689    use alloy_rlp::Decodable;
690    use std::str::FromStr;
691
692    fn signed<T>(tx: T) -> Signed<T> {
693        Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO)
694    }
695
696    /// A plain Ethereum transaction in its JSON-RPC form.
697    const ETH_RPC_TX: &str = r#"{"type":"0x0","chainId":"0x1","nonce":"0x15","gasPrice":"0x4a817c800","gas":"0xc350","to":"0xf02c1c8e6114b1dbe8937a39260b5b0a374432bb","value":"0xf3dbb76162000","input":"0x68656c6c6f21","r":"0x1b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5fea","s":"0x4ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c","v":"0x25","hash":"0x88df016429689c079f3b2f6ad39fa052532c56795b733da78a91ebe6a713944b","blockHash":"0x1d59ff54b1eb26b013ce3cb5fc9dab3705b415a67127a003c3e61eb445bb8df2","blockNumber":"0x5daf3b","transactionIndex":"0x41","from":"0xa7d9ddbe1f17865597fbd27ec712455208b6b76d"}"#;
698
699    /// An OP-stack post-exec transaction (SDM, type `0x7D`) as returned by
700    /// `eth_getTransactionByHash`. The RLP-encoded `PostExecPayload` is carried in `input`; the
701    /// remaining fields are derived placeholders.
702    #[cfg(feature = "optimism")]
703    const OP_POST_EXEC_RPC_TX: &str = r#"{"blockHash":"0x72edd91c1b181b566e08846b9fe67e3d746c4e6555e6fb81f0d1acd9465f7322","blockNumber":"0x44ee2d","blockTimestamp":"0x6aadaad9","from":"0x0000000000000000000000000000000000000000","gas":"0x0","gasPrice":"0xfb","hash":"0x748fc6eb383fc0f2a92089556f639d4bdb1d363cb50e1be8acae2df338ba6963","input":"0xf83d018344ee2df7c4028207d0c4038207d0c4048207d0c4058207d0c4068207d0c4078207d0c4088207d0c4098207d0c40a8207d0c40b8207d0c40c8207d0","transactionIndex":"0xd","type":"0x7d","value":"0x0"}"#;
704
705    /// An `ArbitrumInternalTx`, a type alloy models only as [`AnyTxEnvelope::Unknown`].
706    const ARBITRUM_INTERNAL_RPC_TX: &str = r#"{"type":"0x6a","chainId":"0xa4b1","nonce":"0x0","gasPrice":"0x0","gas":"0x0","to":"0x00000000000000000000000000000000000a4b05","value":"0x0","input":"0x6bf6a42d","r":"0x0","s":"0x0","v":"0x0","hash":"0xe5ad4cc44e5cd67a464c038af87169fde2bd475f2c00306bd2d55ca2c5e4452e","blockHash":"0x0ce1511da42af573bac6870ef058d63bc4c8552440e97c149d4d539c482b5f7a","blockNumber":"0x1dc83ddc","transactionIndex":"0x0","from":"0x00000000000000000000000000000000000a4b05"}"#;
707
708    #[test]
709    fn encode_rpc_2718_matches_consensus_encoding() {
710        let tx: AnyRpcTransaction = serde_json::from_str(ETH_RPC_TX).unwrap();
711        let expected = hex!(
712            "f871158504a817c80082c35094f02c1c8e6114b1dbe8937a39260b5b0a374432bb870f3dbb761620008668656c6c6f2125a01b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5feaa04ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c"
713        );
714
715        assert_eq!(FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap(), expected[..]);
716    }
717
718    #[test]
719    fn encode_rpc_2718_rejects_unmodeled_type() {
720        let tx: AnyRpcTransaction = serde_json::from_str(ARBITRUM_INTERNAL_RPC_TX).unwrap();
721
722        // `AnyTxEnvelope::encode_2718` panics on this type, so it must not be reached.
723        assert!(FoundryTxEnvelope::encode_rpc_2718(&tx).is_err());
724    }
725
726    #[cfg(feature = "optimism")]
727    #[test]
728    fn encode_rpc_2718_post_exec_tx() {
729        let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
730
731        let encoded = FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap();
732
733        // The 2718 envelope is the `0x7d` type byte followed by the payload carried in `input`.
734        let expected = hex!(
735            "7df83d018344ee2df7c4028207d0c4038207d0c4048207d0c4058207d0c4068207d0c4078207d0c4088207d0c4098207d0c40a8207d0c40b8207d0c40c8207d0"
736        );
737        assert_eq!(encoded, expected[..]);
738    }
739
740    /// The RPC form carries the payload as RLP in `input`, not as a `PostExecPayload` object, and
741    /// the recomputed hash matches the one the node reported.
742    #[cfg(feature = "optimism")]
743    #[test]
744    fn post_exec_rpc_tx_decodes() {
745        let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
746
747        let envelope = FoundryTxEnvelope::try_from(tx).unwrap();
748        let FoundryTxEnvelope::PostExec(sealed) = &envelope else {
749            panic!("expected a post-exec envelope, got {envelope:?}");
750        };
751
752        assert_eq!(
753            sealed.hash(),
754            b256!("0x748fc6eb383fc0f2a92089556f639d4bdb1d363cb50e1be8acae2df338ba6963")
755        );
756        // 11 rebated transactions, at indexes 2..=12, each refunded 2000 gas.
757        let payload = &sealed.inner().payload;
758        assert_eq!(payload.block_number, 0x44ee2d);
759        assert_eq!(payload.gas_refund_entries.len(), 11);
760    }
761
762    #[test]
763    fn tx_type_predicates() {
764        assert!(FoundryTxType::Legacy.is_legacy());
765        assert!(FoundryTxType::Eip2930.is_eip2930());
766        assert!(FoundryTxType::Eip1559.is_eip1559());
767        assert!(FoundryTxType::Eip4844.is_eip4844());
768        assert!(FoundryTxType::Eip7702.is_eip7702());
769        assert!(FoundryTxType::Tempo.is_tempo());
770        assert!(!FoundryTxType::Tempo.is_legacy());
771
772        #[cfg(any(feature = "base", feature = "optimism"))]
773        assert!(FoundryTxType::Deposit.is_deposit());
774        #[cfg(feature = "base")]
775        assert!(FoundryTxType::Eip8130.is_eip8130());
776        #[cfg(feature = "optimism")]
777        {
778            assert!(FoundryTxType::PostExec.is_post_exec());
779            assert!(!FoundryTxType::Deposit.is_post_exec());
780        }
781    }
782
783    #[test]
784    fn typed_tx_predicates() {
785        assert!(FoundryTypedTx::Legacy(TxLegacy::default()).is_legacy());
786        assert!(FoundryTypedTx::Eip2930(TxEip2930::default()).is_eip2930());
787        assert!(FoundryTypedTx::Eip1559(TxEip1559::default()).is_eip1559());
788        assert!(
789            FoundryTypedTx::Eip4844(TxEip4844Variant::TxEip4844(Default::default())).is_eip4844()
790        );
791        assert!(FoundryTypedTx::Eip7702(TxEip7702::default()).is_eip7702());
792        assert!(FoundryTypedTx::Tempo(TempoTransaction::default()).is_tempo());
793
794        #[cfg(any(feature = "base", feature = "optimism"))]
795        assert!(FoundryTypedTx::Deposit(TxDeposit::default()).is_deposit());
796        #[cfg(feature = "base")]
797        assert!(FoundryTypedTx::Eip8130(TxEip8130::default()).is_eip8130());
798        #[cfg(feature = "optimism")]
799        {
800            assert!(FoundryTypedTx::PostExec(TxPostExec::default()).is_post_exec());
801        }
802    }
803
804    #[test]
805    fn tx_envelope_predicates() {
806        assert!(FoundryTxEnvelope::Legacy(signed(TxLegacy::default())).is_legacy());
807        assert!(FoundryTxEnvelope::Eip2930(signed(TxEip2930::default())).is_eip2930());
808        assert!(FoundryTxEnvelope::Eip1559(signed(TxEip1559::default())).is_eip1559());
809        assert!(
810            FoundryTxEnvelope::Eip4844(signed(TxEip4844Variant::TxEip4844(Default::default())))
811                .is_eip4844()
812        );
813        assert!(FoundryTxEnvelope::Eip7702(signed(TxEip7702::default())).is_eip7702());
814
815        #[cfg(any(feature = "base", feature = "optimism"))]
816        assert!(FoundryTxEnvelope::Deposit(Sealed::new(TxDeposit::default())).is_deposit());
817        #[cfg(feature = "base")]
818        assert!(
819            FoundryTxEnvelope::Eip8130(Eip8130Signed::new(
820                TxEip8130::default(),
821                Default::default(),
822                Default::default()
823            ))
824            .is_eip8130()
825        );
826        #[cfg(feature = "optimism")]
827        {
828            assert!(FoundryTxEnvelope::PostExec(Sealed::new(TxPostExec::default())).is_post_exec());
829        }
830    }
831
832    #[test]
833    fn impersonated_tx_uses_nonzero_dummy_signature() {
834        let FoundryTxEnvelope::Legacy(tx) =
835            FoundryTypedTx::Legacy(TxLegacy::default()).into_impersonated()
836        else {
837            panic!("expected legacy transaction");
838        };
839
840        assert_eq!(tx.signature().r(), U256::ONE);
841        assert_eq!(tx.signature().s(), U256::ONE);
842        assert!(!tx.signature().v());
843    }
844
845    #[test]
846    fn test_decode_call() {
847        let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
848        let decoded = FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap();
849
850        let tx = TxLegacy {
851            nonce: 2u64,
852            gas_price: 1000000000u128,
853            gas_limit: 100000,
854            to: TxKind::Call(Address::from_slice(
855                &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..],
856            )),
857            value: U256::from(1000000000000000u64),
858            input: Bytes::default(),
859            chain_id: Some(4),
860        };
861
862        let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap();
863
864        let tx = FoundryTxEnvelope::Legacy(Signed::new_unchecked(
865            tx,
866            signature,
867            b256!("0xa517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"),
868        ));
869
870        assert_eq!(tx, decoded);
871    }
872
873    #[test]
874    fn test_decode_create_goerli() {
875        // test that an example create tx from goerli decodes properly
876        let tx_bytes =
877              hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471")
878                  .unwrap();
879        let _decoded = FoundryTxEnvelope::decode(&mut &tx_bytes[..]).unwrap();
880    }
881
882    #[test]
883    fn can_recover_sender() {
884        // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f
885        let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap();
886
887        let Ok(FoundryTxEnvelope::Eip1559(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
888            panic!("decoding FoundryTxEnvelope failed");
889        };
890
891        assert_eq!(
892            tx.hash(),
893            &b256!("0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f")
894        );
895        assert_eq!(
896            tx.recover_signer().unwrap(),
897            address!("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5")
898        );
899    }
900
901    // Test vector from https://sepolia.etherscan.io/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
902    // Blobscan: https://sepolia.blobscan.com/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
903    #[test]
904    fn test_decode_live_4844_tx() {
905        use alloy_primitives::{address, b256};
906
907        // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
908        let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap();
909        let res = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
910        assert!(res.is_type(3));
911
912        let tx = match res {
913            FoundryTxEnvelope::Eip4844(tx) => tx,
914            _ => unreachable!(),
915        };
916
917        assert_eq!(tx.tx().tx().to, address!("0x11E9CA82A3a762b4B5bd264d4173a242e7a77064"));
918
919        assert_eq!(
920            tx.tx().tx().blob_versioned_hashes,
921            vec![
922                b256!("0x012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"),
923                b256!("0x0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"),
924                b256!("0x013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"),
925                b256!("0x01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"),
926                b256!("0x011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549")
927            ]
928        );
929
930        let from = tx.recover_signer().unwrap();
931        assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
932    }
933
934    #[test]
935    fn can_recover_sender_not_normalized() {
936        let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
937
938        let Ok(FoundryTxEnvelope::Legacy(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
939            panic!("decoding FoundryTxEnvelope failed");
940        };
941
942        assert_eq!(tx.tx().input, Bytes::from(b""));
943        assert_eq!(tx.tx().gas_price, 1);
944        assert_eq!(tx.tx().gas_limit, 21000);
945        assert_eq!(tx.tx().nonce, 0);
946        if let TxKind::Call(to) = tx.tx().to {
947            assert_eq!(to, address!("0x095e7baea6a6c7c4c2dfeb977efac326af552d87"));
948        } else {
949            panic!("expected a call transaction");
950        }
951        assert_eq!(tx.tx().value, U256::from(0x0au64));
952        assert_eq!(
953            tx.recover_signer().unwrap(),
954            address!("0f65fe9276bc9a24ae7083ae28e2660ef72df99e")
955        );
956    }
957
958    #[test]
959    fn deser_to_type_tx() {
960        let tx = r#"
961        {
962            "type": "0x2",
963            "chainId": "0x7a69",
964            "nonce": "0x0",
965            "gas": "0x5209",
966            "maxFeePerGas": "0x77359401",
967            "maxPriorityFeePerGas": "0x1",
968            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
969            "value": "0x0",
970            "accessList": [],
971            "input": "0x",
972            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
973            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
974            "yParity": "0x0",
975            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
976        }"#;
977
978        let _typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
979    }
980
981    #[test]
982    fn test_from_recovered_tx_legacy() {
983        let tx = r#"
984        {
985            "type": "0x0",
986            "chainId": "0x1",
987            "nonce": "0x0",
988            "gas": "0x5208",
989            "gasPrice": "0x1",
990            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
991            "value": "0x1",
992            "input": "0x",
993            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
994            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
995            "v": "0x1b",
996            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
997        }"#;
998
999        let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
1000        let sender = typed_tx.recover().unwrap();
1001
1002        // Test TxEnv conversion via FromRecoveredTx trait
1003        let tx_env = TxEnv::from_recovered_tx(&typed_tx, sender);
1004        assert_eq!(tx_env.caller, sender);
1005        assert_eq!(tx_env.gas_limit, 0x5208);
1006        assert_eq!(tx_env.gas_price, 1);
1007    }
1008
1009    // Test vector from Tempo testnet:
1010    // https://explorer.testnet.tempo.xyz/tx/0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd
1011    #[test]
1012    fn test_decode_encode_tempo_tx() {
1013        use alloy_primitives::address;
1014        use tempo_primitives::TEMPO_TX_TYPE_ID;
1015
1016        let tx_hash: TxHash =
1017            b256!("0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd");
1018
1019        // Raw transaction from Tempo testnet via eth_getRawTransactionByHash
1020        let raw_tx = hex::decode(
1021            "76f9025e82a5bd808502cb4178008302d178f8fcf85c9420c000000000000000000000000000000000000080b844095ea7b3000000000000000000000000dec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000989680f89c94dec000000000000000000000000000000000000080b884f8856c0f00000000000000000000000020c000000000000000000000000000000000000000000000000000000000000020c00000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000097d330c0808080809420c000000000000000000000000000000000000180c0b90133027b98b7a8e6c68d7eac741a52e6fdae0560ce3c16ef5427ad46d7a54d0ed86dd41d000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2238453071464a7a50585167546e645473643649456659457776323173516e626966374c4741776e4b43626b222c226f726967696e223a2268747470733a2f2f74656d706f2d6465782e76657263656c2e617070222c2263726f73734f726967696e223a66616c73657dcfd45c3b19745a42f80b134dcb02a8ba099a0e4e7be1984da54734aa81d8f29f74bb9170ae6d25bd510c83fe35895ee5712efe13980a5edc8094c534e23af85eaacc80b21e45fb11f349424dce3a2f23547f60c0ff2f8bcaede2a247545ce8dd87abf0dbb7a5c9507efae2e43833356651b45ac576c2e61cec4e9c0f41fcbf6e",
1022        )
1023        .unwrap();
1024
1025        let tempo_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
1026
1027        // Verify it's a Tempo transaction (type 0x76)
1028        assert!(tempo_tx.is_type(TEMPO_TX_TYPE_ID));
1029
1030        let FoundryTxEnvelope::Tempo(ref aa_signed) = tempo_tx else {
1031            panic!("Expected Tempo transaction");
1032        };
1033
1034        // Verify the chain ID
1035        assert_eq!(aa_signed.tx().chain_id, 42429);
1036
1037        // Verify the fee token
1038        assert_eq!(
1039            aa_signed.tx().fee_token,
1040            Some(address!("0x20C0000000000000000000000000000000000001"))
1041        );
1042
1043        // Verify gas limit
1044        assert_eq!(aa_signed.tx().gas_limit, 184696);
1045
1046        // Verify we have 2 calls
1047        assert_eq!(aa_signed.tx().calls.len(), 2);
1048
1049        // Verify the hash
1050        assert_eq!(tx_hash, tempo_tx.hash());
1051
1052        // Verify round-trip encoding
1053        let mut encoded = Vec::new();
1054        tempo_tx.encode_2718(&mut encoded);
1055        assert_eq!(raw_tx, encoded);
1056
1057        // Verify sender recovery (WebAuthn signature)
1058        let sender = tempo_tx.recover().unwrap();
1059        assert_eq!(sender, address!("0x566Ff0f4a6114F8072ecDC8A7A8A13d8d0C6B45F"));
1060    }
1061}