Skip to main content

foundry_primitives/transaction/
envelope.rs

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