alloy_consensus/transaction/
mod.rs1use crate::Signed;
4use alloc::vec::Vec;
5use alloy_eips::{eip2930::AccessList, eip4844::DATA_GAS_PER_BLOB, eip7702::SignedAuthorization};
6use alloy_primitives::{keccak256, Address, Bytes, ChainId, Selector, TxHash, TxKind, B256, U256};
7use auto_impl::auto_impl;
8use core::{any, fmt};
9
10mod eip1559;
11pub use eip1559::TxEip1559;
12
13mod eip2930;
14pub use eip2930::TxEip2930;
15
16mod eip7702;
17pub use eip7702::TxEip7702;
18
19mod envelope;
20#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
21pub use envelope::serde_bincode_compat as envelope_serde_bincode_compat;
22pub use envelope::{EthereumTxEnvelope, TxEnvelope, TxType};
23
24pub mod eip4844;
26pub use eip4844::{TxEip4844, TxEip4844Variant, TxEip4844WithSidecar};
27
28mod eip4844_sidecar;
29#[cfg(feature = "kzg")]
30pub use eip4844_sidecar::BlobTransactionValidationError;
31pub use eip4844_sidecar::TxEip4844Sidecar;
32
33pub use alloy_eips::eip4844::{
35 builder::{SidecarBuilder, SidecarCoder, SimpleCoder},
36 utils as eip4844_utils, Blob, BlobTransactionSidecar, Bytes48,
37};
38
39pub mod pooled;
40pub use pooled::PooledTransaction;
41
42pub use either::Either;
44
45mod legacy;
46pub use legacy::{from_eip155_value, to_eip155_value, TxLegacy};
47
48mod rlp;
49#[doc(hidden)]
50pub use rlp::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx, RlpEcdsaTx};
51
52mod typed;
53pub use typed::{EthereumTypedTransaction, TypedTransaction};
54
55mod tx_type;
56
57mod meta;
58pub use meta::{TransactionInfo, TransactionMeta};
59
60mod recovered;
61pub use recovered::{Recovered, SignerRecoverable};
62
63#[cfg(feature = "serde")]
64pub use legacy::{signed_legacy_serde, untagged_legacy_serde};
65
66#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
68pub mod serde_bincode_compat {
69 pub use super::{
70 eip1559::serde_bincode_compat::*, eip2930::serde_bincode_compat::*,
71 eip7702::serde_bincode_compat::*, envelope::serde_bincode_compat::*,
72 legacy::serde_bincode_compat::*, typed::serde_bincode_compat::*,
73 };
74}
75
76use alloy_eips::Typed2718;
77
78#[doc(alias = "Tx")]
83#[auto_impl::auto_impl(&, Arc)]
84pub trait Transaction: Typed2718 + fmt::Debug + any::Any + Send + Sync + 'static {
85 fn chain_id(&self) -> Option<ChainId>;
87
88 fn nonce(&self) -> u64;
90
91 fn gas_limit(&self) -> u64;
93
94 fn gas_price(&self) -> Option<u128>;
96
97 fn max_fee_per_gas(&self) -> u128;
103
104 fn max_priority_fee_per_gas(&self) -> Option<u128>;
109
110 fn max_fee_per_blob_gas(&self) -> Option<u128>;
116
117 fn priority_fee_or_price(&self) -> u128;
125
126 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128;
130
131 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
136 let base_fee = base_fee as u128;
137
138 let max_fee_per_gas = self.max_fee_per_gas();
139
140 if max_fee_per_gas < base_fee {
142 return None;
143 }
144
145 let fee = max_fee_per_gas - base_fee;
147
148 self.max_priority_fee_per_gas()
150 .map_or(Some(fee), |priority_fee| Some(fee.min(priority_fee)))
151 }
152
153 fn is_dynamic_fee(&self) -> bool;
155
156 fn kind(&self) -> TxKind;
158
159 fn is_create(&self) -> bool;
163
164 fn to(&self) -> Option<Address> {
169 self.kind().to().copied()
170 }
171
172 fn value(&self) -> U256;
174
175 fn input(&self) -> &Bytes;
177
178 fn function_selector(&self) -> Option<&Selector> {
182 if self.kind().is_call() {
183 self.input().get(..4).and_then(|s| TryFrom::try_from(s).ok())
184 } else {
185 None
186 }
187 }
188
189 fn access_list(&self) -> Option<&AccessList>;
192
193 fn blob_versioned_hashes(&self) -> Option<&[B256]>;
196
197 fn blob_count(&self) -> Option<u64> {
203 self.blob_versioned_hashes().map(|h| h.len() as u64)
204 }
205
206 #[inline]
210 fn blob_gas_used(&self) -> Option<u64> {
211 self.blob_count().map(|blobs| blobs * DATA_GAS_PER_BLOB)
213 }
214
215 fn authorization_list(&self) -> Option<&[SignedAuthorization]>;
219
220 fn authorization_count(&self) -> Option<u64> {
226 self.authorization_list().map(|auths| auths.len() as u64)
227 }
228}
229
230pub trait TransactionEnvelope: Transaction {
232 type TxType: Typed2718;
234}
235
236#[doc(alias = "SignableTx", alias = "TxSignable")]
243pub trait SignableTransaction<Signature>: Transaction {
244 fn set_chain_id(&mut self, chain_id: ChainId);
248
249 fn set_chain_id_checked(&mut self, chain_id: ChainId) -> bool {
252 match self.chain_id() {
253 Some(tx_chain_id) => {
254 if tx_chain_id != chain_id {
255 return false;
256 }
257 self.set_chain_id(chain_id);
258 }
259 None => {
260 self.set_chain_id(chain_id);
261 }
262 }
263 true
264 }
265
266 fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut);
268
269 fn payload_len_for_signature(&self) -> usize;
271
272 fn encoded_for_signing(&self) -> Vec<u8> {
276 let mut buf = Vec::with_capacity(self.payload_len_for_signature());
277 self.encode_for_signing(&mut buf);
278 buf
279 }
280
281 fn signature_hash(&self) -> B256 {
283 keccak256(self.encoded_for_signing())
284 }
285
286 fn into_signed(self, signature: Signature) -> Signed<Self, Signature>
288 where
289 Self: Sized,
290 {
291 Signed::new_unhashed(self, signature)
292 }
293}
294
295#[doc(hidden)]
297impl<S: 'static> dyn SignableTransaction<S> {
298 pub fn __downcast_ref<T: any::Any>(&self) -> Option<&T> {
299 if any::Any::type_id(self) == any::TypeId::of::<T>() {
300 unsafe { Some(&*(self as *const _ as *const T)) }
301 } else {
302 None
303 }
304 }
305}
306
307#[cfg(feature = "serde")]
308impl<T: Transaction> Transaction for alloy_serde::WithOtherFields<T> {
309 #[inline]
310 fn chain_id(&self) -> Option<ChainId> {
311 self.inner.chain_id()
312 }
313
314 #[inline]
315 fn nonce(&self) -> u64 {
316 self.inner.nonce()
317 }
318
319 #[inline]
320 fn gas_limit(&self) -> u64 {
321 self.inner.gas_limit()
322 }
323
324 #[inline]
325 fn gas_price(&self) -> Option<u128> {
326 self.inner.gas_price()
327 }
328
329 #[inline]
330 fn max_fee_per_gas(&self) -> u128 {
331 self.inner.max_fee_per_gas()
332 }
333
334 #[inline]
335 fn max_priority_fee_per_gas(&self) -> Option<u128> {
336 self.inner.max_priority_fee_per_gas()
337 }
338
339 #[inline]
340 fn max_fee_per_blob_gas(&self) -> Option<u128> {
341 self.inner.max_fee_per_blob_gas()
342 }
343
344 #[inline]
345 fn priority_fee_or_price(&self) -> u128 {
346 self.inner.priority_fee_or_price()
347 }
348
349 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
350 self.inner.effective_gas_price(base_fee)
351 }
352
353 #[inline]
354 fn is_dynamic_fee(&self) -> bool {
355 self.inner.is_dynamic_fee()
356 }
357
358 #[inline]
359 fn kind(&self) -> TxKind {
360 self.inner.kind()
361 }
362
363 #[inline]
364 fn is_create(&self) -> bool {
365 self.inner.is_create()
366 }
367
368 #[inline]
369 fn value(&self) -> U256 {
370 self.inner.value()
371 }
372
373 #[inline]
374 fn input(&self) -> &Bytes {
375 self.inner.input()
376 }
377
378 #[inline]
379 fn access_list(&self) -> Option<&AccessList> {
380 self.inner.access_list()
381 }
382
383 #[inline]
384 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
385 self.inner.blob_versioned_hashes()
386 }
387
388 #[inline]
389 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
390 self.inner.authorization_list()
391 }
392}
393
394impl<L, R> Transaction for either::Either<L, R>
395where
396 L: Transaction,
397 R: Transaction,
398{
399 fn chain_id(&self) -> Option<ChainId> {
400 match self {
401 Self::Left(tx) => tx.chain_id(),
402 Self::Right(tx) => tx.chain_id(),
403 }
404 }
405
406 fn nonce(&self) -> u64 {
407 match self {
408 Self::Left(tx) => tx.nonce(),
409 Self::Right(tx) => tx.nonce(),
410 }
411 }
412
413 fn gas_limit(&self) -> u64 {
414 match self {
415 Self::Left(tx) => tx.gas_limit(),
416 Self::Right(tx) => tx.gas_limit(),
417 }
418 }
419
420 fn gas_price(&self) -> Option<u128> {
421 match self {
422 Self::Left(tx) => tx.gas_price(),
423 Self::Right(tx) => tx.gas_price(),
424 }
425 }
426
427 fn max_fee_per_gas(&self) -> u128 {
428 match self {
429 Self::Left(tx) => tx.max_fee_per_gas(),
430 Self::Right(tx) => tx.max_fee_per_gas(),
431 }
432 }
433
434 fn max_priority_fee_per_gas(&self) -> Option<u128> {
435 match self {
436 Self::Left(tx) => tx.max_priority_fee_per_gas(),
437 Self::Right(tx) => tx.max_priority_fee_per_gas(),
438 }
439 }
440
441 fn max_fee_per_blob_gas(&self) -> Option<u128> {
442 match self {
443 Self::Left(tx) => tx.max_fee_per_blob_gas(),
444 Self::Right(tx) => tx.max_fee_per_blob_gas(),
445 }
446 }
447
448 fn priority_fee_or_price(&self) -> u128 {
449 match self {
450 Self::Left(tx) => tx.priority_fee_or_price(),
451 Self::Right(tx) => tx.priority_fee_or_price(),
452 }
453 }
454
455 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
456 match self {
457 Self::Left(tx) => tx.effective_gas_price(base_fee),
458 Self::Right(tx) => tx.effective_gas_price(base_fee),
459 }
460 }
461
462 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
463 match self {
464 Self::Left(tx) => tx.effective_tip_per_gas(base_fee),
465 Self::Right(tx) => tx.effective_tip_per_gas(base_fee),
466 }
467 }
468
469 fn is_dynamic_fee(&self) -> bool {
470 match self {
471 Self::Left(tx) => tx.is_dynamic_fee(),
472 Self::Right(tx) => tx.is_dynamic_fee(),
473 }
474 }
475
476 fn kind(&self) -> TxKind {
477 match self {
478 Self::Left(tx) => tx.kind(),
479 Self::Right(tx) => tx.kind(),
480 }
481 }
482
483 fn is_create(&self) -> bool {
484 match self {
485 Self::Left(tx) => tx.is_create(),
486 Self::Right(tx) => tx.is_create(),
487 }
488 }
489
490 fn to(&self) -> Option<Address> {
491 match self {
492 Self::Left(tx) => tx.to(),
493 Self::Right(tx) => tx.to(),
494 }
495 }
496
497 fn value(&self) -> U256 {
498 match self {
499 Self::Left(tx) => tx.value(),
500 Self::Right(tx) => tx.value(),
501 }
502 }
503
504 fn input(&self) -> &Bytes {
505 match self {
506 Self::Left(tx) => tx.input(),
507 Self::Right(tx) => tx.input(),
508 }
509 }
510
511 fn function_selector(&self) -> Option<&Selector> {
512 match self {
513 Self::Left(tx) => tx.function_selector(),
514 Self::Right(tx) => tx.function_selector(),
515 }
516 }
517
518 fn access_list(&self) -> Option<&AccessList> {
519 match self {
520 Self::Left(tx) => tx.access_list(),
521 Self::Right(tx) => tx.access_list(),
522 }
523 }
524
525 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
526 match self {
527 Self::Left(tx) => tx.blob_versioned_hashes(),
528 Self::Right(tx) => tx.blob_versioned_hashes(),
529 }
530 }
531
532 fn blob_count(&self) -> Option<u64> {
533 match self {
534 Self::Left(tx) => tx.blob_count(),
535 Self::Right(tx) => tx.blob_count(),
536 }
537 }
538
539 fn blob_gas_used(&self) -> Option<u64> {
540 match self {
541 Self::Left(tx) => tx.blob_gas_used(),
542 Self::Right(tx) => tx.blob_gas_used(),
543 }
544 }
545
546 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
547 match self {
548 Self::Left(tx) => tx.authorization_list(),
549 Self::Right(tx) => tx.authorization_list(),
550 }
551 }
552
553 fn authorization_count(&self) -> Option<u64> {
554 match self {
555 Self::Left(tx) => tx.authorization_count(),
556 Self::Right(tx) => tx.authorization_count(),
557 }
558 }
559}
560
561#[auto_impl(&, &mut, Box)]
567pub trait TxHashRef {
568 fn tx_hash(&self) -> &TxHash;
572}
573
574impl<T: TxHashRef> TxHashRef for Recovered<T> {
575 fn tx_hash(&self) -> &TxHash {
576 self.inner().tx_hash()
577 }
578}
579
580impl<T: TxHashRef> TxHashRef for alloy_eips::eip2718::WithEncoded<T> {
581 fn tx_hash(&self) -> &TxHash {
582 self.value().tx_hash()
583 }
584}
585
586#[cfg(test)]
587#[allow(unused_imports)]
588mod tests {
589 use crate::{Signed, TransactionEnvelope, TxEip1559, TxEnvelope, TxType};
590 use alloy_primitives::Signature;
591 use rand::Rng;
592
593 #[test]
594 #[cfg(feature = "serde")]
595 fn test_custom_envelope() {
596 use serde::{Serialize, Serializer};
597 fn serialize_with<S: Serializer>(
598 tx: &Signed<TxEip1559>,
599 serializer: S,
600 ) -> Result<S::Ok, S::Error> {
601 #[derive(Serialize)]
602 struct WithExtra<'a> {
603 #[serde(flatten)]
604 inner: &'a Signed<TxEip1559>,
605 extra: &'a str,
606 }
607 WithExtra { inner: tx, extra: "extra" }.serialize(serializer)
608 }
609
610 #[derive(Debug, Clone, TransactionEnvelope)]
611 #[envelope(alloy_consensus = crate, tx_type_name = MyTxType)]
612 enum MyEnvelope {
613 #[envelope(flatten)]
614 Ethereum(TxEnvelope),
615 #[envelope(ty = 10)]
616 MyTx(Signed<TxEip1559>),
617 #[envelope(ty = 11)]
618 #[serde(serialize_with = "serialize_with")]
619 AnotherMyTx(Signed<TxEip1559>),
620 }
621
622 assert_eq!(u8::from(MyTxType::Ethereum(TxType::Eip1559)), 2);
623 assert_eq!(u8::from(MyTxType::MyTx), 10);
624 assert_eq!(MyTxType::try_from(2u8).unwrap(), MyTxType::Ethereum(TxType::Eip1559));
625 assert_eq!(MyTxType::try_from(10u8).unwrap(), MyTxType::MyTx);
626
627 let mut bytes = [0u8; 1024];
628 rand::thread_rng().fill(bytes.as_mut_slice());
629 let tx = Signed::new_unhashed(
630 TxEip1559 {
631 chain_id: 1,
632 gas_limit: 21000,
633 max_fee_per_gas: 1000,
634 max_priority_fee_per_gas: 1000,
635 ..Default::default()
636 },
637 Signature::new(Default::default(), Default::default(), Default::default()),
638 );
639
640 let my_tx = serde_json::to_string(&MyEnvelope::MyTx(tx.clone())).unwrap();
641 let another_my_tx = serde_json::to_string(&MyEnvelope::AnotherMyTx(tx)).unwrap();
642
643 assert_eq!(
644 my_tx,
645 r#"{"type":"0xa","chainId":"0x1","nonce":"0x0","gas":"0x5208","maxFeePerGas":"0x3e8","maxPriorityFeePerGas":"0x3e8","to":null,"value":"0x0","accessList":[],"input":"0x","r":"0x0","s":"0x0","yParity":"0x0","v":"0x0","hash":"0x2eaaca5609601faae806f5147abb8f51ae91cba12604bedc23a16f2776d5a97b"}"#
646 );
647 assert_eq!(
648 another_my_tx,
649 r#"{"type":"0xb","chainId":"0x1","nonce":"0x0","gas":"0x5208","maxFeePerGas":"0x3e8","maxPriorityFeePerGas":"0x3e8","to":null,"value":"0x0","accessList":[],"input":"0x","r":"0x0","s":"0x0","yParity":"0x0","v":"0x0","hash":"0x2eaaca5609601faae806f5147abb8f51ae91cba12604bedc23a16f2776d5a97b","extra":"extra"}"#
650 );
651 }
652}