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
239#[doc(alias = "SignableTx", alias = "TxSignable")]
246pub trait SignableTransaction<Signature>: Transaction {
247 fn set_chain_id(&mut self, chain_id: ChainId);
251
252 fn set_chain_id_checked(&mut self, chain_id: ChainId) -> bool {
255 match self.chain_id() {
256 Some(tx_chain_id) => {
257 if tx_chain_id != chain_id {
258 return false;
259 }
260 }
262 None => {
263 self.set_chain_id(chain_id);
264 }
265 }
266 true
267 }
268
269 fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut);
271
272 fn payload_len_for_signature(&self) -> usize;
274
275 fn encoded_for_signing(&self) -> Vec<u8> {
279 let mut buf = Vec::with_capacity(self.payload_len_for_signature());
280 self.encode_for_signing(&mut buf);
281 buf
282 }
283
284 fn signature_hash(&self) -> B256 {
286 keccak256(self.encoded_for_signing())
287 }
288
289 fn into_signed(self, signature: Signature) -> Signed<Self, Signature>
291 where
292 Self: Sized,
293 {
294 Signed::new_unhashed(self, signature)
295 }
296}
297
298#[cfg(feature = "serde")]
299impl<T: Transaction> Transaction for alloy_serde::WithOtherFields<T> {
300 #[inline]
301 fn chain_id(&self) -> Option<ChainId> {
302 self.inner.chain_id()
303 }
304
305 #[inline]
306 fn nonce(&self) -> u64 {
307 self.inner.nonce()
308 }
309
310 #[inline]
311 fn gas_limit(&self) -> u64 {
312 self.inner.gas_limit()
313 }
314
315 #[inline]
316 fn gas_price(&self) -> Option<u128> {
317 self.inner.gas_price()
318 }
319
320 #[inline]
321 fn max_fee_per_gas(&self) -> u128 {
322 self.inner.max_fee_per_gas()
323 }
324
325 #[inline]
326 fn max_priority_fee_per_gas(&self) -> Option<u128> {
327 self.inner.max_priority_fee_per_gas()
328 }
329
330 #[inline]
331 fn max_fee_per_blob_gas(&self) -> Option<u128> {
332 self.inner.max_fee_per_blob_gas()
333 }
334
335 #[inline]
336 fn priority_fee_or_price(&self) -> u128 {
337 self.inner.priority_fee_or_price()
338 }
339
340 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
341 self.inner.effective_gas_price(base_fee)
342 }
343
344 #[inline]
345 fn is_dynamic_fee(&self) -> bool {
346 self.inner.is_dynamic_fee()
347 }
348
349 #[inline]
350 fn kind(&self) -> TxKind {
351 self.inner.kind()
352 }
353
354 #[inline]
355 fn is_create(&self) -> bool {
356 self.inner.is_create()
357 }
358
359 #[inline]
360 fn value(&self) -> U256 {
361 self.inner.value()
362 }
363
364 #[inline]
365 fn input(&self) -> &Bytes {
366 self.inner.input()
367 }
368
369 #[inline]
370 fn access_list(&self) -> Option<&AccessList> {
371 self.inner.access_list()
372 }
373
374 #[inline]
375 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
376 self.inner.blob_versioned_hashes()
377 }
378
379 #[inline]
380 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
381 self.inner.authorization_list()
382 }
383}
384
385impl<L, R> Transaction for either::Either<L, R>
386where
387 L: Transaction,
388 R: Transaction,
389{
390 fn chain_id(&self) -> Option<ChainId> {
391 match self {
392 Self::Left(tx) => tx.chain_id(),
393 Self::Right(tx) => tx.chain_id(),
394 }
395 }
396
397 fn nonce(&self) -> u64 {
398 match self {
399 Self::Left(tx) => tx.nonce(),
400 Self::Right(tx) => tx.nonce(),
401 }
402 }
403
404 fn gas_limit(&self) -> u64 {
405 match self {
406 Self::Left(tx) => tx.gas_limit(),
407 Self::Right(tx) => tx.gas_limit(),
408 }
409 }
410
411 fn gas_price(&self) -> Option<u128> {
412 match self {
413 Self::Left(tx) => tx.gas_price(),
414 Self::Right(tx) => tx.gas_price(),
415 }
416 }
417
418 fn max_fee_per_gas(&self) -> u128 {
419 match self {
420 Self::Left(tx) => tx.max_fee_per_gas(),
421 Self::Right(tx) => tx.max_fee_per_gas(),
422 }
423 }
424
425 fn max_priority_fee_per_gas(&self) -> Option<u128> {
426 match self {
427 Self::Left(tx) => tx.max_priority_fee_per_gas(),
428 Self::Right(tx) => tx.max_priority_fee_per_gas(),
429 }
430 }
431
432 fn max_fee_per_blob_gas(&self) -> Option<u128> {
433 match self {
434 Self::Left(tx) => tx.max_fee_per_blob_gas(),
435 Self::Right(tx) => tx.max_fee_per_blob_gas(),
436 }
437 }
438
439 fn priority_fee_or_price(&self) -> u128 {
440 match self {
441 Self::Left(tx) => tx.priority_fee_or_price(),
442 Self::Right(tx) => tx.priority_fee_or_price(),
443 }
444 }
445
446 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
447 match self {
448 Self::Left(tx) => tx.effective_gas_price(base_fee),
449 Self::Right(tx) => tx.effective_gas_price(base_fee),
450 }
451 }
452
453 fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
454 match self {
455 Self::Left(tx) => tx.effective_tip_per_gas(base_fee),
456 Self::Right(tx) => tx.effective_tip_per_gas(base_fee),
457 }
458 }
459
460 fn is_dynamic_fee(&self) -> bool {
461 match self {
462 Self::Left(tx) => tx.is_dynamic_fee(),
463 Self::Right(tx) => tx.is_dynamic_fee(),
464 }
465 }
466
467 fn kind(&self) -> TxKind {
468 match self {
469 Self::Left(tx) => tx.kind(),
470 Self::Right(tx) => tx.kind(),
471 }
472 }
473
474 fn is_create(&self) -> bool {
475 match self {
476 Self::Left(tx) => tx.is_create(),
477 Self::Right(tx) => tx.is_create(),
478 }
479 }
480
481 fn to(&self) -> Option<Address> {
482 match self {
483 Self::Left(tx) => tx.to(),
484 Self::Right(tx) => tx.to(),
485 }
486 }
487
488 fn value(&self) -> U256 {
489 match self {
490 Self::Left(tx) => tx.value(),
491 Self::Right(tx) => tx.value(),
492 }
493 }
494
495 fn input(&self) -> &Bytes {
496 match self {
497 Self::Left(tx) => tx.input(),
498 Self::Right(tx) => tx.input(),
499 }
500 }
501
502 fn function_selector(&self) -> Option<&Selector> {
503 match self {
504 Self::Left(tx) => tx.function_selector(),
505 Self::Right(tx) => tx.function_selector(),
506 }
507 }
508
509 fn access_list(&self) -> Option<&AccessList> {
510 match self {
511 Self::Left(tx) => tx.access_list(),
512 Self::Right(tx) => tx.access_list(),
513 }
514 }
515
516 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
517 match self {
518 Self::Left(tx) => tx.blob_versioned_hashes(),
519 Self::Right(tx) => tx.blob_versioned_hashes(),
520 }
521 }
522
523 fn blob_count(&self) -> Option<u64> {
524 match self {
525 Self::Left(tx) => tx.blob_count(),
526 Self::Right(tx) => tx.blob_count(),
527 }
528 }
529
530 fn blob_gas_used(&self) -> Option<u64> {
531 match self {
532 Self::Left(tx) => tx.blob_gas_used(),
533 Self::Right(tx) => tx.blob_gas_used(),
534 }
535 }
536
537 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
538 match self {
539 Self::Left(tx) => tx.authorization_list(),
540 Self::Right(tx) => tx.authorization_list(),
541 }
542 }
543
544 fn authorization_count(&self) -> Option<u64> {
545 match self {
546 Self::Left(tx) => tx.authorization_count(),
547 Self::Right(tx) => tx.authorization_count(),
548 }
549 }
550}
551
552#[auto_impl(&, &mut, Box)]
558pub trait TxHashRef {
559 fn tx_hash(&self) -> &TxHash;
563}
564
565impl<T: TxHashRef> TxHashRef for Recovered<T> {
566 fn tx_hash(&self) -> &TxHash {
567 self.inner().tx_hash()
568 }
569}
570
571impl<T: TxHashRef> TxHashRef for alloy_eips::eip2718::WithEncoded<T> {
572 fn tx_hash(&self) -> &TxHash {
573 self.value().tx_hash()
574 }
575}
576
577#[cfg(all(test, feature = "serde"))]
578mod tests {
579 use crate::{Signed, TransactionEnvelope, TxEip1559, TxEnvelope, TxType};
580 use alloy_primitives::Signature;
581 use rand::Rng;
582
583 #[test]
584 fn test_custom_envelope() {
585 use serde::{Serialize, Serializer};
586 fn serialize_with<S: Serializer>(
587 tx: &Signed<TxEip1559>,
588 serializer: S,
589 ) -> Result<S::Ok, S::Error> {
590 #[derive(Serialize)]
591 struct WithExtra<'a> {
592 #[serde(flatten)]
593 inner: &'a Signed<TxEip1559>,
594 extra: &'a str,
595 }
596 WithExtra { inner: tx, extra: "extra" }.serialize(serializer)
597 }
598
599 #[derive(Debug, Clone, TransactionEnvelope)]
600 #[envelope(alloy_consensus = crate, tx_type_name = MyTxType)]
601 enum MyEnvelope {
602 #[envelope(flatten)]
603 Ethereum(TxEnvelope),
604 #[envelope(ty = 10)]
605 MyTx(Signed<TxEip1559>),
606 #[envelope(ty = 11)]
607 #[serde(serialize_with = "serialize_with")]
608 AnotherMyTx(Signed<TxEip1559>),
609 }
610
611 assert_eq!(u8::from(MyTxType::Ethereum(TxType::Eip1559)), 2);
612 assert_eq!(u8::from(MyTxType::MyTx), 10);
613 assert_eq!(MyTxType::try_from(2u8).unwrap(), MyTxType::Ethereum(TxType::Eip1559));
614 assert_eq!(MyTxType::try_from(10u8).unwrap(), MyTxType::MyTx);
615
616 let mut bytes = [0u8; 1024];
617 rand::thread_rng().fill(bytes.as_mut_slice());
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}