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