Skip to main content

tempo_primitives/transaction/
envelope.rs

1use super::{tt_signed::AASigned, unique_tx_identifier_from_signable};
2use crate::{TempoAddressExt, 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};
11use alloy_rlp::Encodable;
12use core::fmt;
13use tempo_contracts::precompiles::{ITIP20, ITIP20ChannelReserve, TIP20_CHANNEL_RESERVE_ADDRESS};
14
15/// Maximum RLP-encoded size of a `key_authorization` permitted in a payment transaction
16/// (TIP-1045). Comfortably fits realistic provisioning payloads with limits and scopes.
17pub const KEY_AUTHORIZATION_MAX_RLP_LEN: usize = 1024;
18
19/// Fake signature for Tempo system transactions.
20pub const TEMPO_SYSTEM_TX_SIGNATURE: Signature = Signature::new(U256::ZERO, U256::ZERO, false);
21
22/// Fake sender for Tempo system transactions.
23pub const TEMPO_SYSTEM_TX_SENDER: Address = Address::ZERO;
24
25/// Tempo transaction envelope containing all supported transaction types
26///
27/// Transaction types included:
28/// - Legacy transactions
29/// - EIP-2930 access list transactions
30/// - EIP-1559 dynamic fee transactions
31/// - EIP-7702 authorization list transactions
32/// - Tempo transactions
33#[derive(Clone, Debug, alloy_consensus::TransactionEnvelope)]
34#[envelope(
35    tx_type_name = TempoTxType,
36    typed = TempoTypedTransaction,
37    arbitrary_cfg(any(test, feature = "arbitrary")),
38    serde_cfg(feature = "serde")
39)]
40#[cfg_attr(test, reth_codecs::add_arbitrary_tests(compact, rlp))]
41#[allow(clippy::large_enum_variant)]
42pub enum TempoTxEnvelope {
43    /// Legacy transaction (type 0x00)
44    #[envelope(ty = 0)]
45    Legacy(Signed<TxLegacy>),
46
47    /// EIP-2930 access list transaction (type 0x01)
48    #[envelope(ty = 1)]
49    Eip2930(Signed<TxEip2930>),
50
51    /// EIP-1559 dynamic fee transaction (type 0x02)
52    #[envelope(ty = 2)]
53    Eip1559(Signed<TxEip1559>),
54
55    /// EIP-7702 authorization list transaction (type 0x04)
56    #[envelope(ty = 4)]
57    Eip7702(Signed<TxEip7702>),
58
59    /// Tempo transaction (type 0x76)
60    #[envelope(ty = 0x76, typed = TempoTransaction)]
61    AA(AASigned),
62}
63
64impl TryFrom<TxType> for TempoTxType {
65    type Error = UnsupportedTransactionType<TxType>;
66
67    fn try_from(value: TxType) -> Result<Self, Self::Error> {
68        Ok(match value {
69            TxType::Legacy => Self::Legacy,
70            TxType::Eip2930 => Self::Eip2930,
71            TxType::Eip1559 => Self::Eip1559,
72            TxType::Eip4844 => return Err(UnsupportedTransactionType::new(TxType::Eip4844)),
73            TxType::Eip7702 => Self::Eip7702,
74        })
75    }
76}
77
78impl TryFrom<TempoTxType> for TxType {
79    type Error = UnsupportedTransactionType<TempoTxType>;
80
81    fn try_from(value: TempoTxType) -> Result<Self, Self::Error> {
82        Ok(match value {
83            TempoTxType::Legacy => Self::Legacy,
84            TempoTxType::Eip2930 => Self::Eip2930,
85            TempoTxType::Eip1559 => Self::Eip1559,
86            TempoTxType::Eip7702 => Self::Eip7702,
87            TempoTxType::AA => {
88                return Err(UnsupportedTransactionType::new(TempoTxType::AA));
89            }
90        })
91    }
92}
93
94impl alloy_consensus::InMemorySize for TempoTxType {
95    fn size(&self) -> usize {
96        size_of::<Self>()
97    }
98}
99
100impl TempoTxEnvelope {
101    /// Returns the fee token preference if this is a fee token transaction
102    pub fn fee_token(&self) -> Option<Address> {
103        match self {
104            Self::AA(tx) => tx.tx().fee_token,
105            _ => None,
106        }
107    }
108
109    /// Resolves fee payer for the transaction.
110    pub fn fee_payer(&self, sender: Address) -> Result<Address, RecoveryError> {
111        match self {
112            Self::AA(tx) => tx.tx().recover_fee_payer(sender),
113            _ => Ok(sender),
114        }
115    }
116
117    /// Returns the sender-scoped transaction identifier used for replay-sensitive features.
118    pub fn unique_tx_identifier(&self, sender: Address) -> B256 {
119        match self {
120            Self::Legacy(tx) => unique_tx_identifier_from_signable(tx.tx(), sender),
121            Self::Eip2930(tx) => unique_tx_identifier_from_signable(tx.tx(), sender),
122            Self::Eip1559(tx) => unique_tx_identifier_from_signable(tx.tx(), sender),
123            Self::Eip7702(tx) => unique_tx_identifier_from_signable(tx.tx(), sender),
124            Self::AA(tx) => unique_tx_identifier_from_signable(tx.tx(), sender),
125        }
126    }
127
128    /// Return the [`TempoTxType`] of the inner txn.
129    pub const fn tx_type(&self) -> TempoTxType {
130        match self {
131            Self::Legacy(_) => TempoTxType::Legacy,
132            Self::Eip2930(_) => TempoTxType::Eip2930,
133            Self::Eip1559(_) => TempoTxType::Eip1559,
134            Self::Eip7702(_) => TempoTxType::Eip7702,
135            Self::AA(_) => TempoTxType::AA,
136        }
137    }
138
139    /// Returns true if this is a fee token transaction
140    pub fn is_fee_token(&self) -> bool {
141        matches!(self, Self::AA(_))
142    }
143
144    /// Returns the authorization list if present (for EIP-7702 transactions)
145    pub fn authorization_list(&self) -> Option<&[alloy_eips::eip7702::SignedAuthorization]> {
146        match self {
147            Self::Eip7702(tx) => Some(&tx.tx().authorization_list),
148            _ => None,
149        }
150    }
151
152    /// Returns the Tempo authorization list if present (for Tempo transactions)
153    pub fn tempo_authorization_list(
154        &self,
155    ) -> Option<&[crate::transaction::TempoSignedAuthorization]> {
156        match self {
157            Self::AA(tx) => Some(&tx.tx().tempo_authorization_list),
158            _ => None,
159        }
160    }
161
162    /// Returns true if this is a Tempo system transaction
163    pub fn is_system_tx(&self) -> bool {
164        matches!(self, Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE)
165    }
166
167    /// Returns true if this is a valid Tempo system transaction, i.e all gas fields and nonce are zero.
168    pub fn is_valid_system_tx(&self, chain_id: u64) -> bool {
169        self.max_fee_per_gas() == 0
170            && self.gas_limit() == 0
171            && self.value().is_zero()
172            && self.chain_id() == Some(chain_id)
173            && self.nonce() == 0
174    }
175
176    /// [TIP-20 payment] classification: `to` address has the `0x20c0` prefix.
177    ///
178    /// A transaction is considered a payment if its `to` address carries the TIP-20 prefix.
179    /// For AA transactions, every call must target a TIP-20 address.
180    ///
181    /// # NOTE
182    /// Consensus-level classifier, used during block validation, against `general_gas_limit`.
183    /// See [`is_payment_v2`](Self::is_payment_v2) for the stricter T5+ variant.
184    ///
185    /// [TIP-20 payment]: <https://docs.tempo.xyz/protocol/tip20/overview#get-predictable-payment-fees>
186    pub fn is_payment_v1(&self) -> bool {
187        match self {
188            Self::Legacy(tx) => is_tip20_call(tx.tx().to.to()),
189            Self::Eip2930(tx) => is_tip20_call(tx.tx().to.to()),
190            Self::Eip1559(tx) => is_tip20_call(tx.tx().to.to()),
191            Self::Eip7702(tx) => is_tip20_call(Some(&tx.tx().to)),
192            Self::AA(tx) => tx.tx().calls.iter().all(|call| is_tip20_call(call.to.to())),
193        }
194    }
195
196    /// Strict [TIP-20 payment] (TIP-1045): every call matches the payment call allow-list,
197    /// `access_list` and authorization lists are empty, and key authorization is bounded.
198    ///
199    /// Like [`is_payment_v1`](Self::is_payment_v1), but additionally requires:
200    /// - calldata to match a recognized payment selector with exact ABI-encoded length.
201    /// - `access_list` is empty.
202    /// - `authorization_list` (EIP-7702) is empty.
203    /// - For AA: `calls` is non-empty, `tempo_authorization_list` is empty, and any
204    ///   `key_authorization` has RLP-encoded length `<= KEY_AUTHORIZATION_MAX_RLP_LEN`.
205    ///
206    /// # NOTE
207    /// Used by the transaction pool and payload builder to prevent DoS of the payment lane,
208    /// and enshrined at the consensus level at the T5 hardfork.
209    ///
210    /// [TIP-20 payment]: <https://docs.tempo.xyz/protocol/tip20/overview#get-predictable-payment-fees>
211    pub fn is_payment_v2(&self) -> bool {
212        match self {
213            Self::Legacy(tx) => is_tip1045_call(tx.tx().to.to(), &tx.tx().input),
214            Self::Eip2930(tx) => {
215                let tx = tx.tx();
216                tx.access_list.is_empty() && is_tip1045_call(tx.to.to(), &tx.input)
217            }
218            Self::Eip1559(tx) => {
219                let tx = tx.tx();
220                tx.access_list.is_empty() && is_tip1045_call(tx.to.to(), &tx.input)
221            }
222            Self::Eip7702(tx) => {
223                let tx = tx.tx();
224                tx.access_list.is_empty()
225                    && tx.authorization_list.is_empty()
226                    && is_tip1045_call(Some(&tx.to), &tx.input)
227            }
228            Self::AA(tx) => {
229                let tx = tx.tx();
230                !tx.calls.is_empty()
231                    && tx.access_list.is_empty()
232                    && tx.tempo_authorization_list.is_empty()
233                    && tx
234                        .key_authorization
235                        .as_ref()
236                        .is_none_or(|auth| auth.length() <= KEY_AUTHORIZATION_MAX_RLP_LEN)
237                    && tx
238                        .calls
239                        .iter()
240                        .all(|call| is_tip1045_call(call.to.to(), &call.input))
241            }
242        }
243    }
244
245    /// Returns the proposer of the subblock if this is a subblock transaction.
246    pub fn subblock_proposer(&self) -> Option<PartialValidatorKey> {
247        let Self::AA(tx) = &self else { return None };
248        tx.tx().subblock_proposer()
249    }
250
251    /// Returns the [`AASigned`] transaction if this is a Tempo transaction.
252    pub fn as_aa(&self) -> Option<&AASigned> {
253        match self {
254            Self::AA(tx) => Some(tx),
255            _ => None,
256        }
257    }
258
259    /// Returns the nonce key of this transaction if it's an [`AASigned`] transaction.
260    pub fn nonce_key(&self) -> Option<U256> {
261        self.as_aa().map(|tx| tx.tx().nonce_key)
262    }
263
264    /// Returns true if this is a Tempo transaction
265    pub fn is_aa(&self) -> bool {
266        matches!(self, Self::AA(_))
267    }
268
269    /// Returns iterator over the calls in the transaction.
270    pub fn calls(&self) -> impl Iterator<Item = (TxKind, &Bytes)> {
271        if let Some(aa) = self.as_aa() {
272            Either::Left(aa.tx().calls.iter().map(|call| (call.to, &call.input)))
273        } else {
274            Either::Right(core::iter::once((self.kind(), self.input())))
275        }
276    }
277
278    /// Returns true if this is an expiring nonce transaction.
279    pub fn is_expiring_nonce(&self) -> bool {
280        self.as_aa()
281            .is_some_and(|tx| tx.tx().is_expiring_nonce_tx())
282    }
283}
284
285impl alloy_consensus::transaction::SignerRecoverable for TempoTxEnvelope {
286    fn recover_signer(
287        &self,
288    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
289        match self {
290            Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE => Ok(Address::ZERO),
291            Self::Legacy(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx),
292            Self::Eip2930(tx) => {
293                alloy_consensus::transaction::SignerRecoverable::recover_signer(tx)
294            }
295            Self::Eip1559(tx) => {
296                alloy_consensus::transaction::SignerRecoverable::recover_signer(tx)
297            }
298            Self::Eip7702(tx) => {
299                alloy_consensus::transaction::SignerRecoverable::recover_signer(tx)
300            }
301            Self::AA(tx) => alloy_consensus::transaction::SignerRecoverable::recover_signer(tx),
302        }
303    }
304
305    fn recover_signer_unchecked(
306        &self,
307    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
308        match self {
309            Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE => Ok(Address::ZERO),
310            Self::Legacy(tx) => {
311                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
312            }
313            Self::Eip2930(tx) => {
314                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
315            }
316            Self::Eip1559(tx) => {
317                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
318            }
319            Self::Eip7702(tx) => {
320                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
321            }
322            Self::AA(tx) => {
323                alloy_consensus::transaction::SignerRecoverable::recover_signer_unchecked(tx)
324            }
325        }
326    }
327}
328
329impl alloy_consensus::transaction::TxHashRef for TempoTxEnvelope {
330    fn tx_hash(&self) -> &B256 {
331        match self {
332            Self::Legacy(tx) => tx.hash(),
333            Self::Eip2930(tx) => tx.hash(),
334            Self::Eip1559(tx) => tx.hash(),
335            Self::Eip7702(tx) => tx.hash(),
336            Self::AA(tx) => tx.hash(),
337        }
338    }
339}
340
341impl fmt::Display for TempoTxType {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        match self {
344            Self::Legacy => write!(f, "Legacy"),
345            Self::Eip2930 => write!(f, "EIP-2930"),
346            Self::Eip1559 => write!(f, "EIP-1559"),
347            Self::Eip7702 => write!(f, "EIP-7702"),
348            Self::AA => write!(f, "AA"),
349        }
350    }
351}
352
353impl<Eip4844> TryFrom<EthereumTxEnvelope<Eip4844>> for TempoTxEnvelope {
354    type Error = ValueError<EthereumTxEnvelope<Eip4844>>;
355
356    fn try_from(value: EthereumTxEnvelope<Eip4844>) -> Result<Self, Self::Error> {
357        match value {
358            EthereumTxEnvelope::Legacy(tx) => Ok(Self::Legacy(tx)),
359            EthereumTxEnvelope::Eip2930(tx) => Ok(Self::Eip2930(tx)),
360            tx @ EthereumTxEnvelope::Eip4844(_) => Err(ValueError::new_static(
361                tx,
362                "EIP-4844 transactions are not supported",
363            )),
364            EthereumTxEnvelope::Eip1559(tx) => Ok(Self::Eip1559(tx)),
365            EthereumTxEnvelope::Eip7702(tx) => Ok(Self::Eip7702(tx)),
366        }
367    }
368}
369
370impl From<Signed<TxLegacy>> for TempoTxEnvelope {
371    fn from(value: Signed<TxLegacy>) -> Self {
372        Self::Legacy(value)
373    }
374}
375
376impl From<Signed<TxEip2930>> for TempoTxEnvelope {
377    fn from(value: Signed<TxEip2930>) -> Self {
378        Self::Eip2930(value)
379    }
380}
381
382impl From<Signed<TxEip1559>> for TempoTxEnvelope {
383    fn from(value: Signed<TxEip1559>) -> Self {
384        Self::Eip1559(value)
385    }
386}
387
388impl From<Signed<TxEip7702>> for TempoTxEnvelope {
389    fn from(value: Signed<TxEip7702>) -> Self {
390        Self::Eip7702(value)
391    }
392}
393
394impl From<AASigned> for TempoTxEnvelope {
395    fn from(value: AASigned) -> Self {
396        Self::AA(value)
397    }
398}
399
400impl From<Signed<TempoTypedTransaction>> for TempoTxEnvelope {
401    fn from(value: Signed<TempoTypedTransaction>) -> Self {
402        let sig = *value.signature();
403        let tx = value.strip_signature();
404        tx.into_envelope(sig)
405    }
406}
407
408impl SignableTransaction<Signature> for TempoTypedTransaction {
409    fn set_chain_id(&mut self, chain_id: alloy_primitives::ChainId) {
410        self.as_dyn_signable_mut().set_chain_id(chain_id);
411    }
412
413    fn encode_for_signing(&self, out: &mut dyn alloy_rlp::BufMut) {
414        match self {
415            Self::Legacy(tx) => tx.encode_for_signing(out),
416            Self::Eip2930(tx) => tx.encode_for_signing(out),
417            Self::Eip1559(tx) => tx.encode_for_signing(out),
418            Self::Eip7702(tx) => tx.encode_for_signing(out),
419            Self::AA(tx) => tx.encode_for_signing(out),
420        }
421    }
422
423    fn payload_len_for_signature(&self) -> usize {
424        match self {
425            Self::Legacy(tx) => tx.payload_len_for_signature(),
426            Self::Eip2930(tx) => tx.payload_len_for_signature(),
427            Self::Eip1559(tx) => tx.payload_len_for_signature(),
428            Self::Eip7702(tx) => tx.payload_len_for_signature(),
429            Self::AA(tx) => tx.payload_len_for_signature(),
430        }
431    }
432}
433
434impl TempoTypedTransaction {
435    /// Converts this typed transaction into a signed [`TempoTxEnvelope`]
436    pub fn into_envelope(self, sig: Signature) -> TempoTxEnvelope {
437        match self {
438            Self::Legacy(tx) => tx.into_signed(sig).into(),
439            Self::Eip2930(tx) => tx.into_signed(sig).into(),
440            Self::Eip1559(tx) => tx.into_signed(sig).into(),
441            Self::Eip7702(tx) => tx.into_signed(sig).into(),
442            Self::AA(tx) => tx.into_signed(sig.into()).into(),
443        }
444    }
445
446    /// Returns a dyn mutable reference to the underlying transaction
447    pub fn as_dyn_signable_mut(&mut self) -> &mut dyn SignableTransaction<Signature> {
448        match self {
449            Self::Legacy(tx) => tx,
450            Self::Eip2930(tx) => tx,
451            Self::Eip1559(tx) => tx,
452            Self::Eip7702(tx) => tx,
453            Self::AA(tx) => tx,
454        }
455    }
456}
457
458impl TryFrom<TypedTransaction> for TempoTypedTransaction {
459    type Error = UnsupportedTransactionType<TxType>;
460
461    fn try_from(value: TypedTransaction) -> Result<Self, Self::Error> {
462        Ok(match value {
463            TypedTransaction::Legacy(tx) => Self::Legacy(tx),
464            TypedTransaction::Eip2930(tx) => Self::Eip2930(tx),
465            TypedTransaction::Eip1559(tx) => Self::Eip1559(tx),
466            TypedTransaction::Eip4844(..) => {
467                return Err(UnsupportedTransactionType::new(TxType::Eip4844));
468            }
469            TypedTransaction::Eip7702(tx) => Self::Eip7702(tx),
470        })
471    }
472}
473
474impl From<TempoTxEnvelope> for TempoTypedTransaction {
475    fn from(value: TempoTxEnvelope) -> Self {
476        match value {
477            TempoTxEnvelope::Legacy(tx) => Self::Legacy(tx.into_parts().0),
478            TempoTxEnvelope::Eip2930(tx) => Self::Eip2930(tx.into_parts().0),
479            TempoTxEnvelope::Eip1559(tx) => Self::Eip1559(tx.into_parts().0),
480            TempoTxEnvelope::Eip7702(tx) => Self::Eip7702(tx.into_parts().0),
481            TempoTxEnvelope::AA(tx) => Self::AA(tx.into_parts().0),
482        }
483    }
484}
485
486impl From<TempoTransaction> for TempoTypedTransaction {
487    fn from(value: TempoTransaction) -> Self {
488        Self::AA(value)
489    }
490}
491
492/// Returns `true` if `to` has the TIP-20 payment prefix.
493#[inline]
494fn is_tip20_call(to: Option<&Address>) -> bool {
495    to.is_some_and(|to| to.is_tip20())
496}
497
498/// Returns `true` if the call is in the TIP-1045 payment lane allow-list.
499#[inline]
500fn is_tip1045_call(to: Option<&Address>, input: &[u8]) -> bool {
501    match to {
502        // TIP20 call + payment calldata constraints
503        Some(to) if to.is_tip20() => ITIP20::ITIP20Calls::is_payment(input),
504        // TIP20ChannelReserve call + payment calldata constraints
505        Some(to) if *to == TIP20_CHANNEL_RESERVE_ADDRESS => {
506            ITIP20ChannelReserve::ITIP20ChannelReserveCalls::is_payment_with_valid_signature(
507                input,
508                |signature| super::tt_signature::PrimitiveSignature::from_bytes(signature).is_ok(),
509            )
510        }
511        _ => false,
512    }
513}
514
515#[cfg(feature = "rpc")]
516impl reth_rpc_convert::SignableTxRequest<TempoTxEnvelope>
517    for alloy_rpc_types_eth::TransactionRequest
518{
519    async fn try_build_and_sign(
520        self,
521        signer: impl alloy_network::TxSigner<alloy_primitives::Signature> + Send,
522    ) -> Result<TempoTxEnvelope, reth_rpc_convert::SignTxRequestError> {
523        reth_rpc_convert::SignableTxRequest::<
524            EthereumTxEnvelope<alloy_consensus::TxEip4844>,
525        >::try_build_and_sign(self, signer)
526        .await
527        .and_then(|tx| {
528            tx.try_into()
529                .map_err(|_| reth_rpc_convert::SignTxRequestError::InvalidTransactionRequest)
530        })
531    }
532}
533
534#[cfg(feature = "rpc")]
535impl reth_rpc_convert::TryIntoSimTx<TempoTxEnvelope> for alloy_rpc_types_eth::TransactionRequest {
536    fn try_into_sim_tx(self) -> Result<TempoTxEnvelope, ValueError<Self>> {
537        let tx = self.clone().build_typed_simulate_transaction()?;
538        tx.try_into()
539            .map_err(|_| ValueError::new_static(self, "Invalid transaction request"))
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use crate::transaction::{
547        Call, TempoSignedAuthorization, TempoTransaction, TokenLimit,
548        key_authorization::KeyAuthorization,
549        tt_signature::{KeychainSignature, PrimitiveSignature, TempoSignature},
550    };
551    use alloy_consensus::{TxEip1559, TxEip2930, TxEip7702};
552    use alloy_eips::{
553        eip2930::{AccessList, AccessListItem},
554        eip7702::SignedAuthorization,
555    };
556    use alloy_primitives::{Bytes, Signature, TxKind, U256, address, aliases::U96};
557    use alloy_sol_types::SolCall;
558    use tempo_contracts::precompiles::ITIP20ChannelReserve;
559
560    const PAYMENT_TKN: Address = address!("20c0000000000000000000000000000000000001");
561
562    #[rustfmt::skip]
563    /// Returns valid ABI-encoded calldata for every recognized TIP-20 payment selector.
564    fn payment_calldatas() -> [Bytes; 9] {
565        let (to, from, amount, memo) = (Address::random(), Address::random(), U256::random(), B256::random());
566        [
567            ITIP20::transferCall { to, amount }.abi_encode().into(),
568            ITIP20::transferWithMemoCall { to, amount, memo }.abi_encode().into(),
569            ITIP20::transferFromCall { from, to, amount }.abi_encode().into(),
570            ITIP20::transferFromWithMemoCall { from, to, amount, memo }.abi_encode().into(),
571            ITIP20::approveCall { spender: to, amount }.abi_encode().into(),
572            ITIP20::mintCall { to, amount }.abi_encode().into(),
573            ITIP20::mintWithMemoCall { to, amount, memo }.abi_encode().into(),
574            ITIP20::burnCall { amount }.abi_encode().into(),
575            ITIP20::burnWithMemoCall { amount, memo }.abi_encode().into(),
576        ]
577    }
578
579    fn channel_descriptor() -> ITIP20ChannelReserve::ChannelDescriptor {
580        ITIP20ChannelReserve::ChannelDescriptor {
581            payer: Address::random(),
582            payee: Address::random(),
583            operator: Address::random(),
584            token: PAYMENT_TKN,
585            salt: B256::random(),
586            authorizedSigner: Address::random(),
587            expiringNonceHash: B256::random(),
588        }
589    }
590
591    #[rustfmt::skip]
592    fn channel_reserve_payment_calldatas() -> [Bytes; 6] {
593        let descriptor = channel_descriptor();
594        let signature = TempoSignature::from(Signature::test_signature()).to_bytes();
595        [
596            ITIP20ChannelReserve::openCall { payee: Address::random(), operator: Address::random(), token: PAYMENT_TKN, deposit: U96::from(1), salt: B256::random(), authorizedSigner: Address::random() }.abi_encode().into(),
597            ITIP20ChannelReserve::topUpCall { descriptor: descriptor.clone(), additionalDeposit: U96::from(1) }.abi_encode().into(),
598            ITIP20ChannelReserve::settleCall { descriptor: descriptor.clone(), cumulativeAmount: U96::from(1), signature: signature.clone() }.abi_encode().into(),
599            ITIP20ChannelReserve::closeCall { descriptor: descriptor.clone(), cumulativeAmount: U96::from(1), captureAmount: U96::from(1), signature }.abi_encode().into(),
600            ITIP20ChannelReserve::requestCloseCall { descriptor: descriptor.clone() }.abi_encode().into(),
601            ITIP20ChannelReserve::withdrawCall { descriptor }.abi_encode().into(),
602        ]
603    }
604
605    /// Returns one envelope per tx type, all targeting `PAYMENT_TKN` with the given calldata.
606    fn payment_envelopes(calldata: Bytes) -> [TempoTxEnvelope; 5] {
607        payment_envelopes_to(PAYMENT_TKN, calldata)
608    }
609
610    /// Returns one envelope per tx type, all targeting `to` with the given calldata.
611    fn payment_envelopes_to(to: Address, calldata: Bytes) -> [TempoTxEnvelope; 5] {
612        let legacy = TempoTxEnvelope::Legacy(Signed::new_unhashed(
613            TxLegacy {
614                to: TxKind::Call(to),
615                input: calldata.clone(),
616                ..Default::default()
617            },
618            Signature::test_signature(),
619        ));
620        let [eip2930, eip1559, eip7702, aa] =
621            payment_envelopes_with_access_list_to(to, calldata, AccessList::default());
622        [legacy, eip2930, eip1559, eip7702, aa]
623    }
624
625    /// Like [`payment_envelopes`], but with `access_list` set. Supported by: Eip2930, Eip1559, Eip7702, AA.
626    fn payment_envelopes_with_access_list(
627        calldata: Bytes,
628        access_list: AccessList,
629    ) -> [TempoTxEnvelope; 4] {
630        payment_envelopes_with_access_list_to(PAYMENT_TKN, calldata, access_list)
631    }
632
633    #[rustfmt::skip]
634    fn payment_envelopes_with_access_list_to(to: Address, calldata: Bytes, access_list: AccessList) -> [TempoTxEnvelope; 4] {
635        [
636            TempoTxEnvelope::Eip2930(Signed::new_unhashed(
637                TxEip2930 { to: TxKind::Call(to), input: calldata.clone(), access_list: access_list.clone(), ..Default::default() },
638                Signature::test_signature(),
639            )),
640            TempoTxEnvelope::Eip1559(Signed::new_unhashed(
641                TxEip1559 { to: TxKind::Call(to), input: calldata.clone(), access_list: access_list.clone(), ..Default::default() },
642                Signature::test_signature(),
643            )),
644            TempoTxEnvelope::Eip7702(Signed::new_unhashed(
645                TxEip7702 { to, input: calldata.clone(), access_list: access_list.clone(), ..Default::default() },
646                Signature::test_signature(),
647            )),
648            TempoTxEnvelope::AA(TempoTransaction {
649                fee_token: Some(PAYMENT_TKN),
650                calls: vec![Call { to: TxKind::Call(to), value: U256::ZERO, input: calldata }],
651                access_list,
652                ..Default::default()
653            }.into_signed(Signature::test_signature().into())),
654        ]
655    }
656
657    #[test]
658    fn test_non_fee_token_access() {
659        let legacy_tx = TxLegacy::default();
660        let signature = Signature::new(
661            alloy_primitives::U256::ZERO,
662            alloy_primitives::U256::ZERO,
663            false,
664        );
665        let signed = Signed::new_unhashed(legacy_tx, signature);
666        let envelope = TempoTxEnvelope::Legacy(signed);
667
668        assert!(!envelope.is_fee_token());
669        assert_eq!(envelope.fee_token(), None);
670        assert!(!envelope.is_aa());
671        assert!(envelope.as_aa().is_none());
672    }
673
674    #[test]
675    fn test_payment_classification_legacy_tx() {
676        // Test with legacy transaction type
677        let tx = TxLegacy {
678            to: TxKind::Call(PAYMENT_TKN),
679            gas_limit: 21000,
680            ..Default::default()
681        };
682        let signed = Signed::new_unhashed(tx, Signature::test_signature());
683        let envelope = TempoTxEnvelope::Legacy(signed);
684
685        assert!(envelope.is_payment_v1());
686    }
687
688    #[test]
689    fn test_payment_classification_non_payment() {
690        let non_payment_addr = address!("1234567890123456789012345678901234567890");
691        let tx = TxLegacy {
692            to: TxKind::Call(non_payment_addr),
693            gas_limit: 21000,
694            ..Default::default()
695        };
696        let signed = Signed::new_unhashed(tx, Signature::test_signature());
697        let envelope = TempoTxEnvelope::Legacy(signed);
698
699        assert!(!envelope.is_payment_v1());
700    }
701
702    fn create_aa_envelope(call: Call) -> TempoTxEnvelope {
703        let tx = TempoTransaction {
704            fee_token: Some(PAYMENT_TKN),
705            calls: vec![call],
706            ..Default::default()
707        };
708        TempoTxEnvelope::AA(tx.into_signed(Signature::test_signature().into()))
709    }
710
711    #[test]
712    fn test_payment_classification_aa_with_tip20_prefix() {
713        let payment_addr = address!("20c0000000000000000000000000000000000001");
714        let call = Call {
715            to: TxKind::Call(payment_addr),
716            value: U256::ZERO,
717            input: Bytes::new(),
718        };
719        let envelope = create_aa_envelope(call);
720        assert!(envelope.is_payment_v1());
721    }
722
723    #[test]
724    fn test_payment_classification_aa_without_tip20_prefix() {
725        let non_payment_addr = address!("1234567890123456789012345678901234567890");
726        let call = Call {
727            to: TxKind::Call(non_payment_addr),
728            value: U256::ZERO,
729            input: Bytes::new(),
730        };
731        let envelope = create_aa_envelope(call);
732        assert!(!envelope.is_payment_v1());
733    }
734
735    #[test]
736    fn test_payment_classification_aa_no_to_address() {
737        let call = Call {
738            to: TxKind::Create,
739            value: U256::ZERO,
740            input: Bytes::new(),
741        };
742        let envelope = create_aa_envelope(call);
743        assert!(!envelope.is_payment_v1());
744    }
745
746    #[test]
747    fn test_payment_classification_aa_partial_match() {
748        // First 12 bytes match TIP20_PAYMENT_PREFIX, remaining 8 bytes differ
749        let payment_addr = address!("20c0000000000000000000001111111111111111");
750        let call = Call {
751            to: TxKind::Call(payment_addr),
752            value: U256::ZERO,
753            input: Bytes::new(),
754        };
755        let envelope = create_aa_envelope(call);
756        assert!(envelope.is_payment_v1());
757    }
758
759    #[test]
760    fn test_payment_classification_aa_different_prefix() {
761        // Different prefix (30c0 instead of 20c0)
762        let non_payment_addr = address!("30c0000000000000000000000000000000000001");
763        let call = Call {
764            to: TxKind::Call(non_payment_addr),
765            value: U256::ZERO,
766            input: Bytes::new(),
767        };
768        let envelope = create_aa_envelope(call);
769        assert!(!envelope.is_payment_v1());
770    }
771
772    #[test]
773    fn test_is_payment_eip2930_eip1559_eip7702() {
774        // Eip2930 payment
775        let tx = TxEip2930 {
776            to: TxKind::Call(PAYMENT_TKN),
777            ..Default::default()
778        };
779        let envelope =
780            TempoTxEnvelope::Eip2930(Signed::new_unhashed(tx, Signature::test_signature()));
781        assert!(envelope.is_payment_v1());
782
783        // Eip2930 non-payment
784        let tx = TxEip2930 {
785            to: TxKind::Call(address!("1234567890123456789012345678901234567890")),
786            ..Default::default()
787        };
788        let envelope =
789            TempoTxEnvelope::Eip2930(Signed::new_unhashed(tx, Signature::test_signature()));
790        assert!(!envelope.is_payment_v1());
791
792        // Eip1559 payment
793        let tx = TxEip1559 {
794            to: TxKind::Call(PAYMENT_TKN),
795            ..Default::default()
796        };
797        let envelope =
798            TempoTxEnvelope::Eip1559(Signed::new_unhashed(tx, Signature::test_signature()));
799        assert!(envelope.is_payment_v1());
800
801        // Eip1559 non-payment
802        let tx = TxEip1559 {
803            to: TxKind::Call(address!("1234567890123456789012345678901234567890")),
804            ..Default::default()
805        };
806        let envelope =
807            TempoTxEnvelope::Eip1559(Signed::new_unhashed(tx, Signature::test_signature()));
808        assert!(!envelope.is_payment_v1());
809
810        // Eip7702 payment (note: Eip7702 has direct `to` address, not TxKind)
811        let tx = TxEip7702 {
812            to: PAYMENT_TKN,
813            ..Default::default()
814        };
815        let envelope =
816            TempoTxEnvelope::Eip7702(Signed::new_unhashed(tx, Signature::test_signature()));
817        assert!(envelope.is_payment_v1());
818
819        // Eip7702 non-payment
820        let tx = TxEip7702 {
821            to: address!("1234567890123456789012345678901234567890"),
822            ..Default::default()
823        };
824        let envelope =
825            TempoTxEnvelope::Eip7702(Signed::new_unhashed(tx, Signature::test_signature()));
826        assert!(!envelope.is_payment_v1());
827    }
828
829    #[test]
830    fn test_payment_v2_accepts_valid_calldata() {
831        for calldata in payment_calldatas() {
832            for envelope in payment_envelopes(calldata) {
833                assert!(envelope.is_payment_v1(), "V1 must accept valid calldata");
834                assert!(envelope.is_payment_v2(), "V2 must accept valid calldata");
835            }
836        }
837    }
838
839    #[test]
840    fn test_payment_v2_accepts_valid_channel_reserve_calldata() {
841        for calldata in channel_reserve_payment_calldatas() {
842            for envelope in payment_envelopes_to(TIP20_CHANNEL_RESERVE_ADDRESS, calldata) {
843                assert!(!envelope.is_payment_v1(), "V1 only accepts TIP-20 prefix");
844                assert!(
845                    envelope.is_payment_v2(),
846                    "V2 must accept valid TIP20ChannelReserve calldata"
847                );
848            }
849        }
850    }
851
852    #[test]
853    fn test_payment_v2_rejects_channel_reserve_calldata_to_tip20() {
854        for calldata in channel_reserve_payment_calldatas() {
855            for envelope in payment_envelopes_to(PAYMENT_TKN, calldata) {
856                assert!(envelope.is_payment_v1(), "V1 accepts TIP-20 prefix");
857                assert!(!envelope.is_payment_v2(), "V2 only accepts allowed combos");
858            }
859        }
860    }
861
862    #[test]
863    fn test_payment_v2_rejects_invalid_channel_reserve_signature_encoding() {
864        let descriptor = channel_descriptor();
865        let invalid_signature = Bytes::from(vec![1, 2, 3]);
866        let calldatas = [
867            ITIP20ChannelReserve::settleCall {
868                descriptor: descriptor.clone(),
869                cumulativeAmount: U96::ONE,
870                signature: invalid_signature.clone(),
871            }
872            .abi_encode(),
873            ITIP20ChannelReserve::closeCall {
874                descriptor,
875                cumulativeAmount: U96::ONE,
876                captureAmount: U96::ONE,
877                signature: invalid_signature,
878            }
879            .abi_encode(),
880        ];
881
882        for calldata in calldatas {
883            for envelope in payment_envelopes_to(TIP20_CHANNEL_RESERVE_ADDRESS, calldata.into()) {
884                assert!(
885                    !envelope.is_payment_v2(),
886                    "V2 must reject invalid Tempo signature encoding"
887                );
888            }
889        }
890    }
891
892    #[test]
893    fn test_payment_v2_rejects_keychain_wrapped_channel_reserve_signature() {
894        let descriptor = channel_descriptor();
895        let keychain_signature = TempoSignature::Keychain(KeychainSignature::new_v1(
896            Address::random(),
897            PrimitiveSignature::Secp256k1(Signature::test_signature()),
898        ))
899        .to_bytes();
900        assert!(TempoSignature::from_bytes(&keychain_signature).is_ok());
901        assert!(PrimitiveSignature::from_bytes(&keychain_signature).is_err());
902
903        let calldatas = [
904            ITIP20ChannelReserve::settleCall {
905                descriptor: descriptor.clone(),
906                cumulativeAmount: U96::ONE,
907                signature: keychain_signature.clone(),
908            }
909            .abi_encode(),
910            ITIP20ChannelReserve::closeCall {
911                descriptor,
912                cumulativeAmount: U96::ONE,
913                captureAmount: U96::ONE,
914                signature: keychain_signature,
915            }
916            .abi_encode(),
917        ];
918
919        for calldata in calldatas {
920            for envelope in payment_envelopes_to(TIP20_CHANNEL_RESERVE_ADDRESS, calldata.into()) {
921                assert!(
922                    !envelope.is_payment_v2(),
923                    "V2 must reject Keychain-wrapped channel reserve voucher signatures"
924                );
925            }
926        }
927    }
928
929    #[test]
930    fn test_payment_v2_rejects_invalid_channel_reserve_dynamic_calldata() {
931        let mut corrupted_calldata = ITIP20ChannelReserve::settleCall {
932            descriptor: channel_descriptor(),
933            cumulativeAmount: U96::ONE,
934            signature: TempoSignature::from(Signature::test_signature()).to_bytes(),
935        }
936        .abi_encode();
937        // Corrupt the dynamic `signature` offset word.
938        corrupted_calldata[4 + 8 * 32 + 31] = 0;
939
940        for envelope in
941            payment_envelopes_to(TIP20_CHANNEL_RESERVE_ADDRESS, corrupted_calldata.into())
942        {
943            assert!(!envelope.is_payment_v2(), "V2 must reject malformed ABI");
944        }
945
946        // Calldata > 2KB
947        let long_calldata = ITIP20ChannelReserve::settleCall {
948            descriptor: channel_descriptor(),
949            cumulativeAmount: U96::ONE,
950            signature: vec![0; 2048].into(),
951        }
952        .abi_encode();
953        assert!(long_calldata.len() > 2048);
954
955        for envelope in payment_envelopes_to(TIP20_CHANNEL_RESERVE_ADDRESS, long_calldata.into()) {
956            assert!(!envelope.is_payment_v2(), "V2 must reject large calldata");
957        }
958    }
959
960    #[test]
961    fn test_payment_v2_rejects_empty_calldata() {
962        for envelope in payment_envelopes(Bytes::new()) {
963            assert!(envelope.is_payment_v1(), "V1 must accept (prefix-only)");
964            assert!(!envelope.is_payment_v2(), "V2 must reject empty calldata");
965        }
966    }
967
968    #[test]
969    fn test_payment_v2_rejects_excess_calldata() {
970        for calldata in payment_calldatas() {
971            let mut data = calldata.to_vec();
972            data.extend_from_slice(&[0u8; 32]);
973            for envelope in payment_envelopes(Bytes::from(data)) {
974                assert!(envelope.is_payment_v1(), "V1 must accept (prefix-only)");
975                assert!(!envelope.is_payment_v2(), "V2 must reject excess calldata");
976            }
977        }
978    }
979
980    #[test]
981    fn test_payment_v2_rejects_unknown_selector() {
982        for calldata in payment_calldatas() {
983            let mut data = calldata.to_vec();
984            data[..4].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
985            for envelope in payment_envelopes(Bytes::from(data)) {
986                assert!(envelope.is_payment_v1(), "V1 must accept (prefix-only)");
987                assert!(!envelope.is_payment_v2(), "V2 must reject unknown selector");
988            }
989        }
990    }
991
992    #[test]
993    fn test_payment_v2_aa_empty_calls() {
994        let tx = TempoTransaction {
995            fee_token: Some(PAYMENT_TKN),
996            calls: vec![],
997            ..Default::default()
998        };
999        let envelope = TempoTxEnvelope::AA(tx.into_signed(Signature::test_signature().into()));
1000        assert!(
1001            !envelope.is_payment_v2(),
1002            "AA with empty calls should not be V2 payment"
1003        );
1004    }
1005
1006    #[test]
1007    fn test_payment_v2_eip7702_rejects_authorization_list() {
1008        let calldata = ITIP20::transferCall {
1009            to: Address::random(),
1010            amount: U256::from(1),
1011        }
1012        .abi_encode();
1013        let tx = TxEip7702 {
1014            to: PAYMENT_TKN,
1015            input: Bytes::from(calldata),
1016            authorization_list: vec![SignedAuthorization::new_unchecked(
1017                alloy_eips::eip7702::Authorization {
1018                    chain_id: U256::from(1),
1019                    address: Address::random(),
1020                    nonce: 0,
1021                },
1022                0,
1023                U256::ZERO,
1024                U256::ZERO,
1025            )],
1026            ..Default::default()
1027        };
1028        let envelope =
1029            TempoTxEnvelope::Eip7702(Signed::new_unhashed(tx, Signature::test_signature()));
1030        assert!(
1031            envelope.is_payment_v1(),
1032            "V1 ignores authorization_list (backwards compat)"
1033        );
1034        assert!(
1035            !envelope.is_payment_v2(),
1036            "V2 must reject EIP-7702 tx with non-empty authorization_list"
1037        );
1038    }
1039
1040    fn aa_with_key_authorization(limits: Option<Vec<TokenLimit>>) -> TempoTxEnvelope {
1041        let calldata = ITIP20::transferCall {
1042            to: Address::random(),
1043            amount: U256::from(1),
1044        }
1045        .abi_encode();
1046        let tx = TempoTransaction {
1047            fee_token: Some(PAYMENT_TKN),
1048            calls: vec![Call {
1049                to: TxKind::Call(PAYMENT_TKN),
1050                value: U256::ZERO,
1051                input: Bytes::from(calldata),
1052            }],
1053            key_authorization: Some(
1054                KeyAuthorization {
1055                    chain_id: 1,
1056                    key_type: crate::SignatureType::Secp256k1,
1057                    key_id: Address::random(),
1058                    expiry: None,
1059                    limits,
1060                    allowed_calls: None,
1061                    witness: None,
1062                    is_admin: false,
1063                    account: None,
1064                }
1065                .into_signed(PrimitiveSignature::Secp256k1(Signature::test_signature())),
1066            ),
1067            ..Default::default()
1068        };
1069        TempoTxEnvelope::AA(tx.into_signed(Signature::test_signature().into()))
1070    }
1071
1072    #[test]
1073    fn test_payment_v2_aa_accepts_bounded_key_authorization() {
1074        // TIP-1045: key auth is allowed in payment txs as long as it's bounded.
1075        let envelope = aa_with_key_authorization(None);
1076        assert!(envelope.is_payment_v1());
1077        assert!(envelope.is_payment_v2(), "V2 must accept bounded key auth");
1078
1079        // Pad `limits` with enough entries to push the RLP encoding past the 1 KB cap.
1080        let limits = (0..32)
1081            .map(|i| TokenLimit {
1082                token: Address::repeat_byte(i as u8),
1083                limit: U256::from(u128::MAX),
1084                period: 1,
1085            })
1086            .collect::<Vec<_>>();
1087        let envelope = aa_with_key_authorization(Some(limits));
1088        assert!(envelope.is_payment_v1(), "V1 ignores key auth size");
1089        assert!(!envelope.is_payment_v2(), "V2 must reject huge key auth");
1090
1091        let tx = envelope.as_aa().unwrap().tx();
1092        let key_auth = tx.key_authorization.as_ref().unwrap();
1093        assert!(key_auth.length() > KEY_AUTHORIZATION_MAX_RLP_LEN);
1094    }
1095
1096    #[test]
1097    fn test_payment_v2_aa_rejects_tempo_authorization_list() {
1098        let calldata = ITIP20::transferCall {
1099            to: Address::random(),
1100            amount: U256::from(1),
1101        }
1102        .abi_encode();
1103        let tx = TempoTransaction {
1104            fee_token: Some(PAYMENT_TKN),
1105            calls: vec![Call {
1106                to: TxKind::Call(PAYMENT_TKN),
1107                value: U256::ZERO,
1108                input: Bytes::from(calldata),
1109            }],
1110            tempo_authorization_list: vec![TempoSignedAuthorization::new_unchecked(
1111                alloy_eips::eip7702::Authorization {
1112                    chain_id: U256::from(1),
1113                    address: Address::random(),
1114                    nonce: 0,
1115                },
1116                Signature::test_signature().into(),
1117            )],
1118            ..Default::default()
1119        };
1120        let envelope = TempoTxEnvelope::AA(tx.into_signed(Signature::test_signature().into()));
1121        assert!(
1122            envelope.is_payment_v1(),
1123            "V1 ignores side-effect fields (backwards compat)"
1124        );
1125        assert!(
1126            !envelope.is_payment_v2(),
1127            "V2 must reject AA tx with tempo_authorization_list"
1128        );
1129    }
1130
1131    #[test]
1132    fn test_payment_v2_rejects_access_list() {
1133        let calldata: Bytes = ITIP20::transferCall {
1134            to: Address::random(),
1135            amount: U256::from(1),
1136        }
1137        .abi_encode()
1138        .into();
1139        let access_list = AccessList(vec![AccessListItem {
1140            address: Address::random(),
1141            storage_keys: vec![],
1142        }]);
1143
1144        for envelope in payment_envelopes_with_access_list(calldata, access_list) {
1145            assert!(envelope.is_payment_v1(), "V1 must ignore access_list");
1146            assert!(!envelope.is_payment_v2(), "V2 must reject access_list");
1147        }
1148    }
1149
1150    #[test]
1151    fn test_system_tx_validation_and_recovery() {
1152        use alloy_consensus::transaction::SignerRecoverable;
1153
1154        let chain_id = 1u64;
1155
1156        // Valid system tx: all fields zero, correct chain_id, system signature
1157        let tx = TxLegacy {
1158            chain_id: Some(chain_id),
1159            nonce: 0,
1160            gas_price: 0,
1161            gas_limit: 0,
1162            to: TxKind::Call(Address::ZERO),
1163            value: U256::ZERO,
1164            input: Bytes::new(),
1165        };
1166        let system_tx =
1167            TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1168
1169        assert!(system_tx.is_system_tx(), "Should detect system signature");
1170        assert!(
1171            system_tx.is_valid_system_tx(chain_id),
1172            "Should be valid system tx"
1173        );
1174
1175        // recover_signer returns ZERO for system tx
1176        let signer = system_tx.recover_signer().unwrap();
1177        assert_eq!(
1178            signer,
1179            Address::ZERO,
1180            "System tx signer should be Address::ZERO"
1181        );
1182
1183        // Invalid: wrong chain_id
1184        assert!(
1185            !system_tx.is_valid_system_tx(2),
1186            "Wrong chain_id should fail"
1187        );
1188
1189        // Invalid: non-zero gas_limit
1190        let tx = TxLegacy {
1191            chain_id: Some(chain_id),
1192            gas_limit: 1, // non-zero
1193            ..Default::default()
1194        };
1195        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1196        assert!(
1197            !envelope.is_valid_system_tx(chain_id),
1198            "Non-zero gas_limit should fail"
1199        );
1200
1201        // Invalid: non-zero value
1202        let tx = TxLegacy {
1203            chain_id: Some(chain_id),
1204            value: U256::from(1),
1205            ..Default::default()
1206        };
1207        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1208        assert!(
1209            !envelope.is_valid_system_tx(chain_id),
1210            "Non-zero value should fail"
1211        );
1212
1213        // Invalid: non-zero nonce
1214        let tx = TxLegacy {
1215            chain_id: Some(chain_id),
1216            nonce: 1,
1217            ..Default::default()
1218        };
1219        let envelope = TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, TEMPO_SYSTEM_TX_SIGNATURE));
1220        assert!(
1221            !envelope.is_valid_system_tx(chain_id),
1222            "Non-zero nonce should fail"
1223        );
1224
1225        // Non-system tx with regular signature should recover normally
1226        let tx = TxLegacy::default();
1227        let regular_tx =
1228            TempoTxEnvelope::Legacy(Signed::new_unhashed(tx, Signature::test_signature()));
1229        assert!(
1230            !regular_tx.is_system_tx(),
1231            "Regular tx should not be system tx"
1232        );
1233
1234        // fee_payer() for non-AA returns sender
1235        let sender = Address::random();
1236        assert_eq!(system_tx.fee_payer(sender).unwrap(), sender);
1237
1238        // calls() iterator for non-AA returns single item
1239        let calls: Vec<_> = system_tx.calls().collect();
1240        assert_eq!(calls.len(), 1);
1241        assert_eq!(calls[0].0, TxKind::Call(Address::ZERO));
1242
1243        // subblock_proposer() returns None for non-subblock tx
1244        assert!(system_tx.subblock_proposer().is_none());
1245
1246        // AA-specific methods
1247        let aa_envelope = create_aa_envelope(Call {
1248            to: TxKind::Call(PAYMENT_TKN),
1249            value: U256::ZERO,
1250            input: Bytes::new(),
1251        });
1252        assert!(aa_envelope.is_aa());
1253        assert!(aa_envelope.as_aa().is_some());
1254        assert_eq!(aa_envelope.fee_token(), Some(PAYMENT_TKN));
1255
1256        // calls() for AA tx
1257        let aa_calls: Vec<_> = aa_envelope.calls().collect();
1258        assert_eq!(aa_calls.len(), 1);
1259    }
1260
1261    #[test]
1262    fn test_try_from_ethereum_envelope_eip4844_rejected() {
1263        use alloy_consensus::TxEip4844;
1264
1265        // EIP-4844 should be rejected
1266        let eip4844_tx = TxEip4844::default();
1267        let eth_envelope: EthereumTxEnvelope<TxEip4844> = EthereumTxEnvelope::Eip4844(
1268            Signed::new_unhashed(eip4844_tx, Signature::test_signature()),
1269        );
1270
1271        let result = TempoTxEnvelope::try_from(eth_envelope);
1272        assert!(result.is_err(), "EIP-4844 should be rejected");
1273
1274        // Other types should be accepted
1275        let legacy_tx = TxLegacy::default();
1276        let eth_envelope: EthereumTxEnvelope<TxEip4844> = EthereumTxEnvelope::Legacy(
1277            Signed::new_unhashed(legacy_tx, Signature::test_signature()),
1278        );
1279        assert!(TempoTxEnvelope::try_from(eth_envelope).is_ok());
1280    }
1281
1282    #[test]
1283    fn test_tx_type_conversions() {
1284        // TxType -> TempoTxType: EIP-4844 rejected
1285        assert!(TempoTxType::try_from(TxType::Legacy).is_ok());
1286        assert!(TempoTxType::try_from(TxType::Eip2930).is_ok());
1287        assert!(TempoTxType::try_from(TxType::Eip1559).is_ok());
1288        assert!(TempoTxType::try_from(TxType::Eip7702).is_ok());
1289        assert!(TempoTxType::try_from(TxType::Eip4844).is_err());
1290
1291        // TempoTxType -> TxType: AA rejected
1292        assert!(TxType::try_from(TempoTxType::Legacy).is_ok());
1293        assert!(TxType::try_from(TempoTxType::Eip2930).is_ok());
1294        assert!(TxType::try_from(TempoTxType::Eip1559).is_ok());
1295        assert!(TxType::try_from(TempoTxType::Eip7702).is_ok());
1296        assert!(TxType::try_from(TempoTxType::AA).is_err());
1297    }
1298
1299    #[test]
1300    fn test_payment_v2_rejects_aa_with_empty_calls() {
1301        let tx = TempoTransaction {
1302            fee_token: Some(PAYMENT_TKN),
1303            calls: vec![],
1304            ..Default::default()
1305        };
1306        let envelope = TempoTxEnvelope::AA(tx.into_signed(Signature::test_signature().into()));
1307        assert!(envelope.is_payment_v1(), "V1 must accept AA without calls");
1308        assert!(!envelope.is_payment_v2(), "V2 must reject AA without calls");
1309    }
1310}