foundry_primitives/network/
receipt.rs1use 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 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 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 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 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 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 #[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 #[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}