Skip to main content

tempo_primitives/transaction/
envelope.rs

1use super::tt_signed::AASigned;
2use crate::{TempoTransaction, subblock::PartialValidatorKey};
3use alloy_consensus::{
4    EthereumTxEnvelope, SignableTransaction, Signed, Transaction, TxEip1559, TxEip2930, TxEip7702,
5    TxLegacy, TxType, TypedTransaction,
6    crypto::RecoveryError,
7    error::{UnsupportedTransactionType, ValueError},
8    transaction::Either,
9};
10use alloy_primitives::{Address, B256, Bytes, Signature, TxKind, U256, hex};
11use core::fmt;
12use tempo_contracts::precompiles::ITIP20;
13
14/// TIP20 payment address prefix (12 bytes for payment classification)
15/// Same as TIP20_TOKEN_PREFIX
16pub const TIP20_PAYMENT_PREFIX: [u8; 12] = hex!("20C000000000000000000000");
17
18/// Fake signature for Tempo system transactions.
19pub const TEMPO_SYSTEM_TX_SIGNATURE: Signature = Signature::new(U256::ZERO, U256::ZERO, false);
20
21/// Fake sender for Tempo system transactions.
22pub const TEMPO_SYSTEM_TX_SENDER: Address = Address::ZERO;
23
24/// Tempo transaction envelope containing all supported transaction types
25///
26/// Transaction types included:
27/// - Legacy transactions
28/// - EIP-2930 access list transactions
29/// - EIP-1559 dynamic fee transactions
30/// - EIP-7702 authorization list transactions
31/// - Tempo transactions
32#[derive(Clone, Debug, alloy_consensus::TransactionEnvelope)]
33#[envelope(
34    tx_type_name = TempoTxType,
35    typed = TempoTypedTransaction,
36    arbitrary_cfg(any(test, feature = "arbitrary")),
37    serde_cfg(feature = "serde")
38)]
39#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
40#[allow(clippy::large_enum_variant)]
41pub enum TempoTxEnvelope {
42    /// Legacy transaction (type 0x00)
43    #[envelope(ty = 0)]
44    Legacy(Signed<TxLegacy>),
45
46    /// EIP-2930 access list transaction (type 0x01)
47    #[envelope(ty = 1)]
48    Eip2930(Signed<TxEip2930>),
49
50    /// EIP-1559 dynamic fee transaction (type 0x02)
51    #[envelope(ty = 2)]
52    Eip1559(Signed<TxEip1559>),
53
54    /// EIP-7702 authorization list transaction (type 0x04)
55    #[envelope(ty = 4)]
56    Eip7702(Signed<TxEip7702>),
57
58    /// Tempo transaction (type 0x76)
59    #[envelope(ty = 0x76, typed = TempoTransaction)]
60    AA(AASigned),
61}
62
63impl TryFrom<TxType> for TempoTxType {
64    type Error = UnsupportedTransactionType<TxType>;
65
66    fn try_from(value: TxType) -> Result<Self, Self::Error> {
67        Ok(match value {
68            TxType::Legacy => Self::Legacy,
69            TxType::Eip2930 => Self::Eip2930,
70            TxType::Eip1559 => Self::Eip1559,
71            TxType::Eip4844 => return Err(UnsupportedTransactionType::new(TxType::Eip4844)),
72            TxType::Eip7702 => Self::Eip7702,
73        })
74    }
75}
76
77impl TryFrom<TempoTxType> for TxType {
78    type Error = UnsupportedTransactionType<TempoTxType>;
79
80    fn try_from(value: TempoTxType) -> Result<Self, Self::Error> {
81        Ok(match value {
82            TempoTxType::Legacy => Self::Legacy,
83            TempoTxType::Eip2930 => Self::Eip2930,
84            TempoTxType::Eip1559 => Self::Eip1559,
85            TempoTxType::Eip7702 => Self::Eip7702,
86            TempoTxType::AA => {
87                return Err(UnsupportedTransactionType::new(TempoTxType::AA));
88            }
89        })
90    }
91}
92
93impl TempoTxEnvelope {
94    /// Returns the fee token preference if this is a fee token transaction
95    pub fn fee_token(&self) -> Option<Address> {
96        match self {
97            Self::AA(tx) => tx.tx().fee_token,
98            _ => None,
99        }
100    }
101
102    /// Resolves fee payer for the transaction.
103    pub fn fee_payer(&self, sender: Address) -> Result<Address, RecoveryError> {
104        match self {
105            Self::AA(tx) => tx.tx().recover_fee_payer(sender),
106            _ => Ok(sender),
107        }
108    }
109
110    /// Return the [`TempoTxType`] of the inner txn.
111    pub const fn tx_type(&self) -> TempoTxType {
112        match self {
113            Self::Legacy(_) => TempoTxType::Legacy,
114            Self::Eip2930(_) => TempoTxType::Eip2930,
115            Self::Eip1559(_) => TempoTxType::Eip1559,
116            Self::Eip7702(_) => TempoTxType::Eip7702,
117            Self::AA(_) => TempoTxType::AA,
118        }
119    }
120
121    /// Returns true if this is a fee token transaction
122    pub fn is_fee_token(&self) -> bool {
123        matches!(self, Self::AA(_))
124    }
125
126    /// Returns the authorization list if present (for EIP-7702 transactions)
127    pub fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
128        match self {
129            Self::Eip7702(tx) => Some(&tx.tx().authorization_list),
130            _ => None,
131        }
132    }
133
134    /// Returns the Tempo authorization list if present (for Tempo transactions)
135    pub fn tempo_authorization_list(
136        &self,
137    ) -> Option<&[crate::transaction::TempoSignedAuthorization]> {
138        match self {
139            Self::AA(tx) => Some(&tx.tx().tempo_authorization_list),
140            _ => None,
141        }
142    }
143
144    /// Returns true if this is a Tempo system transaction
145    pub fn is_system_tx(&self) -> bool {
146        matches!(self, Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE)
147    }
148
149    /// Returns true if this is a valid Tempo system transaction, i.e all gas fields and nonce are zero.
150    pub fn is_valid_system_tx(&self, chain_id: u64) -> bool {
151        self.max_fee_per_gas() == 0
152            && self.gas_limit() == 0
153            && self.value().is_zero()
154            && self.chain_id() == Some(chain_id)
155            && self.nonce() == 0
156    }
157
158    /// [TIP-20 payment] classification: `to` address has the `0x20c0` prefix.
159    ///
160    /// A transaction is considered a payment if its `to` address carries the TIP-20 prefix.
161    /// For AA transactions, every call must target a TIP-20 address.
162    ///
163    /// # NOTE
164    /// Consensus-level classifier, used during block validation, against `general_gas_limit`.
165    /// See [`is_payment_v2`](Self::is_payment_v2) for the stricter builder-level variant.
166    ///
167    /// [TIP-20 payment]: <https://docs.tempo.xyz/protocol/tip20/overview#get-predictable-payment-fees>
168    pub fn is_payment_v1(&self) -> bool {
169        match self {
170            Self::Legacy(tx) => is_tip20_call(tx.tx().to.to()),
171            Self::Eip2930(tx) => is_tip20_call(tx.tx().to.to()),
172            Self::Eip1559(tx) => is_tip20_call(tx.tx().to.to()),
173            Self::Eip7702(tx) => is_tip20_call(Some(&tx.tx().to)),
174            Self::AA(tx) => tx.tx().calls.iter().all(|call| is_tip20_call(call.to.to())),
175        }
176    }
177
178    /// Strict [TIP-20 payment] classification: `0x20c0` prefix AND recognized calldata.
179    ///
180    /// Like [`is_payment_v1`](Self::is_payment_v1), but additionally requires calldata to match a
181    /// recognized payment selector with exact ABI-encoded length.
182    ///
183    /// # NOTE
184    /// Builder-level classifier, used by the transaction pool and payload builder to prevent DoS of
185    /// the payment lane. NOT enforced during block validation — a future TIP will enshrine this
186    /// stricter classification at the protocol level.
187    ///
188    /// [TIP-20 payment]: <https://docs.tempo.xyz/protocol/tip20/overview#get-predictable-payment-fees>
189    pub fn is_payment_v2(&self) -> bool {
190        match self {
191            Self::Legacy(tx) => is_tip20_payment(tx.tx().to.to(), &tx.tx().input),
192            Self::Eip2930(tx) => is_tip20_payment(tx.tx().to.to(), &tx.tx().input),
193            Self::Eip1559(tx) => is_tip20_payment(tx.tx().to.to(), &tx.tx().input),
194            Self::Eip7702(tx) => is_tip20_payment(Some(&tx.tx().to), &tx.tx().input),
195            Self::AA(tx) => {
196                !tx.tx().calls.is_empty()
197                    && tx
198                        .tx()
199                        .calls
200                        .iter()
201                        .all(|call| is_tip20_payment(call.to.to(), &call.input))
202            }
203        }
204    }
205
206    /// Returns the proposer of the subblock if this is a subblock transaction.
207    pub fn subblock_proposer(&self) -> Option<PartialValidatorKey> {
208        let Self::AA(tx) = &self else { return None };
209        tx.tx().subblock_proposer()
210    }
211
212    /// Returns the [`AASigned`] transaction if this is a Tempo transaction.
213    pub fn as_aa(&self) -> Option<&AASigned> {
214        match self {
215            Self::AA(tx) => Some(tx),
216            _ => None,
217        }
218    }
219
220    /// Returns the nonce key of this transaction if it's an [`AASigned`] transaction.
221    pub fn nonce_key(&self) -> Option<U256> {
222        self.as_aa().map(|tx| tx.tx().nonce_key)
223    }
224
225    /// Returns true if this is a Tempo transaction
226    pub fn is_aa(&self) -> bool {
227        matches!(self, Self::AA(_))
228    }
229
230    /// Returns iterator over the calls in the transaction.
231    pub fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
232        if let Some(aa) = self.as_aa() {
233            Either::Left(aa.tx().calls.iter().map(|call| (call.to, &call.input)))
234        } else {
235            Either::Right(core::iter::once((self.kind(), self.input())))
236        }
237    }
238}
239
240impl alloy_consensus::transaction::SignerRecoverable for TempoTxEnvelope {
241    fn recover_signer(
242        &self,
243    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
244        match self {
245            Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE => Ok(Address::ZERO),
246            Self::Legacy(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx),
247            Self::Eip2930(tx) => {
248                alloy_consensus::transaction::SignerRecoverable::recover_signer(tx)
249            }
250            Self::Eip1559(tx) => {
251                alloy_consensus::transaction::SignerRecoverable::recover_signer(tx)
252            }
253            Self::Eip7702(tx) => {
254                alloy_consensus::transaction::SignerRecoverable::recover_signer(tx)
255            }
256            Self::AA(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx),
257        }
258    }
259
260    fn recover_signer_unchecked(
261        &self,
262    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
263        match self {
264            Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE => Ok(Address::ZERO),
265            Self::Legacy(tx) => {
266                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
267            }
268            Self::Eip2930(tx) => {
269                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
270            }
271            Self::Eip1559(tx) => {
272                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
273            }
274            Self::Eip7702(tx) => {
275                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
276            }
277            Self::AA(tx) => {
278                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
279            }
280        }
281    }
282}
283
284#[cfg(feature = "reth")]
285impl reth_primitives_traits::InMemorySize for TempoTxEnvelope {
286    fn size(&self) -> usize {
287        match self {
288            Self::Legacy(tx) => tx.size(),
289            Self::Eip2930(tx) => tx.size(),
290            Self::Eip1559(tx) => tx.size(),
291            Self::Eip7702(tx) => tx.size(),
292            Self::AA(tx) => tx.size(),
293        }
294    }
295}
296
297impl alloy_consensus::transaction::TxHashRef for TempoTxEnvelope {
298    fn tx_hash(&self) -> &B256 {
299        match self {
300            Self::Legacy(tx) => tx.hash(),
301            Self::Eip2930(tx) => tx.hash(),
302            Self::Eip1559(tx) => tx.hash(),
303            Self::Eip7702(tx) => tx.hash(),
304            Self::AA(tx) => tx.hash(),
305        }
306    }
307}
308
309#[cfg(feature = "reth")]
310impl reth_primitives_traits::SignedTransaction for TempoTxEnvelope {}
311
312#[cfg(feature = "reth")]
313impl reth_primitives_traits::InMemorySize for TempoTxType {
314    fn size(&self) -> usize {
315        size_of::<Self>()
316    }
317}
318
319impl alloy_consensus::InMemorySize for TempoTxType {
320    fn size(&self) -> usize {
321        size_of::<Self>()
322    }
323}
324
325impl fmt::Display for TempoTxType {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        match self {
328            Self::Legacy => write!(f, "Legacy"),
329            Self::Eip2930 => write!(f, "EIP-2930"),
330            Self::Eip1559 => write!(f, "EIP-1559"),
331            Self::Eip7702 => write!(f, "EIP-7702"),
332            Self::AA => write!(f, "AA"),
333        }
334    }
335}
336
337impl<Eip4844> TryFrom<EthereumTxEnvelope<Eip4844>> for TempoTxEnvelope {
338    type Error = ValueError<EthereumTxEnvelope<Eip4844>>;
339
340    fn try_from(value: EthereumTxEnvelope<Eip4844>) -> Result<Self, Self::Error> {
341        match value {
342            EthereumTxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
343            EthereumTxEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
344            tx @ EthereumTxEnvelope::Eip4844(_) => Err(ValueError::new_static(
345                tx,
346                "EIP-4844 transactions are not supported",
347            )),
348            EthereumTxEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
349            EthereumTxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
350        }
351    }
352}
353
354impl From<Signed<TxLegacy>> for TempoTxEnvelope {
355    fn from(value: Signed<TxLegacy>) -> Self {
356        Self::Legacy(value)
357    }
358}
359
360impl From<Signed<TxEip2930>> for TempoTxEnvelope {
361    fn from(value: Signed<TxEip2930>) -> Self {
362        Self::Eip2930(value)
363    }
364}
365
366impl From<Signed<TxEip1559>> for TempoTxEnvelope {
367    fn from(value: Signed<TxEip1559>) -> Self {
368        Self::Eip1559(value)
369    }
370}
371
372impl From<Signed<TxEip7702>> for TempoTxEnvelope {
373    fn from(value: Signed<TxEip7702>) -> Self {
374        Self::Eip7702(value)
375    }
376}
377
378impl From<AASigned> for TempoTxEnvelope {
379    fn from(value: AASigned) -> Self {
380        Self::AA(value)
381    }
382}
383
384impl TempoTypedTransaction {
385    /// Converts this typed transaction into a signed [`TempoTxEnvelope`]
386    pub fn into_envelope(self, sig: Signature) -> TempoTxEnvelope {
387        match self {
388            Self::Legacy(tx) => tx.into_signed(sig).into(),
389            Self::Eip2930(tx) => tx.into_signed(sig).into(),
390            Self::Eip1559(tx) => tx.into_signed(sig).into(),
391            Self::Eip7702(tx) => tx.into_signed(sig).into(),
392            Self::AA(tx) => tx.into_signed(sig.into()).into(),
393        }
394    }
395
396    /// Returns a dyn mutable reference to the underlying transaction
397    pub fn as_dyn_signable_mut(&mut self) -> &mut dyn SignableTransaction<Signature> {
398        match self {
399            Self::Legacy(tx) => tx,
400            Self::Eip2930(tx) => tx,
401            Self::Eip1559(tx) => tx,
402            Self::Eip7702(tx) => tx,
403            Self::AA(tx) => tx,
404        }
405    }
406}
407
408impl TryFrom<TypedTransaction> for TempoTypedTransaction {
409    type Error = UnsupportedTransactionType<TxType>;
410
411    fn try_from(value: TypedTransaction) -> Result<Self, Self::Error> {
412        Ok(match value {
413            TypedTransaction::Legacy(tx) => Self::Legacy(tx),
414            TypedTransaction::Eip2930(tx) => Self::Eip2930(tx),
415            TypedTransaction::Eip1559(tx) => Self::Eip1559(tx),
416            TypedTransaction::Eip4844(..) => {
417                return Err(UnsupportedTransactionType::new(TxType::Eip4844));
418            }
419            TypedTransaction::Eip7702(tx) => Self::Eip7702(tx),
420        })
421    }
422}
423
424impl From<TempoTxEnvelope> for TempoTypedTransaction {
425    fn from(value: TempoTxEnvelope) -> Self {
426        match value {
427            TempoTxEnvelope::Legacy(tx) => Self::Legacy(tx.into_parts().0),
428            TempoTxEnvelope::Eip2930(tx) => Self::Eip2930(tx.into_parts().0),
429            TempoTxEnvelope::Eip1559(tx) => Self::Eip1559(tx.into_parts().0),
430            TempoTxEnvelope::Eip7702(tx) => Self::Eip7702(tx.into_parts().0),
431            TempoTxEnvelope::AA(tx) => Self::AA(tx.into_parts().0),
432        }
433    }
434}
435
436impl From<TempoTransaction> for TempoTypedTransaction {
437    fn from(value: TempoTransaction) -> Self {
438        Self::AA(value)
439    }
440}
441
442/// Returns `true` if `to` has the TIP-20 payment prefix.
443fn is_tip20_call(to: Option<&Address>) -> bool {
444    to.is_some_and(|to| to.starts_with(&TIP20_PAYMENT_PREFIX))
445}
446
447/// Returns `true` if `to` has the TIP-20 payment prefix and `input` is recognized payment
448/// calldata (selector + exact ABI-encoded length).
449fn is_tip20_payment(to: Option<&Address>, input: &[u8]) -> bool {
450    is_tip20_call(to) && ITIP20::ITIP20Calls::is_payment(input)
451}
452
453#[cfg(feature = "rpc")]
454impl reth_rpc_convert::SignableTxRequest<TempoTxEnvelope>
455    for alloy_rpc_types_eth::TransactionRequest
456{
457    async fn try_build_and_sign(
458        self,
459        signer: impl alloy_network::TxSigner<alloy_primitives::Signature> + Send,
460    ) -> Result<TempoTxEnvelope, reth_rpc_convert::SignTxRequestError> {
461        reth_rpc_convert::SignableTxRequest::<
462            EthereumTxEnvelope<alloy_consensus::TxEip4844>,
463        >::try_build_and_sign(self, signer)
464        .await
465        .and_then(|tx| {
466            tx.try_into()
467                .map_err(|_| reth_rpc_convert::SignTxRequestError::InvalidTransactionRequest)
468        })
469    }
470}
471
472#[cfg(feature = "rpc")]
473impl reth_rpc_convert::TryIntoSimTx<TempoTxEnvelope> for alloy_rpc_types_eth::TransactionRequest {
474    fn try_into_sim_tx(self) -> Result<TempoTxEnvelope, ValueError<Self>> {
475        let tx = self.clone().build_typed_simulate_transaction()?;
476        tx.try_into()
477            .map_err(|_| ValueError::new_static(self, "Invalid transaction request"))
478    }
479}
480
481#[cfg(all(feature = "serde-bincode-compat", feature = "reth"))]
482impl reth_primitives_traits::serde_bincode_compat::RlpBincode for TempoTxEnvelope {}
483
484#[cfg(feature = "reth-codec")]
485mod codec {
486    use crate::{TempoSignature, TempoTransaction};
487
488    use super::*;
489    use alloy_eips::eip2718::EIP7702_TX_TYPE_ID;
490    use alloy_primitives::{
491        Bytes, Signature,
492        bytes::{self, BufMut},
493    };
494    use reth_codecs::{
495        Compact,
496        alloy::transaction::{CompactEnvelope, Envelope},
497        txtype::{
498            COMPACT_EXTENDED_IDENTIFIER_FLAG, COMPACT_IDENTIFIER_EIP1559,
499            COMPACT_IDENTIFIER_EIP2930, COMPACT_IDENTIFIER_LEGACY,
500        },
501    };
502
503    impl reth_codecs::alloy::transaction::FromTxCompact for TempoTxEnvelope {
504        type TxType = TempoTxType;
505
506        fn from_tx_compact(
507            buf: &[u8],
508            tx_type: Self::TxType,
509            signature: Signature,
510        ) -> (Self, &[u8]) {
511            use alloy_consensus::Signed;
512            use reth_codecs::Compact;
513
514            match tx_type {
515                TempoTxType::Legacy => {
516                    let (tx, buf) = TxLegacy::from_compact(buf, buf.len());
517                    let tx = Signed::new_unhashed(tx, signature);
518                    (Self::Legacy(tx), buf)
519                }
520                TempoTxType::Eip2930 => {
521                    let (tx, buf) = TxEip2930::from_compact(buf, buf.len());
522                    let tx = Signed::new_unhashed(tx, signature);
523                    (Self::Eip2930(tx), buf)
524                }
525                TempoTxType::Eip1559 => {
526                    let (tx, buf) = TxEip1559::from_compact(buf, buf.len());
527                    let tx = Signed::new_unhashed(tx, signature);
528                    (Self::Eip1559(tx), buf)
529                }
530                TempoTxType::Eip7702 => {
531                    let (tx, buf) = TxEip7702::from_compact(buf, buf.len());
532                    let tx = Signed::new_unhashed(tx, signature);
533                    (Self::Eip7702(tx), buf)
534                }
535                TempoTxType::AA => {
536                    let (tx, buf) = TempoTransaction::from_compact(buf, buf.len());
537                    // For Tempo transactions, we need to decode the signature bytes as TempoSignature
538                    let (sig_bytes, buf) = Bytes::from_compact(buf, buf.len());
539                    let aa_sig = TempoSignature::from_bytes(&sig_bytes)
540                        .map_err(|e| panic!("Failed to decode AA signature: {e}"))
541                        .unwrap();
542                    let tx = AASigned::new_unhashed(tx, aa_sig);
543                    (Self::AA(tx), buf)
544                }
545            }
546        }
547    }
548
549    impl reth_codecs::alloy::transaction::ToTxCompact for TempoTxEnvelope {
550        fn to_tx_compact(&self, buf: &mut (impl BufMut + AsMut<[u8]>)) {
551            match self {
552                Self::Legacy(tx) => tx.tx().to_compact(buf),
553                Self::Eip2930(tx) => tx.tx().to_compact(buf),
554                Self::Eip1559(tx) => tx.tx().to_compact(buf),
555                Self::Eip7702(tx) => tx.tx().to_compact(buf),
556                Self::AA(tx) => {
557                    let mut len = tx.tx().to_compact(buf);
558                    // Also encode the TempoSignature as Bytes
559                    len += tx.signature().to_bytes().to_compact(buf);
560                    len
561                }
562            };
563        }
564    }
565
566    impl Envelope for TempoTxEnvelope {
567        fn signature(&self) -> &Signature {
568            match self {
569                Self::Legacy(tx) => tx.signature(),
570                Self::Eip2930(tx) => tx.signature(),
571                Self::Eip1559(tx) => tx.signature(),
572                Self::Eip7702(tx) => tx.signature(),
573                Self::AA(_tx) => {
574                    // TODO: Will this work?
575                    &TEMPO_SYSTEM_TX_SIGNATURE
576                }
577            }
578        }
579
580        fn tx_type(&self) -> Self::TxType {
581            Self::tx_type(self)
582        }
583    }
584
585    impl Compact for TempoTxType {
586        fn to_compact<B>(&self, buf: &mut B) -> usize
587        where
588            B: BufMut + AsMut<[u8]>,
589        {
590            match self {
591                Self::Legacy => COMPACT_IDENTIFIER_LEGACY,
592                Self::Eip2930 => COMPACT_IDENTIFIER_EIP2930,
593                Self::Eip1559 => COMPACT_IDENTIFIER_EIP1559,
594                Self::Eip7702 => {
595                    buf.put_u8(EIP7702_TX_TYPE_ID);
596                    COMPACT_EXTENDED_IDENTIFIER_FLAG
597                }
598                Self::AA => {
599                    buf.put_u8(crate::transaction::TEMPO_TX_TYPE_ID);
600                    COMPACT_EXTENDED_IDENTIFIER_FLAG
601                }
602            }
603        }
604
605        // For backwards compatibility purposes only 2 bits of the type are encoded in the identifier
606        // parameter. In the case of a [`COMPACT_EXTENDED_IDENTIFIER_FLAG`], the full transaction type
607        // is read from the buffer as a single byte.
608        fn from_compact(mut buf: &[u8], identifier: usize) -> (Self, &[u8]) {
609            use bytes::Buf;
610            (
611                match identifier {
612                    COMPACT_IDENTIFIER_LEGACY => Self::Legacy,
613                    COMPACT_IDENTIFIER_EIP2930 => Self::Eip2930,
614                    COMPACT_IDENTIFIER_EIP1559 => Self::Eip1559,
615                    COMPACT_EXTENDED_IDENTIFIER_FLAG => {
616                        let extended_identifier = buf.get_u8();
617                        match extended_identifier {
618                            EIP7702_TX_TYPE_ID => Self::Eip7702,
619                            crate::transaction::TEMPO_TX_TYPE_ID => Self::AA,
620                            _ => panic!("Unsupported TxType identifier: {extended_identifier}"),
621                        }
622                    }
623                    _ => panic!("Unknown identifier for TxType: {identifier}"),
624                },
625                buf,
626            )
627        }
628    }
629
630    impl Compact for TempoTxEnvelope {
631        fn to_compact<B>(&self, buf: &mut B) -> usize
632        where
633            B: BufMut + AsMut<[u8]>,
634        {
635            CompactEnvelope::to_compact(self, buf)
636        }
637
638        fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) {
639            CompactEnvelope::from_compact(buf, len)
640        }
641    }
642
643    impl reth_db_api::table::Compress for TempoTxEnvelope {
644        type Compressed = alloc::vec::Vec<u8>;
645
646        fn compress_to_buf<B: alloy_primitives::bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
647            let _ = Compact::to_compact(self, buf);
648        }
649    }
650
651    impl reth_db_api::table::Decompress for TempoTxEnvelope {
652        fn decompress(value: &[u8]) -> Result<Self, reth_db_api::DatabaseError> {
653            let (obj, _) = Compact::from_compact(value, value.len());
654            Ok(obj)
655        }
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662    use crate::transaction::{Call, TempoTransaction};
663    use alloy_primitives::{Bytes, Signature, TxKind, U256, address};
664    use alloy_sol_types::SolCall;
665
666    const PAYMENT_TKN: Address = address!("20c0000000000000000000000000000000000001");
667
668    #[rustfmt::skip]
669    /// Returns valid ABI-encoded calldata for every recognized TIP-20 payment selector.
670    fn payment_calldatas() -> [Bytes; 9] {
671        let (to, from, amount, memo) = (Address::random(), Address::random(), U256::random(), B256::random());
672        [
673            ITIP20::transferCall { to, amount }.abi_encode().into(),
674            ITIP20::transferWithMemoCall { to, amount, memo }.abi_encode().into(),
675            ITIP20::transferFromCall { from, to, amount }.abi_encode().into(),
676            ITIP20::transferFromWithMemoCall { from, to, amount, memo }.abi_encode().into(),
677            ITIP20::approveCall { spender: to, amount }.abi_encode().into(),
678            ITIP20::mintCall { to, amount }.abi_encode().into(),
679            ITIP20::mintWithMemoCall { to, amount, memo }.abi_encode().into(),
680            ITIP20::burnCall { amount }.abi_encode().into(),
681            ITIP20::burnWithMemoCall { amount, memo }.abi_encode().into(),
682        ]
683    }
684
685    #[test]
686    fn test_non_fee_token_access() {
687        let legacy_tx = TxLegacy::default();
688        let signature = Signature::new(
689            alloy_primitives::U256::ZERO,
690            alloy_primitives::U256::ZERO,
691            false,
692        );
693        let signed = Signed::new_unhashed(legacy_tx, signature);
694        let envelope = TempoTxEnvelope::Legacy(signed);
695
696        assert!(!envelope.is_fee_token());
697        assert_eq!(envelope.fee_token(), None);
698        assert!(!envelope.is_aa());
699        assert!(envelope.as_aa().is_none());
700    }
701
702    #[test]
703    fn test_payment_classification_legacy_tx() {
704        // Test with legacy transaction type
705        let tx = TxLegacy {
706            to: TxKind::Call(PAYMENT_TKN),
707            gas_limit: 21000,
708            ..Default::default()
709        };
710        let signed = Signed::new_unhashed(tx, Signature::test_signature());
711        let envelope = TempoTxEnvelope::Legacy(signed);
712
713        assert!(envelope.is_payment_v1());
714    }
715
716    #[test]
717    fn test_payment_classification_non_payment() {
718        let non_payment_addr = address!("1234567890123456789012345678901234567890");
719        let tx = TxLegacy {
720            to: TxKind::Call(non_payment_addr),
721            gas_limit: 21000,
722            ..Default::default()
723        };
724        let signed = Signed::new_unhashed(tx, Signature::test_signature());
725        let envelope = TempoTxEnvelope::Legacy(signed);
726
727        assert!(!envelope.is_payment_v1());
728    }
729
730    fn create_aa_envelope(call: Call) -> TempoTxEnvelope {
731        let tx = TempoTransaction {
732            fee_token: Some(PAYMENT_TKN),
733            calls: vec![call],
734            ..Default::default()
735        };
736        TempoTxEnvelope::AA(tx.into_signed(Signature::test_signature().into()))
737    }
738
739    #[test]
740    fn test_payment_classification_aa_with_tip20_prefix() {
741        let payment_addr = address!("20c0000000000000000000000000000000000001");
742        let call = Call {
743            to: TxKind::Call(payment_addr),
744            value: U256::ZERO,
745            input: Bytes::new(),
746        };
747        let envelope = create_aa_envelope(call);
748        assert!(envelope.is_payment_v1());
749    }
750
751    #[test]
752    fn test_payment_classification_aa_without_tip20_prefix() {
753        let non_payment_addr = address!("1234567890123456789012345678901234567890");
754        let call = Call {
755            to: TxKind::Call(non_payment_addr),
756            value: U256::ZERO,
757            input: Bytes::new(),
758        };
759        let envelope = create_aa_envelope(call);
760        assert!(!envelope.is_payment_v1());
761    }
762
763    #[test]
764    fn test_payment_classification_aa_no_to_address() {
765        let call = Call {
766            to: TxKind::Create,
767            value: U256::ZERO,
768            input: Bytes::new(),
769        };
770        let envelope = create_aa_envelope(call);
771        assert!(!envelope.is_payment_v1());
772    }
773
774    #[test]
775    fn test_payment_classification_aa_partial_match() {
776        // First 12 bytes match TIP20_PAYMENT_PREFIX, remaining 8 bytes differ
777        let payment_addr = address!("20c0000000000000000000001111111111111111");
778        let call = Call {
779            to: TxKind::Call(payment_addr),
780            value: U256::ZERO,
781            input: Bytes::new(),
782        };
783        let envelope = create_aa_envelope(call);
784        assert!(envelope.is_payment_v1());
785    }
786
787    #[test]
788    fn test_payment_classification_aa_different_prefix() {
789        // Different prefix (30c0 instead of 20c0)
790        let non_payment_addr = address!("30c0000000000000000000000000000000000001");
791        let call = Call {
792            to: TxKind::Call(non_payment_addr),
793            value: U256::ZERO,
794            input: Bytes::new(),
795        };
796        let envelope = create_aa_envelope(call);
797        assert!(!envelope.is_payment_v1());
798    }
799
800    #[test]
801    fn test_is_payment_eip2930_eip1559_eip7702() {
802        use alloy_consensus::{TxEip1559, TxEip2930, TxEip7702};
803
804        // Eip2930 payment
805        let tx = TxEip2930 {
806            to: TxKind::Call(PAYMENT_TKN),
807            ..Default::default()
808        };
809        let envelope =
810            TempoTxEnvelope::Eip2930(Signed::new_unhashed(tx, Signature::test_signature()));
811        assert!(envelope.is_payment_v1());
812
813        // Eip2930 non-payment
814        let tx = TxEip2930 {
815            to: TxKind::Call(address!("1234567890123456789012345678901234567890")),
816            ..Default::default()
817        };
818        let envelope =
819            TempoTxEnvelope::Eip2930(Signed::new_unhashed(tx, Signature::test_signature()));
820        assert!(!envelope.is_payment_v1());
821
822        // Eip1559 payment
823        let tx = TxEip1559 {
824            to: TxKind::Call(PAYMENT_TKN),
825            ..Default::default()
826        };
827        let envelope =
828            TempoTxEnvelope::Eip1559(Signed::new_unhashed(tx, Signature::test_signature()));
829        assert!(envelope.is_payment_v1());
830
831        // Eip1559 non-payment
832        let tx = TxEip1559 {
833            to: TxKind::Call(address!("1234567890123456789012345678901234567890")),
834            ..Default::default()
835        };
836        let envelope =
837            TempoTxEnvelope::Eip1559(Signed::new_unhashed(tx, Signature::test_signature()));
838        assert!(!envelope.is_payment_v1());
839
840        // Eip7702 payment (note: Eip7702 has direct `to` address, not TxKind)
841        let tx = TxEip7702 {
842            to: PAYMENT_TKN,
843            ..Default::default()
844        };
845        let envelope =
846            TempoTxEnvelope::Eip7702(Signed::new_unhashed(tx, Signature::test_signature()));
847        assert!(envelope.is_payment_v1());
848
849        // Eip7702 non-payment
850        let tx = TxEip7702 {
851            to: address!("1234567890123456789012345678901234567890"),
852            ..Default::default()
853        };
854        let envelope =
855            TempoTxEnvelope::Eip7702(Signed::new_unhashed(tx, Signature::test_signature()));
856        assert!(!envelope.is_payment_v1());
857    }
858
859    #[test]
860    fn test_strict_payment_accepts_valid_calldata() {
861        for calldata in payment_calldatas() {
862            let tx = TxLegacy {
863                to: TxKind::Call(PAYMENT_TKN),
864                gas_limit: 21000,
865                input: calldata.clone(),
866                ..Default::default()
867            };
868            let signed = Signed::new_unhashed(tx, Signature::test_signature());
869            let envelope = TempoTxEnvelope::Legacy(signed);
870            assert!(
871                envelope.is_payment_v1(),
872                "is_payment should accept valid calldata"
873            );
874            assert!(
875                envelope.is_payment_v2(),
876                "is_strict_payment should accept valid calldata: {calldata}"
877            );
878        }
879    }
880
881    #[test]
882    fn test_strict_payment_rejects_empty_calldata() {
883        let tx = TxLegacy {
884            to: TxKind::Call(PAYMENT_TKN),
885            gas_limit: 21000,
886            ..Default::default()
887        };
888        let signed = Signed::new_unhashed(tx, Signature::test_signature());
889        let envelope = TempoTxEnvelope::Legacy(signed);
890        assert!(
891            envelope.is_payment_v1(),
892            "is_payment should accept (prefix-only)"
893        );
894        assert!(
895            !envelope.is_payment_v2(),
896            "is_strict_payment should reject empty calldata"
897        );
898    }
899
900    #[test]
901    fn test_strict_payment_rejects_excess_calldata() {
902        for calldata in payment_calldatas() {
903            let mut data = calldata.to_vec();
904            data.extend_from_slice(&[0u8; 32]);
905            let tx = TxLegacy {
906                to: TxKind::Call(PAYMENT_TKN),
907                gas_limit: 21000,
908                input: Bytes::from(data),
909                ..Default::default()
910            };
911            let signed = Signed::new_unhashed(tx, Signature::test_signature());
912            let envelope = TempoTxEnvelope::Legacy(signed);
913            assert!(envelope.is_payment_v1(), "v1 should accept (prefix-only)");
914            assert!(
915                !envelope.is_payment_v2(),
916                "v2 should reject excess calldata: {calldata}"
917            );
918        }
919    }
920
921    #[test]
922    fn test_strict_payment_rejects_unknown_selector() {
923        for calldata in payment_calldatas() {
924            let mut data = calldata.to_vec();
925            data[..4].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
926            let tx = TxLegacy {
927                to: TxKind::Call(PAYMENT_TKN),
928                gas_limit: 21000,
929                input: Bytes::from(data),
930                ..Default::default()
931            };
932            let signed = Signed::new_unhashed(tx, Signature::test_signature());
933            let envelope = TempoTxEnvelope::Legacy(signed);
934            assert!(envelope.is_payment_v1(), "v1 should accept (prefix-only)");
935            assert!(
936                !envelope.is_payment_v2(),
937                "v2 should reject unknown selector: {calldata}"
938            );
939        }
940    }
941
942    #[test]
943    fn test_strict_payment_aa_empty_calls() {
944        let tx = TempoTransaction {
945            fee_token: Some(PAYMENT_TKN),
946            calls: vec![],
947            ..Default::default()
948        };
949        let envelope = TempoTxEnvelope::AA(tx.into_signed(Signature::test_signature().into()));
950        assert!(
951            !envelope.is_payment_v2(),
952            "AA with empty calls should not be strict payment"
953        );
954    }
955
956    #[test]
957    fn test_strict_payment_aa_valid_calldata() {
958        for calldata in payment_calldatas() {
959            let call = Call {
960                to: TxKind::Call(PAYMENT_TKN),
961                value: U256::ZERO,
962                input: calldata,
963            };
964            let envelope = create_aa_envelope(call);
965            assert!(envelope.is_payment_v2());
966        }
967    }
968
969    #[test]
970    fn test_system_tx_validation_and_recovery() {
971        use alloy_consensus::transaction::SignerRecoverable;
972
973        let chain_id = 1u64;
974
975        // Valid system tx: all fields zero, correct chain_id, system signature
976        let tx = TxLegacy {
977            chain_id: Some(chain_id),
978            nonce: 0,
979            gas_price: 0,
980            gas_limit: 0,
981            to: TxKind::Call(Address::ZERO),
982            value: U256::ZERO,
983            input: Bytes::new(),
984        };
985        let system_tx =
986            TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
987
988        assert!(system_tx.is_system_tx(), "Should detect system signature");
989        assert!(
990            system_tx.is_valid_system_tx(chain_id),
991            "Should be valid system tx"
992        );
993
994        // recover_signer returns ZERO for system tx
995        let signer = system_tx.recover_signer().unwrap();
996        assert_eq!(
997            signer,
998            Address::ZERO,
999            "System tx signer should be Address::ZERO"
1000        );
1001
1002        // Invalid: wrong chain_id
1003        assert!(
1004            !system_tx.is_valid_system_tx(2),
1005            "Wrong chain_id should fail"
1006        );
1007
1008        // Invalid: non-zero gas_limit
1009        let tx = TxLegacy {
1010            chain_id: Some(chain_id),
1011            gas_limit: 1, // non-zero
1012            ..Default::default()
1013        };
1014        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1015        assert!(
1016            !envelope.is_valid_system_tx(chain_id),
1017            "Non-zero gas_limit should fail"
1018        );
1019
1020        // Invalid: non-zero value
1021        let tx = TxLegacy {
1022            chain_id: Some(chain_id),
1023            value: U256::from(1),
1024            ..Default::default()
1025        };
1026        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1027        assert!(
1028            !envelope.is_valid_system_tx(chain_id),
1029            "Non-zero value should fail"
1030        );
1031
1032        // Invalid: non-zero nonce
1033        let tx = TxLegacy {
1034            chain_id: Some(chain_id),
1035            nonce: 1,
1036            ..Default::default()
1037        };
1038        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1039        assert!(
1040            !envelope.is_valid_system_tx(chain_id),
1041            "Non-zero nonce should fail"
1042        );
1043
1044        // Non-system tx with regular signature should recover normally
1045        let tx = TxLegacy::default();
1046        let regular_tx =
1047            TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()));
1048        assert!(
1049            !regular_tx.is_system_tx(),
1050            "Regular tx should not be system tx"
1051        );
1052
1053        // fee_payer() for non-AA returns sender
1054        let sender = Address::random();
1055        assert_eq!(system_tx.fee_payer(sender).unwrap(), sender);
1056
1057        // calls() iterator for non-AA returns single item
1058        let calls: Vec<_> = system_tx.calls().collect();
1059        assert_eq!(calls.len(), 1);
1060        assert_eq!(calls[0].0, TxKind::Call(Address::ZERO));
1061
1062        // subblock_proposer() returns None for non-subblock tx
1063        assert!(system_tx.subblock_proposer().is_none());
1064
1065        // AA-specific methods
1066        let aa_envelope = create_aa_envelope(Call {
1067            to: TxKind::Call(PAYMENT_TKN),
1068            value: U256::ZERO,
1069            input: Bytes::new(),
1070        });
1071        assert!(aa_envelope.is_aa());
1072        assert!(aa_envelope.as_aa().is_some());
1073        assert_eq!(aa_envelope.fee_token(), Some(PAYMENT_TKN));
1074
1075        // calls() for AA tx
1076        let aa_calls: Vec<_> = aa_envelope.calls().collect();
1077        assert_eq!(aa_calls.len(), 1);
1078    }
1079
1080    #[test]
1081    fn test_try_from_ethereum_envelope_eip4844_rejected() {
1082        use alloy_consensus::TxEip4844;
1083
1084        // EIP-4844 should be rejected
1085        let eip4844_tx = TxEip4844::default();
1086        let eth_envelope: EthereumTxEnvelope<TxEip4844> = EthereumTxEnvelope::Eip4844(
1087            Signed::new_unhashed(eip4844_tx, Signature::test_signature()),
1088        );
1089
1090        let result = TempoTxEnvelope::try_from(eth_envelope);
1091        assert!(result.is_err(), "EIP-4844 should be rejected");
1092
1093        // Other types should be accepted
1094        let legacy_tx = TxLegacy::default();
1095        let eth_envelope: EthereumTxEnvelope<TxEip4844> = EthereumTxEnvelope::Legacy(
1096            Signed::new_unhashed(legacy_tx, Signature::test_signature()),
1097        );
1098        assert!(TempoTxEnvelope::try_from(eth_envelope).is_ok());
1099    }
1100
1101    #[test]
1102    fn test_tx_type_conversions() {
1103        // TxType -> TempoTxType: EIP-4844 rejected
1104        assert!(TempoTxType::try_from(TxType::Legacy).is_ok());
1105        assert!(TempoTxType::try_from(TxType::Eip2930).is_ok());
1106        assert!(TempoTxType::try_from(TxType::Eip1559).is_ok());
1107        assert!(TempoTxType::try_from(TxType::Eip7702).is_ok());
1108        assert!(TempoTxType::try_from(TxType::Eip4844).is_err());
1109
1110        // TempoTxType -> TxType: AA rejected
1111        assert!(TxType::try_from(TempoTxType::Legacy).is_ok());
1112        assert!(TxType::try_from(TempoTxType::Eip2930).is_ok());
1113        assert!(TxType::try_from(TempoTxType::Eip1559).is_ok());
1114        assert!(TxType::try_from(TempoTxType::Eip7702).is_ok());
1115        assert!(TxType::try_from(TempoTxType::AA).is_err());
1116    }
1117}