Skip to main content

foundry_primitives/network/
receipt.rs

1use alloy_consensus::{Receipt, TxReceipt};
2use alloy_network::{AnyReceiptEnvelope, AnyTransactionReceipt, ReceiptResponse};
3use alloy_primitives::{Address, B256, BlockHash, TxHash, U64};
4use alloy_rpc_types::{ConversionError, Log, TransactionReceipt};
5use alloy_serde::WithOtherFields;
6use derive_more::AsRef;
7use op_alloy_consensus::{OpDepositReceipt, OpDepositReceiptWithBloom};
8use serde::{Deserialize, Serialize};
9use tempo_primitives::TEMPO_TX_TYPE_ID;
10
11use crate::FoundryReceiptEnvelope;
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AsRef)]
14pub struct FoundryTxReceipt(pub WithOtherFields<TransactionReceipt<FoundryReceiptEnvelope<Log>>>);
15
16impl FoundryTxReceipt {
17    pub fn new(inner: TransactionReceipt<FoundryReceiptEnvelope<Log>>) -> Self {
18        Self(WithOtherFields::new(inner))
19    }
20
21    /// Creates a new receipt with a timestamp in the other fields.
22    /// This avoids extra block lookups when timestamp is needed later.
23    pub fn with_timestamp(
24        inner: TransactionReceipt<FoundryReceiptEnvelope<Log>>,
25        timestamp: u64,
26    ) -> Self {
27        let mut receipt = WithOtherFields::new(inner);
28        receipt
29            .other
30            .insert("blockTimestamp".to_string(), serde_json::to_value(timestamp).unwrap());
31        Self(receipt)
32    }
33
34    /// Adds a `feePayer` field to the receipt.
35    pub fn with_fee_payer(mut self, fee_payer: Address) -> Self {
36        self.0.other.insert("feePayer".to_string(), serde_json::to_value(fee_payer).unwrap());
37        self
38    }
39
40    /// Get block timestamp from other fields if present.
41    pub fn block_timestamp(&self) -> Option<u64> {
42        self.0.other.get_deserialized::<u64>("blockTimestamp").transpose().ok().flatten()
43    }
44}
45
46impl ReceiptResponse for FoundryTxReceipt {
47    fn contract_address(&self) -> Option<Address> {
48        self.0.contract_address
49    }
50
51    fn status(&self) -> bool {
52        self.0.inner.status()
53    }
54
55    fn block_hash(&self) -> Option<BlockHash> {
56        self.0.block_hash
57    }
58
59    fn block_number(&self) -> Option<u64> {
60        self.0.block_number
61    }
62
63    fn transaction_hash(&self) -> TxHash {
64        self.0.transaction_hash
65    }
66
67    fn transaction_index(&self) -> Option<u64> {
68        self.0.transaction_index()
69    }
70
71    fn gas_used(&self) -> u64 {
72        self.0.gas_used()
73    }
74
75    fn effective_gas_price(&self) -> u128 {
76        self.0.effective_gas_price()
77    }
78
79    fn blob_gas_used(&self) -> Option<u64> {
80        self.0.blob_gas_used()
81    }
82
83    fn blob_gas_price(&self) -> Option<u128> {
84        self.0.blob_gas_price()
85    }
86
87    fn from(&self) -> Address {
88        self.0.from()
89    }
90
91    fn to(&self) -> Option<Address> {
92        self.0.to()
93    }
94
95    fn cumulative_gas_used(&self) -> u64 {
96        self.0.cumulative_gas_used()
97    }
98
99    fn state_root(&self) -> Option<B256> {
100        self.0.state_root()
101    }
102}
103
104impl TryFrom<AnyTransactionReceipt> for FoundryTxReceipt {
105    type Error = ConversionError;
106
107    fn try_from(receipt: AnyTransactionReceipt) -> Result<Self, Self::Error> {
108        let WithOtherFields {
109            inner:
110                TransactionReceipt {
111                    transaction_hash,
112                    transaction_index,
113                    block_hash,
114                    block_number,
115                    gas_used,
116                    contract_address,
117                    effective_gas_price,
118                    from,
119                    to,
120                    blob_gas_price,
121                    blob_gas_used,
122                    inner: AnyReceiptEnvelope { inner: receipt_with_bloom, r#type },
123                },
124            other,
125        } = receipt;
126
127        Ok(Self(WithOtherFields {
128            inner: TransactionReceipt {
129                transaction_hash,
130                transaction_index,
131                block_hash,
132                block_number,
133                gas_used,
134                contract_address,
135                effective_gas_price,
136                from,
137                to,
138                blob_gas_price,
139                blob_gas_used,
140                inner: match r#type {
141                    0x00 => FoundryReceiptEnvelope::Legacy(receipt_with_bloom),
142                    0x01 => FoundryReceiptEnvelope::Eip2930(receipt_with_bloom),
143                    0x02 => FoundryReceiptEnvelope::Eip1559(receipt_with_bloom),
144                    0x03 => FoundryReceiptEnvelope::Eip4844(receipt_with_bloom),
145                    0x04 => FoundryReceiptEnvelope::Eip7702(receipt_with_bloom),
146                    TEMPO_TX_TYPE_ID => FoundryReceiptEnvelope::Tempo(receipt_with_bloom),
147                    0x7E => {
148                        // Construct the deposit receipt, extracting optional deposit fields
149                        // These fields may not be present in all receipts, so missing/invalid
150                        // values are None
151                        let deposit_nonce = other
152                            .get_deserialized::<U64>("depositNonce")
153                            .transpose()
154                            .ok()
155                            .flatten()
156                            .map(|v| v.to::<u64>());
157                        let deposit_receipt_version = other
158                            .get_deserialized::<U64>("depositReceiptVersion")
159                            .transpose()
160                            .ok()
161                            .flatten()
162                            .map(|v| v.to::<u64>());
163
164                        FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom {
165                            receipt: OpDepositReceipt {
166                                inner: Receipt {
167                                    status: alloy_consensus::Eip658Value::Eip658(
168                                        receipt_with_bloom.status(),
169                                    ),
170                                    cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(),
171                                    logs: receipt_with_bloom.receipt.logs,
172                                },
173                                deposit_nonce,
174                                deposit_receipt_version,
175                            },
176                            logs_bloom: receipt_with_bloom.logs_bloom,
177                        })
178                    }
179                    _ => {
180                        let tx_type = r#type;
181                        return Err(ConversionError::Custom(format!(
182                            "Unknown transaction receipt type: 0x{tx_type:02X}"
183                        )));
184                    }
185                },
186            },
187            other,
188        }))
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    // <https://github.com/foundry-rs/foundry/issues/10852>
197    #[test]
198    fn test_receipt_convert() {
199        let s = r#"{"type":"0x4","status":"0x1","cumulativeGasUsed":"0x903fd1","logs":[{"address":"0x0000d9fcd47bf761e7287d8ee09917d7e2100000","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000234ce51365b9c417171b6dad280f49143e1b0547"],"data":"0x00000000000000000000000000000000000000000000032139b42c3431700000","blockHash":"0xd26b59c1d8b5bfa9362d19eb0da3819dfe0b367987a71f6d30908dd45e0d7a60","blockNumber":"0x159663e","blockTimestamp":"0x68411f7b","transactionHash":"0x17a6af73d1317e69cfc3cac9221bd98261d40f24815850a44dbfbf96652ae52a","transactionIndex":"0x22","logIndex":"0x158","removed":false}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000008100000000000000000000000000000000000000000000000020000200000000000000800000000800000000000000010000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x17a6af73d1317e69cfc3cac9221bd98261d40f24815850a44dbfbf96652ae52a","transactionIndex":"0x22","blockHash":"0xd26b59c1d8b5bfa9362d19eb0da3819dfe0b367987a71f6d30908dd45e0d7a60","blockNumber":"0x159663e","gasUsed":"0x28ee7","effectiveGasPrice":"0x4bf02090","from":"0x234ce51365b9c417171b6dad280f49143e1b0547","to":"0x234ce51365b9c417171b6dad280f49143e1b0547","contractAddress":null}"#;
200        let receipt: AnyTransactionReceipt = serde_json::from_str(s).unwrap();
201        let _converted = FoundryTxReceipt::try_from(receipt).unwrap();
202    }
203}