alloy_rpc_types_eth/transaction/
receipt.rs

1use crate::Log;
2use alloy_consensus::{ReceiptEnvelope, TxReceipt, TxType};
3use alloy_network_primitives::ReceiptResponse;
4use alloy_primitives::{Address, BlockHash, TxHash, B256};
5use alloy_sol_types::SolEvent;
6
7/// Transaction receipt
8///
9/// This type is generic over an inner [`ReceiptEnvelope`] which contains
10/// consensus data and metadata.
11#[derive(Clone, Debug, PartialEq, Eq)]
12#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
15#[doc(alias = "TxReceipt")]
16pub struct TransactionReceipt<T = ReceiptEnvelope<Log>> {
17    /// The receipt envelope, which contains the consensus receipt data.
18    #[cfg_attr(feature = "serde", serde(flatten))]
19    pub inner: T,
20    /// Transaction Hash.
21    #[doc(alias = "tx_hash")]
22    pub transaction_hash: TxHash,
23    /// Index within the block.
24    #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity::opt"))]
25    #[doc(alias = "tx_index")]
26    pub transaction_index: Option<u64>,
27    /// Hash of the block this transaction was included within.
28    #[cfg_attr(feature = "serde", serde(default))]
29    pub block_hash: Option<BlockHash>,
30    /// Number of the block this transaction was included within.
31    #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity::opt"))]
32    pub block_number: Option<u64>,
33    /// Gas used by this transaction alone.
34    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
35    pub gas_used: u64,
36    /// The price paid post-execution by the transaction (i.e. base fee + priority fee). Both
37    /// fields in 1559-style transactions are maximums (max fee + max priority fee), the amount
38    /// that's actually paid by users can only be determined post-execution
39    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
40    pub effective_gas_price: u128,
41    /// Blob gas used by the eip-4844 transaction
42    ///
43    /// This is None for non eip-4844 transactions
44    #[cfg_attr(
45        feature = "serde",
46        serde(
47            skip_serializing_if = "Option::is_none",
48            with = "alloy_serde::quantity::opt",
49            default
50        )
51    )]
52    pub blob_gas_used: Option<u64>,
53    /// The price paid by the eip-4844 transaction per blob gas.
54    #[cfg_attr(
55        feature = "serde",
56        serde(
57            skip_serializing_if = "Option::is_none",
58            with = "alloy_serde::quantity::opt",
59            default
60        )
61    )]
62    pub blob_gas_price: Option<u128>,
63    /// Address of the sender
64    pub from: Address,
65    /// Address of the receiver. None when its a contract creation transaction.
66    pub to: Option<Address>,
67    /// Contract address created, or None if not a deployment.
68    pub contract_address: Option<Address>,
69}
70
71#[cfg(feature = "serde")]
72impl<'de, T> serde::Deserialize<'de> for TransactionReceipt<T>
73where
74    T: serde::Deserialize<'de>,
75{
76    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
77    where
78        D: serde::Deserializer<'de>,
79    {
80        #[derive(serde::Deserialize)]
81        #[serde(rename_all = "camelCase")]
82        struct ReceiptDeserHelper<T = ReceiptEnvelope<Log>> {
83            #[serde(flatten)]
84            inner: T,
85            transaction_hash: TxHash,
86            #[serde(default, with = "alloy_serde::quantity::opt")]
87            transaction_index: Option<u64>,
88            #[serde(default)]
89            block_hash: Option<BlockHash>,
90            #[serde(default, with = "alloy_serde::quantity::opt")]
91            block_number: Option<u64>,
92            #[serde(with = "alloy_serde::quantity")]
93            gas_used: u64,
94            // Optional fields for gas prices
95            // 1. Use effectiveGasPrice if present
96            // 2. Fallback to use gasPrice
97            // 3. Default to 0 if neither is present
98            #[serde(default, alias = "gasPrice", with = "alloy_serde::quantity::opt")]
99            effective_gas_price: Option<u128>,
100            #[serde(
101                default,
102                skip_serializing_if = "Option::is_none",
103                with = "alloy_serde::quantity::opt"
104            )]
105            blob_gas_used: Option<u64>,
106            #[serde(
107                default,
108                skip_serializing_if = "Option::is_none",
109                with = "alloy_serde::quantity::opt"
110            )]
111            blob_gas_price: Option<u128>,
112            from: Address,
113            to: Option<Address>,
114            contract_address: Option<Address>,
115        }
116
117        let helper = ReceiptDeserHelper::deserialize(deserializer)?;
118        Ok(Self {
119            inner: helper.inner,
120            transaction_hash: helper.transaction_hash,
121            transaction_index: helper.transaction_index,
122            block_hash: helper.block_hash,
123            block_number: helper.block_number,
124            gas_used: helper.gas_used,
125            effective_gas_price: helper.effective_gas_price.unwrap_or(0),
126            blob_gas_used: helper.blob_gas_used,
127            blob_gas_price: helper.blob_gas_price,
128            from: helper.from,
129            to: helper.to,
130            contract_address: helper.contract_address,
131        })
132    }
133}
134
135impl AsRef<ReceiptEnvelope<Log>> for TransactionReceipt {
136    fn as_ref(&self) -> &ReceiptEnvelope<Log> {
137        &self.inner
138    }
139}
140
141impl TransactionReceipt {
142    /// Returns the status of the transaction.
143    pub const fn status(&self) -> bool {
144        match &self.inner {
145            ReceiptEnvelope::Eip1559(receipt)
146            | ReceiptEnvelope::Eip2930(receipt)
147            | ReceiptEnvelope::Eip4844(receipt)
148            | ReceiptEnvelope::Eip7702(receipt)
149            | ReceiptEnvelope::Legacy(receipt) => receipt.receipt.status.coerce_status(),
150        }
151    }
152
153    /// Returns the transaction type.
154    #[doc(alias = "tx_type")]
155    pub const fn transaction_type(&self) -> TxType {
156        self.inner.tx_type()
157    }
158}
159
160impl<T> TransactionReceipt<T> {
161    /// Maps the inner receipt value of this receipt.
162    pub fn map_inner<U, F>(self, f: F) -> TransactionReceipt<U>
163    where
164        F: FnOnce(T) -> U,
165    {
166        TransactionReceipt {
167            inner: f(self.inner),
168            transaction_hash: self.transaction_hash,
169            transaction_index: self.transaction_index,
170            block_hash: self.block_hash,
171            block_number: self.block_number,
172            gas_used: self.gas_used,
173            effective_gas_price: self.effective_gas_price,
174            blob_gas_used: self.blob_gas_used,
175            blob_gas_price: self.blob_gas_price,
176            from: self.from,
177            to: self.to,
178            contract_address: self.contract_address,
179        }
180    }
181
182    /// Consumes the type and returns the wrapped receipt.
183    pub fn into_inner(self) -> T {
184        self.inner
185    }
186
187    /// Calculates the address that will be created by the transaction, if any.
188    ///
189    /// Returns `None` if the transaction is not a contract creation (the `to` field is set).
190    pub fn calculate_create_address(&self, nonce: u64) -> Option<Address> {
191        if self.to.is_some() {
192            return None;
193        }
194        Some(self.from.create(nonce))
195    }
196}
197
198impl<L> TransactionReceipt<ReceiptEnvelope<L>> {
199    /// Converts the receipt's log type by applying a function to each log.
200    ///
201    /// Returns the receipt with the new log type.
202    pub fn map_logs<U>(self, f: impl FnMut(L) -> U) -> TransactionReceipt<ReceiptEnvelope<U>> {
203        self.map_inner(|inner| inner.map_logs(f))
204    }
205
206    /// Converts the transaction receipt's [`ReceiptEnvelope`] with a custom log type into a
207    /// [`ReceiptEnvelope`] with the primitives [`alloy_primitives::Log`] type by converting the
208    /// logs.
209    pub fn into_primitives_receipt(
210        self,
211    ) -> TransactionReceipt<ReceiptEnvelope<alloy_primitives::Log>>
212    where
213        L: Into<alloy_primitives::Log>,
214    {
215        self.map_logs(Into::into)
216    }
217}
218
219impl<T: TxReceipt> TransactionReceipt<T> {
220    /// Get the receipt logs.
221    pub fn logs(&self) -> &[T::Log] {
222        self.inner.logs()
223    }
224}
225impl<T: TxReceipt<Log: AsRef<alloy_primitives::Log>>> TransactionReceipt<T> {
226    /// Attempts to decode the logs to the provided log type.
227    ///
228    /// Returns the first log that decodes successfully.
229    ///
230    /// Returns None, if none of the logs could be decoded to the provided log type or if there
231    /// are no logs.
232    pub fn decoded_log<E: SolEvent>(&self) -> Option<alloy_primitives::Log<E>> {
233        self.logs().iter().find_map(|log| E::decode_log(log.as_ref()).ok())
234    }
235}
236
237impl<T: TxReceipt<Log = Log>> ReceiptResponse for TransactionReceipt<T> {
238    fn contract_address(&self) -> Option<Address> {
239        self.contract_address
240    }
241
242    fn status(&self) -> bool {
243        self.inner.status()
244    }
245
246    fn block_hash(&self) -> Option<BlockHash> {
247        self.block_hash
248    }
249
250    fn block_number(&self) -> Option<u64> {
251        self.block_number
252    }
253
254    fn transaction_hash(&self) -> TxHash {
255        self.transaction_hash
256    }
257
258    fn transaction_index(&self) -> Option<u64> {
259        self.transaction_index
260    }
261
262    fn gas_used(&self) -> u64 {
263        self.gas_used
264    }
265
266    fn effective_gas_price(&self) -> u128 {
267        self.effective_gas_price
268    }
269
270    fn blob_gas_used(&self) -> Option<u64> {
271        self.blob_gas_used
272    }
273
274    fn blob_gas_price(&self) -> Option<u128> {
275        self.blob_gas_price
276    }
277
278    fn from(&self) -> Address {
279        self.from
280    }
281
282    fn to(&self) -> Option<Address> {
283        self.to
284    }
285
286    fn cumulative_gas_used(&self) -> u64 {
287        self.inner.cumulative_gas_used()
288    }
289
290    fn state_root(&self) -> Option<B256> {
291        self.inner.status_or_post_state().as_post_state()
292    }
293}
294
295impl From<TransactionReceipt> for TransactionReceipt<ReceiptEnvelope<alloy_primitives::Log>> {
296    fn from(value: TransactionReceipt) -> Self {
297        value.into_primitives_receipt()
298    }
299}
300
301#[cfg(test)]
302mod test {
303    use super::*;
304    use crate::TransactionReceipt;
305    use alloy_consensus::{Eip658Value, Receipt, ReceiptWithBloom};
306    use alloy_primitives::{address, b256, bloom, Bloom};
307    use arbitrary::Arbitrary;
308    use rand::Rng;
309    use similar_asserts::assert_eq;
310
311    #[test]
312    fn transaction_receipt_arbitrary() {
313        let mut bytes = [0u8; 1024];
314        rand::thread_rng().fill(bytes.as_mut_slice());
315
316        let _: TransactionReceipt =
317            TransactionReceipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
318    }
319
320    #[test]
321    #[cfg(feature = "serde")]
322    fn test_sanity() {
323        let json_str = r#"{"transactionHash":"0x21f6554c28453a01e7276c1db2fc1695bb512b170818bfa98fa8136433100616","blockHash":"0x4acbdefb861ef4adedb135ca52865f6743451bfbfa35db78076f881a40401a5e","blockNumber":"0x129f4b9","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000200000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000800000000000000000000000000000000004000000000000000000800000000100000020000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000010000000000000000000000000000","gasUsed":"0xbde1","contractAddress":null,"cumulativeGasUsed":"0xa42aec","transactionIndex":"0x7f","from":"0x9a53bfba35269414f3b2d20b52ca01b15932c7b2","to":"0xdac17f958d2ee523a2206206994597c13d831ec7","type":"0x2","effectiveGasPrice":"0xfb0f6e8c9","logs":[{"blockHash":"0x4acbdefb861ef4adedb135ca52865f6743451bfbfa35db78076f881a40401a5e","address":"0xdac17f958d2ee523a2206206994597c13d831ec7","logIndex":"0x118","data":"0x00000000000000000000000000000000000000000052b7d2dcc80cd2e4000000","removed":false,"topics":["0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925","0x0000000000000000000000009a53bfba35269414f3b2d20b52ca01b15932c7b2","0x00000000000000000000000039e5dbb9d2fead31234d7c647d6ce77d85826f76"],"blockNumber":"0x129f4b9","transactionIndex":"0x7f","transactionHash":"0x21f6554c28453a01e7276c1db2fc1695bb512b170818bfa98fa8136433100616"}],"status":"0x1"}"#;
324
325        let receipt: TransactionReceipt = serde_json::from_str(json_str).unwrap();
326        assert_eq!(
327            receipt.transaction_hash,
328            b256!("21f6554c28453a01e7276c1db2fc1695bb512b170818bfa98fa8136433100616")
329        );
330
331        const EXPECTED_BLOOM: Bloom = bloom!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000200000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000800000000000000000000000000000000004000000000000000000800000000100000020000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000010000000000000000000000000000");
332        const EXPECTED_CGU: u64 = 0xa42aec;
333
334        assert!(matches!(
335            receipt.inner,
336            ReceiptEnvelope::Eip1559(ReceiptWithBloom {
337                receipt: Receipt {
338                    status: Eip658Value::Eip658(true),
339                    cumulative_gas_used: EXPECTED_CGU,
340                    ..
341                },
342                logs_bloom: EXPECTED_BLOOM
343            })
344        ));
345
346        let log = receipt.inner.as_receipt().unwrap().logs.first().unwrap();
347        assert_eq!(log.address(), address!("dac17f958d2ee523a2206206994597c13d831ec7"));
348        assert_eq!(log.log_index, Some(0x118));
349        assert_eq!(
350            log.topics(),
351            vec![
352                b256!("8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"),
353                b256!("0000000000000000000000009a53bfba35269414f3b2d20b52ca01b15932c7b2"),
354                b256!("00000000000000000000000039e5dbb9d2fead31234d7c647d6ce77d85826f76")
355            ],
356        );
357
358        assert_eq!(
359            serde_json::to_value(&receipt).unwrap(),
360            serde_json::from_str::<serde_json::Value>(json_str).unwrap()
361        );
362    }
363
364    #[test]
365    #[cfg(feature = "serde")]
366    fn deserialize_pre_eip658_receipt() {
367        let receipt_json = r#"
368        {
369            "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f",
370            "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
371            "blockNumber": "0xf4240",
372            "logsBloom": "0x00000000000000000000000000000000000800000000000000000000000800000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000",
373            "gasUsed": "0x723c",
374            "root": "0x284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10",
375            "contractAddress": null,
376            "cumulativeGasUsed": "0x723c",
377            "transactionIndex": "0x0",
378            "from": "0x39fa8c5f2793459d6622857e7d9fbb4bd91766d3",
379            "to": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
380            "type": "0x0",
381            "effectiveGasPrice": "0x12bfb19e60",
382            "logs": [
383                {
384                    "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
385                    "address": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
386                    "logIndex": "0x0",
387                    "data": "0x00000000000000000000000039fa8c5f2793459d6622857e7d9fbb4bd91766d30000000000000000000000000000000000000000000000056bc75e2d63100000",
388                    "removed": false,
389                    "topics": [
390                    "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"
391                    ],
392                    "blockNumber": "0xf4240",
393                    "transactionIndex": "0x0",
394                    "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f"
395                }
396            ]
397        }
398        "#;
399
400        let receipt = serde_json::from_str::<TransactionReceipt>(receipt_json).unwrap();
401
402        assert_eq!(
403            receipt.transaction_hash,
404            b256!("ea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f")
405        );
406    }
407
408    // <https://github.com/alloy-rs/alloy/issues/2019>
409    #[test]
410    fn no_effective_gas_price_deser() {
411        // <https://xdcscan.com/tx/0x968c2d0a7b38bfd7f57684298b5b4cda08b591e9f59b60e865a6eb8b531ef837>
412        let json = r#"{"blockHash":"0x0fe66313f8b3f8d88d19ac13b05de0f6e0ef7fcb3293db0869062493ff98f9db","blockNumber":"0x4d34901","contractAddress":null,"cumulativeGasUsed":"0x157e6","from":"0x7ee0d8c9a1374e3d5ce33d48cd09578251af708f","gasUsed":"0x157e6","logs":[{"address":"0x03396fe4e58a0778679e2731564f064fa5256c6e","topics":["0x4736edcab43476194077e25fadaf13bbfb18c7db442202d616b41fd1d549dc9c","0x0000000000000000000000000000000000000000000000000e3762762ff00800","0x0000000000000000000000000000000000000000000000000000000067179cea"],"data":"0x","blockNumber":"0x4d34901","transactionHash":"0x968c2d0a7b38bfd7f57684298b5b4cda08b591e9f59b60e865a6eb8b531ef837","transactionIndex":"0x0","blockHash":"0x0fe66313f8b3f8d88d19ac13b05de0f6e0ef7fcb3293db0869062493ff98f9db","logIndex":"0x0","removed":false}],"logsBloom":"0x00000400000000000000000000000000000400010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000080000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000010000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000004000000000000000000000000000000000000000000040000000000000000000000000000000000000000010000000000000000000000000000000","status":"0x1","to":"0x03396fe4e58a0778679e2731564f064fa5256c6e","transactionHash":"0x968c2d0a7b38bfd7f57684298b5b4cda08b591e9f59b60e865a6eb8b531ef837","transactionIndex":"0x0","type":"0x0"}"#;
413
414        let receipt: TransactionReceipt = serde_json::from_str(json).unwrap();
415
416        assert_eq!(receipt.effective_gas_price, 0);
417
418        let with_gas_price = r#"{"blockHash":"0x0fe66313f8b3f8d88d19ac13b05de0f6e0ef7fcb3293db0869062493ff98f9db","blockNumber":"0x4d34901","contractAddress":null,"cumulativeGasUsed":"0x157e6","from":"0x7ee0d8c9a1374e3d5ce33d48cd09578251af708f","gasUsed":"0x157e6","gasPrice":"0x2e90edd00","logs":[{"address":"0x03396fe4e58a0778679e2731564f064fa5256c6e","topics":["0x4736edcab43476194077e25fadaf13bbfb18c7db442202d616b41fd1d549dc9c","0x0000000000000000000000000000000000000000000000000e3762762ff00800","0x0000000000000000000000000000000000000000000000000000000067179cea"],"data":"0x","blockNumber":"0x4d34901","transactionHash":"0x968c2d0a7b38bfd7f57684298b5b4cda08b591e9f59b60e865a6eb8b531ef837","transactionIndex":"0x0","blockHash":"0x0fe66313f8b3f8d88d19ac13b05de0f6e0ef7fcb3293db0869062493ff98f9db","logIndex":"0x0","removed":false}],"logsBloom":"0x00000400000000000000000000000000000400010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000080000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000010000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000004000000000000000000000000000000000000000000040000000000000000000000000000000000000000010000000000000000000000000000000","status":"0x1","to":"0x03396fe4e58a0778679e2731564f064fa5256c6e","transactionHash":"0x968c2d0a7b38bfd7f57684298b5b4cda08b591e9f59b60e865a6eb8b531ef837","transactionIndex":"0x0","type":"0x0"}"#;
419
420        let receipt_with_gas_price: TransactionReceipt =
421            serde_json::from_str(with_gas_price).unwrap();
422
423        assert_eq!(receipt_with_gas_price.effective_gas_price, 12500000000);
424
425        let proper_receipt = r#"{"blockHash":"0x0fe66313f8b3f8d88d19ac13b05de0f6e0ef7fcb3293db0869062493ff98f9db","blockNumber":"0x4d34901","contractAddress":null,"cumulativeGasUsed":"0x157e6","from":"0x7ee0d8c9a1374e3d5ce33d48cd09578251af708f","gasUsed":"0x157e6","effectiveGasPrice":"0x2e90edd00","logs":[{"address":"0x03396fe4e58a0778679e2731564f064fa5256c6e","topics":["0x4736edcab43476194077e25fadaf13bbfb18c7db442202d616b41fd1d549dc9c","0x0000000000000000000000000000000000000000000000000e3762762ff00800","0x0000000000000000000000000000000000000000000000000000000067179cea"],"data":"0x","blockNumber":"0x4d34901","transactionHash":"0x968c2d0a7b38bfd7f57684298b5b4cda08b591e9f59b60e865a6eb8b531ef837","transactionIndex":"0x0","blockHash":"0x0fe66313f8b3f8d88d19ac13b05de0f6e0ef7fcb3293db0869062493ff98f9db","logIndex":"0x0","removed":false}],"logsBloom":"0x00000400000000000000000000000000000400010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000080000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000010000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000004000000000000000000000000000000000000000000040000000000000000000000000000000000000000010000000000000000000000000000000","status":"0x1","to":"0x03396fe4e58a0778679e2731564f064fa5256c6e","transactionHash":"0x968c2d0a7b38bfd7f57684298b5b4cda08b591e9f59b60e865a6eb8b531ef837","transactionIndex":"0x0","type":"0x0"}"#;
426        let proper_receipt: TransactionReceipt = serde_json::from_str(proper_receipt).unwrap();
427        assert_eq!(proper_receipt.effective_gas_price, 12500000000);
428    }
429}