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
15pub const KEY_AUTHORIZATION_MAX_RLP_LEN: usize = 1024;
18
19pub const TEMPO_SYSTEM_TX_SIGNATURE: Signature = Signature::new(U256::ZERO, U256::ZERO, false);
21
22pub const TEMPO_SYSTEM_TX_SENDER: Address = Address::ZERO;
24
25#[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 #[envelope(ty = 0)]
45 Legacy(Signed<TxLegacy>),
46
47 #[envelope(ty = 1)]
49 Eip2930(Signed<TxEip2930>),
50
51 #[envelope(ty = 2)]
53 Eip1559(Signed<TxEip1559>),
54
55 #[envelope(ty = 4)]
57 Eip7702(Signed<TxEip7702>),
58
59 #[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 pub fn fee_token(&self) -> Option<Address> {
103 match self {
104 Self::AA(tx) => tx.tx().fee_token,
105 _ => None,
106 }
107 }
108
109 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 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 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 pub fn is_fee_token(&self) -> bool {
141 matches!(self, Self::AA(_))
142 }
143
144 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 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 pub fn is_system_tx(&self) -> bool {
164 matches!(self, Self::Legacy(tx) if tx.signature() == &TEMPO_SYSTEM_TX_SIGNATURE)
165 }
166
167 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 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 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 pub fn subblock_proposer(&self) -> Option<PartialValidatorKey> {
247 let Self::AA(tx) = &self else { return None };
248 tx.tx().subblock_proposer()
249 }
250
251 pub fn as_aa(&self) -> Option<&AASigned> {
253 match self {
254 Self::AA(tx) => Some(tx),
255 _ => None,
256 }
257 }
258
259 pub fn nonce_key(&self) -> Option<U256> {
261 self.as_aa().map(|tx| tx.tx().nonce_key)
262 }
263
264 pub fn is_aa(&self) -> bool {
266 matches!(self, Self::AA(_))
267 }
268
269 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 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 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 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#[inline]
494fn is_tip20_call(to: Option<&Address>) -> bool {
495 to.is_some_and(|to| to.is_tip20())
496}
497
498#[inline]
500fn is_tip1045_call(to: Option<&Address>, input: &[u8]) -> bool {
501 match to {
502 Some(to) if to.is_tip20() => ITIP20::ITIP20Calls::is_payment(input),
504 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 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 fn payment_envelopes(calldata: Bytes) -> [TempoTxEnvelope; 5] {
607 payment_envelopes_to(PAYMENT_TKN, calldata)
608 }
609
610 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(
1185 !system_tx.is_valid_system_tx(2),
1186 "Wrong chain_id should fail"
1187 );
1188
1189 let tx = TxLegacy {
1191 chain_id: Some(chain_id),
1192 gas_limit: 1, ..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 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 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 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 let sender = Address::random();
1236 assert_eq!(system_tx.fee_payer(sender).unwrap(), sender);
1237
1238 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 assert!(system_tx.subblock_proposer().is_none());
1245
1246 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 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 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 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 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 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}