1use crate::tt_2d_pool::{AA2dTransactionId, AASequenceId};
2use alloy_consensus::{BlobTransactionValidationError, Transaction, transaction::TxHashRef};
3use alloy_eips::{
4 eip2718::{Encodable2718, Typed2718},
5 eip2930::AccessList,
6 eip4844::env_settings::KzgSettings,
7 eip7594::BlobTransactionSidecarVariant,
8 eip7702::SignedAuthorization,
9};
10use alloy_evm::FromRecoveredTx;
11use alloy_primitives::{Address, B256, Bytes, TxHash, TxKind, U256, bytes, map::AddressMap};
12use reth_evm::execute::WithTxEnv;
13use reth_primitives_traits::{InMemorySize, Recovered};
14use reth_transaction_pool::{
15 EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction,
16 error::PoolTransactionError,
17};
18use std::{
19 convert::Infallible,
20 fmt::Debug,
21 sync::{Arc, OnceLock},
22};
23use tempo_precompiles::{DEFAULT_FEE_TOKEN, nonce::NonceManager};
24use tempo_primitives::{TempoTxEnvelope, transaction::calc_gas_balance_spending};
25use tempo_revm::TempoTxEnv;
26use thiserror::Error;
27
28#[derive(Debug, Clone)]
32pub struct TempoPooledTransaction {
33 inner: EthPooledTransaction<TempoTxEnvelope>,
34 is_payment: bool,
36 is_expiring_nonce: bool,
38 nonce_key_slot: OnceLock<Option<U256>>,
40 tx_env: OnceLock<TempoTxEnv>,
42 key_expiry: OnceLock<Option<u64>>,
47 resolved_fee_token: OnceLock<Address>,
52}
53
54impl TempoPooledTransaction {
55 pub fn new(transaction: Recovered<TempoTxEnvelope>) -> Self {
57 let is_payment = transaction.is_payment_v2();
58 let is_expiring_nonce = transaction
59 .as_aa()
60 .map(|tx| tx.tx().is_expiring_nonce_tx())
61 .unwrap_or(false);
62 Self {
63 inner: EthPooledTransaction {
64 cost: calc_gas_balance_spending(
65 transaction.gas_limit(),
66 transaction.max_fee_per_gas(),
67 )
68 .saturating_add(transaction.value()),
69 encoded_length: transaction.encode_2718_len(),
70 blob_sidecar: EthBlobTransactionSidecar::None,
71 transaction,
72 },
73 is_payment,
74 is_expiring_nonce,
75 nonce_key_slot: OnceLock::new(),
76 tx_env: OnceLock::new(),
77 key_expiry: OnceLock::new(),
78 resolved_fee_token: OnceLock::new(),
79 }
80 }
81
82 pub fn fee_token_cost(&self) -> U256 {
84 self.inner.cost - self.inner.value()
85 }
86
87 pub fn inner(&self) -> &Recovered<TempoTxEnvelope> {
89 &self.inner.transaction
90 }
91
92 pub fn is_aa(&self) -> bool {
94 self.inner().is_aa()
95 }
96
97 pub fn nonce_key(&self) -> Option<U256> {
99 self.inner.transaction.nonce_key()
100 }
101
102 pub fn nonce_key_slot(&self) -> Option<U256> {
104 *self.nonce_key_slot.get_or_init(|| {
105 let nonce_key = self.nonce_key()?;
106 let sender = self.sender();
107 let slot = NonceManager::new().nonces[sender][nonce_key].slot();
108 Some(slot)
109 })
110 }
111
112 pub fn is_payment(&self) -> bool {
116 self.is_payment
117 }
118
119 pub(crate) fn is_aa_2d(&self) -> bool {
122 self.inner
123 .transaction
124 .as_aa()
125 .map(|tx| !tx.tx().nonce_key.is_zero())
126 .unwrap_or(false)
127 }
128
129 pub(crate) fn is_expiring_nonce(&self) -> bool {
131 self.is_expiring_nonce
132 }
133
134 pub fn keychain_subject(&self) -> Option<KeychainSubject> {
143 let aa_tx = self.inner().as_aa()?;
144 let keychain_sig = aa_tx.signature().as_keychain()?;
145 let key_id = keychain_sig.key_id(&aa_tx.signature_hash()).ok()?;
146 let fee_token = self
147 .resolved_fee_token
148 .get()
149 .copied()
150 .unwrap_or_else(|| self.inner().fee_token().unwrap_or(DEFAULT_FEE_TOKEN));
151 Some(KeychainSubject {
152 account: keychain_sig.user_address,
153 key_id,
154 fee_token,
155 })
156 }
157
158 pub(crate) fn aa_transaction_id(&self) -> Option<AA2dTransactionId> {
160 let nonce_key = self.nonce_key()?;
161 let sender = AASequenceId {
162 address: self.sender(),
163 nonce_key,
164 };
165 Some(AA2dTransactionId {
166 seq_id: sender,
167 nonce: self.nonce(),
168 })
169 }
170
171 fn tx_env_slow(&self) -> TempoTxEnv {
173 TempoTxEnv::from_recovered_tx(self.inner().inner(), self.sender())
174 }
175
176 pub fn prepare_tx_env(&self) {
181 self.tx_env.get_or_init(|| self.tx_env_slow());
182 }
183
184 pub fn into_with_tx_env(mut self) -> WithTxEnv<TempoTxEnv, Recovered<TempoTxEnvelope>> {
189 let tx_env = self.tx_env.take().unwrap_or_else(|| self.tx_env_slow());
190 WithTxEnv {
191 tx_env,
192 tx: Arc::new(self.inner.transaction),
193 }
194 }
195
196 pub fn set_key_expiry(&self, expiry: Option<u64>) {
202 let _ = self.key_expiry.set(expiry);
203 }
204
205 pub fn key_expiry(&self) -> Option<u64> {
210 self.key_expiry.get().copied().flatten()
211 }
212
213 pub fn set_resolved_fee_token(&self, fee_token: Address) {
215 let _ = self.resolved_fee_token.set(fee_token);
216 }
217
218 pub fn expiring_nonce_hash(&self) -> Option<B256> {
220 let aa_tx = self.inner().as_aa()?;
221 Some(aa_tx.expiring_nonce_hash(self.sender()))
222 }
223}
224
225#[derive(Debug, Error)]
226pub enum TempoPoolTransactionError {
227 #[error(
228 "Transaction exceeds non payment gas limit, please see https://docs.tempo.xyz/errors/tx/ExceedsNonPaymentLimit for more"
229 )]
230 ExceedsNonPaymentLimit,
231
232 #[error(
233 "Invalid fee token: {0}, please see https://docs.tempo.xyz/errors/tx/InvalidFeeToken for more"
234 )]
235 InvalidFeeToken(Address),
236
237 #[error(
238 "Fee token {0} is paused, please see https://docs.tempo.xyz/errors/tx/PausedFeeToken for more"
239 )]
240 PausedFeeToken(Address),
241
242 #[error("No fee token preference configured")]
243 MissingFeeToken,
244
245 #[error(
246 "'valid_before' {valid_before} is too close to current time (min allowed: {min_allowed})"
247 )]
248 InvalidValidBefore { valid_before: u64, min_allowed: u64 },
249
250 #[error("'valid_after' {valid_after} is too far in the future (max allowed: {max_allowed})")]
251 InvalidValidAfter { valid_after: u64, max_allowed: u64 },
252
253 #[error(
254 "max_fee_per_gas {max_fee_per_gas} is below the minimum base fee {min_base_fee} for the current hardfork"
255 )]
256 FeeCapBelowMinBaseFee {
257 max_fee_per_gas: u128,
258 min_base_fee: u64,
259 },
260
261 #[error(
262 "Keychain signature validation failed: {0}, please see https://docs.tempo.xyz/errors/tx/Keychain for more"
263 )]
264 Keychain(&'static str),
265
266 #[error("Fee payer signature recovery failed")]
267 InvalidFeePayerSignature,
268
269 #[error("Fee payer cannot resolve to sender address")]
270 SelfSponsoredFeePayer,
271
272 #[error(
273 "Native transfers are not supported, if you were trying to transfer a stablecoin, please call TIP20::Transfer"
274 )]
275 NonZeroValue,
276
277 #[error("Tempo Transaction with subblock nonce key prefix aren't supported in the pool")]
279 SubblockNonceKey,
280
281 #[error("Fee payer {fee_payer} is blacklisted by fee token: {fee_token}")]
283 BlackListedFeePayer {
284 fee_token: Address,
285 fee_payer: Address,
286 },
287
288 #[error(
291 "Insufficient liquidity for fee token: {0}, please see https://docs.tempo.xyz/protocol/fees for more"
292 )]
293 InsufficientLiquidity(Address),
294
295 #[error(
298 "Insufficient gas for AA transaction: gas limit {gas_limit} is less than intrinsic gas {intrinsic_gas}"
299 )]
300 InsufficientGasForAAIntrinsicCost { gas_limit: u64, intrinsic_gas: u64 },
301
302 #[error(
304 "Too many authorizations in AA transaction: {count} exceeds maximum allowed {max_allowed}"
305 )]
306 TooManyAuthorizations { count: usize, max_allowed: usize },
307
308 #[error("Too many calls in AA transaction: {count} exceeds maximum allowed {max_allowed}")]
310 TooManyCalls { count: usize, max_allowed: usize },
311
312 #[error("AA transaction has no calls")]
314 NoCalls,
315
316 #[error("CREATE calls must be the first call in an AA transaction")]
318 CreateCallNotFirst,
319
320 #[error("CREATE calls are not allowed in the same transaction that has an authorization list")]
322 CreateCallWithAuthorizationList,
323
324 #[error(
326 "Call input size {size} exceeds maximum allowed {max_allowed} bytes (call index: {call_index})"
327 )]
328 CallInputTooLarge {
329 call_index: usize,
330 size: usize,
331 max_allowed: usize,
332 },
333
334 #[error("Too many access list accounts: {count} exceeds maximum allowed {max_allowed}")]
336 TooManyAccessListAccounts { count: usize, max_allowed: usize },
337
338 #[error(
340 "Too many storage keys in access list entry {account_index}: {count} exceeds maximum allowed {max_allowed}"
341 )]
342 TooManyStorageKeysPerAccount {
343 account_index: usize,
344 count: usize,
345 max_allowed: usize,
346 },
347
348 #[error(
350 "Too many total storage keys in access list: {count} exceeds maximum allowed {max_allowed}"
351 )]
352 TooManyTotalStorageKeys { count: usize, max_allowed: usize },
353
354 #[error(
356 "Too many token limits in key authorization: {count} exceeds maximum allowed {max_allowed}"
357 )]
358 TooManyTokenLimits { count: usize, max_allowed: usize },
359
360 #[error(
362 "Expiring nonce 'valid_before' {valid_before} exceeds max allowed {max_allowed} (must be within 30s)"
363 )]
364 ExpiringNonceValidBeforeTooFar { valid_before: u64, max_allowed: u64 },
365
366 #[error("Expiring nonce transaction replay: tx hash already seen and not expired")]
368 ExpiringNonceReplay,
369
370 #[error("Expiring nonce transactions must have 'valid_before' set")]
372 ExpiringNonceMissingValidBefore,
373
374 #[error("Expiring nonce transactions must have nonce == 0")]
376 ExpiringNonceNonceNotZero,
377
378 #[error("Access key expired: expiry {expiry} <= min allowed {min_allowed}")]
380 AccessKeyExpired { expiry: u64, min_allowed: u64 },
381
382 #[error("KeyAuthorization expired: expiry {expiry} <= min allowed {min_allowed}")]
384 KeyAuthorizationExpired { expiry: u64, min_allowed: u64 },
385
386 #[error(
388 "Fee token spending limit exceeded: cost {cost} exceeds remaining limit {remaining} for token {fee_token}"
389 )]
390 SpendingLimitExceeded {
391 fee_token: Address,
392 cost: U256,
393 remaining: U256,
394 },
395
396 #[error("legacy V1 keychain signature is no longer accepted, use V2 (type 0x04)")]
398 LegacyKeychainPostT1C,
399
400 #[error("V2 keychain signature (type 0x04) is not valid before T1C activation")]
402 V2KeychainPreT1C,
403}
404
405impl PoolTransactionError for TempoPoolTransactionError {
406 fn is_bad_transaction(&self) -> bool {
407 match self {
408 Self::ExceedsNonPaymentLimit
409 | Self::InvalidFeeToken(_)
410 | Self::PausedFeeToken(_)
411 | Self::MissingFeeToken
412 | Self::BlackListedFeePayer { .. }
413 | Self::InvalidValidBefore { .. }
414 | Self::InvalidValidAfter { .. }
415 | Self::ExpiringNonceValidBeforeTooFar { .. }
416 | Self::ExpiringNonceReplay
417 | Self::AccessKeyExpired { .. }
418 | Self::KeyAuthorizationExpired { .. }
419 | Self::Keychain(_)
420 | Self::InsufficientLiquidity(_)
421 | Self::SpendingLimitExceeded { .. }
422 | Self::V2KeychainPreT1C => false,
423 Self::NonZeroValue
424 | Self::SubblockNonceKey
425 | Self::InsufficientGasForAAIntrinsicCost { .. }
426 | Self::TooManyAuthorizations { .. }
427 | Self::TooManyCalls { .. }
428 | Self::CallInputTooLarge { .. }
429 | Self::TooManyAccessListAccounts { .. }
430 | Self::TooManyStorageKeysPerAccount { .. }
431 | Self::TooManyTotalStorageKeys { .. }
432 | Self::TooManyTokenLimits { .. }
433 | Self::ExpiringNonceMissingValidBefore
434 | Self::ExpiringNonceNonceNotZero
435 | Self::InvalidFeePayerSignature
436 | Self::SelfSponsoredFeePayer
437 | Self::NoCalls
438 | Self::CreateCallWithAuthorizationList
439 | Self::CreateCallNotFirst
440 | Self::FeeCapBelowMinBaseFee { .. }
441 | Self::LegacyKeychainPostT1C => true,
442 }
443 }
444
445 fn as_any(&self) -> &dyn std::any::Any {
446 self
447 }
448}
449
450impl From<tempo_primitives::transaction::KeychainVersionError> for TempoPoolTransactionError {
451 fn from(err: tempo_primitives::transaction::KeychainVersionError) -> Self {
452 match err {
453 tempo_primitives::transaction::KeychainVersionError::LegacyPostT1C => {
454 Self::LegacyKeychainPostT1C
455 }
456 tempo_primitives::transaction::KeychainVersionError::V2BeforeActivation => {
457 Self::V2KeychainPreT1C
458 }
459 }
460 }
461}
462
463impl InMemorySize for TempoPooledTransaction {
464 fn size(&self) -> usize {
465 self.inner.size()
466 }
467}
468
469impl Typed2718 for TempoPooledTransaction {
470 fn ty(&self) -> u8 {
471 self.inner.transaction.ty()
472 }
473}
474
475impl Encodable2718 for TempoPooledTransaction {
476 fn type_flag(&self) -> Option<u8> {
477 self.inner.transaction.type_flag()
478 }
479
480 fn encode_2718_len(&self) -> usize {
481 self.inner.transaction.encode_2718_len()
482 }
483
484 fn encode_2718(&self, out: &mut dyn bytes::BufMut) {
485 self.inner.transaction.encode_2718(out)
486 }
487}
488
489impl PoolTransaction for TempoPooledTransaction {
490 type TryFromConsensusError = Infallible;
491 type Consensus = TempoTxEnvelope;
492 type Pooled = TempoTxEnvelope;
493
494 fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
495 self.inner.transaction.clone()
496 }
497
498 fn consensus_ref(&self) -> Recovered<&Self::Consensus> {
499 self.inner.transaction.as_recovered_ref()
500 }
501
502 fn into_consensus(self) -> Recovered<Self::Consensus> {
503 self.inner.transaction
504 }
505
506 fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
507 Self::new(tx)
508 }
509
510 fn hash(&self) -> &TxHash {
511 self.inner.transaction.tx_hash()
512 }
513
514 fn sender(&self) -> Address {
515 self.inner.transaction.signer()
516 }
517
518 fn sender_ref(&self) -> &Address {
519 self.inner.transaction.signer_ref()
520 }
521
522 fn cost(&self) -> &U256 {
523 &U256::ZERO
524 }
525
526 fn encoded_length(&self) -> usize {
527 self.inner.encoded_length
528 }
529
530 fn requires_nonce_check(&self) -> bool {
531 self.inner
532 .transaction()
533 .as_aa()
534 .map(|tx| {
535 tx.tx().nonce_key.is_zero()
537 })
538 .unwrap_or(true)
539 }
540}
541
542impl alloy_consensus::Transaction for TempoPooledTransaction {
543 fn chain_id(&self) -> Option<u64> {
544 self.inner.chain_id()
545 }
546
547 fn nonce(&self) -> u64 {
548 self.inner.nonce()
549 }
550
551 fn gas_limit(&self) -> u64 {
552 self.inner.gas_limit()
553 }
554
555 fn gas_price(&self) -> Option<u128> {
556 self.inner.gas_price()
557 }
558
559 fn max_fee_per_gas(&self) -> u128 {
560 self.inner.max_fee_per_gas()
561 }
562
563 fn max_priority_fee_per_gas(&self) -> Option<u128> {
564 self.inner.max_priority_fee_per_gas()
565 }
566
567 fn max_fee_per_blob_gas(&self) -> Option<u128> {
568 self.inner.max_fee_per_blob_gas()
569 }
570
571 fn priority_fee_or_price(&self) -> u128 {
572 self.inner.priority_fee_or_price()
573 }
574
575 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
576 self.inner.effective_gas_price(base_fee)
577 }
578
579 fn is_dynamic_fee(&self) -> bool {
580 self.inner.is_dynamic_fee()
581 }
582
583 fn kind(&self) -> TxKind {
584 self.inner.kind()
585 }
586
587 fn is_create(&self) -> bool {
588 self.inner.is_create()
589 }
590
591 fn value(&self) -> U256 {
592 self.inner.value()
593 }
594
595 fn input(&self) -> &Bytes {
596 self.inner.input()
597 }
598
599 fn access_list(&self) -> Option<&AccessList> {
600 self.inner.access_list()
601 }
602
603 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
604 self.inner.blob_versioned_hashes()
605 }
606
607 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
608 self.inner.authorization_list()
609 }
610}
611
612impl EthPoolTransaction for TempoPooledTransaction {
613 fn take_blob(&mut self) -> EthBlobTransactionSidecar {
614 EthBlobTransactionSidecar::None
615 }
616
617 fn try_into_pooled_eip4844(
618 self,
619 _sidecar: Arc<BlobTransactionSidecarVariant>,
620 ) -> Option<Recovered<Self::Pooled>> {
621 None
622 }
623
624 fn try_from_eip4844(
625 _tx: Recovered<Self::Consensus>,
626 _sidecar: BlobTransactionSidecarVariant,
627 ) -> Option<Self> {
628 None
629 }
630
631 fn validate_blob(
632 &self,
633 _sidecar: &BlobTransactionSidecarVariant,
634 _settings: &KzgSettings,
635 ) -> Result<(), BlobTransactionValidationError> {
636 Err(BlobTransactionValidationError::NotBlobTransaction(
637 self.ty(),
638 ))
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use crate::test_utils::TxBuilder;
646 use alloy_consensus::TxEip1559;
647 use alloy_primitives::{Address, Signature, TxKind, address};
648 use alloy_sol_types::SolCall;
649 use tempo_contracts::precompiles::ITIP20;
650 use tempo_precompiles::{PATH_USD_ADDRESS, nonce::NonceManager};
651 use tempo_primitives::transaction::{
652 TempoTransaction,
653 tempo_transaction::Call,
654 tt_signature::{PrimitiveSignature, TempoSignature},
655 tt_signed::AASigned,
656 };
657
658 #[test]
659 fn test_payment_classification_positive() {
660 let calldata = ITIP20::transferCall {
662 to: Address::random(),
663 amount: U256::random(),
664 }
665 .abi_encode();
666
667 let tx = TxEip1559 {
668 to: TxKind::Call(PATH_USD_ADDRESS),
669 gas_limit: 21000,
670 input: Bytes::from(calldata),
671 ..Default::default()
672 };
673
674 let envelope = TempoTxEnvelope::Eip1559(alloy_consensus::Signed::new_unchecked(
675 tx,
676 Signature::test_signature(),
677 B256::ZERO,
678 ));
679
680 let recovered = Recovered::new_unchecked(
681 envelope,
682 address!("0000000000000000000000000000000000000001"),
683 );
684
685 let pooled_tx = TempoPooledTransaction::new(recovered);
686 assert!(pooled_tx.is_payment());
687 }
688
689 #[test]
690 fn test_payment_classification_tip20_prefix_without_valid_calldata() {
691 let payment_addr = address!("20c0000000000000000000000000000000000001");
693 let tx = TxEip1559 {
694 to: TxKind::Call(payment_addr),
695 gas_limit: 21000,
696 ..Default::default()
697 };
698
699 let envelope = TempoTxEnvelope::Eip1559(alloy_consensus::Signed::new_unchecked(
700 tx,
701 Signature::test_signature(),
702 B256::ZERO,
703 ));
704
705 let recovered = Recovered::new_unchecked(
706 envelope,
707 address!("0000000000000000000000000000000000000001"),
708 );
709
710 let pooled_tx = TempoPooledTransaction::new(recovered);
711 assert!(!pooled_tx.is_payment());
712 }
713
714 #[test]
715 fn test_payment_classification_negative() {
716 let non_payment_addr = Address::random();
718 let pooled_tx = TxBuilder::eip1559(non_payment_addr)
719 .gas_limit(21000)
720 .build_eip1559();
721 assert!(!pooled_tx.is_payment());
722 }
723
724 #[test]
725 fn test_fee_token_cost() {
726 let sender = Address::random();
727 let value = U256::from(1000);
728 let tx = TxBuilder::aa(sender)
729 .gas_limit(1_000_000)
730 .value(value)
731 .build();
732
733 let expected_fee_cost = U256::from(20000);
737 assert_eq!(tx.fee_token_cost(), expected_fee_cost);
738 assert_eq!(tx.inner.cost, expected_fee_cost + value);
739 }
740
741 #[test]
742 fn test_non_aa_transaction_helpers() {
743 let tx = TxBuilder::eip1559(Address::random())
744 .gas_limit(21000)
745 .build_eip1559();
746
747 assert!(!tx.is_aa(), "Non-AA tx should not be AA");
749 assert!(
750 tx.nonce_key().is_none(),
751 "Non-AA tx should have no nonce key"
752 );
753 assert!(
754 tx.nonce_key_slot().is_none(),
755 "Non-AA tx should have no nonce key slot"
756 );
757 assert!(!tx.is_aa_2d(), "Non-AA tx should not be AA 2D");
758 assert!(
759 tx.aa_transaction_id().is_none(),
760 "Non-AA tx should have no AA transaction ID"
761 );
762 }
763
764 #[test]
765 fn test_aa_transaction_with_zero_nonce_key() {
766 let sender = Address::random();
767 let nonce = 5u64;
768 let tx = TxBuilder::aa(sender).nonce(nonce).build();
769
770 assert!(tx.is_aa(), "AA tx should be AA");
771 assert_eq!(
772 tx.nonce_key(),
773 Some(U256::ZERO),
774 "Should have nonce_key = 0"
775 );
776 assert!(!tx.is_aa_2d(), "AA tx with nonce_key=0 should NOT be 2D");
777
778 let aa_id = tx
780 .aa_transaction_id()
781 .expect("Should have AA transaction ID");
782 assert_eq!(aa_id.seq_id.address, sender);
783 assert_eq!(aa_id.seq_id.nonce_key, U256::ZERO);
784 assert_eq!(aa_id.nonce, nonce);
785 }
786
787 #[test]
788 fn test_aa_transaction_with_nonzero_nonce_key() {
789 let sender = Address::random();
790 let nonce_key = U256::from(42);
791 let nonce = 10u64;
792 let tx = TxBuilder::aa(sender)
793 .nonce_key(nonce_key)
794 .nonce(nonce)
795 .build();
796
797 assert!(tx.is_aa(), "AA tx should be AA");
798 assert_eq!(
799 tx.nonce_key(),
800 Some(nonce_key),
801 "Should have correct nonce_key"
802 );
803 assert!(tx.is_aa_2d(), "AA tx with nonce_key > 0 should be 2D");
804
805 let aa_id = tx
807 .aa_transaction_id()
808 .expect("Should have AA transaction ID");
809 assert_eq!(aa_id.seq_id.address, sender);
810 assert_eq!(aa_id.seq_id.nonce_key, nonce_key);
811 assert_eq!(aa_id.nonce, nonce);
812 }
813
814 #[test]
815 fn test_nonce_key_slot_caching_for_2d_tx() {
816 let sender = Address::random();
817 let nonce_key = U256::from(123);
818 let tx = TxBuilder::aa(sender).nonce_key(nonce_key).build();
819
820 let expected_slot = NonceManager::new().nonces[sender][nonce_key].slot();
822
823 let slot1 = tx.nonce_key_slot();
825 assert_eq!(slot1, Some(expected_slot));
826
827 let slot2 = tx.nonce_key_slot();
829 assert_eq!(slot2, Some(expected_slot));
830 assert_eq!(slot1, slot2);
831 }
832
833 #[test]
834 fn test_is_bad_transaction() {
835 let cases: &[(TempoPoolTransactionError, bool)] = &[
836 (TempoPoolTransactionError::ExceedsNonPaymentLimit, false),
837 (
838 TempoPoolTransactionError::InvalidFeeToken(Address::ZERO),
839 false,
840 ),
841 (TempoPoolTransactionError::MissingFeeToken, false),
842 (
843 TempoPoolTransactionError::InvalidValidBefore {
844 valid_before: 100,
845 min_allowed: 200,
846 },
847 false,
848 ),
849 (
850 TempoPoolTransactionError::InvalidValidAfter {
851 valid_after: 200,
852 max_allowed: 100,
853 },
854 false,
855 ),
856 (TempoPoolTransactionError::Keychain("test error"), false),
857 (TempoPoolTransactionError::LegacyKeychainPostT1C, true),
858 (TempoPoolTransactionError::V2KeychainPreT1C, false),
859 (
860 TempoPoolTransactionError::InsufficientLiquidity(Address::ZERO),
861 false,
862 ),
863 (
864 TempoPoolTransactionError::BlackListedFeePayer {
865 fee_token: Address::ZERO,
866 fee_payer: Address::ZERO,
867 },
868 false,
869 ),
870 (
871 TempoPoolTransactionError::AccessKeyExpired {
872 expiry: 100,
873 min_allowed: 200,
874 },
875 false,
876 ),
877 (
878 TempoPoolTransactionError::KeyAuthorizationExpired {
879 expiry: 100,
880 min_allowed: 200,
881 },
882 false,
883 ),
884 (TempoPoolTransactionError::InvalidFeePayerSignature, true),
885 (TempoPoolTransactionError::SelfSponsoredFeePayer, true),
886 (TempoPoolTransactionError::NonZeroValue, true),
887 (TempoPoolTransactionError::SubblockNonceKey, true),
888 (
889 TempoPoolTransactionError::InsufficientGasForAAIntrinsicCost {
890 gas_limit: 21000,
891 intrinsic_gas: 50000,
892 },
893 true,
894 ),
895 ];
896
897 for (err, expected) in cases {
898 assert_eq!(
899 err.is_bad_transaction(),
900 *expected,
901 "Unexpected is_bad_transaction() for: {err}"
902 );
903 }
904 }
905
906 #[test]
907 fn test_requires_nonce_check() {
908 let cases: &[(TempoPooledTransaction, bool, &str)] = &[
909 (
910 TxBuilder::eip1559(Address::random())
911 .gas_limit(21000)
912 .build_eip1559(),
913 true,
914 "Non-AA should require nonce check",
915 ),
916 (
917 TxBuilder::aa(Address::random()).build(),
918 true,
919 "AA with nonce_key=0 should require nonce check",
920 ),
921 (
922 TxBuilder::aa(Address::random())
923 .nonce_key(U256::from(1))
924 .build(),
925 false,
926 "AA with nonce_key > 0 should NOT require nonce check",
927 ),
928 ];
929
930 for (tx, expected, msg) in cases {
931 assert_eq!(tx.requires_nonce_check(), *expected, "{msg}");
932 }
933 }
934
935 #[test]
936 fn test_validate_blob_returns_not_blob_transaction() {
937 use alloy_eips::eip7594::BlobTransactionSidecarVariant;
938
939 let tx = TxBuilder::eip1559(Address::random())
940 .gas_limit(21000)
941 .build_eip1559();
942
943 let sidecar = BlobTransactionSidecarVariant::Eip4844(Default::default());
945 let settings = alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get();
947
948 let result = tx.validate_blob(&sidecar, settings);
949
950 assert!(matches!(
951 result,
952 Err(BlobTransactionValidationError::NotBlobTransaction(ty)) if ty == tx.ty()
953 ));
954 }
955
956 #[test]
957 fn test_take_blob_returns_none() {
958 let mut tx = TxBuilder::eip1559(Address::random())
959 .gas_limit(21000)
960 .build_eip1559();
961 let blob = tx.take_blob();
962 assert!(matches!(blob, EthBlobTransactionSidecar::None));
963 }
964
965 #[test]
966 fn test_pool_transaction_hash_and_sender() {
967 let sender = Address::random();
968 let tx = TxBuilder::aa(sender).build();
969
970 assert!(!tx.hash().is_zero(), "Hash should not be zero");
971 assert_eq!(tx.sender(), sender);
972 assert_eq!(tx.sender_ref(), &sender);
973 }
974
975 #[test]
976 fn test_pool_transaction_clone_into_consensus() {
977 let sender = Address::random();
978 let tx = TxBuilder::aa(sender).build();
979 let hash = *tx.hash();
980
981 let cloned = tx.clone_into_consensus();
982 assert_eq!(cloned.tx_hash(), &hash);
983 assert_eq!(cloned.signer(), sender);
984 }
985
986 #[test]
987 fn test_pool_transaction_into_consensus() {
988 let sender = Address::random();
989 let tx = TxBuilder::aa(sender).build();
990 let hash = *tx.hash();
991
992 let consensus = tx.into_consensus();
993 assert_eq!(consensus.tx_hash(), &hash);
994 assert_eq!(consensus.signer(), sender);
995 }
996
997 #[test]
998 fn test_pool_transaction_from_pooled() {
999 let sender = Address::random();
1000 let nonce = 42u64;
1001 let aa_tx = TempoTransaction {
1002 chain_id: 1,
1003 max_priority_fee_per_gas: 1_000_000_000,
1004 max_fee_per_gas: 20_000_000_000,
1005 gas_limit: 1_000_000,
1006 calls: vec![Call {
1007 to: TxKind::Call(Address::random()),
1008 value: U256::ZERO,
1009 input: Default::default(),
1010 }],
1011 nonce_key: U256::ZERO,
1012 nonce,
1013 ..Default::default()
1014 };
1015
1016 let signature =
1017 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1018 let aa_signed = AASigned::new_unhashed(aa_tx, signature);
1019 let envelope: TempoTxEnvelope = aa_signed.into();
1020 let recovered = Recovered::new_unchecked(envelope, sender);
1021
1022 let pooled = TempoPooledTransaction::from_pooled(recovered);
1023 assert_eq!(pooled.sender(), sender);
1024 assert_eq!(pooled.nonce(), nonce);
1025 }
1026
1027 #[test]
1028 fn test_transaction_trait_forwarding() {
1029 let sender = Address::random();
1030 let tx = TxBuilder::aa(sender)
1031 .gas_limit(1_000_000)
1032 .value(U256::from(500))
1033 .build();
1034
1035 assert_eq!(tx.chain_id(), Some(1));
1037 assert_eq!(tx.nonce(), 0);
1038 assert_eq!(tx.gas_limit(), 1_000_000);
1039 assert_eq!(tx.max_fee_per_gas(), 20_000_000_000);
1040 assert_eq!(tx.max_priority_fee_per_gas(), Some(1_000_000_000));
1041 assert!(tx.is_dynamic_fee());
1042 assert!(!tx.is_create());
1043 }
1044
1045 #[test]
1046 fn test_cost_returns_zero() {
1047 let tx = TxBuilder::aa(Address::random())
1048 .gas_limit(1_000_000)
1049 .value(U256::from(1000))
1050 .build();
1051
1052 assert_eq!(*tx.cost(), U256::ZERO);
1054 }
1055}
1056
1057#[derive(Debug, Clone, Default)]
1066pub struct RevokedKeys {
1067 by_account: AddressMap<Vec<Address>>,
1069}
1070
1071impl RevokedKeys {
1072 pub fn new() -> Self {
1074 Self::default()
1075 }
1076
1077 pub fn insert(&mut self, account: Address, key_id: Address) {
1079 self.by_account.entry(account).or_default().push(key_id);
1080 }
1081
1082 pub fn is_empty(&self) -> bool {
1084 self.by_account.is_empty()
1085 }
1086
1087 pub fn len(&self) -> usize {
1089 self.by_account.values().map(Vec::len).sum()
1090 }
1091
1092 pub fn contains(&self, account: Address, key_id: Address) -> bool {
1094 self.by_account
1095 .get(&account)
1096 .is_some_and(|key_ids| key_ids.contains(&key_id))
1097 }
1098}
1099
1100#[derive(Debug, Clone, Default)]
1105pub struct SpendingLimitUpdates {
1106 by_account: AddressMap<Vec<(Address, Option<Address>)>>,
1109}
1110
1111impl SpendingLimitUpdates {
1112 pub fn new() -> Self {
1114 Self::default()
1115 }
1116
1117 pub fn insert(&mut self, account: Address, key_id: Address, token: Option<Address>) {
1119 self.by_account
1120 .entry(account)
1121 .or_default()
1122 .push((key_id, token));
1123 }
1124
1125 pub fn is_empty(&self) -> bool {
1127 self.by_account.is_empty()
1128 }
1129
1130 pub fn len(&self) -> usize {
1132 self.by_account.values().map(Vec::len).sum()
1133 }
1134
1135 pub fn contains(&self, account: Address, key_id: Address, token: Address) -> bool {
1140 self.by_account
1141 .get(&account)
1142 .is_some_and(|pairs: &Vec<(Address, Option<Address>)>| {
1143 pairs
1144 .iter()
1145 .any(|&(k, t)| k == key_id && t.is_none_or(|t| t == token))
1146 })
1147 }
1148}
1149
1150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1155pub struct KeychainSubject {
1156 pub account: Address,
1158 pub key_id: Address,
1160 pub fee_token: Address,
1162}
1163
1164impl KeychainSubject {
1165 pub fn matches_revoked(&self, revoked_keys: &RevokedKeys) -> bool {
1170 revoked_keys.contains(self.account, self.key_id)
1171 }
1172
1173 pub fn matches_spending_limit_update(
1178 &self,
1179 spending_limit_updates: &SpendingLimitUpdates,
1180 ) -> bool {
1181 spending_limit_updates.contains(self.account, self.key_id, self.fee_token)
1182 }
1183}