1use std::{collections::VecDeque, sync::Arc};
2
3use alloy_consensus::BlockHeader;
4use alloy_primitives::{
5 Address, U256,
6 map::{AddressMap, HashMap, U256Map},
7};
8use itertools::Itertools;
9use parking_lot::RwLock;
10use reth_primitives_traits::SealedHeader;
11use reth_provider::{
12 ChainSpecProvider, ExecutionOutcome, HeaderProvider, ProviderError, ProviderResult,
13 StateProviderFactory,
14};
15use tempo_chainspec::{
16 TempoChainSpec,
17 hardfork::{TempoHardfork, TempoHardforks},
18};
19use tempo_evm::TempoStateAccess;
20use tempo_precompiles::{
21 DEFAULT_FEE_TOKEN, TIP_FEE_MANAGER_ADDRESS,
22 error::Result as TempoResult,
23 storage::StorageActions,
24 tip_fee_manager::{
25 TipFeeManager,
26 amm::{Pool, compute_amount_out},
27 },
28 tip20,
29};
30use tempo_primitives::{TempoHeader, TempoReceipt};
31use tempo_revm::IntoAddress;
32
33const LAST_SEEN_WINDOW: usize = 10;
35
36#[derive(Debug, Clone)]
37pub struct AmmLiquidityCache {
38 inner: Arc<RwLock<AmmLiquidityCacheInner>>,
39}
40
41impl AmmLiquidityCache {
42 pub fn new<Client>(client: Client) -> ProviderResult<Self>
45 where
46 Client: StateProviderFactory
47 + HeaderProvider<Header = TempoHeader>
48 + ChainSpecProvider<ChainSpec = TempoChainSpec>,
49 {
50 let this = Self {
51 inner: Default::default(),
52 };
53 this.repopulate(&client)?;
54
55 Ok(this)
56 }
57
58 pub fn has_enough_liquidity<S, M>(
64 &self,
65 user_token: Address,
66 fee: U256,
67 mut state_provider: S,
68 ) -> Result<bool, ProviderError>
69 where
70 S: TempoStateAccess<M>,
71 {
72 let mut missing_in_cache = Vec::new();
73 let hardfork;
74
75 {
77 let inner = self.inner.read();
78 hardfork = inner.hardfork;
79
80 let calc_swap = |input| compute_amount_out(input).map_err(ProviderError::other);
81 let out1 = calc_swap(fee)?;
82 let out2 = hardfork.is_t5().then(|| calc_swap(out1)).transpose()?;
83
84 for &validator_token in &inner.unique_tokens {
85 if validator_token == user_token {
87 return Ok(true);
88 }
89
90 let direct = inner
91 .pool_cache
92 .get(&(user_token, validator_token))
93 .copied();
94
95 let mut defer = match direct {
96 Some(r) if r >= out1 => return Ok(true),
97 Some(_) => false, None => true, };
100
101 if let Some(out2) = out2 {
102 if let Some(hop) = inner.quote_token_cache.get(&user_token).copied() {
103 if !hop.is_zero() && hop != validator_token {
104 let r1 = inner.pool_cache.get(&(user_token, hop)).copied();
105 let r2 = inner.pool_cache.get(&(hop, validator_token)).copied();
106 match (r1, r2) {
107 (Some(r1), Some(r2)) if r1 >= out1 && r2 >= out2 => {
108 return Ok(true);
109 }
110 (Some(_), Some(_)) => {} _ => defer = true, }
113 }
114 } else {
115 defer = true; }
117 }
118
119 if defer {
120 missing_in_cache.push(validator_token);
121 }
122 }
123 }
124
125 if missing_in_cache.is_empty() {
126 return Ok(false);
127 }
128
129 state_provider
132 .with_read_only_storage_ctx(
133 hardfork,
134 StorageActions::disabled(),
135 || -> TempoResult<bool> {
136 let manager = TipFeeManager::new();
137 for validator_token in missing_in_cache {
138 let (route, intermediate, pools) =
139 manager.plan_fee_route(user_token, validator_token, fee)?;
140 if !pools.is_empty() || intermediate.is_some() {
141 let mut inner = self.inner.write();
142 for &(pair, reserve) in &pools {
143 let id = manager.pool_id(pair.0, pair.1);
144 let slot = manager.pools[id].base_slot();
145 inner.pool_cache.insert(pair, U256::from(reserve));
146 inner.slot_to_pool.insert(slot, pair);
147 }
148 if let Some(hop) = intermediate {
149 inner.quote_token_cache.insert(user_token, hop);
150 }
151 }
152 if route.is_some() {
154 return Ok(true);
155 }
156 }
157
158 Ok(false)
159 },
160 )
161 .map_err(ProviderError::other)
162 }
163
164 pub fn clear(&self) {
167 *self.inner.write() = AmmLiquidityCacheInner::default();
168 }
169
170 pub fn repopulate<Client>(&self, client: &Client) -> ProviderResult<()>
175 where
176 Client: StateProviderFactory
177 + HeaderProvider<Header = TempoHeader>
178 + ChainSpecProvider<ChainSpec = TempoChainSpec>,
179 {
180 self.clear();
181 let tip = client.best_block_number()?;
182 let headers =
183 client.sealed_headers_range(tip.saturating_sub(LAST_SEEN_WINDOW as u64 + 1)..=tip)?;
184 self.on_new_blocks(headers.iter(), client)
185 }
186
187 pub fn on_new_state(&self, execution_outcome: &ExecutionOutcome<TempoReceipt>) {
193 let mut inner = self.inner.write();
194
195 if let Some(storage) = execution_outcome
197 .account_state(&TIP_FEE_MANAGER_ADDRESS)
198 .map(|acc| &acc.storage)
199 {
200 for (slot, value) in storage.iter() {
201 if let Some(pool) = inner.slot_to_pool.get(slot).copied() {
202 let validator_reserve = U256::from(
204 Pool::decode_from_slot(value.present_value).reserve_validator_token,
205 );
206 inner.pool_cache.insert(pool, validator_reserve);
207 } else if let Some(validator) = inner.slot_to_validator.get(slot).copied() {
208 inner
210 .validator_preferences
211 .insert(validator, value.present_value().into_address());
212 }
213 }
214 }
215
216 inner.quote_token_cache.retain(|token, _| {
218 execution_outcome
219 .account_state(token)
220 .and_then(|acc| acc.storage.get(&tip20::slots::QUOTE_TOKEN))
221 .is_none()
222 });
223 }
224
225 pub fn on_new_blocks<'a, P>(
227 &self,
228 headers: impl IntoIterator<Item = &'a SealedHeader<TempoHeader>>,
229 client: P,
230 ) -> ProviderResult<()>
231 where
232 P: StateProviderFactory + ChainSpecProvider<ChainSpec: TempoHardforks>,
233 {
234 let headers = headers.into_iter().collect::<Vec<_>>();
235 let (latest_hash, latest_timestamp) = if let Some(header) = headers.last() {
236 (header.hash(), header.timestamp())
237 } else {
238 return Ok(());
239 };
240
241 let mut state = None;
242
243 for header in headers {
244 let beneficiary = header.beneficiary();
245 let validator_token_slot = TipFeeManager::new().validator_tokens[beneficiary].slot();
246
247 let cached_preference = self
248 .inner
249 .read()
250 .validator_preferences
251 .get(&beneficiary)
252 .copied();
253
254 let preference = if let Some(cached) = cached_preference {
255 cached
256 } else {
257 if state.is_none() {
261 state = Some(client.state_by_block_hash(latest_hash)?);
262 }
263
264 state
265 .as_mut()
266 .expect("initialized above")
267 .storage(TIP_FEE_MANAGER_ADDRESS, validator_token_slot.into())?
268 .unwrap_or_default()
269 .into_address()
270 };
271
272 let fee_token = if preference.is_zero() {
274 DEFAULT_FEE_TOKEN
275 } else {
276 preference
277 };
278
279 let mut inner = self.inner.write();
280
281 if cached_preference.is_none() {
283 inner.validator_preferences.insert(beneficiary, preference);
284 inner
285 .slot_to_validator
286 .insert(validator_token_slot, beneficiary);
287 }
288
289 inner.last_seen_tokens.push_back(fee_token);
291 if inner.last_seen_tokens.len() > LAST_SEEN_WINDOW {
292 inner.last_seen_tokens.pop_front();
293 }
294 inner.unique_tokens = inner.last_seen_tokens.iter().copied().unique().collect();
295
296 inner.last_seen_validators.push_back(beneficiary);
298 if inner.last_seen_validators.len() > LAST_SEEN_WINDOW {
299 inner.last_seen_validators.pop_front();
300 }
301 inner.unique_validators = inner
302 .last_seen_validators
303 .iter()
304 .copied()
305 .unique()
306 .collect();
307 }
308
309 self.inner.write().hardfork = client.chain_spec().tempo_hardfork_at(latest_timestamp);
311
312 Ok(())
313 }
314}
315
316#[derive(Debug, Default)]
317struct AmmLiquidityCacheInner {
318 hardfork: TempoHardfork,
320
321 pool_cache: HashMap<(Address, Address), U256>,
323
324 quote_token_cache: AddressMap<Address>,
326
327 slot_to_pool: U256Map<(Address, Address)>,
329
330 last_seen_tokens: VecDeque<Address>,
332
333 unique_tokens: Vec<Address>,
335
336 last_seen_validators: VecDeque<Address>,
338
339 unique_validators: Vec<Address>,
341
342 validator_preferences: AddressMap<Address>,
344
345 slot_to_validator: U256Map<Address>,
347}
348
349impl AmmLiquidityCache {
350 pub fn is_active_validator(&self, validator: &Address) -> bool {
356 self.inner.read().unique_validators.contains(validator)
357 }
358
359 pub fn is_active_validator_token(&self, token: &Address) -> bool {
362 self.inner.read().unique_tokens.contains(token)
363 }
364
365 pub fn track_tokens(&self, tokens: &[Address]) -> bool {
371 let mut updated = false;
372 if tokens.is_empty() {
373 return updated;
374 }
375
376 let mut inner = self.inner.write();
377 for &token in tokens {
378 if !inner.unique_tokens.contains(&token) {
379 inner.unique_tokens.push(token);
380 updated = true;
381 }
382 }
383 updated
384 }
385}
386
387#[cfg(any(test, feature = "test-utils"))]
388impl AmmLiquidityCache {
389 pub fn with_unique_tokens(unique_tokens: Vec<Address>) -> Self {
391 Self {
392 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
393 unique_tokens,
394 ..Default::default()
395 })),
396 }
397 }
398
399 pub fn with_unique_validators(unique_validators: Vec<Address>) -> Self {
401 Self {
402 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
403 unique_validators,
404 ..Default::default()
405 })),
406 }
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::test_utils::create_mock_provider;
414 use alloy_primitives::address;
415
416 #[test]
421 fn test_has_enough_liquidity_user_token_matches_validator_token() {
422 let cache = AmmLiquidityCache {
423 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
424 unique_tokens: vec![address!("1111111111111111111111111111111111111111")],
425 ..Default::default()
426 })),
427 };
428
429 let provider = create_mock_provider();
430 let state = provider.latest().unwrap();
431
432 let user_token = address!("1111111111111111111111111111111111111111");
433 let result = cache.has_enough_liquidity(user_token, U256::from(100), &state);
434
435 assert!(result.is_ok());
436 assert!(
437 result.unwrap(),
438 "Should return true when user token matches validator token"
439 );
440 }
441
442 #[test]
443 fn test_has_enough_liquidity_cached_pool_sufficient() {
444 let user_token = address!("2222222222222222222222222222222222222222");
445 let validator_token = address!("3333333333333333333333333333333333333333");
446
447 let cache = AmmLiquidityCache {
448 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
449 unique_tokens: vec![validator_token],
450 pool_cache: {
451 let mut m = HashMap::default();
452 m.insert((user_token, validator_token), U256::MAX);
453 m
454 },
455 ..Default::default()
456 })),
457 };
458
459 let provider = create_mock_provider();
460 let state = provider.latest().unwrap();
461
462 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &state);
463 assert!(result.is_ok());
464 assert!(
465 result.unwrap(),
466 "Should return true for sufficient cached reserve"
467 );
468 }
469
470 #[test]
471 fn test_has_enough_liquidity_cached_pool_insufficient() {
472 let user_token = address!("2222222222222222222222222222222222222222");
473 let validator_token = address!("3333333333333333333333333333333333333333");
474
475 let cache = AmmLiquidityCache {
476 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
477 unique_tokens: vec![validator_token],
478 pool_cache: {
479 let mut m = HashMap::default();
480 m.insert((user_token, validator_token), U256::ZERO);
481 m
482 },
483 ..Default::default()
484 })),
485 };
486
487 let provider = create_mock_provider();
488 let state = provider.latest().unwrap();
489
490 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &state);
491 assert!(result.is_ok());
492 assert!(
493 !result.unwrap(),
494 "Should return false for insufficient cached reserve"
495 );
496 }
497
498 #[test]
499 fn test_has_enough_liquidity_no_unique_tokens() {
500 let cache = AmmLiquidityCache {
501 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner::default())),
502 };
503
504 let provider = create_mock_provider();
505 let state = provider.latest().unwrap();
506
507 let user_token = address!("1111111111111111111111111111111111111111");
508 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &state);
509 assert!(result.is_ok());
510 assert!(
511 !result.unwrap(),
512 "Should return false when no unique tokens"
513 );
514 }
515
516 #[test]
517 fn test_has_enough_liquidity_two_hop_cached() {
518 let user = address!("1111111111111111111111111111111111111111");
519 let hop = address!("2222222222222222222222222222222222222222");
520 let validator = address!("3333333333333333333333333333333333333333");
521
522 let cache = AmmLiquidityCache {
523 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
524 hardfork: TempoHardfork::T5,
525 unique_tokens: vec![validator],
526 pool_cache: {
527 let mut m = HashMap::default();
528 m.insert((user, hop), U256::from(1_000_000));
530 m.insert((hop, validator), U256::from(1_000_000));
531 m
532 },
533 quote_token_cache: {
534 let mut m = AddressMap::default();
535 m.insert(user, hop);
536 m
537 },
538 ..Default::default()
539 })),
540 };
541
542 let provider = create_mock_provider();
545 let state = provider.latest().unwrap();
546
547 let result = cache.has_enough_liquidity(user, U256::from(100), &state);
548 assert!(result.is_ok());
549 assert!(
550 result.unwrap(),
551 "two-hop primitives cached should resolve from hot path",
552 );
553 }
554
555 #[test]
556 fn test_has_enough_liquidity_cache_miss_insufficient() {
557 let user_token = address!("2222222222222222222222222222222222222222");
558 let validator_token = address!("3333333333333333333333333333333333333333");
559
560 let cache = AmmLiquidityCache {
561 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
562 unique_tokens: vec![validator_token],
563 pool_cache: HashMap::default(),
564 ..Default::default()
565 })),
566 };
567
568 let provider = create_mock_provider();
569 let state = provider.latest().unwrap();
570
571 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &state);
573 assert!(result.is_ok());
574 assert!(
575 !result.unwrap(),
576 "Should return false for insufficient reserve"
577 );
578
579 let inner = cache.inner.read();
582 assert_eq!(
583 inner.pool_cache.get(&(user_token, validator_token)),
584 Some(&U256::ZERO),
585 "failed direct check should still warm pool_cache",
586 );
587 assert!(
588 !inner.slot_to_pool.is_empty(),
589 "slot_to_pool reverse index should be populated for the check pool",
590 );
591 }
592
593 #[test]
598 fn test_on_new_state_early_return_no_fee_manager_account() {
599 use reth_provider::ExecutionOutcome;
600 use tempo_primitives::TempoReceipt;
601
602 let cache = AmmLiquidityCache {
603 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner::default())),
604 };
605
606 let execution_outcome: ExecutionOutcome<TempoReceipt> = ExecutionOutcome::default();
607 cache.on_new_state(&execution_outcome);
608
609 let inner = cache.inner.read();
610 assert!(inner.pool_cache.is_empty());
611 assert!(inner.quote_token_cache.is_empty());
612 assert!(inner.validator_preferences.is_empty());
613 }
614
615 #[test]
616 fn test_on_new_state_invalidates_stale_quote_token_cache() {
617 use reth_provider::ExecutionOutcome;
618 use revm::database::{AccountStatus, BundleAccount, BundleState, states::StorageSlot};
619 use tempo_primitives::TempoReceipt;
620
621 let user_token = address!("20c0000000000000000000000000000000000001");
623 let hop_old = address!("20c0000000000000000000000000000000000002");
624 let hop_new = address!("20c0000000000000000000000000000000000003");
625 let other_user = address!("20c0000000000000000000000000000000000099");
626
627 let cache = AmmLiquidityCache {
628 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
629 quote_token_cache: {
630 let mut m = AddressMap::default();
631 m.insert(user_token, hop_old);
632 m.insert(other_user, hop_old);
633 m
634 },
635 ..Default::default()
636 })),
637 };
638
639 let mut storage = HashMap::default();
641 storage.insert(
642 tip20::slots::QUOTE_TOKEN,
643 StorageSlot::new_changed(hop_old.into_word().into(), hop_new.into_word().into()),
644 );
645 let mut bundle_state = AddressMap::default();
646 bundle_state.insert(
647 user_token,
648 BundleAccount::new(None, None, storage, AccountStatus::Changed),
649 );
650 let bundle = BundleState {
651 state: bundle_state,
652 ..Default::default()
653 };
654 let execution_outcome: ExecutionOutcome<TempoReceipt> = ExecutionOutcome {
655 bundle,
656 ..Default::default()
657 };
658
659 cache.on_new_state(&execution_outcome);
660
661 let inner = cache.inner.read();
662 assert!(
663 !inner.quote_token_cache.contains_key(&user_token),
664 "stale quote_token_cache entry must be dropped on slot write",
665 );
666 assert_eq!(
667 inner.quote_token_cache.get(&other_user),
668 Some(&hop_old),
669 "untouched user tokens must keep their cached intermediate",
670 );
671 }
672
673 #[test]
678 fn test_sliding_window_max_size() {
679 let mut inner = AmmLiquidityCacheInner::default();
680
681 for i in 0..LAST_SEEN_WINDOW {
682 let token = Address::new([i as u8; 20]);
683 inner.last_seen_tokens.push_back(token);
684 }
685
686 assert_eq!(inner.last_seen_tokens.len(), LAST_SEEN_WINDOW);
687
688 let new_token = Address::new([0xFF; 20]);
689 inner.last_seen_tokens.push_back(new_token);
690 if inner.last_seen_tokens.len() > LAST_SEEN_WINDOW {
691 inner.last_seen_tokens.pop_front();
692 }
693
694 assert_eq!(inner.last_seen_tokens.len(), LAST_SEEN_WINDOW);
695 assert_eq!(inner.last_seen_tokens.back(), Some(&new_token));
696 assert_eq!(inner.last_seen_tokens.front(), Some(&Address::new([1; 20])));
697 }
698
699 #[test]
700 fn test_sliding_window_validators() {
701 let mut inner = AmmLiquidityCacheInner::default();
702
703 for i in 0..LAST_SEEN_WINDOW {
704 let validator = Address::new([i as u8; 20]);
705 inner.last_seen_validators.push_back(validator);
706 }
707
708 assert_eq!(inner.last_seen_validators.len(), LAST_SEEN_WINDOW);
709
710 let new_validator = Address::new([0xFF; 20]);
711 inner.last_seen_validators.push_back(new_validator);
712 if inner.last_seen_validators.len() > LAST_SEEN_WINDOW {
713 inner.last_seen_validators.pop_front();
714 }
715
716 assert_eq!(inner.last_seen_validators.len(), LAST_SEEN_WINDOW);
717 assert_eq!(inner.last_seen_validators.back(), Some(&new_validator));
718 assert_eq!(
719 inner.last_seen_validators.front(),
720 Some(&Address::new([1; 20]))
721 );
722
723 inner.unique_validators = inner
724 .last_seen_validators
725 .iter()
726 .copied()
727 .unique()
728 .collect();
729 assert!(inner.unique_validators.contains(&new_validator));
730 }
731
732 #[test]
733 fn test_unique_tokens_deduplication() {
734 let mut inner = AmmLiquidityCacheInner::default();
735
736 let token_a = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
737 let token_b = address!("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
738
739 inner.last_seen_tokens.push_back(token_a);
740 inner.last_seen_tokens.push_back(token_b);
741 inner.last_seen_tokens.push_back(token_b);
742 inner.last_seen_tokens.push_back(token_b);
743
744 inner.unique_tokens = inner.last_seen_tokens.iter().copied().unique().collect();
745
746 assert_eq!(inner.unique_tokens.len(), 2, "duplicates must be removed");
747 assert_eq!(inner.unique_tokens[0], token_a);
748 assert_eq!(inner.unique_tokens[1], token_b);
749 }
750
751 #[test]
756 fn test_cache_insert_and_lookup() {
757 let mut inner = AmmLiquidityCacheInner::default();
758
759 let user_token = address!("1111111111111111111111111111111111111111");
760 let validator_token = address!("2222222222222222222222222222222222222222");
761 let reserve = U256::from(5000);
762
763 inner
764 .pool_cache
765 .insert((user_token, validator_token), reserve);
766
767 assert_eq!(
768 inner.pool_cache.get(&(user_token, validator_token)),
769 Some(&reserve)
770 );
771 }
772
773 #[test]
774 fn test_slot_to_pool_mapping() {
775 let mut inner = AmmLiquidityCacheInner::default();
776
777 let user_token = address!("1111111111111111111111111111111111111111");
778 let validator_token = address!("2222222222222222222222222222222222222222");
779 let slot = U256::from(12345);
780
781 inner
782 .slot_to_pool
783 .insert(slot, (user_token, validator_token));
784
785 assert_eq!(
786 inner.slot_to_pool.get(&slot),
787 Some(&(user_token, validator_token))
788 );
789 }
790
791 #[test]
792 fn test_validator_preferences_mapping() {
793 let mut inner = AmmLiquidityCacheInner::default();
794
795 let validator = address!("3333333333333333333333333333333333333333");
796 let fee_token = address!("4444444444444444444444444444444444444444");
797
798 inner.validator_preferences.insert(validator, fee_token);
799
800 assert_eq!(
801 inner.validator_preferences.get(&validator),
802 Some(&fee_token)
803 );
804 }
805
806 #[test]
807 fn test_slot_to_validator_mapping() {
808 let mut inner = AmmLiquidityCacheInner::default();
809
810 let validator = address!("3333333333333333333333333333333333333333");
811 let slot = U256::from(67890);
812
813 inner.slot_to_validator.insert(slot, validator);
814
815 assert_eq!(inner.slot_to_validator.get(&slot), Some(&validator));
816 }
817
818 #[test]
819 fn test_clear_resets_all_state() {
820 let user_token = Address::random();
821 let validator_token = Address::random();
822 let validator = Address::random();
823
824 let cache = AmmLiquidityCache {
825 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
826 pool_cache: {
827 let mut m = HashMap::default();
828 m.insert((user_token, validator_token), U256::from(1000));
829 m
830 },
831 quote_token_cache: {
832 let mut m = AddressMap::default();
833 m.insert(user_token, validator_token);
834 m
835 },
836 slot_to_pool: {
837 let mut m = U256Map::default();
838 m.insert(U256::from(1), (user_token, validator_token));
839 m
840 },
841 last_seen_tokens: VecDeque::from(vec![validator_token]),
842 unique_tokens: vec![validator_token],
843 last_seen_validators: VecDeque::from(vec![validator]),
844 unique_validators: vec![validator],
845 validator_preferences: {
846 let mut m = AddressMap::default();
847 m.insert(validator, validator_token);
848 m
849 },
850 slot_to_validator: {
851 let mut m = U256Map::default();
852 m.insert(U256::from(2), validator);
853 m
854 },
855 ..Default::default()
856 })),
857 };
858
859 cache.clear();
860
861 let inner = cache.inner.read();
862 assert!(
863 inner.pool_cache.is_empty(),
864 "pools should be empty after clear"
865 );
866 assert!(
867 inner.quote_token_cache.is_empty(),
868 "quote_tokens should be empty after clear"
869 );
870 assert!(
871 inner.slot_to_pool.is_empty(),
872 "slot_to_pool should be empty after clear"
873 );
874 assert!(
875 inner.last_seen_tokens.is_empty(),
876 "last_seen_tokens should be empty after clear"
877 );
878 assert!(
879 inner.unique_tokens.is_empty(),
880 "unique_tokens should be empty after clear"
881 );
882 assert!(
883 inner.last_seen_validators.is_empty(),
884 "last_seen_validators should be empty after clear"
885 );
886 assert!(
887 inner.unique_validators.is_empty(),
888 "unique_validators should be empty after clear"
889 );
890 assert!(
891 inner.validator_preferences.is_empty(),
892 "validator_preferences should be empty after clear"
893 );
894 assert!(
895 inner.slot_to_validator.is_empty(),
896 "slot_to_validator should be empty after clear"
897 );
898 }
899
900 #[test]
901 fn test_repopulate_clears_stale_data_and_rebuilds_from_canonical_chain() {
902 use alloy_consensus::Header;
903
904 let stale_validator = Address::random();
905 let stale_token = Address::random();
906 let stale_user_token = Address::random();
907
908 let cache = AmmLiquidityCache {
909 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
910 pool_cache: {
911 let mut m = HashMap::default();
912 m.insert((stale_user_token, stale_token), U256::from(9999));
913 m
914 },
915 slot_to_pool: {
916 let mut m = U256Map::default();
917 m.insert(U256::from(42), (stale_user_token, stale_token));
918 m
919 },
920 last_seen_tokens: VecDeque::from(vec![stale_token]),
921 unique_tokens: vec![stale_token],
922 last_seen_validators: VecDeque::from(vec![stale_validator]),
923 unique_validators: vec![stale_validator],
924 validator_preferences: {
925 let mut m = AddressMap::default();
926 m.insert(stale_validator, stale_token);
927 m
928 },
929 slot_to_validator: {
930 let mut m = U256Map::default();
931 m.insert(U256::from(99), stale_validator);
932 m
933 },
934 ..Default::default()
935 })),
936 };
937
938 {
939 let inner = cache.inner.read();
940 assert!(inner.unique_validators.contains(&stale_validator));
941 assert!(inner.unique_tokens.contains(&stale_token));
942 assert_eq!(
943 inner.pool_cache.get(&(stale_user_token, stale_token)),
944 Some(&U256::from(9999))
945 );
946 }
947
948 let new_validator = Address::random();
949 let provider = create_mock_provider();
950 for i in 0..3u64 {
951 let header = TempoHeader {
952 inner: Header {
953 number: i,
954 beneficiary: new_validator,
955 ..Default::default()
956 },
957 ..Default::default()
958 };
959 provider.add_header(alloy_primitives::B256::random(), header);
960 }
961
962 cache
963 .repopulate(&provider)
964 .expect("repopulate should succeed");
965
966 let inner = cache.inner.read();
967
968 assert!(
969 !inner.unique_validators.contains(&stale_validator),
970 "stale validator should be gone after repopulate"
971 );
972 assert!(
973 !inner.unique_tokens.contains(&stale_token),
974 "stale token should be gone after repopulate"
975 );
976 assert!(
977 !inner
978 .pool_cache
979 .contains_key(&(stale_user_token, stale_token)),
980 "stale liquidity entry should be gone after repopulate"
981 );
982 assert!(
983 inner.slot_to_pool.is_empty(),
984 "stale slot_to_pool should be gone after repopulate"
985 );
986
987 assert!(
988 inner.unique_validators.contains(&new_validator),
989 "new canonical validator should be present after repopulate"
990 );
991 assert_eq!(
992 inner.last_seen_validators.len(),
993 3,
994 "should have 3 validators from new canonical headers"
995 );
996 }
997
998 #[test]
999 fn test_is_active_validator() {
1000 let active = address!("1111111111111111111111111111111111111111");
1001 let inactive = address!("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF");
1002
1003 let cases = [
1004 (vec![active], active, true, "active validator in set"),
1005 (
1006 vec![active],
1007 inactive,
1008 false,
1009 "inactive validator not in set",
1010 ),
1011 (vec![], active, false, "empty set"),
1012 ];
1013
1014 for (unique_validators, query, expected, desc) in cases {
1015 let cache = AmmLiquidityCache::with_unique_validators(unique_validators);
1016 assert_eq!(cache.is_active_validator(&query), expected, "{desc}");
1017 }
1018 }
1019
1020 #[test]
1021 fn test_track_tokens() {
1022 let token_a = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
1023 let token_b = address!("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
1024
1025 let cache = AmmLiquidityCache::with_unique_tokens(vec![]);
1027 assert!(!cache.track_tokens(&[]));
1028 assert!(cache.inner.read().unique_tokens.is_empty());
1029
1030 let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
1032 assert!(cache.track_tokens(&[token_b]));
1033 assert_eq!(cache.inner.read().unique_tokens, vec![token_a, token_b]);
1034
1035 let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
1037 assert!(!cache.track_tokens(&[token_a]));
1038 assert_eq!(cache.inner.read().unique_tokens.len(), 1);
1039
1040 let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
1042 assert!(cache.track_tokens(&[token_b, token_b]));
1043 assert_eq!(cache.inner.read().unique_tokens.len(), 2);
1044 }
1045}