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