reth_optimism_primitives/transaction/
signed.rs

1//! A signed Optimism transaction.
2
3use alloc::vec::Vec;
4use alloy_consensus::{
5    transaction::{RlpEcdsaDecodableTx, RlpEcdsaEncodableTx},
6    Sealed, SignableTransaction, Signed, Transaction, TxEip1559, TxEip2930, TxEip7702, TxLegacy,
7    Typed2718,
8};
9use alloy_eips::{
10    eip2718::{Decodable2718, Eip2718Error, Eip2718Result, Encodable2718},
11    eip2930::AccessList,
12    eip7702::SignedAuthorization,
13};
14use alloy_evm::FromRecoveredTx;
15use alloy_primitives::{
16    keccak256, Address, Bytes, PrimitiveSignature as Signature, TxHash, TxKind, Uint, B256,
17};
18use alloy_rlp::Header;
19use core::{
20    hash::{Hash, Hasher},
21    mem,
22    ops::Deref,
23};
24use derive_more::{AsRef, Deref};
25use op_alloy_consensus::{
26    DepositTransaction, OpPooledTransaction, OpTxEnvelope, OpTypedTransaction, TxDeposit,
27};
28use op_revm::transaction::deposit::DepositTransactionParts;
29#[cfg(any(test, feature = "reth-codec"))]
30use proptest as _;
31use reth_primitives_traits::{
32    crypto::secp256k1::{recover_signer, recover_signer_unchecked},
33    sync::OnceLock,
34    transaction::{error::TransactionConversionError, signed::RecoveryError},
35    InMemorySize, SignedTransaction,
36};
37use revm_context::TxEnv;
38
39/// Signed transaction.
40#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(rlp))]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[derive(Debug, Clone, Eq, AsRef, Deref)]
43pub struct OpTransactionSigned {
44    /// Transaction hash
45    #[cfg_attr(feature = "serde", serde(skip))]
46    hash: OnceLock<TxHash>,
47    /// The transaction signature values
48    signature: Signature,
49    /// Raw transaction info
50    #[deref]
51    #[as_ref]
52    transaction: OpTypedTransaction,
53}
54
55impl OpTransactionSigned {
56    /// Creates a new signed transaction from the given transaction, signature and hash.
57    pub fn new(transaction: OpTypedTransaction, signature: Signature, hash: B256) -> Self {
58        Self { hash: hash.into(), signature, transaction }
59    }
60
61    /// Consumes the type and returns the transaction.
62    #[inline]
63    pub fn into_transaction(self) -> OpTypedTransaction {
64        self.transaction
65    }
66
67    /// Returns the transaction.
68    #[inline]
69    pub const fn transaction(&self) -> &OpTypedTransaction {
70        &self.transaction
71    }
72
73    /// Splits the `OpTransactionSigned` into its transaction and signature.
74    pub fn split(self) -> (OpTypedTransaction, Signature) {
75        (self.transaction, self.signature)
76    }
77
78    /// Creates a new signed transaction from the given transaction and signature without the hash.
79    ///
80    /// Note: this only calculates the hash on the first [`OpTransactionSigned::hash`] call.
81    pub fn new_unhashed(transaction: OpTypedTransaction, signature: Signature) -> Self {
82        Self { hash: Default::default(), signature, transaction }
83    }
84
85    /// Returns whether this transaction is a deposit.
86    pub const fn is_deposit(&self) -> bool {
87        matches!(self.transaction, OpTypedTransaction::Deposit(_))
88    }
89
90    /// Splits the transaction into parts.
91    pub fn into_parts(self) -> (OpTypedTransaction, Signature, B256) {
92        let hash = *self.hash.get_or_init(|| self.recalculate_hash());
93        (self.transaction, self.signature, hash)
94    }
95}
96
97impl SignedTransaction for OpTransactionSigned {
98    fn tx_hash(&self) -> &TxHash {
99        self.hash.get_or_init(|| self.recalculate_hash())
100    }
101
102    fn signature(&self) -> &Signature {
103        &self.signature
104    }
105
106    fn recover_signer(&self) -> Result<Address, RecoveryError> {
107        // Optimism's Deposit transaction does not have a signature. Directly return the
108        // `from` address.
109        if let OpTypedTransaction::Deposit(TxDeposit { from, .. }) = self.transaction {
110            return Ok(from)
111        }
112
113        let Self { transaction, signature, .. } = self;
114        let signature_hash = signature_hash(transaction);
115        recover_signer(signature, signature_hash)
116    }
117
118    fn recover_signer_unchecked(&self) -> Result<Address, RecoveryError> {
119        // Optimism's Deposit transaction does not have a signature. Directly return the
120        // `from` address.
121        if let OpTypedTransaction::Deposit(TxDeposit { from, .. }) = &self.transaction {
122            return Ok(*from)
123        }
124
125        let Self { transaction, signature, .. } = self;
126        let signature_hash = signature_hash(transaction);
127        recover_signer_unchecked(signature, signature_hash)
128    }
129
130    fn recover_signer_unchecked_with_buf(
131        &self,
132        buf: &mut Vec<u8>,
133    ) -> Result<Address, RecoveryError> {
134        match &self.transaction {
135            // Optimism's Deposit transaction does not have a signature. Directly return the
136            // `from` address.
137            OpTypedTransaction::Deposit(tx) => return Ok(tx.from),
138            OpTypedTransaction::Legacy(tx) => tx.encode_for_signing(buf),
139            OpTypedTransaction::Eip2930(tx) => tx.encode_for_signing(buf),
140            OpTypedTransaction::Eip1559(tx) => tx.encode_for_signing(buf),
141            OpTypedTransaction::Eip7702(tx) => tx.encode_for_signing(buf),
142        };
143        recover_signer_unchecked(&self.signature, keccak256(buf))
144    }
145
146    fn recalculate_hash(&self) -> B256 {
147        keccak256(self.encoded_2718())
148    }
149}
150
151macro_rules! impl_from_signed {
152    ($($tx:ident),*) => {
153        $(
154            impl From<Signed<$tx>> for OpTransactionSigned {
155                fn from(value: Signed<$tx>) -> Self {
156                    let(tx,sig,hash) = value.into_parts();
157                    Self::new(tx.into(), sig, hash)
158                }
159            }
160        )*
161    };
162}
163
164impl_from_signed!(TxLegacy, TxEip2930, TxEip1559, TxEip7702, OpTypedTransaction);
165
166impl From<OpTxEnvelope> for OpTransactionSigned {
167    fn from(value: OpTxEnvelope) -> Self {
168        match value {
169            OpTxEnvelope::Legacy(tx) => tx.into(),
170            OpTxEnvelope::Eip2930(tx) => tx.into(),
171            OpTxEnvelope::Eip1559(tx) => tx.into(),
172            OpTxEnvelope::Eip7702(tx) => tx.into(),
173            OpTxEnvelope::Deposit(tx) => tx.into(),
174        }
175    }
176}
177
178impl From<OpTransactionSigned> for OpTxEnvelope {
179    fn from(value: OpTransactionSigned) -> Self {
180        let (tx, signature, hash) = value.into_parts();
181        match tx {
182            OpTypedTransaction::Legacy(tx) => Signed::new_unchecked(tx, signature, hash).into(),
183            OpTypedTransaction::Eip2930(tx) => Signed::new_unchecked(tx, signature, hash).into(),
184            OpTypedTransaction::Eip1559(tx) => Signed::new_unchecked(tx, signature, hash).into(),
185            OpTypedTransaction::Deposit(tx) => Sealed::new_unchecked(tx, hash).into(),
186            OpTypedTransaction::Eip7702(tx) => Signed::new_unchecked(tx, signature, hash).into(),
187        }
188    }
189}
190
191impl From<OpTransactionSigned> for Signed<OpTypedTransaction> {
192    fn from(value: OpTransactionSigned) -> Self {
193        let (tx, sig, hash) = value.into_parts();
194        Self::new_unchecked(tx, sig, hash)
195    }
196}
197
198impl From<Sealed<TxDeposit>> for OpTransactionSigned {
199    fn from(value: Sealed<TxDeposit>) -> Self {
200        let (tx, hash) = value.into_parts();
201        Self::new(OpTypedTransaction::Deposit(tx), TxDeposit::signature(), hash)
202    }
203}
204
205/// A trait that represents an optimism transaction, mainly used to indicate whether or not the
206/// transaction is a deposit transaction.
207pub trait OpTransaction {
208    /// Whether or not the transaction is a dpeosit transaction.
209    fn is_deposit(&self) -> bool;
210}
211
212impl OpTransaction for OpTransactionSigned {
213    fn is_deposit(&self) -> bool {
214        self.is_deposit()
215    }
216}
217
218impl FromRecoveredTx<OpTransactionSigned> for op_revm::OpTransaction<TxEnv> {
219    fn from_recovered_tx(tx: &OpTransactionSigned, sender: Address) -> Self {
220        let envelope = tx.encoded_2718();
221
222        let base = match &tx.transaction {
223            OpTypedTransaction::Legacy(tx) => TxEnv {
224                gas_limit: tx.gas_limit,
225                gas_price: tx.gas_price,
226                gas_priority_fee: None,
227                kind: tx.to,
228                value: tx.value,
229                data: tx.input.clone(),
230                chain_id: tx.chain_id,
231                nonce: tx.nonce,
232                access_list: Default::default(),
233                blob_hashes: Default::default(),
234                max_fee_per_blob_gas: Default::default(),
235                authorization_list: Default::default(),
236                tx_type: 0,
237                caller: sender,
238            },
239            OpTypedTransaction::Eip2930(tx) => TxEnv {
240                gas_limit: tx.gas_limit,
241                gas_price: tx.gas_price,
242                gas_priority_fee: None,
243                kind: tx.to,
244                value: tx.value,
245                data: tx.input.clone(),
246                chain_id: Some(tx.chain_id),
247                nonce: tx.nonce,
248                access_list: tx.access_list.clone(),
249                blob_hashes: Default::default(),
250                max_fee_per_blob_gas: Default::default(),
251                authorization_list: Default::default(),
252                tx_type: 1,
253                caller: sender,
254            },
255            OpTypedTransaction::Eip1559(tx) => TxEnv {
256                gas_limit: tx.gas_limit,
257                gas_price: tx.max_fee_per_gas,
258                gas_priority_fee: Some(tx.max_priority_fee_per_gas),
259                kind: tx.to,
260                value: tx.value,
261                data: tx.input.clone(),
262                chain_id: Some(tx.chain_id),
263                nonce: tx.nonce,
264                access_list: tx.access_list.clone(),
265                blob_hashes: Default::default(),
266                max_fee_per_blob_gas: Default::default(),
267                authorization_list: Default::default(),
268                tx_type: 2,
269                caller: sender,
270            },
271            OpTypedTransaction::Eip7702(tx) => TxEnv {
272                gas_limit: tx.gas_limit,
273                gas_price: tx.max_fee_per_gas,
274                gas_priority_fee: Some(tx.max_priority_fee_per_gas),
275                kind: TxKind::Call(tx.to),
276                value: tx.value,
277                data: tx.input.clone(),
278                chain_id: Some(tx.chain_id),
279                nonce: tx.nonce,
280                access_list: tx.access_list.clone(),
281                blob_hashes: Default::default(),
282                max_fee_per_blob_gas: Default::default(),
283                authorization_list: tx.authorization_list.clone(),
284                tx_type: 4,
285                caller: sender,
286            },
287            OpTypedTransaction::Deposit(tx) => TxEnv {
288                gas_limit: tx.gas_limit,
289                gas_price: 0,
290                kind: tx.to,
291                value: tx.value,
292                data: tx.input.clone(),
293                chain_id: None,
294                nonce: 0,
295                access_list: Default::default(),
296                blob_hashes: Default::default(),
297                max_fee_per_blob_gas: Default::default(),
298                authorization_list: Default::default(),
299                gas_priority_fee: Default::default(),
300                tx_type: 126,
301                caller: sender,
302            },
303        };
304
305        Self {
306            base,
307            enveloped_tx: Some(envelope.into()),
308            deposit: if let OpTypedTransaction::Deposit(tx) = &tx.transaction {
309                DepositTransactionParts {
310                    is_system_transaction: tx.is_system_transaction,
311                    source_hash: tx.source_hash,
312                    // For consistency with op-geth, we always return `0x0` for mint if it is
313                    // missing This is because op-geth does not distinguish
314                    // between null and 0, because this value is decoded from RLP where null is
315                    // represented as 0
316                    mint: Some(tx.mint.unwrap_or_default()),
317                }
318            } else {
319                Default::default()
320            },
321        }
322    }
323}
324
325impl InMemorySize for OpTransactionSigned {
326    #[inline]
327    fn size(&self) -> usize {
328        mem::size_of::<TxHash>() + self.transaction.size() + mem::size_of::<Signature>()
329    }
330}
331
332impl alloy_rlp::Encodable for OpTransactionSigned {
333    fn encode(&self, out: &mut dyn alloy_rlp::bytes::BufMut) {
334        self.network_encode(out);
335    }
336
337    fn length(&self) -> usize {
338        let mut payload_length = self.encode_2718_len();
339        if !self.is_legacy() {
340            payload_length += Header { list: false, payload_length }.length();
341        }
342
343        payload_length
344    }
345}
346
347impl alloy_rlp::Decodable for OpTransactionSigned {
348    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
349        Self::network_decode(buf).map_err(Into::into)
350    }
351}
352
353impl Encodable2718 for OpTransactionSigned {
354    fn type_flag(&self) -> Option<u8> {
355        if Typed2718::is_legacy(self) {
356            None
357        } else {
358            Some(self.ty())
359        }
360    }
361
362    fn encode_2718_len(&self) -> usize {
363        match &self.transaction {
364            OpTypedTransaction::Legacy(legacy_tx) => {
365                legacy_tx.eip2718_encoded_length(&self.signature)
366            }
367            OpTypedTransaction::Eip2930(access_list_tx) => {
368                access_list_tx.eip2718_encoded_length(&self.signature)
369            }
370            OpTypedTransaction::Eip1559(dynamic_fee_tx) => {
371                dynamic_fee_tx.eip2718_encoded_length(&self.signature)
372            }
373            OpTypedTransaction::Eip7702(set_code_tx) => {
374                set_code_tx.eip2718_encoded_length(&self.signature)
375            }
376            OpTypedTransaction::Deposit(deposit_tx) => deposit_tx.eip2718_encoded_length(),
377        }
378    }
379
380    fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
381        let Self { transaction, signature, .. } = self;
382
383        match &transaction {
384            OpTypedTransaction::Legacy(legacy_tx) => {
385                // do nothing w/ with_header
386                legacy_tx.eip2718_encode(signature, out)
387            }
388            OpTypedTransaction::Eip2930(access_list_tx) => {
389                access_list_tx.eip2718_encode(signature, out)
390            }
391            OpTypedTransaction::Eip1559(dynamic_fee_tx) => {
392                dynamic_fee_tx.eip2718_encode(signature, out)
393            }
394            OpTypedTransaction::Eip7702(set_code_tx) => set_code_tx.eip2718_encode(signature, out),
395            OpTypedTransaction::Deposit(deposit_tx) => deposit_tx.encode_2718(out),
396        }
397    }
398}
399
400impl Decodable2718 for OpTransactionSigned {
401    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
402        match ty.try_into().map_err(|_| Eip2718Error::UnexpectedType(ty))? {
403            op_alloy_consensus::OpTxType::Legacy => Err(Eip2718Error::UnexpectedType(0)),
404            op_alloy_consensus::OpTxType::Eip2930 => {
405                let (tx, signature, hash) = TxEip2930::rlp_decode_signed(buf)?.into_parts();
406                let signed_tx = Self::new_unhashed(OpTypedTransaction::Eip2930(tx), signature);
407                signed_tx.hash.get_or_init(|| hash);
408                Ok(signed_tx)
409            }
410            op_alloy_consensus::OpTxType::Eip1559 => {
411                let (tx, signature, hash) = TxEip1559::rlp_decode_signed(buf)?.into_parts();
412                let signed_tx = Self::new_unhashed(OpTypedTransaction::Eip1559(tx), signature);
413                signed_tx.hash.get_or_init(|| hash);
414                Ok(signed_tx)
415            }
416            op_alloy_consensus::OpTxType::Eip7702 => {
417                let (tx, signature, hash) = TxEip7702::rlp_decode_signed(buf)?.into_parts();
418                let signed_tx = Self::new_unhashed(OpTypedTransaction::Eip7702(tx), signature);
419                signed_tx.hash.get_or_init(|| hash);
420                Ok(signed_tx)
421            }
422            op_alloy_consensus::OpTxType::Deposit => Ok(Self::new_unhashed(
423                OpTypedTransaction::Deposit(TxDeposit::rlp_decode(buf)?),
424                TxDeposit::signature(),
425            )),
426        }
427    }
428
429    fn fallback_decode(buf: &mut &[u8]) -> Eip2718Result<Self> {
430        let (transaction, signature) = TxLegacy::rlp_decode_with_signature(buf)?;
431        let signed_tx = Self::new_unhashed(OpTypedTransaction::Legacy(transaction), signature);
432
433        Ok(signed_tx)
434    }
435}
436
437impl Transaction for OpTransactionSigned {
438    fn chain_id(&self) -> Option<u64> {
439        self.deref().chain_id()
440    }
441
442    fn nonce(&self) -> u64 {
443        self.deref().nonce()
444    }
445
446    fn gas_limit(&self) -> u64 {
447        self.deref().gas_limit()
448    }
449
450    fn gas_price(&self) -> Option<u128> {
451        self.deref().gas_price()
452    }
453
454    fn max_fee_per_gas(&self) -> u128 {
455        self.deref().max_fee_per_gas()
456    }
457
458    fn max_priority_fee_per_gas(&self) -> Option<u128> {
459        self.deref().max_priority_fee_per_gas()
460    }
461
462    fn max_fee_per_blob_gas(&self) -> Option<u128> {
463        self.deref().max_fee_per_blob_gas()
464    }
465
466    fn priority_fee_or_price(&self) -> u128 {
467        self.deref().priority_fee_or_price()
468    }
469
470    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
471        self.deref().effective_gas_price(base_fee)
472    }
473
474    fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
475        self.deref().effective_tip_per_gas(base_fee)
476    }
477
478    fn is_dynamic_fee(&self) -> bool {
479        self.deref().is_dynamic_fee()
480    }
481
482    fn kind(&self) -> TxKind {
483        self.deref().kind()
484    }
485
486    fn is_create(&self) -> bool {
487        self.deref().is_create()
488    }
489
490    fn value(&self) -> Uint<256, 4> {
491        self.deref().value()
492    }
493
494    fn input(&self) -> &Bytes {
495        self.deref().input()
496    }
497
498    fn access_list(&self) -> Option<&AccessList> {
499        self.deref().access_list()
500    }
501
502    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
503        self.deref().blob_versioned_hashes()
504    }
505
506    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
507        self.deref().authorization_list()
508    }
509}
510
511impl Typed2718 for OpTransactionSigned {
512    fn ty(&self) -> u8 {
513        self.deref().ty()
514    }
515}
516
517impl PartialEq for OpTransactionSigned {
518    fn eq(&self, other: &Self) -> bool {
519        self.signature == other.signature &&
520            self.transaction == other.transaction &&
521            self.tx_hash() == other.tx_hash()
522    }
523}
524
525impl Hash for OpTransactionSigned {
526    fn hash<H: Hasher>(&self, state: &mut H) {
527        self.signature.hash(state);
528        self.transaction.hash(state);
529    }
530}
531
532#[cfg(feature = "reth-codec")]
533impl reth_codecs::Compact for OpTransactionSigned {
534    fn to_compact<B>(&self, buf: &mut B) -> usize
535    where
536        B: bytes::BufMut + AsMut<[u8]>,
537    {
538        let start = buf.as_mut().len();
539
540        // Placeholder for bitflags.
541        // The first byte uses 4 bits as flags: IsCompressed[1bit], TxType[2bits], Signature[1bit]
542        buf.put_u8(0);
543
544        let sig_bit = self.signature.to_compact(buf) as u8;
545        let zstd_bit = self.transaction.input().len() >= 32;
546
547        let tx_bits = if zstd_bit {
548            let mut tmp = Vec::with_capacity(256);
549            if cfg!(feature = "std") {
550                reth_zstd_compressors::TRANSACTION_COMPRESSOR.with(|compressor| {
551                    let mut compressor = compressor.borrow_mut();
552                    let tx_bits = self.transaction.to_compact(&mut tmp);
553                    buf.put_slice(&compressor.compress(&tmp).expect("Failed to compress"));
554                    tx_bits as u8
555                })
556            } else {
557                let mut compressor = reth_zstd_compressors::create_tx_compressor();
558                let tx_bits = self.transaction.to_compact(&mut tmp);
559                buf.put_slice(&compressor.compress(&tmp).expect("Failed to compress"));
560                tx_bits as u8
561            }
562        } else {
563            self.transaction.to_compact(buf) as u8
564        };
565
566        // Replace bitflags with the actual values
567        buf.as_mut()[start] = sig_bit | (tx_bits << 1) | ((zstd_bit as u8) << 3);
568
569        buf.as_mut().len() - start
570    }
571
572    fn from_compact(mut buf: &[u8], _len: usize) -> (Self, &[u8]) {
573        use bytes::Buf;
574
575        // The first byte uses 4 bits as flags: IsCompressed[1], TxType[2], Signature[1]
576        let bitflags = buf.get_u8() as usize;
577
578        let sig_bit = bitflags & 1;
579        let (signature, buf) = Signature::from_compact(buf, sig_bit);
580
581        let zstd_bit = bitflags >> 3;
582        let (transaction, buf) = if zstd_bit != 0 {
583            if cfg!(feature = "std") {
584                reth_zstd_compressors::TRANSACTION_DECOMPRESSOR.with(|decompressor| {
585                    let mut decompressor = decompressor.borrow_mut();
586
587                    // TODO: enforce that zstd is only present at a "top" level type
588                    let transaction_type = (bitflags & 0b110) >> 1;
589                    let (transaction, _) = OpTypedTransaction::from_compact(
590                        decompressor.decompress(buf),
591                        transaction_type,
592                    );
593
594                    (transaction, buf)
595                })
596            } else {
597                let mut decompressor = reth_zstd_compressors::create_tx_decompressor();
598                let transaction_type = (bitflags & 0b110) >> 1;
599                let (transaction, _) = OpTypedTransaction::from_compact(
600                    decompressor.decompress(buf),
601                    transaction_type,
602                );
603
604                (transaction, buf)
605            }
606        } else {
607            let transaction_type = bitflags >> 1;
608            OpTypedTransaction::from_compact(buf, transaction_type)
609        };
610
611        (Self { signature, transaction, hash: Default::default() }, buf)
612    }
613}
614
615#[cfg(any(test, feature = "arbitrary"))]
616impl<'a> arbitrary::Arbitrary<'a> for OpTransactionSigned {
617    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
618        #[allow(unused_mut)]
619        let mut transaction = OpTypedTransaction::arbitrary(u)?;
620
621        let secp = secp256k1::Secp256k1::new();
622        let key_pair = secp256k1::Keypair::new(&secp, &mut rand::thread_rng());
623        let signature = reth_primitives_traits::crypto::secp256k1::sign_message(
624            B256::from_slice(&key_pair.secret_bytes()[..]),
625            signature_hash(&transaction),
626        )
627        .unwrap();
628
629        // Both `Some(0)` and `None` values are encoded as empty string byte. This introduces
630        // ambiguity in roundtrip tests. Patch the mint value of deposit transaction here, so that
631        // it's `None` if zero.
632        if let OpTypedTransaction::Deposit(ref mut tx_deposit) = transaction {
633            if tx_deposit.mint == Some(0) {
634                tx_deposit.mint = None;
635            }
636        }
637
638        let signature = if is_deposit(&transaction) { TxDeposit::signature() } else { signature };
639
640        Ok(Self::new_unhashed(transaction, signature))
641    }
642}
643
644/// Calculates the signing hash for the transaction.
645fn signature_hash(tx: &OpTypedTransaction) -> B256 {
646    match tx {
647        OpTypedTransaction::Legacy(tx) => tx.signature_hash(),
648        OpTypedTransaction::Eip2930(tx) => tx.signature_hash(),
649        OpTypedTransaction::Eip1559(tx) => tx.signature_hash(),
650        OpTypedTransaction::Eip7702(tx) => tx.signature_hash(),
651        OpTypedTransaction::Deposit(_) => B256::ZERO,
652    }
653}
654
655/// Returns `true` if transaction is deposit transaction.
656pub const fn is_deposit(tx: &OpTypedTransaction) -> bool {
657    matches!(tx, OpTypedTransaction::Deposit(_))
658}
659
660impl From<OpPooledTransaction> for OpTransactionSigned {
661    fn from(value: OpPooledTransaction) -> Self {
662        match value {
663            OpPooledTransaction::Legacy(tx) => tx.into(),
664            OpPooledTransaction::Eip2930(tx) => tx.into(),
665            OpPooledTransaction::Eip1559(tx) => tx.into(),
666            OpPooledTransaction::Eip7702(tx) => tx.into(),
667        }
668    }
669}
670
671impl TryFrom<OpTransactionSigned> for OpPooledTransaction {
672    type Error = TransactionConversionError;
673
674    fn try_from(value: OpTransactionSigned) -> Result<Self, Self::Error> {
675        let hash = *value.tx_hash();
676        let OpTransactionSigned { hash: _, signature, transaction } = value;
677
678        match transaction {
679            OpTypedTransaction::Legacy(tx) => {
680                Ok(Self::Legacy(Signed::new_unchecked(tx, signature, hash)))
681            }
682            OpTypedTransaction::Eip2930(tx) => {
683                Ok(Self::Eip2930(Signed::new_unchecked(tx, signature, hash)))
684            }
685            OpTypedTransaction::Eip1559(tx) => {
686                Ok(Self::Eip1559(Signed::new_unchecked(tx, signature, hash)))
687            }
688            OpTypedTransaction::Eip7702(tx) => {
689                Ok(Self::Eip7702(Signed::new_unchecked(tx, signature, hash)))
690            }
691            OpTypedTransaction::Deposit(_) => Err(TransactionConversionError::UnsupportedForP2P),
692        }
693    }
694}
695
696impl DepositTransaction for OpTransactionSigned {
697    fn source_hash(&self) -> Option<B256> {
698        match &self.transaction {
699            OpTypedTransaction::Deposit(tx) => Some(tx.source_hash),
700            _ => None,
701        }
702    }
703
704    fn mint(&self) -> Option<u128> {
705        match &self.transaction {
706            OpTypedTransaction::Deposit(tx) => tx.mint,
707            _ => None,
708        }
709    }
710
711    fn is_system_transaction(&self) -> bool {
712        self.is_deposit()
713    }
714}
715
716/// Bincode-compatible transaction type serde implementations.
717#[cfg(feature = "serde-bincode-compat")]
718pub mod serde_bincode_compat {
719    use alloy_consensus::transaction::serde_bincode_compat::{
720        TxEip1559, TxEip2930, TxEip7702, TxLegacy,
721    };
722    use alloy_primitives::{PrimitiveSignature as Signature, TxHash};
723    use reth_primitives_traits::{serde_bincode_compat::SerdeBincodeCompat, SignedTransaction};
724    use serde::{Deserialize, Serialize};
725
726    /// Bincode-compatible [`super::OpTypedTransaction`] serde implementation.
727    #[derive(Debug, Serialize, Deserialize)]
728    #[allow(missing_docs)]
729    enum OpTypedTransaction<'a> {
730        Legacy(TxLegacy<'a>),
731        Eip2930(TxEip2930<'a>),
732        Eip1559(TxEip1559<'a>),
733        Eip7702(TxEip7702<'a>),
734        Deposit(op_alloy_consensus::serde_bincode_compat::TxDeposit<'a>),
735    }
736
737    impl<'a> From<&'a super::OpTypedTransaction> for OpTypedTransaction<'a> {
738        fn from(value: &'a super::OpTypedTransaction) -> Self {
739            match value {
740                super::OpTypedTransaction::Legacy(tx) => Self::Legacy(TxLegacy::from(tx)),
741                super::OpTypedTransaction::Eip2930(tx) => Self::Eip2930(TxEip2930::from(tx)),
742                super::OpTypedTransaction::Eip1559(tx) => Self::Eip1559(TxEip1559::from(tx)),
743                super::OpTypedTransaction::Eip7702(tx) => Self::Eip7702(TxEip7702::from(tx)),
744                super::OpTypedTransaction::Deposit(tx) => {
745                    Self::Deposit(op_alloy_consensus::serde_bincode_compat::TxDeposit::from(tx))
746                }
747            }
748        }
749    }
750
751    impl<'a> From<OpTypedTransaction<'a>> for super::OpTypedTransaction {
752        fn from(value: OpTypedTransaction<'a>) -> Self {
753            match value {
754                OpTypedTransaction::Legacy(tx) => Self::Legacy(tx.into()),
755                OpTypedTransaction::Eip2930(tx) => Self::Eip2930(tx.into()),
756                OpTypedTransaction::Eip1559(tx) => Self::Eip1559(tx.into()),
757                OpTypedTransaction::Eip7702(tx) => Self::Eip7702(tx.into()),
758                OpTypedTransaction::Deposit(tx) => Self::Deposit(tx.into()),
759            }
760        }
761    }
762
763    /// Bincode-compatible [`super::OpTransactionSigned`] serde implementation.
764    #[derive(Debug, Serialize, Deserialize)]
765    pub struct OpTransactionSigned<'a> {
766        hash: TxHash,
767        signature: Signature,
768        transaction: OpTypedTransaction<'a>,
769    }
770
771    impl<'a> From<&'a super::OpTransactionSigned> for OpTransactionSigned<'a> {
772        fn from(value: &'a super::OpTransactionSigned) -> Self {
773            Self {
774                hash: *value.tx_hash(),
775                signature: value.signature,
776                transaction: OpTypedTransaction::from(&value.transaction),
777            }
778        }
779    }
780
781    impl<'a> From<OpTransactionSigned<'a>> for super::OpTransactionSigned {
782        fn from(value: OpTransactionSigned<'a>) -> Self {
783            Self {
784                hash: value.hash.into(),
785                signature: value.signature,
786                transaction: value.transaction.into(),
787            }
788        }
789    }
790
791    impl SerdeBincodeCompat for super::OpTransactionSigned {
792        type BincodeRepr<'a> = OpTransactionSigned<'a>;
793
794        fn as_repr(&self) -> Self::BincodeRepr<'_> {
795            self.into()
796        }
797
798        fn from_repr(repr: Self::BincodeRepr<'_>) -> Self {
799            repr.into()
800        }
801    }
802}