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