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