Skip to main content

foundry_primitives/transaction/
receipt.rs

1use alloy_consensus::{
2    Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt, Typed2718,
3};
4use alloy_network::eip2718::{
5    Decodable2718, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID,
6    Eip2718Error, Encodable2718, LEGACY_TX_TYPE_ID,
7};
8use alloy_primitives::{Bloom, Log, TxHash, logs_bloom};
9use alloy_rlp::{BufMut, Decodable, Encodable, Header, bytes};
10use alloy_rpc_types::{BlockNumHash, trace::otterscan::OtsReceipt};
11#[cfg(feature = "optimism")]
12use op_alloy_consensus::{
13    DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom, POST_EXEC_TX_TYPE_ID,
14};
15use serde::{Deserialize, Serialize};
16use tempo_primitives::TEMPO_TX_TYPE_ID;
17
18use crate::FoundryTxType;
19
20#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(tag = "type")]
22pub enum FoundryReceiptEnvelope<T = Log> {
23    #[serde(rename = "0x0", alias = "0x00")]
24    Legacy(ReceiptWithBloom<Receipt<T>>),
25    #[serde(rename = "0x1", alias = "0x01")]
26    Eip2930(ReceiptWithBloom<Receipt<T>>),
27    #[serde(rename = "0x2", alias = "0x02")]
28    Eip1559(ReceiptWithBloom<Receipt<T>>),
29    #[serde(rename = "0x3", alias = "0x03")]
30    Eip4844(ReceiptWithBloom<Receipt<T>>),
31    #[serde(rename = "0x4", alias = "0x04")]
32    Eip7702(ReceiptWithBloom<Receipt<T>>),
33    #[cfg(feature = "optimism")]
34    #[serde(rename = "0x7D", alias = "0x7d")]
35    PostExec(ReceiptWithBloom<Receipt<T>>),
36    #[cfg(feature = "optimism")]
37    #[serde(rename = "0x7E", alias = "0x7e")]
38    Deposit(OpDepositReceiptWithBloom<T>),
39    #[serde(rename = "0x76")]
40    Tempo(ReceiptWithBloom<Receipt<T>>),
41}
42
43impl FoundryReceiptEnvelope<alloy_rpc_types::Log> {
44    /// Creates a new [`FoundryReceiptEnvelope`] from the given parts.
45    pub fn from_parts(
46        status: bool,
47        cumulative_gas_used: u64,
48        logs: impl IntoIterator<Item = alloy_rpc_types::Log>,
49        tx_type: FoundryTxType,
50        #[cfg_attr(not(feature = "optimism"), allow(unused_variables))] deposit_nonce: Option<u64>,
51        #[cfg_attr(not(feature = "optimism"), allow(unused_variables))]
52        deposit_receipt_version: Option<u64>,
53    ) -> Self {
54        let logs = logs.into_iter().collect::<Vec<_>>();
55        let logs_bloom = logs_bloom(logs.iter().map(|l| &l.inner));
56        let inner_receipt =
57            Receipt { status: Eip658Value::Eip658(status), cumulative_gas_used, logs };
58        match tx_type {
59            FoundryTxType::Legacy => {
60                Self::Legacy(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
61            }
62            FoundryTxType::Eip2930 => {
63                Self::Eip2930(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
64            }
65            FoundryTxType::Eip1559 => {
66                Self::Eip1559(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
67            }
68            FoundryTxType::Eip4844 => {
69                Self::Eip4844(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
70            }
71            FoundryTxType::Eip7702 => {
72                Self::Eip7702(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
73            }
74            #[cfg(feature = "optimism")]
75            FoundryTxType::PostExec => {
76                Self::PostExec(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
77            }
78            #[cfg(feature = "optimism")]
79            FoundryTxType::Deposit => {
80                let inner = OpDepositReceiptWithBloom {
81                    receipt: OpDepositReceipt {
82                        inner: inner_receipt,
83                        deposit_nonce,
84                        deposit_receipt_version,
85                    },
86                    logs_bloom,
87                };
88                Self::Deposit(inner)
89            }
90            FoundryTxType::Tempo => {
91                Self::Tempo(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
92            }
93        }
94    }
95}
96
97impl FoundryReceiptEnvelope<Log> {
98    pub fn convert_logs_rpc(
99        self,
100        block_numhash: BlockNumHash,
101        block_timestamp: u64,
102        transaction_hash: TxHash,
103        transaction_index: u64,
104        next_log_index: usize,
105    ) -> FoundryReceiptEnvelope<alloy_rpc_types::Log> {
106        let logs = self
107            .logs()
108            .iter()
109            .enumerate()
110            .map(|(index, log)| alloy_rpc_types::Log {
111                inner: log.clone(),
112                block_hash: Some(block_numhash.hash),
113                block_number: Some(block_numhash.number),
114                block_timestamp: Some(block_timestamp),
115                transaction_hash: Some(transaction_hash),
116                transaction_index: Some(transaction_index),
117                log_index: Some((next_log_index + index) as u64),
118                removed: false,
119            })
120            .collect::<Vec<_>>();
121        #[cfg(feature = "optimism")]
122        let (deposit_nonce, deposit_receipt_version) =
123            (self.deposit_nonce(), self.deposit_receipt_version());
124        #[cfg(not(feature = "optimism"))]
125        let (deposit_nonce, deposit_receipt_version) = (None, None);
126        FoundryReceiptEnvelope::<alloy_rpc_types::Log>::from_parts(
127            self.status(),
128            self.cumulative_gas_used(),
129            logs,
130            self.tx_type(),
131            deposit_nonce,
132            deposit_receipt_version,
133        )
134    }
135}
136
137impl<T> FoundryReceiptEnvelope<T> {
138    /// Return the [`FoundryTxType`] of the inner receipt.
139    pub const fn tx_type(&self) -> FoundryTxType {
140        match self {
141            Self::Legacy(_) => FoundryTxType::Legacy,
142            Self::Eip2930(_) => FoundryTxType::Eip2930,
143            Self::Eip1559(_) => FoundryTxType::Eip1559,
144            Self::Eip4844(_) => FoundryTxType::Eip4844,
145            Self::Eip7702(_) => FoundryTxType::Eip7702,
146            #[cfg(feature = "optimism")]
147            Self::PostExec(_) => FoundryTxType::PostExec,
148            #[cfg(feature = "optimism")]
149            Self::Deposit(_) => FoundryTxType::Deposit,
150            Self::Tempo(_) => FoundryTxType::Tempo,
151        }
152    }
153
154    /// Returns the success status of the receipt's transaction.
155    pub const fn status(&self) -> bool {
156        self.as_receipt().status.coerce_status()
157    }
158
159    /// Returns the cumulative gas used at this receipt.
160    pub const fn cumulative_gas_used(&self) -> u64 {
161        self.as_receipt().cumulative_gas_used
162    }
163
164    /// Converts the receipt's log type by applying a function to each log.
165    ///
166    /// Returns the receipt with the new log type.
167    pub fn map_logs<U>(self, f: impl FnMut(T) -> U) -> FoundryReceiptEnvelope<U> {
168        match self {
169            Self::Legacy(r) => FoundryReceiptEnvelope::Legacy(r.map_logs(f)),
170            Self::Eip2930(r) => FoundryReceiptEnvelope::Eip2930(r.map_logs(f)),
171            Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)),
172            Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)),
173            Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)),
174            #[cfg(feature = "optimism")]
175            Self::PostExec(r) => FoundryReceiptEnvelope::PostExec(r.map_logs(f)),
176            #[cfg(feature = "optimism")]
177            Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(
178                r.map_receipt(|r: OpDepositReceipt<T>| r.map_logs(f)),
179            ),
180            Self::Tempo(r) => FoundryReceiptEnvelope::Tempo(r.map_logs(f)),
181        }
182    }
183
184    /// Return the receipt logs.
185    pub fn logs(&self) -> &[T] {
186        &self.as_receipt().logs
187    }
188
189    /// Consumes the type and returns the logs.
190    pub fn into_logs(self) -> Vec<T> {
191        self.into_receipt().logs
192    }
193
194    /// Return the receipt's bloom.
195    pub const fn logs_bloom(&self) -> &Bloom {
196        match self {
197            Self::Legacy(t) => &t.logs_bloom,
198            Self::Eip2930(t) => &t.logs_bloom,
199            Self::Eip1559(t) => &t.logs_bloom,
200            Self::Eip4844(t) => &t.logs_bloom,
201            Self::Eip7702(t) => &t.logs_bloom,
202            #[cfg(feature = "optimism")]
203            Self::PostExec(t) => &t.logs_bloom,
204            #[cfg(feature = "optimism")]
205            Self::Deposit(t) => &t.logs_bloom,
206            Self::Tempo(t) => &t.logs_bloom,
207        }
208    }
209
210    /// Consumes the type and returns the underlying [`Receipt`].
211    pub fn into_receipt(self) -> Receipt<T> {
212        match self {
213            Self::Legacy(t)
214            | Self::Eip2930(t)
215            | Self::Eip1559(t)
216            | Self::Eip4844(t)
217            | Self::Eip7702(t)
218            | Self::Tempo(t) => t.receipt,
219            #[cfg(feature = "optimism")]
220            Self::PostExec(t) => t.receipt,
221            #[cfg(feature = "optimism")]
222            Self::Deposit(t) => t.receipt.into_inner(),
223        }
224    }
225
226    /// Return the inner receipt.
227    pub const fn as_receipt(&self) -> &Receipt<T> {
228        match self {
229            Self::Legacy(t)
230            | Self::Eip2930(t)
231            | Self::Eip1559(t)
232            | Self::Eip4844(t)
233            | Self::Eip7702(t)
234            | Self::Tempo(t) => &t.receipt,
235            #[cfg(feature = "optimism")]
236            Self::PostExec(t) => &t.receipt,
237            #[cfg(feature = "optimism")]
238            Self::Deposit(t) => &t.receipt.inner,
239        }
240    }
241}
242
243impl<T> TxReceipt for FoundryReceiptEnvelope<T>
244where
245    T: Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync,
246{
247    type Log = T;
248
249    fn status_or_post_state(&self) -> Eip658Value {
250        self.as_receipt().status
251    }
252
253    fn status(&self) -> bool {
254        self.status()
255    }
256
257    /// Return the receipt's bloom.
258    fn bloom(&self) -> Bloom {
259        *self.logs_bloom()
260    }
261
262    fn bloom_cheap(&self) -> Option<Bloom> {
263        Some(self.bloom())
264    }
265
266    /// Returns the cumulative gas used at this receipt.
267    fn cumulative_gas_used(&self) -> u64 {
268        self.cumulative_gas_used()
269    }
270
271    /// Return the receipt logs.
272    fn logs(&self) -> &[T] {
273        self.logs()
274    }
275}
276
277impl Encodable for FoundryReceiptEnvelope {
278    fn encode(&self, out: &mut dyn bytes::BufMut) {
279        match self {
280            Self::Legacy(r) => r.encode(out),
281            receipt => {
282                let payload_len = match receipt {
283                    Self::Eip2930(r) => r.length() + 1,
284                    Self::Eip1559(r) => r.length() + 1,
285                    Self::Eip4844(r) => r.length() + 1,
286                    Self::Eip7702(r) => r.length() + 1,
287                    #[cfg(feature = "optimism")]
288                    Self::PostExec(r) => r.length() + 1,
289                    #[cfg(feature = "optimism")]
290                    Self::Deposit(r) => r.length() + 1,
291                    Self::Tempo(r) => r.length() + 1,
292                    _ => unreachable!("receipt already matched"),
293                };
294
295                match receipt {
296                    Self::Eip2930(r) => {
297                        Header { list: true, payload_length: payload_len }.encode(out);
298                        EIP2930_TX_TYPE_ID.encode(out);
299                        r.encode(out);
300                    }
301                    Self::Eip1559(r) => {
302                        Header { list: true, payload_length: payload_len }.encode(out);
303                        EIP1559_TX_TYPE_ID.encode(out);
304                        r.encode(out);
305                    }
306                    Self::Eip4844(r) => {
307                        Header { list: true, payload_length: payload_len }.encode(out);
308                        EIP4844_TX_TYPE_ID.encode(out);
309                        r.encode(out);
310                    }
311                    Self::Eip7702(r) => {
312                        Header { list: true, payload_length: payload_len }.encode(out);
313                        EIP7702_TX_TYPE_ID.encode(out);
314                        r.encode(out);
315                    }
316                    #[cfg(feature = "optimism")]
317                    Self::PostExec(r) => {
318                        Header { list: true, payload_length: payload_len }.encode(out);
319                        POST_EXEC_TX_TYPE_ID.encode(out);
320                        r.encode(out);
321                    }
322                    #[cfg(feature = "optimism")]
323                    Self::Deposit(r) => {
324                        Header { list: true, payload_length: payload_len }.encode(out);
325                        DEPOSIT_TX_TYPE_ID.encode(out);
326                        r.encode(out);
327                    }
328                    Self::Tempo(r) => {
329                        Header { list: true, payload_length: payload_len }.encode(out);
330                        TEMPO_TX_TYPE_ID.encode(out);
331                        r.encode(out);
332                    }
333                    _ => unreachable!("receipt already matched"),
334                }
335            }
336        }
337    }
338}
339
340impl Decodable for FoundryReceiptEnvelope {
341    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
342        use bytes::Buf;
343        use std::cmp::Ordering;
344
345        // a receipt is either encoded as a string (non legacy) or a list (legacy).
346        // We should not consume the buffer if we are decoding a legacy receipt, so let's
347        // check if the first byte is between 0x80 and 0xbf.
348        let rlp_type = *buf
349            .first()
350            .ok_or(alloy_rlp::Error::Custom("cannot decode a receipt from empty bytes"))?;
351
352        match rlp_type.cmp(&alloy_rlp::EMPTY_LIST_CODE) {
353            Ordering::Less => {
354                // strip out the string header
355                let _header = Header::decode(buf)?;
356                let receipt_type = *buf.first().ok_or(alloy_rlp::Error::Custom(
357                    "typed receipt cannot be decoded from an empty slice",
358                ))?;
359                if receipt_type == EIP2930_TX_TYPE_ID {
360                    buf.advance(1);
361                    <ReceiptWithBloom as Decodable>::decode(buf)
362                        .map(FoundryReceiptEnvelope::Eip2930)
363                } else if receipt_type == EIP1559_TX_TYPE_ID {
364                    buf.advance(1);
365                    <ReceiptWithBloom as Decodable>::decode(buf)
366                        .map(FoundryReceiptEnvelope::Eip1559)
367                } else if receipt_type == EIP4844_TX_TYPE_ID {
368                    buf.advance(1);
369                    <ReceiptWithBloom as Decodable>::decode(buf)
370                        .map(FoundryReceiptEnvelope::Eip4844)
371                } else if receipt_type == EIP7702_TX_TYPE_ID {
372                    buf.advance(1);
373                    <ReceiptWithBloom as Decodable>::decode(buf)
374                        .map(FoundryReceiptEnvelope::Eip7702)
375                } else if receipt_type == TEMPO_TX_TYPE_ID {
376                    buf.advance(1);
377                    <ReceiptWithBloom as Decodable>::decode(buf).map(FoundryReceiptEnvelope::Tempo)
378                } else {
379                    #[cfg(feature = "optimism")]
380                    {
381                        if receipt_type == POST_EXEC_TX_TYPE_ID {
382                            buf.advance(1);
383                            return <ReceiptWithBloom as Decodable>::decode(buf)
384                                .map(FoundryReceiptEnvelope::PostExec);
385                        }
386                        if receipt_type == DEPOSIT_TX_TYPE_ID {
387                            buf.advance(1);
388                            return <OpDepositReceiptWithBloom as Decodable>::decode(buf)
389                                .map(FoundryReceiptEnvelope::Deposit);
390                        }
391                    }
392                    Err(alloy_rlp::Error::Custom("invalid receipt type"))
393                }
394            }
395            Ordering::Equal => {
396                Err(alloy_rlp::Error::Custom("an empty list is not a valid receipt encoding"))
397            }
398            Ordering::Greater => {
399                <ReceiptWithBloom as Decodable>::decode(buf).map(FoundryReceiptEnvelope::Legacy)
400            }
401        }
402    }
403}
404
405impl Typed2718 for FoundryReceiptEnvelope {
406    fn ty(&self) -> u8 {
407        match self {
408            Self::Legacy(_) => LEGACY_TX_TYPE_ID,
409            Self::Eip2930(_) => EIP2930_TX_TYPE_ID,
410            Self::Eip1559(_) => EIP1559_TX_TYPE_ID,
411            Self::Eip4844(_) => EIP4844_TX_TYPE_ID,
412            Self::Eip7702(_) => EIP7702_TX_TYPE_ID,
413            #[cfg(feature = "optimism")]
414            Self::PostExec(_) => POST_EXEC_TX_TYPE_ID,
415            #[cfg(feature = "optimism")]
416            Self::Deposit(_) => DEPOSIT_TX_TYPE_ID,
417            Self::Tempo(_) => TEMPO_TX_TYPE_ID,
418        }
419    }
420}
421
422impl Encodable2718 for FoundryReceiptEnvelope {
423    fn encode_2718_len(&self) -> usize {
424        match self {
425            Self::Legacy(r) => r.length(),
426            Self::Eip2930(r) => 1 + r.length(),
427            Self::Eip1559(r) => 1 + r.length(),
428            Self::Eip4844(r) => 1 + r.length(),
429            Self::Eip7702(r) => 1 + r.length(),
430            #[cfg(feature = "optimism")]
431            Self::PostExec(r) => 1 + r.length(),
432            #[cfg(feature = "optimism")]
433            Self::Deposit(r) => 1 + r.length(),
434            Self::Tempo(r) => 1 + r.length(),
435        }
436    }
437
438    fn encode_2718(&self, out: &mut dyn BufMut) {
439        if let Some(ty) = self.type_flag() {
440            out.put_u8(ty);
441        }
442        match self {
443            Self::Legacy(r)
444            | Self::Eip2930(r)
445            | Self::Eip1559(r)
446            | Self::Eip4844(r)
447            | Self::Eip7702(r)
448            | Self::Tempo(r) => r.encode(out),
449            #[cfg(feature = "optimism")]
450            Self::PostExec(r) => r.encode(out),
451            #[cfg(feature = "optimism")]
452            Self::Deposit(r) => r.encode(out),
453        }
454    }
455}
456
457impl Decodable2718 for FoundryReceiptEnvelope {
458    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
459        #[cfg(feature = "optimism")]
460        {
461            if ty == DEPOSIT_TX_TYPE_ID {
462                return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?));
463            }
464            if ty == POST_EXEC_TX_TYPE_ID {
465                return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?));
466            }
467        }
468        if ty == TEMPO_TX_TYPE_ID {
469            return Ok(Self::Tempo(ReceiptWithBloom::decode(buf)?));
470        }
471        match ReceiptEnvelope::typed_decode(ty, buf)? {
472            ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
473            ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
474            ReceiptEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
475            ReceiptEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
476            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
477        }
478    }
479
480    fn fallback_decode(buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
481        match ReceiptEnvelope::fallback_decode(buf)? {
482            ReceiptEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
483            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
484        }
485    }
486}
487
488impl From<FoundryReceiptEnvelope<alloy_rpc_types::Log>> for OtsReceipt {
489    fn from(receipt: FoundryReceiptEnvelope<alloy_rpc_types::Log>) -> Self {
490        Self {
491            status: receipt.status(),
492            cumulative_gas_used: receipt.cumulative_gas_used(),
493            logs: Some(receipt.logs().to_vec()),
494            logs_bloom: Some(receipt.logs_bloom().to_owned()),
495            r#type: receipt.tx_type() as u8,
496        }
497    }
498}
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503    use alloy_primitives::{Address, B256, Bytes, LogData, hex};
504    use std::str::FromStr;
505
506    #[test]
507    fn encode_legacy_receipt() {
508        let expected = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
509
510        let mut data = vec![];
511        let receipt = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
512            receipt: Receipt {
513                status: false.into(),
514                cumulative_gas_used: 0x1,
515                logs: vec![Log {
516                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
517                    data: LogData::new_unchecked(
518                        vec![
519                            B256::from_str(
520                                "000000000000000000000000000000000000000000000000000000000000dead",
521                            )
522                            .unwrap(),
523                            B256::from_str(
524                                "000000000000000000000000000000000000000000000000000000000000beef",
525                            )
526                            .unwrap(),
527                        ],
528                        Bytes::from_str("0100ff").unwrap(),
529                    ),
530                }],
531            },
532            logs_bloom: [0; 256].into(),
533        });
534
535        receipt.encode(&mut data);
536
537        // check that the rlp length equals the length of the expected rlp
538        assert_eq!(receipt.length(), expected.len());
539        assert_eq!(data, expected);
540    }
541
542    #[test]
543    fn decode_legacy_receipt() {
544        let data = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
545
546        let expected = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
547            receipt: Receipt {
548                status: false.into(),
549                cumulative_gas_used: 0x1,
550                logs: vec![Log {
551                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
552                    data: LogData::new_unchecked(
553                        vec![
554                            B256::from_str(
555                                "000000000000000000000000000000000000000000000000000000000000dead",
556                            )
557                            .unwrap(),
558                            B256::from_str(
559                                "000000000000000000000000000000000000000000000000000000000000beef",
560                            )
561                            .unwrap(),
562                        ],
563                        Bytes::from_str("0100ff").unwrap(),
564                    ),
565                }],
566            },
567            logs_bloom: [0; 256].into(),
568        });
569
570        let receipt = FoundryReceiptEnvelope::decode(&mut &data[..]).unwrap();
571
572        assert_eq!(receipt, expected);
573    }
574
575    #[test]
576    fn encode_tempo_receipt() {
577        use alloy_network::eip2718::Encodable2718;
578        use tempo_primitives::TEMPO_TX_TYPE_ID;
579
580        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
581            receipt: Receipt {
582                status: true.into(),
583                cumulative_gas_used: 157716,
584                logs: vec![Log {
585                    address: Address::from_str("20c0000000000000000000000000000000000000").unwrap(),
586                    data: LogData::new_unchecked(
587                        vec![
588                            B256::from_str(
589                                "8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
590                            )
591                            .unwrap(),
592                            B256::from_str(
593                                "000000000000000000000000566ff0f4a6114f8072ecdc8a7a8a13d8d0c6b45f",
594                            )
595                            .unwrap(),
596                            B256::from_str(
597                                "000000000000000000000000dec0000000000000000000000000000000000000",
598                            )
599                            .unwrap(),
600                        ],
601                        Bytes::from_str(
602                            "0000000000000000000000000000000000000000000000000000000000989680",
603                        )
604                        .unwrap(),
605                    ),
606                }],
607            },
608            logs_bloom: [0; 256].into(),
609        });
610
611        assert_eq!(receipt.tx_type(), FoundryTxType::Tempo);
612        assert_eq!(receipt.ty(), TEMPO_TX_TYPE_ID);
613        assert!(receipt.status());
614        assert_eq!(receipt.cumulative_gas_used(), 157716);
615        assert_eq!(receipt.logs().len(), 1);
616
617        // Encode and decode round-trip
618        let mut encoded = Vec::new();
619        receipt.encode_2718(&mut encoded);
620
621        // First byte should be the Tempo type ID
622        assert_eq!(encoded[0], TEMPO_TX_TYPE_ID);
623
624        // Decode it back
625        let decoded = FoundryReceiptEnvelope::decode(&mut &encoded[..]).unwrap();
626        assert_eq!(receipt, decoded);
627    }
628
629    #[test]
630    fn decode_tempo_receipt() {
631        use alloy_network::eip2718::Encodable2718;
632        use tempo_primitives::TEMPO_TX_TYPE_ID;
633
634        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
635            receipt: Receipt { status: true.into(), cumulative_gas_used: 21000, logs: vec![] },
636            logs_bloom: [0; 256].into(),
637        });
638
639        // Encode and decode via 2718
640        let mut encoded = Vec::new();
641        receipt.encode_2718(&mut encoded);
642        assert_eq!(encoded[0], TEMPO_TX_TYPE_ID);
643
644        use alloy_network::eip2718::Decodable2718;
645        let decoded = FoundryReceiptEnvelope::decode_2718(&mut &encoded[..]).unwrap();
646        assert_eq!(receipt, decoded);
647    }
648
649    #[test]
650    fn tempo_receipt_from_parts() {
651        let receipt = FoundryReceiptEnvelope::<alloy_rpc_types::Log>::from_parts(
652            true,
653            100000,
654            vec![],
655            FoundryTxType::Tempo,
656            None,
657            None,
658        );
659
660        assert_eq!(receipt.tx_type(), FoundryTxType::Tempo);
661        assert!(receipt.status());
662        assert_eq!(receipt.cumulative_gas_used(), 100000);
663        assert!(receipt.logs().is_empty());
664        #[cfg(feature = "optimism")]
665        {
666            assert!(receipt.deposit_nonce().is_none());
667            assert!(receipt.deposit_receipt_version().is_none());
668        }
669    }
670
671    #[test]
672    fn tempo_receipt_map_logs() {
673        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
674            receipt: Receipt {
675                status: true.into(),
676                cumulative_gas_used: 21000,
677                logs: vec![Log {
678                    address: Address::from_str("20c0000000000000000000000000000000000000").unwrap(),
679                    data: LogData::new_unchecked(vec![], Bytes::default()),
680                }],
681            },
682            logs_bloom: [0; 256].into(),
683        });
684
685        // Map logs to a different type (just clone in this case)
686        let mapped = receipt.map_logs(|log| log);
687        assert_eq!(mapped.logs().len(), 1);
688        assert_eq!(mapped.tx_type(), FoundryTxType::Tempo);
689    }
690}