Skip to main content

foundry_primitives/transaction/
envelope.rs

1#[cfg(feature = "optimism")]
2use alloy_consensus::{Sealed, Transaction as _};
3use alloy_consensus::{
4    SignableTransaction, Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy,
5    TxType, Typed2718,
6    crypto::RecoveryError,
7    transaction::{
8        SignerRecoverable, TxEip7702, TxHashRef,
9        eip4844::{TxEip4844Variant, TxEip4844WithSidecar},
10    },
11};
12use alloy_evm::{FromRecoveredTx, FromTxWithEncoded};
13use alloy_network::{AnyRpcTransaction, AnyTxEnvelope, TransactionResponse};
14use alloy_primitives::{Address, B256, Bytes, Signature, TxHash};
15use alloy_rpc_types::ConversionError;
16#[cfg(feature = "optimism")]
17use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, POST_EXEC_TX_TYPE_ID, TxDeposit, TxPostExec};
18use revm::context::TxEnv;
19use tempo_primitives::{AASigned, TempoSignature, TempoTransaction};
20use tempo_revm::TempoTxEnv;
21
22//
23/// Container type for signed, typed transactions.
24// NOTE(onbjerg): Boxing `Tempo(AASigned)` breaks `TransactionEnvelope` derive macro trait bounds.
25#[allow(clippy::large_enum_variant)]
26#[derive(Clone, Debug, TransactionEnvelope)]
27#[envelope(
28    tx_type_name = FoundryTxType,
29    typed = FoundryTypedTx,
30)]
31pub enum FoundryTxEnvelope {
32    /// Legacy transaction type
33    #[envelope(ty = 0)]
34    Legacy(Signed<TxLegacy>),
35    /// [EIP-2930] transaction.
36    ///
37    /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930
38    #[envelope(ty = 1)]
39    Eip2930(Signed<TxEip2930>),
40    /// [EIP-1559] transaction.
41    ///
42    /// [EIP-1559]: https://eips.ethereum.org/EIPS/eip-1559
43    #[envelope(ty = 2)]
44    Eip1559(Signed<TxEip1559>),
45    /// [EIP-4844] transaction.
46    ///
47    /// [EIP-4844]: https://eips.ethereum.org/EIPS/eip-4844
48    #[envelope(ty = 3)]
49    Eip4844(Signed<TxEip4844Variant>),
50    /// [EIP-7702] transaction.
51    ///
52    /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702
53    #[envelope(ty = 4)]
54    Eip7702(Signed<TxEip7702>),
55    /// OP stack deposit transaction.
56    ///
57    /// See <https://docs.optimism.io/op-stack/bridging/deposit-flow>.
58    #[cfg(feature = "optimism")]
59    #[envelope(ty = 126)]
60    Deposit(Sealed<TxDeposit>),
61    /// OP stack post-execution synthetic transaction.
62    #[cfg(feature = "optimism")]
63    #[envelope(ty = 0x7D)]
64    PostExec(Sealed<TxPostExec>),
65    /// Tempo transaction type.
66    ///
67    /// See <https://docs.tempo.xyz/protocol/transactions>.
68    #[envelope(ty = 0x76, typed = TempoTransaction)]
69    Tempo(AASigned),
70}
71
72impl FoundryTxEnvelope {
73    /// Returns `true` if this is a legacy transaction.
74    #[inline]
75    pub const fn is_legacy(&self) -> bool {
76        matches!(self, Self::Legacy(_))
77    }
78
79    /// Returns `true` if this is an EIP-2930 transaction.
80    #[inline]
81    pub const fn is_eip2930(&self) -> bool {
82        matches!(self, Self::Eip2930(_))
83    }
84
85    /// Returns `true` if this is an EIP-1559 transaction.
86    #[inline]
87    pub const fn is_eip1559(&self) -> bool {
88        matches!(self, Self::Eip1559(_))
89    }
90
91    /// Returns `true` if this is an EIP-4844 transaction.
92    #[inline]
93    pub const fn is_eip4844(&self) -> bool {
94        matches!(self, Self::Eip4844(_))
95    }
96
97    /// Returns `true` if this is an EIP-7702 transaction.
98    #[inline]
99    pub const fn is_eip7702(&self) -> bool {
100        matches!(self, Self::Eip7702(_))
101    }
102
103    /// Returns `true` if this is an OP stack deposit transaction.
104    #[cfg(feature = "optimism")]
105    #[inline]
106    pub const fn is_deposit(&self) -> bool {
107        matches!(self, Self::Deposit(_))
108    }
109
110    /// Returns `true` if this is an OP stack post-execution synthetic transaction.
111    #[cfg(feature = "optimism")]
112    #[inline]
113    pub const fn is_post_exec(&self) -> bool {
114        matches!(self, Self::PostExec(_))
115    }
116
117    /// Converts the transaction into an Ethereum [`TxEnvelope`].
118    ///
119    /// Returns an error if the transaction is not part of the standard Ethereum transaction types.
120    pub fn try_into_eth(self) -> Result<TxEnvelope, Self> {
121        match self {
122            Self::Legacy(tx) => Ok(TxEnvelope::Legacy(tx)),
123            Self::Eip2930(tx) => Ok(TxEnvelope::Eip2930(tx)),
124            Self::Eip1559(tx) => Ok(TxEnvelope::Eip1559(tx)),
125            Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)),
126            Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)),
127            #[cfg(feature = "optimism")]
128            Self::Deposit(_) => Err(self),
129            #[cfg(feature = "optimism")]
130            Self::PostExec(_) => Err(self),
131            Self::Tempo(_) => Err(self),
132        }
133    }
134
135    pub const fn sidecar(&self) -> Option<&TxEip4844WithSidecar> {
136        match self {
137            Self::Eip4844(signed_variant) => match signed_variant.tx() {
138                TxEip4844Variant::TxEip4844WithSidecar(with_sidecar) => Some(with_sidecar),
139                _ => None,
140            },
141            _ => None,
142        }
143    }
144
145    /// Drops pooled sidecars so the transaction uses its canonical block-body representation.
146    pub fn into_canonical(self) -> Self {
147        match self {
148            Self::Eip4844(tx) => Self::Eip4844(tx.map(TxEip4844Variant::drop_sidecar)),
149            tx => tx,
150        }
151    }
152
153    /// Returns the hash of the transaction.
154    ///
155    /// # Note
156    ///
157    /// If this transaction has the Impersonated signature then this returns a modified unique
158    /// hash. This allows us to treat impersonated transactions as unique.
159    pub fn hash(&self) -> B256 {
160        *self.tx_hash()
161    }
162
163    /// Returns `true` if this is a Tempo transaction.
164    pub const fn is_tempo(&self) -> bool {
165        matches!(self, Self::Tempo(_))
166    }
167
168    /// Returns `true` if this is a Tempo transaction with a nonzero nonce key.
169    pub fn has_nonzero_tempo_nonce_key(&self) -> bool {
170        matches!(self, Self::Tempo(tx) if !tx.tx().nonce_key.is_zero())
171    }
172
173    /// Recovers the Ethereum address which was used to sign the transaction.
174    pub fn recover(&self) -> Result<Address, RecoveryError> {
175        Ok(match self {
176            Self::Legacy(tx) => tx.recover_signer()?,
177            Self::Eip2930(tx) => tx.recover_signer()?,
178            Self::Eip1559(tx) => tx.recover_signer()?,
179            Self::Eip4844(tx) => tx.recover_signer()?,
180            Self::Eip7702(tx) => tx.recover_signer()?,
181            #[cfg(feature = "optimism")]
182            Self::Deposit(tx) => tx.from,
183            #[cfg(feature = "optimism")]
184            Self::PostExec(tx) => tx.inner().signer_address(),
185            Self::Tempo(tx) => tx.signature().recover_signer(&tx.signature_hash())?,
186        })
187    }
188}
189
190impl FoundryTxType {
191    /// Returns `true` if this is an OP stack deposit transaction type.
192    #[cfg(feature = "optimism")]
193    pub const fn is_deposit(&self) -> bool {
194        matches!(self, Self::Deposit)
195    }
196
197    /// Returns `true` if this is an OP stack post-execution synthetic transaction type.
198    #[cfg(feature = "optimism")]
199    pub const fn is_post_exec(&self) -> bool {
200        matches!(self, Self::PostExec)
201    }
202
203    /// Returns `true` if this is a Tempo transaction type.
204    pub const fn is_tempo(&self) -> bool {
205        matches!(self, Self::Tempo)
206    }
207}
208
209impl FoundryTypedTx {
210    /// Builds an envelope with a dummy signature for an impersonated account.
211    ///
212    /// The signature uses `r = 1` and `s = 1` because clients reject zero scalar values.
213    pub fn into_impersonated(self) -> FoundryTxEnvelope {
214        let signature = Signature::from_scalars_and_parity(
215            B256::with_last_byte(1),
216            B256::with_last_byte(1),
217            false,
218        );
219        match self {
220            Self::Legacy(tx) => FoundryTxEnvelope::Legacy(tx.into_signed(signature)),
221            Self::Eip2930(tx) => FoundryTxEnvelope::Eip2930(tx.into_signed(signature)),
222            Self::Eip1559(tx) => FoundryTxEnvelope::Eip1559(tx.into_signed(signature)),
223            Self::Eip7702(tx) => FoundryTxEnvelope::Eip7702(tx.into_signed(signature)),
224            Self::Eip4844(tx) => FoundryTxEnvelope::Eip4844(tx.into_signed(signature)),
225            #[cfg(feature = "optimism")]
226            Self::Deposit(tx) => FoundryTxEnvelope::Deposit(Sealed::new(tx)),
227            #[cfg(feature = "optimism")]
228            Self::PostExec(_) => {
229                unreachable!("op post-exec txs should not be impersonated")
230            }
231            Self::Tempo(tx) => {
232                let tempo_sig: TempoSignature = signature.into();
233                FoundryTxEnvelope::Tempo(tx.into_signed(tempo_sig))
234            }
235        }
236    }
237
238    /// Returns `true` if this is an OP stack deposit transaction.
239    #[cfg(feature = "optimism")]
240    pub const fn is_deposit(&self) -> bool {
241        matches!(self, Self::Deposit(_))
242    }
243
244    /// Returns `true` if this is an OP stack post-execution synthetic transaction.
245    #[cfg(feature = "optimism")]
246    pub const fn is_post_exec(&self) -> bool {
247        matches!(self, Self::PostExec(_))
248    }
249
250    /// Returns `true` if this is a Tempo transaction.
251    pub const fn is_tempo(&self) -> bool {
252        matches!(self, Self::Tempo(_))
253    }
254}
255
256impl TxHashRef for FoundryTxEnvelope {
257    fn tx_hash(&self) -> &TxHash {
258        match self {
259            Self::Legacy(t) => t.hash(),
260            Self::Eip2930(t) => t.hash(),
261            Self::Eip1559(t) => t.hash(),
262            Self::Eip4844(t) => t.hash(),
263            Self::Eip7702(t) => t.hash(),
264            #[cfg(feature = "optimism")]
265            Self::Deposit(t) => t.hash_ref(),
266            #[cfg(feature = "optimism")]
267            Self::PostExec(t) => t.hash_ref(),
268            Self::Tempo(t) => t.hash(),
269        }
270    }
271}
272
273impl SignerRecoverable for FoundryTxEnvelope {
274    fn recover_signer(&self) -> Result<Address, RecoveryError> {
275        self.recover()
276    }
277
278    fn recover_signer_unchecked(&self) -> Result<Address, RecoveryError> {
279        self.recover()
280    }
281}
282
283impl TryFrom<FoundryTxEnvelope> for TxEnvelope {
284    type Error = FoundryTxEnvelope;
285
286    fn try_from(envelope: FoundryTxEnvelope) -> Result<Self, Self::Error> {
287        envelope.try_into_eth()
288    }
289}
290
291impl From<TxEnvelope> for FoundryTxEnvelope {
292    fn from(tx: TxEnvelope) -> Self {
293        match tx {
294            TxEnvelope::Legacy(tx) => Self::Legacy(tx),
295            TxEnvelope::Eip2930(tx) => Self::Eip2930(tx),
296            TxEnvelope::Eip1559(tx) => Self::Eip1559(tx),
297            TxEnvelope::Eip4844(tx) => Self::Eip4844(tx),
298            TxEnvelope::Eip7702(tx) => Self::Eip7702(tx),
299        }
300    }
301}
302
303impl From<tempo_primitives::TempoTxEnvelope> for FoundryTxEnvelope {
304    fn from(tx: tempo_primitives::TempoTxEnvelope) -> Self {
305        match tx {
306            tempo_primitives::TempoTxEnvelope::Legacy(tx) => Self::Legacy(tx),
307            tempo_primitives::TempoTxEnvelope::Eip2930(tx) => Self::Eip2930(tx),
308            tempo_primitives::TempoTxEnvelope::Eip1559(tx) => Self::Eip1559(tx),
309            tempo_primitives::TempoTxEnvelope::Eip7702(tx) => Self::Eip7702(tx),
310            tempo_primitives::TempoTxEnvelope::AA(tx) => Self::Tempo(tx),
311        }
312    }
313}
314
315impl TryFrom<AnyRpcTransaction> for FoundryTxEnvelope {
316    type Error = ConversionError;
317
318    fn try_from(value: AnyRpcTransaction) -> Result<Self, Self::Error> {
319        let transaction = value.into_inner();
320        let from = transaction.from();
321        match transaction.into_inner() {
322            AnyTxEnvelope::Ethereum(tx) => match tx {
323                TxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
324                TxEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
325                TxEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
326                TxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
327                TxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
328            },
329            AnyTxEnvelope::Unknown(tx) => {
330                #[cfg(feature = "optimism")]
331                {
332                    let mut tx = tx;
333                    let _ = from;
334                    // Try to convert to deposit transaction
335                    if tx.ty() == DEPOSIT_TX_TYPE_ID {
336                        tx.inner
337                            .fields
338                            .insert("from".to_string(), serde_json::to_value(from).unwrap());
339                        let deposit_tx =
340                            tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|e| {
341                                ConversionError::Custom(format!(
342                                    "Failed to deserialize deposit tx: {e}"
343                                ))
344                            })?;
345
346                        return Ok(Self::Deposit(Sealed::new(deposit_tx)));
347                    }
348
349                    if tx.ty() == POST_EXEC_TX_TYPE_ID {
350                        let post_exec_tx =
351                            tx.inner.fields.deserialize_into::<TxPostExec>().map_err(|e| {
352                                ConversionError::Custom(format!(
353                                    "Failed to deserialize post-exec tx: {e}"
354                                ))
355                            })?;
356
357                        return Ok(Self::PostExec(Sealed::new(post_exec_tx)));
358                    }
359
360                    let tx_type = tx.ty();
361                    Err(ConversionError::Custom(format!(
362                        "Unknown transaction type: 0x{tx_type:02X}"
363                    )))
364                }
365                #[cfg(not(feature = "optimism"))]
366                {
367                    let _ = from;
368                    let tx_type = tx.ty();
369                    Err(ConversionError::Custom(format!(
370                        "Unknown transaction type: 0x{tx_type:02X}"
371                    )))
372                }
373            }
374        }
375    }
376}
377
378impl FromRecoveredTx<FoundryTxEnvelope> for TxEnv {
379    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
380        match tx {
381            FoundryTxEnvelope::Legacy(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
382            FoundryTxEnvelope::Eip2930(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
383            FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
384            FoundryTxEnvelope::Eip4844(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
385            FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
386            #[cfg(feature = "optimism")]
387            FoundryTxEnvelope::Deposit(sealed_tx) => {
388                let tx = sealed_tx.inner();
389                Self {
390                    tx_type: tx.ty(),
391                    caller,
392                    gas_limit: tx.gas_limit,
393                    kind: tx.to,
394                    value: tx.value,
395                    data: tx.input.clone(),
396                    ..Default::default()
397                }
398            }
399            #[cfg(feature = "optimism")]
400            FoundryTxEnvelope::PostExec(sealed_tx) => {
401                let tx = sealed_tx.inner();
402                Self {
403                    tx_type: tx.ty(),
404                    caller,
405                    kind: tx.kind(),
406                    data: tx.input.clone(),
407                    ..Default::default()
408                }
409            }
410            FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Ethereum context"),
411        }
412    }
413}
414
415impl FromTxWithEncoded<FoundryTxEnvelope> for TxEnv {
416    fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
417        Self::from_recovered_tx(tx, sender)
418    }
419}
420
421impl FromRecoveredTx<FoundryTxEnvelope> for TempoTxEnv {
422    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
423        match tx {
424            FoundryTxEnvelope::Legacy(signed_tx) => {
425                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
426            }
427            FoundryTxEnvelope::Eip2930(signed_tx) => {
428                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
429            }
430            FoundryTxEnvelope::Eip1559(signed_tx) => {
431                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
432            }
433            FoundryTxEnvelope::Eip4844(signed_tx) => {
434                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
435            }
436            FoundryTxEnvelope::Eip7702(signed_tx) => {
437                Self::from(TxEnv::from_recovered_tx(signed_tx, caller))
438            }
439            #[cfg(feature = "optimism")]
440            FoundryTxEnvelope::Deposit(_) => unreachable!("Deposit tx in Tempo context"),
441            #[cfg(feature = "optimism")]
442            FoundryTxEnvelope::PostExec(_) => unreachable!("Post-exec tx in Tempo context"),
443            FoundryTxEnvelope::Tempo(aa_signed) => Self::from_recovered_tx(aa_signed, caller),
444        }
445    }
446}
447
448impl FromTxWithEncoded<FoundryTxEnvelope> for TempoTxEnv {
449    fn from_encoded_tx(tx: &FoundryTxEnvelope, sender: Address, _encoded: Bytes) -> Self {
450        Self::from_recovered_tx(tx, sender)
451    }
452}
453
454impl std::fmt::Display for FoundryTxType {
455    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
456        match self {
457            Self::Legacy => write!(f, "legacy"),
458            Self::Eip2930 => write!(f, "eip2930"),
459            Self::Eip1559 => write!(f, "eip1559"),
460            Self::Eip4844 => write!(f, "eip4844"),
461            Self::Eip7702 => write!(f, "eip7702"),
462            #[cfg(feature = "optimism")]
463            Self::Deposit => write!(f, "deposit"),
464            #[cfg(feature = "optimism")]
465            Self::PostExec => write!(f, "post-exec"),
466            Self::Tempo => write!(f, "tempo"),
467        }
468    }
469}
470
471impl From<TxType> for FoundryTxType {
472    fn from(tx: TxType) -> Self {
473        match tx {
474            TxType::Legacy => Self::Legacy,
475            TxType::Eip2930 => Self::Eip2930,
476            TxType::Eip1559 => Self::Eip1559,
477            TxType::Eip4844 => Self::Eip4844,
478            TxType::Eip7702 => Self::Eip7702,
479        }
480    }
481}
482
483impl From<FoundryTxEnvelope> for FoundryTypedTx {
484    fn from(envelope: FoundryTxEnvelope) -> Self {
485        match envelope {
486            FoundryTxEnvelope::Legacy(signed_tx) => Self::Legacy(signed_tx.strip_signature()),
487            FoundryTxEnvelope::Eip2930(signed_tx) => Self::Eip2930(signed_tx.strip_signature()),
488            FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()),
489            FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()),
490            FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()),
491            #[cfg(feature = "optimism")]
492            FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.into_inner()),
493            #[cfg(feature = "optimism")]
494            FoundryTxEnvelope::PostExec(sealed_tx) => Self::PostExec(sealed_tx.into_inner()),
495            FoundryTxEnvelope::Tempo(signed_tx) => Self::Tempo(signed_tx.strip_signature()),
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use std::str::FromStr;
503
504    use alloy_primitives::{TxKind, U256, b256, hex};
505    use alloy_rlp::Decodable;
506
507    use super::*;
508
509    fn signed<T>(tx: T) -> Signed<T> {
510        Signed::new_unchecked(tx, Signature::test_signature(), B256::ZERO)
511    }
512
513    #[test]
514    fn tx_type_predicates() {
515        assert!(FoundryTxType::Legacy.is_legacy());
516        assert!(FoundryTxType::Eip2930.is_eip2930());
517        assert!(FoundryTxType::Eip1559.is_eip1559());
518        assert!(FoundryTxType::Eip4844.is_eip4844());
519        assert!(FoundryTxType::Eip7702.is_eip7702());
520        assert!(FoundryTxType::Tempo.is_tempo());
521        assert!(!FoundryTxType::Tempo.is_legacy());
522
523        #[cfg(feature = "optimism")]
524        {
525            assert!(FoundryTxType::Deposit.is_deposit());
526            assert!(FoundryTxType::PostExec.is_post_exec());
527            assert!(!FoundryTxType::Deposit.is_post_exec());
528        }
529    }
530
531    #[test]
532    fn typed_tx_predicates() {
533        assert!(FoundryTypedTx::Legacy(TxLegacy::default()).is_legacy());
534        assert!(FoundryTypedTx::Eip2930(TxEip2930::default()).is_eip2930());
535        assert!(FoundryTypedTx::Eip1559(TxEip1559::default()).is_eip1559());
536        assert!(
537            FoundryTypedTx::Eip4844(TxEip4844Variant::TxEip4844(Default::default())).is_eip4844()
538        );
539        assert!(FoundryTypedTx::Eip7702(TxEip7702::default()).is_eip7702());
540        assert!(FoundryTypedTx::Tempo(TempoTransaction::default()).is_tempo());
541
542        #[cfg(feature = "optimism")]
543        {
544            assert!(FoundryTypedTx::Deposit(TxDeposit::default()).is_deposit());
545            assert!(FoundryTypedTx::PostExec(TxPostExec::default()).is_post_exec());
546        }
547    }
548
549    #[test]
550    fn tx_envelope_predicates() {
551        assert!(FoundryTxEnvelope::Legacy(signed(TxLegacy::default())).is_legacy());
552        assert!(FoundryTxEnvelope::Eip2930(signed(TxEip2930::default())).is_eip2930());
553        assert!(FoundryTxEnvelope::Eip1559(signed(TxEip1559::default())).is_eip1559());
554        assert!(
555            FoundryTxEnvelope::Eip4844(signed(TxEip4844Variant::TxEip4844(Default::default())))
556                .is_eip4844()
557        );
558        assert!(FoundryTxEnvelope::Eip7702(signed(TxEip7702::default())).is_eip7702());
559
560        #[cfg(feature = "optimism")]
561        {
562            assert!(FoundryTxEnvelope::Deposit(Sealed::new(TxDeposit::default())).is_deposit());
563            assert!(FoundryTxEnvelope::PostExec(Sealed::new(TxPostExec::default())).is_post_exec());
564        }
565    }
566
567    #[test]
568    fn impersonated_tx_uses_nonzero_dummy_signature() {
569        let FoundryTxEnvelope::Legacy(tx) =
570            FoundryTypedTx::Legacy(TxLegacy::default()).into_impersonated()
571        else {
572            panic!("expected legacy transaction");
573        };
574
575        assert_eq!(tx.signature().r(), U256::from(1));
576        assert_eq!(tx.signature().s(), U256::from(1));
577        assert!(!tx.signature().v());
578    }
579
580    #[test]
581    fn test_decode_call() {
582        let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
583        let decoded = FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap();
584
585        let tx = TxLegacy {
586            nonce: 2u64,
587            gas_price: 1000000000u128,
588            gas_limit: 100000,
589            to: TxKind::Call(Address::from_slice(
590                &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..],
591            )),
592            value: U256::from(1000000000000000u64),
593            input: Bytes::default(),
594            chain_id: Some(4),
595        };
596
597        let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap();
598
599        let tx = FoundryTxEnvelope::Legacy(Signed::new_unchecked(
600            tx,
601            signature,
602            b256!("0xa517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"),
603        ));
604
605        assert_eq!(tx, decoded);
606    }
607
608    #[test]
609    fn test_decode_create_goerli() {
610        // test that an example create tx from goerli decodes properly
611        let tx_bytes =
612              hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471")
613                  .unwrap();
614        let _decoded = FoundryTxEnvelope::decode(&mut &tx_bytes[..]).unwrap();
615    }
616
617    #[test]
618    fn can_recover_sender() {
619        // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f
620        let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap();
621
622        let Ok(FoundryTxEnvelope::Eip1559(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
623            panic!("decoding FoundryTxEnvelope failed");
624        };
625
626        assert_eq!(
627            tx.hash(),
628            &"0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f"
629                .parse::<B256>()
630                .unwrap()
631        );
632        assert_eq!(
633            tx.recover_signer().unwrap(),
634            "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5".parse::<Address>().unwrap()
635        );
636    }
637
638    // Test vector from https://sepolia.etherscan.io/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
639    // Blobscan: https://sepolia.blobscan.com/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
640    #[test]
641    fn test_decode_live_4844_tx() {
642        use alloy_primitives::{address, b256};
643
644        // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
645        let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap();
646        let res = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
647        assert!(res.is_type(3));
648
649        let tx = match res {
650            FoundryTxEnvelope::Eip4844(tx) => tx,
651            _ => unreachable!(),
652        };
653
654        assert_eq!(tx.tx().tx().to, address!("0x11E9CA82A3a762b4B5bd264d4173a242e7a77064"));
655
656        assert_eq!(
657            tx.tx().tx().blob_versioned_hashes,
658            vec![
659                b256!("0x012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"),
660                b256!("0x0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"),
661                b256!("0x013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"),
662                b256!("0x01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"),
663                b256!("0x011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549")
664            ]
665        );
666
667        let from = tx.recover_signer().unwrap();
668        assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
669    }
670
671    #[test]
672    fn can_recover_sender_not_normalized() {
673        let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
674
675        let Ok(FoundryTxEnvelope::Legacy(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
676            panic!("decoding FoundryTxEnvelope failed");
677        };
678
679        assert_eq!(tx.tx().input, Bytes::from(b""));
680        assert_eq!(tx.tx().gas_price, 1);
681        assert_eq!(tx.tx().gas_limit, 21000);
682        assert_eq!(tx.tx().nonce, 0);
683        if let TxKind::Call(to) = tx.tx().to {
684            assert_eq!(
685                to,
686                "0x095e7baea6a6c7c4c2dfeb977efac326af552d87".parse::<Address>().unwrap()
687            );
688        } else {
689            panic!("expected a call transaction");
690        }
691        assert_eq!(tx.tx().value, U256::from(0x0au64));
692        assert_eq!(
693            tx.recover_signer().unwrap(),
694            "0f65fe9276bc9a24ae7083ae28e2660ef72df99e".parse::<Address>().unwrap()
695        );
696    }
697
698    #[test]
699    fn deser_to_type_tx() {
700        let tx = r#"
701        {
702            "type": "0x2",
703            "chainId": "0x7a69",
704            "nonce": "0x0",
705            "gas": "0x5209",
706            "maxFeePerGas": "0x77359401",
707            "maxPriorityFeePerGas": "0x1",
708            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
709            "value": "0x0",
710            "accessList": [],
711            "input": "0x",
712            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
713            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
714            "yParity": "0x0",
715            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
716        }"#;
717
718        let _typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
719    }
720
721    #[test]
722    fn test_from_recovered_tx_legacy() {
723        let tx = r#"
724        {
725            "type": "0x0",
726            "chainId": "0x1",
727            "nonce": "0x0",
728            "gas": "0x5208",
729            "gasPrice": "0x1",
730            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
731            "value": "0x1",
732            "input": "0x",
733            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
734            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
735            "v": "0x1b",
736            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
737        }"#;
738
739        let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
740        let sender = typed_tx.recover().unwrap();
741
742        // Test TxEnv conversion via FromRecoveredTx trait
743        let tx_env = TxEnv::from_recovered_tx(&typed_tx, sender);
744        assert_eq!(tx_env.caller, sender);
745        assert_eq!(tx_env.gas_limit, 0x5208);
746        assert_eq!(tx_env.gas_price, 1);
747    }
748
749    // Test vector from Tempo testnet:
750    // https://explorer.testnet.tempo.xyz/tx/0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd
751    #[test]
752    fn test_decode_encode_tempo_tx() {
753        use alloy_primitives::address;
754        use tempo_primitives::TEMPO_TX_TYPE_ID;
755
756        let tx_hash: TxHash = "0x6d6d8c102064e6dee44abad2024a8b1d37959230baab80e70efbf9b0c739c4fd"
757            .parse::<TxHash>()
758            .unwrap();
759
760        // Raw transaction from Tempo testnet via eth_getRawTransactionByHash
761        let raw_tx = hex::decode(
762            "76f9025e82a5bd808502cb4178008302d178f8fcf85c9420c000000000000000000000000000000000000080b844095ea7b3000000000000000000000000dec00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000989680f89c94dec000000000000000000000000000000000000080b884f8856c0f00000000000000000000000020c000000000000000000000000000000000000000000000000000000000000020c00000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000989680000000000000000000000000000000000000000000000000000000000097d330c0808080809420c000000000000000000000000000000000000180c0b90133027b98b7a8e6c68d7eac741a52e6fdae0560ce3c16ef5427ad46d7a54d0ed86dd41d000000007b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a2238453071464a7a50585167546e645473643649456659457776323173516e626966374c4741776e4b43626b222c226f726967696e223a2268747470733a2f2f74656d706f2d6465782e76657263656c2e617070222c2263726f73734f726967696e223a66616c73657dcfd45c3b19745a42f80b134dcb02a8ba099a0e4e7be1984da54734aa81d8f29f74bb9170ae6d25bd510c83fe35895ee5712efe13980a5edc8094c534e23af85eaacc80b21e45fb11f349424dce3a2f23547f60c0ff2f8bcaede2a247545ce8dd87abf0dbb7a5c9507efae2e43833356651b45ac576c2e61cec4e9c0f41fcbf6e",
763        )
764        .unwrap();
765
766        let tempo_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
767
768        // Verify it's a Tempo transaction (type 0x76)
769        assert!(tempo_tx.is_type(TEMPO_TX_TYPE_ID));
770
771        let FoundryTxEnvelope::Tempo(ref aa_signed) = tempo_tx else {
772            panic!("Expected Tempo transaction");
773        };
774
775        // Verify the chain ID
776        assert_eq!(aa_signed.tx().chain_id, 42429);
777
778        // Verify the fee token
779        assert_eq!(
780            aa_signed.tx().fee_token,
781            Some(address!("0x20C0000000000000000000000000000000000001"))
782        );
783
784        // Verify gas limit
785        assert_eq!(aa_signed.tx().gas_limit, 184696);
786
787        // Verify we have 2 calls
788        assert_eq!(aa_signed.tx().calls.len(), 2);
789
790        // Verify the hash
791        assert_eq!(tx_hash, tempo_tx.hash());
792
793        // Verify round-trip encoding
794        let mut encoded = Vec::new();
795        tempo_tx.encode_2718(&mut encoded);
796        assert_eq!(raw_tx, encoded);
797
798        // Verify sender recovery (WebAuthn signature)
799        let sender = tempo_tx.recover().unwrap();
800        assert_eq!(sender, address!("0x566Ff0f4a6114F8072ecDC8A7A8A13d8d0C6B45F"));
801    }
802}