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, logs_bloom};
9use alloy_rlp::{BufMut, Decodable, Encodable, Header, bytes};
10use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom};
11use serde::{Deserialize, Serialize};
12
13use crate::FoundryTxType;
14
15#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(tag = "type")]
17pub enum FoundryReceiptEnvelope<T = Log> {
18    #[serde(rename = "0x0", alias = "0x00")]
19    Legacy(ReceiptWithBloom<Receipt<T>>),
20    #[serde(rename = "0x1", alias = "0x01")]
21    Eip2930(ReceiptWithBloom<Receipt<T>>),
22    #[serde(rename = "0x2", alias = "0x02")]
23    Eip1559(ReceiptWithBloom<Receipt<T>>),
24    #[serde(rename = "0x3", alias = "0x03")]
25    Eip4844(ReceiptWithBloom<Receipt<T>>),
26    #[serde(rename = "0x4", alias = "0x04")]
27    Eip7702(ReceiptWithBloom<Receipt<T>>),
28    #[serde(rename = "0x7E", alias = "0x7e")]
29    Deposit(OpDepositReceiptWithBloom<T>),
30}
31
32impl FoundryReceiptEnvelope<Log> {
33    /// Creates a new [`FoundryReceiptEnvelope`] from the given parts.
34    pub fn from_parts<'a>(
35        status: bool,
36        cumulative_gas_used: u64,
37        logs: impl IntoIterator<Item = &'a Log>,
38        tx_type: FoundryTxType,
39        deposit_nonce: Option<u64>,
40        deposit_receipt_version: Option<u64>,
41    ) -> Self {
42        let logs = logs.into_iter().cloned().collect::<Vec<_>>();
43        let logs_bloom = logs_bloom(&logs);
44        let inner_receipt =
45            Receipt { status: Eip658Value::Eip658(status), cumulative_gas_used, logs };
46        match tx_type {
47            FoundryTxType::Legacy => {
48                Self::Legacy(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
49            }
50            FoundryTxType::Eip2930 => {
51                Self::Eip2930(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
52            }
53            FoundryTxType::Eip1559 => {
54                Self::Eip1559(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
55            }
56            FoundryTxType::Eip4844 => {
57                Self::Eip4844(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
58            }
59            FoundryTxType::Eip7702 => {
60                Self::Eip7702(ReceiptWithBloom { receipt: inner_receipt, logs_bloom })
61            }
62            FoundryTxType::Deposit => {
63                let inner = OpDepositReceiptWithBloom {
64                    receipt: OpDepositReceipt {
65                        inner: inner_receipt,
66                        deposit_nonce,
67                        deposit_receipt_version,
68                    },
69                    logs_bloom,
70                };
71                Self::Deposit(inner)
72            }
73        }
74    }
75}
76
77impl<T> FoundryReceiptEnvelope<T> {
78    /// Return the [`FoundryTxType`] of the inner receipt.
79    pub const fn tx_type(&self) -> FoundryTxType {
80        match self {
81            Self::Legacy(_) => FoundryTxType::Legacy,
82            Self::Eip2930(_) => FoundryTxType::Eip2930,
83            Self::Eip1559(_) => FoundryTxType::Eip1559,
84            Self::Eip4844(_) => FoundryTxType::Eip4844,
85            Self::Eip7702(_) => FoundryTxType::Eip7702,
86            Self::Deposit(_) => FoundryTxType::Deposit,
87        }
88    }
89
90    /// Return true if the transaction was successful.
91    pub const fn is_success(&self) -> bool {
92        self.status()
93    }
94
95    /// Returns the success status of the receipt's transaction.
96    pub const fn status(&self) -> bool {
97        self.as_receipt().status.coerce_status()
98    }
99
100    /// Returns the cumulative gas used at this receipt.
101    pub const fn cumulative_gas_used(&self) -> u64 {
102        self.as_receipt().cumulative_gas_used
103    }
104
105    /// Converts the receipt's log type by applying a function to each log.
106    ///
107    /// Returns the receipt with the new log type.
108    pub fn map_logs<U>(self, f: impl FnMut(T) -> U) -> FoundryReceiptEnvelope<U> {
109        match self {
110            Self::Legacy(r) => FoundryReceiptEnvelope::Legacy(r.map_logs(f)),
111            Self::Eip2930(r) => FoundryReceiptEnvelope::Eip2930(r.map_logs(f)),
112            Self::Eip1559(r) => FoundryReceiptEnvelope::Eip1559(r.map_logs(f)),
113            Self::Eip4844(r) => FoundryReceiptEnvelope::Eip4844(r.map_logs(f)),
114            Self::Eip7702(r) => FoundryReceiptEnvelope::Eip7702(r.map_logs(f)),
115            Self::Deposit(r) => FoundryReceiptEnvelope::Deposit(r.map_receipt(|r| r.map_logs(f))),
116        }
117    }
118
119    /// Return the receipt logs.
120    pub fn logs(&self) -> &[T] {
121        &self.as_receipt().logs
122    }
123
124    /// Consumes the type and returns the logs.
125    pub fn into_logs(self) -> Vec<T> {
126        self.into_receipt().logs
127    }
128
129    /// Return the receipt's bloom.
130    pub const fn logs_bloom(&self) -> &Bloom {
131        match self {
132            Self::Legacy(t) => &t.logs_bloom,
133            Self::Eip2930(t) => &t.logs_bloom,
134            Self::Eip1559(t) => &t.logs_bloom,
135            Self::Eip4844(t) => &t.logs_bloom,
136            Self::Eip7702(t) => &t.logs_bloom,
137            Self::Deposit(t) => &t.logs_bloom,
138        }
139    }
140
141    /// Return the receipt's deposit_nonce if it is a deposit receipt.
142    pub fn deposit_nonce(&self) -> Option<u64> {
143        self.as_deposit_receipt().and_then(|r| r.deposit_nonce)
144    }
145
146    /// Return the receipt's deposit version if it is a deposit receipt.
147    pub fn deposit_receipt_version(&self) -> Option<u64> {
148        self.as_deposit_receipt().and_then(|r| r.deposit_receipt_version)
149    }
150
151    /// Returns the deposit receipt if it is a deposit receipt.
152    pub const fn as_deposit_receipt_with_bloom(&self) -> Option<&OpDepositReceiptWithBloom<T>> {
153        match self {
154            Self::Deposit(t) => Some(t),
155            _ => None,
156        }
157    }
158
159    /// Returns the deposit receipt if it is a deposit receipt.
160    pub const fn as_deposit_receipt(&self) -> Option<&OpDepositReceipt<T>> {
161        match self {
162            Self::Deposit(t) => Some(&t.receipt),
163            _ => None,
164        }
165    }
166
167    /// Consumes the type and returns the underlying [`Receipt`].
168    pub fn into_receipt(self) -> Receipt<T> {
169        match self {
170            Self::Legacy(t)
171            | Self::Eip2930(t)
172            | Self::Eip1559(t)
173            | Self::Eip4844(t)
174            | Self::Eip7702(t) => t.receipt,
175            Self::Deposit(t) => t.receipt.into_inner(),
176        }
177    }
178
179    /// Return the inner receipt.
180    pub const fn as_receipt(&self) -> &Receipt<T> {
181        match self {
182            Self::Legacy(t)
183            | Self::Eip2930(t)
184            | Self::Eip1559(t)
185            | Self::Eip4844(t)
186            | Self::Eip7702(t) => &t.receipt,
187            Self::Deposit(t) => &t.receipt.inner,
188        }
189    }
190}
191
192impl FoundryReceiptEnvelope {
193    /// Get the length of the inner receipt in the 2718 encoding.
194    pub fn inner_length(&self) -> usize {
195        match self {
196            Self::Legacy(t) => t.length(),
197            Self::Eip2930(t) => t.length(),
198            Self::Eip1559(t) => t.length(),
199            Self::Eip4844(t) => t.length(),
200            Self::Eip7702(t) => t.length(),
201            Self::Deposit(t) => t.length(),
202        }
203    }
204
205    /// Calculate the length of the rlp payload of the network encoded receipt.
206    pub fn rlp_payload_length(&self) -> usize {
207        let length = self.inner_length();
208        match self {
209            Self::Legacy(_) => length,
210            _ => length + 1,
211        }
212    }
213}
214
215impl<T> TxReceipt for FoundryReceiptEnvelope<T>
216where
217    T: Clone + core::fmt::Debug + PartialEq + Eq + Send + Sync,
218{
219    type Log = T;
220
221    fn status_or_post_state(&self) -> Eip658Value {
222        self.as_receipt().status
223    }
224
225    fn status(&self) -> bool {
226        self.as_receipt().status.coerce_status()
227    }
228
229    /// Return the receipt's bloom.
230    fn bloom(&self) -> Bloom {
231        *self.logs_bloom()
232    }
233
234    fn bloom_cheap(&self) -> Option<Bloom> {
235        Some(self.bloom())
236    }
237
238    /// Returns the cumulative gas used at this receipt.
239    fn cumulative_gas_used(&self) -> u64 {
240        self.as_receipt().cumulative_gas_used
241    }
242
243    /// Return the receipt logs.
244    fn logs(&self) -> &[T] {
245        &self.as_receipt().logs
246    }
247}
248
249impl Encodable for FoundryReceiptEnvelope {
250    fn encode(&self, out: &mut dyn bytes::BufMut) {
251        match self {
252            Self::Legacy(r) => r.encode(out),
253            receipt => {
254                let payload_len = match receipt {
255                    Self::Eip2930(r) => r.length() + 1,
256                    Self::Eip1559(r) => r.length() + 1,
257                    Self::Eip4844(r) => r.length() + 1,
258                    Self::Eip7702(r) => r.length() + 1,
259                    Self::Deposit(r) => r.length() + 1,
260                    _ => unreachable!("receipt already matched"),
261                };
262
263                match receipt {
264                    Self::Eip2930(r) => {
265                        Header { list: true, payload_length: payload_len }.encode(out);
266                        EIP2930_TX_TYPE_ID.encode(out);
267                        r.encode(out);
268                    }
269                    Self::Eip1559(r) => {
270                        Header { list: true, payload_length: payload_len }.encode(out);
271                        EIP1559_TX_TYPE_ID.encode(out);
272                        r.encode(out);
273                    }
274                    Self::Eip4844(r) => {
275                        Header { list: true, payload_length: payload_len }.encode(out);
276                        EIP4844_TX_TYPE_ID.encode(out);
277                        r.encode(out);
278                    }
279                    Self::Eip7702(r) => {
280                        Header { list: true, payload_length: payload_len }.encode(out);
281                        EIP7702_TX_TYPE_ID.encode(out);
282                        r.encode(out);
283                    }
284                    Self::Deposit(r) => {
285                        Header { list: true, payload_length: payload_len }.encode(out);
286                        DEPOSIT_TX_TYPE_ID.encode(out);
287                        r.encode(out);
288                    }
289                    _ => unreachable!("receipt already matched"),
290                }
291            }
292        }
293    }
294}
295
296impl Decodable for FoundryReceiptEnvelope {
297    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
298        use bytes::Buf;
299        use std::cmp::Ordering;
300
301        // a receipt is either encoded as a string (non legacy) or a list (legacy).
302        // We should not consume the buffer if we are decoding a legacy receipt, so let's
303        // check if the first byte is between 0x80 and 0xbf.
304        let rlp_type = *buf
305            .first()
306            .ok_or(alloy_rlp::Error::Custom("cannot decode a receipt from empty bytes"))?;
307
308        match rlp_type.cmp(&alloy_rlp::EMPTY_LIST_CODE) {
309            Ordering::Less => {
310                // strip out the string header
311                let _header = Header::decode(buf)?;
312                let receipt_type = *buf.first().ok_or(alloy_rlp::Error::Custom(
313                    "typed receipt cannot be decoded from an empty slice",
314                ))?;
315                if receipt_type == EIP2930_TX_TYPE_ID {
316                    buf.advance(1);
317                    <ReceiptWithBloom as Decodable>::decode(buf)
318                        .map(FoundryReceiptEnvelope::Eip2930)
319                } else if receipt_type == EIP1559_TX_TYPE_ID {
320                    buf.advance(1);
321                    <ReceiptWithBloom as Decodable>::decode(buf)
322                        .map(FoundryReceiptEnvelope::Eip1559)
323                } else if receipt_type == EIP4844_TX_TYPE_ID {
324                    buf.advance(1);
325                    <ReceiptWithBloom as Decodable>::decode(buf)
326                        .map(FoundryReceiptEnvelope::Eip4844)
327                } else if receipt_type == EIP7702_TX_TYPE_ID {
328                    buf.advance(1);
329                    <ReceiptWithBloom as Decodable>::decode(buf)
330                        .map(FoundryReceiptEnvelope::Eip7702)
331                } else if receipt_type == DEPOSIT_TX_TYPE_ID {
332                    buf.advance(1);
333                    <OpDepositReceiptWithBloom as Decodable>::decode(buf)
334                        .map(FoundryReceiptEnvelope::Deposit)
335                } else {
336                    Err(alloy_rlp::Error::Custom("invalid receipt type"))
337                }
338            }
339            Ordering::Equal => {
340                Err(alloy_rlp::Error::Custom("an empty list is not a valid receipt encoding"))
341            }
342            Ordering::Greater => {
343                <ReceiptWithBloom as Decodable>::decode(buf).map(FoundryReceiptEnvelope::Legacy)
344            }
345        }
346    }
347}
348
349impl Typed2718 for FoundryReceiptEnvelope {
350    fn ty(&self) -> u8 {
351        match self {
352            Self::Legacy(_) => LEGACY_TX_TYPE_ID,
353            Self::Eip2930(_) => EIP2930_TX_TYPE_ID,
354            Self::Eip1559(_) => EIP1559_TX_TYPE_ID,
355            Self::Eip4844(_) => EIP4844_TX_TYPE_ID,
356            Self::Eip7702(_) => EIP7702_TX_TYPE_ID,
357            Self::Deposit(_) => DEPOSIT_TX_TYPE_ID,
358        }
359    }
360}
361
362impl Encodable2718 for FoundryReceiptEnvelope {
363    fn encode_2718_len(&self) -> usize {
364        match self {
365            Self::Legacy(r) => ReceiptEnvelope::Legacy(r.clone()).encode_2718_len(),
366            Self::Eip2930(r) => ReceiptEnvelope::Eip2930(r.clone()).encode_2718_len(),
367            Self::Eip1559(r) => ReceiptEnvelope::Eip1559(r.clone()).encode_2718_len(),
368            Self::Eip4844(r) => ReceiptEnvelope::Eip4844(r.clone()).encode_2718_len(),
369            Self::Eip7702(r) => 1 + r.length(),
370            Self::Deposit(r) => 1 + r.length(),
371        }
372    }
373
374    fn encode_2718(&self, out: &mut dyn BufMut) {
375        if let Some(ty) = self.type_flag() {
376            out.put_u8(ty);
377        }
378        match self {
379            Self::Legacy(r)
380            | Self::Eip2930(r)
381            | Self::Eip1559(r)
382            | Self::Eip4844(r)
383            | Self::Eip7702(r) => r.encode(out),
384            Self::Deposit(r) => r.encode(out),
385        }
386    }
387}
388
389impl Decodable2718 for FoundryReceiptEnvelope {
390    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
391        if ty == 0x7E {
392            return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?));
393        }
394        match ReceiptEnvelope::typed_decode(ty, buf)? {
395            ReceiptEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
396            ReceiptEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
397            ReceiptEnvelope::Eip4844(tx) => Ok(Self::Eip4844(tx)),
398            ReceiptEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
399            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
400        }
401    }
402
403    fn fallback_decode(buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
404        match ReceiptEnvelope::fallback_decode(buf)? {
405            ReceiptEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
406            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use alloy_primitives::{Address, B256, Bytes, LogData, hex};
415    use std::str::FromStr;
416
417    #[test]
418    fn encode_legacy_receipt() {
419        let expected = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
420
421        let mut data = vec![];
422        let receipt = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
423            receipt: Receipt {
424                status: false.into(),
425                cumulative_gas_used: 0x1,
426                logs: vec![Log {
427                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
428                    data: LogData::new_unchecked(
429                        vec![
430                            B256::from_str(
431                                "000000000000000000000000000000000000000000000000000000000000dead",
432                            )
433                            .unwrap(),
434                            B256::from_str(
435                                "000000000000000000000000000000000000000000000000000000000000beef",
436                            )
437                            .unwrap(),
438                        ],
439                        Bytes::from_str("0100ff").unwrap(),
440                    ),
441                }],
442            },
443            logs_bloom: [0; 256].into(),
444        });
445
446        receipt.encode(&mut data);
447
448        // check that the rlp length equals the length of the expected rlp
449        assert_eq!(receipt.length(), expected.len());
450        assert_eq!(data, expected);
451    }
452
453    #[test]
454    fn decode_legacy_receipt() {
455        let data = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
456
457        let expected = FoundryReceiptEnvelope::Legacy(ReceiptWithBloom {
458            receipt: Receipt {
459                status: false.into(),
460                cumulative_gas_used: 0x1,
461                logs: vec![Log {
462                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
463                    data: LogData::new_unchecked(
464                        vec![
465                            B256::from_str(
466                                "000000000000000000000000000000000000000000000000000000000000dead",
467                            )
468                            .unwrap(),
469                            B256::from_str(
470                                "000000000000000000000000000000000000000000000000000000000000beef",
471                            )
472                            .unwrap(),
473                        ],
474                        Bytes::from_str("0100ff").unwrap(),
475                    ),
476                }],
477            },
478            logs_bloom: [0; 256].into(),
479        });
480
481        let receipt = FoundryReceiptEnvelope::decode(&mut &data[..]).unwrap();
482
483        assert_eq!(receipt, expected);
484    }
485}