arb_rpc/
receipt.rs

1//! Arbitrum receipt conversion for RPC responses.
2
3use alloy_consensus::{Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt, Typed2718};
4use alloy_primitives::{Address, Bloom, TxKind};
5use alloy_rpc_types_eth::TransactionReceipt;
6use alloy_serde::WithOtherFields;
7use arb_primitives::ArbPrimitives;
8use reth_primitives_traits::SealedBlock;
9use reth_rpc_convert::transaction::{ConvertReceiptInput, ReceiptConverter};
10use reth_rpc_eth_types::EthApiError;
11
12use crate::header::l1_block_number_from_mix_hash;
13
14/// Converts Arbitrum receipts to RPC transaction receipts with extension fields.
15#[derive(Debug, Clone)]
16pub struct ArbReceiptConverter;
17
18impl ReceiptConverter<ArbPrimitives> for ArbReceiptConverter {
19    type RpcReceipt = WithOtherFields<TransactionReceipt>;
20    type Error = EthApiError;
21
22    fn convert_receipts(
23        &self,
24        receipts: Vec<ConvertReceiptInput<'_, ArbPrimitives>>,
25    ) -> Result<Vec<Self::RpcReceipt>, EthApiError> {
26        let results = receipts
27            .into_iter()
28            .map(|input| convert_single_receipt(input, None))
29            .collect();
30        Ok(results)
31    }
32
33    fn convert_receipts_with_block(
34        &self,
35        receipts: Vec<ConvertReceiptInput<'_, ArbPrimitives>>,
36        block: &SealedBlock<alloy_consensus::Block<arb_primitives::ArbTransactionSigned>>,
37    ) -> Result<Vec<Self::RpcReceipt>, Self::Error> {
38        let mix_hash = block.header().mix_hash;
39        let l1_block_number = l1_block_number_from_mix_hash(&mix_hash);
40
41        let results = receipts
42            .into_iter()
43            .map(|input| convert_single_receipt(input, Some(l1_block_number)))
44            .collect();
45        Ok(results)
46    }
47}
48
49fn convert_single_receipt(
50    input: ConvertReceiptInput<'_, ArbPrimitives>,
51    l1_block_number: Option<u64>,
52) -> WithOtherFields<TransactionReceipt> {
53    use alloy_consensus::{transaction::TxHashRef, Transaction};
54
55    let ConvertReceiptInput {
56        receipt,
57        tx,
58        gas_used,
59        next_log_index,
60        meta,
61    } = input;
62
63    let from = tx.signer();
64    let tx_hash = *tx.tx_hash();
65    let tx_type = tx.ty();
66
67    let (contract_address, to) = match tx.kind() {
68        TxKind::Create => (Some(from.create(tx.nonce())), None),
69        TxKind::Call(addr) => (None, Some(Address(*addr))),
70    };
71
72    let cumulative_gas_used = receipt.cumulative_gas_used();
73    let status = receipt.status_or_post_state();
74    let gas_used_for_l1 = receipt.gas_used_for_l1;
75
76    // Convert primitive logs to RPC logs with block/tx metadata.
77    let rpc_logs: Vec<alloy_rpc_types_eth::Log> = receipt
78        .logs()
79        .iter()
80        .enumerate()
81        .map(|(i, log)| alloy_rpc_types_eth::Log {
82            inner: log.clone(),
83            block_hash: Some(meta.block_hash),
84            block_number: Some(meta.block_number),
85            block_timestamp: None,
86            transaction_hash: Some(tx_hash),
87            transaction_index: Some(meta.index),
88            log_index: Some(next_log_index as u64 + i as u64),
89            removed: false,
90        })
91        .collect();
92
93    let bloom: Bloom = receipt.logs().iter().collect();
94
95    let receipt_with_bloom = ReceiptWithBloom::new(
96        Receipt {
97            status,
98            cumulative_gas_used,
99            logs: rpc_logs,
100        },
101        bloom,
102    );
103
104    // Build envelope matching transaction type.
105    let envelope = match tx_type {
106        0x01 => ReceiptEnvelope::Eip2930(receipt_with_bloom),
107        0x02 => ReceiptEnvelope::Eip1559(receipt_with_bloom),
108        0x03 => ReceiptEnvelope::Eip4844(receipt_with_bloom),
109        0x04 => ReceiptEnvelope::Eip7702(receipt_with_bloom),
110        _ => ReceiptEnvelope::Legacy(receipt_with_bloom),
111    };
112
113    // On Arbitrum, effective gas price is always the block base fee for all tx types.
114    let effective_gas_price = meta.base_fee.unwrap_or(0) as u128;
115
116    let base_receipt = TransactionReceipt {
117        inner: envelope,
118        transaction_hash: tx_hash,
119        transaction_index: Some(meta.index),
120        block_hash: Some(meta.block_hash),
121        block_number: Some(meta.block_number),
122        gas_used,
123        effective_gas_price,
124        blob_gas_used: None,
125        blob_gas_price: None,
126        from,
127        to,
128        contract_address,
129    };
130
131    // Add Arbitrum-specific extension fields.
132    let mut other = std::collections::BTreeMap::new();
133
134    // Override `type` for Arbitrum tx types (0x64+) since ReceiptEnvelope
135    // only supports standard Ethereum types and falls back to Legacy (0x0).
136    if tx_type >= 0x64 {
137        other.insert(
138            "type".to_string(),
139            serde_json::to_value(format!("{tx_type:#x}")).unwrap_or_default(),
140        );
141    }
142
143    // gasUsedForL1: always present on Arbitrum receipts.
144    other.insert(
145        "gasUsedForL1".to_string(),
146        serde_json::to_value(format!("{:#x}", gas_used_for_l1)).unwrap_or_default(),
147    );
148
149    // l1BlockNumber: included when block header is available.
150    if let Some(l1_bn) = l1_block_number {
151        other.insert(
152            "l1BlockNumber".to_string(),
153            serde_json::to_value(format!("{l1_bn:#x}")).unwrap_or_default(),
154        );
155    }
156
157    // multiGasUsed: multi-dimensional gas breakdown.
158    if !receipt.multi_gas_used.is_zero() {
159        other.insert(
160            "multiGasUsed".to_string(),
161            serde_json::to_value(receipt.multi_gas_used).unwrap_or_default(),
162        );
163    }
164
165    WithOtherFields {
166        inner: base_receipt,
167        other: alloy_serde::OtherFields::new(other),
168    }
169}