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
63mod hashable;
64pub use hashable::TxHashable;
65
66#[cfg(feature = "serde")]
67pub use legacy::{signed_legacy_serde, untagged_legacy_serde};
68
69#[cfg(all(feature = "serde", feature = "serde-bincode-compat"))]
71pub mod serde_bincode_compat {
72 pub use super::{
73 eip1559::serde_bincode_compat::*, eip2930::serde_bincode_compat::*,
74 eip7702::serde_bincode_compat::*, envelope::serde_bincode_compat::*,
75 legacy::serde_bincode_compat::*, typed::serde_bincode_compat::*,
76 };
77}
78
79use alloy_eips::Typed2718;
80
81#[doc(alias = "Tx")]
86#[auto_impl::auto_impl(&, Arc)]
87pub trait Transaction: Typed2718 + fmt::Debug + any::Any + Send + Sync + 'static {
88 fn chain_id(&self) -> Option<ChainId>;
90
91 fn nonce(&self) -> u64;
93
94 fn gas_limit(&self) -> u64;
96
97 fn gas_price(&self) -> Option<u128>;
99
100 fn max_fee_per_gas(&self) -> u128;
106
107 fn max_priority_fee_per_gas(&self) -> Option<u128>;
112
113 fn max_fee_per_blob_gas(&self) -> Option<u128>;
119
120 fn priority_fee_or_price(&self) -> u128;
128
129 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128;
133
134 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
139 let base_fee = base_fee as u128;
140
141 let max_fee_per_gas = self.max_fee_per_gas();
142
143 if max_fee_per_gas < base_fee {
145 return None;
146 }
147
148 let fee = max_fee_per_gas - base_fee;
150
151 self.max_priority_fee_per_gas()
153 .map_or(Some(fee), |priority_fee| Some(fee.min(priority_fee)))
154 }
155
156 fn is_dynamic_fee(&self) -> bool;
158
159 fn kind(&self) -> TxKind;
161
162 fn is_create(&self) -> bool;
166
167 fn to(&self) -> Option<Address> {
172 self.kind().into_to()
173 }
174
175 fn value(&self) -> U256;
177
178 fn input(&self) -> &Bytes;
180
181 fn function_selector(&self) -> Option<&Selector> {
185 if self.kind().is_call() {
186 self.input().get(..4).and_then(|s| TryFrom::try_from(s).ok())
187 } else {
188 None
189 }
190 }
191
192 fn access_list(&self) -> Option<&AccessList>;
195
196 fn blob_versioned_hashes(&self) -> Option<&[B256]>;
199
200 fn blob_count(&self) -> Option<u64> {
206 self.blob_versioned_hashes().map(|h| h.len() as u64)
207 }
208
209 #[inline]
213 fn blob_gas_used(&self) -> Option<u64> {
214 self.blob_count().map(|blobs| blobs * DATA_GAS_PER_BLOB)
216 }
217
218 fn authorization_list(&self) -> Option<&[SignedAuthorization]>;
222
223 fn authorization_count(&self) -> Option<u64> {
229 self.authorization_list().map(|auths| auths.len() as u64)
230 }
231}
232
233pub trait TransactionEnvelope: Transaction {
235 type TxType: Typed2718;
237
238 fn tx_type(&self) -> Self::TxType;
240}
241
242#[doc(alias = "SignableTx", alias = "TxSignable")]
249pub trait SignableTransaction<Signature>: Transaction {
250 fn set_chain_id(&mut self, chain_id: ChainId);
254
255 fn set_chain_id_checked(&mut self, chain_id: ChainId) -> bool {
258 match self.chain_id() {
259 Some(tx_chain_id) => {
260 if tx_chain_id != chain_id {
261 return false;
262 }
263 }
265 None => {
266 self.set_chain_id(chain_id);
267 }
268 }
269 true
270 }
271
272 fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut);
274
275 fn payload_len_for_signature(&self) -> usize;
277
278 fn encoded_for_signing(&self) -> Vec<u8> {
282 let mut buf = Vec::with_capacity(self.payload_len_for_signature());
283 self.encode_for_signing(&mut buf);
284 buf
285 }
286
287 fn signature_hash(&self) -> B256 {
289 keccak256(self.encoded_for_signing())
290 }
291
292 fn into_signed(self, signature: Signature) -> Signed<Self, Signature>
294 where
295 Self: Sized,
296 {
297 Signed::new_unhashed(self, signature)
298 }
299}
300
301#[cfg(feature = "serde")]
302impl<T: Transaction> Transaction for alloy_serde::WithOtherFields<T> {
303 #[inline]
304 fn chain_id(&self) -> Option<ChainId> {
305 self.inner.chain_id()
306 }
307
308 #[inline]
309 fn nonce(&self) -> u64 {
310 self.inner.nonce()
311 }
312
313 #[inline]
314 fn gas_limit(&self) -> u64 {
315 self.inner.gas_limit()
316 }
317
318 #[inline]
319 fn gas_price(&self) -> Option<u128> {
320 self.inner.gas_price()
321 }
322
323 #[inline]
324 fn max_fee_per_gas(&self) -> u128 {
325 self.inner.max_fee_per_gas()
326 }
327
328 #[inline]
329 fn max_priority_fee_per_gas(&self) -> Option<u128> {
330 self.inner.max_priority_fee_per_gas()
331 }
332
333 #[inline]
334 fn max_fee_per_blob_gas(&self) -> Option<u128> {
335 self.inner.max_fee_per_blob_gas()
336 }
337
338 #[inline]
339 fn priority_fee_or_price(&self) -> u128 {
340 self.inner.priority_fee_or_price()
341 }
342
343 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
344 self.inner.effective_gas_price(base_fee)
345 }
346
347 #[inline]
348 fn is_dynamic_fee(&self) -> bool {
349 self.inner.is_dynamic_fee()
350 }
351
352 #[inline]
353 fn kind(&self) -> TxKind {
354 self.inner.kind()
355 }
356
357 #[inline]
358 fn is_create(&self) -> bool {
359 self.inner.is_create()
360 }
361
362 #[inline]
363 fn value(&self) -> U256 {
364 self.inner.value()
365 }
366
367 #[inline]
368 fn input(&self) -> &Bytes {
369 self.inner.input()
370 }
371
372 #[inline]
373 fn access_list(&self) -> Option<&AccessList> {
374 self.inner.access_list()
375 }
376
377 #[inline]
378 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
379 self.inner.blob_versioned_hashes()
380 }
381
382 #[inline]
383 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
384 self.inner.authorization_list()
385 }
386}
387
388impl<L, R> Transaction for either::Either<L, R>
389where
390 L: Transaction,
391 R: Transaction,
392{
393 fn chain_id(&self) -> Option<ChainId> {
394 match self {
395 Self::Left(tx) => tx.chain_id(),
396 Self::Right(tx) => tx.chain_id(),
397 }
398 }
399
400 fn nonce(&self) -> u64 {
401 match self {
402 Self::Left(tx) => tx.nonce(),
403 Self::Right(tx) => tx.nonce(),
404 }
405 }
406
407 fn gas_limit(&self) -> u64 {
408 match self {
409 Self::Left(tx) => tx.gas_limit(),
410 Self::Right(tx) => tx.gas_limit(),
411 }
412 }
413
414 fn gas_price(&self) -> Option<u128> {
415 match self {
416 Self::Left(tx) => tx.gas_price(),
417 Self::Right(tx) => tx.gas_price(),
418 }
419 }
420
421 fn max_fee_per_gas(&self) -> u128 {
422 match self {
423 Self::Left(tx) => tx.max_fee_per_gas(),
424 Self::Right(tx) => tx.max_fee_per_gas(),
425 }
426 }
427
428 fn max_priority_fee_per_gas(&self) -> Option<u128> {
429 match self {
430 Self::Left(tx) => tx.max_priority_fee_per_gas(),
431 Self::Right(tx) => tx.max_priority_fee_per_gas(),
432 }
433 }
434
435 fn max_fee_per_blob_gas(&self) -> Option<u128> {
436 match self {
437 Self::Left(tx) => tx.max_fee_per_blob_gas(),
438 Self::Right(tx) => tx.max_fee_per_blob_gas(),
439 }
440 }
441
442 fn priority_fee_or_price(&self) -> u128 {
443 match self {
444 Self::Left(tx) => tx.priority_fee_or_price(),
445 Self::Right(tx) => tx.priority_fee_or_price(),
446 }
447 }
448
449 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
450 match self {
451 Self::Left(tx) => tx.effective_gas_price(base_fee),
452 Self::Right(tx) => tx.effective_gas_price(base_fee),
453 }
454 }
455
456 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
457 match self {
458 Self::Left(tx) => tx.effective_tip_per_gas(base_fee),
459 Self::Right(tx) => tx.effective_tip_per_gas(base_fee),
460 }
461 }
462
463 fn is_dynamic_fee(&self) -> bool {
464 match self {
465 Self::Left(tx) => tx.is_dynamic_fee(),
466 Self::Right(tx) => tx.is_dynamic_fee(),
467 }
468 }
469
470 fn kind(&self) -> TxKind {
471 match self {
472 Self::Left(tx) => tx.kind(),
473 Self::Right(tx) => tx.kind(),
474 }
475 }
476
477 fn is_create(&self) -> bool {
478 match self {
479 Self::Left(tx) => tx.is_create(),
480 Self::Right(tx) => tx.is_create(),
481 }
482 }
483
484 fn to(&self) -> Option<Address> {
485 match self {
486 Self::Left(tx) => tx.to(),
487 Self::Right(tx) => tx.to(),
488 }
489 }
490
491 fn value(&self) -> U256 {
492 match self {
493 Self::Left(tx) => tx.value(),
494 Self::Right(tx) => tx.value(),
495 }
496 }
497
498 fn input(&self) -> &Bytes {
499 match self {
500 Self::Left(tx) => tx.input(),
501 Self::Right(tx) => tx.input(),
502 }
503 }
504
505 fn function_selector(&self) -> Option<&Selector> {
506 match self {
507 Self::Left(tx) => tx.function_selector(),
508 Self::Right(tx) => tx.function_selector(),
509 }
510 }
511
512 fn access_list(&self) -> Option<&AccessList> {
513 match self {
514 Self::Left(tx) => tx.access_list(),
515 Self::Right(tx) => tx.access_list(),
516 }
517 }
518
519 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
520 match self {
521 Self::Left(tx) => tx.blob_versioned_hashes(),
522 Self::Right(tx) => tx.blob_versioned_hashes(),
523 }
524 }
525
526 fn blob_count(&self) -> Option<u64> {
527 match self {
528 Self::Left(tx) => tx.blob_count(),
529 Self::Right(tx) => tx.blob_count(),
530 }
531 }
532
533 fn blob_gas_used(&self) -> Option<u64> {
534 match self {
535 Self::Left(tx) => tx.blob_gas_used(),
536 Self::Right(tx) => tx.blob_gas_used(),
537 }
538 }
539
540 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
541 match self {
542 Self::Left(tx) => tx.authorization_list(),
543 Self::Right(tx) => tx.authorization_list(),
544 }
545 }
546
547 fn authorization_count(&self) -> Option<u64> {
548 match self {
549 Self::Left(tx) => tx.authorization_count(),
550 Self::Right(tx) => tx.authorization_count(),
551 }
552 }
553}
554
555#[auto_impl(&, &mut, Box)]
561pub trait TxHashRef {
562 fn tx_hash(&self) -> &TxHash;
566}
567
568impl<T: TxHashRef> TxHashRef for Recovered<T> {
569 fn tx_hash(&self) -> &TxHash {
570 self.inner().tx_hash()
571 }
572}
573
574impl<T: TxHashRef> TxHashRef for alloy_eips::eip2718::WithEncoded<T> {
575 fn tx_hash(&self) -> &TxHash {
576 self.value().tx_hash()
577 }
578}
579
580#[cfg(all(test, feature = "serde"))]
581mod tests {
582 use crate::{Signed, TransactionEnvelope, TxEip1559, TxEnvelope, TxType};
583 use alloy_primitives::Signature;
584
585 #[test]
586 fn test_custom_envelope() {
587 use serde::{Serialize, Serializer};
588 fn serialize_with<S: Serializer>(
589 tx: &Signed<TxEip1559>,
590 serializer: S,
591 ) -> Result<S::Ok, S::Error> {
592 #[derive(Serialize)]
593 struct WithExtra<'a> {
594 #[serde(flatten)]
595 inner: &'a Signed<TxEip1559>,
596 extra: &'a str,
597 }
598 WithExtra { inner: tx, extra: "extra" }.serialize(serializer)
599 }
600
601 #[derive(Debug, Clone, TransactionEnvelope)]
602 #[envelope(alloy_consensus = crate, tx_type_name = MyTxType)]
603 enum MyEnvelope {
604 #[envelope(flatten)]
605 Ethereum(TxEnvelope),
606 #[envelope(ty = 10)]
607 MyTx(Signed<TxEip1559>),
608 #[envelope(ty = 11)]
609 #[serde(serialize_with = "serialize_with")]
610 AnotherMyTx(Signed<TxEip1559>),
611 }
612
613 assert_eq!(u8::from(MyTxType::Ethereum(TxType::Eip1559)), 2);
614 assert_eq!(u8::from(MyTxType::MyTx), 10);
615 assert_eq!(MyTxType::try_from(2u8).unwrap(), MyTxType::Ethereum(TxType::Eip1559));
616 assert_eq!(MyTxType::try_from(10u8).unwrap(), MyTxType::MyTx);
617
618 let tx = Signed::new_unhashed(
619 TxEip1559 {
620 chain_id: 1,
621 gas_limit: 21000,
622 max_fee_per_gas: 1000,
623 max_priority_fee_per_gas: 1000,
624 ..Default::default()
625 },
626 Signature::new(Default::default(), Default::default(), Default::default()),
627 );
628
629 let my_tx = serde_json::to_string(&MyEnvelope::MyTx(tx.clone())).unwrap();
630 let another_my_tx = serde_json::to_string(&MyEnvelope::AnotherMyTx(tx)).unwrap();
631
632 assert_eq!(
633 my_tx,
634 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"}"#
635 );
636 assert_eq!(
637 another_my_tx,
638 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"}"#
639 );
640 }
641}