Skip to main content

foundry_primitives/transaction/
envelope.rs

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