Skip to main content

foundry_primitives/transaction/
receipt.rs

1use crate::FoundryTxType;
2use alloy_consensus::{
3    Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom, TxReceipt, Typed2718,
4};
5use alloy_network::{
6    AnyReceiptEnvelope,
7    eip2718::{
8        Decodable2718, EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID,
9        EIP7702_TX_TYPE_ID, Eip2718Error, Encodable2718, LEGACY_TX_TYPE_ID,
10    },
11};
12use alloy_primitives::{Bloom, Log, TxHash, logs_bloom};
13use alloy_rlp::{BufMut, Decodable, Encodable, bytes};
14use alloy_rpc_types::{BlockNumHash, trace::otterscan::OtsReceipt};
15use serde::{Deserialize, Serialize};
16use tempo_primitives::TEMPO_TX_TYPE_ID;
17
18#[cfg(all(feature = "base", not(feature = "optimism")))]
19use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom};
20
21#[cfg(feature = "base")]
22use base_common_consensus::Eip8130Receipt;
23#[cfg(feature = "base")]
24use base_common_evm::EIP8130_TRANSACTION_TYPE;
25
26#[cfg(feature = "optimism")]
27use op_alloy_consensus::{
28    DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom, POST_EXEC_TX_TYPE_ID,
29};
30
31#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(tag = "type")]
33pub enum FoundryReceiptEnvelope<T = Log> {
34    #[serde(rename = "0x0", alias = "0x00")]
35    Legacy(ReceiptWithBloom<Receipt<T>>),
36    #[serde(rename = "0x1", alias = "0x01")]
37    Eip2930(ReceiptWithBloom<Receipt<T>>),
38    #[serde(rename = "0x2", alias = "0x02")]
39    Eip1559(ReceiptWithBloom<Receipt<T>>),
40    #[serde(rename = "0x3", alias = "0x03")]
41    Eip4844(ReceiptWithBloom<Receipt<T>>),
42    #[serde(rename = "0x4", alias = "0x04")]
43    Eip7702(ReceiptWithBloom<Receipt<T>>),
44    #[cfg(feature = "optimism")]
45    #[serde(rename = "0x7D", alias = "0x7d")]
46    PostExec(ReceiptWithBloom<Receipt<T>>),
47    #[cfg(any(feature = "base", feature = "optimism"))]
48    #[serde(rename = "0x7E", alias = "0x7e")]
49    Deposit(OpDepositReceiptWithBloom<T>),
50    #[cfg(feature = "base")]
51    #[serde(rename = "0x79")]
52    Eip8130(ReceiptWithBloom<Eip8130Receipt<T>>),
53    #[serde(rename = "0x76")]
54    Tempo(ReceiptWithBloom<Receipt<T>>),
55    /// A receipt with a transaction type Foundry does not model.
56    ///
57    /// Anvil cannot execute these transactions, but it must still relay receipts fetched from a
58    /// forked chain that mints them, for example Arbitrum Nitro's `0x64`-`0x6a` types. The type
59    /// byte is preserved so the receipt round-trips through RPC and RLP unchanged.
60    #[serde(untagged)]
61    Unknown(AnyReceiptEnvelope<T>),
62}
63
64impl FoundryReceiptEnvelope<alloy_rpc_types::Log> {
65    /// Creates a new [`FoundryReceiptEnvelope`] from the given parts.
66    pub fn from_parts(
67        status: bool,
68        cumulative_gas_used: u64,
69        logs: impl IntoIterator<Item = alloy_rpc_types::Log>,
70        tx_type: FoundryTxType,
71        #[cfg(feature = "base")] eip8130_phase_statuses: Vec<u8>,
72        #[cfg_attr(not(any(feature = "base", feature = "optimism")), allow(unused_variables))]
73        deposit_nonce: Option<u64>,
74        #[cfg_attr(not(any(feature = "base", feature = "optimism")), allow(unused_variables))]
75        deposit_receipt_version: Option<u64>,
76    ) -> Self {
77        let logs = logs.into_iter().collect::<Vec<_>>();
78        let logs_bloom = logs_bloom(logs.iter().map(|l| &l.inner));
79        let inner_receipt =
80            Receipt { status: Eip658Value::Eip658(status), cumulative_gas_used, logs };
81        match tx_type {
82            FoundryTxType::Legacy => {
83                Self::Legacy(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
84            }
85            FoundryTxType::Eip2930 => {
86                Self::Eip2930(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
87            }
88            FoundryTxType::Eip1559 => {
89                Self::Eip1559(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
90            }
91            FoundryTxType::Eip4844 => {
92                Self::Eip4844(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
93            }
94            FoundryTxType::Eip7702 => {
95                Self::Eip7702(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
96            }
97            #[cfg(feature = "optimism")]
98            FoundryTxType::PostExec => {
99                Self::PostExec(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
100            }
101            #[cfg(any(feature = "base", feature = "optimism"))]
102            FoundryTxType::Deposit => {
103                let inner = OpDepositReceiptWithBloom {
104                    receipt: OpDepositReceipt {
105                        inner: inner_receipt,
106                        deposit_nonce,
107                        deposit_receipt_version,
108                    },
109                    logs_bloom,
110                };
111                Self::Deposit(inner)
112            }
113            #[cfg(feature = "base")]
114            FoundryTxType::Eip8130 => Self::Eip8130(ReceiptWithBloom {
115                receipt: Eip8130Receipt::new(inner_receipt, eip8130_phase_statuses),
116                logs_bloom,
117            }),
118            FoundryTxType::Celo => Self::Unknown(AnyReceiptEnvelope {
119                inner: ReceiptWithBloom { receipt: inner_receipt, logs_bloom },
120                r#type: super::CIP64_TX_TYPE,
121            }),
122            FoundryTxType::Tempo => {
123                Self::Tempo(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
124            }
125        }
126    }
127}
128
129impl FoundryReceiptEnvelope<Log> {
130    pub fn convert_logs_rpc(
131        self,
132        block_numhash: BlockNumHash,
133        block_timestamp: u64,
134        transaction_hash: TxHash,
135        transaction_index: u64,
136        next_log_index: usize,
137    ) -> FoundryReceiptEnvelope<alloy_rpc_types::Log> {
138        let mut index = 0;
139        self.map_logs(|inner| {
140            let log = alloy_rpc_types::Log {
141                inner,
142                block_hash: Some(block_numhash.hash),
143                block_number: Some(block_numhash.number),
144                block_timestamp: Some(block_timestamp),
145                transaction_hash: Some(transaction_hash),
146                transaction_index: Some(transaction_index),
147                log_index: Some((next_log_index + index) as u64),
148                removed: false,
149            };
150            index += 1;
151            log
152        })
153    }
154}
155
156impl<T> FoundryReceiptEnvelope<T> {
157    /// Returns `true` if this is an OP stack deposit receipt.
158    #[cfg(any(feature = "base", feature = "optimism"))]
159    pub const fn is_deposit(&self) -> bool {
160        matches!(self, Self::Deposit(_))
161    }
162
163    /// Returns `true` if this is an OP stack post-execution synthetic receipt.
164    #[cfg(feature = "optimism")]
165    pub const fn is_post_exec(&self) -> bool {
166        matches!(self, Self::PostExec(_))
167    }
168
169    /// Returns `true` if this is a Base EIP-8130 receipt.
170    #[cfg(feature = "base")]
171    pub const fn is_eip8130(&self) -> bool {
172        matches!(self, Self::Eip8130(_))
173    }
174
175    /// Returns EIP-8130 per-phase statuses, or an empty slice for other receipt types.
176    #[cfg(feature = "base")]
177    pub fn eip8130_phase_statuses(&self) -> &[u8] {
178        match self {
179            Self::Eip8130(receipt) => &receipt.receipt.phase_statuses,
180            _ => &[],
181        }
182    }
183
184    /// Returns `true` if this is a Tempo receipt.
185    pub const fn is_tempo(&self) -> bool {
186        matches!(self, Self::Tempo(_))
187    }
188
189    /// Returns `true` if the receipt's transaction type is not modelled by Foundry.
190    pub const fn is_unknown(&self) -> bool {
191        matches!(self, Self::Unknown(_))
192    }
193
194    /// Returns the EIP-2718 type byte of the inner receipt.
195    pub const fn ty(&self) -> u8 {
196        match self {
197            Self::Legacy(_) => LEGACY_TX_TYPE_ID,
198            Self::Eip2930(_) => EIP2930_TX_TYPE_ID,
199            Self::Eip1559(_) => EIP1559_TX_TYPE_ID,
200            Self::Eip4844(_) => EIP4844_TX_TYPE_ID,
201            Self::Eip7702(_) => EIP7702_TX_TYPE_ID,
202            #[cfg(feature = "optimism")]
203            Self::PostExec(_) => POST_EXEC_TX_TYPE_ID,
204            #[cfg(any(feature = "base", feature = "optimism"))]
205            Self::Deposit(_) => DEPOSIT_TX_TYPE_ID,
206            #[cfg(feature = "base")]
207            Self::Eip8130(_) => EIP8130_TRANSACTION_TYPE,
208            Self::Tempo(_) => TEMPO_TX_TYPE_ID,
209            Self::Unknown(r) => r.r#type,
210        }
211    }
212
213    /// Return the [`FoundryTxType`] of the inner receipt, or `None` for an unknown type.
214    pub const fn tx_type(&self) -> Option<FoundryTxType> {
215        Some(match self {
216            Self::Legacy(_) => FoundryTxType::Legacy,
217            Self::Eip2930(_) => FoundryTxType::Eip2930,
218            Self::Eip1559(_) => FoundryTxType::Eip1559,
219            Self::Eip4844(_) => FoundryTxType::Eip4844,
220            Self::Eip7702(_) => FoundryTxType::Eip7702,
221            #[cfg(feature = "optimism")]
222            Self::PostExec(_) => FoundryTxType::PostExec,
223            #[cfg(any(feature = "base", feature = "optimism"))]
224            Self::Deposit(_) => FoundryTxType::Deposit,
225            #[cfg(feature = "base")]
226            Self::Eip8130(_) => FoundryTxType::Eip8130,
227            Self::Tempo(_) => FoundryTxType::Tempo,
228            Self::Unknown(_) => return None,
229        })
230    }
231
232    /// Returns the success status of the receipt's transaction.
233    pub const fn status(&self) -> bool {
234        self.as_receipt().status.coerce_status()
235    }
236
237    /// Returns the cumulative gas used at this receipt.
238    pub const fn cumulative_gas_used(&self) -> u64 {
239        self.as_receipt().cumulative_gas_used
240    }
241
242    /// Converts the receipt's log type by applying a function to each log.
243    ///
244    /// Returns the receipt with the new log type.
245    pub fn map_logs<U>(self, f: impl FnMut(T) -> U) -> FoundryReceiptEnvelope<U> {
246        match self {
247            Self::Legacy(r) => FoundryReceiptEnvelope::Legacy(r.map_logs(f)),
248            Self::Eip2930(r) => FoundryReceiptEnvelope::Eip2930(r.map_logs(f)),
249            Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)),
250            Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)),
251            Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)),
252            #[cfg(feature = "optimism")]
253            Self::PostExec(r) => FoundryReceiptEnvelope::PostExec(r.map_logs(f)),
254            #[cfg(any(feature = "base", feature = "optimism"))]
255            Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(
256                r.map_receipt(|r: OpDepositReceipt<T>| r.map_logs(f)),
257            ),
258            #[cfg(feature = "base")]
259            Self::Eip8130(r) => {
260                FoundryReceiptEnvelope::Eip8130(r.map_receipt(|r: Eip8130Receipt<T>| r.map_logs(f)))
261            }
262            Self::Tempo(r) => FoundryReceiptEnvelope::Tempo(r.map_logs(f)),
263            Self::Unknown(r) => FoundryReceiptEnvelope::Unknown(AnyReceiptEnvelope {
264                inner: r.inner.map_logs(f),
265                r#type: r.r#type,
266            }),
267        }
268    }
269
270    /// Return the receipt logs.
271    pub fn logs(&self) -> &[T] {
272        &self.as_receipt().logs
273    }
274
275    /// Consumes the type and returns the logs.
276    pub fn into_logs(self) -> Vec<T> {
277        self.into_receipt().logs
278    }
279
280    /// Return the receipt's bloom.
281    pub const fn logs_bloom(&self) -> &Bloom {
282        match self {
283            Self::Legacy(t) => t.bloom_ref(),
284            Self::Eip2930(t) => t.bloom_ref(),
285            Self::Eip1559(t) => t.bloom_ref(),
286            Self::Eip4844(t) => t.bloom_ref(),
287            Self::Eip7702(t) => t.bloom_ref(),
288            #[cfg(feature = "optimism")]
289            Self::PostExec(t) => t.bloom_ref(),
290            #[cfg(any(feature = "base", feature = "optimism"))]
291            Self::Deposit(t) => t.bloom_ref(),
292            #[cfg(feature = "base")]
293            Self::Eip8130(t) => t.bloom_ref(),
294            Self::Tempo(t) => t.bloom_ref(),
295            Self::Unknown(t) => &t.inner.logs_bloom,
296        }
297    }
298
299    /// Consumes the type and returns the underlying [`Receipt`].
300    pub fn into_receipt(self) -> Receipt<T> {
301        match self {
302            Self::Legacy(t)
303            | Self::Eip2930(t)
304            | Self::Eip1559(t)
305            | Self::Eip4844(t)
306            | Self::Eip7702(t)
307            | Self::Tempo(t) => t.receipt,
308            #[cfg(feature = "optimism")]
309            Self::PostExec(t) => t.receipt,
310            #[cfg(any(feature = "base", feature = "optimism"))]
311            Self::Deposit(t) => t.receipt.into_inner(),
312            #[cfg(feature = "base")]
313            Self::Eip8130(t) => t.receipt.into_inner(),
314            Self::Unknown(t) => t.inner.receipt,
315        }
316    }
317
318    /// Return the inner receipt.
319    pub const fn as_receipt(&self) -> &Receipt<T> {
320        match self {
321            Self::Legacy(t)
322            | Self::Eip2930(t)
323            | Self::Eip1559(t)
324            | Self::Eip4844(t)
325            | Self::Eip7702(t)
326            | Self::Tempo(t) => &t.receipt,
327            #[cfg(feature = "optimism")]
328            Self::PostExec(t) => &t.receipt,
329            #[cfg(any(feature = "base", feature = "optimism"))]
330            Self::Deposit(t) => &t.receipt.inner,
331            #[cfg(feature = "base")]
332            Self::Eip8130(t) => &t.receipt.inner,
333            Self::Unknown(t) => &t.inner.receipt,
334        }
335    }
336}
337
338impl<T> TxReceipt for FoundryReceiptEnvelope<T>
339where
340    T: Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync,
341{
342    type Log = T;
343
344    fn status_or_post_state(&self) -> Eip658Value {
345        self.as_receipt().status
346    }
347
348    fn status(&self) -> bool {
349        self.status()
350    }
351
352    /// Return the receipt's bloom.
353    fn bloom(&self) -> Bloom {
354        *self.logs_bloom()
355    }
356
357    fn bloom_cheap(&self) -> Option<Bloom> {
358        Some(self.bloom())
359    }
360
361    /// Returns the cumulative gas used at this receipt.
362    fn cumulative_gas_used(&self) -> u64 {
363        self.cumulative_gas_used()
364    }
365
366    /// Return the receipt logs.
367    fn logs(&self) -> &[T] {
368        self.logs()
369    }
370}
371
372impl Encodable for FoundryReceiptEnvelope {
373    fn encode(&self, out: &mut dyn bytes::BufMut) {
374        self.network_encode(out);
375    }
376
377    fn length(&self) -> usize {
378        self.network_len()
379    }
380}
381
382impl Decodable for FoundryReceiptEnvelope {
383    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
384        Self::network_decode(buf).map_err(Into::into)
385    }
386}
387
388impl<T> Typed2718 for FoundryReceiptEnvelope<T> {
389    fn ty(&self) -> u8 {
390        self.ty()
391    }
392}
393
394impl Encodable2718 for FoundryReceiptEnvelope {
395    fn encode_2718_len(&self) -> usize {
396        match self {
397            Self::Legacy(r) => r.length(),
398            Self::Eip2930(r) => 1 + r.length(),
399            Self::Eip1559(r) => 1 + r.length(),
400            Self::Eip4844(r) => 1 + r.length(),
401            Self::Eip7702(r) => 1 + r.length(),
402            #[cfg(feature = "optimism")]
403            Self::PostExec(r) => 1 + r.length(),
404            #[cfg(any(feature = "base", feature = "optimism"))]
405            Self::Deposit(r) => 1 + r.length(),
406            #[cfg(feature = "base")]
407            Self::Eip8130(r) => 1 + r.length(),
408            Self::Tempo(r) => 1 + r.length(),
409            Self::Unknown(r) => r.rlp_payload_length(),
410        }
411    }
412
413    fn encode_2718(&self, out: &mut dyn BufMut) {
414        if let Some(ty) = self.type_flag() {
415            out.put_u8(ty);
416        }
417        match self {
418            Self::Legacy(r)
419            | Self::Eip2930(r)
420            | Self::Eip1559(r)
421            | Self::Eip4844(r)
422            | Self::Eip7702(r)
423            | Self::Tempo(r) => r.encode(out),
424            #[cfg(feature = "optimism")]
425            Self::PostExec(r) => r.encode(out),
426            #[cfg(any(feature = "base", feature = "optimism"))]
427            Self::Deposit(r) => r.encode(out),
428            #[cfg(feature = "base")]
429            Self::Eip8130(r) => r.encode(out),
430            Self::Unknown(r) => r.inner.encode(out),
431        }
432    }
433}
434
435impl Decodable2718 for FoundryReceiptEnvelope {
436    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
437        #[cfg(feature = "base")]
438        if ty == EIP8130_TRANSACTION_TYPE {
439            return Ok(Self::Eip8130(ReceiptWithBloom::decode(buf)?));
440        }
441        #[cfg(all(feature = "base", not(feature = "optimism")))]
442        if ty == DEPOSIT_TX_TYPE_ID {
443            return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?));
444        }
445        #[cfg(feature = "optimism")]
446        {
447            if ty == DEPOSIT_TX_TYPE_ID {
448                return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?));
449            }
450            if ty == POST_EXEC_TX_TYPE_ID {
451                return Ok(Self::PostExec(ReceiptWithBloom::decode(buf)?));
452            }
453        }
454        if ty == TEMPO_TX_TYPE_ID {
455            return Ok(Self::Tempo(ReceiptWithBloom::decode(buf)?));
456        }
457        match ty {
458            LEGACY_TX_TYPE_ID => Err(Eip2718Error::UnexpectedType(LEGACY_TX_TYPE_ID)),
459            EIP2930_TX_TYPE_ID | EIP1559_TX_TYPE_ID | EIP4844_TX_TYPE_ID | EIP7702_TX_TYPE_ID => {
460                match ReceiptEnvelope::typed_decode(ty, buf)? {
461                    ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
462                    ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
463                    ReceiptEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
464                    ReceiptEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
465                    _ => {
466                        Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type")))
467                    }
468                }
469            }
470            // Receipts for transaction types Foundry does not model, such as Arbitrum's, are kept
471            // verbatim so they survive a decode/encode round-trip.
472            _ => Ok(Self::Unknown(AnyReceiptEnvelope {
473                inner: ReceiptWithBloom::decode(buf)?,
474                r#type: ty,
475            })),
476        }
477    }
478
479    fn fallback_decode(buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
480        match ReceiptEnvelope::fallback_decode(buf)? {
481            ReceiptEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
482            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
483        }
484    }
485}
486
487impl From<FoundryReceiptEnvelope<alloy_rpc_types::Log>> for OtsReceipt {
488    fn from(receipt: FoundryReceiptEnvelope<alloy_rpc_types::Log>) -> Self {
489        Self {
490            status: receipt.status(),
491            cumulative_gas_used: receipt.cumulative_gas_used(),
492            logs: Some(receipt.logs().to_vec()),
493            logs_bloom: Some(receipt.logs_bloom().to_owned()),
494            r#type: receipt.ty(),
495        }
496    }
497}
498
499#[cfg(test)]
500mod tests {
501
502    use super::*;
503    use alloy_primitives::{Address, B256, Bytes, LogData, address, b256, hex};
504
505    fn receipt_for(tx_type: FoundryTxType) -> FoundryReceiptEnvelope {
506        FoundryReceiptEnvelope::<alloy_rpc_types::Log>::from_parts(
507            true,
508            0,
509            Vec::new(),
510            tx_type,
511            #[cfg(feature = "base")]
512            Vec::new(),
513            None,
514            None,
515        )
516        .map_logs(|log| log.inner)
517    }
518
519    #[test]
520    fn rlp_roundtrip() {
521        fn assert_roundtrip(receipt: FoundryReceiptEnvelope) {
522            let mut encoded = Vec::new();
523            receipt.encode(&mut encoded);
524            assert_eq!(encoded.len(), receipt.length());
525
526            let mut encoded = encoded.as_slice();
527            let decoded = FoundryReceiptEnvelope::decode(&mut encoded).unwrap();
528            assert_eq!(decoded, receipt);
529            assert!(encoded.is_empty());
530        }
531
532        for tx_type in [
533            FoundryTxType::Legacy,
534            FoundryTxType::Eip2930,
535            FoundryTxType::Eip1559,
536            FoundryTxType::Eip4844,
537            FoundryTxType::Eip7702,
538            FoundryTxType::Tempo,
539            #[cfg(feature = "base")]
540            FoundryTxType::Eip8130,
541            #[cfg(any(feature = "base", feature = "optimism"))]
542            FoundryTxType::Deposit,
543        ] {
544            assert_roundtrip(receipt_for(tx_type));
545        }
546        #[cfg(feature = "optimism")]
547        assert_roundtrip(receipt_for(FoundryTxType::PostExec));
548
549        // Varied payload so encodings differ beyond the type byte.
550        let logs = vec![Log {
551            address: Address::with_last_byte(0x11),
552            data: LogData::new_unchecked(
553                vec![B256::repeat_byte(0x22)],
554                Bytes::from_static(&[0x01, 0x02, 0x03]),
555            ),
556        }];
557        let logs_bloom = logs_bloom(&logs);
558        let receipt = Receipt { status: false.into(), cumulative_gas_used: 0x2a, logs };
559        assert_roundtrip(FoundryReceiptEnvelope::Eip1559(ReceiptWithBloom {
560            receipt: receipt.clone(),
561            logs_bloom,
562        }));
563        // A deposit receipt with set deposit fields; op-alloy encodes them only when `Some`, so
564        // this catches decode paths that drop them.
565        #[cfg(any(feature = "base", feature = "optimism"))]
566        assert_roundtrip(FoundryReceiptEnvelope::Deposit(OpDepositReceiptWithBloom {
567            receipt: OpDepositReceipt {
568                inner: receipt,
569                deposit_nonce: Some(7),
570                deposit_receipt_version: Some(1),
571            },
572            logs_bloom,
573        }));
574    }
575
576    #[test]
577    fn encode_typed_receipt_uses_rlp_string() {
578        let receipt = receipt_for(FoundryTxType::Eip2930);
579        let mut encoded = Vec::new();
580        receipt.encode(&mut encoded);
581
582        // Long string containing a 266-byte EIP-2718 envelope, beginning with type 0x01.
583        assert_eq!(&encoded[..4], &[0xb9, 0x01, 0x0a, EIP2930_TX_TYPE_ID]);
584    }
585
586    /// Arbitrum's `ArbitrumInternalTx`; anvil forks these chains but cannot execute their
587    /// transactions, so their receipts must survive as-is.
588    const ARBITRUM_INTERNAL_TX_TYPE: u8 = 0x6a;
589
590    fn unknown_receipt(ty: u8) -> FoundryReceiptEnvelope {
591        let logs = vec![Log {
592            address: Address::with_last_byte(0x64),
593            data: LogData::new_unchecked(
594                vec![B256::repeat_byte(0x33)],
595                Bytes::from_static(&[0xaa, 0xbb]),
596            ),
597        }];
598        let logs_bloom = logs_bloom(&logs);
599        FoundryReceiptEnvelope::Unknown(AnyReceiptEnvelope {
600            inner: ReceiptWithBloom {
601                receipt: Receipt { status: true.into(), cumulative_gas_used: 0x1234, logs },
602                logs_bloom,
603            },
604            r#type: ty,
605        })
606    }
607
608    #[test]
609    fn unknown_receipt_roundtrips_and_keeps_type() {
610        let receipt = unknown_receipt(ARBITRUM_INTERNAL_TX_TYPE);
611
612        assert!(receipt.is_unknown());
613        assert_eq!(receipt.tx_type(), None);
614        assert_eq!(receipt.ty(), ARBITRUM_INTERNAL_TX_TYPE);
615        assert!(receipt.status());
616        assert_eq!(receipt.cumulative_gas_used(), 0x1234);
617        assert_eq!(receipt.logs().len(), 1);
618
619        let mut encoded_2718 = Vec::new();
620        receipt.encode_2718(&mut encoded_2718);
621        assert_eq!(encoded_2718[0], ARBITRUM_INTERNAL_TX_TYPE);
622        assert_eq!(encoded_2718.len(), receipt.encode_2718_len());
623        assert_eq!(FoundryReceiptEnvelope::decode_2718(&mut &encoded_2718[..]).unwrap(), receipt);
624
625        let mut encoded = Vec::new();
626        receipt.encode(&mut encoded);
627        assert_eq!(encoded.len(), receipt.length());
628        assert_eq!(FoundryReceiptEnvelope::decode(&mut &encoded[..]).unwrap(), receipt);
629    }
630
631    #[test]
632    fn unknown_receipt_serde_roundtrip() {
633        let receipt = unknown_receipt(ARBITRUM_INTERNAL_TX_TYPE);
634        let json = serde_json::to_value(&receipt).unwrap();
635        assert_eq!(json["type"], "0x6a");
636        assert_eq!(serde_json::from_value::<FoundryReceiptEnvelope>(json).unwrap(), receipt);
637
638        // The fallback must not swallow types that have a dedicated variant.
639        let known = receipt_for(FoundryTxType::Eip1559);
640        let json = serde_json::to_value(&known).unwrap();
641        let decoded = serde_json::from_value::<FoundryReceiptEnvelope>(json).unwrap();
642        assert_eq!(decoded, known);
643        assert!(!decoded.is_unknown());
644    }
645
646    #[test]
647    fn receipt_predicates() {
648        assert!(receipt_for(FoundryTxType::Legacy).is_legacy());
649        assert!(receipt_for(FoundryTxType::Eip2930).is_eip2930());
650        assert!(receipt_for(FoundryTxType::Eip1559).is_eip1559());
651        assert!(receipt_for(FoundryTxType::Eip4844).is_eip4844());
652        assert!(receipt_for(FoundryTxType::Eip7702).is_eip7702());
653        assert!(receipt_for(FoundryTxType::Tempo).is_tempo());
654        assert!(!receipt_for(FoundryTxType::Tempo).is_legacy());
655
656        #[cfg(any(feature = "base", feature = "optimism"))]
657        assert!(receipt_for(FoundryTxType::Deposit).is_deposit());
658        #[cfg(feature = "base")]
659        assert!(receipt_for(FoundryTxType::Eip8130).is_eip8130());
660        #[cfg(feature = "optimism")]
661        {
662            assert!(receipt_for(FoundryTxType::PostExec).is_post_exec());
663        }
664    }
665
666    #[cfg(feature = "base")]
667    #[test]
668    fn eip8130_receipt_preserves_phase_statuses_outside_consensus_encoding() {
669        let receipt = FoundryReceiptEnvelope::<alloy_rpc_types::Log>::from_parts(
670            false,
671            42_000,
672            Vec::new(),
673            FoundryTxType::Eip8130,
674            vec![0x01, 0x00],
675            None,
676            None,
677        )
678        .map_logs(|log| log.inner);
679        assert_eq!(receipt.eip8130_phase_statuses(), &[0x01, 0x00]);
680
681        let encoded = receipt.encoded_2718();
682        let decoded = FoundryReceiptEnvelope::decode_2718(&mut encoded.as_slice()).unwrap();
683        assert!(decoded.eip8130_phase_statuses().is_empty());
684        assert_eq!(decoded.encoded_2718(), encoded);
685    }
686
687    #[test]
688    fn encode_legacy_receipt() {
689        let expected = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
690
691        let mut data = vec![];
692        let receipt = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
693            receipt: Receipt {
694                status: false.into(),
695                cumulative_gas_used: 0x1,
696                logs: vec![Log {
697                    address: Address::with_last_byte(0x11),
698                    data: LogData::new_unchecked(
699                        vec![
700                            b256!(
701                                "000000000000000000000000000000000000000000000000000000000000dead"
702                            ),
703                            b256!(
704                                "000000000000000000000000000000000000000000000000000000000000beef"
705                            ),
706                        ],
707                        alloy_primitives::bytes!("0100ff"),
708                    ),
709                }],
710            },
711            logs_bloom: Bloom::ZERO,
712        });
713
714        receipt.encode(&mut data);
715
716        // check that the rlp length equals the length of the expected rlp
717        assert_eq!(receipt.length(), expected.len());
718        assert_eq!(data, expected);
719    }
720
721    #[test]
722    fn decode_legacy_receipt() {
723        let data = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
724
725        let expected = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
726            receipt: Receipt {
727                status: false.into(),
728                cumulative_gas_used: 0x1,
729                logs: vec![Log {
730                    address: Address::with_last_byte(0x11),
731                    data: LogData::new_unchecked(
732                        vec![
733                            b256!(
734                                "000000000000000000000000000000000000000000000000000000000000dead"
735                            ),
736                            b256!(
737                                "000000000000000000000000000000000000000000000000000000000000beef"
738                            ),
739                        ],
740                        alloy_primitives::bytes!("0100ff"),
741                    ),
742                }],
743            },
744            logs_bloom: Bloom::ZERO,
745        });
746
747        let receipt = FoundryReceiptEnvelope::decode(&mut &data[..]).unwrap();
748
749        assert_eq!(receipt, expected);
750    }
751
752    #[test]
753    fn encode_tempo_receipt() {
754        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
755            receipt: Receipt {
756                status: true.into(),
757                cumulative_gas_used: 157716,
758                logs: vec![Log {
759                    address: address!("20c0000000000000000000000000000000000000"),
760                    data: LogData::new_unchecked(
761                        vec![
762                            b256!(
763                                "8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
764                            ),
765                            b256!(
766                                "000000000000000000000000566ff0f4a6114f8072ecdc8a7a8a13d8d0c6b45f"
767                            ),
768                            b256!(
769                                "000000000000000000000000dec0000000000000000000000000000000000000"
770                            ),
771                        ],
772                        alloy_primitives::bytes!(
773                            "0000000000000000000000000000000000000000000000000000000000989680"
774                        ),
775                    ),
776                }],
777            },
778            logs_bloom: Bloom::ZERO,
779        });
780
781        assert_eq!(receipt.tx_type(), Some(FoundryTxType::Tempo));
782        assert_eq!(receipt.ty(), TEMPO_TX_TYPE_ID);
783        assert!(receipt.status());
784        assert_eq!(receipt.cumulative_gas_used(), 157716);
785        assert_eq!(receipt.logs().len(), 1);
786
787        // The EIP-2718 encoding starts with the Tempo type byte.
788        let mut encoded_2718 = Vec::new();
789        receipt.encode_2718(&mut encoded_2718);
790        assert_eq!(encoded_2718[0], TEMPO_TX_TYPE_ID);
791
792        // `Decodable` expects the network format, which wraps the typed payload in an RLP string.
793        let mut encoded = Vec::new();
794        receipt.encode(&mut encoded);
795        let decoded = FoundryReceiptEnvelope::decode(&mut &encoded[..]).unwrap();
796        assert_eq!(receipt, decoded);
797    }
798
799    #[test]
800    fn decode_tempo_receipt() {
801        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
802            receipt: Receipt { status: true.into(), cumulative_gas_used: 21000, logs: vec![] },
803            logs_bloom: Bloom::ZERO,
804        });
805
806        // Encode and decode via 2718.
807        let mut encoded = Vec::new();
808        receipt.encode_2718(&mut encoded);
809        assert_eq!(encoded[0], TEMPO_TX_TYPE_ID);
810
811        let decoded = FoundryReceiptEnvelope::decode_2718(&mut &encoded[..]).unwrap();
812        assert_eq!(receipt, decoded);
813    }
814
815    #[test]
816    fn tempo_receipt_from_parts() {
817        let receipt = FoundryReceiptEnvelope::<alloy_rpc_types::Log>::from_parts(
818            true,
819            100000,
820            vec![],
821            FoundryTxType::Tempo,
822            #[cfg(feature = "base")]
823            Vec::new(),
824            None,
825            None,
826        );
827
828        assert_eq!(receipt.tx_type(), Some(FoundryTxType::Tempo));
829        assert!(receipt.status());
830        assert_eq!(receipt.cumulative_gas_used(), 100000);
831        assert!(receipt.logs().is_empty());
832        #[cfg(any(feature = "base", feature = "optimism"))]
833        {
834            assert!(receipt.deposit_nonce().is_none());
835            assert!(receipt.deposit_receipt_version().is_none());
836        }
837    }
838
839    #[test]
840    fn tempo_receipt_map_logs() {
841        let receipt = FoundryReceiptEnvelope::Tempo(ReceiptWithBloom {
842            receipt: Receipt {
843                status: true.into(),
844                cumulative_gas_used: 21000,
845                logs: vec![Log {
846                    address: address!("20c0000000000000000000000000000000000000"),
847                    data: LogData::new_unchecked(vec![], Bytes::default()),
848                }],
849            },
850            logs_bloom: Bloom::ZERO,
851        });
852
853        // Map logs to a different type (just clone in this case)
854        let mapped = receipt.map_logs(|log| log);
855        assert_eq!(mapped.logs().len(), 1);
856        assert_eq!(mapped.tx_type(), Some(FoundryTxType::Tempo));
857    }
858}