1use crate::tt_2d_pool::{AA2dTransactionId, AASequenceId};
2use alloy_consensus::{
3 BlobTransactionValidationError, Transaction, crypto::RecoveryError, transaction::TxHashRef,
4};
5use alloy_eips::{
6 eip2718::{Decodable2718, Encodable2718, Typed2718},
7 eip2930::AccessList,
8 eip4844::env_settings::KzgSettings,
9 eip7594::BlobTransactionSidecarVariant,
10 eip7702::SignedAuthorization,
11};
12use alloy_evm::FromRecoveredTx;
13use alloy_primitives::{
14 Address, B256, Bytes, TxHash, TxKind, U256, bytes, keccak256, map::AddressMap,
15};
16use alloy_sol_types::SolInterface;
17use reth_evm::execute::WithTxEnv;
18use reth_primitives_traits::{InMemorySize, Recovered, SignerRecoverable};
19use reth_transaction_pool::{
20 EthBlobTransactionSidecar, EthPoolTransaction, EthPooledTransaction, PoolTransaction,
21 error::{PoolTransactionError, RawPoolTransactionError},
22};
23use std::{
24 convert::Infallible,
25 fmt::Debug,
26 sync::{Arc, OnceLock},
27};
28use tempo_contracts::precompiles::ITIP20;
29use tempo_precompiles::{
30 DEFAULT_FEE_TOKEN,
31 nonce::NonceManager,
32 storage::StorageKey,
33 tip20::{TIP20Token, tip20_slots},
34 tip403_registry::tip403_registry_slots,
35};
36use tempo_primitives::{TempoTxEnvelope, transaction::calc_gas_balance_spending};
37use tempo_revm::{TempoInvalidTransaction, TempoTxEnv};
38use thiserror::Error;
39
40#[derive(Debug, Clone)]
44pub struct TempoPooledTransaction {
45 inner: EthPooledTransaction<TempoTxEnvelope>,
46 fee_token_cost: U256,
48 is_payment: bool,
50 expiring_nonce_hash: Option<B256>,
52 nonce_key_slot: OnceLock<Option<U256>>,
54 expiring_nonce_slot: OnceLock<Option<U256>>,
56 tx_env: OnceLock<TempoTxEnv>,
58 key_expiry: OnceLock<Option<u64>>,
63 resolved_fee_token: OnceLock<Address>,
68 key_authorization_signer_subject: OnceLock<Option<KeychainSubject>>,
70 key_authorization_target_subject: OnceLock<Option<KeyAuthorizationTargetSubject>>,
72 fee_balance_slot: OnceLock<Option<(Address, U256)>>,
77}
78
79impl TempoPooledTransaction {
80 pub fn new(transaction: Recovered<TempoTxEnvelope>) -> Self {
82 let sender = transaction.signer();
83 let expiring_nonce_hash = transaction.as_aa().and_then(|tx| {
84 tx.tx()
85 .is_expiring_nonce_tx()
86 .then(|| tx.expiring_nonce_hash(sender))
87 });
88 let encoded_length = transaction.encode_2718_len();
89 Self::new_with(transaction, expiring_nonce_hash, encoded_length)
90 }
91
92 fn new_with(
99 transaction: Recovered<TempoTxEnvelope>,
100 expiring_nonce_hash: Option<B256>,
101 encoded_length: usize,
102 ) -> Self {
103 let is_payment = transaction.is_payment_v2();
104 let value = transaction.value();
105 let cost =
106 calc_gas_balance_spending(transaction.gas_limit(), transaction.max_fee_per_gas())
107 .saturating_add(value);
108 let fee_token_cost = cost - value;
109 Self {
110 inner: EthPooledTransaction {
111 cost,
112 encoded_length,
113 blob_sidecar: EthBlobTransactionSidecar::None,
114 transaction,
115 },
116 fee_token_cost,
117 is_payment,
118 expiring_nonce_hash,
119 nonce_key_slot: OnceLock::new(),
120 expiring_nonce_slot: OnceLock::new(),
121 tx_env: OnceLock::new(),
122 key_expiry: OnceLock::new(),
123 resolved_fee_token: OnceLock::new(),
124 key_authorization_signer_subject: OnceLock::new(),
125 key_authorization_target_subject: OnceLock::new(),
126 fee_balance_slot: OnceLock::new(),
127 }
128 }
129
130 #[inline]
132 pub const fn fee_token_cost(&self) -> U256 {
133 self.fee_token_cost
134 }
135
136 pub fn inner(&self) -> &Recovered<TempoTxEnvelope> {
138 &self.inner.transaction
139 }
140
141 pub fn fee_payer(&self) -> Result<Address, RecoveryError> {
146 if let Some(tx_env) = self.cached_tx_env() {
147 return tx_env.fee_payer().map_err(|_| RecoveryError::new());
148 }
149
150 self.inner().fee_payer(self.inner().signer())
151 }
152
153 pub fn is_aa(&self) -> bool {
155 self.inner().is_aa()
156 }
157
158 pub fn nonce_key(&self) -> Option<U256> {
160 self.inner.transaction.nonce_key()
161 }
162
163 pub fn nonce_key_slot(&self) -> Option<U256> {
165 *self.nonce_key_slot.get_or_init(|| {
166 let nonce_key = self.nonce_key()?;
167 let sender = self.sender();
168 let slot = NonceManager::new().nonces[sender][nonce_key].slot();
169 Some(slot)
170 })
171 }
172
173 pub fn is_payment(&self) -> bool {
175 self.is_payment
176 }
177
178 pub fn is_aa_2d(&self) -> bool {
181 self.inner
182 .transaction
183 .as_aa()
184 .map(|tx| !tx.tx().nonce_key.is_zero())
185 .unwrap_or(false)
186 }
187
188 pub fn is_expiring_nonce(&self) -> bool {
190 self.expiring_nonce_hash.is_some()
191 }
192
193 pub fn keychain_subject(&self) -> Option<KeychainSubject> {
202 let aa_tx = self.inner().as_aa()?;
203 let keychain_sig = aa_tx.signature().as_keychain()?;
204 let key_id = keychain_sig.key_id(&aa_tx.signature_hash()).ok()?;
205 let fee_token = self.effective_fee_token();
206 Some(KeychainSubject {
207 account: keychain_sig.user_address,
208 key_id,
209 fee_token,
210 })
211 }
212
213 pub fn key_authorization_signer_subject(&self) -> Option<KeychainSubject> {
218 *self.key_authorization_signer_subject.get_or_init(|| {
219 let aa_tx = self.inner().as_aa()?;
220 let key_authorization = aa_tx.tx().key_authorization.as_ref()?;
221 let key_id = key_authorization.recover_signer().ok()?;
222 let account = key_authorization
223 .authorization
224 .account
225 .unwrap_or(*self.sender_ref());
226 let fee_token = self.effective_fee_token();
227 Some(KeychainSubject {
228 account,
229 key_id,
230 fee_token,
231 })
232 })
233 }
234
235 pub fn key_authorization_target_subject(&self) -> Option<KeyAuthorizationTargetSubject> {
240 *self.key_authorization_target_subject.get_or_init(|| {
241 let aa_tx = self.inner().as_aa()?;
242 let key_authorization = aa_tx.tx().key_authorization.as_ref()?;
243 let account = key_authorization
244 .authorization
245 .account
246 .unwrap_or(*self.sender_ref());
247 Some(KeyAuthorizationTargetSubject {
248 account,
249 key_id: key_authorization.authorization.key_id,
250 })
251 })
252 }
253
254 pub fn key_authorization_witness_subject(&self) -> Option<KeyAuthorizationWitnessSubject> {
256 let aa_tx = self.inner().as_aa()?;
257 let witness = aa_tx
258 .tx()
259 .key_authorization
260 .as_ref()?
261 .authorization
262 .witness()?;
263 Some(KeyAuthorizationWitnessSubject {
264 account: *self.sender_ref(),
265 witness,
266 })
267 }
268
269 pub fn aa_transaction_id(&self) -> Option<AA2dTransactionId> {
271 let nonce_key = self.nonce_key()?;
272 let sender = AASequenceId {
273 address: self.sender(),
274 nonce_key,
275 };
276 Some(AA2dTransactionId {
277 seq_id: sender,
278 nonce: self.nonce(),
279 })
280 }
281
282 pub(crate) fn tx_env_slow(&self) -> TempoTxEnv {
284 TempoTxEnv::from_recovered_tx(self.inner().inner(), self.sender())
285 }
286
287 pub fn tx_env(&self) -> &TempoTxEnv {
292 self.tx_env.get_or_init(|| self.tx_env_slow())
293 }
294
295 pub(crate) fn cached_tx_env(&self) -> Option<&TempoTxEnv> {
297 self.tx_env.get()
298 }
299
300 pub(crate) fn cache_tx_env(&self, tx_env: TempoTxEnv) {
302 let _ = self.tx_env.set(tx_env);
303 }
304
305 pub fn clone_tx_env(&self) -> TempoTxEnv {
310 self.tx_env().clone()
311 }
312
313 pub fn executable(&self) -> (TempoTxEnv, &Recovered<TempoTxEnvelope>) {
315 (self.tx_env().clone(), &self.inner.transaction)
316 }
317
318 pub fn clone_into_with_tx_env(&self) -> WithTxEnv<TempoTxEnv, Recovered<TempoTxEnvelope>> {
324 WithTxEnv {
325 tx_env: self.clone_tx_env(),
326 tx: Arc::new(self.inner.transaction.clone()),
327 }
328 }
329
330 pub fn into_with_tx_env(mut self) -> WithTxEnv<TempoTxEnv, Recovered<TempoTxEnvelope>> {
335 let tx_env = self.tx_env.take().unwrap_or_else(|| self.tx_env_slow());
336 WithTxEnv {
337 tx_env,
338 tx: Arc::new(self.inner.transaction),
339 }
340 }
341
342 pub fn set_key_expiry(&self, expiry: Option<u64>) {
348 let _ = self.key_expiry.set(expiry);
349 }
350
351 pub fn key_expiry(&self) -> Option<u64> {
356 self.key_expiry.get().copied().flatten()
357 }
358
359 pub fn set_resolved_fee_token(&self, fee_token: Address) {
365 let _ = self.resolved_fee_token.set(fee_token);
366 }
367
368 pub fn resolved_fee_token(&self) -> Option<Address> {
374 self.resolved_fee_token.get().copied()
375 }
376
377 pub fn effective_fee_token(&self) -> Address {
386 self.resolved_fee_token()
387 .unwrap_or_else(|| self.inner().fee_token().unwrap_or(DEFAULT_FEE_TOKEN))
388 }
389
390 pub fn fee_balance_slot(&self) -> Option<(Address, U256)> {
393 *self.fee_balance_slot.get_or_init(|| {
394 let fee_token = self
395 .resolved_fee_token()
396 .unwrap_or_else(|| self.inner().fee_token().unwrap_or(DEFAULT_FEE_TOKEN));
397 let fee_payer = self.fee_payer().ok()?;
398 let slot = TIP20Token::from_address_unchecked(fee_token).balances[fee_payer].slot();
399 Some((fee_token, slot))
400 })
401 }
402
403 pub(crate) fn is_sender_paid_fee(&self) -> bool {
408 let sender = self.sender();
409 self.fee_payer()
410 .map_or(true, |fee_payer| fee_payer == sender)
411 }
412
413 pub fn expiring_nonce_hash(&self) -> Option<B256> {
418 if let Some(hash) = self.expiring_nonce_hash {
419 return Some(hash);
420 }
421
422 let aa_tx = self.inner().as_aa()?;
423 Some(aa_tx.expiring_nonce_hash(self.sender()))
424 }
425
426 pub(crate) fn precomputed_expiring_nonce_hash(&self) -> B256 {
428 self.expiring_nonce_hash
429 .expect("expiring nonce hash must be precomputed")
430 }
431
432 pub fn expiring_nonce_slot(&self) -> Option<U256> {
434 *self.expiring_nonce_slot.get_or_init(|| {
435 let hash = self.expiring_nonce_hash()?;
436 Some(NonceManager::new().expiring_nonce_seen[hash].slot())
437 })
438 }
439
440 pub fn precalculate_keccak_slots(&self) {
448 if !self.is_payment {
449 return;
450 }
451
452 let sender = self.sender();
453 let fee_payer = self.fee_payer().unwrap_or(sender);
454 let fee_collection_warms_fee_payer_rewards = !self.fee_token_cost.is_zero();
455
456 if fee_payer != sender {
458 sender.mapping_slot(tip20_slots::BALANCES);
459 }
460 for (_kind, input) in self.inner().calls() {
461 if let Ok(call) = ITIP20::ITIP20Calls::abi_decode(input) {
462 for addr in call.balance_addresses().into_iter().flatten() {
463 if addr != fee_payer {
464 addr.mapping_slot(tip20_slots::BALANCES);
465 }
466 }
467 for addr in call.reward_addresses(sender).into_iter().flatten() {
468 if fee_collection_warms_fee_payer_rewards && addr == fee_payer {
469 continue;
470 }
471 addr.mapping_slot(tip20_slots::USER_REWARD_INFO);
472 }
473 if let Some(slot) = call
474 .to()
475 .map(|addr| addr.mapping_slot(tip403_registry_slots::RECEIVE_POLICIES))
476 {
477 let _ = keccak256(slot.to_be_bytes::<32>());
478 }
479
480 let from = match &call {
482 ITIP20::ITIP20Calls::transferFrom(c) => Some(c.from),
483 ITIP20::ITIP20Calls::transferFromWithMemo(c) => Some(c.from),
484 _ => None,
485 };
486 if let Some(from) = from {
487 sender.mapping_slot(from.mapping_slot(tip20_slots::ALLOWANCES));
488 }
489 }
490 }
491 }
492}
493
494#[derive(Debug, Error)]
500pub enum TempoPoolTransactionError {
501 #[error(
506 "Transaction exceeds non payment gas limit, please see https://docs.tempo.xyz/errors/tx/ExceedsNonPaymentLimit for more"
507 )]
508 ExceedsNonPaymentLimit,
509
510 #[error(
515 "'valid_before' {valid_before} is too close to current time (min allowed: {min_allowed})"
516 )]
517 InvalidValidBefore {
518 valid_before: u64,
520 min_allowed: u64,
522 },
523
524 #[error("'valid_after' {valid_after} is too far in the future (max allowed: {max_allowed})")]
529 InvalidValidAfter {
530 valid_after: u64,
532 max_allowed: u64,
534 },
535
536 #[error(
542 "Keychain signature validation failed: {0}, please see https://docs.tempo.xyz/errors/tx/Keychain for more"
543 )]
544 Keychain(&'static str),
545
546 #[error("Tempo Transaction with subblock nonce key prefix aren't supported in the pool")]
552 SubblockNonceKey,
553
554 #[error(
559 "Too many authorizations in AA transaction: {count} exceeds maximum allowed {max_allowed}"
560 )]
561 TooManyAuthorizations {
562 count: usize,
564 max_allowed: usize,
566 },
567
568 #[error("Too many calls in AA transaction: {count} exceeds maximum allowed {max_allowed}")]
573 TooManyCalls {
574 count: usize,
576 max_allowed: usize,
578 },
579
580 #[error(
585 "Call input size {size} exceeds maximum allowed {max_allowed} bytes (call index: {call_index})"
586 )]
587 CallInputTooLarge {
588 call_index: usize,
590 size: usize,
592 max_allowed: usize,
594 },
595
596 #[error("Too many access list accounts: {count} exceeds maximum allowed {max_allowed}")]
601 TooManyAccessListAccounts {
602 count: usize,
604 max_allowed: usize,
606 },
607
608 #[error(
613 "Too many storage keys in access list entry {account_index}: {count} exceeds maximum allowed {max_allowed}"
614 )]
615 TooManyStorageKeysPerAccount {
616 account_index: usize,
618 count: usize,
620 max_allowed: usize,
622 },
623
624 #[error(
629 "Too many total storage keys in access list: {count} exceeds maximum allowed {max_allowed}"
630 )]
631 TooManyTotalStorageKeys {
632 count: usize,
634 max_allowed: usize,
636 },
637
638 #[error(
643 "Too many token limits in key authorization: {count} exceeds maximum allowed {max_allowed}"
644 )]
645 TooManyTokenLimits {
646 count: usize,
648 max_allowed: usize,
650 },
651
652 #[error("Access key expired: expiry {expiry} <= min allowed {min_allowed}")]
657 AccessKeyExpired {
658 expiry: u64,
660 min_allowed: u64,
662 },
663
664 #[error("KeyAuthorization expired: expiry {expiry} <= min allowed {min_allowed}")]
669 KeyAuthorizationExpired {
670 expiry: u64,
672 min_allowed: u64,
674 },
675
676 #[error(transparent)]
683 Evm(TempoInvalidTransaction),
684}
685
686impl PoolTransactionError for TempoPoolTransactionError {
687 fn is_bad_transaction(&self) -> bool {
688 match self {
689 Self::Evm(err) => err.is_bad_transaction(),
690 Self::ExceedsNonPaymentLimit
691 | Self::InvalidValidBefore { .. }
692 | Self::InvalidValidAfter { .. }
693 | Self::AccessKeyExpired { .. }
694 | Self::KeyAuthorizationExpired { .. }
695 | Self::Keychain(_) => false,
696 Self::SubblockNonceKey
697 | Self::TooManyAuthorizations { .. }
698 | Self::TooManyCalls { .. }
699 | Self::CallInputTooLarge { .. }
700 | Self::TooManyAccessListAccounts { .. }
701 | Self::TooManyStorageKeysPerAccount { .. }
702 | Self::TooManyTotalStorageKeys { .. }
703 | Self::TooManyTokenLimits { .. } => true,
704 }
705 }
706
707 fn as_any(&self) -> &dyn std::any::Any {
708 self
709 }
710}
711
712impl InMemorySize for TempoPooledTransaction {
713 fn size(&self) -> usize {
714 self.inner.size()
715 }
716}
717
718impl Typed2718 for TempoPooledTransaction {
719 fn ty(&self) -> u8 {
720 self.inner.transaction.ty()
721 }
722}
723
724impl Encodable2718 for TempoPooledTransaction {
725 fn type_flag(&self) -> Option<u8> {
726 self.inner.transaction.type_flag()
727 }
728
729 fn encode_2718_len(&self) -> usize {
730 self.inner.transaction.encode_2718_len()
731 }
732
733 fn encode_2718(&self, out: &mut dyn bytes::BufMut) {
734 self.inner.transaction.encode_2718(out)
735 }
736}
737
738impl PoolTransaction for TempoPooledTransaction {
739 type TryFromConsensusError = Infallible;
740 type Consensus = TempoTxEnvelope;
741 type Pooled = TempoTxEnvelope;
742
743 fn clone_into_consensus(&self) -> Recovered<Self::Consensus> {
744 self.inner.transaction.clone()
745 }
746
747 fn consensus_ref(&self) -> Recovered<&Self::Consensus> {
748 self.inner.transaction.as_recovered_ref()
749 }
750
751 fn into_consensus(self) -> Recovered<Self::Consensus> {
752 self.inner.transaction
753 }
754
755 fn from_pooled(tx: Recovered<Self::Pooled>) -> Self {
756 Self::new(tx)
757 }
758
759 fn recover_raw_transaction(data: &[u8]) -> Result<Self, RawPoolTransactionError> {
760 if data.is_empty() {
761 return Err(RawPoolTransactionError::EmptyRawTransactionData);
762 }
763
764 let encoded_length = data.len();
765 let transaction = Self::Pooled::decode_2718_exact(data)
766 .map_err(|_| RawPoolTransactionError::FailedToDecodeSignedTransaction)?;
767
768 let (signer, expiring_nonce_hash) = match &transaction {
769 TempoTxEnvelope::AA(tx) => tx
770 .recover_signer_with_expiring_nonce_hash()
771 .map_err(|_| RawPoolTransactionError::InvalidTransactionSignature)?,
772 _ => (
773 transaction
774 .recover_signer()
775 .map_err(|_| RawPoolTransactionError::InvalidTransactionSignature)?,
776 None,
777 ),
778 };
779
780 Ok(Self::new_with(
781 Recovered::new_unchecked(transaction, signer),
782 expiring_nonce_hash,
783 encoded_length,
784 ))
785 }
786
787 fn hash(&self) -> &TxHash {
788 self.inner.transaction.tx_hash()
789 }
790
791 fn sender(&self) -> Address {
792 self.inner.transaction.signer()
793 }
794
795 fn sender_ref(&self) -> &Address {
796 self.inner.transaction.signer_ref()
797 }
798
799 fn cost(&self) -> &U256 {
800 &U256::ZERO
801 }
802
803 fn encoded_length(&self) -> usize {
804 self.inner.encoded_length
805 }
806
807 fn requires_nonce_check(&self) -> bool {
808 self.inner
809 .transaction()
810 .as_aa()
811 .map(|tx| {
812 tx.tx().nonce_key.is_zero()
814 })
815 .unwrap_or(true)
816 }
817}
818
819impl alloy_consensus::Transaction for TempoPooledTransaction {
820 fn chain_id(&self) -> Option<u64> {
821 self.inner.chain_id()
822 }
823
824 fn nonce(&self) -> u64 {
825 self.inner.nonce()
826 }
827
828 fn gas_limit(&self) -> u64 {
829 self.inner.gas_limit()
830 }
831
832 fn gas_price(&self) -> Option<u128> {
833 self.inner.gas_price()
834 }
835
836 fn max_fee_per_gas(&self) -> u128 {
837 self.inner.max_fee_per_gas()
838 }
839
840 fn max_priority_fee_per_gas(&self) -> Option<u128> {
841 self.inner.max_priority_fee_per_gas()
842 }
843
844 fn max_fee_per_blob_gas(&self) -> Option<u128> {
845 self.inner.max_fee_per_blob_gas()
846 }
847
848 fn priority_fee_or_price(&self) -> u128 {
849 self.inner.priority_fee_or_price()
850 }
851
852 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
853 self.inner.effective_gas_price(base_fee)
854 }
855
856 fn is_dynamic_fee(&self) -> bool {
857 self.inner.is_dynamic_fee()
858 }
859
860 fn kind(&self) -> TxKind {
861 self.inner.kind()
862 }
863
864 fn is_create(&self) -> bool {
865 self.inner.is_create()
866 }
867
868 fn value(&self) -> U256 {
869 self.inner.value()
870 }
871
872 fn input(&self) -> &Bytes {
873 self.inner.input()
874 }
875
876 fn access_list(&self) -> Option<&AccessList> {
877 self.inner.access_list()
878 }
879
880 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
881 self.inner.blob_versioned_hashes()
882 }
883
884 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
885 self.inner.authorization_list()
886 }
887}
888
889impl EthPoolTransaction for TempoPooledTransaction {
890 fn take_blob(&mut self) -> EthBlobTransactionSidecar {
891 EthBlobTransactionSidecar::None
892 }
893
894 fn try_into_pooled_eip4844(
895 self,
896 _sidecar: Arc<BlobTransactionSidecarVariant>,
897 ) -> Option<Recovered<Self::Pooled>> {
898 None
899 }
900
901 fn try_from_eip4844(
902 _tx: Recovered<Self::Consensus>,
903 _sidecar: BlobTransactionSidecarVariant,
904 ) -> Option<Self> {
905 None
906 }
907
908 fn validate_blob(
909 &self,
910 _sidecar: &BlobTransactionSidecarVariant,
911 _settings: &KzgSettings,
912 ) -> Result<(), BlobTransactionValidationError> {
913 Err(BlobTransactionValidationError::NotBlobTransaction(
914 self.ty(),
915 ))
916 }
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922 use crate::test_utils::TxBuilder;
923 use alloy_consensus::TxEip1559;
924 use alloy_primitives::{Address, Signature, TxKind, address};
925 use alloy_signer::SignerSync;
926 use alloy_signer_local::PrivateKeySigner;
927 use alloy_sol_types::SolCall;
928 use proptest::prelude::*;
929 use proptest_arbitrary_interop::arb_sized;
930 use tempo_contracts::precompiles::ITIP20;
931 use tempo_precompiles::{PATH_USD_ADDRESS, nonce::NonceManager};
932 use tempo_primitives::transaction::{
933 TEMPO_EXPIRING_NONCE_KEY, TempoTransaction,
934 tempo_transaction::Call,
935 tt_signature::{PrimitiveSignature, TempoSignature},
936 tt_signed::AASigned,
937 };
938
939 const TEMPO_TRANSACTION_ARBITRARY_SIZE: usize = 4096;
940
941 fn signed_aa_envelope(tx: TempoTransaction) -> (TempoTxEnvelope, Address) {
942 let signer = PrivateKeySigner::from_bytes(&B256::with_last_byte(1)).unwrap();
943 let signature = signer
944 .sign_hash_sync(&tx.signature_hash())
945 .expect("signing failed");
946 let signed = AASigned::new_unhashed(
947 tx,
948 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(signature)),
949 );
950 (signed.into(), signer.address())
951 }
952
953 fn raw_pooled_transaction(
954 tx: TempoTransaction,
955 ) -> (TempoPooledTransaction, TempoTxEnvelope, Address, usize) {
956 let (envelope, sender) = signed_aa_envelope(tx);
957 let mut raw = Vec::with_capacity(envelope.encode_2718_len());
958 envelope.encode_2718(&mut raw);
959 let encoded_length = raw.len();
960 let pooled = <TempoPooledTransaction as PoolTransaction>::recover_raw_transaction(&raw)
961 .expect("raw transaction recovery failed");
962 (pooled, envelope, sender, encoded_length)
963 }
964
965 #[test]
966 fn test_payment_classification_positive() {
967 let calldata = ITIP20::transferCall {
969 to: Address::random(),
970 amount: U256::random(),
971 }
972 .abi_encode();
973
974 let tx = TxEip1559 {
975 to: TxKind::Call(PATH_USD_ADDRESS),
976 gas_limit: 21000,
977 input: Bytes::from(calldata),
978 ..Default::default()
979 };
980
981 let envelope = TempoTxEnvelope::Eip1559(alloy_consensus::Signed::new_unchecked(
982 tx,
983 Signature::test_signature(),
984 B256::ZERO,
985 ));
986
987 let recovered = Recovered::new_unchecked(
988 envelope,
989 address!("0000000000000000000000000000000000000001"),
990 );
991
992 let pooled_tx = TempoPooledTransaction::new(recovered);
993 assert!(pooled_tx.is_payment());
994 }
995
996 #[test]
997 fn test_payment_classification_tip20_prefix_without_valid_calldata() {
998 let payment_addr = address!("20c0000000000000000000000000000000000001");
1000 let tx = TxEip1559 {
1001 to: TxKind::Call(payment_addr),
1002 gas_limit: 21000,
1003 ..Default::default()
1004 };
1005
1006 let envelope = TempoTxEnvelope::Eip1559(alloy_consensus::Signed::new_unchecked(
1007 tx,
1008 Signature::test_signature(),
1009 B256::ZERO,
1010 ));
1011
1012 let recovered = Recovered::new_unchecked(
1013 envelope,
1014 address!("0000000000000000000000000000000000000001"),
1015 );
1016
1017 let pooled_tx = TempoPooledTransaction::new(recovered);
1018 assert!(!pooled_tx.is_payment());
1019 }
1020
1021 #[test]
1022 fn test_payment_classification_negative() {
1023 let non_payment_addr = Address::random();
1025 let pooled_tx = TxBuilder::eip1559(non_payment_addr)
1026 .gas_limit(21000)
1027 .build_eip1559();
1028 assert!(!pooled_tx.is_payment());
1029 }
1030
1031 #[test]
1032 fn test_fee_token_cost() {
1033 let sender = Address::random();
1034 let value = U256::from(1000);
1035 let tx = TxBuilder::aa(sender)
1036 .gas_limit(1_000_000)
1037 .value(value)
1038 .build();
1039
1040 let expected_fee_cost = U256::from(20000);
1044 assert_eq!(tx.fee_token_cost(), expected_fee_cost);
1045 assert_eq!(tx.fee_token_cost, expected_fee_cost);
1046 assert_eq!(tx.inner.cost, expected_fee_cost + value);
1047 }
1048
1049 #[test]
1050 fn test_non_aa_transaction_helpers() {
1051 let tx = TxBuilder::eip1559(Address::random())
1052 .gas_limit(21000)
1053 .build_eip1559();
1054
1055 assert!(!tx.is_aa(), "Non-AA tx should not be AA");
1057 assert!(
1058 tx.nonce_key().is_none(),
1059 "Non-AA tx should have no nonce key"
1060 );
1061 assert!(
1062 tx.nonce_key_slot().is_none(),
1063 "Non-AA tx should have no nonce key slot"
1064 );
1065 assert!(!tx.is_aa_2d(), "Non-AA tx should not be AA 2D");
1066 assert!(
1067 tx.aa_transaction_id().is_none(),
1068 "Non-AA tx should have no AA transaction ID"
1069 );
1070 }
1071
1072 #[test]
1073 fn test_aa_transaction_with_zero_nonce_key() {
1074 let sender = Address::random();
1075 let nonce = 5u64;
1076 let tx = TxBuilder::aa(sender).nonce(nonce).build();
1077
1078 assert!(tx.is_aa(), "AA tx should be AA");
1079 assert_eq!(
1080 tx.nonce_key(),
1081 Some(U256::ZERO),
1082 "Should have nonce_key = 0"
1083 );
1084 assert!(!tx.is_aa_2d(), "AA tx with nonce_key=0 should NOT be 2D");
1085
1086 let aa_id = tx
1088 .aa_transaction_id()
1089 .expect("Should have AA transaction ID");
1090 assert_eq!(aa_id.seq_id.address, sender);
1091 assert_eq!(aa_id.seq_id.nonce_key, U256::ZERO);
1092 assert_eq!(aa_id.nonce, nonce);
1093 }
1094
1095 #[test]
1096 fn test_aa_transaction_with_nonzero_nonce_key() {
1097 let sender = Address::random();
1098 let nonce_key = U256::from(42);
1099 let nonce = 10u64;
1100 let tx = TxBuilder::aa(sender)
1101 .nonce_key(nonce_key)
1102 .nonce(nonce)
1103 .build();
1104
1105 assert!(tx.is_aa(), "AA tx should be AA");
1106 assert_eq!(
1107 tx.nonce_key(),
1108 Some(nonce_key),
1109 "Should have correct nonce_key"
1110 );
1111 assert!(tx.is_aa_2d(), "AA tx with nonce_key > 0 should be 2D");
1112
1113 let aa_id = tx
1115 .aa_transaction_id()
1116 .expect("Should have AA transaction ID");
1117 assert_eq!(aa_id.seq_id.address, sender);
1118 assert_eq!(aa_id.seq_id.nonce_key, nonce_key);
1119 assert_eq!(aa_id.nonce, nonce);
1120 }
1121
1122 #[test]
1123 fn test_nonce_key_slot_caching_for_2d_tx() {
1124 let sender = Address::random();
1125 let nonce_key = U256::from(123);
1126 let tx = TxBuilder::aa(sender).nonce_key(nonce_key).build();
1127
1128 let expected_slot = NonceManager::new().nonces[sender][nonce_key].slot();
1130
1131 let slot1 = tx.nonce_key_slot();
1133 assert_eq!(slot1, Some(expected_slot));
1134
1135 let slot2 = tx.nonce_key_slot();
1137 assert_eq!(slot2, Some(expected_slot));
1138 assert_eq!(slot1, slot2);
1139 }
1140
1141 #[test]
1142 fn test_is_bad_transaction() {
1143 let cases: &[(TempoPoolTransactionError, bool)] = &[
1144 (TempoPoolTransactionError::ExceedsNonPaymentLimit, false),
1145 (
1146 TempoPoolTransactionError::InvalidValidBefore {
1147 valid_before: 100,
1148 min_allowed: 200,
1149 },
1150 false,
1151 ),
1152 (
1153 TempoPoolTransactionError::InvalidValidAfter {
1154 valid_after: 200,
1155 max_allowed: 100,
1156 },
1157 false,
1158 ),
1159 (TempoPoolTransactionError::Keychain("test error"), false),
1160 (
1161 TempoPoolTransactionError::Evm(TempoInvalidTransaction::NonceManagerError(
1162 "nonce error".to_string(),
1163 )),
1164 false,
1165 ),
1166 (
1167 TempoPoolTransactionError::Evm(TempoInvalidTransaction::FeeTokenNotTip20 {
1168 address: Address::repeat_byte(0x20),
1169 }),
1170 false,
1171 ),
1172 (
1173 TempoPoolTransactionError::Evm(TempoInvalidTransaction::FeeTokenNotUsdCurrency {
1174 address: Address::repeat_byte(0x20),
1175 currency: "EUR".to_string(),
1176 }),
1177 false,
1178 ),
1179 (
1180 TempoPoolTransactionError::Evm(TempoInvalidTransaction::FeeTokenPaused {
1181 address: Address::repeat_byte(0x20),
1182 }),
1183 false,
1184 ),
1185 (
1186 TempoPoolTransactionError::AccessKeyExpired {
1187 expiry: 100,
1188 min_allowed: 200,
1189 },
1190 false,
1191 ),
1192 (
1193 TempoPoolTransactionError::KeyAuthorizationExpired {
1194 expiry: 100,
1195 min_allowed: 200,
1196 },
1197 false,
1198 ),
1199 (TempoPoolTransactionError::SubblockNonceKey, true),
1200 (
1201 TempoPoolTransactionError::Evm(TempoInvalidTransaction::CallsValidation(
1202 "calls error",
1203 )),
1204 true,
1205 ),
1206 ];
1207
1208 for (err, expected) in cases {
1209 assert_eq!(
1210 err.is_bad_transaction(),
1211 *expected,
1212 "Unexpected is_bad_transaction() for: {err}"
1213 );
1214 }
1215 }
1216
1217 #[test]
1218 fn test_requires_nonce_check() {
1219 let cases: &[(TempoPooledTransaction, bool, &str)] = &[
1220 (
1221 TxBuilder::eip1559(Address::random())
1222 .gas_limit(21000)
1223 .build_eip1559(),
1224 true,
1225 "Non-AA should require nonce check",
1226 ),
1227 (
1228 TxBuilder::aa(Address::random()).build(),
1229 true,
1230 "AA with nonce_key=0 should require nonce check",
1231 ),
1232 (
1233 TxBuilder::aa(Address::random())
1234 .nonce_key(U256::from(1))
1235 .build(),
1236 false,
1237 "AA with nonce_key > 0 should NOT require nonce check",
1238 ),
1239 ];
1240
1241 for (tx, expected, msg) in cases {
1242 assert_eq!(tx.requires_nonce_check(), *expected, "{msg}");
1243 }
1244 }
1245
1246 #[test]
1247 fn test_validate_blob_returns_not_blob_transaction() {
1248 use alloy_eips::eip7594::BlobTransactionSidecarVariant;
1249
1250 let tx = TxBuilder::eip1559(Address::random())
1251 .gas_limit(21000)
1252 .build_eip1559();
1253
1254 let sidecar = BlobTransactionSidecarVariant::Eip4844(Default::default());
1256 let settings = alloy_eips::eip4844::env_settings::EnvKzgSettings::Default.get();
1258
1259 let result = tx.validate_blob(&sidecar, settings);
1260
1261 assert!(matches!(
1262 result,
1263 Err(BlobTransactionValidationError::NotBlobTransaction(ty)) if ty == tx.ty()
1264 ));
1265 }
1266
1267 #[test]
1268 fn test_take_blob_returns_none() {
1269 let mut tx = TxBuilder::eip1559(Address::random())
1270 .gas_limit(21000)
1271 .build_eip1559();
1272 let blob = tx.take_blob();
1273 assert!(matches!(blob, EthBlobTransactionSidecar::None));
1274 }
1275
1276 #[test]
1277 fn test_pool_transaction_hash_and_sender() {
1278 let sender = Address::random();
1279 let tx = TxBuilder::aa(sender).build();
1280
1281 assert!(!tx.hash().is_zero(), "Hash should not be zero");
1282 assert_eq!(tx.sender(), sender);
1283 assert_eq!(tx.sender_ref(), &sender);
1284 }
1285
1286 #[test]
1287 fn test_pool_transaction_clone_into_consensus() {
1288 let sender = Address::random();
1289 let tx = TxBuilder::aa(sender).build();
1290 let hash = *tx.hash();
1291
1292 let cloned = tx.clone_into_consensus();
1293 assert_eq!(cloned.tx_hash(), &hash);
1294 assert_eq!(cloned.signer(), sender);
1295 }
1296
1297 #[test]
1298 fn test_pool_transaction_into_consensus() {
1299 let sender = Address::random();
1300 let tx = TxBuilder::aa(sender).build();
1301 let hash = *tx.hash();
1302
1303 let consensus = tx.into_consensus();
1304 assert_eq!(consensus.tx_hash(), &hash);
1305 assert_eq!(consensus.signer(), sender);
1306 }
1307
1308 #[test]
1309 fn test_pool_transaction_from_pooled() {
1310 let sender = Address::random();
1311 let nonce = 42u64;
1312 let aa_tx = TempoTransaction {
1313 chain_id: 1,
1314 max_priority_fee_per_gas: 1_000_000_000,
1315 max_fee_per_gas: 20_000_000_000,
1316 gas_limit: 1_000_000,
1317 calls: vec![Call {
1318 to: TxKind::Call(Address::random()),
1319 value: U256::ZERO,
1320 input: Default::default(),
1321 }],
1322 nonce_key: U256::ZERO,
1323 nonce,
1324 ..Default::default()
1325 };
1326
1327 let signature =
1328 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
1329 let aa_signed = AASigned::new_unhashed(aa_tx, signature);
1330 let envelope: TempoTxEnvelope = aa_signed.into();
1331 let recovered = Recovered::new_unchecked(envelope, sender);
1332
1333 let pooled = TempoPooledTransaction::from_pooled(recovered);
1334 assert_eq!(pooled.sender(), sender);
1335 assert_eq!(pooled.nonce(), nonce);
1336 }
1337
1338 proptest! {
1339 #[test]
1340 fn proptest_recover_raw_transaction_precomputes_expiring_nonce_hash(
1341 mut tx in arb_sized::<TempoTransaction>(TEMPO_TRANSACTION_ARBITRARY_SIZE)
1342 ) {
1343 tx.nonce_key = TEMPO_EXPIRING_NONCE_KEY;
1344 let (pooled, envelope, sender, encoded_length) = raw_pooled_transaction(tx);
1345 let expected = envelope.as_aa().unwrap().expiring_nonce_hash(sender);
1346 let via_new = TempoPooledTransaction::new(Recovered::new_unchecked(envelope, sender));
1347
1348 prop_assert!(pooled.is_expiring_nonce());
1349 prop_assert_eq!(pooled.encoded_length(), encoded_length);
1350 prop_assert_eq!(pooled.expiring_nonce_hash, Some(expected));
1351 prop_assert_eq!(pooled.expiring_nonce_hash(), via_new.expiring_nonce_hash());
1352 prop_assert_eq!(pooled.hash(), via_new.hash());
1353 prop_assert_eq!(pooled.sender(), via_new.sender());
1354 }
1355
1356 #[test]
1357 fn proptest_recover_raw_transaction_matches_new_for_non_expiring_aa(
1358 mut tx in arb_sized::<TempoTransaction>(TEMPO_TRANSACTION_ARBITRARY_SIZE)
1359 ) {
1360 tx.nonce_key = U256::ZERO;
1361 let (pooled, envelope, sender, encoded_length) = raw_pooled_transaction(tx);
1362 let via_new = TempoPooledTransaction::new(Recovered::new_unchecked(envelope, sender));
1363
1364 prop_assert!(!pooled.is_expiring_nonce());
1365 prop_assert_eq!(pooled.encoded_length(), encoded_length);
1366 prop_assert_eq!(pooled.expiring_nonce_hash, None);
1367 prop_assert_eq!(pooled.expiring_nonce_hash(), via_new.expiring_nonce_hash());
1368 prop_assert_eq!(pooled.hash(), via_new.hash());
1369 prop_assert_eq!(pooled.sender(), via_new.sender());
1370 }
1371 }
1372
1373 #[test]
1374 fn test_transaction_trait_forwarding() {
1375 let sender = Address::random();
1376 let tx = TxBuilder::aa(sender)
1377 .gas_limit(1_000_000)
1378 .value(U256::from(500))
1379 .build();
1380
1381 assert_eq!(tx.chain_id(), Some(42431));
1383 assert_eq!(tx.nonce(), 0);
1384 assert_eq!(tx.gas_limit(), 1_000_000);
1385 assert_eq!(tx.max_fee_per_gas(), 20_000_000_000);
1386 assert_eq!(tx.max_priority_fee_per_gas(), Some(1_000_000_000));
1387 assert!(tx.is_dynamic_fee());
1388 assert!(!tx.is_create());
1389 }
1390
1391 #[test]
1392 fn test_cost_returns_zero() {
1393 let tx = TxBuilder::aa(Address::random())
1394 .gas_limit(1_000_000)
1395 .value(U256::from(1000))
1396 .build();
1397
1398 assert_eq!(*tx.cost(), U256::ZERO);
1400 }
1401}
1402
1403#[derive(Debug, Clone, Default)]
1412pub struct RevokedKeys {
1413 by_account: AddressMap<Vec<Address>>,
1415}
1416
1417impl RevokedKeys {
1418 pub fn new() -> Self {
1420 Self::default()
1421 }
1422
1423 pub fn insert(&mut self, account: Address, key_id: Address) {
1425 self.by_account.entry(account).or_default().push(key_id);
1426 }
1427
1428 pub fn is_empty(&self) -> bool {
1430 self.by_account.is_empty()
1431 }
1432
1433 pub fn len(&self) -> usize {
1435 self.by_account.values().map(Vec::len).sum()
1436 }
1437
1438 pub fn contains(&self, account: Address, key_id: Address) -> bool {
1440 self.by_account
1441 .get(&account)
1442 .is_some_and(|key_ids| key_ids.contains(&key_id))
1443 }
1444}
1445
1446#[derive(Debug, Clone, Default)]
1451pub struct SpendingLimitUpdates {
1452 by_account: AddressMap<Vec<(Address, Option<Address>)>>,
1455}
1456
1457impl SpendingLimitUpdates {
1458 pub fn new() -> Self {
1460 Self::default()
1461 }
1462
1463 pub fn insert(&mut self, account: Address, key_id: Address, token: Option<Address>) {
1465 self.by_account
1466 .entry(account)
1467 .or_default()
1468 .push((key_id, token));
1469 }
1470
1471 pub fn is_empty(&self) -> bool {
1473 self.by_account.is_empty()
1474 }
1475
1476 pub fn len(&self) -> usize {
1478 self.by_account.values().map(Vec::len).sum()
1479 }
1480
1481 pub fn contains(&self, account: Address, key_id: Address, token: Address) -> bool {
1486 self.by_account
1487 .get(&account)
1488 .is_some_and(|pairs: &Vec<(Address, Option<Address>)>| {
1489 pairs
1490 .iter()
1491 .any(|&(k, t)| k == key_id && t.is_none_or(|t| t == token))
1492 })
1493 }
1494}
1495
1496#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1501pub struct KeychainSubject {
1502 pub account: Address,
1504 pub key_id: Address,
1506 pub fee_token: Address,
1508}
1509
1510impl KeychainSubject {
1511 pub fn matches_revoked(&self, revoked_keys: &RevokedKeys) -> bool {
1516 revoked_keys.contains(self.account, self.key_id)
1517 }
1518
1519 pub fn matches_spending_limit_update(
1524 &self,
1525 spending_limit_updates: &SpendingLimitUpdates,
1526 ) -> bool {
1527 spending_limit_updates.contains(self.account, self.key_id, self.fee_token)
1528 }
1529}
1530
1531#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1533pub struct KeyAuthorizationWitnessSubject {
1534 pub account: Address,
1536 pub witness: B256,
1538}
1539
1540#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1542pub struct KeyAuthorizationTargetSubject {
1543 pub account: Address,
1545 pub key_id: Address,
1547}
1548
1549impl KeyAuthorizationTargetSubject {
1550 pub fn matches_key_update(&self, key_updates: &RevokedKeys) -> bool {
1552 key_updates.contains(self.account, self.key_id)
1553 }
1554}