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