Skip to main content

op_alloy_rpc_types/
receipt.rs

1//! Receipt types for RPC
2
3use alloy_consensus::{Receipt, ReceiptWithBloom, TxReceipt};
4use alloy_rpc_types_eth::Log;
5use alloy_serde::OtherFields;
6use op_alloy_consensus::{
7    OpDepositReceipt, OpDepositReceiptWithBloom, OpReceipt, OpReceiptEnvelope,
8};
9use serde::{Deserialize, Serialize};
10
11/// OP Transaction Receipt type
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14#[doc(alias = "OpTxReceipt")]
15pub struct OpTransactionReceipt {
16    /// Regular eth transaction receipt including deposit receipts
17    #[serde(flatten)]
18    pub inner: alloy_rpc_types_eth::TransactionReceipt<ReceiptWithBloom<OpReceipt<Log>>>,
19    /// L1 block info of the transaction.
20    #[serde(flatten)]
21    pub l1_block_info: L1BlockInfo,
22    /// Per-transaction gas refund from post-exec block-level warming.
23    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
24    pub op_gas_refund: Option<u64>,
25}
26
27impl alloy_network_primitives::ReceiptResponse for OpTransactionReceipt {
28    fn contract_address(&self) -> Option<alloy_primitives::Address> {
29        self.inner.contract_address
30    }
31
32    fn status(&self) -> bool {
33        self.inner.inner.status()
34    }
35
36    fn block_hash(&self) -> Option<alloy_primitives::BlockHash> {
37        self.inner.block_hash
38    }
39
40    fn block_number(&self) -> Option<u64> {
41        self.inner.block_number
42    }
43
44    fn transaction_hash(&self) -> alloy_primitives::TxHash {
45        self.inner.transaction_hash
46    }
47
48    fn transaction_index(&self) -> Option<u64> {
49        self.inner.transaction_index()
50    }
51
52    fn gas_used(&self) -> u64 {
53        self.inner.gas_used()
54    }
55
56    fn effective_gas_price(&self) -> u128 {
57        self.inner.effective_gas_price()
58    }
59
60    fn blob_gas_used(&self) -> Option<u64> {
61        self.inner.blob_gas_used()
62    }
63
64    fn blob_gas_price(&self) -> Option<u128> {
65        self.inner.blob_gas_price()
66    }
67
68    fn from(&self) -> alloy_primitives::Address {
69        self.inner.from()
70    }
71
72    fn to(&self) -> Option<alloy_primitives::Address> {
73        self.inner.to()
74    }
75
76    fn cumulative_gas_used(&self) -> u64 {
77        self.inner.cumulative_gas_used()
78    }
79
80    fn state_root(&self) -> Option<alloy_primitives::B256> {
81        self.inner.state_root()
82    }
83}
84
85/// Additional fields for Optimism transaction receipts: <https://github.com/ethereum-optimism/op-geth/blob/f2e69450c6eec9c35d56af91389a1c47737206ca/core/types/receipt.go#L87-L87>
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88#[doc(alias = "OptimismTxReceiptFields")]
89pub struct OpTransactionReceiptFields {
90    /// L1 block info.
91    #[serde(flatten)]
92    pub l1_block_info: L1BlockInfo,
93    /// Per-transaction gas refund from post-exec block-level warming.
94    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
95    pub op_gas_refund: Option<u64>,
96    /* --------------------------------------- Regolith --------------------------------------- */
97    /// Deposit nonce for deposit transactions.
98    ///
99    /// Always null prior to the Regolith hardfork.
100    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
101    pub deposit_nonce: Option<u64>,
102    /* ---------------------------------------- Canyon ---------------------------------------- */
103    /// Deposit receipt version for deposit transactions.
104    ///
105    /// Always null prior to the Canyon hardfork.
106    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
107    pub deposit_receipt_version: Option<u64>,
108}
109
110/// Serialize/Deserialize l1FeeScalar to/from string
111mod l1_fee_scalar_serde {
112    use serde::{Deserialize, de};
113
114    pub(super) fn serialize<S>(value: &Option<f64>, s: S) -> Result<S::Ok, S::Error>
115    where
116        S: serde::Serializer,
117    {
118        use alloc::string::ToString;
119        if let Some(v) = value {
120            return s.serialize_str(&v.to_string());
121        }
122        s.serialize_none()
123    }
124
125    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
126    where
127        D: serde::Deserializer<'de>,
128    {
129        use alloc::string::String;
130        let s: Option<String> = Option::deserialize(deserializer)?;
131        if let Some(s) = s {
132            return Ok(Some(s.parse::<f64>().map_err(de::Error::custom)?));
133        }
134
135        Ok(None)
136    }
137}
138
139impl From<OpTransactionReceiptFields> for OtherFields {
140    fn from(value: OpTransactionReceiptFields) -> Self {
141        serde_json::to_value(value).unwrap().try_into().unwrap()
142    }
143}
144
145/// L1 block info extracted from input of first transaction in every block.
146///
147/// The subset of [`OpTransactionReceiptFields`], that encompasses L1 block
148/// info:
149/// <https://github.com/ethereum-optimism/op-geth/blob/f2e69450c6eec9c35d56af91389a1c47737206ca/core/types/receipt.go#L87-L87>
150#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct L1BlockInfo {
153    /// L1 base fee is the minimum price per unit of gas.
154    ///
155    /// Present from pre-bedrock as de facto L1 price per unit of gas. L1 base fee after Bedrock.
156    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
157    pub l1_gas_price: Option<u128>,
158    /// L1 gas used.
159    ///
160    /// Present from pre-bedrock, deprecated as of Fjord.
161    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
162    pub l1_gas_used: Option<u128>,
163    /// L1 fee for the transaction.
164    ///
165    /// Present from pre-bedrock.
166    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
167    pub l1_fee: Option<u128>,
168    /// L1 fee scalar for the transaction
169    ///
170    /// Present from pre-bedrock to Ecotone. Null after Ecotone.
171    #[serde(default, skip_serializing_if = "Option::is_none", with = "l1_fee_scalar_serde")]
172    pub l1_fee_scalar: Option<f64>,
173    /* ---------------------------------------- Ecotone ---------------------------------------- */
174    /// L1 base fee scalar. Applied to base fee to compute weighted gas price multiplier.
175    ///
176    /// Always null prior to the Ecotone hardfork.
177    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
178    pub l1_base_fee_scalar: Option<u128>,
179    /// L1 blob base fee.
180    ///
181    /// Always null prior to the Ecotone hardfork.
182    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
183    pub l1_blob_base_fee: Option<u128>,
184    /// L1 blob base fee scalar. Applied to blob base fee to compute weighted gas price multiplier.
185    ///
186    /// Always null prior to the Ecotone hardfork.
187    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
188    pub l1_blob_base_fee_scalar: Option<u128>,
189    /* ---------------------------------------- Isthmus ---------------------------------------- */
190    /// Operator fee scalar.
191    ///
192    /// Always null prior to the Isthmus hardfork.
193    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
194    pub operator_fee_scalar: Option<u128>,
195    /// Operator fee constant.
196    ///
197    /// Always null prior to the Isthmus hardfork.
198    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
199    pub operator_fee_constant: Option<u128>,
200    /* ---------------------------------------- Jovian ---------------------------------------- */
201    /// DA footprint gas scalar. Used to set the DA footprint block limit on the L2.
202    ///
203    /// Always null prior to the Jovian hardfork.
204    #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
205    pub da_footprint_gas_scalar: Option<u16>,
206}
207
208impl Eq for L1BlockInfo {}
209
210impl From<OpTransactionReceipt> for OpReceiptEnvelope<alloy_primitives::Log> {
211    fn from(value: OpTransactionReceipt) -> Self {
212        let inner_envelope = value.inner.inner.into();
213
214        /// Helper function to convert the inner logs within a [`ReceiptWithBloom`] from RPC to
215        /// consensus types.
216        #[inline(always)]
217        fn convert_standard_receipt(
218            receipt: ReceiptWithBloom<Receipt<alloy_rpc_types_eth::Log>>,
219        ) -> ReceiptWithBloom<Receipt<alloy_primitives::Log>> {
220            let ReceiptWithBloom { logs_bloom, receipt } = receipt;
221
222            let consensus_logs = receipt.logs.into_iter().map(|log| log.inner).collect();
223            ReceiptWithBloom {
224                receipt: Receipt {
225                    status: receipt.status,
226                    cumulative_gas_used: receipt.cumulative_gas_used,
227                    logs: consensus_logs,
228                },
229                logs_bloom,
230            }
231        }
232
233        match inner_envelope {
234            OpReceiptEnvelope::Legacy(receipt) => Self::Legacy(convert_standard_receipt(receipt)),
235            OpReceiptEnvelope::Eip2930(receipt) => Self::Eip2930(convert_standard_receipt(receipt)),
236            OpReceiptEnvelope::Eip1559(receipt) => Self::Eip1559(convert_standard_receipt(receipt)),
237            OpReceiptEnvelope::Eip7702(receipt) => Self::Eip7702(convert_standard_receipt(receipt)),
238            OpReceiptEnvelope::PostExec(receipt) => {
239                Self::PostExec(convert_standard_receipt(receipt))
240            }
241            OpReceiptEnvelope::Deposit(OpDepositReceiptWithBloom { logs_bloom, receipt }) => {
242                let consensus_logs = receipt.inner.logs.into_iter().map(|log| log.inner).collect();
243                let consensus_receipt = OpDepositReceiptWithBloom {
244                    receipt: OpDepositReceipt {
245                        inner: Receipt {
246                            status: receipt.inner.status,
247                            cumulative_gas_used: receipt.inner.cumulative_gas_used,
248                            logs: consensus_logs,
249                        },
250                        deposit_nonce: receipt.deposit_nonce,
251                        deposit_receipt_version: receipt.deposit_receipt_version,
252                    },
253                    logs_bloom,
254                };
255                Self::Deposit(consensus_receipt)
256            }
257        }
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use alloc::string::ToString;
265    use serde_json::{Value, json};
266
267    // <https://github.com/alloy-rs/op-alloy/issues/18>
268    #[test]
269    fn parse_rpc_receipt() {
270        let s = r#"{
271        "blockHash": "0x9e6a0fb7e22159d943d760608cc36a0fb596d1ab3c997146f5b7c55c8c718c67",
272        "blockNumber": "0x6cfef89",
273        "contractAddress": null,
274        "cumulativeGasUsed": "0xfa0d",
275        "depositNonce": "0x8a2d11",
276        "effectiveGasPrice": "0x0",
277        "from": "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001",
278        "gasUsed": "0xfa0d",
279        "logs": [],
280        "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
281        "status": "0x1",
282        "to": "0x4200000000000000000000000000000000000015",
283        "transactionHash": "0xb7c74afdeb7c89fb9de2c312f49b38cb7a850ba36e064734c5223a477e83fdc9",
284        "transactionIndex": "0x0",
285        "type": "0x7e",
286        "l1GasPrice": "0x3ef12787",
287        "l1GasUsed": "0x1177",
288        "l1Fee": "0x5bf1ab43d",
289        "l1BaseFeeScalar": "0x1",
290        "l1BlobBaseFee": "0x600ab8f05e64",
291        "l1BlobBaseFeeScalar": "0x1",
292        "operatorFeeScalar": "0x1",
293        "operatorFeeConstant": "0x1",
294        "daFootprintGasScalar": "0x1"
295    }"#;
296
297        let receipt: OpTransactionReceipt = serde_json::from_str(s).unwrap();
298        let value = serde_json::to_value(&receipt).unwrap();
299        let expected_value = serde_json::from_str::<serde_json::Value>(s).unwrap();
300        assert_eq!(value, expected_value);
301    }
302
303    #[test]
304    fn serialize_empty_optimism_transaction_receipt_fields_struct() {
305        let op_fields = OpTransactionReceiptFields::default();
306
307        let json = serde_json::to_value(op_fields).unwrap();
308        assert_eq!(json, json!({}));
309    }
310
311    #[test]
312    fn serialize_l1_fee_scalar() {
313        let op_fields = OpTransactionReceiptFields {
314            l1_block_info: L1BlockInfo { l1_fee_scalar: Some(0.678), ..Default::default() },
315            ..Default::default()
316        };
317
318        let json = serde_json::to_value(op_fields).unwrap();
319
320        assert_eq!(json["l1FeeScalar"], serde_json::Value::String("0.678".to_string()));
321    }
322
323    #[test]
324    fn deserialize_l1_fee_scalar() {
325        let json = json!({
326            "l1FeeScalar": "0.678"
327        });
328
329        let op_fields: OpTransactionReceiptFields = serde_json::from_value(json).unwrap();
330        assert_eq!(op_fields.l1_block_info.l1_fee_scalar, Some(0.678f64));
331
332        let json = json!({
333            "l1FeeScalar": Value::Null
334        });
335
336        let op_fields: OpTransactionReceiptFields = serde_json::from_value(json).unwrap();
337        assert_eq!(op_fields.l1_block_info.l1_fee_scalar, None);
338
339        let json = json!({});
340
341        let op_fields: OpTransactionReceiptFields = serde_json::from_value(json).unwrap();
342        assert_eq!(op_fields.l1_block_info.l1_fee_scalar, None);
343    }
344}