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