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        Self::is_deposit(self)
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        let mut tx = Self::from_recovered_tx(tx, caller);
118        tx.enveloped_tx = Some(encoded);
119        tx
120    }
121}
122
123impl From<op_alloy_rpc_types::Transaction<FoundryTxEnvelope>> for FoundryTransactionRequest {
124    fn from(tx: op_alloy_rpc_types::Transaction<FoundryTxEnvelope>) -> Self {
125        tx.inner.into_inner().into()
126    }
127}
128
129/// Converts `OtherFields` to `DepositTransactionParts`, produces error with missing fields.
130pub fn get_deposit_tx_parts(
131    other: &OtherFields,
132) -> Result<DepositTransactionParts, Vec<&'static str>> {
133    let mut missing = Vec::new();
134    let source_hash =
135        other.get_deserialized::<B256>("sourceHash").transpose().ok().flatten().unwrap_or_else(
136            || {
137                missing.push("sourceHash");
138                Default::default()
139            },
140        );
141    let mint = other
142        .get_deserialized::<U256>("mint")
143        .transpose()
144        .unwrap_or_else(|_| {
145            missing.push("mint");
146            Default::default()
147        })
148        .map(|value| value.saturating_to::<u128>());
149    let is_system_transaction =
150        other.get_deserialized::<bool>("isSystemTx").transpose().ok().flatten().unwrap_or_else(
151            || {
152                missing.push("isSystemTx");
153                Default::default()
154            },
155        );
156    if missing.is_empty() {
157        Ok(DepositTransactionParts { source_hash, mint, is_system_transaction })
158    } else {
159        Err(missing)
160    }
161}
162
163/// OP-stack-specific accessors on [`FoundryReceiptEnvelope`].
164impl<T> FoundryReceiptEnvelope<T> {
165    /// Return the receipt's deposit_nonce if it is a deposit receipt.
166    pub fn deposit_nonce(&self) -> Option<u64> {
167        self.as_deposit_receipt().and_then(|r| r.deposit_nonce)
168    }
169
170    /// Return the receipt's deposit version if it is a deposit receipt.
171    pub fn deposit_receipt_version(&self) -> Option<u64> {
172        self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version)
173    }
174
175    /// Returns the deposit receipt if it is a deposit receipt.
176    pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt<T>> {
177        match self {
178            Self::Deposit(t) => Some(&t.receipt),
179            _ => None,
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use alloy_network::eip2718::Encodable2718;
187    use alloy_primitives::TxHash;
188    use alloy_rlp::Decodable;
189
190    use super::*;
191
192    #[test]
193    fn test_from_recovered_tx_legacy_op() {
194        use alloy_consensus::transaction::SignerRecoverable;
195
196        let tx = r#"
197        {
198            "type": "0x0",
199            "chainId": "0x1",
200            "nonce": "0x0",
201            "gas": "0x5208",
202            "gasPrice": "0x1",
203            "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
204            "value": "0x1",
205            "input": "0x",
206            "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
207            "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
208            "v": "0x1b",
209            "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
210        }"#;
211
212        let typed_tx: FoundryTxEnvelope = serde_json::from_str(tx).unwrap();
213        let sender = typed_tx.recover_signer().unwrap();
214
215        // Test OpTransaction<TxEnv> conversion via FromRecoveredTx trait
216        let op_tx = OpTransaction::<TxEnv>::from_recovered_tx(&typed_tx, sender);
217        assert_eq!(op_tx.base.caller, sender);
218        assert_eq!(op_tx.base.gas_limit, 0x5208);
219    }
220
221    #[test]
222    fn test_decode_encode_deposit_tx() {
223        // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
224        let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
225            .parse::<TxHash>()
226            .unwrap();
227
228        // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
229        let raw_tx = alloy_primitives::hex::decode(
230            "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
231        )
232        .unwrap();
233        let dep_tx = FoundryTxEnvelope::decode(&mut raw_tx.as_slice()).unwrap();
234
235        let mut encoded = Vec::new();
236        dep_tx.encode_2718(&mut encoded);
237
238        assert_eq!(raw_tx, encoded);
239
240        assert_eq!(tx_hash, dep_tx.hash());
241    }
242}