Skip to main content

foundry_primitives/network/
receipt.rs

1use crate::FoundryReceiptEnvelope;
2use alloy_network::{AnyReceiptEnvelope, AnyTransactionReceipt, ReceiptResponse};
3use alloy_primitives::{Address, B256, BlockHash, TxHash};
4use alloy_rpc_types::{ConversionError, Log, TransactionReceipt};
5use alloy_serde::WithOtherFields;
6use derive_more::AsRef;
7use serde::{Deserialize, Serialize};
8use tempo_primitives::TEMPO_TX_TYPE_ID;
9
10#[cfg(any(feature = "base", feature = "optimism"))]
11use super::optimism::build_deposit_receipt_envelope;
12
13#[cfg(feature = "base")]
14use alloy_consensus::ReceiptWithBloom;
15#[cfg(feature = "base")]
16use alloy_serde::OtherFields;
17#[cfg(feature = "base")]
18use base_common_consensus::Eip8130Receipt;
19#[cfg(feature = "base")]
20use base_common_evm::EIP8130_TRANSACTION_TYPE;
21
22#[derive(Clone, Debug, PartialEq, Eq, Serialize, AsRef)]
23pub struct FoundryTxReceipt(pub WithOtherFields<TransactionReceipt<FoundryReceiptEnvelope<Log>>>);
24
25impl<'de> Deserialize<'de> for FoundryTxReceipt {
26    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
27        #[allow(unused_mut)]
28        let mut receipt =
29            WithOtherFields::<TransactionReceipt<FoundryReceiptEnvelope<Log>>>::deserialize(
30                deserializer,
31            )?;
32        #[cfg(feature = "base")]
33        if let FoundryReceiptEnvelope::Eip8130(inner) = &mut receipt.inner.inner {
34            inner.receipt.phase_statuses =
35                rpc_phase_statuses(&receipt.other).map_err(serde::de::Error::custom)?;
36        }
37        Ok(Self(receipt))
38    }
39}
40
41#[cfg(feature = "base")]
42fn rpc_phase_statuses(other: &OtherFields) -> serde_json::Result<Vec<u8>> {
43    #[derive(Deserialize)]
44    struct PhaseStatuses(#[serde(with = "alloy_serde::quantity::vec")] Vec<u8>);
45    Ok(other
46        .get_deserialized::<Option<PhaseStatuses>>("phaseStatuses")
47        .transpose()?
48        .flatten()
49        .map(|statuses| statuses.0)
50        .unwrap_or_default())
51}
52
53impl FoundryTxReceipt {
54    pub fn new(inner: TransactionReceipt<FoundryReceiptEnvelope<Log>>) -> Self {
55        Self(WithOtherFields::new(inner))
56    }
57
58    /// Creates a new receipt with a timestamp in the other fields.
59    /// This avoids extra block lookups when timestamp is needed later.
60    pub fn with_timestamp(
61        inner: TransactionReceipt<FoundryReceiptEnvelope<Log>>,
62        timestamp: u64,
63    ) -> Self {
64        let mut receipt = WithOtherFields::new(inner);
65        receipt
66            .other
67            .insert("blockTimestamp".to_string(), serde_json::to_value(timestamp).unwrap());
68        Self(receipt)
69    }
70
71    /// Adds a `feePayer` field to the receipt.
72    pub fn with_fee_payer(mut self, fee_payer: Address) -> Self {
73        self.0.other.insert("feePayer".to_string(), serde_json::to_value(fee_payer).unwrap());
74        self
75    }
76
77    /// Adds a `feeToken` field to the receipt.
78    pub fn with_fee_token(mut self, fee_token: Address) -> Self {
79        self.0.other.insert("feeToken".to_string(), serde_json::to_value(fee_token).unwrap());
80        self
81    }
82
83    /// Get block timestamp from other fields if present.
84    pub fn block_timestamp(&self) -> Option<u64> {
85        self.0.other.get_deserialized::<u64>("blockTimestamp").transpose().ok().flatten()
86    }
87}
88
89impl ReceiptResponse for FoundryTxReceipt {
90    fn contract_address(&self) -> Option<Address> {
91        self.0.contract_address
92    }
93
94    fn status(&self) -> bool {
95        self.0.inner.status()
96    }
97
98    fn block_hash(&self) -> Option<BlockHash> {
99        self.0.block_hash
100    }
101
102    fn block_number(&self) -> Option<u64> {
103        self.0.block_number
104    }
105
106    fn transaction_hash(&self) -> TxHash {
107        self.0.transaction_hash
108    }
109
110    fn transaction_index(&self) -> Option<u64> {
111        self.0.transaction_index()
112    }
113
114    fn gas_used(&self) -> u64 {
115        self.0.gas_used()
116    }
117
118    fn effective_gas_price(&self) -> u128 {
119        self.0.effective_gas_price()
120    }
121
122    fn blob_gas_used(&self) -> Option<u64> {
123        self.0.blob_gas_used()
124    }
125
126    fn blob_gas_price(&self) -> Option<u128> {
127        self.0.blob_gas_price()
128    }
129
130    fn from(&self) -> Address {
131        self.0.from()
132    }
133
134    fn to(&self) -> Option<Address> {
135        self.0.to()
136    }
137
138    fn cumulative_gas_used(&self) -> u64 {
139        self.0.cumulative_gas_used()
140    }
141
142    fn state_root(&self) -> Option<B256> {
143        self.0.state_root()
144    }
145}
146
147impl TryFrom<AnyTransactionReceipt> for FoundryTxReceipt {
148    type Error = ConversionError;
149
150    fn try_from(receipt: AnyTransactionReceipt) -> Result<Self, Self::Error> {
151        let WithOtherFields {
152            inner:
153                TransactionReceipt {
154                    transaction_hash,
155                    transaction_index,
156                    block_hash,
157                    block_number,
158                    gas_used,
159                    contract_address,
160                    effective_gas_price,
161                    from,
162                    to,
163                    blob_gas_price,
164                    blob_gas_used,
165                    inner: AnyReceiptEnvelope { inner: receipt_with_bloom, r#type },
166                },
167            other,
168        } = receipt.0;
169
170        #[cfg(feature = "base")]
171        let phase_statuses = if r#type == EIP8130_TRANSACTION_TYPE {
172            rpc_phase_statuses(&other).map_err(|err| ConversionError::Custom(err.to_string()))?
173        } else {
174            Vec::new()
175        };
176
177        Ok(Self(WithOtherFields {
178            inner: TransactionReceipt {
179                transaction_hash,
180                transaction_index,
181                block_hash,
182                block_number,
183                gas_used,
184                contract_address,
185                effective_gas_price,
186                from,
187                to,
188                blob_gas_price,
189                blob_gas_used,
190                inner: match r#type {
191                    0x00 => FoundryReceiptEnvelope::Legacy(receipt_with_bloom),
192                    0x01 => FoundryReceiptEnvelope::Eip2930(receipt_with_bloom),
193                    0x02 => FoundryReceiptEnvelope::Eip1559(receipt_with_bloom),
194                    0x03 => FoundryReceiptEnvelope::Eip4844(receipt_with_bloom),
195                    0x04 => FoundryReceiptEnvelope::Eip7702(receipt_with_bloom),
196                    #[cfg(feature = "base")]
197                    EIP8130_TRANSACTION_TYPE => FoundryReceiptEnvelope::Eip8130(ReceiptWithBloom {
198                        receipt: Eip8130Receipt::new(receipt_with_bloom.receipt, phase_statuses),
199                        logs_bloom: receipt_with_bloom.logs_bloom,
200                    }),
201                    TEMPO_TX_TYPE_ID => FoundryReceiptEnvelope::Tempo(receipt_with_bloom),
202                    #[cfg(any(feature = "base", feature = "optimism"))]
203                    0x7E => build_deposit_receipt_envelope(receipt_with_bloom, &other),
204                    // Chains anvil can fork but not execute, such as Arbitrum and its Orbit
205                    // rollups, mint their own transaction types. Keep those receipts verbatim
206                    // instead of failing the whole request.
207                    ty => FoundryReceiptEnvelope::Unknown(AnyReceiptEnvelope {
208                        inner: receipt_with_bloom,
209                        r#type: ty,
210                    }),
211                },
212            },
213            other,
214        }))
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[cfg(feature = "base")]
223    #[test]
224    fn eip8130_rpc_receipt_preserves_phase_statuses() {
225        let receipt: AnyTransactionReceipt = serde_json::from_value(serde_json::json!({
226            "type": "0x79", "status": "0x1", "cumulativeGasUsed": "0x1", "gasUsed": "0x1",
227            "logs": [], "logsBloom": alloy_primitives::Bloom::ZERO,
228            "transactionHash": B256::ZERO, "from": Address::ZERO
229        }))
230        .unwrap();
231        for (statuses, expected) in [
232            (None, vec![]),
233            (Some(serde_json::Value::Null), vec![]),
234            (Some(serde_json::json!([])), vec![]),
235            (Some(serde_json::json!(["0x1", "0x0"])), vec![1, 0]),
236        ] {
237            let mut original = receipt.clone();
238            if let Some(statuses) = statuses {
239                original.0.other.insert("phaseStatuses".into(), statuses);
240            }
241            let converted = FoundryTxReceipt::try_from(original.clone()).unwrap();
242            assert_eq!(converted.0.inner.inner.eip8130_phase_statuses(), expected);
243            let json = serde_json::to_value(&converted).unwrap();
244            let typed: FoundryTxReceipt = serde_json::from_value(json.clone()).unwrap();
245            assert_eq!(typed, converted);
246            let roundtrip: AnyTransactionReceipt = serde_json::from_value(json).unwrap();
247            assert_eq!(roundtrip, original);
248        }
249        let mut invalid = receipt;
250        invalid.0.other.insert("phaseStatuses".into(), serde_json::json!(["invalid"]));
251        assert!(
252            serde_json::from_value::<FoundryTxReceipt>(serde_json::to_value(&invalid).unwrap())
253                .is_err()
254        );
255        assert!(FoundryTxReceipt::try_from(invalid).is_err());
256    }
257
258    // <https://github.com/foundry-rs/foundry/issues/10852>
259    #[test]
260    fn test_receipt_convert() {
261        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}"#;
262        let receipt: AnyTransactionReceipt = serde_json::from_str(s).unwrap();
263        let _converted = FoundryTxReceipt::try_from(receipt).unwrap();
264    }
265
266    // Arbitrum and its Orbit rollups mint transaction types anvil cannot execute; forked receipts
267    // for them must still convert, keeping the original type byte.
268    #[test]
269    fn test_arbitrum_internal_receipt_convert() {
270        let s = r#"{"type":"0x6a","status":"0x1","cumulativeGasUsed":"0x0","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x7c9e0e2b0f2ffbd0a1ee3e2e2b6ff5a2ff8b6a0f1c0b4a5c1d5a8b6c9d0e1f22","transactionIndex":"0x0","blockHash":"0x3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b7c6d5e4f3a2b","blockNumber":"0x159663e","gasUsed":"0x0","effectiveGasPrice":"0x0","from":"0x00000000000000000000000000000000000a4b05","to":"0x00000000000000000000000000000000000a4b05","contractAddress":null,"gasUsedForL1":"0x0","l1BlockNumber":"0x1499e2c"}"#;
271        let receipt: AnyTransactionReceipt = serde_json::from_str(s).unwrap();
272        let converted = FoundryTxReceipt::try_from(receipt).unwrap();
273
274        assert!(converted.0.inner.inner.is_unknown());
275        assert_eq!(converted.0.inner.inner.ty(), 0x6a);
276        assert_eq!(serde_json::to_value(&converted).unwrap()["type"], "0x6a");
277    }
278}