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