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 mut fields = tx.inner.fields;
455                    fields.insert("hash".into(), serde_json::to_value(tx.hash).unwrap());
456                    let tempo_tx = fields.deserialize_into::<AASigned>().map_err(|e| {
457                        ConversionError::Custom(format!("Failed to deserialize tempo tx: {e}"))
458                    })?;
459                    return Ok(Self::Tempo(tempo_tx));
460                }
461
462                #[cfg(all(feature = "base", not(feature = "optimism")))]
463                {
464                    let mut tx = tx;
465                    if tx.ty() == DEPOSIT_TX_TYPE_ID {
466                        tx.inner
467                            .fields
468                            .insert("from".to_string(), serde_json::to_value(from).unwrap());
469                        let deposit =
470                            tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|err| {
471                                ConversionError::Custom(format!(
472                                    "Failed to deserialize deposit transaction: {err}"
473                                ))
474                            })?;
475                        return Ok(Self::Deposit(Sealed::new(deposit)));
476                    }
477                    let tx_type = tx.ty();
478                    Err(ConversionError::Custom(format!(
479                        "Unknown transaction type: 0x{tx_type:02X}"
480                    )))
481                }
482                #[cfg(feature = "optimism")]
483                {
484                    let mut tx = tx;
485                    let _ = from;
486                    // Try to convert to deposit transaction
487                    if tx.ty() == DEPOSIT_TX_TYPE_ID {
488                        tx.inner
489                            .fields
490                            .insert("from".to_string(), serde_json::to_value(from).unwrap());
491                        let deposit_tx =
492                            tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|e| {
493                                ConversionError::Custom(format!(
494                                    "Failed to deserialize deposit tx: {e}"
495                                ))
496                            })?;
497
498                        return Ok(Self::Deposit(Sealed::new(deposit_tx)));
499                    }
500
501                    if tx.ty() == POST_EXEC_TX_TYPE_ID {
502                        // The RPC form carries the RLP-encoded `PostExecPayload` in `input`;
503                        // `TxPostExec`'s own serde form expects the payload fields instead.
504                        let input = tx
505                            .inner
506                            .fields
507                            .get_deserialized::<Bytes>("input")
508                            .ok_or_else(|| {
509                                ConversionError::Custom(
510                                    "Post-exec tx is missing `input`".to_string(),
511                                )
512                            })?
513                            .map_err(|e| {
514                                ConversionError::Custom(format!(
515                                    "Failed to deserialize post-exec tx `input`: {e}"
516                                ))
517                            })?;
518                        let payload =
519                            PostExecPayload::from_rlp_bytes(input.as_ref()).map_err(|e| {
520                                ConversionError::Custom(format!(
521                                    "Failed to decode post-exec tx payload: {e}"
522                                ))
523                            })?;
524
525                        return Ok(Self::PostExec(Sealed::new(TxPostExec::new(payload))));
526                    }
527
528                    let tx_type = tx.ty();
529                    Err(ConversionError::Custom(format!(
530                        "Unknown transaction type: 0x{tx_type:02X}"
531                    )))
532                }
533                #[cfg(not(any(feature = "base", feature = "optimism")))]
534                {
535                    let _ = from;
536                    let tx_type = tx.ty();
537                    Err(ConversionError::Custom(format!(
538                        "Unknown transaction type: 0x{tx_type:02X}"
539                    )))
540                }
541            }
542        }
543    }
544}
545
546impl FromRecoveredTx<FoundryTxEnvelope> for TxEnv {
547    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
548        match tx {
549            FoundryTxEnvelope::Legacy(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
550            FoundryTxEnvelope::Eip2930(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
551            FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
552            // Anvil preserves the CIP-64 envelope but executes with native EIP-1559 fees.
553            FoundryTxEnvelope::Celo(signed_tx) => {
554                Self::from_recovered_tx(&signed_tx.tx().inner, caller)
555            }
556            FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
557            FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
558            #[cfg(any(feature = "base", feature = "optimism"))]
559            FoundryTxEnvelope::Deposit(sealed_tx) => {
560                let tx = sealed_tx.inner();
561                Self {
562                    tx_type: tx.ty(),
563                    caller,
564                    gas_limit: tx.gas_limit,
565                    kind: tx.to,
566                    value: tx.value,
567                    data: tx.input.clone(),
568                    ..Default::default()
569                }
570            }
571            #[cfg(feature = "optimism")]
572            FoundryTxEnvelope::PostExec(sealed_tx) => {
573                let tx = sealed_tx.inner();
574                Self {
575                    tx_type: tx.ty(),
576                    caller,
577                    kind: tx.kind(),
578                    data: tx.input.clone(),
579                    ..Default::default()
580                }
581            }
582            #[cfg(feature = "base")]
583            FoundryTxEnvelope::Eip8130(_) => {
584                unreachable!("EIP-8130 transaction in Ethereum context")
585            }
586            FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Ethereum context"),
587        }
588    }
589}
590
591impl FromTxWithEncoded<FoundryTxEnvelope> for TxEnv {
592    fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
593        Self::from_recovered_tx(tx, sender)
594    }
595}
596
597impl FromRecoveredTx<FoundryTxEnvelope> for TempoTxEnv {
598    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
599        match tx {
600            FoundryTxEnvelope::Legacy(signed_tx) => {
601                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
602            }
603            FoundryTxEnvelope::Eip2930(signed_tx) => {
604                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
605            }
606            FoundryTxEnvelope::Eip1559(signed_tx) => {
607                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
608            }
609            FoundryTxEnvelope::Eip4844(signed_tx) => {
610                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
611            }
612            FoundryTxEnvelope::Eip7702(signed_tx) => {
613                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
614            }
615            #[cfg(any(feature = "base", feature = "optimism"))]
616            FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"),
617            #[cfg(feature = "optimism")]
618            FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"),
619            #[cfg(feature = "base")]
620            FoundryTxEnvelope::Eip8130(_) => {
621                unreachable!("EIP-8130 transaction in Tempo context")
622            }
623            FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller),
624            FoundryTxEnvelope::Celo(_) => unreachable!("CIP-64 transaction in Tempo context"),
625        }
626    }
627}
628
629impl FromTxWithEncoded<FoundryTxEnvelope> for TempoTxEnv {
630    fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
631        Self::from_recovered_tx(tx, sender)
632    }
633}
634
635impl std::fmt::Display for FoundryTxType {
636    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
637        match self {
638            Self::Legacy => write!(f, "legacy"),
639            Self::Eip2930 => write!(f, "eip2930"),
640            Self::Eip1559 => write!(f, "eip1559"),
641            Self::Eip4844 => write!(f, "eip4844"),
642            Self::Eip7702 => write!(f, "eip7702"),
643            #[cfg(any(feature = "base", feature = "optimism"))]
644            Self::Deposit => write!(f, "deposit"),
645            #[cfg(feature = "optimism")]
646            Self::PostExec => write!(f, "post-exec"),
647            #[cfg(feature = "base")]
648            Self::Eip8130 => write!(f, "eip8130"),
649            Self::Tempo => write!(f, "tempo"),
650            Self::Celo => write!(f, "celo"),
651        }
652    }
653}
654
655impl From<TxType> for FoundryTxType {
656    fn from(tx: TxType) -> Self {
657        match tx {
658            TxType::Legacy => Self::Legacy,
659            TxType::Eip2930 => Self::Eip2930,
660            TxType::Eip1559 => Self::Eip1559,
661            TxType::Eip4844 => Self::Eip4844,
662            TxType::Eip7702 => Self::Eip7702,
663        }
664    }
665}
666
667impl From<FoundryTxEnvelope> for FoundryTypedTx {
668    fn from(envelope: FoundryTxEnvelope) -> Self {
669        match envelope {
670            FoundryTxEnvelope::Legacy(signed_tx) => Self::Legacy(signed_tx.strip_signature()),
671            FoundryTxEnvelope::Eip2930(signed_tx) => Self::Eip2930(signed_tx.strip_signature()),
672            FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()),
673            FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()),
674            FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()),
675            #[cfg(any(feature = "base", feature = "optimism"))]
676            FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.unseal()),
677            #[cfg(feature = "optimism")]
678            FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.unseal()),
679            #[cfg(feature = "base")]
680            FoundryTxEnvelope::Eip8130(signed_tx) => Self::Eip8130(signed_tx.into_tx()),
681            FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()),
682            FoundryTxEnvelope::Celo(signed_tx) => Self::Celo(signed_tx.strip_signature()),
683        }
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690    use alloy_primitives::{TxKind, U256, address, b256, hex};
691    use alloy_rlp::Decodable;
692    use std::str::FromStr;
693
694    fn signed<T>(tx: T) -> Signed<T> {
695        Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO)
696    }
697
698    /// A plain Ethereum transaction in its JSON-RPC form.
699    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"}"#;
700
701    /// An OP-stack post-exec transaction (SDM, type `0x7D`) as returned by
702    /// `eth_getTransactionByHash`. The RLP-encoded `PostExecPayload` is carried in `input`; the
703    /// remaining fields are derived placeholders.
704    #[cfg(feature = "optimism")]
705    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"}"#;
706
707    /// An `ArbitrumInternalTx`, a type alloy models only as [`AnyTxEnvelope::Unknown`].
708    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"}"#;
709
710    #[test]
711    fn encode_rpc_2718_matches_consensus_encoding() {
712        let tx: AnyRpcTransaction = serde_json::from_str(ETH_RPC_TX).unwrap();
713        let expected = hex!(
714            "f871158504a817c80082c35094f02c1c8e6114b1dbe8937a39260b5b0a374432bb870f3dbb761620008668656c6c6f2125a01b5e176d927f8e9ab405058b2d2457392da3e20f328b16ddabcebc33eaac5feaa04ba69724e8f69de52f0125ad8b3c5c2cef33019bac3249e2c0a2192766d1721c"
715        );
716
717        assert_eq!(FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap(), expected[..]);
718    }
719
720    #[test]
721    fn encode_rpc_2718_rejects_unmodeled_type() {
722        let tx: AnyRpcTransaction = serde_json::from_str(ARBITRUM_INTERNAL_RPC_TX).unwrap();
723
724        // `AnyTxEnvelope::encode_2718` panics on this type, so it must not be reached.
725        assert!(FoundryTxEnvelope::encode_rpc_2718(&tx).is_err());
726    }
727
728    #[cfg(feature = "optimism")]
729    #[test]
730    fn encode_rpc_2718_post_exec_tx() {
731        let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
732
733        let encoded = FoundryTxEnvelope::encode_rpc_2718(&tx).unwrap();
734
735        // The 2718 envelope is the `0x7d` type byte followed by the payload carried in `input`.
736        let expected = hex!(
737            "7df83d018344ee2df7c4028207d0c4038207d0c4048207d0c4058207d0c4068207d0c4078207d0c4088207d0c4098207d0c40a8207d0c40b8207d0c40c8207d0"
738        );
739        assert_eq!(encoded, expected[..]);
740    }
741
742    /// The RPC form carries the payload as RLP in `input`, not as a `PostExecPayload` object, and
743    /// the recomputed hash matches the one the node reported.
744    #[cfg(feature = "optimism")]
745    #[test]
746    fn post_exec_rpc_tx_decodes() {
747        let tx: AnyRpcTransaction = serde_json::from_str(OP_POST_EXEC_RPC_TX).unwrap();
748
749        let envelope = FoundryTxEnvelope::try_from(tx).unwrap();
750        let FoundryTxEnvelope::PostExec(sealed) = &envelope else {
751            panic!("expected a post-exec envelope, got {envelope:?}");
752        };
753
754        assert_eq!(
755            sealed.hash(),
756            b256!("0x748fc6eb383fc0f2a92089556f639d4bdb1d363cb50e1be8acae2df338ba6963")
757        );
758        // 11 rebated transactions, at indexes 2..=12, each refunded 2000 gas.
759        let payload = &sealed.inner().payload;
760        assert_eq!(payload.block_number, 0x44ee2d);
761        assert_eq!(payload.gas_refund_entries.len(), 11);
762    }
763
764    #[test]
765    fn tx_type_predicates() {
766        assert!(FoundryTxType::Legacy.is_legacy());
767        assert!(FoundryTxType::Eip2930.is_eip2930());
768        assert!(FoundryTxType::Eip1559.is_eip1559());
769        assert!(FoundryTxType::Eip4844.is_eip4844());
770        assert!(FoundryTxType::Eip7702.is_eip7702());
771        assert!(FoundryTxType::Tempo.is_tempo());
772        assert!(!FoundryTxType::Tempo.is_legacy());
773
774        #[cfg(any(feature = "base", feature = "optimism"))]
775        assert!(FoundryTxType::Deposit.is_deposit());
776        #[cfg(feature = "base")]
777        assert!(FoundryTxType::Eip8130.is_eip8130());
778        #[cfg(feature = "optimism")]
779        {
780            assert!(FoundryTxType::PostExec.is_post_exec());
781            assert!(!FoundryTxType::Deposit.is_post_exec());
782        }
783    }
784
785    #[test]
786    fn typed_tx_predicates() {
787        assert!(FoundryTypedTx::Legacy(TxLegacy::default()).is_legacy());
788        assert!(FoundryTypedTx::Eip2930(TxEip2930::default()).is_eip2930());
789        assert!(FoundryTypedTx::Eip1559(TxEip1559::default()).is_eip1559());
790        assert!(
791            FoundryTypedTx::Eip4844(TxEip4844Variant::TxEip4844(Default::default())).is_eip4844()
792        );
793        assert!(FoundryTypedTx::Eip7702(TxEip7702::default()).is_eip7702());
794        assert!(FoundryTypedTx::Tempo(TempoTransaction::default()).is_tempo());
795
796        #[cfg(any(feature = "base", feature = "optimism"))]
797        assert!(FoundryTypedTx::Deposit(TxDeposit::default()).is_deposit());
798        #[cfg(feature = "base")]
799        assert!(FoundryTypedTx::Eip8130(TxEip8130::default()).is_eip8130());
800        #[cfg(feature = "optimism")]
801        {
802            assert!(FoundryTypedTx::PostExec(TxPostExec::default()).is_post_exec());
803        }
804    }
805
806    #[test]
807    fn tx_envelope_predicates() {
808        assert!(FoundryTxEnvelope::Legacy(signed(TxLegacy::default())).is_legacy());
809        assert!(FoundryTxEnvelope::Eip2930(signed(TxEip2930::default())).is_eip2930());
810        assert!(FoundryTxEnvelope::Eip1559(signed(TxEip1559::default())).is_eip1559());
811        assert!(
812            FoundryTxEnvelope::Eip4844(signed(TxEip4844Variant::TxEip4844(Default::default())))
813                .is_eip4844()
814        );
815        assert!(FoundryTxEnvelope::Eip7702(signed(TxEip7702::default())).is_eip7702());
816
817        #[cfg(any(feature = "base", feature = "optimism"))]
818        assert!(FoundryTxEnvelope::Deposit(Sealed::new(TxDeposit::default())).is_deposit());
819        #[cfg(feature = "base")]
820        assert!(
821            FoundryTxEnvelope::Eip8130(Eip8130Signed::new(
822                TxEip8130::default(),
823                Default::default(),
824                Default::default()
825            ))
826            .is_eip8130()
827        );
828        #[cfg(feature = "optimism")]
829        {
830            assert!(FoundryTxEnvelope::PostExec(Sealed::new(TxPostExec::default())).is_post_exec());
831        }
832    }
833
834    #[test]
835    fn impersonated_tx_uses_nonzero_dummy_signature() {
836        let FoundryTxEnvelope::Legacy(tx) =
837            FoundryTypedTx::Legacy(TxLegacy::default()).into_impersonated()
838        else {
839            panic!("expected legacy transaction");
840        };
841
842        assert_eq!(tx.signature().r(), U256::ONE);
843        assert_eq!(tx.signature().s(), U256::ONE);
844        assert!(!tx.signature().v());
845    }
846
847    #[test]
848    fn test_decode_call() {
849        let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
850        let decoded = FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap();
851
852        let tx = TxLegacy {
853            nonce: 2u64,
854            gas_price: 1000000000u128,
855            gas_limit: 100000,
856            to: TxKind::Call(Address::from_slice(
857                &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..],
858            )),
859            value: U256::from(1000000000000000u64),
860            input: Bytes::default(),
861            chain_id: Some(4),
862        };
863
864        let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap();
865
866        let tx = FoundryTxEnvelope::Legacy(Signed::new_unchecked(
867            tx,
868            signature,
869            b256!("0xa517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"),
870        ));
871
872        assert_eq!(tx, decoded);
873    }
874
875    #[test]
876    fn test_decode_create_goerli() {
877        // test that an example create tx from goerli decodes properly
878        let tx_bytes =
879              hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471")
880                  .unwrap();
881        let _decoded = FoundryTxEnvelope::decode(&mut &tx_bytes[..]).unwrap();
882    }
883
884    #[test]
885    fn can_recover_sender() {
886        // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f
887        let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap();
888
889        let Ok(FoundryTxEnvelope::Eip1559(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
890            panic!("decoding FoundryTxEnvelope failed");
891        };
892
893        assert_eq!(
894            tx.hash(),
895            &b256!("0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f")
896        );
897        assert_eq!(
898            tx.recover_signer().unwrap(),
899            address!("0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5")
900        );
901    }
902
903    // Test vector from https://sepolia.etherscan.io/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
904    // Blobscan: https://sepolia.blobscan.com/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
905    #[test]
906    fn test_decode_live_4844_tx() {
907        use alloy_primitives::{address, b256};
908
909        // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
910        let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap();
911        let res = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
912        assert!(res.is_type(3));
913
914        let tx = match res {
915            FoundryTxEnvelope::Eip4844(tx) => tx,
916            _ => unreachable!(),
917        };
918
919        assert_eq!(tx.tx().tx().to, address!("0x11E9CA82A3a762b4B5bd264d4173a242e7a77064"));
920
921        assert_eq!(
922            tx.tx().tx().blob_versioned_hashes,
923            vec![
924                b256!("0x012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"),
925                b256!("0x0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"),
926                b256!("0x013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"),
927                b256!("0x01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"),
928                b256!("0x011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549")
929            ]
930        );
931
932        let from = tx.recover_signer().unwrap();
933        assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
934    }
935
936    #[test]
937    fn can_recover_sender_not_normalized() {
938        let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
939
940        let Ok(FoundryTxEnvelope::Legacy(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
941            panic!("decoding FoundryTxEnvelope failed");
942        };
943
944        assert_eq!(tx.tx().input, Bytes::from(b""));
945        assert_eq!(tx.tx().gas_price, 1);
946        assert_eq!(tx.tx().gas_limit, 21000);
947        assert_eq!(tx.tx().nonce, 0);
948        if let TxKind::Call(to) = tx.tx().to {
949            assert_eq!(to, address!("0x095e7baea6a6c7c4c2dfeb977efac326af552d87"));
950        } else {
951            panic!("expected a call transaction");
952        }
953        assert_eq!(tx.tx().value, U256::from(0x0au64));
954        assert_eq!(
955            tx.recover_signer().unwrap(),
956            address!("0f65fe9276bc9a24ae7083ae28e2660ef72df99e")
957        );
958    }
959
960    #[test]
961    fn deser_to_type_tx() {
962        let tx = r#"
963        {
964            "type": "0x2",
965            "chainId": "0x7a69",
966            "nonce": "0x0",
967            "gas": "0x5209",
968            "maxFeePerGas": "0x77359401",
969            "maxPriorityFeePerGas": "0x1",
970            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
971            "value": "0x0",
972            "accessList": [],
973            "input": "0x",
974            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
975            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
976            "yParity": "0x0",
977            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
978        }"#;
979
980        let _typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
981    }
982
983    #[test]
984    fn test_from_recovered_tx_legacy() {
985        let tx = r#"
986        {
987            "type": "0x0",
988            "chainId": "0x1",
989            "nonce": "0x0",
990            "gas": "0x5208",
991            "gasPrice": "0x1",
992            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
993            "value": "0x1",
994            "input": "0x",
995            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
996            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
997            "v": "0x1b",
998            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
999        }"#;
1000
1001        let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
1002        let sender = typed_tx.recover().unwrap();
1003
1004        // Test TxEnv conversion via FromRecoveredTx trait
1005        let tx_env = TxEnv::from_recovered_tx(&typed_tx, sender);
1006        assert_eq!(tx_env.caller, sender);
1007        assert_eq!(tx_env.gas_limit, 0x5208);
1008        assert_eq!(tx_env.gas_price, 1);
1009    }
1010
1011    // Test vector from Tempo testnet:
1012    // https://explorer.testnet.tempo.xyz/tx/0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd
1013    #[test]
1014    fn test_decode_encode_tempo_tx() {
1015        use alloy_primitives::address;
1016        use tempo_primitives::TEMPO_TX_TYPE_ID;
1017
1018        let tx_hash: TxHash =
1019            b256!("0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd");
1020
1021        // Raw transaction from Tempo testnet via eth_getRawTransactionByHash
1022        let raw_tx = hex::decode(
1023            "76f9025e82a5bd808502cb4178008302d178f8fcf85c9420c000000000000000000000000000000000000080b844095ea7b3000000000000000000000000dec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000989680f89c94dec000000000000000000000000000000000000080b884f8856c0f00000000000000000000000020c000000000000000000000000000000000000000000000000000000000000020c00000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000097d330c0808080809420c000000000000000000000000000000000000180c0b90133027b98b7a8e6c68d7eac741a52e6fdae0560ce3c16ef5427ad46d7a54d0ed86dd41d000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2238453071464a7a50585167546e645473643649456659457776323173516e626966374c4741776e4b43626b222c226f726967696e223a2268747470733a2f2f74656d706f2d6465782e76657263656c2e617070222c2263726f73734f726967696e223a66616c73657dcfd45c3b19745a42f80b134dcb02a8ba099a0e4e7be1984da54734aa81d8f29f74bb9170ae6d25bd510c83fe35895ee5712efe13980a5edc8094c534e23af85eaacc80b21e45fb11f349424dce3a2f23547f60c0ff2f8bcaede2a247545ce8dd87abf0dbb7a5c9507efae2e43833356651b45ac576c2e61cec4e9c0f41fcbf6e",
1024        )
1025        .unwrap();
1026
1027        let tempo_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
1028
1029        // Verify it's a Tempo transaction (type 0x76)
1030        assert!(tempo_tx.is_type(TEMPO_TX_TYPE_ID));
1031
1032        let FoundryTxEnvelope::Tempo(ref aa_signed) = tempo_tx else {
1033            panic!("Expected Tempo transaction");
1034        };
1035
1036        // Verify the chain ID
1037        assert_eq!(aa_signed.tx().chain_id, 42429);
1038
1039        // Verify the fee token
1040        assert_eq!(
1041            aa_signed.tx().fee_token,
1042            Some(address!("0x20C0000000000000000000000000000000000001"))
1043        );
1044
1045        // Verify gas limit
1046        assert_eq!(aa_signed.tx().gas_limit, 184696);
1047
1048        // Verify we have 2 calls
1049        assert_eq!(aa_signed.tx().calls.len(), 2);
1050
1051        // Verify the hash
1052        assert_eq!(tx_hash, tempo_tx.hash());
1053
1054        // Verify round-trip encoding
1055        let mut encoded = Vec::new();
1056        tempo_tx.encode_2718(&mut encoded);
1057        assert_eq!(raw_tx, encoded);
1058
1059        // Verify sender recovery (WebAuthn signature)
1060        let sender = tempo_tx.recover().unwrap();
1061        assert_eq!(sender, address!("0x566Ff0f4a6114F8072ecDC8A7A8A13d8d0C6B45F"));
1062    }
1063}