stratus/eth/primitives/
external_receipt.rs

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    /// Returns the transaction hash.
23    pub fn hash(&self) -> Hash {
24        Hash::from(self.0.transaction_hash.0)
25    }
26
27    /// Returns the block number.
28    #[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    /// Returns the block hash.
34    #[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    /// Retuns the effective price the sender had to pay to execute the transaction.
40    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    /// Checks if the transaction was completed with success.
47    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
103// -----------------------------------------------------------------------------
104// Serialization / Deserialization
105// -----------------------------------------------------------------------------
106
107impl<'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        // During migration from ethers to alloy, we need to handle receipts from both libraries.
113        // Ethers receipts do not include `effectiveGasPrice` and `type` fields which are
114        // required by alloy.
115        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
134// -----------------------------------------------------------------------------
135// Conversions: Other -> Self
136// -----------------------------------------------------------------------------
137
138impl 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}