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 alloy_consensus::InMemorySize for TempoTxType {
94 fn size(&self) -> usize {
95 size_of::<Self>()
96 }
97}
98
99impl TempoTxEnvelope {
100 pub fn fee_token(&self) -> Option<Address> {
102 match self {
103 Self::AA(tx) => tx.tx().fee_token,
104 _ => None,
105 }
106 }
107
108 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 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 pub fn is_fee_token(&self) -> bool {
129 matches!(self, Self::AA(_))
130 }
131
132 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 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 pub fn is_system_tx(&self) -> bool {
152 matches!(self, Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE)
153 }
154
155 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 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 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 pub fn subblock_proposer(&self) -> Option<PartialValidatorKey> {
230 let Self::AA(tx) = &self else { return None };
231 tx.tx().subblock_proposer()
232 }
233
234 pub fn as_aa(&self) -> Option<&AASigned> {
236 match self {
237 Self::AA(tx) => Some(tx),
238 _ => None,
239 }
240 }
241
242 pub fn nonce_key(&self) -> Option<U256> {
244 self.as_aa().map(|tx| tx.tx().nonce_key)
245 }
246
247 pub fn is_aa(&self) -> bool {
249 matches!(self, Self::AA(_))
250 }
251
252 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 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 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 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#[inline]
477fn is_tip20_call(to: Option<&Address>) -> bool {
478 to.is_some_and(|to| to.starts_with(&TIP20_PAYMENT_PREFIX))
479}
480
481#[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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 assert!(
980 !system_tx.is_valid_system_tx(2),
981 "Wrong chain_id should fail"
982 );
983
984 let tx = TxLegacy {
986 chain_id: Some(chain_id),
987 gas_limit: 1, ..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 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 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 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 let sender = Address::random();
1031 assert_eq!(system_tx.fee_payer(sender).unwrap(), sender);
1032
1033 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 assert!(system_tx.subblock_proposer().is_none());
1040
1041 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 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 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 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 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 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}