alloy_consensus/transaction/
typed.rs

1pub use crate::transaction::envelope::EthereumTypedTransaction;
2use crate::{
3    error::ValueError,
4    private::alloy_eips::eip2718::Eip2718Error,
5    transaction::{
6        eip4844::{TxEip4844, TxEip4844Variant, TxEip4844WithSidecar},
7        RlpEcdsaDecodableTx, RlpEcdsaEncodableTx,
8    },
9    EthereumTxEnvelope, SignableTransaction, Signed, TxEip1559, TxEip2930, TxEip7702, TxLegacy,
10    TxType,
11};
12use alloy_eips::{eip2718::Eip2718Result, Typed2718};
13use alloy_primitives::{ChainId, Signature, TxHash};
14use alloy_rlp::{Buf, BufMut, Decodable};
15
16/// Basic typed transaction which can contain both [`TxEip4844`] and [`TxEip4844WithSidecar`].
17pub type TypedTransaction = EthereumTypedTransaction<TxEip4844Variant>;
18
19impl<Eip4844> From<TxLegacy> for EthereumTypedTransaction<Eip4844> {
20    fn from(tx: TxLegacy) -> Self {
21        Self::Legacy(tx)
22    }
23}
24
25impl<Eip4844> From<TxEip2930> for EthereumTypedTransaction<Eip4844> {
26    fn from(tx: TxEip2930) -> Self {
27        Self::Eip2930(tx)
28    }
29}
30
31impl<Eip4844> From<TxEip1559> for EthereumTypedTransaction<Eip4844> {
32    fn from(tx: TxEip1559) -> Self {
33        Self::Eip1559(tx)
34    }
35}
36
37impl<Eip4844: From<TxEip4844>> From<TxEip4844> for EthereumTypedTransaction<Eip4844> {
38    fn from(tx: TxEip4844) -> Self {
39        Self::Eip4844(tx.into())
40    }
41}
42
43impl<T, Eip4844: From<TxEip4844WithSidecar<T>>> From<TxEip4844WithSidecar<T>>
44    for EthereumTypedTransaction<Eip4844>
45{
46    fn from(tx: TxEip4844WithSidecar<T>) -> Self {
47        Self::Eip4844(tx.into())
48    }
49}
50
51impl<Sidecar, Eip4844: From<TxEip4844Variant<Sidecar>>> From<TxEip4844Variant<Sidecar>>
52    for EthereumTypedTransaction<Eip4844>
53{
54    fn from(tx: TxEip4844Variant<Sidecar>) -> Self {
55        Self::Eip4844(tx.into())
56    }
57}
58
59impl<Eip4844> From<TxEip7702> for EthereumTypedTransaction<Eip4844> {
60    fn from(tx: TxEip7702) -> Self {
61        Self::Eip7702(tx)
62    }
63}
64
65impl<Eip4844> From<EthereumTxEnvelope<Eip4844>> for EthereumTypedTransaction<Eip4844> {
66    fn from(envelope: EthereumTxEnvelope<Eip4844>) -> Self {
67        match envelope {
68            EthereumTxEnvelope::Legacy(tx) => Self::Legacy(tx.strip_signature()),
69            EthereumTxEnvelope::Eip2930(tx) => Self::Eip2930(tx.strip_signature()),
70            EthereumTxEnvelope::Eip1559(tx) => Self::Eip1559(tx.strip_signature()),
71            EthereumTxEnvelope::Eip4844(tx) => Self::Eip4844(tx.strip_signature()),
72            EthereumTxEnvelope::Eip7702(tx) => Self::Eip7702(tx.strip_signature()),
73        }
74    }
75}
76
77impl<T> From<EthereumTypedTransaction<TxEip4844WithSidecar<T>>>
78    for EthereumTypedTransaction<TxEip4844>
79{
80    fn from(value: EthereumTypedTransaction<TxEip4844WithSidecar<T>>) -> Self {
81        value.map_eip4844(|eip4844| eip4844.into())
82    }
83}
84
85impl<T> From<EthereumTypedTransaction<TxEip4844Variant<T>>>
86    for EthereumTypedTransaction<TxEip4844>
87{
88    fn from(value: EthereumTypedTransaction<TxEip4844Variant<T>>) -> Self {
89        value.map_eip4844(|eip4844| eip4844.into())
90    }
91}
92
93impl<T> From<EthereumTypedTransaction<TxEip4844>>
94    for EthereumTypedTransaction<TxEip4844Variant<T>>
95{
96    fn from(value: EthereumTypedTransaction<TxEip4844>) -> Self {
97        value.map_eip4844(|eip4844| eip4844.into())
98    }
99}
100
101impl<Eip4844> EthereumTypedTransaction<Eip4844> {
102    /// Converts the EIP-4844 variant of this transaction with the given closure.
103    ///
104    /// This is intended to convert between the EIP-4844 variants, specifically for stripping away
105    /// non consensus data (blob sidecar data).
106    pub fn map_eip4844<U>(self, mut f: impl FnMut(Eip4844) -> U) -> EthereumTypedTransaction<U> {
107        match self {
108            Self::Legacy(tx) => EthereumTypedTransaction::Legacy(tx),
109            Self::Eip2930(tx) => EthereumTypedTransaction::Eip2930(tx),
110            Self::Eip1559(tx) => EthereumTypedTransaction::Eip1559(tx),
111            Self::Eip4844(tx) => EthereumTypedTransaction::Eip4844(f(tx)),
112            Self::Eip7702(tx) => EthereumTypedTransaction::Eip7702(tx),
113        }
114    }
115
116    /// Converts this typed transaction into a signed [`EthereumTxEnvelope`]
117    pub fn into_envelope(self, signature: Signature) -> EthereumTxEnvelope<Eip4844> {
118        match self {
119            Self::Legacy(tx) => EthereumTxEnvelope::Legacy(tx.into_signed(signature)),
120            Self::Eip2930(tx) => EthereumTxEnvelope::Eip2930(tx.into_signed(signature)),
121            Self::Eip1559(tx) => EthereumTxEnvelope::Eip1559(tx.into_signed(signature)),
122            Self::Eip4844(tx) => EthereumTxEnvelope::Eip4844(Signed::new_unhashed(tx, signature)),
123            Self::Eip7702(tx) => EthereumTxEnvelope::Eip7702(tx.into_signed(signature)),
124        }
125    }
126}
127
128impl<Eip4844: RlpEcdsaDecodableTx> EthereumTypedTransaction<Eip4844> {
129    /// Decode an unsigned typed transaction from RLP bytes.
130    pub fn decode_unsigned(buf: &mut &[u8]) -> Eip2718Result<Self> {
131        if buf.is_empty() {
132            return Err(alloy_rlp::Error::InputTooShort.into());
133        }
134
135        let first_byte = buf[0];
136
137        // Eip2718: legacy transactions start with >= 0xc0
138        if first_byte >= 0xc0 {
139            return Ok(Self::Legacy(TxLegacy::decode(buf)?));
140        }
141
142        let tx_type = buf.get_u8();
143        match tx_type {
144            0x00 => Ok(Self::Legacy(TxLegacy::decode(buf)?)),
145            0x01 => Ok(Self::Eip2930(TxEip2930::decode(buf)?)),
146            0x02 => Ok(Self::Eip1559(TxEip1559::decode(buf)?)),
147            0x03 => Ok(Self::Eip4844(Eip4844::rlp_decode(buf)?)),
148            0x04 => Ok(Self::Eip7702(TxEip7702::decode(buf)?)),
149            _ => Err(Eip2718Error::UnexpectedType(tx_type)),
150        }
151    }
152}
153
154impl<Eip4844: RlpEcdsaEncodableTx> EthereumTypedTransaction<Eip4844> {
155    /// Return the [`TxType`] of the inner txn.
156    #[doc(alias = "transaction_type")]
157    pub const fn tx_type(&self) -> TxType {
158        match self {
159            Self::Legacy(_) => TxType::Legacy,
160            Self::Eip2930(_) => TxType::Eip2930,
161            Self::Eip1559(_) => TxType::Eip1559,
162            Self::Eip4844(_) => TxType::Eip4844,
163            Self::Eip7702(_) => TxType::Eip7702,
164        }
165    }
166
167    /// Return the inner legacy transaction if it exists.
168    pub const fn legacy(&self) -> Option<&TxLegacy> {
169        match self {
170            Self::Legacy(tx) => Some(tx),
171            _ => None,
172        }
173    }
174
175    /// Return the inner EIP-2930 transaction if it exists.
176    pub const fn eip2930(&self) -> Option<&TxEip2930> {
177        match self {
178            Self::Eip2930(tx) => Some(tx),
179            _ => None,
180        }
181    }
182
183    /// Return the inner EIP-1559 transaction if it exists.
184    pub const fn eip1559(&self) -> Option<&TxEip1559> {
185        match self {
186            Self::Eip1559(tx) => Some(tx),
187            _ => None,
188        }
189    }
190
191    /// Return the inner EIP-7702 transaction if it exists.
192    pub const fn eip7702(&self) -> Option<&TxEip7702> {
193        match self {
194            Self::Eip7702(tx) => Some(tx),
195            _ => None,
196        }
197    }
198
199    /// Consumes the type and returns the [`TxLegacy`] if this transaction is of that type.
200    pub fn try_into_legacy(self) -> Result<TxLegacy, ValueError<Self>> {
201        match self {
202            Self::Legacy(tx) => Ok(tx),
203            _ => Err(ValueError::new(self, "Expected legacy transaction")),
204        }
205    }
206
207    /// Consumes the type and returns the [`TxEip2930`]if this transaction is of that type.
208    pub fn try_into_eip2930(self) -> Result<TxEip2930, ValueError<Self>> {
209        match self {
210            Self::Eip2930(tx) => Ok(tx),
211            _ => Err(ValueError::new(self, "Expected EIP-2930 transaction")),
212        }
213    }
214
215    /// Consumes the type and returns the EIP-4844 if this transaction is of that type.
216    pub fn try_into_eip4844(self) -> Result<Eip4844, ValueError<Self>> {
217        match self {
218            Self::Eip4844(tx) => Ok(tx),
219            _ => Err(ValueError::new(self, "Expected EIP-4844 transaction")),
220        }
221    }
222
223    /// Consumes the type and returns the EIP-7702 if this transaction is of that type.
224    pub fn try_into_eip7702(self) -> Result<TxEip7702, ValueError<Self>> {
225        match self {
226            Self::Eip7702(tx) => Ok(tx),
227            _ => Err(ValueError::new(self, "Expected EIP-7702 transaction")),
228        }
229    }
230
231    /// Calculate the transaction hash for the given signature.
232    pub fn tx_hash(&self, signature: &Signature) -> TxHash {
233        match self {
234            Self::Legacy(tx) => tx.tx_hash(signature),
235            Self::Eip2930(tx) => tx.tx_hash(signature),
236            Self::Eip1559(tx) => tx.tx_hash(signature),
237            Self::Eip4844(tx) => tx.tx_hash(signature),
238            Self::Eip7702(tx) => tx.tx_hash(signature),
239        }
240    }
241}
242
243impl<Eip4844: RlpEcdsaEncodableTx + Typed2718> RlpEcdsaEncodableTx
244    for EthereumTypedTransaction<Eip4844>
245{
246    fn rlp_encoded_fields_length(&self) -> usize {
247        match self {
248            Self::Legacy(tx) => tx.rlp_encoded_fields_length(),
249            Self::Eip2930(tx) => tx.rlp_encoded_fields_length(),
250            Self::Eip1559(tx) => tx.rlp_encoded_fields_length(),
251            Self::Eip4844(tx) => tx.rlp_encoded_fields_length(),
252            Self::Eip7702(tx) => tx.rlp_encoded_fields_length(),
253        }
254    }
255
256    fn rlp_encode_fields(&self, out: &mut dyn alloy_rlp::BufMut) {
257        match self {
258            Self::Legacy(tx) => tx.rlp_encode_fields(out),
259            Self::Eip2930(tx) => tx.rlp_encode_fields(out),
260            Self::Eip1559(tx) => tx.rlp_encode_fields(out),
261            Self::Eip4844(tx) => tx.rlp_encode_fields(out),
262            Self::Eip7702(tx) => tx.rlp_encode_fields(out),
263        }
264    }
265
266    fn eip2718_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
267        match self {
268            Self::Legacy(tx) => tx.eip2718_encode_with_type(signature, tx.ty(), out),
269            Self::Eip2930(tx) => tx.eip2718_encode_with_type(signature, tx.ty(), out),
270            Self::Eip1559(tx) => tx.eip2718_encode_with_type(signature, tx.ty(), out),
271            Self::Eip4844(tx) => tx.eip2718_encode_with_type(signature, tx.ty(), out),
272            Self::Eip7702(tx) => tx.eip2718_encode_with_type(signature, tx.ty(), out),
273        }
274    }
275
276    fn eip2718_encode(&self, signature: &Signature, out: &mut dyn BufMut) {
277        match self {
278            Self::Legacy(tx) => tx.eip2718_encode(signature, out),
279            Self::Eip2930(tx) => tx.eip2718_encode(signature, out),
280            Self::Eip1559(tx) => tx.eip2718_encode(signature, out),
281            Self::Eip4844(tx) => tx.eip2718_encode(signature, out),
282            Self::Eip7702(tx) => tx.eip2718_encode(signature, out),
283        }
284    }
285
286    fn network_encode_with_type(&self, signature: &Signature, _ty: u8, out: &mut dyn BufMut) {
287        match self {
288            Self::Legacy(tx) => tx.network_encode_with_type(signature, tx.ty(), out),
289            Self::Eip2930(tx) => tx.network_encode_with_type(signature, tx.ty(), out),
290            Self::Eip1559(tx) => tx.network_encode_with_type(signature, tx.ty(), out),
291            Self::Eip4844(tx) => tx.network_encode_with_type(signature, tx.ty(), out),
292            Self::Eip7702(tx) => tx.network_encode_with_type(signature, tx.ty(), out),
293        }
294    }
295
296    fn network_encode(&self, signature: &Signature, out: &mut dyn BufMut) {
297        match self {
298            Self::Legacy(tx) => tx.network_encode(signature, out),
299            Self::Eip2930(tx) => tx.network_encode(signature, out),
300            Self::Eip1559(tx) => tx.network_encode(signature, out),
301            Self::Eip4844(tx) => tx.network_encode(signature, out),
302            Self::Eip7702(tx) => tx.network_encode(signature, out),
303        }
304    }
305
306    fn tx_hash_with_type(&self, signature: &Signature, _ty: u8) -> TxHash {
307        match self {
308            Self::Legacy(tx) => tx.tx_hash_with_type(signature, tx.ty()),
309            Self::Eip2930(tx) => tx.tx_hash_with_type(signature, tx.ty()),
310            Self::Eip1559(tx) => tx.tx_hash_with_type(signature, tx.ty()),
311            Self::Eip4844(tx) => tx.tx_hash_with_type(signature, tx.ty()),
312            Self::Eip7702(tx) => tx.tx_hash_with_type(signature, tx.ty()),
313        }
314    }
315
316    fn tx_hash(&self, signature: &Signature) -> TxHash {
317        match self {
318            Self::Legacy(tx) => tx.tx_hash(signature),
319            Self::Eip2930(tx) => tx.tx_hash(signature),
320            Self::Eip1559(tx) => tx.tx_hash(signature),
321            Self::Eip4844(tx) => tx.tx_hash(signature),
322            Self::Eip7702(tx) => tx.tx_hash(signature),
323        }
324    }
325}
326
327impl<Eip4844: SignableTransaction<Signature>> SignableTransaction<Signature>
328    for EthereumTypedTransaction<Eip4844>
329{
330    fn set_chain_id(&mut self, chain_id: ChainId) {
331        match self {
332            Self::Legacy(tx) => tx.set_chain_id(chain_id),
333            Self::Eip2930(tx) => tx.set_chain_id(chain_id),
334            Self::Eip1559(tx) => tx.set_chain_id(chain_id),
335            Self::Eip4844(tx) => tx.set_chain_id(chain_id),
336            Self::Eip7702(tx) => tx.set_chain_id(chain_id),
337        }
338    }
339
340    fn encode_for_signing(&self, out: &mut dyn BufMut) {
341        match self {
342            Self::Legacy(tx) => tx.encode_for_signing(out),
343            Self::Eip2930(tx) => tx.encode_for_signing(out),
344            Self::Eip1559(tx) => tx.encode_for_signing(out),
345            Self::Eip4844(tx) => tx.encode_for_signing(out),
346            Self::Eip7702(tx) => tx.encode_for_signing(out),
347        }
348    }
349
350    fn payload_len_for_signature(&self) -> usize {
351        match self {
352            Self::Legacy(tx) => tx.payload_len_for_signature(),
353            Self::Eip2930(tx) => tx.payload_len_for_signature(),
354            Self::Eip1559(tx) => tx.payload_len_for_signature(),
355            Self::Eip4844(tx) => tx.payload_len_for_signature(),
356            Self::Eip7702(tx) => tx.payload_len_for_signature(),
357        }
358    }
359}
360
361#[cfg(feature = "serde")]
362impl<Eip4844, T: From<EthereumTypedTransaction<Eip4844>>> From<EthereumTypedTransaction<Eip4844>>
363    for alloy_serde::WithOtherFields<T>
364{
365    fn from(value: EthereumTypedTransaction<Eip4844>) -> Self {
366        Self::new(value.into())
367    }
368}
369
370#[cfg(feature = "serde")]
371impl<Eip4844, T> From<EthereumTxEnvelope<Eip4844>> for alloy_serde::WithOtherFields<T>
372where
373    T: From<EthereumTxEnvelope<Eip4844>>,
374{
375    fn from(value: EthereumTxEnvelope<Eip4844>) -> Self {
376        Self::new(value.into())
377    }
378}
379
380/// Bincode-compatible [`EthereumTypedTransaction`] serde implementation.
381#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
382pub(crate) mod serde_bincode_compat {
383    use alloc::borrow::Cow;
384    use serde::{Deserialize, Deserializer, Serialize, Serializer};
385    use serde_with::{DeserializeAs, SerializeAs};
386
387    /// Bincode-compatible [`super::EthereumTypedTransaction`] serde implementation.
388    ///
389    /// Intended to use with the [`serde_with::serde_as`] macro in the following way:
390    /// ```rust
391    /// use alloy_consensus::{serde_bincode_compat, EthereumTypedTransaction};
392    /// use serde::{de::DeserializeOwned, Deserialize, Serialize};
393    /// use serde_with::serde_as;
394    ///
395    /// #[serde_as]
396    /// #[derive(Serialize, Deserialize)]
397    /// struct Data<T: Serialize + DeserializeOwned + Clone + 'static> {
398    ///     #[serde_as(as = "serde_bincode_compat::EthereumTypedTransaction<'_, T>")]
399    ///     receipt: EthereumTypedTransaction<T>,
400    /// }
401    /// ```
402    #[derive(Debug, Serialize, Deserialize)]
403    pub enum EthereumTypedTransaction<'a, Eip4844: Clone = crate::transaction::TxEip4844> {
404        /// Legacy transaction
405        Legacy(crate::serde_bincode_compat::transaction::TxLegacy<'a>),
406        /// EIP-2930 transaction
407        Eip2930(crate::serde_bincode_compat::transaction::TxEip2930<'a>),
408        /// EIP-1559 transaction
409        Eip1559(crate::serde_bincode_compat::transaction::TxEip1559<'a>),
410        /// EIP-4844 transaction
411        /// Note: assumes EIP4844 is bincode compatible, which it is because no flatten or skipped
412        /// fields.
413        Eip4844(Cow<'a, Eip4844>),
414        /// EIP-7702 transaction
415        Eip7702(crate::serde_bincode_compat::transaction::TxEip7702<'a>),
416    }
417
418    impl<'a, T: Clone> From<&'a super::EthereumTypedTransaction<T>>
419        for EthereumTypedTransaction<'a, T>
420    {
421        fn from(value: &'a super::EthereumTypedTransaction<T>) -> Self {
422            match value {
423                super::EthereumTypedTransaction::Legacy(tx) => Self::Legacy(tx.into()),
424                super::EthereumTypedTransaction::Eip2930(tx) => Self::Eip2930(tx.into()),
425                super::EthereumTypedTransaction::Eip1559(tx) => Self::Eip1559(tx.into()),
426                super::EthereumTypedTransaction::Eip4844(tx) => Self::Eip4844(Cow::Borrowed(tx)),
427                super::EthereumTypedTransaction::Eip7702(tx) => Self::Eip7702(tx.into()),
428            }
429        }
430    }
431
432    impl<'a, T: Clone> From<EthereumTypedTransaction<'a, T>> for super::EthereumTypedTransaction<T> {
433        fn from(value: EthereumTypedTransaction<'a, T>) -> Self {
434            match value {
435                EthereumTypedTransaction::Legacy(tx) => Self::Legacy(tx.into()),
436                EthereumTypedTransaction::Eip2930(tx) => Self::Eip2930(tx.into()),
437                EthereumTypedTransaction::Eip1559(tx) => Self::Eip1559(tx.into()),
438                EthereumTypedTransaction::Eip4844(tx) => Self::Eip4844(tx.into_owned()),
439                EthereumTypedTransaction::Eip7702(tx) => Self::Eip7702(tx.into()),
440            }
441        }
442    }
443
444    impl<T: Serialize + Clone> SerializeAs<super::EthereumTypedTransaction<T>>
445        for EthereumTypedTransaction<'_, T>
446    {
447        fn serialize_as<S>(
448            source: &super::EthereumTypedTransaction<T>,
449            serializer: S,
450        ) -> Result<S::Ok, S::Error>
451        where
452            S: Serializer,
453        {
454            EthereumTypedTransaction::<'_, T>::from(source).serialize(serializer)
455        }
456    }
457
458    impl<'de, T: Deserialize<'de> + Clone> DeserializeAs<'de, super::EthereumTypedTransaction<T>>
459        for EthereumTypedTransaction<'de, T>
460    {
461        fn deserialize_as<D>(
462            deserializer: D,
463        ) -> Result<super::EthereumTypedTransaction<T>, D::Error>
464        where
465            D: Deserializer<'de>,
466        {
467            EthereumTypedTransaction::<'_, T>::deserialize(deserializer).map(Into::into)
468        }
469    }
470
471    #[cfg(test)]
472    mod tests {
473        use super::super::{serde_bincode_compat, EthereumTypedTransaction};
474        use crate::TxEip4844;
475        use arbitrary::Arbitrary;
476        use bincode::config;
477        use rand::Rng;
478        use serde::{Deserialize, Serialize};
479        use serde_with::serde_as;
480
481        #[test]
482        fn test_typed_tx_bincode_roundtrip() {
483            #[serde_as]
484            #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
485            struct Data {
486                #[serde_as(as = "serde_bincode_compat::EthereumTypedTransaction<'_>")]
487                transaction: EthereumTypedTransaction<TxEip4844>,
488            }
489
490            let mut bytes = [0u8; 1024];
491            rand::thread_rng().fill(bytes.as_mut_slice());
492            let data = Data {
493                transaction: EthereumTypedTransaction::arbitrary(
494                    &mut arbitrary::Unstructured::new(&bytes),
495                )
496                .unwrap(),
497            };
498
499            let encoded = bincode::serde::encode_to_vec(&data, config::legacy()).unwrap();
500            let (decoded, _) =
501                bincode::serde::decode_from_slice::<Data, _>(&encoded, config::legacy()).unwrap();
502            assert_eq!(decoded, data);
503        }
504    }
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use alloy_eips::eip2930::AccessList;
511    use alloy_primitives::{Address, Bytes, U256};
512
513    #[test]
514    fn test_decode_unsigned_all_types() {
515        let transactions = [
516            TypedTransaction::Legacy(TxLegacy {
517                chain_id: Some(1),
518                nonce: 0,
519                gas_price: 1,
520                gas_limit: 2,
521                to: Address::ZERO.into(),
522                value: U256::ZERO,
523                input: Bytes::default(),
524            }),
525            TypedTransaction::Eip2930(TxEip2930 {
526                chain_id: 1,
527                nonce: 0,
528                gas_price: 1,
529                gas_limit: 2,
530                to: Address::ZERO.into(),
531                value: U256::ZERO,
532                input: Bytes::default(),
533                access_list: AccessList::default(),
534            }),
535            TypedTransaction::Eip1559(TxEip1559 {
536                chain_id: 1,
537                nonce: 0,
538                gas_limit: 1,
539                max_fee_per_gas: 2,
540                max_priority_fee_per_gas: 3,
541                to: Address::ZERO.into(),
542                value: U256::ZERO,
543                input: Bytes::default(),
544                access_list: AccessList::default(),
545            }),
546            TypedTransaction::Eip7702(TxEip7702 {
547                chain_id: 1,
548                nonce: 0,
549                gas_limit: 1,
550                max_fee_per_gas: 2,
551                max_priority_fee_per_gas: 3,
552                to: Address::ZERO,
553                value: U256::ZERO,
554                input: Bytes::default(),
555                access_list: AccessList::default(),
556                authorization_list: vec![],
557            }),
558        ];
559
560        for tx in transactions {
561            let mut encoded = Vec::new();
562            tx.encode_for_signing(&mut encoded);
563            let decoded = TypedTransaction::decode_unsigned(&mut encoded.as_slice()).unwrap();
564            assert_eq!(decoded, tx);
565        }
566    }
567
568    #[test]
569    fn test_decode_unsigned_invalid_type() {
570        let invalid = vec![0x99, 0xc0];
571        let result = TypedTransaction::decode_unsigned(&mut invalid.as_slice());
572        assert!(result.is_err());
573        if let Err(err) = result {
574            assert!(matches!(err, alloy_eips::eip2718::Eip2718Error::UnexpectedType(0x99)));
575        }
576    }
577
578    #[test]
579    fn test_decode_unsigned_encode_stability() {
580        // Verify that decode(encode(tx)) == tx and encode(decode(encode(tx))) == encode(tx)
581        let tx = TypedTransaction::Eip1559(TxEip1559 {
582            chain_id: 1,
583            nonce: 100,
584            gas_limit: 50000,
585            max_fee_per_gas: 30_000_000_000,
586            max_priority_fee_per_gas: 2_000_000_000,
587            to: Address::random().into(),
588            value: U256::from(1_000_000),
589            input: Bytes::from(vec![1, 2, 3]),
590            access_list: AccessList::default(),
591        });
592
593        // Encode
594        let mut encoded = Vec::new();
595        tx.encode_for_signing(&mut encoded);
596
597        // Decode
598        let decoded = TypedTransaction::decode_unsigned(&mut encoded.as_slice()).unwrap();
599        assert_eq!(decoded, tx);
600
601        // Re-encode
602        let mut re_encoded = Vec::new();
603        decoded.encode_for_signing(&mut re_encoded);
604
605        assert_eq!(encoded, re_encoded);
606    }
607}