anvil_core/eth/transaction/
mod.rs

1//! Transaction related types
2use alloy_consensus::{
3    Receipt, ReceiptEnvelope, ReceiptWithBloom, Signed, Transaction, TxEip1559, TxEip2930,
4    TxEnvelope, TxLegacy, TxReceipt, Typed2718,
5    constants::{
6        EIP1559_TX_TYPE_ID, EIP2930_TX_TYPE_ID, EIP4844_TX_TYPE_ID, EIP7702_TX_TYPE_ID,
7        LEGACY_TX_TYPE_ID,
8    },
9    transaction::{
10        Recovered, TxEip7702,
11        eip4844::{TxEip4844, TxEip4844Variant, TxEip4844WithSidecar},
12    },
13};
14
15use alloy_eips::eip2718::{Decodable2718, Eip2718Error, Encodable2718};
16use alloy_network::{AnyReceiptEnvelope, AnyRpcTransaction, AnyTransactionReceipt, AnyTxEnvelope};
17use alloy_primitives::{Address, B256, Bloom, Bytes, Signature, TxHash, TxKind, U64, U256};
18use alloy_rlp::{Decodable, Encodable, Header};
19use alloy_rpc_types::{
20    AccessList, ConversionError, Transaction as RpcTransaction, TransactionReceipt,
21    request::TransactionRequest, trace::otterscan::OtsReceipt,
22};
23use alloy_serde::{OtherFields, WithOtherFields};
24use bytes::BufMut;
25use foundry_evm::traces::CallTraceNode;
26
27use op_alloy_consensus::{
28    DEPOSIT_TX_TYPE_ID, OpDepositReceipt, OpDepositReceiptWithBloom, TxDeposit,
29};
30use op_revm::{OpTransaction, transaction::deposit::DepositTransactionParts};
31use revm::{context::TxEnv, interpreter::InstructionResult};
32use serde::{Deserialize, Serialize};
33use std::ops::{Deref, Mul};
34
35/// Converts a [TransactionRequest] into a [TypedTransactionRequest].
36/// Should be removed once the call builder abstraction for providers is in place.
37pub fn transaction_request_to_typed(
38    tx: WithOtherFields<TransactionRequest>,
39) -> Option<TypedTransactionRequest> {
40    let WithOtherFields::<TransactionRequest> {
41        inner:
42            TransactionRequest {
43                from,
44                to,
45                gas_price,
46                max_fee_per_gas,
47                max_priority_fee_per_gas,
48                max_fee_per_blob_gas,
49                blob_versioned_hashes,
50                gas,
51                value,
52                input,
53                nonce,
54                access_list,
55                sidecar,
56                transaction_type,
57                authorization_list,
58                chain_id: _,
59            },
60        other,
61    } = tx;
62
63    // Special case: OP-stack deposit tx
64    if transaction_type == Some(0x7E) || has_optimism_fields(&other) {
65        let mint = other.get_deserialized::<U256>("mint")?.map(|m| m.to::<u128>()).ok()?;
66
67        return Some(TypedTransactionRequest::Deposit(TxDeposit {
68            from: from.unwrap_or_default(),
69            source_hash: other.get_deserialized::<B256>("sourceHash")?.ok()?,
70            to: to.unwrap_or_default(),
71            mint,
72            value: value.unwrap_or_default(),
73            gas_limit: gas.unwrap_or_default(),
74            is_system_transaction: other.get_deserialized::<bool>("isSystemTx")?.ok()?,
75            input: input.into_input().unwrap_or_default(),
76        }));
77    }
78
79    // EIP7702
80    if transaction_type == Some(4) || authorization_list.is_some() {
81        return Some(TypedTransactionRequest::EIP7702(TxEip7702 {
82            nonce: nonce.unwrap_or_default(),
83            max_fee_per_gas: max_fee_per_gas.unwrap_or_default(),
84            max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default(),
85            gas_limit: gas.unwrap_or_default(),
86            value: value.unwrap_or(U256::ZERO),
87            input: input.into_input().unwrap_or_default(),
88            // requires to
89            to: to?.into_to()?,
90            chain_id: 0,
91            access_list: access_list.unwrap_or_default(),
92            authorization_list: authorization_list.unwrap_or_default(),
93        }));
94    }
95
96    match (
97        transaction_type,
98        gas_price,
99        max_fee_per_gas,
100        max_priority_fee_per_gas,
101        access_list.as_ref(),
102        max_fee_per_blob_gas,
103        blob_versioned_hashes.as_ref(),
104        sidecar.as_ref(),
105        to,
106    ) {
107        // legacy transaction
108        (Some(0), _, None, None, None, None, None, None, _)
109        | (None, Some(_), None, None, None, None, None, None, _) => {
110            Some(TypedTransactionRequest::Legacy(TxLegacy {
111                nonce: nonce.unwrap_or_default(),
112                gas_price: gas_price.unwrap_or_default(),
113                gas_limit: gas.unwrap_or_default(),
114                value: value.unwrap_or(U256::ZERO),
115                input: input.into_input().unwrap_or_default(),
116                to: to.unwrap_or_default(),
117                chain_id: None,
118            }))
119        }
120        // EIP2930
121        (Some(1), _, None, None, _, None, None, None, _)
122        | (None, _, None, None, Some(_), None, None, None, _) => {
123            Some(TypedTransactionRequest::EIP2930(TxEip2930 {
124                nonce: nonce.unwrap_or_default(),
125                gas_price: gas_price.unwrap_or_default(),
126                gas_limit: gas.unwrap_or_default(),
127                value: value.unwrap_or(U256::ZERO),
128                input: input.into_input().unwrap_or_default(),
129                to: to.unwrap_or_default(),
130                chain_id: 0,
131                access_list: access_list.unwrap_or_default(),
132            }))
133        }
134        // EIP1559
135        (Some(2), None, _, _, _, _, None, None, _)
136        | (None, None, Some(_), _, _, _, None, None, _)
137        | (None, None, _, Some(_), _, _, None, None, _)
138        | (None, None, None, None, None, _, None, None, _) => {
139            // Empty fields fall back to the canonical transaction schema.
140            Some(TypedTransactionRequest::EIP1559(TxEip1559 {
141                nonce: nonce.unwrap_or_default(),
142                max_fee_per_gas: max_fee_per_gas.unwrap_or_default(),
143                max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default(),
144                gas_limit: gas.unwrap_or_default(),
145                value: value.unwrap_or(U256::ZERO),
146                input: input.into_input().unwrap_or_default(),
147                to: to.unwrap_or_default(),
148                chain_id: 0,
149                access_list: access_list.unwrap_or_default(),
150            }))
151        }
152        // EIP4844
153        (Some(3), None, _, _, _, _, Some(_), _, to) => {
154            let tx = TxEip4844 {
155                nonce: nonce.unwrap_or_default(),
156                max_fee_per_gas: max_fee_per_gas.unwrap_or_default(),
157                max_priority_fee_per_gas: max_priority_fee_per_gas.unwrap_or_default(),
158                max_fee_per_blob_gas: max_fee_per_blob_gas.unwrap_or_default(),
159                gas_limit: gas.unwrap_or_default(),
160                value: value.unwrap_or(U256::ZERO),
161                input: input.into_input().unwrap_or_default(),
162                to: match to.unwrap_or(TxKind::Create) {
163                    TxKind::Call(to) => to,
164                    TxKind::Create => Address::ZERO,
165                },
166                chain_id: 0,
167                access_list: access_list.unwrap_or_default(),
168                blob_versioned_hashes: blob_versioned_hashes.unwrap_or_default(),
169            };
170
171            if let Some(sidecar) = sidecar {
172                Some(TypedTransactionRequest::EIP4844(TxEip4844Variant::TxEip4844WithSidecar(
173                    TxEip4844WithSidecar::from_tx_and_sidecar(tx, sidecar),
174                )))
175            } else {
176                Some(TypedTransactionRequest::EIP4844(TxEip4844Variant::TxEip4844(tx)))
177            }
178        }
179        _ => None,
180    }
181}
182
183pub fn has_optimism_fields(other: &OtherFields) -> bool {
184    other.contains_key("sourceHash")
185        && other.contains_key("mint")
186        && other.contains_key("isSystemTx")
187}
188
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub enum TypedTransactionRequest {
191    Legacy(TxLegacy),
192    EIP2930(TxEip2930),
193    EIP1559(TxEip1559),
194    EIP7702(TxEip7702),
195    EIP4844(TxEip4844Variant),
196    Deposit(TxDeposit),
197}
198
199/// A wrapper for [TypedTransaction] that allows impersonating accounts.
200///
201/// This is a helper that carries the `impersonated` sender so that the right hash
202/// [TypedTransaction::impersonated_hash] can be created.
203#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
204pub struct MaybeImpersonatedTransaction {
205    pub transaction: TypedTransaction,
206    pub impersonated_sender: Option<Address>,
207}
208
209impl Typed2718 for MaybeImpersonatedTransaction {
210    fn ty(&self) -> u8 {
211        self.transaction.ty()
212    }
213}
214
215impl MaybeImpersonatedTransaction {
216    /// Creates a new wrapper for the given transaction
217    pub fn new(transaction: TypedTransaction) -> Self {
218        Self { transaction, impersonated_sender: None }
219    }
220
221    /// Creates a new impersonated transaction wrapper using the given sender
222    pub fn impersonated(transaction: TypedTransaction, impersonated_sender: Address) -> Self {
223        Self { transaction, impersonated_sender: Some(impersonated_sender) }
224    }
225
226    /// Recovers the Ethereum address which was used to sign the transaction.
227    pub fn recover(&self) -> Result<Address, alloy_primitives::SignatureError> {
228        if let Some(sender) = self.impersonated_sender {
229            return Ok(sender);
230        }
231        self.transaction.recover()
232    }
233
234    /// Returns whether the transaction is impersonated
235    pub fn is_impersonated(&self) -> bool {
236        self.impersonated_sender.is_some()
237    }
238
239    /// Returns the hash of the transaction
240    pub fn hash(&self) -> B256 {
241        if let Some(sender) = self.impersonated_sender {
242            return self.transaction.impersonated_hash(sender);
243        }
244        self.transaction.hash()
245    }
246
247    /// Converts the transaction into an [`RpcTransaction`]
248    pub fn into_rpc_transaction(self) -> RpcTransaction {
249        let hash = self.hash();
250        let from = self.recover().unwrap_or_default();
251        let envelope = self.transaction.try_into_eth().expect("cant build deposit transactions");
252
253        // NOTE: we must update the hash because the tx can be impersonated, this requires forcing
254        // the hash
255        let inner_envelope = match envelope {
256            TxEnvelope::Legacy(t) => {
257                let (tx, sig, _) = t.into_parts();
258                TxEnvelope::Legacy(Signed::new_unchecked(tx, sig, hash))
259            }
260            TxEnvelope::Eip2930(t) => {
261                let (tx, sig, _) = t.into_parts();
262                TxEnvelope::Eip2930(Signed::new_unchecked(tx, sig, hash))
263            }
264            TxEnvelope::Eip1559(t) => {
265                let (tx, sig, _) = t.into_parts();
266                TxEnvelope::Eip1559(Signed::new_unchecked(tx, sig, hash))
267            }
268            TxEnvelope::Eip4844(t) => {
269                let (tx, sig, _) = t.into_parts();
270                TxEnvelope::Eip4844(Signed::new_unchecked(tx, sig, hash))
271            }
272            TxEnvelope::Eip7702(t) => {
273                let (tx, sig, _) = t.into_parts();
274                TxEnvelope::Eip7702(Signed::new_unchecked(tx, sig, hash))
275            }
276        };
277
278        RpcTransaction {
279            block_hash: None,
280            block_number: None,
281            transaction_index: None,
282            effective_gas_price: None,
283            inner: Recovered::new_unchecked(inner_envelope, from),
284        }
285    }
286}
287
288impl Encodable2718 for MaybeImpersonatedTransaction {
289    fn encode_2718_len(&self) -> usize {
290        self.transaction.encode_2718_len()
291    }
292
293    fn encode_2718(&self, out: &mut dyn BufMut) {
294        self.transaction.encode_2718(out)
295    }
296}
297
298impl Encodable for MaybeImpersonatedTransaction {
299    fn encode(&self, out: &mut dyn bytes::BufMut) {
300        self.transaction.encode(out)
301    }
302}
303
304impl From<MaybeImpersonatedTransaction> for TypedTransaction {
305    fn from(value: MaybeImpersonatedTransaction) -> Self {
306        value.transaction
307    }
308}
309
310impl From<TypedTransaction> for MaybeImpersonatedTransaction {
311    fn from(value: TypedTransaction) -> Self {
312        Self::new(value)
313    }
314}
315
316impl Decodable for MaybeImpersonatedTransaction {
317    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
318        TypedTransaction::decode(buf).map(Self::new)
319    }
320}
321
322impl AsRef<TypedTransaction> for MaybeImpersonatedTransaction {
323    fn as_ref(&self) -> &TypedTransaction {
324        &self.transaction
325    }
326}
327
328impl Deref for MaybeImpersonatedTransaction {
329    type Target = TypedTransaction;
330
331    fn deref(&self) -> &Self::Target {
332        &self.transaction
333    }
334}
335
336impl From<MaybeImpersonatedTransaction> for RpcTransaction {
337    fn from(value: MaybeImpersonatedTransaction) -> Self {
338        value.into_rpc_transaction()
339    }
340}
341
342/// Queued transaction
343#[derive(Clone, Debug, PartialEq, Eq)]
344pub struct PendingTransaction {
345    /// The actual transaction
346    pub transaction: MaybeImpersonatedTransaction,
347    /// the recovered sender of this transaction
348    sender: Address,
349    /// hash of `transaction`, so it can easily be reused with encoding and hashing again
350    hash: TxHash,
351}
352
353impl PendingTransaction {
354    pub fn new(transaction: TypedTransaction) -> Result<Self, alloy_primitives::SignatureError> {
355        let sender = transaction.recover()?;
356        let hash = transaction.hash();
357        Ok(Self { transaction: MaybeImpersonatedTransaction::new(transaction), sender, hash })
358    }
359
360    pub fn with_impersonated(transaction: TypedTransaction, sender: Address) -> Self {
361        let hash = transaction.impersonated_hash(sender);
362        Self {
363            transaction: MaybeImpersonatedTransaction::impersonated(transaction, sender),
364            sender,
365            hash,
366        }
367    }
368
369    /// Converts a [`MaybeImpersonatedTransaction`] into a [`PendingTransaction`].
370    pub fn from_maybe_impersonated(
371        transaction: MaybeImpersonatedTransaction,
372    ) -> Result<Self, alloy_primitives::SignatureError> {
373        if let Some(impersonated) = transaction.impersonated_sender {
374            Ok(Self::with_impersonated(transaction.transaction, impersonated))
375        } else {
376            Self::new(transaction.transaction)
377        }
378    }
379
380    pub fn nonce(&self) -> u64 {
381        self.transaction.nonce()
382    }
383
384    pub fn hash(&self) -> &TxHash {
385        &self.hash
386    }
387
388    pub fn sender(&self) -> &Address {
389        &self.sender
390    }
391
392    /// Converts the [PendingTransaction] into the [TxEnv] context that [`revm`](foundry_evm)
393    /// expects.
394    ///
395    /// Base [`TxEnv`] is encapsulated in the [`op_revm::OpTransaction`]
396    pub fn to_revm_tx_env(&self) -> OpTransaction<TxEnv> {
397        fn transact_to(kind: &TxKind) -> TxKind {
398            match kind {
399                TxKind::Call(c) => TxKind::Call(*c),
400                TxKind::Create => TxKind::Create,
401            }
402        }
403
404        let caller = *self.sender();
405        match &self.transaction.transaction {
406            TypedTransaction::Legacy(tx) => {
407                let chain_id = tx.tx().chain_id;
408                let TxLegacy { nonce, gas_price, gas_limit, value, to, input, .. } = tx.tx();
409                OpTransaction::new(TxEnv {
410                    caller,
411                    kind: transact_to(to),
412                    data: input.clone(),
413                    chain_id,
414                    nonce: *nonce,
415                    value: (*value),
416                    gas_price: *gas_price,
417                    gas_priority_fee: None,
418                    gas_limit: *gas_limit,
419                    access_list: vec![].into(),
420                    tx_type: LEGACY_TX_TYPE_ID,
421                    ..Default::default()
422                })
423            }
424            TypedTransaction::EIP2930(tx) => {
425                let TxEip2930 {
426                    chain_id,
427                    nonce,
428                    gas_price,
429                    gas_limit,
430                    to,
431                    value,
432                    input,
433                    access_list,
434                    ..
435                } = tx.tx();
436                OpTransaction::new(TxEnv {
437                    caller,
438                    kind: transact_to(to),
439                    data: input.clone(),
440                    chain_id: Some(*chain_id),
441                    nonce: *nonce,
442                    value: *value,
443                    gas_price: *gas_price,
444                    gas_priority_fee: None,
445                    gas_limit: *gas_limit,
446                    access_list: access_list.clone(),
447                    tx_type: EIP2930_TX_TYPE_ID,
448                    ..Default::default()
449                })
450            }
451            TypedTransaction::EIP1559(tx) => {
452                let TxEip1559 {
453                    chain_id,
454                    nonce,
455                    max_priority_fee_per_gas,
456                    max_fee_per_gas,
457                    gas_limit,
458                    to,
459                    value,
460                    input,
461                    access_list,
462                    ..
463                } = tx.tx();
464                OpTransaction::new(TxEnv {
465                    caller,
466                    kind: transact_to(to),
467                    data: input.clone(),
468                    chain_id: Some(*chain_id),
469                    nonce: *nonce,
470                    value: *value,
471                    gas_price: *max_fee_per_gas,
472                    gas_priority_fee: Some(*max_priority_fee_per_gas),
473                    gas_limit: *gas_limit,
474                    access_list: access_list.clone(),
475                    tx_type: EIP1559_TX_TYPE_ID,
476                    ..Default::default()
477                })
478            }
479            TypedTransaction::EIP4844(tx) => {
480                let TxEip4844 {
481                    chain_id,
482                    nonce,
483                    max_fee_per_blob_gas,
484                    max_fee_per_gas,
485                    max_priority_fee_per_gas,
486                    gas_limit,
487                    to,
488                    value,
489                    input,
490                    access_list,
491                    blob_versioned_hashes,
492                    ..
493                } = tx.tx().tx();
494                OpTransaction::new(TxEnv {
495                    caller,
496                    kind: TxKind::Call(*to),
497                    data: input.clone(),
498                    chain_id: Some(*chain_id),
499                    nonce: *nonce,
500                    value: *value,
501                    gas_price: *max_fee_per_gas,
502                    gas_priority_fee: Some(*max_priority_fee_per_gas),
503                    max_fee_per_blob_gas: *max_fee_per_blob_gas,
504                    blob_hashes: blob_versioned_hashes.clone(),
505                    gas_limit: *gas_limit,
506                    access_list: access_list.clone(),
507                    tx_type: EIP4844_TX_TYPE_ID,
508                    ..Default::default()
509                })
510            }
511            TypedTransaction::EIP7702(tx) => {
512                let TxEip7702 {
513                    chain_id,
514                    nonce,
515                    gas_limit,
516                    max_fee_per_gas,
517                    max_priority_fee_per_gas,
518                    to,
519                    value,
520                    access_list,
521                    authorization_list,
522                    input,
523                } = tx.tx();
524
525                let mut tx = TxEnv {
526                    caller,
527                    kind: TxKind::Call(*to),
528                    data: input.clone(),
529                    chain_id: Some(*chain_id),
530                    nonce: *nonce,
531                    value: *value,
532                    gas_price: *max_fee_per_gas,
533                    gas_priority_fee: Some(*max_priority_fee_per_gas),
534                    gas_limit: *gas_limit,
535                    access_list: access_list.clone(),
536                    tx_type: EIP7702_TX_TYPE_ID,
537                    ..Default::default()
538                };
539                tx.set_signed_authorization(authorization_list.clone());
540
541                OpTransaction::new(tx)
542            }
543            TypedTransaction::Deposit(tx) => {
544                let chain_id = tx.chain_id();
545                let TxDeposit {
546                    source_hash,
547                    to,
548                    mint,
549                    value,
550                    gas_limit,
551                    is_system_transaction,
552                    input,
553                    ..
554                } = tx;
555
556                let base = TxEnv {
557                    caller,
558                    kind: transact_to(to),
559                    data: input.clone(),
560                    chain_id,
561                    nonce: 0,
562                    value: *value,
563                    gas_price: 0,
564                    gas_priority_fee: None,
565                    gas_limit: { *gas_limit },
566                    access_list: vec![].into(),
567                    tx_type: DEPOSIT_TX_TYPE_ID,
568                    ..Default::default()
569                };
570
571                let deposit = DepositTransactionParts {
572                    source_hash: *source_hash,
573                    mint: Some(*mint),
574                    is_system_transaction: *is_system_transaction,
575                };
576
577                OpTransaction { base, deposit, enveloped_tx: None }
578            }
579        }
580    }
581}
582
583/// Container type for signed, typed transactions.
584#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
585pub enum TypedTransaction {
586    /// Legacy transaction type
587    Legacy(Signed<TxLegacy>),
588    /// EIP-2930 transaction
589    EIP2930(Signed<TxEip2930>),
590    /// EIP-1559 transaction
591    EIP1559(Signed<TxEip1559>),
592    /// EIP-4844 transaction
593    EIP4844(Signed<TxEip4844Variant>),
594    /// EIP-7702 transaction
595    EIP7702(Signed<TxEip7702>),
596    /// op-stack deposit transaction
597    Deposit(TxDeposit),
598}
599
600impl TryFrom<AnyRpcTransaction> for TypedTransaction {
601    type Error = ConversionError;
602
603    fn try_from(value: AnyRpcTransaction) -> Result<Self, Self::Error> {
604        let WithOtherFields { inner, .. } = value.0;
605        let from = inner.inner.signer();
606        match inner.inner.into_inner() {
607            AnyTxEnvelope::Ethereum(tx) => match tx {
608                TxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
609                TxEnvelope::Eip2930(tx) => Ok(Self::EIP2930(tx)),
610                TxEnvelope::Eip1559(tx) => Ok(Self::EIP1559(tx)),
611                TxEnvelope::Eip4844(tx) => Ok(Self::EIP4844(tx)),
612                TxEnvelope::Eip7702(tx) => Ok(Self::EIP7702(tx)),
613            },
614            AnyTxEnvelope::Unknown(mut tx) => {
615                // Try to convert to deposit transaction
616                if tx.ty() == DEPOSIT_TX_TYPE_ID {
617                    tx.inner.fields.insert("from".to_string(), serde_json::to_value(from).unwrap());
618                    let deposit_tx =
619                        tx.inner.fields.deserialize_into::<TxDeposit>().map_err(|e| {
620                            ConversionError::Custom(format!(
621                                "Failed to deserialize deposit tx: {e}"
622                            ))
623                        })?;
624
625                    return Ok(Self::Deposit(deposit_tx));
626                };
627
628                Err(ConversionError::Custom("UnknownTxType".to_string()))
629            }
630        }
631    }
632}
633
634impl TypedTransaction {
635    /// Converts the transaction into a [`TxEnvelope`].
636    ///
637    /// Returns an error if the transaction is a Deposit transaction, which is not part of the
638    /// standard Ethereum transaction types.
639    pub fn try_into_eth(self) -> Result<TxEnvelope, Self> {
640        match self {
641            Self::Legacy(tx) => Ok(TxEnvelope::Legacy(tx)),
642            Self::EIP2930(tx) => Ok(TxEnvelope::Eip2930(tx)),
643            Self::EIP1559(tx) => Ok(TxEnvelope::Eip1559(tx)),
644            Self::EIP4844(tx) => Ok(TxEnvelope::Eip4844(tx)),
645            Self::EIP7702(tx) => Ok(TxEnvelope::Eip7702(tx)),
646            Self::Deposit(_) => Err(self),
647        }
648    }
649
650    /// Returns true if the transaction uses dynamic fees: EIP1559, EIP4844 or EIP7702
651    pub fn is_dynamic_fee(&self) -> bool {
652        matches!(self, Self::EIP1559(_) | Self::EIP4844(_) | Self::EIP7702(_))
653    }
654
655    pub fn gas_price(&self) -> u128 {
656        match self {
657            Self::Legacy(tx) => tx.tx().gas_price,
658            Self::EIP2930(tx) => tx.tx().gas_price,
659            Self::EIP1559(tx) => tx.tx().max_fee_per_gas,
660            Self::EIP4844(tx) => tx.tx().tx().max_fee_per_gas,
661            Self::EIP7702(tx) => tx.tx().max_fee_per_gas,
662            Self::Deposit(_) => 0,
663        }
664    }
665
666    pub fn gas_limit(&self) -> u64 {
667        match self {
668            Self::Legacy(tx) => tx.tx().gas_limit,
669            Self::EIP2930(tx) => tx.tx().gas_limit,
670            Self::EIP1559(tx) => tx.tx().gas_limit,
671            Self::EIP4844(tx) => tx.tx().tx().gas_limit,
672            Self::EIP7702(tx) => tx.tx().gas_limit,
673            Self::Deposit(tx) => tx.gas_limit,
674        }
675    }
676
677    pub fn value(&self) -> U256 {
678        U256::from(match self {
679            Self::Legacy(tx) => tx.tx().value,
680            Self::EIP2930(tx) => tx.tx().value,
681            Self::EIP1559(tx) => tx.tx().value,
682            Self::EIP4844(tx) => tx.tx().tx().value,
683            Self::EIP7702(tx) => tx.tx().value,
684            Self::Deposit(tx) => tx.value,
685        })
686    }
687
688    pub fn data(&self) -> &Bytes {
689        match self {
690            Self::Legacy(tx) => &tx.tx().input,
691            Self::EIP2930(tx) => &tx.tx().input,
692            Self::EIP1559(tx) => &tx.tx().input,
693            Self::EIP4844(tx) => &tx.tx().tx().input,
694            Self::EIP7702(tx) => &tx.tx().input,
695            Self::Deposit(tx) => &tx.input,
696        }
697    }
698
699    /// Returns the transaction type
700    pub fn r#type(&self) -> Option<u8> {
701        match self {
702            Self::Legacy(_) => None,
703            Self::EIP2930(_) => Some(1),
704            Self::EIP1559(_) => Some(2),
705            Self::EIP4844(_) => Some(3),
706            Self::EIP7702(_) => Some(4),
707            Self::Deposit(_) => Some(0x7E),
708        }
709    }
710
711    /// Max cost of the transaction
712    /// It is the gas limit multiplied by the gas price,
713    /// and if the transaction is EIP-4844, the result of (total blob gas cost * max fee per blob
714    /// gas) is also added
715    pub fn max_cost(&self) -> u128 {
716        let mut max_cost = (self.gas_limit() as u128).saturating_mul(self.gas_price());
717
718        if self.is_eip4844() {
719            max_cost = max_cost.saturating_add(
720                self.blob_gas()
721                    .map(|g| g as u128)
722                    .unwrap_or(0)
723                    .mul(self.max_fee_per_blob_gas().unwrap_or(0)),
724            )
725        }
726
727        max_cost
728    }
729
730    pub fn blob_gas(&self) -> Option<u64> {
731        match self {
732            Self::EIP4844(tx) => Some(tx.tx().tx().blob_gas()),
733            _ => None,
734        }
735    }
736
737    pub fn sidecar(&self) -> Option<&TxEip4844WithSidecar> {
738        match self {
739            Self::EIP4844(signed_variant) => match signed_variant.tx() {
740                TxEip4844Variant::TxEip4844WithSidecar(with_sidecar) => Some(with_sidecar),
741                _ => None,
742            },
743            _ => None,
744        }
745    }
746
747    pub fn max_fee_per_blob_gas(&self) -> Option<u128> {
748        match self {
749            Self::EIP4844(tx) => Some(tx.tx().tx().max_fee_per_blob_gas),
750            _ => None,
751        }
752    }
753
754    /// Returns a helper type that contains commonly used values as fields
755    pub fn essentials(&self) -> TransactionEssentials {
756        match self {
757            Self::Legacy(t) => TransactionEssentials {
758                kind: t.tx().to,
759                input: t.tx().input.clone(),
760                nonce: t.tx().nonce,
761                gas_limit: t.tx().gas_limit,
762                gas_price: Some(t.tx().gas_price),
763                max_fee_per_gas: None,
764                max_priority_fee_per_gas: None,
765                max_fee_per_blob_gas: None,
766                blob_versioned_hashes: None,
767                value: t.tx().value,
768                chain_id: t.tx().chain_id,
769                access_list: Default::default(),
770            },
771            Self::EIP2930(t) => TransactionEssentials {
772                kind: t.tx().to,
773                input: t.tx().input.clone(),
774                nonce: t.tx().nonce,
775                gas_limit: t.tx().gas_limit,
776                gas_price: Some(t.tx().gas_price),
777                max_fee_per_gas: None,
778                max_priority_fee_per_gas: None,
779                max_fee_per_blob_gas: None,
780                blob_versioned_hashes: None,
781                value: t.tx().value,
782                chain_id: Some(t.tx().chain_id),
783                access_list: t.tx().access_list.clone(),
784            },
785            Self::EIP1559(t) => TransactionEssentials {
786                kind: t.tx().to,
787                input: t.tx().input.clone(),
788                nonce: t.tx().nonce,
789                gas_limit: t.tx().gas_limit,
790                gas_price: None,
791                max_fee_per_gas: Some(t.tx().max_fee_per_gas),
792                max_priority_fee_per_gas: Some(t.tx().max_priority_fee_per_gas),
793                max_fee_per_blob_gas: None,
794                blob_versioned_hashes: None,
795                value: t.tx().value,
796                chain_id: Some(t.tx().chain_id),
797                access_list: t.tx().access_list.clone(),
798            },
799            Self::EIP4844(t) => TransactionEssentials {
800                kind: TxKind::Call(t.tx().tx().to),
801                input: t.tx().tx().input.clone(),
802                nonce: t.tx().tx().nonce,
803                gas_limit: t.tx().tx().gas_limit,
804                gas_price: None,
805                max_fee_per_gas: Some(t.tx().tx().max_fee_per_gas),
806                max_priority_fee_per_gas: Some(t.tx().tx().max_priority_fee_per_gas),
807                max_fee_per_blob_gas: Some(t.tx().tx().max_fee_per_blob_gas),
808                blob_versioned_hashes: Some(t.tx().tx().blob_versioned_hashes.clone()),
809                value: t.tx().tx().value,
810                chain_id: Some(t.tx().tx().chain_id),
811                access_list: t.tx().tx().access_list.clone(),
812            },
813            Self::EIP7702(t) => TransactionEssentials {
814                kind: TxKind::Call(t.tx().to),
815                input: t.tx().input.clone(),
816                nonce: t.tx().nonce,
817                gas_limit: t.tx().gas_limit,
818                gas_price: None,
819                max_fee_per_gas: Some(t.tx().max_fee_per_gas),
820                max_priority_fee_per_gas: Some(t.tx().max_priority_fee_per_gas),
821                max_fee_per_blob_gas: None,
822                blob_versioned_hashes: None,
823                value: t.tx().value,
824                chain_id: Some(t.tx().chain_id),
825                access_list: t.tx().access_list.clone(),
826            },
827            Self::Deposit(t) => TransactionEssentials {
828                kind: t.to,
829                input: t.input.clone(),
830                nonce: 0,
831                gas_limit: t.gas_limit,
832                gas_price: Some(0),
833                max_fee_per_gas: None,
834                max_priority_fee_per_gas: None,
835                max_fee_per_blob_gas: None,
836                blob_versioned_hashes: None,
837                value: t.value,
838                chain_id: t.chain_id(),
839                access_list: Default::default(),
840            },
841        }
842    }
843
844    pub fn nonce(&self) -> u64 {
845        match self {
846            Self::Legacy(t) => t.tx().nonce,
847            Self::EIP2930(t) => t.tx().nonce,
848            Self::EIP1559(t) => t.tx().nonce,
849            Self::EIP4844(t) => t.tx().tx().nonce,
850            Self::EIP7702(t) => t.tx().nonce,
851            Self::Deposit(_t) => 0,
852        }
853    }
854
855    pub fn chain_id(&self) -> Option<u64> {
856        match self {
857            Self::Legacy(t) => t.tx().chain_id,
858            Self::EIP2930(t) => Some(t.tx().chain_id),
859            Self::EIP1559(t) => Some(t.tx().chain_id),
860            Self::EIP4844(t) => Some(t.tx().tx().chain_id),
861            Self::EIP7702(t) => Some(t.tx().chain_id),
862            Self::Deposit(t) => t.chain_id(),
863        }
864    }
865
866    pub fn as_legacy(&self) -> Option<&Signed<TxLegacy>> {
867        match self {
868            Self::Legacy(tx) => Some(tx),
869            _ => None,
870        }
871    }
872
873    /// Returns true whether this tx is a legacy transaction
874    pub fn is_legacy(&self) -> bool {
875        matches!(self, Self::Legacy(_))
876    }
877
878    /// Returns true whether this tx is a EIP1559 transaction
879    pub fn is_eip1559(&self) -> bool {
880        matches!(self, Self::EIP1559(_))
881    }
882
883    /// Returns true whether this tx is a EIP2930 transaction
884    pub fn is_eip2930(&self) -> bool {
885        matches!(self, Self::EIP2930(_))
886    }
887
888    /// Returns true whether this tx is a EIP4844 transaction
889    pub fn is_eip4844(&self) -> bool {
890        matches!(self, Self::EIP4844(_))
891    }
892
893    /// Returns true whether this tx is a EIP7702 transaction
894    pub fn is_eip7702(&self) -> bool {
895        matches!(self, Self::EIP7702(_))
896    }
897
898    /// Returns the hash of the transaction.
899    ///
900    /// Note: If this transaction has the Impersonated signature then this returns a modified unique
901    /// hash. This allows us to treat impersonated transactions as unique.
902    pub fn hash(&self) -> B256 {
903        match self {
904            Self::Legacy(t) => *t.hash(),
905            Self::EIP2930(t) => *t.hash(),
906            Self::EIP1559(t) => *t.hash(),
907            Self::EIP4844(t) => *t.hash(),
908            Self::EIP7702(t) => *t.hash(),
909            Self::Deposit(t) => t.tx_hash(),
910        }
911    }
912
913    /// Returns the hash if the transaction is impersonated (using a fake signature)
914    ///
915    /// This appends the `address` before hashing it
916    pub fn impersonated_hash(&self, sender: Address) -> B256 {
917        let mut buffer = Vec::new();
918        Encodable::encode(self, &mut buffer);
919        buffer.extend_from_slice(sender.as_ref());
920        B256::from_slice(alloy_primitives::utils::keccak256(&buffer).as_slice())
921    }
922
923    /// Recovers the Ethereum address which was used to sign the transaction.
924    pub fn recover(&self) -> Result<Address, alloy_primitives::SignatureError> {
925        match self {
926            Self::Legacy(tx) => tx.recover_signer(),
927            Self::EIP2930(tx) => tx.recover_signer(),
928            Self::EIP1559(tx) => tx.recover_signer(),
929            Self::EIP4844(tx) => tx.recover_signer(),
930            Self::EIP7702(tx) => tx.recover_signer(),
931            Self::Deposit(tx) => Ok(tx.from),
932        }
933    }
934
935    /// Returns what kind of transaction this is
936    pub fn kind(&self) -> TxKind {
937        match self {
938            Self::Legacy(tx) => tx.tx().to,
939            Self::EIP2930(tx) => tx.tx().to,
940            Self::EIP1559(tx) => tx.tx().to,
941            Self::EIP4844(tx) => TxKind::Call(tx.tx().tx().to),
942            Self::EIP7702(tx) => TxKind::Call(tx.tx().to),
943            Self::Deposit(tx) => tx.to,
944        }
945    }
946
947    /// Returns the callee if this transaction is a call
948    pub fn to(&self) -> Option<Address> {
949        self.kind().to().copied()
950    }
951
952    /// Returns the Signature of the transaction
953    pub fn signature(&self) -> Signature {
954        match self {
955            Self::Legacy(tx) => *tx.signature(),
956            Self::EIP2930(tx) => *tx.signature(),
957            Self::EIP1559(tx) => *tx.signature(),
958            Self::EIP4844(tx) => *tx.signature(),
959            Self::EIP7702(tx) => *tx.signature(),
960            Self::Deposit(_) => Signature::from_scalars_and_parity(
961                B256::with_last_byte(1),
962                B256::with_last_byte(1),
963                false,
964            ),
965        }
966    }
967}
968
969impl Encodable for TypedTransaction {
970    fn encode(&self, out: &mut dyn bytes::BufMut) {
971        if !self.is_legacy() {
972            Header { list: false, payload_length: self.encode_2718_len() }.encode(out);
973        }
974
975        self.encode_2718(out);
976    }
977}
978
979impl Decodable for TypedTransaction {
980    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
981        let mut h_decode_copy = *buf;
982        let header = alloy_rlp::Header::decode(&mut h_decode_copy)?;
983
984        // Legacy TX
985        if header.list {
986            return Ok(TxEnvelope::decode(buf)?.into());
987        }
988
989        // Check byte after header
990        let ty = *h_decode_copy.first().ok_or(alloy_rlp::Error::Custom("empty slice"))?;
991
992        if ty != 0x7E {
993            Ok(TxEnvelope::decode(buf)?.into())
994        } else {
995            Ok(Self::Deposit(TxDeposit::decode_2718(buf)?))
996        }
997    }
998}
999
1000impl Typed2718 for TypedTransaction {
1001    fn ty(&self) -> u8 {
1002        self.r#type().unwrap_or(0)
1003    }
1004}
1005
1006impl Encodable2718 for TypedTransaction {
1007    fn encode_2718_len(&self) -> usize {
1008        match self {
1009            Self::Legacy(tx) => TxEnvelope::from(tx.clone()).encode_2718_len(),
1010            Self::EIP2930(tx) => TxEnvelope::from(tx.clone()).encode_2718_len(),
1011            Self::EIP1559(tx) => TxEnvelope::from(tx.clone()).encode_2718_len(),
1012            Self::EIP4844(tx) => TxEnvelope::from(tx.clone()).encode_2718_len(),
1013            Self::EIP7702(tx) => TxEnvelope::from(tx.clone()).encode_2718_len(),
1014            Self::Deposit(tx) => 1 + tx.length(),
1015        }
1016    }
1017
1018    fn encode_2718(&self, out: &mut dyn BufMut) {
1019        match self {
1020            Self::Legacy(tx) => TxEnvelope::from(tx.clone()).encode_2718(out),
1021            Self::EIP2930(tx) => TxEnvelope::from(tx.clone()).encode_2718(out),
1022            Self::EIP1559(tx) => TxEnvelope::from(tx.clone()).encode_2718(out),
1023            Self::EIP4844(tx) => TxEnvelope::from(tx.clone()).encode_2718(out),
1024            Self::EIP7702(tx) => TxEnvelope::from(tx.clone()).encode_2718(out),
1025            Self::Deposit(tx) => {
1026                tx.encode_2718(out);
1027            }
1028        }
1029    }
1030}
1031
1032impl Decodable2718 for TypedTransaction {
1033    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
1034        if ty == 0x7E {
1035            return Ok(Self::Deposit(TxDeposit::decode(buf)?));
1036        }
1037        match TxEnvelope::typed_decode(ty, buf)? {
1038            TxEnvelope::Eip2930(tx) => Ok(Self::EIP2930(tx)),
1039            TxEnvelope::Eip1559(tx) => Ok(Self::EIP1559(tx)),
1040            TxEnvelope::Eip4844(tx) => Ok(Self::EIP4844(tx)),
1041            TxEnvelope::Eip7702(tx) => Ok(Self::EIP7702(tx)),
1042            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
1043        }
1044    }
1045
1046    fn fallback_decode(buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
1047        match TxEnvelope::fallback_decode(buf)? {
1048            TxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
1049            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
1050        }
1051    }
1052}
1053
1054impl From<TxEnvelope> for TypedTransaction {
1055    fn from(value: TxEnvelope) -> Self {
1056        match value {
1057            TxEnvelope::Legacy(tx) => Self::Legacy(tx),
1058            TxEnvelope::Eip2930(tx) => Self::EIP2930(tx),
1059            TxEnvelope::Eip1559(tx) => Self::EIP1559(tx),
1060            TxEnvelope::Eip4844(tx) => Self::EIP4844(tx),
1061            TxEnvelope::Eip7702(tx) => Self::EIP7702(tx),
1062        }
1063    }
1064}
1065
1066#[derive(Clone, Debug, PartialEq, Eq)]
1067pub struct TransactionEssentials {
1068    pub kind: TxKind,
1069    pub input: Bytes,
1070    pub nonce: u64,
1071    pub gas_limit: u64,
1072    pub gas_price: Option<u128>,
1073    pub max_fee_per_gas: Option<u128>,
1074    pub max_priority_fee_per_gas: Option<u128>,
1075    pub max_fee_per_blob_gas: Option<u128>,
1076    pub blob_versioned_hashes: Option<Vec<B256>>,
1077    pub value: U256,
1078    pub chain_id: Option<u64>,
1079    pub access_list: AccessList,
1080}
1081
1082/// Represents all relevant information of an executed transaction
1083#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1084pub struct TransactionInfo {
1085    pub transaction_hash: B256,
1086    pub transaction_index: u64,
1087    pub from: Address,
1088    pub to: Option<Address>,
1089    pub contract_address: Option<Address>,
1090    pub traces: Vec<CallTraceNode>,
1091    pub exit: InstructionResult,
1092    pub out: Option<Bytes>,
1093    pub nonce: u64,
1094    pub gas_used: u64,
1095}
1096
1097#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1098#[serde(tag = "type")]
1099pub enum TypedReceipt {
1100    #[serde(rename = "0x0", alias = "0x00")]
1101    Legacy(ReceiptWithBloom<Receipt<alloy_primitives::Log>>),
1102    #[serde(rename = "0x1", alias = "0x01")]
1103    EIP2930(ReceiptWithBloom<Receipt<alloy_primitives::Log>>),
1104    #[serde(rename = "0x2", alias = "0x02")]
1105    EIP1559(ReceiptWithBloom<Receipt<alloy_primitives::Log>>),
1106    #[serde(rename = "0x3", alias = "0x03")]
1107    EIP4844(ReceiptWithBloom<Receipt<alloy_primitives::Log>>),
1108    #[serde(rename = "0x4", alias = "0x04")]
1109    EIP7702(ReceiptWithBloom<Receipt<alloy_primitives::Log>>),
1110    #[serde(rename = "0x7E", alias = "0x7e")]
1111    Deposit(OpDepositReceiptWithBloom),
1112}
1113
1114/// RPC-specific variant of TypedReceipt for boundary conversion
1115#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1116#[serde(tag = "type")]
1117pub enum TypedReceiptRpc {
1118    #[serde(rename = "0x0", alias = "0x00")]
1119    Legacy(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
1120    #[serde(rename = "0x1", alias = "0x01")]
1121    EIP2930(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
1122    #[serde(rename = "0x2", alias = "0x02")]
1123    EIP1559(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
1124    #[serde(rename = "0x3", alias = "0x03")]
1125    EIP4844(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
1126    #[serde(rename = "0x4", alias = "0x04")]
1127    EIP7702(ReceiptWithBloom<Receipt<alloy_rpc_types::Log>>),
1128    #[serde(rename = "0x7E", alias = "0x7e")]
1129    Deposit(OpDepositReceiptWithBloom),
1130}
1131
1132impl TypedReceipt {
1133    /// Convert to RPC-specific receipt type
1134    pub fn into_rpc_receipt(self) -> TypedReceiptRpc {
1135        match self {
1136            Self::Legacy(r) => TypedReceiptRpc::Legacy(convert_receipt_to_rpc(r)),
1137            Self::EIP2930(r) => TypedReceiptRpc::EIP2930(convert_receipt_to_rpc(r)),
1138            Self::EIP1559(r) => TypedReceiptRpc::EIP1559(convert_receipt_to_rpc(r)),
1139            Self::EIP4844(r) => TypedReceiptRpc::EIP4844(convert_receipt_to_rpc(r)),
1140            Self::EIP7702(r) => TypedReceiptRpc::EIP7702(convert_receipt_to_rpc(r)),
1141            Self::Deposit(r) => TypedReceiptRpc::Deposit(r),
1142        }
1143    }
1144
1145    pub fn as_receipt_with_bloom(&self) -> &ReceiptWithBloom<Receipt<alloy_primitives::Log>> {
1146        match self {
1147            Self::Legacy(r)
1148            | Self::EIP1559(r)
1149            | Self::EIP2930(r)
1150            | Self::EIP4844(r)
1151            | Self::EIP7702(r) => r,
1152            Self::Deposit(_) => unreachable!("use variant-specific helpers for deposit"),
1153        }
1154    }
1155
1156    pub fn logs(&self) -> &[alloy_primitives::Log] {
1157        match self {
1158            Self::Legacy(r)
1159            | Self::EIP1559(r)
1160            | Self::EIP2930(r)
1161            | Self::EIP4844(r)
1162            | Self::EIP7702(r) => &r.receipt.logs,
1163            Self::Deposit(_) => unreachable!("use variant-specific helpers for deposit"),
1164        }
1165    }
1166
1167    pub fn logs_bloom(&self) -> &Bloom {
1168        match self {
1169            Self::Legacy(r)
1170            | Self::EIP1559(r)
1171            | Self::EIP2930(r)
1172            | Self::EIP4844(r)
1173            | Self::EIP7702(r) => &r.logs_bloom,
1174            Self::Deposit(r) => &r.logs_bloom,
1175        }
1176    }
1177
1178    pub fn cumulative_gas_used(&self) -> u64 {
1179        match self {
1180            Self::Deposit(r) => r.receipt.inner.cumulative_gas_used,
1181            _ => self.as_receipt_with_bloom().cumulative_gas_used(),
1182        }
1183    }
1184}
1185
1186/// Convert internal receipt to RPC receipt
1187fn convert_receipt_to_rpc(
1188    receipt: ReceiptWithBloom<Receipt<alloy_primitives::Log>>,
1189) -> ReceiptWithBloom<Receipt<alloy_rpc_types::Log>> {
1190    receipt.map_logs(|log| alloy_rpc_types::Log {
1191        inner: log,
1192        block_hash: None,
1193        block_number: None,
1194        block_timestamp: None,
1195        transaction_hash: None,
1196        transaction_index: None,
1197        log_index: None,
1198        removed: false,
1199    })
1200}
1201
1202impl TypedReceiptRpc {
1203    pub fn as_receipt_with_bloom(&self) -> &ReceiptWithBloom<Receipt<alloy_rpc_types::Log>> {
1204        match self {
1205            Self::Legacy(r)
1206            | Self::EIP1559(r)
1207            | Self::EIP2930(r)
1208            | Self::EIP4844(r)
1209            | Self::EIP7702(r) => r,
1210            Self::Deposit(_) => unreachable!("use variant-specific helpers for deposit"),
1211        }
1212    }
1213
1214    pub fn logs_bloom(&self) -> &Bloom {
1215        match self {
1216            Self::Legacy(r)
1217            | Self::EIP1559(r)
1218            | Self::EIP2930(r)
1219            | Self::EIP4844(r)
1220            | Self::EIP7702(r) => &r.logs_bloom,
1221            Self::Deposit(r) => &r.logs_bloom,
1222        }
1223    }
1224
1225    pub fn logs(&self) -> &[alloy_rpc_types::Log] {
1226        match self {
1227            Self::Legacy(r)
1228            | Self::EIP1559(r)
1229            | Self::EIP2930(r)
1230            | Self::EIP4844(r)
1231            | Self::EIP7702(r) => &r.receipt.logs,
1232            Self::Deposit(_) => unreachable!("use variant-specific helpers for deposit"),
1233        }
1234    }
1235
1236    pub fn cumulative_gas_used(&self) -> u64 {
1237        self.as_receipt_with_bloom().cumulative_gas_used()
1238    }
1239}
1240
1241// Intentionally only provide a concrete conversion used by RPC response/Otterscan path.
1242impl From<TypedReceiptRpc> for ReceiptWithBloom<Receipt<alloy_rpc_types::Log>> {
1243    fn from(value: TypedReceiptRpc) -> Self {
1244        match value {
1245            TypedReceiptRpc::Legacy(r)
1246            | TypedReceiptRpc::EIP1559(r)
1247            | TypedReceiptRpc::EIP2930(r)
1248            | TypedReceiptRpc::EIP4844(r)
1249            | TypedReceiptRpc::EIP7702(r) => r,
1250            TypedReceiptRpc::Deposit(r) => {
1251                // Convert OP deposit receipt (primitives::Log) to RPC receipt (rpc_types::Log)
1252                let receipt = Receipt::<alloy_rpc_types::Log> {
1253                    status: r.receipt.inner.status,
1254                    cumulative_gas_used: r.receipt.inner.cumulative_gas_used,
1255                    logs: r
1256                        .receipt
1257                        .inner
1258                        .logs
1259                        .into_iter()
1260                        .map(|l| alloy_rpc_types::Log {
1261                            inner: l,
1262                            block_hash: None,
1263                            block_number: None,
1264                            block_timestamp: None,
1265                            transaction_hash: None,
1266                            transaction_index: None,
1267                            log_index: None,
1268                            removed: false,
1269                        })
1270                        .collect(),
1271                };
1272                Self { receipt, logs_bloom: r.logs_bloom }
1273            }
1274        }
1275    }
1276}
1277
1278impl From<TypedReceiptRpc> for OtsReceipt {
1279    fn from(value: TypedReceiptRpc) -> Self {
1280        let r#type = match value {
1281            TypedReceiptRpc::Legacy(_) => 0x00,
1282            TypedReceiptRpc::EIP2930(_) => 0x01,
1283            TypedReceiptRpc::EIP1559(_) => 0x02,
1284            TypedReceiptRpc::EIP4844(_) => 0x03,
1285            TypedReceiptRpc::EIP7702(_) => 0x04,
1286            TypedReceiptRpc::Deposit(_) => 0x7E,
1287        } as u8;
1288        let receipt = ReceiptWithBloom::<Receipt<alloy_rpc_types::Log>>::from(value);
1289        let status = receipt.status();
1290        let cumulative_gas_used = receipt.cumulative_gas_used();
1291        let logs = receipt.logs().to_vec();
1292        let logs_bloom = receipt.logs_bloom;
1293
1294        Self { status, cumulative_gas_used, logs: Some(logs), logs_bloom: Some(logs_bloom), r#type }
1295    }
1296}
1297
1298impl From<ReceiptEnvelope<alloy_rpc_types::Log>> for TypedReceiptRpc {
1299    fn from(value: ReceiptEnvelope<alloy_rpc_types::Log>) -> Self {
1300        match value {
1301            ReceiptEnvelope::Legacy(r) => Self::Legacy(r),
1302            ReceiptEnvelope::Eip2930(r) => Self::EIP2930(r),
1303            ReceiptEnvelope::Eip1559(r) => Self::EIP1559(r),
1304            ReceiptEnvelope::Eip4844(r) => Self::EIP4844(r),
1305            ReceiptEnvelope::Eip7702(r) => Self::EIP7702(r),
1306        }
1307    }
1308}
1309
1310impl Encodable for TypedReceipt {
1311    fn encode(&self, out: &mut dyn bytes::BufMut) {
1312        match self {
1313            Self::Legacy(r) => r.encode(out),
1314            receipt => {
1315                let payload_len = match receipt {
1316                    Self::EIP2930(r) => r.length() + 1,
1317                    Self::EIP1559(r) => r.length() + 1,
1318                    Self::EIP4844(r) => r.length() + 1,
1319                    Self::EIP7702(r) => r.length() + 1,
1320                    Self::Deposit(r) => r.length() + 1,
1321                    _ => unreachable!("receipt already matched"),
1322                };
1323
1324                match receipt {
1325                    Self::EIP2930(r) => {
1326                        Header { list: true, payload_length: payload_len }.encode(out);
1327                        1u8.encode(out);
1328                        r.encode(out);
1329                    }
1330                    Self::EIP1559(r) => {
1331                        Header { list: true, payload_length: payload_len }.encode(out);
1332                        2u8.encode(out);
1333                        r.encode(out);
1334                    }
1335                    Self::EIP4844(r) => {
1336                        Header { list: true, payload_length: payload_len }.encode(out);
1337                        3u8.encode(out);
1338                        r.encode(out);
1339                    }
1340                    Self::EIP7702(r) => {
1341                        Header { list: true, payload_length: payload_len }.encode(out);
1342                        4u8.encode(out);
1343                        r.encode(out);
1344                    }
1345                    Self::Deposit(r) => {
1346                        Header { list: true, payload_length: payload_len }.encode(out);
1347                        0x7Eu8.encode(out);
1348                        r.encode(out);
1349                    }
1350                    _ => unreachable!("receipt already matched"),
1351                }
1352            }
1353        }
1354    }
1355}
1356
1357impl Decodable for TypedReceipt {
1358    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
1359        use bytes::Buf;
1360        use std::cmp::Ordering;
1361
1362        // a receipt is either encoded as a string (non legacy) or a list (legacy).
1363        // We should not consume the buffer if we are decoding a legacy receipt, so let's
1364        // check if the first byte is between 0x80 and 0xbf.
1365        let rlp_type = *buf
1366            .first()
1367            .ok_or(alloy_rlp::Error::Custom("cannot decode a receipt from empty bytes"))?;
1368
1369        match rlp_type.cmp(&alloy_rlp::EMPTY_LIST_CODE) {
1370            Ordering::Less => {
1371                // strip out the string header
1372                let _header = Header::decode(buf)?;
1373                let receipt_type = *buf.first().ok_or(alloy_rlp::Error::Custom(
1374                    "typed receipt cannot be decoded from an empty slice",
1375                ))?;
1376                if receipt_type == 0x01 {
1377                    buf.advance(1);
1378                    <ReceiptWithBloom as Decodable>::decode(buf).map(TypedReceipt::EIP2930)
1379                } else if receipt_type == 0x02 {
1380                    buf.advance(1);
1381                    <ReceiptWithBloom as Decodable>::decode(buf).map(TypedReceipt::EIP1559)
1382                } else if receipt_type == 0x03 {
1383                    buf.advance(1);
1384                    <ReceiptWithBloom as Decodable>::decode(buf).map(TypedReceipt::EIP4844)
1385                } else if receipt_type == 0x04 {
1386                    buf.advance(1);
1387                    <ReceiptWithBloom as Decodable>::decode(buf).map(TypedReceipt::EIP7702)
1388                } else if receipt_type == 0x7E {
1389                    buf.advance(1);
1390                    <OpDepositReceiptWithBloom as Decodable>::decode(buf).map(TypedReceipt::Deposit)
1391                } else {
1392                    Err(alloy_rlp::Error::Custom("invalid receipt type"))
1393                }
1394            }
1395            Ordering::Equal => {
1396                Err(alloy_rlp::Error::Custom("an empty list is not a valid receipt encoding"))
1397            }
1398            Ordering::Greater => {
1399                <ReceiptWithBloom as Decodable>::decode(buf).map(TypedReceipt::Legacy)
1400            }
1401        }
1402    }
1403}
1404
1405impl Typed2718 for TypedReceipt {
1406    fn ty(&self) -> u8 {
1407        match self {
1408            Self::Legacy(_) => alloy_consensus::constants::LEGACY_TX_TYPE_ID,
1409            Self::EIP2930(_) => alloy_consensus::constants::EIP2930_TX_TYPE_ID,
1410            Self::EIP1559(_) => alloy_consensus::constants::EIP1559_TX_TYPE_ID,
1411            Self::EIP4844(_) => alloy_consensus::constants::EIP4844_TX_TYPE_ID,
1412            Self::EIP7702(_) => alloy_consensus::constants::EIP7702_TX_TYPE_ID,
1413            Self::Deposit(_) => DEPOSIT_TX_TYPE_ID,
1414        }
1415    }
1416}
1417
1418impl Encodable2718 for TypedReceipt {
1419    fn encode_2718_len(&self) -> usize {
1420        match self {
1421            Self::Legacy(r) => ReceiptEnvelope::Legacy(r.clone()).encode_2718_len(),
1422            Self::EIP2930(r) => ReceiptEnvelope::Eip2930(r.clone()).encode_2718_len(),
1423            Self::EIP1559(r) => ReceiptEnvelope::Eip1559(r.clone()).encode_2718_len(),
1424            Self::EIP4844(r) => ReceiptEnvelope::Eip4844(r.clone()).encode_2718_len(),
1425            Self::EIP7702(r) => 1 + r.length(),
1426            Self::Deposit(r) => 1 + r.length(),
1427        }
1428    }
1429
1430    fn encode_2718(&self, out: &mut dyn BufMut) {
1431        if let Some(ty) = self.type_flag() {
1432            out.put_u8(ty);
1433        }
1434        match self {
1435            Self::Legacy(r)
1436            | Self::EIP2930(r)
1437            | Self::EIP1559(r)
1438            | Self::EIP4844(r)
1439            | Self::EIP7702(r) => r.encode(out),
1440            Self::Deposit(r) => r.encode(out),
1441        }
1442    }
1443}
1444
1445impl Decodable2718 for TypedReceipt {
1446    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
1447        if ty == 0x7E {
1448            return Ok(Self::Deposit(OpDepositReceiptWithBloom::decode(buf)?));
1449        }
1450        match ReceiptEnvelope::typed_decode(ty, buf)? {
1451            ReceiptEnvelope::Eip2930(tx) => Ok(Self::EIP2930(tx)),
1452            ReceiptEnvelope::Eip1559(tx) => Ok(Self::EIP1559(tx)),
1453            ReceiptEnvelope::Eip4844(tx) => Ok(Self::EIP4844(tx)),
1454            ReceiptEnvelope::Eip7702(tx) => Ok(Self::EIP7702(tx)),
1455            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
1456        }
1457    }
1458
1459    fn fallback_decode(buf: &mut &[u8]) -> Result<Self, Eip2718Error> {
1460        match ReceiptEnvelope::fallback_decode(buf)? {
1461            ReceiptEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
1462            _ => Err(Eip2718Error::RlpError(alloy_rlp::Error::Custom("unexpected tx type"))),
1463        }
1464    }
1465}
1466
1467pub type ReceiptResponse = WithOtherFields<TransactionReceipt<TypedReceiptRpc>>;
1468
1469pub fn convert_to_anvil_receipt(receipt: AnyTransactionReceipt) -> Option<ReceiptResponse> {
1470    let WithOtherFields {
1471        inner:
1472            TransactionReceipt {
1473                transaction_hash,
1474                transaction_index,
1475                block_hash,
1476                block_number,
1477                gas_used,
1478                contract_address,
1479                effective_gas_price,
1480                from,
1481                to,
1482                blob_gas_price,
1483                blob_gas_used,
1484                inner: AnyReceiptEnvelope { inner: receipt_with_bloom, r#type },
1485            },
1486        other,
1487    } = receipt;
1488
1489    Some(WithOtherFields {
1490        inner: TransactionReceipt {
1491            transaction_hash,
1492            transaction_index,
1493            block_hash,
1494            block_number,
1495            gas_used,
1496            contract_address,
1497            effective_gas_price,
1498            from,
1499            to,
1500            blob_gas_price,
1501            blob_gas_used,
1502            inner: match r#type {
1503                0x00 => TypedReceiptRpc::Legacy(receipt_with_bloom),
1504                0x01 => TypedReceiptRpc::EIP2930(receipt_with_bloom),
1505                0x02 => TypedReceiptRpc::EIP1559(receipt_with_bloom),
1506                0x03 => TypedReceiptRpc::EIP4844(receipt_with_bloom),
1507                0x04 => TypedReceiptRpc::EIP7702(receipt_with_bloom),
1508                0x7E => TypedReceiptRpc::Deposit(OpDepositReceiptWithBloom {
1509                    receipt: OpDepositReceipt {
1510                        inner: Receipt {
1511                            status: alloy_consensus::Eip658Value::Eip658(
1512                                receipt_with_bloom.status(),
1513                            ),
1514                            cumulative_gas_used: receipt_with_bloom.cumulative_gas_used(),
1515                            logs: receipt_with_bloom
1516                                .receipt
1517                                .logs
1518                                .into_iter()
1519                                .map(|l| l.inner)
1520                                .collect(),
1521                        },
1522                        deposit_nonce: other
1523                            .get_deserialized::<U64>("depositNonce")
1524                            .transpose()
1525                            .ok()?
1526                            .map(|v| v.to()),
1527                        deposit_receipt_version: other
1528                            .get_deserialized::<U64>("depositReceiptVersion")
1529                            .transpose()
1530                            .ok()?
1531                            .map(|v| v.to()),
1532                    },
1533                    logs_bloom: receipt_with_bloom.logs_bloom,
1534                }),
1535                _ => return None,
1536            },
1537        },
1538        other,
1539    })
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544    use super::*;
1545    use alloy_primitives::{Log, LogData, b256, hex};
1546    use std::str::FromStr;
1547
1548    // <https://github.com/foundry-rs/foundry/issues/10852>
1549    #[test]
1550    fn test_receipt_convert() {
1551        let s = r#"{"type":"0x4","status":"0x1","cumulativeGasUsed":"0x903fd1","logs":[{"address":"0x0000d9fcd47bf761e7287d8ee09917d7e2100000","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000234ce51365b9c417171b6dad280f49143e1b0547"],"data":"0x00000000000000000000000000000000000000000000032139b42c3431700000","blockHash":"0xd26b59c1d8b5bfa9362d19eb0da3819dfe0b367987a71f6d30908dd45e0d7a60","blockNumber":"0x159663e","blockTimestamp":"0x68411f7b","transactionHash":"0x17a6af73d1317e69cfc3cac9221bd98261d40f24815850a44dbfbf96652ae52a","transactionIndex":"0x22","logIndex":"0x158","removed":false}],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000008100000000000000000000000000000000000000000000000020000200000000000000800000000800000000000000010000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000","transactionHash":"0x17a6af73d1317e69cfc3cac9221bd98261d40f24815850a44dbfbf96652ae52a","transactionIndex":"0x22","blockHash":"0xd26b59c1d8b5bfa9362d19eb0da3819dfe0b367987a71f6d30908dd45e0d7a60","blockNumber":"0x159663e","gasUsed":"0x28ee7","effectiveGasPrice":"0x4bf02090","from":"0x234ce51365b9c417171b6dad280f49143e1b0547","to":"0x234ce51365b9c417171b6dad280f49143e1b0547","contractAddress":null}"#;
1552        let receipt: AnyTransactionReceipt = serde_json::from_str(s).unwrap();
1553        let _converted = convert_to_anvil_receipt(receipt).unwrap();
1554    }
1555
1556    #[test]
1557    fn test_decode_call() {
1558        let bytes_first = &mut &hex::decode("f86b02843b9aca00830186a094d3e8763675e4c425df46cc3b5c0f6cbdac39604687038d7ea4c68000802ba00eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5aea03a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca18").unwrap()[..];
1559        let decoded = TypedTransaction::decode(&mut &bytes_first[..]).unwrap();
1560
1561        let tx = TxLegacy {
1562            nonce: 2u64,
1563            gas_price: 1000000000u128,
1564            gas_limit: 100000,
1565            to: TxKind::Call(Address::from_slice(
1566                &hex::decode("d3e8763675e4c425df46cc3b5c0f6cbdac396046").unwrap()[..],
1567            )),
1568            value: U256::from(1000000000000000u64),
1569            input: Bytes::default(),
1570            chain_id: Some(4),
1571        };
1572
1573        let signature = Signature::from_str("0eb96ca19e8a77102767a41fc85a36afd5c61ccb09911cec5d3e86e193d9c5ae3a456401896b1b6055311536bf00a718568c744d8c1f9df59879e8350220ca182b").unwrap();
1574
1575        let tx = TypedTransaction::Legacy(Signed::new_unchecked(
1576            tx,
1577            signature,
1578            b256!("0xa517b206d2223278f860ea017d3626cacad4f52ff51030dc9a96b432f17f8d34"),
1579        ));
1580
1581        assert_eq!(tx, decoded);
1582    }
1583
1584    #[test]
1585    fn test_decode_create_goerli() {
1586        // test that an example create tx from goerli decodes properly
1587        let tx_bytes =
1588              hex::decode("02f901ee05228459682f008459682f11830209bf8080b90195608060405234801561001057600080fd5b50610175806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630c49c36c14610030575b600080fd5b61003861004e565b604051610045919061011d565b60405180910390f35b60606020600052600f6020527f68656c6c6f2073746174656d696e64000000000000000000000000000000000060405260406000f35b600081519050919050565b600082825260208201905092915050565b60005b838110156100be5780820151818401526020810190506100a3565b838111156100cd576000848401525b50505050565b6000601f19601f8301169050919050565b60006100ef82610084565b6100f9818561008f565b93506101098185602086016100a0565b610112816100d3565b840191505092915050565b6000602082019050818103600083015261013781846100e4565b90509291505056fea264697066735822122051449585839a4ea5ac23cae4552ef8a96b64ff59d0668f76bfac3796b2bdbb3664736f6c63430008090033c080a0136ebffaa8fc8b9fda9124de9ccb0b1f64e90fbd44251b4c4ac2501e60b104f9a07eb2999eec6d185ef57e91ed099afb0a926c5b536f0155dd67e537c7476e1471")
1589                  .unwrap();
1590        let _decoded = TypedTransaction::decode(&mut &tx_bytes[..]).unwrap();
1591    }
1592
1593    #[test]
1594    fn can_recover_sender() {
1595        // random mainnet tx: https://etherscan.io/tx/0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f
1596        let bytes = hex::decode("02f872018307910d808507204d2cb1827d0094388c818ca8b9251b393131c08a736a67ccb19297880320d04823e2701c80c001a0cf024f4815304df2867a1a74e9d2707b6abda0337d2d54a4438d453f4160f190a07ac0e6b3bc9395b5b9c8b9e6d77204a236577a5b18467b9175c01de4faa208d9").unwrap();
1597
1598        let Ok(TypedTransaction::EIP1559(tx)) = TypedTransaction::decode(&mut &bytes[..]) else {
1599            panic!("decoding TypedTransaction failed");
1600        };
1601
1602        assert_eq!(
1603            tx.hash(),
1604            &"0x86718885c4b4218c6af87d3d0b0d83e3cc465df2a05c048aa4db9f1a6f9de91f"
1605                .parse::<B256>()
1606                .unwrap()
1607        );
1608        assert_eq!(
1609            tx.recover_signer().unwrap(),
1610            "0x95222290DD7278Aa3Ddd389Cc1E1d165CC4BAfe5".parse::<Address>().unwrap()
1611        );
1612    }
1613
1614    // Test vector from https://sepolia.etherscan.io/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
1615    // Blobscan: https://sepolia.blobscan.com/tx/0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
1616    #[test]
1617    fn test_decode_live_4844_tx() {
1618        use alloy_primitives::{address, b256};
1619
1620        // https://sepolia.etherscan.io/getRawTx?tx=0x9a22ccb0029bc8b0ddd073be1a1d923b7ae2b2ea52100bae0db4424f9107e9c0
1621        let raw_tx = alloy_primitives::hex::decode("0x03f9011d83aa36a7820fa28477359400852e90edd0008252089411e9ca82a3a762b4b5bd264d4173a242e7a770648080c08504a817c800f8a5a0012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921aa00152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4a0013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7a001148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1a0011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e654901a0c8de4cced43169f9aa3d36506363b2d2c44f6c49fc1fd91ea114c86f3757077ea01e11fdd0d1934eda0492606ee0bb80a7bf8f35cc5f86ec60fe5031ba48bfd544").unwrap();
1622        let res = TypedTransaction::decode(&mut raw_tx.as_slice()).unwrap();
1623        assert_eq!(res.r#type(), Some(3));
1624
1625        let tx = match res {
1626            TypedTransaction::EIP4844(tx) => tx,
1627            _ => unreachable!(),
1628        };
1629
1630        assert_eq!(tx.tx().tx().to, address!("0x11E9CA82A3a762b4B5bd264d4173a242e7a77064"));
1631
1632        assert_eq!(
1633            tx.tx().tx().blob_versioned_hashes,
1634            vec![
1635                b256!("0x012ec3d6f66766bedb002a190126b3549fce0047de0d4c25cffce0dc1c57921a"),
1636                b256!("0x0152d8e24762ff22b1cfd9f8c0683786a7ca63ba49973818b3d1e9512cd2cec4"),
1637                b256!("0x013b98c6c83e066d5b14af2b85199e3d4fc7d1e778dd53130d180f5077e2d1c7"),
1638                b256!("0x01148b495d6e859114e670ca54fb6e2657f0cbae5b08063605093a4b3dc9f8f1"),
1639                b256!("0x011ac212f13c5dff2b2c6b600a79635103d6f580a4221079951181b25c7e6549")
1640            ]
1641        );
1642
1643        let from = tx.recover_signer().unwrap();
1644        assert_eq!(from, address!("0xA83C816D4f9b2783761a22BA6FADB0eB0606D7B2"));
1645    }
1646
1647    #[test]
1648    fn test_decode_encode_deposit_tx() {
1649        // https://sepolia-optimism.etherscan.io/tx/0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
1650        let tx_hash: TxHash = "0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7"
1651            .parse::<TxHash>()
1652            .unwrap();
1653
1654        // https://sepolia-optimism.etherscan.io/getRawTx?tx=0xbf8b5f08c43e4b860715cd64fc0849bbce0d0ea20a76b269e7bc8886d112fca7
1655        let raw_tx = alloy_primitives::hex::decode(
1656            "7ef861a0dfd7ae78bf3c414cfaa77f13c0205c82eb9365e217b2daa3448c3156b69b27ac94778f2146f48179643473b82931c4cd7b8f153efd94778f2146f48179643473b82931c4cd7b8f153efd872386f26fc10000872386f26fc10000830186a08080",
1657        )
1658        .unwrap();
1659        let dep_tx = TypedTransaction::decode(&mut raw_tx.as_slice()).unwrap();
1660
1661        let mut encoded = Vec::new();
1662        dep_tx.encode_2718(&mut encoded);
1663
1664        assert_eq!(raw_tx, encoded);
1665
1666        assert_eq!(tx_hash, dep_tx.hash());
1667    }
1668
1669    #[test]
1670    fn can_recover_sender_not_normalized() {
1671        let bytes = hex::decode("f85f800182520894095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804").unwrap();
1672
1673        let Ok(TypedTransaction::Legacy(tx)) = TypedTransaction::decode(&mut &bytes[..]) else {
1674            panic!("decoding TypedTransaction failed");
1675        };
1676
1677        assert_eq!(tx.tx().input, Bytes::from(b""));
1678        assert_eq!(tx.tx().gas_price, 1);
1679        assert_eq!(tx.tx().gas_limit, 21000);
1680        assert_eq!(tx.tx().nonce, 0);
1681        if let TxKind::Call(to) = tx.tx().to {
1682            assert_eq!(
1683                to,
1684                "0x095e7baea6a6c7c4c2dfeb977efac326af552d87".parse::<Address>().unwrap()
1685            );
1686        } else {
1687            panic!("expected a call transaction");
1688        }
1689        assert_eq!(tx.tx().value, U256::from(0x0au64));
1690        assert_eq!(
1691            tx.recover_signer().unwrap(),
1692            "0f65fe9276bc9a24ae7083ae28e2660ef72df99e".parse::<Address>().unwrap()
1693        );
1694    }
1695
1696    #[test]
1697    fn encode_legacy_receipt() {
1698        let expected = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
1699
1700        let mut data = vec![];
1701        let receipt = TypedReceipt::Legacy(ReceiptWithBloom {
1702            receipt: Receipt {
1703                status: false.into(),
1704                cumulative_gas_used: 0x1,
1705                logs: vec![Log {
1706                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
1707                    data: LogData::new_unchecked(
1708                        vec![
1709                            B256::from_str(
1710                                "000000000000000000000000000000000000000000000000000000000000dead",
1711                            )
1712                            .unwrap(),
1713                            B256::from_str(
1714                                "000000000000000000000000000000000000000000000000000000000000beef",
1715                            )
1716                            .unwrap(),
1717                        ],
1718                        Bytes::from_str("0100ff").unwrap(),
1719                    ),
1720                }],
1721            },
1722            logs_bloom: [0; 256].into(),
1723        });
1724
1725        receipt.encode(&mut data);
1726
1727        // check that the rlp length equals the length of the expected rlp
1728        assert_eq!(receipt.length(), expected.len());
1729        assert_eq!(data, expected);
1730    }
1731
1732    #[test]
1733    fn decode_legacy_receipt() {
1734        let data = hex::decode("f901668001b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f85ff85d940000000000000000000000000000000000000011f842a0000000000000000000000000000000000000000000000000000000000000deada0000000000000000000000000000000000000000000000000000000000000beef830100ff").unwrap();
1735
1736        let expected = TypedReceipt::Legacy(ReceiptWithBloom {
1737            receipt: Receipt {
1738                status: false.into(),
1739                cumulative_gas_used: 0x1,
1740                logs: vec![Log {
1741                    address: Address::from_str("0000000000000000000000000000000000000011").unwrap(),
1742                    data: LogData::new_unchecked(
1743                        vec![
1744                            B256::from_str(
1745                                "000000000000000000000000000000000000000000000000000000000000dead",
1746                            )
1747                            .unwrap(),
1748                            B256::from_str(
1749                                "000000000000000000000000000000000000000000000000000000000000beef",
1750                            )
1751                            .unwrap(),
1752                        ],
1753                        Bytes::from_str("0100ff").unwrap(),
1754                    ),
1755                }],
1756            },
1757            logs_bloom: [0; 256].into(),
1758        });
1759
1760        let receipt = TypedReceipt::decode(&mut &data[..]).unwrap();
1761
1762        assert_eq!(receipt, expected);
1763    }
1764
1765    #[test]
1766    fn deser_to_type_tx() {
1767        let tx = r#"
1768        {
1769            "EIP1559": {
1770                "chainId": "0x7a69",
1771                "nonce": "0x0",
1772                "gas": "0x5209",
1773                "maxFeePerGas": "0x77359401",
1774                "maxPriorityFeePerGas": "0x1",
1775                "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
1776                "value": "0x0",
1777                "accessList": [],
1778                "input": "0x",
1779                "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
1780                "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
1781                "yParity": "0x0",
1782                "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515"
1783            }
1784        }"#;
1785
1786        let _typed_tx: TypedTransaction = serde_json::from_str(tx).unwrap();
1787    }
1788}