Skip to main content

foundry_primitives/transaction/
optimism.rs

1//! OP-stack-specific impls for [`FoundryTxEnvelope`] and [`FoundryTransactionRequest`].
2
3use alloy_consensus::{Sealed, Transaction as _, Typed2718};
4use alloy_evm::{FromRecoveredTx, FromTxWithEncoded};
5use alloy_op_evm::OpTx;
6use alloy_primitives::{Address, B256, Bytes, U256};
7use alloy_serde::OtherFields;
8use op_alloy_consensus::{
9    OpDepositReceipt, OpDepositReceiptWithBloom, OpTransaction as OpTransactionTrait, OpTxEnvelope,
10    TxDeposit, TxPostExec,
11};
12use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts};
13use revm::context::TxEnv;
14
15use super::{FoundryReceiptEnvelope, FoundryTransactionRequest, FoundryTxEnvelope};
16
17impl OpTransactionTrait for FoundryTxEnvelope {
18    fn is_deposit(&self) -> bool {
19        matches!(self, Self::Deposit(_))
20    }
21
22    fn as_deposit(&self) -> Option<&Sealed<TxDeposit>> {
23        match self {
24            Self::Deposit(tx) => Some(tx),
25            _ => None,
26        }
27    }
28
29    fn as_post_exec(&self) -> Option<&Sealed<TxPostExec>> {
30        if let Self::PostExec(tx) = self { Some(tx) } else { None }
31    }
32}
33
34impl From<OpTxEnvelope> for FoundryTxEnvelope {
35    fn from(tx: OpTxEnvelope) -> Self {
36        match tx {
37            OpTxEnvelope::Legacy(tx) => Self::Legacy(tx),
38            OpTxEnvelope::Eip2930(tx) => Self::Eip2930(tx),
39            OpTxEnvelope::Eip1559(tx) => Self::Eip1559(tx),
40            OpTxEnvelope::Eip7702(tx) => Self::Eip7702(tx),
41            OpTxEnvelope::Deposit(tx) => Self::Deposit(tx),
42            OpTxEnvelope::PostExec(tx) => Self::PostExec(tx),
43        }
44    }
45}
46
47impl FromRecoveredTx<FoundryTxEnvelope> for OpTransaction<TxEnv> {
48    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
49        match tx {
50            FoundryTxEnvelope::Legacy(signed_tx) => {
51                let base = TxEnv::from_recovered_tx(signed_tx, caller);
52                Self { base, enveloped_tx: None, deposit: Default::default() }
53            }
54            FoundryTxEnvelope::Eip2930(signed_tx) => {
55                let base = TxEnv::from_recovered_tx(signed_tx, caller);
56                Self { base, enveloped_tx: None, deposit: Default::default() }
57            }
58            FoundryTxEnvelope::Eip1559(signed_tx) => {
59                let base = TxEnv::from_recovered_tx(signed_tx, caller);
60                Self { base, enveloped_tx: None, deposit: Default::default() }
61            }
62            FoundryTxEnvelope::Eip4844(signed_tx) => {
63                let base = TxEnv::from_recovered_tx(signed_tx, caller);
64                Self { base, enveloped_tx: None, deposit: Default::default() }
65            }
66            FoundryTxEnvelope::Eip7702(signed_tx) => {
67                let base = TxEnv::from_recovered_tx(signed_tx, caller);
68                Self { base, enveloped_tx: None, deposit: Default::default() }
69            }
70            FoundryTxEnvelope::Deposit(sealed_tx) => {
71                let deposit_tx = sealed_tx.inner();
72                let base = TxEnv {
73                    tx_type: deposit_tx.ty(),
74                    caller,
75                    gas_limit: deposit_tx.gas_limit,
76                    kind: deposit_tx.to,
77                    value: deposit_tx.value,
78                    data: deposit_tx.input.clone(),
79                    ..Default::default()
80                };
81                let deposit = DepositTransactionParts {
82                    source_hash: deposit_tx.source_hash,
83                    mint: Some(deposit_tx.mint),
84                    is_system_transaction: deposit_tx.is_system_transaction,
85                };
86                Self { base, enveloped_tx: None, deposit }
87            }
88            FoundryTxEnvelope::PostExec(sealed_tx) => {
89                let tx = sealed_tx.inner();
90                let base = TxEnv {
91                    tx_type: tx.ty(),
92                    caller,
93                    kind: tx.kind(),
94                    data: tx.input.clone(),
95                    ..Default::default()
96                };
97                Self { base, enveloped_tx: None, deposit: Default::default() }
98            }
99            FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"),
100        }
101    }
102}
103
104impl FromRecoveredTx<FoundryTxEnvelope> for OpTx {
105    fn from_recovered_tx(tx: &FoundryTxEnvelope, caller: Address) -> Self {
106        Self(OpTransaction::<TxEnv>::from_recovered_tx(tx, caller))
107    }
108}
109
110impl FromTxWithEncoded<FoundryTxEnvelope> for OpTx {
111    fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self {
112        Self(OpTransaction::<TxEnv>::from_encoded_tx(tx, caller, encoded))
113    }
114}
115
116impl FromTxWithEncoded<FoundryTxEnvelope> for OpTransaction<TxEnv> {
117    fn from_encoded_tx(tx: &FoundryTxEnvelope, caller: Address, encoded: Bytes) -> Self {
118        match tx {
119            FoundryTxEnvelope::Legacy(signed_tx) => {
120                let base = TxEnv::from_recovered_tx(signed_tx, caller);
121                Self { base, enveloped_tx: Some(encoded), deposit: Default::default() }
122            }
123            FoundryTxEnvelope::Eip2930(signed_tx) => {
124                let base = TxEnv::from_recovered_tx(signed_tx, caller);
125                Self { base, enveloped_tx: Some(encoded), deposit: Default::default() }
126            }
127            FoundryTxEnvelope::Eip1559(signed_tx) => {
128                let base = TxEnv::from_recovered_tx(signed_tx, caller);
129                Self { base, enveloped_tx: Some(encoded), deposit: Default::default() }
130            }
131            FoundryTxEnvelope::Eip4844(signed_tx) => {
132                let base = TxEnv::from_recovered_tx(signed_tx, caller);
133                Self { base, enveloped_tx: Some(encoded), deposit: Default::default() }
134            }
135            FoundryTxEnvelope::Eip7702(signed_tx) => {
136                let base = TxEnv::from_recovered_tx(signed_tx, caller);
137                Self { base, enveloped_tx: Some(encoded), deposit: Default::default() }
138            }
139            FoundryTxEnvelope::Deposit(sealed_tx) => {
140                let deposit_tx = sealed_tx.inner();
141                let base = TxEnv {
142                    tx_type: deposit_tx.ty(),
143                    caller,
144                    gas_limit: deposit_tx.gas_limit,
145                    kind: deposit_tx.to,
146                    value: deposit_tx.value,
147                    data: deposit_tx.input.clone(),
148                    ..Default::default()
149                };
150                let deposit = DepositTransactionParts {
151                    source_hash: deposit_tx.source_hash,
152                    mint: Some(deposit_tx.mint),
153                    is_system_transaction: deposit_tx.is_system_transaction,
154                };
155                Self { base, enveloped_tx: Some(encoded), deposit }
156            }
157            FoundryTxEnvelope::PostExec(sealed_tx) => {
158                let tx = sealed_tx.inner();
159                let base = TxEnv {
160                    tx_type: tx.ty(),
161                    caller,
162                    kind: tx.kind(),
163                    data: tx.input.clone(),
164                    ..Default::default()
165                };
166                Self { base, enveloped_tx: Some(encoded), deposit: Default::default() }
167            }
168            FoundryTxEnvelope::Tempo(_) => unreachable!("Tempo tx in Optimism context"),
169        }
170    }
171}
172
173impl From<op_alloy_rpc_types::Transaction<FoundryTxEnvelope>> for FoundryTransactionRequest {
174    fn from(tx: op_alloy_rpc_types::Transaction<FoundryTxEnvelope>) -> Self {
175        tx.inner.into_inner().into()
176    }
177}
178
179/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields.
180pub fn get_deposit_tx_parts(
181    other: &OtherFields,
182) -> Result<DepositTransactionParts, Vec<&'static str>> {
183    let mut missing = Vec::new();
184    let source_hash =
185        other.get_deserialized::<B256>("sourceHash").transpose().ok().flatten().unwrap_or_else(
186            || {
187                missing.push("sourceHash");
188                Default::default()
189            },
190        );
191    let mint = other
192        .get_deserialized::<U256>("mint")
193        .transpose()
194        .unwrap_or_else(|_| {
195            missing.push("mint");
196            Default::default()
197        })
198        .map(|value| value.saturating_to::<u128>());
199    let is_system_transaction =
200        other.get_deserialized::<bool>("isSystemTx").transpose().ok().flatten().unwrap_or_else(
201            || {
202                missing.push("isSystemTx");
203                Default::default()
204            },
205        );
206    if missing.is_empty() {
207        Ok(DepositTransactionParts { source_hash, mint, is_system_transaction })
208    } else {
209        Err(missing)
210    }
211}
212
213/// OP-stack-specific accessors on [`FoundryReceiptEnvelope`].
214impl<T> FoundryReceiptEnvelope<T> {
215    /// Return the receipt's deposit_nonce if it is a deposit receipt.
216    pub fn deposit_nonce(&self) -> Option<u64> {
217        self.as_deposit_receipt().and_then(|r| r.deposit_nonce)
218    }
219
220    /// Return the receipt's deposit version if it is a deposit receipt.
221    pub fn deposit_receipt_version(&self) -> Option<u64> {
222        self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version)
223    }
224
225    /// Returns the deposit receipt if it is a deposit receipt.
226    pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom<T>> {
227        match self {
228            Self::Deposit(t) => Some(t),
229            _ => None,
230        }
231    }
232
233    /// Returns the deposit receipt if it is a deposit receipt.
234    pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt<T>> {
235        match self {
236            Self::Deposit(t) => Some(&t.receipt),
237            _ => None,
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use alloy_network::eip2718::Encodable2718;
245    use alloy_primitives::TxHash;
246    use alloy_rlp::Decodable;
247
248    use super::*;
249
250    #[test]
251    fn test_from_recovered_tx_legacy_op() {
252        use alloy_consensus::transaction::SignerRecoverable;
253
254        let tx = r#"
255        {
256            "type": "0x0",
257            "chainId": "0x1",
258            "nonce": "0x0",
259            "gas": "0x5208",
260            "gasPrice": "0x1",
261            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
262            "value": "0x1",
263            "input": "0x",
264            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
265            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
266            "v": "0x1b",
267            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
268        }"#;
269
270        let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
271        let sender = typed_tx.recover_signer().unwrap();
272
273        // Test OpTransaction<TxEnv> conversion via FromRecoveredTx trait
274        let op_tx = OpTransaction::<TxEnv>::from_recovered_tx(&typed_tx, sender);
275        assert_eq!(op_tx.base.caller, sender);
276        assert_eq!(op_tx.base.gas_limit, 0x5208);
277    }
278
279    #[test]
280    fn test_decode_encode_deposit_tx() {
281        // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
282        let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
283            .parse::<TxHash>()
284            .unwrap();
285
286        // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
287        let raw_tx = alloy_primitives::hex::decode(
288            "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
289        )
290        .unwrap();
291        let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
292
293        let mut encoded = Vec::new();
294        dep_tx.encode_2718(&mut encoded);
295
296        assert_eq!(raw_tx, encoded);
297
298        assert_eq!(tx_hash, dep_tx.hash());
299    }
300}