Skip to main content

alloy_network/any/
either.rs

1use crate::{UnknownTxEnvelope, UnknownTypedTransaction};
2use alloy_consensus::{
3    error::ValueError, transaction::Either, SignableTransaction, Signed,
4    Transaction as TransactionTrait, TxEip1559, TxEip2930, TxEip4844Variant, TxEip7702, TxEnvelope,
5    TxLegacy, Typed2718, TypedTransaction,
6};
7use alloy_eips::{
8    eip2718::{Decodable2718, Encodable2718},
9    eip7702::SignedAuthorization,
10};
11use alloy_primitives::{bytes::BufMut, Bytes, ChainId, Signature, B256, U256};
12use alloy_rpc_types_eth::{AccessList, TransactionRequest};
13use alloy_serde::WithOtherFields;
14
15/// Unsigned transaction type for a catch-all network.
16#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17#[serde(untagged)]
18#[doc(alias = "AnyTypedTx")]
19pub enum AnyTypedTransaction {
20    /// An Ethereum transaction.
21    Ethereum(TypedTransaction),
22    /// A transaction with unknown type.
23    Unknown(UnknownTypedTransaction),
24}
25
26impl From<UnknownTypedTransaction> for AnyTypedTransaction {
27    fn from(value: UnknownTypedTransaction) -> Self {
28        Self::Unknown(value)
29    }
30}
31
32impl From<TypedTransaction> for AnyTypedTransaction {
33    fn from(value: TypedTransaction) -> Self {
34        Self::Ethereum(value)
35    }
36}
37
38impl From<AnyTxEnvelope> for AnyTypedTransaction {
39    fn from(value: AnyTxEnvelope) -> Self {
40        match value {
41            AnyTxEnvelope::Ethereum(tx) => Self::Ethereum(tx.into()),
42            AnyTxEnvelope::Unknown(UnknownTxEnvelope { inner, .. }) => inner.into(),
43        }
44    }
45}
46
47impl From<AnyTypedTransaction> for WithOtherFields<TransactionRequest> {
48    fn from(value: AnyTypedTransaction) -> Self {
49        match value {
50            AnyTypedTransaction::Ethereum(tx) => Self::new(tx.into()),
51            AnyTypedTransaction::Unknown(UnknownTypedTransaction { ty, mut fields, .. }) => {
52                fields.insert("type".to_string(), serde_json::Value::Number(ty.0.into()));
53                Self { inner: Default::default(), other: fields }
54            }
55        }
56    }
57}
58
59impl From<AnyTxEnvelope> for WithOtherFields<TransactionRequest> {
60    fn from(value: AnyTxEnvelope) -> Self {
61        AnyTypedTransaction::from(value).into()
62    }
63}
64
65impl TransactionTrait for AnyTypedTransaction {
66    #[inline]
67    fn chain_id(&self) -> Option<ChainId> {
68        match self {
69            Self::Ethereum(inner) => inner.chain_id(),
70            Self::Unknown(inner) => inner.chain_id(),
71        }
72    }
73
74    #[inline]
75    fn nonce(&self) -> u64 {
76        match self {
77            Self::Ethereum(inner) => inner.nonce(),
78            Self::Unknown(inner) => inner.nonce(),
79        }
80    }
81
82    #[inline]
83    fn gas_limit(&self) -> u64 {
84        match self {
85            Self::Ethereum(inner) => inner.gas_limit(),
86            Self::Unknown(inner) => inner.gas_limit(),
87        }
88    }
89
90    #[inline]
91    fn gas_price(&self) -> Option<u128> {
92        match self {
93            Self::Ethereum(inner) => inner.gas_price(),
94            Self::Unknown(inner) => inner.gas_price(),
95        }
96    }
97
98    #[inline]
99    fn max_fee_per_gas(&self) -> u128 {
100        match self {
101            Self::Ethereum(inner) => inner.max_fee_per_gas(),
102            Self::Unknown(inner) => inner.max_fee_per_gas(),
103        }
104    }
105
106    #[inline]
107    fn max_priority_fee_per_gas(&self) -> Option<u128> {
108        match self {
109            Self::Ethereum(inner) => inner.max_priority_fee_per_gas(),
110            Self::Unknown(inner) => inner.max_priority_fee_per_gas(),
111        }
112    }
113
114    #[inline]
115    fn max_fee_per_blob_gas(&self) -> Option<u128> {
116        match self {
117            Self::Ethereum(inner) => inner.max_fee_per_blob_gas(),
118            Self::Unknown(inner) => inner.max_fee_per_blob_gas(),
119        }
120    }
121
122    #[inline]
123    fn priority_fee_or_price(&self) -> u128 {
124        self.max_priority_fee_per_gas().or_else(|| self.gas_price()).unwrap_or_default()
125    }
126
127    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
128        match self {
129            Self::Ethereum(inner) => inner.effective_gas_price(base_fee),
130            Self::Unknown(inner) => inner.effective_gas_price(base_fee),
131        }
132    }
133
134    #[inline]
135    fn is_dynamic_fee(&self) -> bool {
136        match self {
137            Self::Ethereum(inner) => inner.is_dynamic_fee(),
138            Self::Unknown(inner) => inner.is_dynamic_fee(),
139        }
140    }
141
142    fn kind(&self) -> alloy_primitives::TxKind {
143        match self {
144            Self::Ethereum(inner) => inner.kind(),
145            Self::Unknown(inner) => inner.kind(),
146        }
147    }
148
149    #[inline]
150    fn is_create(&self) -> bool {
151        match self {
152            Self::Ethereum(inner) => inner.is_create(),
153            Self::Unknown(inner) => inner.is_create(),
154        }
155    }
156
157    #[inline]
158    fn value(&self) -> U256 {
159        match self {
160            Self::Ethereum(inner) => inner.value(),
161            Self::Unknown(inner) => inner.value(),
162        }
163    }
164
165    #[inline]
166    fn input(&self) -> &Bytes {
167        match self {
168            Self::Ethereum(inner) => inner.input(),
169            Self::Unknown(inner) => inner.input(),
170        }
171    }
172
173    #[inline]
174    fn access_list(&self) -> Option<&AccessList> {
175        match self {
176            Self::Ethereum(inner) => inner.access_list(),
177            Self::Unknown(inner) => inner.access_list(),
178        }
179    }
180
181    #[inline]
182    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
183        match self {
184            Self::Ethereum(inner) => inner.blob_versioned_hashes(),
185            Self::Unknown(inner) => inner.blob_versioned_hashes(),
186        }
187    }
188
189    #[inline]
190    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
191        match self {
192            Self::Ethereum(inner) => inner.authorization_list(),
193            Self::Unknown(inner) => inner.authorization_list(),
194        }
195    }
196}
197
198impl Typed2718 for AnyTypedTransaction {
199    fn ty(&self) -> u8 {
200        match self {
201            Self::Ethereum(inner) => inner.ty(),
202            Self::Unknown(inner) => inner.ty(),
203        }
204    }
205}
206
207impl SignableTransaction<Signature> for AnyTypedTransaction {
208    fn set_chain_id(&mut self, chain_id: ChainId) {
209        match self {
210            Self::Ethereum(typed_tx) => typed_tx.set_chain_id(chain_id),
211            Self::Unknown(_) => (),
212        }
213    }
214
215    fn encode_for_signing(&self, out: &mut dyn BufMut) {
216        match self {
217            Self::Ethereum(typed_tx) => typed_tx.encode_for_signing(out),
218            Self::Unknown(_) => (),
219        }
220    }
221
222    fn payload_len_for_signature(&self) -> usize {
223        match self {
224            Self::Ethereum(typed_tx) => typed_tx.payload_len_for_signature(),
225            Self::Unknown(_) => 0,
226        }
227    }
228}
229
230/// Transaction envelope for a catch-all network.
231#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
232#[serde(untagged)]
233#[doc(alias = "AnyTransactionEnvelope")]
234pub enum AnyTxEnvelope {
235    /// An Ethereum transaction.
236    Ethereum(TxEnvelope),
237    /// A transaction with unknown type.
238    Unknown(UnknownTxEnvelope),
239}
240
241impl From<Signed<AnyTypedTransaction>> for AnyTxEnvelope {
242    fn from(value: Signed<AnyTypedTransaction>) -> Self {
243        let sig = *value.signature();
244        let tx = value.strip_signature();
245        match tx {
246            AnyTypedTransaction::Ethereum(typed_tx) => Self::Ethereum(match typed_tx {
247                TypedTransaction::Legacy(tx) => TxEnvelope::Legacy(tx.into_signed(sig)),
248                TypedTransaction::Eip2930(tx) => TxEnvelope::Eip2930(tx.into_signed(sig)),
249                TypedTransaction::Eip1559(tx) => TxEnvelope::Eip1559(tx.into_signed(sig)),
250                TypedTransaction::Eip4844(tx) => TxEnvelope::Eip4844(tx.into_signed(sig)),
251                TypedTransaction::Eip7702(tx) => TxEnvelope::Eip7702(tx.into_signed(sig)),
252            }),
253            AnyTypedTransaction::Unknown(unknown_tx) => {
254                Self::Unknown(UnknownTxEnvelope { hash: B256::ZERO, inner: unknown_tx })
255            }
256        }
257    }
258}
259
260impl AnyTxEnvelope {
261    /// Returns true if this is the ethereum transaction variant
262    pub const fn is_ethereum(&self) -> bool {
263        matches!(self, Self::Ethereum(_))
264    }
265
266    /// Returns true if this is the unknown transaction variant
267    pub const fn is_unknown(&self) -> bool {
268        matches!(self, Self::Unknown(_))
269    }
270
271    /// Returns the inner Ethereum transaction envelope, if it is an Ethereum transaction.
272    pub const fn as_envelope(&self) -> Option<&TxEnvelope> {
273        match self {
274            Self::Ethereum(inner) => Some(inner),
275            Self::Unknown(_) => None,
276        }
277    }
278
279    /// Returns the inner [`UnknownTxEnvelope`] if it is an unknown variant.
280    pub const fn as_unknown(&self) -> Option<&UnknownTxEnvelope> {
281        match self {
282            Self::Unknown(inner) => Some(inner),
283            Self::Ethereum(_) => None,
284        }
285    }
286
287    /// Returns the inner Ethereum transaction envelope, if it is an Ethereum transaction.
288    /// If the transaction is not an Ethereum transaction, it is returned as an error.
289    pub fn try_into_envelope(self) -> Result<TxEnvelope, ValueError<Self>> {
290        match self {
291            Self::Ethereum(inner) => Ok(inner),
292            this => Err(ValueError::new_static(this, "unknown transaction envelope")),
293        }
294    }
295
296    /// Returns the inner [`UnknownTxEnvelope`], if it is an unknown transaction.
297    /// If the transaction is not an unknown transaction, it is returned as an error.
298    pub fn try_into_unknown(self) -> Result<UnknownTxEnvelope, Self> {
299        match self {
300            Self::Unknown(inner) => Ok(inner),
301            this => Err(this),
302        }
303    }
304
305    /// Attempts to convert the [`UnknownTxEnvelope`] into `Either::Right` if this is an unknown
306    /// variant.
307    ///
308    /// Returns `Either::Left` with the ethereum `TxEnvelope` if this is the
309    /// [`AnyTxEnvelope::Ethereum`] variant and [`Either::Right`] with the converted variant.
310    ///
311    /// # Examples
312    ///
313    /// ```no_run
314    /// # use alloy_network::any::AnyTxEnvelope;
315    /// # use alloy_consensus::transaction::Either;
316    /// # // Assuming you have a custom type: struct CustomTx;
317    /// # // impl TryFrom<alloy_network::any::UnknownTxEnvelope> for CustomTx { ... }
318    /// # fn example(envelope: AnyTxEnvelope) -> Result<(), Box<dyn std::error::Error>> {
319    /// # struct CustomTx;
320    /// # impl TryFrom<alloy_network::any::UnknownTxEnvelope> for CustomTx {
321    /// #     type Error = String;
322    /// #     fn try_from(_: alloy_network::any::UnknownTxEnvelope) -> Result<Self, Self::Error> {
323    /// #         Ok(CustomTx)
324    /// #     }
325    /// # }
326    /// match envelope.try_into_either::<CustomTx>()? {
327    ///     Either::Left(eth_tx) => { /* Ethereum transaction */ }
328    ///     Either::Right(custom_tx) => { /* Custom transaction */ }
329    /// }
330    /// # Ok(())
331    /// # }
332    /// ```
333    pub fn try_into_either<T>(self) -> Result<Either<TxEnvelope, T>, T::Error>
334    where
335        T: TryFrom<UnknownTxEnvelope>,
336    {
337        self.try_map_unknown(|inner| inner.try_into())
338    }
339
340    /// Attempts to convert the [`UnknownTxEnvelope`] into `Either::Right` if this is an
341    /// [`AnyTxEnvelope::Unknown`].
342    ///
343    /// Returns `Either::Left` with the ethereum `TxEnvelope` if this is the
344    /// [`AnyTxEnvelope::Ethereum`] variant and [`Either::Right`] with the converted variant.
345    ///
346    /// Use this for custom conversion logic when [`try_into_either`](Self::try_into_either) is
347    /// insufficient.
348    ///
349    /// # Examples
350    ///
351    /// ```no_run
352    /// # use alloy_network::any::AnyTxEnvelope;
353    /// # use alloy_consensus::transaction::Either;
354    /// # use alloy_primitives::B256;
355    /// # fn example(envelope: AnyTxEnvelope) -> Result<(), Box<dyn std::error::Error>> {
356    /// let result = envelope.try_map_unknown(|unknown| Ok::<B256, String>(unknown.hash))?;
357    /// match result {
358    ///     Either::Left(eth_tx) => { /* Ethereum */ }
359    ///     Either::Right(hash) => { /* Unknown tx hash */ }
360    /// }
361    /// # Ok(())
362    /// # }
363    /// ```
364    pub fn try_map_unknown<T, E>(
365        self,
366        f: impl FnOnce(UnknownTxEnvelope) -> Result<T, E>,
367    ) -> Result<Either<TxEnvelope, T>, E> {
368        match self {
369            Self::Ethereum(tx) => Ok(Either::Left(tx)),
370            Self::Unknown(tx) => Ok(Either::Right(f(tx)?)),
371        }
372    }
373
374    /// Returns the [`TxLegacy`] variant if the transaction is a legacy transaction.
375    pub const fn as_legacy(&self) -> Option<&Signed<TxLegacy>> {
376        match self.as_envelope() {
377            Some(TxEnvelope::Legacy(tx)) => Some(tx),
378            _ => None,
379        }
380    }
381
382    /// Returns the [`TxEip2930`] variant if the transaction is an EIP-2930 transaction.
383    pub const fn as_eip2930(&self) -> Option<&Signed<TxEip2930>> {
384        match self.as_envelope() {
385            Some(TxEnvelope::Eip2930(tx)) => Some(tx),
386            _ => None,
387        }
388    }
389
390    /// Returns the [`TxEip1559`] variant if the transaction is an EIP-1559 transaction.
391    pub const fn as_eip1559(&self) -> Option<&Signed<TxEip1559>> {
392        match self.as_envelope() {
393            Some(TxEnvelope::Eip1559(tx)) => Some(tx),
394            _ => None,
395        }
396    }
397
398    /// Returns the [`TxEip4844Variant`] variant if the transaction is an EIP-4844 transaction.
399    pub const fn as_eip4844(&self) -> Option<&Signed<TxEip4844Variant>> {
400        match self.as_envelope() {
401            Some(TxEnvelope::Eip4844(tx)) => Some(tx),
402            _ => None,
403        }
404    }
405
406    /// Returns the [`TxEip7702`] variant if the transaction is an EIP-7702 transaction.
407    pub const fn as_eip7702(&self) -> Option<&Signed<TxEip7702>> {
408        match self.as_envelope() {
409            Some(TxEnvelope::Eip7702(tx)) => Some(tx),
410            _ => None,
411        }
412    }
413
414    /// Returns true if the transaction is a legacy transaction.
415    #[inline]
416    pub const fn is_legacy(&self) -> bool {
417        matches!(self.as_envelope(), Some(TxEnvelope::Legacy(_)))
418    }
419
420    /// Returns true if the transaction is an EIP-2930 transaction.
421    #[inline]
422    pub const fn is_eip2930(&self) -> bool {
423        matches!(self.as_envelope(), Some(TxEnvelope::Eip2930(_)))
424    }
425
426    /// Returns true if the transaction is an EIP-1559 transaction.
427    #[inline]
428    pub const fn is_eip1559(&self) -> bool {
429        matches!(self.as_envelope(), Some(TxEnvelope::Eip1559(_)))
430    }
431
432    /// Returns true if the transaction is an EIP-4844 transaction.
433    #[inline]
434    pub const fn is_eip4844(&self) -> bool {
435        matches!(self.as_envelope(), Some(TxEnvelope::Eip4844(_)))
436    }
437
438    /// Returns true if the transaction is an EIP-7702 transaction.
439    #[inline]
440    pub const fn is_eip7702(&self) -> bool {
441        matches!(self.as_envelope(), Some(TxEnvelope::Eip7702(_)))
442    }
443}
444
445impl Typed2718 for AnyTxEnvelope {
446    fn ty(&self) -> u8 {
447        match self {
448            Self::Ethereum(inner) => inner.ty(),
449            Self::Unknown(inner) => inner.ty(),
450        }
451    }
452}
453
454impl Encodable2718 for AnyTxEnvelope {
455    fn encode_2718_len(&self) -> usize {
456        match self {
457            Self::Ethereum(t) => t.encode_2718_len(),
458            Self::Unknown(_) => 1,
459        }
460    }
461
462    #[track_caller]
463    fn encode_2718(&self, out: &mut dyn alloy_primitives::bytes::BufMut) {
464        match self {
465            Self::Ethereum(t) => t.encode_2718(out),
466            Self::Unknown(inner) => {
467                panic!(
468                    "Attempted to encode unknown transaction type: {}. This is not a bug in alloy. To encode or decode unknown transaction types, use a custom Transaction type and a custom Network implementation. See https://docs.rs/alloy-network/latest/alloy_network/ for network documentation.",
469                    inner.as_ref().ty
470                )
471            }
472        }
473    }
474
475    fn trie_hash(&self) -> B256 {
476        match self {
477            Self::Ethereum(tx) => tx.trie_hash(),
478            Self::Unknown(inner) => inner.hash,
479        }
480    }
481}
482
483impl Decodable2718 for AnyTxEnvelope {
484    fn typed_decode(ty: u8, buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Self> {
485        TxEnvelope::typed_decode(ty, buf).map(Self::Ethereum)
486    }
487
488    fn fallback_decode(buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Self> {
489        TxEnvelope::fallback_decode(buf).map(Self::Ethereum)
490    }
491}
492
493impl TransactionTrait for AnyTxEnvelope {
494    #[inline]
495    fn chain_id(&self) -> Option<ChainId> {
496        match self {
497            Self::Ethereum(inner) => inner.chain_id(),
498            Self::Unknown(inner) => inner.chain_id(),
499        }
500    }
501
502    #[inline]
503    fn nonce(&self) -> u64 {
504        match self {
505            Self::Ethereum(inner) => inner.nonce(),
506            Self::Unknown(inner) => inner.nonce(),
507        }
508    }
509
510    #[inline]
511    fn gas_limit(&self) -> u64 {
512        match self {
513            Self::Ethereum(inner) => inner.gas_limit(),
514            Self::Unknown(inner) => inner.gas_limit(),
515        }
516    }
517
518    #[inline]
519    fn gas_price(&self) -> Option<u128> {
520        match self {
521            Self::Ethereum(inner) => inner.gas_price(),
522            Self::Unknown(inner) => inner.gas_price(),
523        }
524    }
525
526    #[inline]
527    fn max_fee_per_gas(&self) -> u128 {
528        match self {
529            Self::Ethereum(inner) => inner.max_fee_per_gas(),
530            Self::Unknown(inner) => inner.max_fee_per_gas(),
531        }
532    }
533
534    #[inline]
535    fn max_priority_fee_per_gas(&self) -> Option<u128> {
536        match self {
537            Self::Ethereum(inner) => inner.max_priority_fee_per_gas(),
538            Self::Unknown(inner) => inner.max_priority_fee_per_gas(),
539        }
540    }
541
542    #[inline]
543    fn max_fee_per_blob_gas(&self) -> Option<u128> {
544        match self {
545            Self::Ethereum(inner) => inner.max_fee_per_blob_gas(),
546            Self::Unknown(inner) => inner.max_fee_per_blob_gas(),
547        }
548    }
549
550    #[inline]
551    fn priority_fee_or_price(&self) -> u128 {
552        self.max_priority_fee_per_gas().or_else(|| self.gas_price()).unwrap_or_default()
553    }
554
555    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
556        match self {
557            Self::Ethereum(inner) => inner.effective_gas_price(base_fee),
558            Self::Unknown(inner) => inner.effective_gas_price(base_fee),
559        }
560    }
561
562    #[inline]
563    fn is_dynamic_fee(&self) -> bool {
564        match self {
565            Self::Ethereum(inner) => inner.is_dynamic_fee(),
566            Self::Unknown(inner) => inner.is_dynamic_fee(),
567        }
568    }
569
570    fn kind(&self) -> alloy_primitives::TxKind {
571        match self {
572            Self::Ethereum(inner) => inner.kind(),
573            Self::Unknown(inner) => inner.kind(),
574        }
575    }
576
577    #[inline]
578    fn is_create(&self) -> bool {
579        match self {
580            Self::Ethereum(inner) => inner.is_create(),
581            Self::Unknown(inner) => inner.is_create(),
582        }
583    }
584
585    #[inline]
586    fn value(&self) -> U256 {
587        match self {
588            Self::Ethereum(inner) => inner.value(),
589            Self::Unknown(inner) => inner.value(),
590        }
591    }
592
593    #[inline]
594    fn input(&self) -> &Bytes {
595        match self {
596            Self::Ethereum(inner) => inner.input(),
597            Self::Unknown(inner) => inner.input(),
598        }
599    }
600
601    #[inline]
602    fn access_list(&self) -> Option<&AccessList> {
603        match self {
604            Self::Ethereum(inner) => inner.access_list(),
605            Self::Unknown(inner) => inner.access_list(),
606        }
607    }
608
609    #[inline]
610    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
611        match self {
612            Self::Ethereum(inner) => inner.blob_versioned_hashes(),
613            Self::Unknown(inner) => inner.blob_versioned_hashes(),
614        }
615    }
616
617    #[inline]
618    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
619        match self {
620            Self::Ethereum(inner) => inner.authorization_list(),
621            Self::Unknown(inner) => inner.authorization_list(),
622        }
623    }
624}