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#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17#[serde(untagged)]
18#[doc(alias = "AnyTypedTx")]
19pub enum AnyTypedTransaction {
20 Ethereum(TypedTransaction),
22 Unknown(UnknownTypedTransaction),
24}
25
26#[cold]
27#[track_caller]
28fn panic_unknown_transaction_signing(ty: u8) -> ! {
29 panic!("unknown transaction type 0x{ty:02x} cannot be signed via AnyTypedTransaction")
30}
31
32#[cold]
33#[track_caller]
34fn panic_unknown_transaction_encoding(ty: u8) -> ! {
35 panic!(
36 "Attempted to encode unknown transaction type 0x{ty:02x}. 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."
37 )
38}
39
40impl From<UnknownTypedTransaction> for AnyTypedTransaction {
41 fn from(value: UnknownTypedTransaction) -> Self {
42 Self::Unknown(value)
43 }
44}
45
46impl From<TypedTransaction> for AnyTypedTransaction {
47 fn from(value: TypedTransaction) -> Self {
48 Self::Ethereum(value)
49 }
50}
51
52impl From<AnyTxEnvelope> for AnyTypedTransaction {
53 fn from(value: AnyTxEnvelope) -> Self {
54 match value {
55 AnyTxEnvelope::Ethereum(tx) => Self::Ethereum(tx.into()),
56 AnyTxEnvelope::Unknown(UnknownTxEnvelope { inner, .. }) => inner.into(),
57 }
58 }
59}
60
61impl From<AnyTypedTransaction> for WithOtherFields<TransactionRequest> {
62 fn from(value: AnyTypedTransaction) -> Self {
63 match value {
64 AnyTypedTransaction::Ethereum(tx) => Self::new(tx.into()),
65 AnyTypedTransaction::Unknown(UnknownTypedTransaction { ty, mut fields, .. }) => {
66 fields.insert("type".to_string(), serde_json::Value::Number(ty.0.into()));
67 Self { inner: Default::default(), other: fields }
68 }
69 }
70 }
71}
72
73impl From<AnyTxEnvelope> for WithOtherFields<TransactionRequest> {
74 fn from(value: AnyTxEnvelope) -> Self {
75 AnyTypedTransaction::from(value).into()
76 }
77}
78
79impl TransactionTrait for AnyTypedTransaction {
80 #[inline]
81 fn chain_id(&self) -> Option<ChainId> {
82 match self {
83 Self::Ethereum(inner) => inner.chain_id(),
84 Self::Unknown(inner) => inner.chain_id(),
85 }
86 }
87
88 #[inline]
89 fn nonce(&self) -> u64 {
90 match self {
91 Self::Ethereum(inner) => inner.nonce(),
92 Self::Unknown(inner) => inner.nonce(),
93 }
94 }
95
96 #[inline]
97 fn gas_limit(&self) -> u64 {
98 match self {
99 Self::Ethereum(inner) => inner.gas_limit(),
100 Self::Unknown(inner) => inner.gas_limit(),
101 }
102 }
103
104 #[inline]
105 fn gas_price(&self) -> Option<u128> {
106 match self {
107 Self::Ethereum(inner) => inner.gas_price(),
108 Self::Unknown(inner) => inner.gas_price(),
109 }
110 }
111
112 #[inline]
113 fn max_fee_per_gas(&self) -> u128 {
114 match self {
115 Self::Ethereum(inner) => inner.max_fee_per_gas(),
116 Self::Unknown(inner) => inner.max_fee_per_gas(),
117 }
118 }
119
120 #[inline]
121 fn max_priority_fee_per_gas(&self) -> Option<u128> {
122 match self {
123 Self::Ethereum(inner) => inner.max_priority_fee_per_gas(),
124 Self::Unknown(inner) => inner.max_priority_fee_per_gas(),
125 }
126 }
127
128 #[inline]
129 fn max_fee_per_blob_gas(&self) -> Option<u128> {
130 match self {
131 Self::Ethereum(inner) => inner.max_fee_per_blob_gas(),
132 Self::Unknown(inner) => inner.max_fee_per_blob_gas(),
133 }
134 }
135
136 #[inline]
137 fn priority_fee_or_price(&self) -> u128 {
138 self.max_priority_fee_per_gas().or_else(|| self.gas_price()).unwrap_or_default()
139 }
140
141 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
142 match self {
143 Self::Ethereum(inner) => inner.effective_gas_price(base_fee),
144 Self::Unknown(inner) => inner.effective_gas_price(base_fee),
145 }
146 }
147
148 #[inline]
149 fn is_dynamic_fee(&self) -> bool {
150 match self {
151 Self::Ethereum(inner) => inner.is_dynamic_fee(),
152 Self::Unknown(inner) => inner.is_dynamic_fee(),
153 }
154 }
155
156 fn kind(&self) -> alloy_primitives::TxKind {
157 match self {
158 Self::Ethereum(inner) => inner.kind(),
159 Self::Unknown(inner) => inner.kind(),
160 }
161 }
162
163 #[inline]
164 fn is_create(&self) -> bool {
165 match self {
166 Self::Ethereum(inner) => inner.is_create(),
167 Self::Unknown(inner) => inner.is_create(),
168 }
169 }
170
171 #[inline]
172 fn value(&self) -> U256 {
173 match self {
174 Self::Ethereum(inner) => inner.value(),
175 Self::Unknown(inner) => inner.value(),
176 }
177 }
178
179 #[inline]
180 fn input(&self) -> &Bytes {
181 match self {
182 Self::Ethereum(inner) => inner.input(),
183 Self::Unknown(inner) => inner.input(),
184 }
185 }
186
187 #[inline]
188 fn access_list(&self) -> Option<&AccessList> {
189 match self {
190 Self::Ethereum(inner) => inner.access_list(),
191 Self::Unknown(inner) => inner.access_list(),
192 }
193 }
194
195 #[inline]
196 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
197 match self {
198 Self::Ethereum(inner) => inner.blob_versioned_hashes(),
199 Self::Unknown(inner) => inner.blob_versioned_hashes(),
200 }
201 }
202
203 #[inline]
204 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
205 match self {
206 Self::Ethereum(inner) => inner.authorization_list(),
207 Self::Unknown(inner) => inner.authorization_list(),
208 }
209 }
210}
211
212impl Typed2718 for AnyTypedTransaction {
213 fn ty(&self) -> u8 {
214 match self {
215 Self::Ethereum(inner) => inner.ty(),
216 Self::Unknown(inner) => inner.ty(),
217 }
218 }
219}
220
221impl SignableTransaction<Signature> for AnyTypedTransaction {
222 fn set_chain_id(&mut self, chain_id: ChainId) {
223 match self {
224 Self::Ethereum(typed_tx) => typed_tx.set_chain_id(chain_id),
225 Self::Unknown(unknown_tx) => panic_unknown_transaction_signing(unknown_tx.ty()),
226 }
227 }
228
229 fn encode_for_signing(&self, out: &mut dyn BufMut) {
230 match self {
231 Self::Ethereum(typed_tx) => typed_tx.encode_for_signing(out),
232 Self::Unknown(unknown_tx) => panic_unknown_transaction_signing(unknown_tx.ty()),
233 }
234 }
235
236 fn payload_len_for_signature(&self) -> usize {
237 match self {
238 Self::Ethereum(typed_tx) => typed_tx.payload_len_for_signature(),
239 Self::Unknown(unknown_tx) => panic_unknown_transaction_signing(unknown_tx.ty()),
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
246#[serde(untagged)]
247#[doc(alias = "AnyTransactionEnvelope")]
248pub enum AnyTxEnvelope {
249 Ethereum(TxEnvelope),
251 Unknown(UnknownTxEnvelope),
253}
254
255impl From<Signed<AnyTypedTransaction>> for AnyTxEnvelope {
256 fn from(value: Signed<AnyTypedTransaction>) -> Self {
257 let sig = *value.signature();
258 let tx = value.strip_signature();
259 match tx {
260 AnyTypedTransaction::Ethereum(typed_tx) => Self::Ethereum(match typed_tx {
261 TypedTransaction::Legacy(tx) => TxEnvelope::Legacy(tx.into_signed(sig)),
262 TypedTransaction::Eip2930(tx) => TxEnvelope::Eip2930(tx.into_signed(sig)),
263 TypedTransaction::Eip1559(tx) => TxEnvelope::Eip1559(tx.into_signed(sig)),
264 TypedTransaction::Eip4844(tx) => TxEnvelope::Eip4844(tx.into_signed(sig)),
265 TypedTransaction::Eip7702(tx) => TxEnvelope::Eip7702(tx.into_signed(sig)),
266 }),
267 AnyTypedTransaction::Unknown(unknown_tx) => {
268 panic_unknown_transaction_signing(unknown_tx.ty())
269 }
270 }
271 }
272}
273
274impl AnyTxEnvelope {
275 pub const fn is_ethereum(&self) -> bool {
277 matches!(self, Self::Ethereum(_))
278 }
279
280 pub const fn is_unknown(&self) -> bool {
282 matches!(self, Self::Unknown(_))
283 }
284
285 pub const fn as_envelope(&self) -> Option<&TxEnvelope> {
287 match self {
288 Self::Ethereum(inner) => Some(inner),
289 Self::Unknown(_) => None,
290 }
291 }
292
293 pub const fn as_unknown(&self) -> Option<&UnknownTxEnvelope> {
295 match self {
296 Self::Unknown(inner) => Some(inner),
297 Self::Ethereum(_) => None,
298 }
299 }
300
301 pub fn try_into_envelope(self) -> Result<TxEnvelope, ValueError<Self>> {
304 match self {
305 Self::Ethereum(inner) => Ok(inner),
306 this => Err(ValueError::new_static(this, "unknown transaction envelope")),
307 }
308 }
309
310 pub fn try_into_unknown(self) -> Result<UnknownTxEnvelope, Self> {
313 match self {
314 Self::Unknown(inner) => Ok(inner),
315 this => Err(this),
316 }
317 }
318
319 pub fn try_into_either<T>(self) -> Result<Either<TxEnvelope, T>, T::Error>
348 where
349 T: TryFrom<UnknownTxEnvelope>,
350 {
351 self.try_map_unknown(|inner| inner.try_into())
352 }
353
354 pub fn try_map_unknown<T, E>(
379 self,
380 f: impl FnOnce(UnknownTxEnvelope) -> Result<T, E>,
381 ) -> Result<Either<TxEnvelope, T>, E> {
382 match self {
383 Self::Ethereum(tx) => Ok(Either::Left(tx)),
384 Self::Unknown(tx) => Ok(Either::Right(f(tx)?)),
385 }
386 }
387
388 pub const fn as_legacy(&self) -> Option<&Signed<TxLegacy>> {
390 match self.as_envelope() {
391 Some(TxEnvelope::Legacy(tx)) => Some(tx),
392 _ => None,
393 }
394 }
395
396 pub const fn as_eip2930(&self) -> Option<&Signed<TxEip2930>> {
398 match self.as_envelope() {
399 Some(TxEnvelope::Eip2930(tx)) => Some(tx),
400 _ => None,
401 }
402 }
403
404 pub const fn as_eip1559(&self) -> Option<&Signed<TxEip1559>> {
406 match self.as_envelope() {
407 Some(TxEnvelope::Eip1559(tx)) => Some(tx),
408 _ => None,
409 }
410 }
411
412 pub const fn as_eip4844(&self) -> Option<&Signed<TxEip4844Variant>> {
414 match self.as_envelope() {
415 Some(TxEnvelope::Eip4844(tx)) => Some(tx),
416 _ => None,
417 }
418 }
419
420 pub const fn as_eip7702(&self) -> Option<&Signed<TxEip7702>> {
422 match self.as_envelope() {
423 Some(TxEnvelope::Eip7702(tx)) => Some(tx),
424 _ => None,
425 }
426 }
427
428 #[inline]
430 pub const fn is_legacy(&self) -> bool {
431 matches!(self.as_envelope(), Some(TxEnvelope::Legacy(_)))
432 }
433
434 #[inline]
436 pub const fn is_eip2930(&self) -> bool {
437 matches!(self.as_envelope(), Some(TxEnvelope::Eip2930(_)))
438 }
439
440 #[inline]
442 pub const fn is_eip1559(&self) -> bool {
443 matches!(self.as_envelope(), Some(TxEnvelope::Eip1559(_)))
444 }
445
446 #[inline]
448 pub const fn is_eip4844(&self) -> bool {
449 matches!(self.as_envelope(), Some(TxEnvelope::Eip4844(_)))
450 }
451
452 #[inline]
454 pub const fn is_eip7702(&self) -> bool {
455 matches!(self.as_envelope(), Some(TxEnvelope::Eip7702(_)))
456 }
457}
458
459impl Typed2718 for AnyTxEnvelope {
460 fn ty(&self) -> u8 {
461 match self {
462 Self::Ethereum(inner) => inner.ty(),
463 Self::Unknown(inner) => inner.ty(),
464 }
465 }
466}
467
468impl Encodable2718 for AnyTxEnvelope {
469 fn encode_2718_len(&self) -> usize {
470 match self {
471 Self::Ethereum(t) => t.encode_2718_len(),
472 Self::Unknown(inner) => panic_unknown_transaction_encoding(inner.ty()),
473 }
474 }
475
476 #[track_caller]
477 fn encode_2718(&self, out: &mut dyn alloy_primitives::bytes::BufMut) {
478 match self {
479 Self::Ethereum(t) => t.encode_2718(out),
480 Self::Unknown(inner) => panic_unknown_transaction_encoding(inner.ty()),
481 }
482 }
483
484 fn trie_hash(&self) -> B256 {
485 match self {
486 Self::Ethereum(tx) => tx.trie_hash(),
487 Self::Unknown(inner) => inner.hash,
488 }
489 }
490}
491
492impl Decodable2718 for AnyTxEnvelope {
493 fn typed_decode(ty: u8, buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Self> {
494 TxEnvelope::typed_decode(ty, buf).map(Self::Ethereum)
495 }
496
497 fn fallback_decode(buf: &mut &[u8]) -> alloy_eips::eip2718::Eip2718Result<Self> {
498 TxEnvelope::fallback_decode(buf).map(Self::Ethereum)
499 }
500}
501
502impl TransactionTrait for AnyTxEnvelope {
503 #[inline]
504 fn chain_id(&self) -> Option<ChainId> {
505 match self {
506 Self::Ethereum(inner) => inner.chain_id(),
507 Self::Unknown(inner) => inner.chain_id(),
508 }
509 }
510
511 #[inline]
512 fn nonce(&self) -> u64 {
513 match self {
514 Self::Ethereum(inner) => inner.nonce(),
515 Self::Unknown(inner) => inner.nonce(),
516 }
517 }
518
519 #[inline]
520 fn gas_limit(&self) -> u64 {
521 match self {
522 Self::Ethereum(inner) => inner.gas_limit(),
523 Self::Unknown(inner) => inner.gas_limit(),
524 }
525 }
526
527 #[inline]
528 fn gas_price(&self) -> Option<u128> {
529 match self {
530 Self::Ethereum(inner) => inner.gas_price(),
531 Self::Unknown(inner) => inner.gas_price(),
532 }
533 }
534
535 #[inline]
536 fn max_fee_per_gas(&self) -> u128 {
537 match self {
538 Self::Ethereum(inner) => inner.max_fee_per_gas(),
539 Self::Unknown(inner) => inner.max_fee_per_gas(),
540 }
541 }
542
543 #[inline]
544 fn max_priority_fee_per_gas(&self) -> Option<u128> {
545 match self {
546 Self::Ethereum(inner) => inner.max_priority_fee_per_gas(),
547 Self::Unknown(inner) => inner.max_priority_fee_per_gas(),
548 }
549 }
550
551 #[inline]
552 fn max_fee_per_blob_gas(&self) -> Option<u128> {
553 match self {
554 Self::Ethereum(inner) => inner.max_fee_per_blob_gas(),
555 Self::Unknown(inner) => inner.max_fee_per_blob_gas(),
556 }
557 }
558
559 #[inline]
560 fn priority_fee_or_price(&self) -> u128 {
561 self.max_priority_fee_per_gas().or_else(|| self.gas_price()).unwrap_or_default()
562 }
563
564 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
565 match self {
566 Self::Ethereum(inner) => inner.effective_gas_price(base_fee),
567 Self::Unknown(inner) => inner.effective_gas_price(base_fee),
568 }
569 }
570
571 #[inline]
572 fn is_dynamic_fee(&self) -> bool {
573 match self {
574 Self::Ethereum(inner) => inner.is_dynamic_fee(),
575 Self::Unknown(inner) => inner.is_dynamic_fee(),
576 }
577 }
578
579 fn kind(&self) -> alloy_primitives::TxKind {
580 match self {
581 Self::Ethereum(inner) => inner.kind(),
582 Self::Unknown(inner) => inner.kind(),
583 }
584 }
585
586 #[inline]
587 fn is_create(&self) -> bool {
588 match self {
589 Self::Ethereum(inner) => inner.is_create(),
590 Self::Unknown(inner) => inner.is_create(),
591 }
592 }
593
594 #[inline]
595 fn value(&self) -> U256 {
596 match self {
597 Self::Ethereum(inner) => inner.value(),
598 Self::Unknown(inner) => inner.value(),
599 }
600 }
601
602 #[inline]
603 fn input(&self) -> &Bytes {
604 match self {
605 Self::Ethereum(inner) => inner.input(),
606 Self::Unknown(inner) => inner.input(),
607 }
608 }
609
610 #[inline]
611 fn access_list(&self) -> Option<&AccessList> {
612 match self {
613 Self::Ethereum(inner) => inner.access_list(),
614 Self::Unknown(inner) => inner.access_list(),
615 }
616 }
617
618 #[inline]
619 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
620 match self {
621 Self::Ethereum(inner) => inner.blob_versioned_hashes(),
622 Self::Unknown(inner) => inner.blob_versioned_hashes(),
623 }
624 }
625
626 #[inline]
627 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
628 match self {
629 Self::Ethereum(inner) => inner.authorization_list(),
630 Self::Unknown(inner) => inner.authorization_list(),
631 }
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use crate::AnyTxType;
639 use std::str::FromStr;
640
641 fn unknown_inner_tx() -> UnknownTypedTransaction {
642 UnknownTypedTransaction {
643 ty: AnyTxType(0x7e),
644 fields: Default::default(),
645 memo: Default::default(),
646 }
647 }
648
649 fn unknown_tx() -> AnyTypedTransaction {
650 AnyTypedTransaction::Unknown(unknown_inner_tx())
651 }
652
653 fn unknown_envelope() -> AnyTxEnvelope {
654 AnyTxEnvelope::Unknown(UnknownTxEnvelope { hash: B256::ZERO, inner: unknown_inner_tx() })
655 }
656
657 #[test]
658 #[should_panic(expected = "cannot be signed via AnyTypedTransaction")]
659 fn unknown_transaction_signature_hash_panics() {
660 let tx = unknown_tx();
661 let _ = tx.signature_hash();
662 }
663
664 #[test]
665 #[should_panic(expected = "cannot be signed via AnyTypedTransaction")]
666 fn signed_unknown_transaction_cannot_convert_to_envelope() {
667 let tx = unknown_tx();
668 let sig = Signature::from_str(
669 "48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b",
670 )
671 .unwrap();
672
673 let _: AnyTxEnvelope = tx.into_signed(sig).into();
674 }
675
676 #[test]
677 #[should_panic(expected = "Attempted to encode unknown transaction type")]
678 fn unknown_transaction_encode_2718_len_panics() {
679 let envelope = unknown_envelope();
680 let _ = envelope.encode_2718_len();
681 }
682
683 #[test]
684 #[should_panic(expected = "Attempted to encode unknown transaction type")]
685 fn unknown_transaction_encoded_2718_panics() {
686 let envelope = unknown_envelope();
687 let _ = envelope.encoded_2718();
688 }
689}