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#[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 #[cfg_attr(feature = "serde", serde(flatten))]
19 pub inner: T,
20 #[doc(alias = "tx_hash")]
22 pub transaction_hash: TxHash,
23 #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity::opt"))]
25 #[doc(alias = "tx_index")]
26 pub transaction_index: Option<u64>,
27 #[cfg_attr(feature = "serde", serde(default))]
29 pub block_hash: Option<BlockHash>,
30 #[cfg_attr(feature = "serde", serde(default, with = "alloy_serde::quantity::opt"))]
32 pub block_number: Option<u64>,
33 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
35 pub gas_used: u64,
36 #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
40 pub effective_gas_price: u128,
41 #[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 #[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 pub from: Address,
65 pub to: Option<Address>,
67 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 #[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 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 #[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 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 pub fn into_inner(self) -> T {
184 self.inner
185 }
186
187 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 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 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 pub fn logs(&self) -> &[T::Log] {
222 self.inner.logs()
223 }
224}
225impl<T: TxReceipt<Log: AsRef<alloy_primitives::Log>>> TransactionReceipt<T> {
226 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 pub fn decode_first_log<E: SolEvent>(&self) -> Option<alloy_primitives::Log<E>> {
238 self.logs().first().and_then(|log| E::decode_log(log.as_ref()).ok())
239 }
240
241 pub fn decode_nth_log<E: SolEvent>(&self, idx: usize) -> Option<alloy_primitives::Log<E>> {
243 self.logs().get(idx).and_then(|log| E::decode_log(log.as_ref()).ok())
244 }
245
246 pub fn decode_last_log<E: SolEvent>(&self) -> Option<alloy_primitives::Log<E>> {
248 self.logs().last().and_then(|log| E::decode_log(log.as_ref()).ok())
249 }
250}
251
252impl<T: TxReceipt<Log = Log>> ReceiptResponse for TransactionReceipt<T> {
253 fn contract_address(&self) -> Option<Address> {
254 self.contract_address
255 }
256
257 fn status(&self) -> bool {
258 self.inner.status()
259 }
260
261 fn block_hash(&self) -> Option<BlockHash> {
262 self.block_hash
263 }
264
265 fn block_number(&self) -> Option<u64> {
266 self.block_number
267 }
268
269 fn transaction_hash(&self) -> TxHash {
270 self.transaction_hash
271 }
272
273 fn transaction_index(&self) -> Option<u64> {
274 self.transaction_index
275 }
276
277 fn gas_used(&self) -> u64 {
278 self.gas_used
279 }
280
281 fn effective_gas_price(&self) -> u128 {
282 self.effective_gas_price
283 }
284
285 fn blob_gas_used(&self) -> Option<u64> {
286 self.blob_gas_used
287 }
288
289 fn blob_gas_price(&self) -> Option<u128> {
290 self.blob_gas_price
291 }
292
293 fn from(&self) -> Address {
294 self.from
295 }
296
297 fn to(&self) -> Option<Address> {
298 self.to
299 }
300
301 fn cumulative_gas_used(&self) -> u64 {
302 self.inner.cumulative_gas_used()
303 }
304
305 fn state_root(&self) -> Option<B256> {
306 self.inner.status_or_post_state().as_post_state()
307 }
308}
309
310impl From<TransactionReceipt> for TransactionReceipt<ReceiptEnvelope<alloy_primitives::Log>> {
311 fn from(value: TransactionReceipt) -> Self {
312 value.into_primitives_receipt()
313 }
314}
315
316#[cfg(test)]
317mod test {
318 use super::*;
319 use crate::TransactionReceipt;
320 use alloy_consensus::{Eip658Value, Receipt, ReceiptWithBloom};
321 use alloy_primitives::{address, b256, bloom, Bloom};
322 use arbitrary::Arbitrary;
323 use rand::Rng;
324 use similar_asserts::assert_eq;
325
326 #[test]
327 fn transaction_receipt_arbitrary() {
328 let mut bytes = [0u8; 1024];
329 rand::thread_rng().fill(bytes.as_mut_slice());
330
331 let _: TransactionReceipt =
332 TransactionReceipt::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
333 }
334
335 #[test]
336 #[cfg(feature = "serde")]
337 fn test_sanity() {
338 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"}"#;
339
340 let receipt: TransactionReceipt = serde_json::from_str(json_str).unwrap();
341 assert_eq!(
342 receipt.transaction_hash,
343 b256!("21f6554c28453a01e7276c1db2fc1695bb512b170818bfa98fa8136433100616")
344 );
345
346 const EXPECTED_BLOOM: Bloom = bloom!("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000200000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000800000000000000000000000000000000004000000000000000000800000000100000020000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000010000000000000000000000000000");
347 const EXPECTED_CGU: u64 = 0xa42aec;
348
349 assert!(matches!(
350 receipt.inner,
351 ReceiptEnvelope::Eip1559(ReceiptWithBloom {
352 receipt: Receipt {
353 status: Eip658Value::Eip658(true),
354 cumulative_gas_used: EXPECTED_CGU,
355 ..
356 },
357 logs_bloom: EXPECTED_BLOOM
358 })
359 ));
360
361 let log = receipt.inner.as_receipt().unwrap().logs.first().unwrap();
362 assert_eq!(log.address(), address!("dac17f958d2ee523a2206206994597c13d831ec7"));
363 assert_eq!(log.log_index, Some(0x118));
364 assert_eq!(
365 log.topics(),
366 vec![
367 b256!("8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"),
368 b256!("0000000000000000000000009a53bfba35269414f3b2d20b52ca01b15932c7b2"),
369 b256!("00000000000000000000000039e5dbb9d2fead31234d7c647d6ce77d85826f76")
370 ],
371 );
372
373 assert_eq!(
374 serde_json::to_value(&receipt).unwrap(),
375 serde_json::from_str::<serde_json::Value>(json_str).unwrap()
376 );
377 }
378
379 #[test]
380 #[cfg(feature = "serde")]
381 fn deserialize_pre_eip658_receipt() {
382 let receipt_json = r#"
383 {
384 "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f",
385 "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
386 "blockNumber": "0xf4240",
387 "logsBloom": "0x00000000000000000000000000000000000800000000000000000000000800000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000",
388 "gasUsed": "0x723c",
389 "root": "0x284d35bf53b82ef480ab4208527325477439c64fb90ef518450f05ee151c8e10",
390 "contractAddress": null,
391 "cumulativeGasUsed": "0x723c",
392 "transactionIndex": "0x0",
393 "from": "0x39fa8c5f2793459d6622857e7d9fbb4bd91766d3",
394 "to": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
395 "type": "0x0",
396 "effectiveGasPrice": "0x12bfb19e60",
397 "logs": [
398 {
399 "blockHash": "0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e",
400 "address": "0xc083e9947cf02b8ffc7d3090ae9aea72df98fd47",
401 "logIndex": "0x0",
402 "data": "0x00000000000000000000000039fa8c5f2793459d6622857e7d9fbb4bd91766d30000000000000000000000000000000000000000000000056bc75e2d63100000",
403 "removed": false,
404 "topics": [
405 "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"
406 ],
407 "blockNumber": "0xf4240",
408 "transactionIndex": "0x0",
409 "transactionHash": "0xea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f"
410 }
411 ]
412 }
413 "#;
414
415 let receipt = serde_json::from_str::<TransactionReceipt>(receipt_json).unwrap();
416
417 assert_eq!(
418 receipt.transaction_hash,
419 b256!("ea1093d492a1dcb1bef708f771a99a96ff05dcab81ca76c31940300177fcf49f")
420 );
421 }
422
423 #[test]
425 fn no_effective_gas_price_deser() {
426 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"}"#;
428
429 let receipt: TransactionReceipt = serde_json::from_str(json).unwrap();
430
431 assert_eq!(receipt.effective_gas_price, 0);
432
433 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"}"#;
434
435 let receipt_with_gas_price: TransactionReceipt =
436 serde_json::from_str(with_gas_price).unwrap();
437
438 assert_eq!(receipt_with_gas_price.effective_gas_price, 12500000000);
439
440 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"}"#;
441 let proper_receipt: TransactionReceipt = serde_json::from_str(proper_receipt).unwrap();
442 assert_eq!(proper_receipt.effective_gas_price, 12500000000);
443 }
444}