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