Skip to main content

foundry_primitives/transaction/
envelope.rs

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