stratus/eth/primitives/
external_receipt.rs

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    /// Returns the transaction hash.
29    pub fn hash(&self) -> Hash {
30        Hash::from(self.0.transaction_hash.0)
31    }
32
33    /// Returns the block number.
34    #[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    /// Returns the block hash.
40    #[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    /// Retuns the effective price the sender had to pay to execute the transaction.
46    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    /// Checks if the transaction was completed with success.
53    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
110// -----------------------------------------------------------------------------
111// Serialization / Deserialization
112// -----------------------------------------------------------------------------
113
114impl<'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        // During migration from ethers to alloy, we need to handle receipts from both libraries.
120        // Ethers receipts do not include `effectiveGasPrice` and `type` fields which are
121        // required by alloy.
122        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
141// -----------------------------------------------------------------------------
142// Conversions: Other -> Self
143// -----------------------------------------------------------------------------
144
145impl 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}