1use 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14#[doc(alias = "OpTxReceipt")]
15pub struct OpTransactionReceipt {
16 #[serde(flatten)]
18 pub inner: alloy_rpc_types_eth::TransactionReceipt<ReceiptWithBloom<OpReceipt<Log>>>,
19 #[serde(flatten)]
21 pub l1_block_info: L1BlockInfo,
22 #[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88#[doc(alias = "OptimismTxReceiptFields")]
89pub struct OpTransactionReceiptFields {
90 #[serde(flatten)]
92 pub l1_block_info: L1BlockInfo,
93 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
95 pub op_gas_refund: Option<u64>,
96 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
101 pub deposit_nonce: Option<u64>,
102 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
107 pub deposit_receipt_version: Option<u64>,
108}
109
110mod 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
151#[serde(rename_all = "camelCase")]
152pub struct L1BlockInfo {
153 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
157 pub l1_gas_price: Option<u128>,
158 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
162 pub l1_gas_used: Option<u128>,
163 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
167 pub l1_fee: Option<u128>,
168 #[serde(default, skip_serializing_if = "Option::is_none", with = "l1_fee_scalar_serde")]
172 pub l1_fee_scalar: Option<f64>,
173 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
178 pub l1_base_fee_scalar: Option<u128>,
179 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
183 pub l1_blob_base_fee: Option<u128>,
184 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
188 pub l1_blob_base_fee_scalar: Option<u128>,
189 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
194 pub operator_fee_scalar: Option<u128>,
195 #[serde(default, skip_serializing_if = "Option::is_none", with = "alloy_serde::quantity::opt")]
199 pub operator_fee_constant: Option<u128>,
200 #[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 #[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 #[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}