alloy_rpc_types_eth/transaction/
mod.rs

1//! RPC types for transactions
2
3use alloy_consensus::{
4    EthereumTxEnvelope, EthereumTypedTransaction, Signed, TxEip1559, TxEip2930, TxEip4844,
5    TxEip4844Variant, TxEip7702, TxEnvelope, TxLegacy, Typed2718,
6};
7use alloy_eips::{eip2718::Encodable2718, eip7702::SignedAuthorization};
8use alloy_network_primitives::TransactionResponse;
9use alloy_primitives::{Address, BlockHash, Bytes, ChainId, TxKind, B256, U256};
10
11use alloy_consensus::transaction::Recovered;
12pub use alloy_consensus::{
13    transaction::TransactionInfo, BlobTransactionSidecar, Receipt, ReceiptEnvelope,
14    ReceiptWithBloom, Transaction as TransactionTrait,
15};
16pub use alloy_consensus_any::AnyReceiptEnvelope;
17pub use alloy_eips::{
18    eip2930::{AccessList, AccessListItem, AccessListResult},
19    eip7702::Authorization,
20};
21
22mod error;
23pub use error::ConversionError;
24
25mod receipt;
26pub use receipt::TransactionReceipt;
27
28pub mod request;
29pub use request::{TransactionInput, TransactionInputKind, TransactionRequest};
30
31/// Serde-bincode-compat
32#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
33pub mod serde_bincode_compat {
34    pub use super::request::serde_bincode_compat::*;
35}
36
37/// Transaction object used in RPC.
38///
39/// This represents a transaction in RPC format (`eth_getTransactionByHash`) and contains the full
40/// transaction object and additional block metadata if the transaction has been mined.
41#[derive(Clone, Debug, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43#[cfg_attr(all(any(test, feature = "arbitrary"), feature = "k256"), derive(arbitrary::Arbitrary))]
44#[cfg_attr(
45    feature = "serde",
46    serde(
47        into = "tx_serde::TransactionSerdeHelper<T>",
48        try_from = "tx_serde::TransactionSerdeHelper<T>",
49        bound = "T: TransactionTrait + Clone + serde::Serialize + serde::de::DeserializeOwned"
50    )
51)]
52#[doc(alias = "Tx")]
53pub struct Transaction<T = TxEnvelope> {
54    /// The inner transaction object
55    pub inner: Recovered<T>,
56
57    /// Hash of block where transaction was included, `None` if pending
58    pub block_hash: Option<BlockHash>,
59
60    /// Number of block where transaction was included, `None` if pending
61    pub block_number: Option<u64>,
62
63    /// Transaction Index
64    pub transaction_index: Option<u64>,
65
66    /// Deprecated effective gas price value.
67    pub effective_gas_price: Option<u128>,
68}
69
70impl<T> Default for Transaction<T>
71where
72    T: Default,
73{
74    fn default() -> Self {
75        Self {
76            inner: Recovered::new_unchecked(Default::default(), Default::default()),
77            block_hash: Default::default(),
78            block_number: Default::default(),
79            transaction_index: Default::default(),
80            effective_gas_price: Default::default(),
81        }
82    }
83}
84
85impl<T> Transaction<T> {
86    /// Consumes the type and returns the wrapped transaction.
87    pub fn into_inner(self) -> T {
88        self.inner.into_inner()
89    }
90
91    /// Consumes the type and returns a [`Recovered`] transaction with the sender
92    pub fn into_recovered(self) -> Recovered<T> {
93        self.inner
94    }
95
96    /// Returns a `Recovered<&T>` with the transaction and the sender.
97    pub const fn as_recovered(&self) -> Recovered<&T> {
98        self.inner.as_recovered_ref()
99    }
100
101    /// Converts the transaction type to the given alternative that is `From<T>`
102    pub fn convert<U>(self) -> Transaction<U>
103    where
104        U: From<T>,
105    {
106        self.map(U::from)
107    }
108
109    /// Converts the transaction to the given alternative that is `TryFrom<T>`
110    ///
111    /// Returns the transaction with the new transaction type if all conversions were successful.
112    pub fn try_convert<U>(self) -> Result<Transaction<U>, U::Error>
113    where
114        U: TryFrom<T>,
115    {
116        self.try_map(U::try_from)
117    }
118
119    /// Applies the given closure to the inner transaction type.
120    pub fn map<Tx>(self, f: impl FnOnce(T) -> Tx) -> Transaction<Tx> {
121        let Self { inner, block_hash, block_number, transaction_index, effective_gas_price } = self;
122        Transaction {
123            inner: inner.map(f),
124            block_hash,
125            block_number,
126            transaction_index,
127            effective_gas_price,
128        }
129    }
130
131    /// Applies the given fallible closure to the inner transactions.
132    pub fn try_map<Tx, E>(self, f: impl FnOnce(T) -> Result<Tx, E>) -> Result<Transaction<Tx>, E> {
133        let Self { inner, block_hash, block_number, transaction_index, effective_gas_price } = self;
134        Ok(Transaction {
135            inner: inner.try_map(f)?,
136            block_hash,
137            block_number,
138            transaction_index,
139            effective_gas_price,
140        })
141    }
142}
143
144impl<T> AsRef<T> for Transaction<T> {
145    fn as_ref(&self) -> &T {
146        &self.inner
147    }
148}
149
150impl<T> Transaction<T>
151where
152    T: TransactionTrait,
153{
154    /// Returns true if the transaction is a legacy or 2930 transaction.
155    pub fn is_legacy_gas(&self) -> bool {
156        self.inner.gas_price().is_some()
157    }
158
159    /// Converts a consensus `tx` with an additional context `tx_info` into an RPC [`Transaction`].
160    pub fn from_transaction(tx: Recovered<T>, tx_info: TransactionInfo) -> Self {
161        let TransactionInfo {
162            block_hash, block_number, index: transaction_index, base_fee, ..
163        } = tx_info;
164        let effective_gas_price = base_fee
165            .map(|base_fee| {
166                tx.effective_tip_per_gas(base_fee).unwrap_or_default() + base_fee as u128
167            })
168            .unwrap_or_else(|| tx.max_fee_per_gas());
169
170        Self {
171            inner: tx,
172            block_hash,
173            block_number,
174            transaction_index,
175            effective_gas_price: Some(effective_gas_price),
176        }
177    }
178}
179
180impl<T> Transaction<T>
181where
182    T: TransactionTrait + Encodable2718,
183{
184    /// Returns the [`TransactionInfo`] for this transaction.
185    ///
186    /// This contains various metadata about the transaction and block context if available.
187    pub fn info(&self) -> TransactionInfo {
188        TransactionInfo {
189            hash: Some(self.tx_hash()),
190            index: self.transaction_index,
191            block_hash: self.block_hash,
192            block_number: self.block_number,
193            // We don't know the base fee of the block when we're constructing this from
194            // `Transaction`
195            base_fee: None,
196        }
197    }
198}
199
200impl<T> Transaction<T>
201where
202    T: Into<TransactionRequest>,
203{
204    /// Converts [Transaction] into [TransactionRequest].
205    ///
206    /// During this conversion data for [TransactionRequest::sidecar] is not
207    /// populated as it is not part of [Transaction].
208    pub fn into_request(self) -> TransactionRequest {
209        self.inner.into_inner().into()
210    }
211}
212
213impl<Eip4844> Transaction<EthereumTxEnvelope<Eip4844>> {
214    /// Consumes the transaction and returns it as [`Signed`] with [`EthereumTypedTransaction`] as
215    /// the transaction type.
216    pub fn into_signed(self) -> Signed<EthereumTypedTransaction<Eip4844>>
217    where
218        EthereumTypedTransaction<Eip4844>: From<Eip4844>,
219    {
220        self.inner.into_inner().into_signed()
221    }
222
223    /// Consumes the transaction and returns it a [`Recovered`] signed [`EthereumTypedTransaction`].
224    pub fn into_signed_recovered(self) -> Recovered<Signed<EthereumTypedTransaction<Eip4844>>>
225    where
226        EthereumTypedTransaction<Eip4844>: From<Eip4844>,
227    {
228        self.inner.map(|tx| tx.into_signed())
229    }
230}
231
232impl<T> From<&Transaction<T>> for TransactionInfo
233where
234    T: TransactionTrait + Encodable2718,
235{
236    fn from(tx: &Transaction<T>) -> Self {
237        tx.info()
238    }
239}
240
241impl<T> From<Transaction<T>> for Recovered<T> {
242    fn from(tx: Transaction<T>) -> Self {
243        tx.into_recovered()
244    }
245}
246
247impl<Eip4844> TryFrom<Transaction<EthereumTxEnvelope<Eip4844>>> for Signed<TxLegacy> {
248    type Error = ConversionError;
249
250    fn try_from(tx: Transaction<EthereumTxEnvelope<Eip4844>>) -> Result<Self, Self::Error> {
251        match tx.inner.into_inner() {
252            EthereumTxEnvelope::Legacy(tx) => Ok(tx),
253            tx => Err(ConversionError::Custom(format!("expected Legacy, got {}", tx.tx_type()))),
254        }
255    }
256}
257
258impl<Eip4844> TryFrom<Transaction<EthereumTxEnvelope<Eip4844>>> for Signed<TxEip1559> {
259    type Error = ConversionError;
260
261    fn try_from(tx: Transaction<EthereumTxEnvelope<Eip4844>>) -> Result<Self, Self::Error> {
262        match tx.inner.into_inner() {
263            EthereumTxEnvelope::Eip1559(tx) => Ok(tx),
264            tx => Err(ConversionError::Custom(format!("expected Eip1559, got {}", tx.tx_type()))),
265        }
266    }
267}
268
269impl<Eip4844> TryFrom<Transaction<EthereumTxEnvelope<Eip4844>>> for Signed<TxEip2930> {
270    type Error = ConversionError;
271
272    fn try_from(tx: Transaction<EthereumTxEnvelope<Eip4844>>) -> Result<Self, Self::Error> {
273        match tx.inner.into_inner() {
274            EthereumTxEnvelope::Eip2930(tx) => Ok(tx),
275            tx => Err(ConversionError::Custom(format!("expected Eip2930, got {}", tx.tx_type()))),
276        }
277    }
278}
279
280impl TryFrom<Transaction> for Signed<TxEip4844> {
281    type Error = ConversionError;
282
283    fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
284        let tx: Signed<TxEip4844Variant> = tx.try_into()?;
285
286        let (tx, sig, hash) = tx.into_parts();
287
288        Ok(Self::new_unchecked(tx.into(), sig, hash))
289    }
290}
291
292impl TryFrom<Transaction> for Signed<TxEip4844Variant> {
293    type Error = ConversionError;
294
295    fn try_from(tx: Transaction) -> Result<Self, Self::Error> {
296        match tx.inner.into_inner() {
297            TxEnvelope::Eip4844(tx) => Ok(tx),
298            tx => Err(ConversionError::Custom(format!(
299                "expected TxEip4844Variant, got {}",
300                tx.tx_type()
301            ))),
302        }
303    }
304}
305
306impl<Eip4844> TryFrom<Transaction<EthereumTxEnvelope<Eip4844>>> for Signed<TxEip7702> {
307    type Error = ConversionError;
308
309    fn try_from(tx: Transaction<EthereumTxEnvelope<Eip4844>>) -> Result<Self, Self::Error> {
310        match tx.inner.into_inner() {
311            EthereumTxEnvelope::Eip7702(tx) => Ok(tx),
312            tx => Err(ConversionError::Custom(format!("expected Eip7702, got {}", tx.tx_type()))),
313        }
314    }
315}
316
317impl<Eip4844, Other> From<Transaction<EthereumTxEnvelope<Eip4844>>> for EthereumTxEnvelope<Other>
318where
319    Self: From<EthereumTxEnvelope<Eip4844>>,
320{
321    fn from(tx: Transaction<EthereumTxEnvelope<Eip4844>>) -> Self {
322        tx.inner.into_inner().into()
323    }
324}
325
326impl<Eip4844, Other> From<Transaction<EthereumTypedTransaction<Eip4844>>>
327    for EthereumTypedTransaction<Other>
328where
329    Self: From<EthereumTypedTransaction<Eip4844>>,
330{
331    fn from(tx: Transaction<EthereumTypedTransaction<Eip4844>>) -> Self {
332        tx.inner.into_inner().into()
333    }
334}
335
336impl<Eip4844> From<Transaction<EthereumTxEnvelope<Eip4844>>>
337    for Signed<EthereumTypedTransaction<Eip4844>>
338where
339    EthereumTypedTransaction<Eip4844>: From<Eip4844>,
340{
341    fn from(tx: Transaction<EthereumTxEnvelope<Eip4844>>) -> Self {
342        tx.into_signed()
343    }
344}
345
346impl<T: TransactionTrait> TransactionTrait for Transaction<T> {
347    fn chain_id(&self) -> Option<ChainId> {
348        self.inner.chain_id()
349    }
350
351    fn nonce(&self) -> u64 {
352        self.inner.nonce()
353    }
354
355    fn gas_limit(&self) -> u64 {
356        self.inner.gas_limit()
357    }
358
359    fn gas_price(&self) -> Option<u128> {
360        self.inner.gas_price()
361    }
362
363    fn max_fee_per_gas(&self) -> u128 {
364        self.inner.max_fee_per_gas()
365    }
366
367    fn max_priority_fee_per_gas(&self) -> Option<u128> {
368        self.inner.max_priority_fee_per_gas()
369    }
370
371    fn max_fee_per_blob_gas(&self) -> Option<u128> {
372        self.inner.max_fee_per_blob_gas()
373    }
374
375    fn priority_fee_or_price(&self) -> u128 {
376        self.inner.priority_fee_or_price()
377    }
378
379    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
380        self.inner.effective_gas_price(base_fee)
381    }
382
383    fn is_dynamic_fee(&self) -> bool {
384        self.inner.is_dynamic_fee()
385    }
386
387    fn kind(&self) -> TxKind {
388        self.inner.kind()
389    }
390
391    fn is_create(&self) -> bool {
392        self.inner.is_create()
393    }
394
395    fn value(&self) -> U256 {
396        self.inner.value()
397    }
398
399    fn input(&self) -> &Bytes {
400        self.inner.input()
401    }
402
403    fn access_list(&self) -> Option<&AccessList> {
404        self.inner.access_list()
405    }
406
407    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
408        self.inner.blob_versioned_hashes()
409    }
410
411    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
412        self.inner.authorization_list()
413    }
414}
415
416impl<T: TransactionTrait + Encodable2718> TransactionResponse for Transaction<T> {
417    fn tx_hash(&self) -> B256 {
418        self.inner.trie_hash()
419    }
420
421    fn block_hash(&self) -> Option<BlockHash> {
422        self.block_hash
423    }
424
425    fn block_number(&self) -> Option<u64> {
426        self.block_number
427    }
428
429    fn transaction_index(&self) -> Option<u64> {
430        self.transaction_index
431    }
432
433    fn from(&self) -> Address {
434        self.inner.signer()
435    }
436}
437
438impl<T: Typed2718> Typed2718 for Transaction<T> {
439    fn ty(&self) -> u8 {
440        self.inner.ty()
441    }
442}
443
444#[cfg(feature = "serde")]
445mod tx_serde {
446    //! Helper module for serializing and deserializing OP [`Transaction`].
447    //!
448    //! This is needed because we might need to deserialize the `gasPrice` field into both
449    //! [`crate::Transaction::effective_gas_price`] and [`alloy_consensus::TxLegacy::gas_price`].
450    use super::*;
451    use serde::{Deserialize, Serialize};
452
453    /// Helper struct which will be flattened into the transaction and will only contain `gasPrice`
454    /// field if inner [`TxEnvelope`] did not consume it.
455    #[derive(Serialize, Deserialize)]
456    struct MaybeGasPrice {
457        #[serde(
458            default,
459            rename = "gasPrice",
460            skip_serializing_if = "Option::is_none",
461            with = "alloy_serde::quantity::opt"
462        )]
463        pub effective_gas_price: Option<u128>,
464    }
465
466    #[derive(Serialize, Deserialize)]
467    #[serde(rename_all = "camelCase")]
468    pub(crate) struct TransactionSerdeHelper<T> {
469        #[serde(flatten)]
470        inner: T,
471        #[serde(default)]
472        block_hash: Option<BlockHash>,
473        #[serde(default, with = "alloy_serde::quantity::opt")]
474        block_number: Option<u64>,
475        #[serde(default, with = "alloy_serde::quantity::opt")]
476        transaction_index: Option<u64>,
477        /// Sender
478        from: Address,
479
480        #[serde(flatten)]
481        gas_price: MaybeGasPrice,
482    }
483
484    impl<T: TransactionTrait> From<Transaction<T>> for TransactionSerdeHelper<T> {
485        fn from(value: Transaction<T>) -> Self {
486            let Transaction {
487                inner,
488                block_hash,
489                block_number,
490                transaction_index,
491                effective_gas_price,
492            } = value;
493
494            let (inner, from) = inner.into_parts();
495
496            // if inner transaction has its own `gasPrice` don't serialize it in this struct.
497            let effective_gas_price = effective_gas_price.filter(|_| inner.gas_price().is_none());
498
499            Self {
500                inner,
501                block_hash,
502                block_number,
503                transaction_index,
504                from,
505                gas_price: MaybeGasPrice { effective_gas_price },
506            }
507        }
508    }
509
510    impl<T: TransactionTrait> TryFrom<TransactionSerdeHelper<T>> for Transaction<T> {
511        type Error = serde_json::Error;
512
513        fn try_from(value: TransactionSerdeHelper<T>) -> Result<Self, Self::Error> {
514            let TransactionSerdeHelper {
515                inner,
516                block_hash,
517                block_number,
518                transaction_index,
519                from,
520                gas_price,
521            } = value;
522
523            // Try to get `gasPrice` field from inner envelope or from `MaybeGasPrice`, otherwise
524            // return error
525            let effective_gas_price = inner.gas_price().or(gas_price.effective_gas_price);
526
527            Ok(Self {
528                inner: Recovered::new_unchecked(inner, from),
529                block_hash,
530                block_number,
531                transaction_index,
532                effective_gas_price,
533            })
534        }
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[allow(unused)]
543    fn assert_convert_into_envelope(tx: Transaction) -> TxEnvelope {
544        tx.into()
545    }
546    #[allow(unused)]
547    fn assert_convert_into_consensus(tx: Transaction) -> EthereumTxEnvelope<TxEip4844> {
548        tx.into()
549    }
550
551    #[allow(unused)]
552    fn assert_convert_into_typed(
553        tx: Transaction<EthereumTypedTransaction<TxEip4844>>,
554    ) -> EthereumTypedTransaction<TxEip4844> {
555        tx.into()
556    }
557
558    #[test]
559    #[cfg(feature = "serde")]
560    fn into_request_legacy() {
561        // cast rpc eth_getTransactionByHash
562        // 0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e --rpc-url mainnet
563        let rpc_tx = r#"{"blockHash":"0x8e38b4dbf6b11fcc3b9dee84fb7986e29ca0a02cecd8977c161ff7333329681e","blockNumber":"0xf4240","hash":"0xe9e91f1ee4b56c0df2e9f06c2b8c27c6076195a88a7b8537ba8313d80e6f124e","transactionIndex":"0x1","type":"0x0","nonce":"0x43eb","input":"0x","r":"0x3b08715b4403c792b8c7567edea634088bedcd7f60d9352b1f16c69830f3afd5","s":"0x10b9afb67d2ec8b956f0e1dbc07eb79152904f3a7bf789fc869db56320adfe09","chainId":"0x0","v":"0x1c","gas":"0xc350","from":"0x32be343b94f860124dc4fee278fdcbd38c102d88","to":"0xdf190dc7190dfba737d7777a163445b7fff16133","value":"0x6113a84987be800","gasPrice":"0xdf8475800"}"#;
564
565        let tx = serde_json::from_str::<Transaction>(rpc_tx).unwrap();
566        let request = tx.into_request();
567        assert!(request.gas_price.is_some());
568        assert!(request.max_fee_per_gas.is_none());
569    }
570
571    #[test]
572    #[cfg(feature = "serde")]
573    fn into_request_eip1559() {
574        // cast rpc eth_getTransactionByHash
575        // 0x0e07d8b53ed3d91314c80e53cf25bcde02084939395845cbb625b029d568135c --rpc-url mainnet
576        let rpc_tx = r#"{"blockHash":"0x883f974b17ca7b28cb970798d1c80f4d4bb427473dc6d39b2a7fe24edc02902d","blockNumber":"0xe26e6d","hash":"0x0e07d8b53ed3d91314c80e53cf25bcde02084939395845cbb625b029d568135c","accessList":[],"transactionIndex":"0xad","type":"0x2","nonce":"0x16d","input":"0x5ae401dc00000000000000000000000000000000000000000000000000000000628ced5b000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000000e442712a6700000000000000000000000000000000000000000000b3ff1489674e11c40000000000000000000000000000000000000000000000000000004a6ed55bbcc18000000000000000000000000000000000000000000000000000000000000000800000000000000000000000003cf412d970474804623bb4e3a42de13f9bca54360000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000003a75941763f31c930b19c041b709742b0b31ebb600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000412210e8a00000000000000000000000000000000000000000000000000000000","r":"0x7f2153019a74025d83a73effdd91503ceecefac7e35dd933adc1901c875539aa","s":"0x334ab2f714796d13c825fddf12aad01438db3a8152b2fe3ef7827707c25ecab3","chainId":"0x1","v":"0x0","gas":"0x46a02","maxPriorityFeePerGas":"0x59682f00","from":"0x3cf412d970474804623bb4e3a42de13f9bca5436","to":"0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45","maxFeePerGas":"0x7fc1a20a8","value":"0x4a6ed55bbcc180","gasPrice":"0x50101df3a"}"#;
577
578        let tx = serde_json::from_str::<Transaction>(rpc_tx).unwrap();
579        let request = tx.into_request();
580        assert!(request.gas_price.is_none());
581        assert!(request.max_fee_per_gas.is_some());
582    }
583
584    #[test]
585    #[cfg(feature = "serde")]
586    fn serde_tx_from_contract_mod() {
587        let rpc_tx = r#"{"hash":"0x018b2331d461a4aeedf6a1f9cc37463377578244e6a35216057a8370714e798f","nonce":"0x1","blockHash":"0x6e4e53d1de650d5a5ebed19b38321db369ef1dc357904284ecf4d89b8834969c","blockNumber":"0x2","transactionIndex":"0x0","from":"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266","to":"0x5fbdb2315678afecb367f032d93f642f64180aa3","value":"0x0","gasPrice":"0x3a29f0f8","gas":"0x1c9c380","maxFeePerGas":"0xba43b7400","maxPriorityFeePerGas":"0x5f5e100","input":"0xd09de08a","r":"0xd309309a59a49021281cb6bb41d164c96eab4e50f0c1bd24c03ca336e7bc2bb7","s":"0x28a7f089143d0a1355ebeb2a1b9f0e5ad9eca4303021c1400d61bc23c9ac5319","v":"0x0","yParity":"0x0","chainId":"0x7a69","accessList":[],"type":"0x2"}"#;
588
589        let tx = serde_json::from_str::<Transaction>(rpc_tx).unwrap();
590        assert_eq!(tx.block_number, Some(2));
591    }
592
593    #[test]
594    #[cfg(feature = "serde")]
595    fn test_gas_price_present() {
596        let blob_rpc_tx = r#"{"blockHash":"0x1732a5fe86d54098c431fa4fea34387b650e41dbff65ca554370028172fcdb6a","blockNumber":"0x3","from":"0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f","gas":"0x186a0","gasPrice":"0x281d620e","maxFeePerGas":"0x281d620e","maxPriorityFeePerGas":"0x1","maxFeePerBlobGas":"0x20000","hash":"0xb0ebf0d8fca6724d5111d0be9ac61f0e7bf174208e0fafcb653f337c72465b83","input":"0xdc4c8669df128318656d6974","nonce":"0x8","to":"0x7dcd17433742f4c0ca53122ab541d0ba67fc27df","transactionIndex":"0x0","value":"0x3","type":"0x3","accessList":[{"address":"0x7dcd17433742f4c0ca53122ab541d0ba67fc27df","storageKeys":["0x0000000000000000000000000000000000000000000000000000000000000000","0x462708a3c1cd03b21605715d090136df64e227f7e7792f74bb1bd7a8288f8801"]}],"chainId":"0xc72dd9d5e883e","blobVersionedHashes":["0x015a4cab4911426699ed34483de6640cf55a568afc5c5edffdcbd8bcd4452f68"],"v":"0x0","r":"0x478385a47075dd6ba56300b623038052a6e4bb03f8cfc53f367712f1c1d3e7de","s":"0x2f79ed9b154b0af2c97ddfc1f4f76e6c17725713b6d44ea922ca4c6bbc20775c","yParity":"0x0"}"#;
597        let legacy_rpc_tx = r#"{"blockHash":"0x7e5d03caac4eb2b613ae9c919ef3afcc8ed0e384f31ee746381d3c8739475d2a","blockNumber":"0x4","from":"0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f","gas":"0x5208","gasPrice":"0x23237dee","hash":"0x3f38cdc805c02e152bfed34471a3a13a786fed436b3aec0c3eca35d23e2cdd2c","input":"0x","nonce":"0xc","to":"0x4dde844b71bcdf95512fb4dc94e84fb67b512ed8","transactionIndex":"0x0","value":"0x1","type":"0x0","chainId":"0xc72dd9d5e883e","v":"0x18e5bb3abd10a0","r":"0x3d61f5d7e93eecd0669a31eb640ab3349e9e5868a44c2be1337c90a893b51990","s":"0xc55f44ba123af37d0e73ed75e578647c3f473805349936f64ea902ea9e03bc7"}"#;
598
599        let blob_tx = serde_json::from_str::<Transaction>(blob_rpc_tx).unwrap();
600        assert_eq!(blob_tx.block_number, Some(3));
601        assert_eq!(blob_tx.effective_gas_price, Some(0x281d620e));
602
603        let legacy_tx = serde_json::from_str::<Transaction>(legacy_rpc_tx).unwrap();
604        assert_eq!(legacy_tx.block_number, Some(4));
605        assert_eq!(legacy_tx.effective_gas_price, Some(0x23237dee));
606    }
607
608    // <https://github.com/alloy-rs/alloy/issues/1643>
609    #[test]
610    #[cfg(feature = "serde")]
611    fn deserialize_7702_v() {
612        let raw = r#"{"blockHash":"0xb14eac260f0cb7c3bbf4c9ff56034defa4f566780ed3e44b7a79b6365d02887c","blockNumber":"0xb022","from":"0x6d2d4e1c2326a069f36f5d6337470dc26adb7156","gas":"0xf8ac","gasPrice":"0xe07899f","maxFeePerGas":"0xe0789a0","maxPriorityFeePerGas":"0xe078998","hash":"0xadc3f24d05f05f1065debccb1c4b033eaa35917b69b343d88d9062cdf8ecad83","input":"0x","nonce":"0x1a","to":"0x6d2d4e1c2326a069f36f5d6337470dc26adb7156","transactionIndex":"0x0","value":"0x0","type":"0x4","accessList":[],"chainId":"0x1a5ee289c","authorizationList":[{"chainId":"0x1a5ee289c","address":"0x529f773125642b12a44bd543005650989eceaa2a","nonce":"0x1a","v":"0x0","r":"0x9b3de20cf8bd07f3c5c55c38c920c146f081bc5ab4580d0c87786b256cdab3c2","s":"0x74841956f4832bace3c02aed34b8f0a2812450da3728752edbb5b5e1da04497"}],"v":"0x1","r":"0xb3bf7d6877864913bba04d6f93d98009a5af16ee9c12295cd634962a2346b67c","s":"0x31ca4a874afa964ec7643e58c6b56b35b1bcc7698eb1b5e15e61e78b353bd42d","yParity":"0x1"}"#;
613        let tx = serde_json::from_str::<Transaction>(raw).unwrap();
614        assert!(tx.inner.is_eip7702());
615    }
616}