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, tip20::TIP20Token};
24use tempo_primitives::{TempoTxEnvelope, transaction::calc_gas_balance_spending};
25use tempo_revm::{TempoInvalidTransaction, 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 expiring_nonce_slot: OnceLock<Option<U256>>,
42 tx_env: OnceLock<TempoTxEnv>,
44 key_expiry: OnceLock<Option<u64>>,
49 resolved_fee_token: OnceLock<Address>,
54 fee_balance_slot: OnceLock<Option<(Address, U256)>>,
59}
60
61impl TempoPooledTransaction {
62 pub fn new(transaction: Recovered<TempoTxEnvelope>) -> Self {
64 let is_payment = transaction.is_payment_v2();
65 let is_expiring_nonce = transaction
66 .as_aa()
67 .map(|tx| tx.tx().is_expiring_nonce_tx())
68 .unwrap_or(false);
69 Self {
70 inner: EthPooledTransaction {
71 cost: calc_gas_balance_spending(
72 transaction.gas_limit(),
73 transaction.max_fee_per_gas(),
74 )
75 .saturating_add(transaction.value()),
76 encoded_length: transaction.encode_2718_len(),
77 blob_sidecar: EthBlobTransactionSidecar::None,
78 transaction,
79 },
80 is_payment,
81 is_expiring_nonce,
82 nonce_key_slot: OnceLock::new(),
83 expiring_nonce_slot: OnceLock::new(),
84 tx_env: OnceLock::new(),
85 key_expiry: OnceLock::new(),
86 resolved_fee_token: OnceLock::new(),
87 fee_balance_slot: OnceLock::new(),
88 }
89 }
90
91 pub fn fee_token_cost(&self) -> U256 {
93 self.inner.cost - self.inner.value()
94 }
95
96 pub fn inner(&self) -> &Recovered<TempoTxEnvelope> {
98 &self.inner.transaction
99 }
100
101 pub fn is_aa(&self) -> bool {
103 self.inner().is_aa()
104 }
105
106 pub fn nonce_key(&self) -> Option<U256> {
108 self.inner.transaction.nonce_key()
109 }
110
111 pub fn nonce_key_slot(&self) -> Option<U256> {
113 *self.nonce_key_slot.get_or_init(|| {
114 let nonce_key = self.nonce_key()?;
115 let sender = self.sender();
116 let slot = NonceManager::new().nonces[sender][nonce_key].slot();
117 Some(slot)
118 })
119 }
120
121 pub fn is_payment(&self) -> bool {
125 self.is_payment
126 }
127
128 pub(crate) fn is_aa_2d(&self) -> bool {
131 self.inner
132 .transaction
133 .as_aa()
134 .map(|tx| !tx.tx().nonce_key.is_zero())
135 .unwrap_or(false)
136 }
137
138 pub(crate) fn is_expiring_nonce(&self) -> bool {
140 self.is_expiring_nonce
141 }
142
143 pub fn keychain_subject(&self) -> Option<KeychainSubject> {
152 let aa_tx = self.inner().as_aa()?;
153 let keychain_sig = aa_tx.signature().as_keychain()?;
154 let key_id = keychain_sig.key_id(&aa_tx.signature_hash()).ok()?;
155 let fee_token = self
156 .resolved_fee_token
157 .get()
158 .copied()
159 .unwrap_or_else(|| self.inner().fee_token().unwrap_or(DEFAULT_FEE_TOKEN));
160 Some(KeychainSubject {
161 account: keychain_sig.user_address,
162 key_id,
163 fee_token,
164 })
165 }
166
167 pub(crate) fn aa_transaction_id(&self) -> Option<AA2dTransactionId> {
169 let nonce_key = self.nonce_key()?;
170 let sender = AASequenceId {
171 address: self.sender(),
172 nonce_key,
173 };
174 Some(AA2dTransactionId {
175 seq_id: sender,
176 nonce: self.nonce(),
177 })
178 }
179
180 fn tx_env_slow(&self) -> TempoTxEnv {
182 TempoTxEnv::from_recovered_tx(self.inner().inner(), self.sender())
183 }
184
185 pub fn tx_env(&self) -> &TempoTxEnv {
190 self.tx_env.get_or_init(|| self.tx_env_slow())
191 }
192
193 pub fn into_with_tx_env(mut self) -> WithTxEnv<TempoTxEnv, Recovered<TempoTxEnvelope>> {
198 let tx_env = self.tx_env.take().unwrap_or_else(|| self.tx_env_slow());
199 WithTxEnv {
200 tx_env,
201 tx: Arc::new(self.inner.transaction),
202 }
203 }
204
205 pub fn set_key_expiry(&self, expiry: Option<u64>) {
211 let _ = self.key_expiry.set(expiry);
212 }
213
214 pub fn key_expiry(&self) -> Option<u64> {
219 self.key_expiry.get().copied().flatten()
220 }
221
222 pub fn set_resolved_fee_token(&self, fee_token: Address) {
224 let _ = self.resolved_fee_token.set(fee_token);
225 }
226
227 pub fn resolved_fee_token(&self) -> Option<Address> {
229 self.resolved_fee_token.get().copied()
230 }
231
232 pub fn fee_balance_slot(&self) -> Option<(Address, U256)> {
235 *self.fee_balance_slot.get_or_init(|| {
236 let fee_token = self
237 .resolved_fee_token()
238 .unwrap_or_else(|| self.inner().fee_token().unwrap_or(DEFAULT_FEE_TOKEN));
239 let fee_payer = self.inner().fee_payer(self.sender()).ok()?;
240 let slot = TIP20Token::from_address_unchecked(fee_token).balances[fee_payer].slot();
241 Some((fee_token, slot))
242 })
243 }
244
245 pub fn expiring_nonce_hash(&self) -> Option<B256> {
247 let aa_tx = self.inner().as_aa()?;
248 Some(aa_tx.expiring_nonce_hash(self.sender()))
249 }
250
251 pub fn expiring_nonce_slot(&self) -> Option<U256> {
253 *self.expiring_nonce_slot.get_or_init(|| {
254 let hash = self.expiring_nonce_hash()?;
255 Some(NonceManager::new().expiring_nonce_seen[hash].slot())
256 })
257 }
258}
259
260#[derive(Debug, Error)]
266pub enum TempoPoolTransactionError {
267 #[error(
272 "Transaction exceeds non payment gas limit, please see https://docs.tempo.xyz/errors/tx/ExceedsNonPaymentLimit for more"
273 )]
274 ExceedsNonPaymentLimit,
275
276 #[error(
281 "'valid_before' {valid_before} is too close to current time (min allowed: {min_allowed})"
282 )]
283 InvalidValidBefore {
284 valid_before: u64,
286 min_allowed: u64,
288 },
289
290 #[error("'valid_after' {valid_after} is too far in the future (max allowed: {max_allowed})")]
295 InvalidValidAfter {
296 valid_after: u64,
298 max_allowed: u64,
300 },
301
302 #[error(
308 "Keychain signature validation failed: {0}, please see https://docs.tempo.xyz/errors/tx/Keychain for more"
309 )]
310 Keychain(&'static str),
311
312 #[error("Tempo Transaction with subblock nonce key prefix aren't supported in the pool")]
318 SubblockNonceKey,
319
320 #[error(
325 "Too many authorizations in AA transaction: {count} exceeds maximum allowed {max_allowed}"
326 )]
327 TooManyAuthorizations {
328 count: usize,
330 max_allowed: usize,
332 },
333
334 #[error("Too many calls in AA transaction: {count} exceeds maximum allowed {max_allowed}")]
339 TooManyCalls {
340 count: usize,
342 max_allowed: usize,
344 },
345
346 #[error(
351 "Call input size {size} exceeds maximum allowed {max_allowed} bytes (call index: {call_index})"
352 )]
353 CallInputTooLarge {
354 call_index: usize,
356 size: usize,
358 max_allowed: usize,
360 },
361
362 #[error("Too many access list accounts: {count} exceeds maximum allowed {max_allowed}")]
367 TooManyAccessListAccounts {
368 count: usize,
370 max_allowed: usize,
372 },
373
374 #[error(
379 "Too many storage keys in access list entry {account_index}: {count} exceeds maximum allowed {max_allowed}"
380 )]
381 TooManyStorageKeysPerAccount {
382 account_index: usize,
384 count: usize,
386 max_allowed: usize,
388 },
389
390 #[error(
395 "Too many total storage keys in access list: {count} exceeds maximum allowed {max_allowed}"
396 )]
397 TooManyTotalStorageKeys {
398 count: usize,
400 max_allowed: usize,
402 },
403
404 #[error(
409 "Too many token limits in key authorization: {count} exceeds maximum allowed {max_allowed}"
410 )]
411 TooManyTokenLimits {
412 count: usize,
414 max_allowed: usize,
416 },
417
418 #[error("Access key expired: expiry {expiry} <= min allowed {min_allowed}")]
423 AccessKeyExpired {
424 expiry: u64,
426 min_allowed: u64,
428 },
429
430 #[error("KeyAuthorization expired: expiry {expiry} <= min allowed {min_allowed}")]
435 KeyAuthorizationExpired {
436 expiry: u64,
438 min_allowed: u64,
440 },
441
442 #[error(transparent)]
449 Evm(TempoInvalidTransaction),
450}
451
452impl PoolTransactionError for TempoPoolTransactionError {
453 fn is_bad_transaction(&self) -> bool {
454 match self {
455 Self::Evm(err) => err.is_bad_transaction(),
456 Self::ExceedsNonPaymentLimit
457 | Self::InvalidValidBefore { .. }
458 | Self::InvalidValidAfter { .. }
459 | Self::AccessKeyExpired { .. }
460 | Self::KeyAuthorizationExpired { .. }
461 | Self::Keychain(_) => false,
462 Self::SubblockNonceKey
463 | Self::TooManyAuthorizations { .. }
464 | Self::TooManyCalls { .. }
465 | Self::CallInputTooLarge { .. }
466 | Self::TooManyAccessListAccounts { .. }
467 | Self::TooManyStorageKeysPerAccount { .. }
468 | Self::TooManyTotalStorageKeys { .. }
469 | Self::TooManyTokenLimits { .. } => true,
470 }
471 }
472
473 fn as_any(&self) -> &dyn std::any::Any {
474 self
475 }
476}
477
478impl InMemorySize for TempoPooledTransaction {
479 fn size(&self) -> usize {
480 self.inner.size()
481 }
482}
483
484impl Typed2718 for TempoPooledTransaction {
485 fn ty(&self) -> u8 {
486 self.inner.transaction.ty()
487 }
488}
489
490impl Encodable2718 for TempoPooledTransaction {
491 fn type_flag(&self) -> Option<u8> {
492 self.inner.transaction.type_flag()
493 }
494
495 fn encode_2718_len(&self) -> usize {
496 self.inner.transaction.encode_2718_len()
497 }
498
499 fn encode_2718(&self, out: &mut dyn bytes::BufMut) {
500 self.inner.transaction.encode_2718(out)
501 }
502}
503
504impl PoolTransaction for TempoPooledTransaction {
505 type TryFromConsensusError = Infallible;
506 type Consensus = TempoTxEnvelope;
507 type Pooled = TempoTxEnvelope;
508
509 fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
510 self.inner.transaction.clone()
511 }
512
513 fn consensus_ref(&self) -> Recovered<&Self::Consensus> {
514 self.inner.transaction.as_recovered_ref()
515 }
516
517 fn into_consensus(self) -> Recovered<Self::Consensus> {
518 self.inner.transaction
519 }
520
521 fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
522 Self::new(tx)
523 }
524
525 fn hash(&self) -> &TxHash {
526 self.inner.transaction.tx_hash()
527 }
528
529 fn sender(&self) -> Address {
530 self.inner.transaction.signer()
531 }
532
533 fn sender_ref(&self) -> &Address {
534 self.inner.transaction.signer_ref()
535 }
536
537 fn cost(&self) -> &U256 {
538 &U256::ZERO
539 }
540
541 fn encoded_length(&self) -> usize {
542 self.inner.encoded_length
543 }
544
545 fn requires_nonce_check(&self) -> bool {
546 self.inner
547 .transaction()
548 .as_aa()
549 .map(|tx| {
550 tx.tx().nonce_key.is_zero()
552 })
553 .unwrap_or(true)
554 }
555}
556
557impl alloy_consensus::Transaction for TempoPooledTransaction {
558 fn chain_id(&self) -> Option<u64> {
559 self.inner.chain_id()
560 }
561
562 fn nonce(&self) -> u64 {
563 self.inner.nonce()
564 }
565
566 fn gas_limit(&self) -> u64 {
567 self.inner.gas_limit()
568 }
569
570 fn gas_price(&self) -> Option<u128> {
571 self.inner.gas_price()
572 }
573
574 fn max_fee_per_gas(&self) -> u128 {
575 self.inner.max_fee_per_gas()
576 }
577
578 fn max_priority_fee_per_gas(&self) -> Option<u128> {
579 self.inner.max_priority_fee_per_gas()
580 }
581
582 fn max_fee_per_blob_gas(&self) -> Option<u128> {
583 self.inner.max_fee_per_blob_gas()
584 }
585
586 fn priority_fee_or_price(&self) -> u128 {
587 self.inner.priority_fee_or_price()
588 }
589
590 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
591 self.inner.effective_gas_price(base_fee)
592 }
593
594 fn is_dynamic_fee(&self) -> bool {
595 self.inner.is_dynamic_fee()
596 }
597
598 fn kind(&self) -> TxKind {
599 self.inner.kind()
600 }
601
602 fn is_create(&self) -> bool {
603 self.inner.is_create()
604 }
605
606 fn value(&self) -> U256 {
607 self.inner.value()
608 }
609
610 fn input(&self) -> &Bytes {
611 self.inner.input()
612 }
613
614 fn access_list(&self) -> Option<&AccessList> {
615 self.inner.access_list()
616 }
617
618 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
619 self.inner.blob_versioned_hashes()
620 }
621
622 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
623 self.inner.authorization_list()
624 }
625}
626
627impl EthPoolTransaction for TempoPooledTransaction {
628 fn take_blob(&mut self) -> EthBlobTransactionSidecar {
629 EthBlobTransactionSidecar::None
630 }
631
632 fn try_into_pooled_eip4844(
633 self,
634 _sidecar: Arc<BlobTransactionSidecarVariant>,
635 ) -> Option<Recovered<Self::Pooled>> {
636 None
637 }
638
639 fn try_from_eip4844(
640 _tx: Recovered<Self::Consensus>,
641 _sidecar: BlobTransactionSidecarVariant,
642 ) -> Option<Self> {
643 None
644 }
645
646 fn validate_blob(
647 &self,
648 _sidecar: &BlobTransactionSidecarVariant,
649 _settings: &KzgSettings,
650 ) -> Result<(), BlobTransactionValidationError> {
651 Err(BlobTransactionValidationError::NotBlobTransaction(
652 self.ty(),
653 ))
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660 use crate::test_utils::TxBuilder;
661 use alloy_consensus::TxEip1559;
662 use alloy_primitives::{Address, Signature, TxKind, address};
663 use alloy_sol_types::SolCall;
664 use tempo_contracts::precompiles::ITIP20;
665 use tempo_precompiles::{PATH_USD_ADDRESS, nonce::NonceManager};
666 use tempo_primitives::transaction::{
667 TempoTransaction,
668 tempo_transaction::Call,
669 tt_signature::{PrimitiveSignature, TempoSignature},
670 tt_signed::AASigned,
671 };
672
673 #[test]
674 fn test_payment_classification_positive() {
675 let calldata = ITIP20::transferCall {
677 to: Address::random(),
678 amount: U256::random(),
679 }
680 .abi_encode();
681
682 let tx = TxEip1559 {
683 to: TxKind::Call(PATH_USD_ADDRESS),
684 gas_limit: 21000,
685 input: Bytes::from(calldata),
686 ..Default::default()
687 };
688
689 let envelope = TempoTxEnvelope::Eip1559(alloy_consensus::Signed::new_unchecked(
690 tx,
691 Signature::test_signature(),
692 B256::ZERO,
693 ));
694
695 let recovered = Recovered::new_unchecked(
696 envelope,
697 address!("0000000000000000000000000000000000000001"),
698 );
699
700 let pooled_tx = TempoPooledTransaction::new(recovered);
701 assert!(pooled_tx.is_payment());
702 }
703
704 #[test]
705 fn test_payment_classification_tip20_prefix_without_valid_calldata() {
706 let payment_addr = address!("20c0000000000000000000000000000000000001");
708 let tx = TxEip1559 {
709 to: TxKind::Call(payment_addr),
710 gas_limit: 21000,
711 ..Default::default()
712 };
713
714 let envelope = TempoTxEnvelope::Eip1559(alloy_consensus::Signed::new_unchecked(
715 tx,
716 Signature::test_signature(),
717 B256::ZERO,
718 ));
719
720 let recovered = Recovered::new_unchecked(
721 envelope,
722 address!("0000000000000000000000000000000000000001"),
723 );
724
725 let pooled_tx = TempoPooledTransaction::new(recovered);
726 assert!(!pooled_tx.is_payment());
727 }
728
729 #[test]
730 fn test_payment_classification_negative() {
731 let non_payment_addr = Address::random();
733 let pooled_tx = TxBuilder::eip1559(non_payment_addr)
734 .gas_limit(21000)
735 .build_eip1559();
736 assert!(!pooled_tx.is_payment());
737 }
738
739 #[test]
740 fn test_fee_token_cost() {
741 let sender = Address::random();
742 let value = U256::from(1000);
743 let tx = TxBuilder::aa(sender)
744 .gas_limit(1_000_000)
745 .value(value)
746 .build();
747
748 let expected_fee_cost = U256::from(20000);
752 assert_eq!(tx.fee_token_cost(), expected_fee_cost);
753 assert_eq!(tx.inner.cost, expected_fee_cost + value);
754 }
755
756 #[test]
757 fn test_non_aa_transaction_helpers() {
758 let tx = TxBuilder::eip1559(Address::random())
759 .gas_limit(21000)
760 .build_eip1559();
761
762 assert!(!tx.is_aa(), "Non-AA tx should not be AA");
764 assert!(
765 tx.nonce_key().is_none(),
766 "Non-AA tx should have no nonce key"
767 );
768 assert!(
769 tx.nonce_key_slot().is_none(),
770 "Non-AA tx should have no nonce key slot"
771 );
772 assert!(!tx.is_aa_2d(), "Non-AA tx should not be AA 2D");
773 assert!(
774 tx.aa_transaction_id().is_none(),
775 "Non-AA tx should have no AA transaction ID"
776 );
777 }
778
779 #[test]
780 fn test_aa_transaction_with_zero_nonce_key() {
781 let sender = Address::random();
782 let nonce = 5u64;
783 let tx = TxBuilder::aa(sender).nonce(nonce).build();
784
785 assert!(tx.is_aa(), "AA tx should be AA");
786 assert_eq!(
787 tx.nonce_key(),
788 Some(U256::ZERO),
789 "Should have nonce_key = 0"
790 );
791 assert!(!tx.is_aa_2d(), "AA tx with nonce_key=0 should NOT be 2D");
792
793 let aa_id = tx
795 .aa_transaction_id()
796 .expect("Should have AA transaction ID");
797 assert_eq!(aa_id.seq_id.address, sender);
798 assert_eq!(aa_id.seq_id.nonce_key, U256::ZERO);
799 assert_eq!(aa_id.nonce, nonce);
800 }
801
802 #[test]
803 fn test_aa_transaction_with_nonzero_nonce_key() {
804 let sender = Address::random();
805 let nonce_key = U256::from(42);
806 let nonce = 10u64;
807 let tx = TxBuilder::aa(sender)
808 .nonce_key(nonce_key)
809 .nonce(nonce)
810 .build();
811
812 assert!(tx.is_aa(), "AA tx should be AA");
813 assert_eq!(
814 tx.nonce_key(),
815 Some(nonce_key),
816 "Should have correct nonce_key"
817 );
818 assert!(tx.is_aa_2d(), "AA tx with nonce_key > 0 should be 2D");
819
820 let aa_id = tx
822 .aa_transaction_id()
823 .expect("Should have AA transaction ID");
824 assert_eq!(aa_id.seq_id.address, sender);
825 assert_eq!(aa_id.seq_id.nonce_key, nonce_key);
826 assert_eq!(aa_id.nonce, nonce);
827 }
828
829 #[test]
830 fn test_nonce_key_slot_caching_for_2d_tx() {
831 let sender = Address::random();
832 let nonce_key = U256::from(123);
833 let tx = TxBuilder::aa(sender).nonce_key(nonce_key).build();
834
835 let expected_slot = NonceManager::new().nonces[sender][nonce_key].slot();
837
838 let slot1 = tx.nonce_key_slot();
840 assert_eq!(slot1, Some(expected_slot));
841
842 let slot2 = tx.nonce_key_slot();
844 assert_eq!(slot2, Some(expected_slot));
845 assert_eq!(slot1, slot2);
846 }
847
848 #[test]
849 fn test_is_bad_transaction() {
850 let cases: &[(TempoPoolTransactionError, bool)] = &[
851 (TempoPoolTransactionError::ExceedsNonPaymentLimit, false),
852 (
853 TempoPoolTransactionError::InvalidValidBefore {
854 valid_before: 100,
855 min_allowed: 200,
856 },
857 false,
858 ),
859 (
860 TempoPoolTransactionError::InvalidValidAfter {
861 valid_after: 200,
862 max_allowed: 100,
863 },
864 false,
865 ),
866 (TempoPoolTransactionError::Keychain("test error"), false),
867 (
868 TempoPoolTransactionError::Evm(TempoInvalidTransaction::NonceManagerError(
869 "nonce error".to_string(),
870 )),
871 false,
872 ),
873 (
874 TempoPoolTransactionError::AccessKeyExpired {
875 expiry: 100,
876 min_allowed: 200,
877 },
878 false,
879 ),
880 (
881 TempoPoolTransactionError::KeyAuthorizationExpired {
882 expiry: 100,
883 min_allowed: 200,
884 },
885 false,
886 ),
887 (TempoPoolTransactionError::SubblockNonceKey, true),
888 (
889 TempoPoolTransactionError::Evm(TempoInvalidTransaction::CallsValidation(
890 "calls error",
891 )),
892 true,
893 ),
894 ];
895
896 for (err, expected) in cases {
897 assert_eq!(
898 err.is_bad_transaction(),
899 *expected,
900 "Unexpected is_bad_transaction() for: {err}"
901 );
902 }
903 }
904
905 #[test]
906 fn test_requires_nonce_check() {
907 let cases: &[(TempoPooledTransaction, bool, &str)] = &[
908 (
909 TxBuilder::eip1559(Address::random())
910 .gas_limit(21000)
911 .build_eip1559(),
912 true,
913 "Non-AA should require nonce check",
914 ),
915 (
916 TxBuilder::aa(Address::random()).build(),
917 true,
918 "AA with nonce_key=0 should require nonce check",
919 ),
920 (
921 TxBuilder::aa(Address::random())
922 .nonce_key(U256::from(1))
923 .build(),
924 false,
925 "AA with nonce_key > 0 should NOT require nonce check",
926 ),
927 ];
928
929 for (tx, expected, msg) in cases {
930 assert_eq!(tx.requires_nonce_check(), *expected, "{msg}");
931 }
932 }
933
934 #[test]
935 fn test_validate_blob_returns_not_blob_transaction() {
936 use alloy_eips::eip7594::BlobTransactionSidecarVariant;
937
938 let tx = TxBuilder::eip1559(Address::random())
939 .gas_limit(21000)
940 .build_eip1559();
941
942 let sidecar = BlobTransactionSidecarVariant::Eip4844(Default::default());
944 let settings = alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get();
946
947 let result = tx.validate_blob(&sidecar, settings);
948
949 assert!(matches!(
950 result,
951 Err(BlobTransactionValidationError::NotBlobTransaction(ty)) if ty == tx.ty()
952 ));
953 }
954
955 #[test]
956 fn test_take_blob_returns_none() {
957 let mut tx = TxBuilder::eip1559(Address::random())
958 .gas_limit(21000)
959 .build_eip1559();
960 let blob = tx.take_blob();
961 assert!(matches!(blob, EthBlobTransactionSidecar::None));
962 }
963
964 #[test]
965 fn test_pool_transaction_hash_and_sender() {
966 let sender = Address::random();
967 let tx = TxBuilder::aa(sender).build();
968
969 assert!(!tx.hash().is_zero(), "Hash should not be zero");
970 assert_eq!(tx.sender(), sender);
971 assert_eq!(tx.sender_ref(), &sender);
972 }
973
974 #[test]
975 fn test_pool_transaction_clone_into_consensus() {
976 let sender = Address::random();
977 let tx = TxBuilder::aa(sender).build();
978 let hash = *tx.hash();
979
980 let cloned = tx.clone_into_consensus();
981 assert_eq!(cloned.tx_hash(), &hash);
982 assert_eq!(cloned.signer(), sender);
983 }
984
985 #[test]
986 fn test_pool_transaction_into_consensus() {
987 let sender = Address::random();
988 let tx = TxBuilder::aa(sender).build();
989 let hash = *tx.hash();
990
991 let consensus = tx.into_consensus();
992 assert_eq!(consensus.tx_hash(), &hash);
993 assert_eq!(consensus.signer(), sender);
994 }
995
996 #[test]
997 fn test_pool_transaction_from_pooled() {
998 let sender = Address::random();
999 let nonce = 42u64;
1000 let aa_tx = TempoTransaction {
1001 chain_id: 1,
1002 max_priority_fee_per_gas: 1_000_000_000,
1003 max_fee_per_gas: 20_000_000_000,
1004 gas_limit: 1_000_000,
1005 calls: vec![Call {
1006 to: TxKind::Call(Address::random()),
1007 value: U256::ZERO,
1008 input: Default::default(),
1009 }],
1010 nonce_key: U256::ZERO,
1011 nonce,
1012 ..Default::default()
1013 };
1014
1015 let signature =
1016 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1017 let aa_signed = AASigned::new_unhashed(aa_tx, signature);
1018 let envelope: TempoTxEnvelope = aa_signed.into();
1019 let recovered = Recovered::new_unchecked(envelope, sender);
1020
1021 let pooled = TempoPooledTransaction::from_pooled(recovered);
1022 assert_eq!(pooled.sender(), sender);
1023 assert_eq!(pooled.nonce(), nonce);
1024 }
1025
1026 #[test]
1027 fn test_transaction_trait_forwarding() {
1028 let sender = Address::random();
1029 let tx = TxBuilder::aa(sender)
1030 .gas_limit(1_000_000)
1031 .value(U256::from(500))
1032 .build();
1033
1034 assert_eq!(tx.chain_id(), Some(42431));
1036 assert_eq!(tx.nonce(), 0);
1037 assert_eq!(tx.gas_limit(), 1_000_000);
1038 assert_eq!(tx.max_fee_per_gas(), 20_000_000_000);
1039 assert_eq!(tx.max_priority_fee_per_gas(), Some(1_000_000_000));
1040 assert!(tx.is_dynamic_fee());
1041 assert!(!tx.is_create());
1042 }
1043
1044 #[test]
1045 fn test_cost_returns_zero() {
1046 let tx = TxBuilder::aa(Address::random())
1047 .gas_limit(1_000_000)
1048 .value(U256::from(1000))
1049 .build();
1050
1051 assert_eq!(*tx.cost(), U256::ZERO);
1053 }
1054}
1055
1056#[derive(Debug, Clone, Default)]
1065pub struct RevokedKeys {
1066 by_account: AddressMap<Vec<Address>>,
1068}
1069
1070impl RevokedKeys {
1071 pub fn new() -> Self {
1073 Self::default()
1074 }
1075
1076 pub fn insert(&mut self, account: Address, key_id: Address) {
1078 self.by_account.entry(account).or_default().push(key_id);
1079 }
1080
1081 pub fn is_empty(&self) -> bool {
1083 self.by_account.is_empty()
1084 }
1085
1086 pub fn len(&self) -> usize {
1088 self.by_account.values().map(Vec::len).sum()
1089 }
1090
1091 pub fn contains(&self, account: Address, key_id: Address) -> bool {
1093 self.by_account
1094 .get(&account)
1095 .is_some_and(|key_ids| key_ids.contains(&key_id))
1096 }
1097}
1098
1099#[derive(Debug, Clone, Default)]
1104pub struct SpendingLimitUpdates {
1105 by_account: AddressMap<Vec<(Address, Option<Address>)>>,
1108}
1109
1110impl SpendingLimitUpdates {
1111 pub fn new() -> Self {
1113 Self::default()
1114 }
1115
1116 pub fn insert(&mut self, account: Address, key_id: Address, token: Option<Address>) {
1118 self.by_account
1119 .entry(account)
1120 .or_default()
1121 .push((key_id, token));
1122 }
1123
1124 pub fn is_empty(&self) -> bool {
1126 self.by_account.is_empty()
1127 }
1128
1129 pub fn len(&self) -> usize {
1131 self.by_account.values().map(Vec::len).sum()
1132 }
1133
1134 pub fn contains(&self, account: Address, key_id: Address, token: Address) -> bool {
1139 self.by_account
1140 .get(&account)
1141 .is_some_and(|pairs: &Vec<(Address, Option<Address>)>| {
1142 pairs
1143 .iter()
1144 .any(|&(k, t)| k == key_id && t.is_none_or(|t| t == token))
1145 })
1146 }
1147}
1148
1149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1154pub struct KeychainSubject {
1155 pub account: Address,
1157 pub key_id: Address,
1159 pub fee_token: Address,
1161}
1162
1163impl KeychainSubject {
1164 pub fn matches_revoked(&self, revoked_keys: &RevokedKeys) -> bool {
1169 revoked_keys.contains(self.account, self.key_id)
1170 }
1171
1172 pub fn matches_spending_limit_update(
1177 &self,
1178 spending_limit_updates: &SpendingLimitUpdates,
1179 ) -> bool {
1180 spending_limit_updates.contains(self.account, self.key_id, self.fee_token)
1181 }
1182}