1use alloy_consensus::ReceiptEnvelope;
2use alloy_primitives::B256;
3use alloy_primitives::Bloom;
4use alloy_primitives::Bytes;
5use alloy_primitives::U256;
6use fake::Dummy;
7use fake::Faker;
8use serde::Deserialize;
9
10use crate::alias::AlloyReceipt;
11use crate::alias::JsonValue;
12use crate::eth::primitives::BlockNumber;
13use crate::eth::primitives::Hash;
14use crate::eth::primitives::Wei;
15use crate::log_and_err;
16
17#[derive(Debug, Clone, PartialEq, derive_more::Deref, serde::Serialize)]
18#[serde(transparent)]
19pub struct ExternalReceipt(#[deref] pub AlloyReceipt);
20
21impl ExternalReceipt {
22 pub fn hash(&self) -> Hash {
24 Hash::from(self.0.transaction_hash.0)
25 }
26
27 #[allow(clippy::expect_used)]
29 pub fn block_number(&self) -> BlockNumber {
30 self.0.block_number.expect("external receipt must have block number").into()
31 }
32
33 #[allow(clippy::expect_used)]
35 pub fn block_hash(&self) -> Hash {
36 Hash::from(self.0.block_hash.expect("external receipt must have block hash").0)
37 }
38
39 pub fn execution_cost(&self) -> Wei {
41 let gas_price = U256::from(self.0.effective_gas_price);
42 let gas_used = U256::from(self.0.gas_used);
43 (gas_price * gas_used).into()
44 }
45
46 pub fn is_success(&self) -> bool {
48 self.0.inner.status()
49 }
50}
51
52impl Dummy<Faker> for ExternalReceipt {
53 fn dummy_with_rng<R: rand::Rng + ?Sized>(_faker: &Faker, rng: &mut R) -> Self {
54 let mut addr_bytes = [0u8; 20];
55 let mut hash_bytes = [0u8; 32];
56 rng.fill_bytes(&mut addr_bytes);
57 rng.fill_bytes(&mut hash_bytes);
58
59 let log = alloy_rpc_types_eth::Log {
60 inner: alloy_primitives::Log {
61 address: alloy_primitives::Address::from_slice(&addr_bytes),
62 data: alloy_primitives::LogData::new_unchecked(vec![B256::from_slice(&hash_bytes)], Bytes::default()),
63 },
64 block_hash: Some(B256::from_slice(&hash_bytes)),
65 block_number: Some(rng.next_u64()),
66 transaction_hash: Some(B256::from_slice(&hash_bytes)),
67 transaction_index: Some(rng.next_u64()),
68 log_index: Some(rng.next_u64()),
69 removed: false,
70 block_timestamp: Some(rng.next_u64()),
71 };
72
73 let receipt = alloy_consensus::Receipt {
74 status: alloy_consensus::Eip658Value::Eip658(true),
75 cumulative_gas_used: rng.next_u64(),
76 logs: vec![log],
77 };
78
79 let receipt_envelope = ReceiptEnvelope::Legacy(alloy_consensus::ReceiptWithBloom {
80 receipt,
81 logs_bloom: Bloom::default(),
82 });
83
84 let receipt = alloy_rpc_types_eth::TransactionReceipt {
85 inner: receipt_envelope,
86 transaction_hash: B256::from_slice(&hash_bytes),
87 transaction_index: Some(rng.next_u64()),
88 block_hash: Some(B256::from_slice(&hash_bytes)),
89 block_number: Some(rng.next_u64()),
90 from: alloy_primitives::Address::from_slice(&addr_bytes),
91 to: Some(alloy_primitives::Address::from_slice(&addr_bytes)),
92 contract_address: None,
93 gas_used: rng.next_u64(),
94 effective_gas_price: rng.next_u64() as u128,
95 blob_gas_used: None,
96 blob_gas_price: None,
97 };
98
99 ExternalReceipt(receipt)
100 }
101}
102
103impl<'de> serde::Deserialize<'de> for ExternalReceipt {
108 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109 where
110 D: serde::Deserializer<'de>,
111 {
112 let mut value = JsonValue::deserialize(deserializer)?;
116
117 if let Some(obj) = value.as_object_mut() {
118 if !obj.contains_key("effectiveGasPrice") {
119 obj.insert("effectiveGasPrice".to_string(), serde_json::json!("0x0"));
120 }
121 if !obj.contains_key("type") {
122 obj.insert("type".to_string(), serde_json::json!("0x0"));
123 }
124 } else {
125 return Err(serde::de::Error::custom("ExternalReceipt must be a JSON object, received invalid type"));
126 }
127
128 let receipt = serde_json::from_value(value).map_err(|e| serde::de::Error::custom(format!("Failed to deserialize ExternalReceipt: {e}")))?;
129
130 Ok(ExternalReceipt(receipt))
131 }
132}
133
134impl TryFrom<JsonValue> for ExternalReceipt {
139 type Error = anyhow::Error;
140
141 fn try_from(value: JsonValue) -> Result<Self, Self::Error> {
142 match ExternalReceipt::deserialize(&value) {
143 Ok(v) => Ok(v),
144 Err(e) => log_and_err!(reason = e, payload = value, "failed to convert payload value to ExternalReceipt"),
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151
152 use alloy_consensus::TxType;
153
154 use super::*;
155
156 #[test]
157 fn test_deserialize_ethers_receipt() {
158 let ethers_receipt = r#"{
159 "blockHash": "0xc05ff25c9e4bcfb57a5bab271a38b46a8c8b2d5d9ef815ba449d6e211da42251",
160 "blockNumber": "0x20",
161 "contractAddress": null,
162 "cumulativeGasUsed": "0x0",
163 "from": "0x4fe666531f4a27d0cf5e3d2e73d9122a7f03777b",
164 "gasUsed": "0xe19c",
165 "logs": [{
166 "address": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512",
167 "blockHash": "0xc05ff25c9e4bcfb57a5bab271a38b46a8c8b2d5d9ef815ba449d6e211da42251",
168 "blockNumber": "0x20",
169 "data": "0x000000000000000000000000000000000000000000000000000000000000000a",
170 "logIndex": "0x0",
171 "removed": false,
172 "topics": [
173 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
174 "0x0000000000000000000000004fe666531f4a27d0cf5e3d2e73d9122a7f03777b",
175 "0x000000000000000000000000673dfa23201c98b7a3bfb48fc5cc4011d6759869"
176 ],
177 "transactionHash": "0x1c9b122e1321398ac869512b121f97c057e28e0e2fa96e9a8df1ecbfa9824faf",
178 "transactionIndex": "0x20"
179 }],
180 "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000004200000000000000000000000000000008000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000800000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000002000000000000000000001000000000000000000000000000080000000000000000000000000000000000000000000000000000800000000000000000",
181 "status": "0x1",
182 "to": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512",
183 "transactionHash": "0x1c9b122e1321398ac869512b121f97c057e28e0e2fa96e9a8df1ecbfa9824faf",
184 "transactionIndex": "0x20"
185 }"#;
186
187 let receipt: ExternalReceipt = serde_json::from_str(ethers_receipt).unwrap();
188 assert_eq!(receipt.0.effective_gas_price, 0);
189 assert_eq!(receipt.0.transaction_type(), TxType::Legacy);
190 }
191
192 #[test]
193 fn test_deserialize_alloy_receipt() {
194 let alloy_receipt = r#"{
195 "blockHash": "0x20dd72172e4bd9c99a919c217dd8c0154cbe0f9e305e67c5247f2ee8ae987c06",
196 "blockNumber": "0x16",
197 "contractAddress": null,
198 "cumulativeGasUsed": "0xe19c",
199 "effectiveGasPrice": "0x0",
200 "from": "0x08ea581a1da0e4c8a3e494501102c1cb16a89d1d",
201 "gasUsed": "0xe19c",
202 "logs": [{
203 "address": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512",
204 "blockHash": "0x20dd72172e4bd9c99a919c217dd8c0154cbe0f9e305e67c5247f2ee8ae987c06",
205 "blockNumber": "0x16",
206 "data": "0x0000000000000000000000000000000000000000000000000000000000000002",
207 "logIndex": "0x0",
208 "removed": false,
209 "topics": [
210 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
211 "0x00000000000000000000000008ea581a1da0e4c8a3e494501102c1cb16a89d1d",
212 "0x0000000000000000000000008259d2809ea92d5fad80c279ea11d2e371b8e33c"
213 ],
214 "transactionHash": "0x8eef471d6dad6584888af17b80f01f25f79875a0e0a1cbd17809c74093381bbc",
215 "transactionIndex": "0x26"
216 }],
217 "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000010000000000000000000000000010000000000000000000000000008000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000002000004000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000",
218 "status": "0x1",
219 "to": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512",
220 "transactionHash": "0x8eef471d6dad6584888af17b80f01f25f79875a0e0a1cbd17809c74093381bbc",
221 "transactionIndex": "0x26",
222 "type": "0x0"
223 }"#;
224
225 let receipt: ExternalReceipt = serde_json::from_str(alloy_receipt).unwrap();
226 assert_eq!(receipt.0.effective_gas_price, 0);
227 assert_eq!(receipt.0.transaction_type(), TxType::Legacy);
228 }
229}