foundry_primitives/transaction/
envelope.rs

1use alloy_consensus::{
2    Sealed, Signed, TransactionEnvelope, TxEip1559, TxEip2930, TxEnvelope, TxLegacy, Typed2718,
3    transaction::{
4        TxEip7702,
5        eip4844::{TxEip4844Variant, TxEip4844WithSidecar},
6    },
7};
8use alloy_evm::FromRecoveredTx;
9use alloy_network::{AnyRpcTransaction, AnyTxEnvelope};
10use alloy_primitives::{Address, B256};
11use alloy_rlp::Encodable;
12use alloy_rpc_types::ConversionError;
13use alloy_serde::WithOtherFields;
14use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpTransaction as OpTransactionTrait, TxDeposit};
15use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts};
16use revm::context::TxEnv;
17
18/// Container type for signed, typed transactions.
19#[derive(Clone, Debug, TransactionEnvelope)]
20#[envelope(
21    tx_type_name = FoundryTxType,
22    typed = FoundryTypedTx,
23)]
24pub enum FoundryTxEnvelope {
25    /// Legacy transaction type
26    #[envelope(ty = 0)]
27    Legacy(Signed<TxLegacy>),
28    /// EIP-2930 transaction
29    #[envelope(ty = 1)]
30    Eip2930(Signed<TxEip2930>),
31    /// EIP-1559 transaction
32    #[envelope(ty = 2)]
33    Eip1559(Signed<TxEip1559>),
34    /// EIP-4844 transaction
35    #[envelope(ty = 3)]
36    Eip4844(Signed<TxEip4844Variant>),
37    /// EIP-7702 transaction
38    #[envelope(ty = 4)]
39    Eip7702(Signed<TxEip7702>),
40    /// op-stack deposit transaction
41    #[envelope(ty = 126)]
42    Deposit(Sealed<TxDeposit>),
43}
44
45impl FoundryTxEnvelope {
46    /// Converts the transaction into a [`TxEnvelope`].
47    ///
48    /// Returns an error if the transaction is a Deposit transaction, which is not part of the
49    /// standard Ethereum transaction types.
50    pub fn try_into_eth(self) -> Result<TxEnvelope, Self> {
51        match self {
52            Self::Legacy(tx) => Ok(TxEnvelope::Legacy(tx)),
53            Self::Eip2930(tx) => Ok(TxEnvelope::Eip2930(tx)),
54            Self::Eip1559(tx) => Ok(TxEnvelope::Eip1559(tx)),
55            Self::Eip4844(tx) => Ok(TxEnvelope::Eip4844(tx)),
56            Self::Eip7702(tx) => Ok(TxEnvelope::Eip7702(tx)),
57            Self::Deposit(_) => Err(self),
58        }
59    }
60
61    pub fn sidecar(&self) -> Option<&TxEip4844WithSidecar> {
62        match self {
63            Self::Eip4844(signed_variant) => match signed_variant.tx() {
64                TxEip4844Variant::TxEip4844WithSidecar(with_sidecar) => Some(with_sidecar),
65                _ => None,
66            },
67            _ => None,
68        }
69    }
70
71    /// Returns the hash of the transaction.
72    ///
73    /// Note: If this transaction has the Impersonated signature then this returns a modified unique
74    /// hash. This allows us to treat impersonated transactions as unique.
75    pub fn hash(&self) -> B256 {
76        match self {
77            Self::Legacy(t) => *t.hash(),
78            Self::Eip2930(t) => *t.hash(),
79            Self::Eip1559(t) => *t.hash(),
80            Self::Eip4844(t) => *t.hash(),
81            Self::Eip7702(t) => *t.hash(),
82            Self::Deposit(t) => t.tx_hash(),
83        }
84    }
85
86    /// Returns the hash if the transaction is impersonated (using a fake signature)
87    ///
88    /// This appends the `address` before hashing it
89    pub fn impersonated_hash(&self, sender: Address) -> B256 {
90        let mut buffer = Vec::new();
91        Encodable::encode(self, &mut buffer);
92        buffer.extend_from_slice(sender.as_ref());
93        B256::from_slice(alloy_primitives::utils::keccak256(&buffer).as_slice())
94    }
95
96    /// Recovers the Ethereum address which was used to sign the transaction.
97    pub fn recover(&self) -> Result<Address, alloy_primitives::SignatureError> {
98        match self {
99            Self::Legacy(tx) => tx.recover_signer(),
100            Self::Eip2930(tx) => tx.recover_signer(),
101            Self::Eip1559(tx) => tx.recover_signer(),
102            Self::Eip4844(tx) => tx.recover_signer(),
103            Self::Eip7702(tx) => tx.recover_signer(),
104            Self::Deposit(tx) => Ok(tx.from),
105        }
106    }
107}
108
109impl OpTransactionTrait for FoundryTxEnvelope {
110    fn is_deposit(&self) -> bool {
111        matches!(self, Self::Deposit(_))
112    }
113
114    fn as_deposit(&self) -> Option<&Sealed<TxDeposit>> {
115        match self {
116            Self::Deposit(tx) => Some(tx),
117            _ => None,
118        }
119    }
120}
121
122impl TryFrom<FoundryTxEnvelope> for TxEnvelope {
123    type Error = FoundryTxEnvelope;
124
125    fn try_from(envelope: FoundryTxEnvelope) -> Result<Self, Self::Error> {
126        match envelope {
127            FoundryTxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
128            FoundryTxEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
129            FoundryTxEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
130            FoundryTxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
131            FoundryTxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
132            FoundryTxEnvelope::Deposit(_) => Err(envelope),
133        }
134    }
135}
136
137impl TryFrom<AnyRpcTransaction> for FoundryTxEnvelope {
138    type Error = ConversionError;
139
140    fn try_from(value: AnyRpcTransaction) -> Result<Self, Self::Error> {
141        let WithOtherFields { inner, .. } = value.0;
142        let from = inner.inner.signer();
143        match inner.inner.into_inner() {
144            AnyTxEnvelope::Ethereum(tx) => match tx {
145                TxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
146                TxEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
147                TxEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
148                TxEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
149                TxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
150            },
151            AnyTxEnvelope::Unknown(mut tx) => {
152                // Try to convert to deposit transaction
153                if tx.ty() == DEPOSIT_TX_TYPE_ID {
154                    tx.inner.fields.insert("from".to_string(), serde_json::to_value(from).unwrap());
155                    let deposit_tx =
156                        tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|e| {
157                            ConversionError::Custom(format!(
158                                "Failed to deserialize deposit tx: {e}"
159                            ))
160                        })?;
161
162                    return Ok(Self::Deposit(Sealed::new(deposit_tx)));
163                };
164
165                Err(ConversionError::Custom("UnknownTxType".to_string()))
166            }
167        }
168    }
169}
170
171impl FromRecoveredTx<FoundryTxEnvelope> for TxEnv {
172    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
173        match tx {
174            FoundryTxEnvelope::Legacy(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
175            FoundryTxEnvelope::Eip2930(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
176            FoundryTxEnvelope::Eip1559(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
177            FoundryTxEnvelope::Eip4844(signed_tx) => {
178                Self::from_recovered_tx(signed_tx.tx().tx(), caller)
179            }
180            FoundryTxEnvelope::Eip7702(signed_tx) => Self::from_recovered_tx(signed_tx, caller),
181            FoundryTxEnvelope::Deposit(sealed_tx) => {
182                Self::from_recovered_tx(sealed_tx.inner(), caller)
183            }
184        }
185    }
186}
187
188impl FromRecoveredTx<FoundryTxEnvelope> for OpTransaction<TxEnv> {
189    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
190        let base = TxEnv::from_recovered_tx(tx, caller);
191
192        let deposit = if let FoundryTxEnvelope::Deposit(deposit_tx) = tx {
193            DepositTransactionParts {
194                source_hash: deposit_tx.source_hash,
195                mint: Some(deposit_tx.mint),
196                is_system_transaction: deposit_tx.is_system_transaction,
197            }
198        } else {
199            Default::default()
200        };
201
202        Self { base, deposit, enveloped_tx: None }
203    }
204}
205
206impl std::fmt::Display for FoundryTxType {
207    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
208        match self {
209            Self::Legacy => write!(f, "legacy"),
210            Self::Eip2930 => write!(f, "eip2930"),
211            Self::Eip1559 => write!(f, "eip1559"),
212            Self::Eip4844 => write!(f, "eip4844"),
213            Self::Eip7702 => write!(f, "eip7702"),
214            Self::Deposit => write!(f, "deposit"),
215        }
216    }
217}
218
219impl From<FoundryTxEnvelope> for FoundryTypedTx {
220    fn from(envelope: FoundryTxEnvelope) -> Self {
221        match envelope {
222            FoundryTxEnvelope::Legacy(signed_tx) => Self::Legacy(signed_tx.strip_signature()),
223            FoundryTxEnvelope::Eip2930(signed_tx) => Self::Eip2930(signed_tx.strip_signature()),
224            FoundryTxEnvelope::Eip1559(signed_tx) => Self::Eip1559(signed_tx.strip_signature()),
225            FoundryTxEnvelope::Eip4844(signed_tx) => Self::Eip4844(signed_tx.strip_signature()),
226            FoundryTxEnvelope::Eip7702(signed_tx) => Self::Eip7702(signed_tx.strip_signature()),
227            FoundryTxEnvelope::Deposit(sealed_tx) => Self::Deposit(sealed_tx.into_inner()),
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use std::str::FromStr;
235
236    use alloy_primitives::{Bytes, Signature, TxHash, TxKind, U256, b256, hex};
237    use alloy_rlp::Decodable;
238
239    use super::*;
240
241    #[test]
242    fn test_decode_call() {
243        let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
244        let decoded = FoundryTxEnvelope::decode(&mut &bytes_first[..]).unwrap();
245
246        let tx = TxLegacy {
247            nonce: 2u64,
248            gas_price: 1000000000u128,
249            gas_limit: 100000,
250            to: TxKind::Call(Address::from_slice(
251                &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..],
252            )),
253            value: U256::from(1000000000000000u64),
254            input: Bytes::default(),
255            chain_id: Some(4),
256        };
257
258        let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap();
259
260        let tx = FoundryTxEnvelope::Legacy(Signed::new_unchecked(
261            tx,
262            signature,
263            b256!("0xa517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"),
264        ));
265
266        assert_eq!(tx, decoded);
267    }
268
269    #[test]
270    fn test_decode_create_goerli() {
271        // test that an example create tx from goerli decodes properly
272        let tx_bytes =
273              hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471")
274                  .unwrap();
275        let _decoded = FoundryTxEnvelope::decode(&mut &tx_bytes[..]).unwrap();
276    }
277
278    #[test]
279    fn can_recover_sender() {
280        // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f
281        let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap();
282
283        let Ok(FoundryTxEnvelope::Eip1559(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
284            panic!("decoding FoundryTxEnvelope failed");
285        };
286
287        assert_eq!(
288            tx.hash(),
289            &"0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f"
290                .parse::<B256>()
291                .unwrap()
292        );
293        assert_eq!(
294            tx.recover_signer().unwrap(),
295            "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5".parse::<Address>().unwrap()
296        );
297    }
298
299    // Test vector from https://sepolia.etherscan.io/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
300    // Blobscan: https://sepolia.blobscan.com/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
301    #[test]
302    fn test_decode_live_4844_tx() {
303        use alloy_primitives::{address, b256};
304
305        // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
306        let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap();
307        let res = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
308        assert!(res.is_type(3));
309
310        let tx = match res {
311            FoundryTxEnvelope::Eip4844(tx) => tx,
312            _ => unreachable!(),
313        };
314
315        assert_eq!(tx.tx().tx().to, address!("0x11E9CA82A3a762b4B5bd264d4173a242e7a77064"));
316
317        assert_eq!(
318            tx.tx().tx().blob_versioned_hashes,
319            vec![
320                b256!("0x012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"),
321                b256!("0x0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"),
322                b256!("0x013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"),
323                b256!("0x01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"),
324                b256!("0x011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549")
325            ]
326        );
327
328        let from = tx.recover_signer().unwrap();
329        assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
330    }
331
332    #[test]
333    fn test_decode_encode_deposit_tx() {
334        // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
335        let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
336            .parse::<TxHash>()
337            .unwrap();
338
339        // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
340        let raw_tx = alloy_primitives::hex::decode(
341            "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
342        )
343        .unwrap();
344        let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
345
346        let mut encoded = Vec::new();
347        dep_tx.encode_2718(&mut encoded);
348
349        assert_eq!(raw_tx, encoded);
350
351        assert_eq!(tx_hash, dep_tx.hash());
352    }
353
354    #[test]
355    fn can_recover_sender_not_normalized() {
356        let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
357
358        let Ok(FoundryTxEnvelope::Legacy(tx)) = FoundryTxEnvelope::decode(&mut &bytes[..]) else {
359            panic!("decoding FoundryTxEnvelope failed");
360        };
361
362        assert_eq!(tx.tx().input, Bytes::from(b""));
363        assert_eq!(tx.tx().gas_price, 1);
364        assert_eq!(tx.tx().gas_limit, 21000);
365        assert_eq!(tx.tx().nonce, 0);
366        if let TxKind::Call(to) = tx.tx().to {
367            assert_eq!(
368                to,
369                "0x095e7baea6a6c7c4c2dfeb977efac326af552d87".parse::<Address>().unwrap()
370            );
371        } else {
372            panic!("expected a call transaction");
373        }
374        assert_eq!(tx.tx().value, U256::from(0x0au64));
375        assert_eq!(
376            tx.recover_signer().unwrap(),
377            "0f65fe9276bc9a24ae7083ae28e2660ef72df99e".parse::<Address>().unwrap()
378        );
379    }
380
381    #[test]
382    fn deser_to_type_tx() {
383        let tx = r#"
384        {
385            "type": "0x2",
386            "chainId": "0x7a69",
387            "nonce": "0x0",
388            "gas": "0x5209",
389            "maxFeePerGas": "0x77359401",
390            "maxPriorityFeePerGas": "0x1",
391            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
392            "value": "0x0",
393            "accessList": [],
394            "input": "0x",
395            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
396            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
397            "yParity": "0x0",
398            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
399        }"#;
400
401        let _typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
402    }
403
404    #[test]
405    fn test_from_recovered_tx_legacy() {
406        let tx = r#"
407        {
408            "type": "0x0",
409            "chainId": "0x1",
410            "nonce": "0x0",
411            "gas": "0x5208",
412            "gasPrice": "0x1",
413            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
414            "value": "0x1",
415            "input": "0x",
416            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
417            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
418            "v": "0x1b",
419            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
420        }"#;
421
422        let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
423        let sender = typed_tx.recover().unwrap();
424
425        // Test TxEnv conversion via FromRecoveredTx trait
426        let tx_env = TxEnv::from_recovered_tx(&typed_tx, sender);
427        assert_eq!(tx_env.caller, sender);
428        assert_eq!(tx_env.gas_limit, 0x5208);
429        assert_eq!(tx_env.gas_price, 1);
430
431        // Test OpTransaction<TxEnv> conversion via FromRecoveredTx trait
432        let op_tx = OpTransaction::<TxEnv>::from_recovered_tx(&typed_tx, sender);
433        assert_eq!(op_tx.base.caller, sender);
434        assert_eq!(op_tx.base.gas_limit, 0x5208);
435    }
436}