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, 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 mut index = 0;
107        self.map_logs(|inner| {
108            let log = alloy_rpc_types::Log {
109                inner,
110                block_hash: Some(block_numhash.hash),
111                block_number: Some(block_numhash.number),
112                block_timestamp: Some(block_timestamp),
113                transaction_hash: Some(transaction_hash),
114                transaction_index: Some(transaction_index),
115                log_index: Some((next_log_index + index) as u64),
116                removed: false,
117            };
118            index += 1;
119            log
120        })
121    }
122}
123
124impl<T> FoundryReceiptEnvelope<T> {
125    /// Returns `true` if this is an OP stack deposit receipt.
126    #[cfg(feature = "optimism")]
127    pub const fn is_deposit(&self) -> bool {
128        matches!(self, Self::Deposit(_))
129    }
130
131    /// Returns `true` if this is an OP stack post-execution synthetic receipt.
132    #[cfg(feature = "optimism")]
133    pub const fn is_post_exec(&self) -> bool {
134        matches!(self, Self::PostExec(_))
135    }
136
137    /// Returns `true` if this is a Tempo receipt.
138    pub const fn is_tempo(&self) -> bool {
139        matches!(self, Self::Tempo(_))
140    }
141
142    /// Return the [`FoundryTxType`] of the inner receipt.
143    pub const fn tx_type(&self) -> FoundryTxType {
144        match self {
145            Self::Legacy(_) => FoundryTxType::Legacy,
146            Self::Eip2930(_) => FoundryTxType::Eip2930,
147            Self::Eip1559(_) => FoundryTxType::Eip1559,
148            Self::Eip4844(_) => FoundryTxType::Eip4844,
149            Self::Eip7702(_) => FoundryTxType::Eip7702,
150            #[cfg(feature = "optimism")]
151            Self::PostExec(_) => FoundryTxType::PostExec,
152            #[cfg(feature = "optimism")]
153            Self::Deposit(_) => FoundryTxType::Deposit,
154            Self::Tempo(_) => FoundryTxType::Tempo,
155        }
156    }
157
158    /// Returns the success status of the receipt's transaction.
159    pub const fn status(&self) -> bool {
160        self.as_receipt().status.coerce_status()
161    }
162
163    /// Returns the cumulative gas used at this receipt.
164    pub const fn cumulative_gas_used(&self) -> u64 {
165        self.as_receipt().cumulative_gas_used
166    }
167
168    /// Converts the receipt's log type by applying a function to each log.
169    ///
170    /// Returns the receipt with the new log type.
171    pub fn map_logs<U>(self, f: impl FnMut(T) -> U) -> FoundryReceiptEnvelope<U> {
172        match self {
173            Self::Legacy(r) => FoundryReceiptEnvelope::Legacy(r.map_logs(f)),
174            Self::Eip2930(r) => FoundryReceiptEnvelope::Eip2930(r.map_logs(f)),
175            Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)),
176            Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)),
177            Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)),
178            #[cfg(feature = "optimism")]
179            Self::PostExec(r) => FoundryReceiptEnvelope::PostExec(r.map_logs(f)),
180            #[cfg(feature = "optimism")]
181            Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(
182                r.map_receipt(|r: OpDepositReceipt<T>| r.map_logs(f)),
183            ),
184            Self::Tempo(r) => FoundryReceiptEnvelope::Tempo(r.map_logs(f)),
185        }
186    }
187
188    /// Return the receipt logs.
189    pub fn logs(&self) -> &[T] {
190        &self.as_receipt().logs
191    }
192
193    /// Consumes the type and returns the logs.
194    pub fn into_logs(self) -> Vec<T> {
195        self.into_receipt().logs
196    }
197
198    /// Return the receipt's bloom.
199    pub const fn logs_bloom(&self) -> &Bloom {
200        match self {
201            Self::Legacy(t) => &t.logs_bloom,
202            Self::Eip2930(t) => &t.logs_bloom,
203            Self::Eip1559(t) => &t.logs_bloom,
204            Self::Eip4844(t) => &t.logs_bloom,
205            Self::Eip7702(t) => &t.logs_bloom,
206            #[cfg(feature = "optimism")]
207            Self::PostExec(t) => &t.logs_bloom,
208            #[cfg(feature = "optimism")]
209            Self::Deposit(t) => &t.logs_bloom,
210            Self::Tempo(t) => &t.logs_bloom,
211        }
212    }
213
214    /// Consumes the type and returns the underlying [`Receipt`].
215    pub fn into_receipt(self) -> Receipt<T> {
216        match self {
217            Self::Legacy(t)
218            | Self::Eip2930(t)
219            | Self::Eip1559(t)
220            | Self::Eip4844(t)
221            | Self::Eip7702(t)
222            | Self::Tempo(t) => t.receipt,
223            #[cfg(feature = "optimism")]
224            Self::PostExec(t) => t.receipt,
225            #[cfg(feature = "optimism")]
226            Self::Deposit(t) => t.receipt.into_inner(),
227        }
228    }
229
230    /// Return the inner receipt.
231    pub const fn as_receipt(&self) -> &Receipt<T> {
232        match self {
233            Self::Legacy(t)
234            | Self::Eip2930(t)
235            | Self::Eip1559(t)
236            | Self::Eip4844(t)
237            | Self::Eip7702(t)
238            | Self::Tempo(t) => &t.receipt,
239            #[cfg(feature = "optimism")]
240            Self::PostExec(t) => &t.receipt,
241            #[cfg(feature = "optimism")]
242            Self::Deposit(t) => &t.receipt.inner,
243        }
244    }
245}
246
247impl<T> TxReceipt for FoundryReceiptEnvelope<T>
248where
249    T: Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync,
250{
251    type Log = T;
252
253    fn status_or_post_state(&self) -> Eip658Value {
254        self.as_receipt().status
255    }
256
257    fn status(&self) -> bool {
258        self.status()
259    }
260
261    /// Return the receipt's bloom.
262    fn bloom(&self) -> Bloom {
263        *self.logs_bloom()
264    }
265
266    fn bloom_cheap(&self) -> Option<Bloom> {
267        Some(self.bloom())
268    }
269
270    /// Returns the cumulative gas used at this receipt.
271    fn cumulative_gas_used(&self) -> u64 {
272        self.cumulative_gas_used()
273    }
274
275    /// Return the receipt logs.
276    fn logs(&self) -> &[T] {
277        self.logs()
278    }
279}
280
281impl Encodable for FoundryReceiptEnvelope {
282    fn encode(&self, out: &mut dyn bytes::BufMut) {
283        self.network_encode(out);
284    }
285
286    fn length(&self) -> usize {
287        self.network_len()
288    }
289}
290
291impl Decodable for FoundryReceiptEnvelope {
292    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
293        Self::network_decode(buf).map_err(Into::into)
294    }
295}
296
297impl Typed2718 for FoundryReceiptEnvelope {
298    fn ty(&self) -> u8 {
299        match self {
300            Self::Legacy(_) => LEGACY_TX_TYPE_ID,
301            Self::Eip2930(_) => EIP2930_TX_TYPE_ID,
302            Self::Eip1559(_) => EIP1559_TX_TYPE_ID,
303            Self::Eip4844(_) => EIP4844_TX_TYPE_ID,
304            Self::Eip7702(_) => EIP7702_TX_TYPE_ID,
305            #[cfg(feature = "optimism")]
306            Self::PostExec(_) => POST_EXEC_TX_TYPE_ID,
307            #[cfg(feature = "optimism")]
308            Self::Deposit(_) => DEPOSIT_TX_TYPE_ID,
309            Self::Tempo(_) => TEMPO_TX_TYPE_ID,
310        }
311    }
312}
313
314impl Encodable2718 for FoundryReceiptEnvelope {
315    fn encode_2718_len(&self) -> usize {
316        match self {
317            Self::Legacy(r) => r.length(),
318            Self::Eip2930(r) => 1 + r.length(),
319            Self::Eip1559(r) => 1 + r.length(),
320            Self::Eip4844(r) => 1 + r.length(),
321            Self::Eip7702(r) => 1 + r.length(),
322            #[cfg(feature = "optimism")]
323            Self::PostExec(r) => 1 + r.length(),
324            #[cfg(feature = "optimism")]
325            Self::Deposit(r) => 1 + r.length(),
326            Self::Tempo(r) => 1 + r.length(),
327        }
328    }
329
330    fn encode_2718(&self, out: &mut dyn BufMut) {
331        if let Some(ty) = self.type_flag() {
332            out.put_u8(ty);
333        }
334        match self {
335            Self::Legacy(r)
336            | Self::Eip2930(r)
337            | Self::Eip1559(r)
338            | Self::Eip4844(r)
339            | Self::Eip7702(r)
340            | Self::Tempo(r) => r.encode(out),
341            #[cfg(feature = "optimism")]
342            Self::PostExec(r) => r.encode(out),
343            #[cfg(feature = "optimism")]
344            Self::Deposit(r) => r.encode(out),
345        }
346    }
347}
348
349impl Decodable2718 for FoundryReceiptEnvelope {
350    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
351        #[cfg(feature = "optimism")]
352        {
353            if ty == DEPOSIT_TX_TYPE_ID {
354                return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?));
355            }
356            if ty == POST_EXEC_TX_TYPE_ID {
357                return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?));
358            }
359        }
360        if ty == TEMPO_TX_TYPE_ID {
361            return Ok(Self::Tempo(ReceiptWithBloom::decode(buf)?));
362        }
363        match ReceiptEnvelope::typed_decode(ty, buf)? {
364            ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
365            ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
366            ReceiptEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
367            ReceiptEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
368            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
369        }
370    }
371
372    fn fallback_decode(buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
373        match ReceiptEnvelope::fallback_decode(buf)? {
374            ReceiptEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
375            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
376        }
377    }
378}
379
380impl From<FoundryReceiptEnvelope<alloy_rpc_types::Log>> for OtsReceipt {
381    fn from(receipt: FoundryReceiptEnvelope<alloy_rpc_types::Log>) -> Self {
382        Self {
383            status: receipt.status(),
384            cumulative_gas_used: receipt.cumulative_gas_used(),
385            logs: Some(receipt.logs().to_vec()),
386            logs_bloom: Some(receipt.logs_bloom().to_owned()),
387            r#type: receipt.tx_type() as u8,
388        }
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use alloy_primitives::{Address, B256, Bytes, LogData, hex};
396    use std::str::FromStr;
397
398    fn receipt_for(tx_type: FoundryTxType) -> FoundryReceiptEnvelope {
399        FoundryReceiptEnvelope::<alloy_rpc_types::Log>::from_parts(
400            true,
401            0,
402            Vec::new(),
403            tx_type,
404            None,
405            None,
406        )
407        .map_logs(|log| log.inner)
408    }
409
410    #[test]
411    fn rlp_roundtrip() {
412        fn assert_roundtrip(receipt: FoundryReceiptEnvelope) {
413            let mut encoded = Vec::new();
414            receipt.encode(&mut encoded);
415            assert_eq!(encoded.len(), receipt.length());
416
417            let mut encoded = encoded.as_slice();
418            let decoded = FoundryReceiptEnvelope::decode(&mut encoded).unwrap();
419            assert_eq!(decoded, receipt);
420            assert!(encoded.is_empty());
421        }
422
423        for tx_type in [
424            FoundryTxType::Legacy,
425            FoundryTxType::Eip2930,
426            FoundryTxType::Eip1559,
427            FoundryTxType::Eip4844,
428            FoundryTxType::Eip7702,
429            FoundryTxType::Tempo,
430        ] {
431            assert_roundtrip(receipt_for(tx_type));
432        }
433        #[cfg(feature = "optimism")]
434        for tx_type in [FoundryTxType::PostExec, FoundryTxType::Deposit] {
435            assert_roundtrip(receipt_for(tx_type));
436        }
437
438        // Varied payload so encodings differ beyond the type byte.
439        let logs = vec![Log {
440            address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
441            data: LogData::new_unchecked(
442                vec![B256::repeat_byte(0x22)],
443                Bytes::from_static(&[0x01, 0x02, 0x03]),
444            ),
445        }];
446        let logs_bloom = logs_bloom(&logs);
447        let receipt = Receipt { status: false.into(), cumulative_gas_used: 0x2a, logs };
448        assert_roundtrip(FoundryReceiptEnvelope::Eip1559(ReceiptWithBloom {
449            receipt: receipt.clone(),
450            logs_bloom,
451        }));
452        // A deposit receipt with set deposit fields; op-alloy encodes them only when `Some`, so
453        // this catches decode paths that drop them.
454        #[cfg(feature = "optimism")]
455        assert_roundtrip(FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom {
456            receipt: OpDepositReceipt {
457                inner: receipt,
458                deposit_nonce: Some(7),
459                deposit_receipt_version: Some(1),
460            },
461            logs_bloom,
462        }));
463    }
464
465    #[test]
466    fn encode_typed_receipt_uses_rlp_string() {
467        let receipt = receipt_for(FoundryTxType::Eip2930);
468        let mut encoded = Vec::new();
469        receipt.encode(&mut encoded);
470
471        // Long string containing a 266-byte EIP-2718 envelope, beginning with type 0x01.
472        assert_eq!(&encoded[..4], &[0xb9, 0x01, 0x0a, EIP2930_TX_TYPE_ID]);
473    }
474
475    #[test]
476    fn receipt_predicates() {
477        assert!(receipt_for(FoundryTxType::Legacy).is_legacy());
478        assert!(receipt_for(FoundryTxType::Eip2930).is_eip2930());
479        assert!(receipt_for(FoundryTxType::Eip1559).is_eip1559());
480        assert!(receipt_for(FoundryTxType::Eip4844).is_eip4844());
481        assert!(receipt_for(FoundryTxType::Eip7702).is_eip7702());
482        assert!(receipt_for(FoundryTxType::Tempo).is_tempo());
483        assert!(!receipt_for(FoundryTxType::Tempo).is_legacy());
484
485        #[cfg(feature = "optimism")]
486        {
487            assert!(receipt_for(FoundryTxType::Deposit).is_deposit());
488            assert!(receipt_for(FoundryTxType::PostExec).is_post_exec());
489        }
490    }
491
492    #[test]
493    fn encode_legacy_receipt() {
494        let expected = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
495
496        let mut data = vec![];
497        let receipt = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
498            receipt: Receipt {
499                status: false.into(),
500                cumulative_gas_used: 0x1,
501                logs: vec![Log {
502                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
503                    data: LogData::new_unchecked(
504                        vec![
505                            B256::from_str(
506                                "000000000000000000000000000000000000000000000000000000000000dead",
507                            )
508                            .unwrap(),
509                            B256::from_str(
510                                "000000000000000000000000000000000000000000000000000000000000beef",
511                            )
512                            .unwrap(),
513                        ],
514                        Bytes::from_str("0100ff").unwrap(),
515                    ),
516                }],
517            },
518            logs_bloom: [0; 256].into(),
519        });
520
521        receipt.encode(&mut data);
522
523        // check that the rlp length equals the length of the expected rlp
524        assert_eq!(receipt.length(), expected.len());
525        assert_eq!(data, expected);
526    }
527
528    #[test]
529    fn decode_legacy_receipt() {
530        let data = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
531
532        let expected = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
533            receipt: Receipt {
534                status: false.into(),
535                cumulative_gas_used: 0x1,
536                logs: vec![Log {
537                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
538                    data: LogData::new_unchecked(
539                        vec![
540                            B256::from_str(
541                                "000000000000000000000000000000000000000000000000000000000000dead",
542                            )
543                            .unwrap(),
544                            B256::from_str(
545                                "000000000000000000000000000000000000000000000000000000000000beef",
546                            )
547                            .unwrap(),
548                        ],
549                        Bytes::from_str("0100ff").unwrap(),
550                    ),
551                }],
552            },
553            logs_bloom: [0; 256].into(),
554        });
555
556        let receipt = FoundryReceiptEnvelope::decode(&mut &data[..]).unwrap();
557
558        assert_eq!(receipt, expected);
559    }
560
561    #[test]
562    fn encode_tempo_receipt() {
563        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
564            receipt: Receipt {
565                status: true.into(),
566                cumulative_gas_used: 157716,
567                logs: vec![Log {
568                    address: Address::from_str("20c0000000000000000000000000000000000000").unwrap(),
569                    data: LogData::new_unchecked(
570                        vec![
571                            B256::from_str(
572                                "8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925",
573                            )
574                            .unwrap(),
575                            B256::from_str(
576                                "000000000000000000000000566ff0f4a6114f8072ecdc8a7a8a13d8d0c6b45f",
577                            )
578                            .unwrap(),
579                            B256::from_str(
580                                "000000000000000000000000dec0000000000000000000000000000000000000",
581                            )
582                            .unwrap(),
583                        ],
584                        Bytes::from_str(
585                            "0000000000000000000000000000000000000000000000000000000000989680",
586                        )
587                        .unwrap(),
588                    ),
589                }],
590            },
591            logs_bloom: [0; 256].into(),
592        });
593
594        assert_eq!(receipt.tx_type(), FoundryTxType::Tempo);
595        assert_eq!(receipt.ty(), TEMPO_TX_TYPE_ID);
596        assert!(receipt.status());
597        assert_eq!(receipt.cumulative_gas_used(), 157716);
598        assert_eq!(receipt.logs().len(), 1);
599
600        // The EIP-2718 encoding starts with the Tempo type byte.
601        let mut encoded_2718 = Vec::new();
602        receipt.encode_2718(&mut encoded_2718);
603        assert_eq!(encoded_2718[0], TEMPO_TX_TYPE_ID);
604
605        // `Decodable` expects the network format, which wraps the typed payload in an RLP string.
606        let mut encoded = Vec::new();
607        receipt.encode(&mut encoded);
608        let decoded = FoundryReceiptEnvelope::decode(&mut &encoded[..]).unwrap();
609        assert_eq!(receipt, decoded);
610    }
611
612    #[test]
613    fn decode_tempo_receipt() {
614        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
615            receipt: Receipt { status: true.into(), cumulative_gas_used: 21000, logs: vec![] },
616            logs_bloom: [0; 256].into(),
617        });
618
619        // Encode and decode via 2718.
620        let mut encoded = Vec::new();
621        receipt.encode_2718(&mut encoded);
622        assert_eq!(encoded[0], TEMPO_TX_TYPE_ID);
623
624        let decoded = FoundryReceiptEnvelope::decode_2718(&mut &encoded[..]).unwrap();
625        assert_eq!(receipt, decoded);
626    }
627
628    #[test]
629    fn tempo_receipt_from_parts() {
630        let receipt = FoundryReceiptEnvelope::<alloy_rpc_types::Log>::from_parts(
631            true,
632            100000,
633            vec![],
634            FoundryTxType::Tempo,
635            None,
636            None,
637        );
638
639        assert_eq!(receipt.tx_type(), FoundryTxType::Tempo);
640        assert!(receipt.status());
641        assert_eq!(receipt.cumulative_gas_used(), 100000);
642        assert!(receipt.logs().is_empty());
643        #[cfg(feature = "optimism")]
644        {
645            assert!(receipt.deposit_nonce().is_none());
646            assert!(receipt.deposit_receipt_version().is_none());
647        }
648    }
649
650    #[test]
651    fn tempo_receipt_map_logs() {
652        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
653            receipt: Receipt {
654                status: true.into(),
655                cumulative_gas_used: 21000,
656                logs: vec![Log {
657                    address: Address::from_str("20c0000000000000000000000000000000000000").unwrap(),
658                    data: LogData::new_unchecked(vec![], Bytes::default()),
659                }],
660            },
661            logs_bloom: [0; 256].into(),
662        });
663
664        // Map logs to a different type (just clone in this case)
665        let mapped = receipt.map_logs(|log| log);
666        assert_eq!(mapped.logs().len(), 1);
667        assert_eq!(mapped.tx_type(), FoundryTxType::Tempo);
668    }
669}