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 StateProvider, 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::Handler,
24 tip_fee_manager::{
25 TipFeeManager,
26 amm::{Pool, PoolKey, compute_amount_out},
27 },
28};
29use tempo_primitives::{TempoHeader, TempoReceipt};
30use tempo_revm::IntoAddress;
31
32const LAST_SEEN_WINDOW: usize = 10;
34
35#[derive(Debug, Clone)]
36pub struct AmmLiquidityCache {
37 inner: Arc<RwLock<AmmLiquidityCacheInner>>,
38}
39
40impl AmmLiquidityCache {
41 pub fn new<Client>(client: Client) -> ProviderResult<Self>
44 where
45 Client: StateProviderFactory
46 + HeaderProvider<Header = TempoHeader>
47 + ChainSpecProvider<ChainSpec = TempoChainSpec>,
48 {
49 let this = Self {
50 inner: Default::default(),
51 };
52 this.repopulate(&client)?;
53
54 Ok(this)
55 }
56
57 pub fn has_enough_liquidity(
60 &self,
61 user_token: Address,
62 fee: U256,
63 state_provider: &mut impl StateProvider,
64 ) -> Result<bool, ProviderError> {
65 let amount_out = compute_amount_out(fee).map_err(ProviderError::other)?;
66
67 let mut missing_in_cache = Vec::new();
68
69 {
71 let inner = self.inner.read();
72 for validator_token in &inner.unique_tokens {
73 if validator_token == &user_token {
77 return Ok(true);
78 }
79
80 if let Some(validator_reserve) = inner.cache.get(&(user_token, *validator_token)) {
81 if *validator_reserve >= amount_out {
82 return Ok(true);
83 }
84 } else {
85 missing_in_cache.push(*validator_token);
86 }
87 }
88 }
89
90 if missing_in_cache.is_empty() {
92 return Ok(false);
93 }
94
95 state_provider
97 .with_read_only_storage_ctx(TempoHardfork::default(), || -> TempoResult<bool> {
98 for validator_token in missing_in_cache {
100 let pool_key = PoolKey::new(user_token, validator_token).get_id();
102 let manager = TipFeeManager::new();
103 let (slot, pool) = (
104 manager.pools[pool_key].base_slot(),
105 manager.pools[pool_key].read()?,
106 );
107 let reserve = U256::from(pool.reserve_validator_token);
108
109 let mut inner = self.inner.write();
110 inner.cache.insert((user_token, validator_token), reserve);
111 inner
112 .slot_to_pool
113 .insert(slot, (user_token, validator_token));
114
115 if reserve >= amount_out {
117 return Ok(true);
118 }
119 }
120
121 Ok(false)
122 })
123 .map_err(ProviderError::other)
124 }
125
126 pub fn clear(&self) {
129 *self.inner.write() = AmmLiquidityCacheInner::default();
130 }
131
132 pub fn repopulate<Client>(&self, client: &Client) -> ProviderResult<()>
137 where
138 Client: StateProviderFactory
139 + HeaderProvider<Header = TempoHeader>
140 + ChainSpecProvider<ChainSpec = TempoChainSpec>,
141 {
142 self.clear();
143 let tip = client.best_block_number()?;
144 let headers =
145 client.sealed_headers_range(tip.saturating_sub(LAST_SEEN_WINDOW as u64 + 1)..=tip)?;
146 self.on_new_blocks(headers.iter(), client)
147 }
148
149 pub fn on_new_state(&self, execution_outcome: &ExecutionOutcome<TempoReceipt>) {
152 let Some(storage) = execution_outcome
153 .account_state(&TIP_FEE_MANAGER_ADDRESS)
154 .map(|acc| &acc.storage)
155 else {
156 return;
157 };
158
159 let mut inner = self.inner.write();
160
161 for (slot, value) in storage.iter() {
163 if let Some(pool) = inner.slot_to_pool.get(slot).copied() {
164 let validator_reserve =
166 U256::from(Pool::decode_from_slot(value.present_value).reserve_validator_token);
167 inner.cache.insert(pool, validator_reserve);
168 } else if let Some(validator) = inner.slot_to_validator.get(slot).copied() {
169 inner
171 .validator_preferences
172 .insert(validator, value.present_value().into_address());
173 }
174 }
175 }
176
177 pub fn on_new_blocks<'a, P>(
179 &self,
180 headers: impl IntoIterator<Item = &'a SealedHeader<TempoHeader>>,
181 client: P,
182 ) -> ProviderResult<()>
183 where
184 P: StateProviderFactory + ChainSpecProvider<ChainSpec: TempoHardforks>,
185 {
186 let headers = headers.into_iter().collect::<Vec<_>>();
187 let latest_hash = if let Some(header) = headers.last() {
188 header.hash()
189 } else {
190 return Ok(());
191 };
192
193 let mut state = None;
194
195 for header in headers {
196 let beneficiary = header.beneficiary();
197 let validator_token_slot = TipFeeManager::new().validator_tokens[beneficiary].slot();
198
199 let cached_preference = self
200 .inner
201 .read()
202 .validator_preferences
203 .get(&beneficiary)
204 .copied();
205
206 let preference = if let Some(cached) = cached_preference {
207 cached
208 } else {
209 if state.is_none() {
213 state = Some(client.state_by_block_hash(latest_hash)?);
214 }
215
216 state
217 .as_mut()
218 .expect("initialized above")
219 .storage(TIP_FEE_MANAGER_ADDRESS, validator_token_slot.into())?
220 .unwrap_or_default()
221 .into_address()
222 };
223
224 let fee_token = if preference.is_zero() {
226 DEFAULT_FEE_TOKEN
227 } else {
228 preference
229 };
230
231 let mut inner = self.inner.write();
232
233 if cached_preference.is_none() {
235 inner.validator_preferences.insert(beneficiary, preference);
236 inner
237 .slot_to_validator
238 .insert(validator_token_slot, beneficiary);
239 }
240
241 inner.last_seen_tokens.push_back(fee_token);
243 if inner.last_seen_tokens.len() > LAST_SEEN_WINDOW {
244 inner.last_seen_tokens.pop_front();
245 }
246 inner.unique_tokens = inner.last_seen_tokens.iter().copied().unique().collect();
247
248 inner.last_seen_validators.push_back(beneficiary);
250 if inner.last_seen_validators.len() > LAST_SEEN_WINDOW {
251 inner.last_seen_validators.pop_front();
252 }
253 inner.unique_validators = inner
254 .last_seen_validators
255 .iter()
256 .copied()
257 .unique()
258 .collect();
259 }
260
261 Ok(())
262 }
263}
264
265#[derive(Debug, Default)]
266struct AmmLiquidityCacheInner {
267 cache: HashMap<(Address, Address), U256>,
269
270 slot_to_pool: U256Map<(Address, Address)>,
272
273 last_seen_tokens: VecDeque<Address>,
275
276 unique_tokens: Vec<Address>,
278
279 last_seen_validators: VecDeque<Address>,
281
282 unique_validators: Vec<Address>,
284
285 validator_preferences: AddressMap<Address>,
287
288 slot_to_validator: U256Map<Address>,
290}
291
292impl AmmLiquidityCache {
293 pub fn is_active_validator(&self, validator: &Address) -> bool {
299 self.inner.read().unique_validators.contains(validator)
300 }
301
302 pub fn is_active_validator_token(&self, token: &Address) -> bool {
305 self.inner.read().unique_tokens.contains(token)
306 }
307
308 pub fn track_tokens(&self, tokens: &[Address]) -> bool {
314 let mut updated = false;
315 if tokens.is_empty() {
316 return updated;
317 }
318
319 let mut inner = self.inner.write();
320 for &token in tokens {
321 if !inner.unique_tokens.contains(&token) {
322 inner.unique_tokens.push(token);
323 updated = true;
324 }
325 }
326 updated
327 }
328}
329
330#[cfg(any(test, feature = "test-utils"))]
331impl AmmLiquidityCache {
332 pub fn with_unique_tokens(unique_tokens: Vec<Address>) -> Self {
334 Self {
335 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
336 unique_tokens,
337 ..Default::default()
338 })),
339 }
340 }
341
342 pub fn with_unique_validators(unique_validators: Vec<Address>) -> Self {
344 Self {
345 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
346 unique_validators,
347 ..Default::default()
348 })),
349 }
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356 use crate::test_utils::create_mock_provider;
357 use alloy_primitives::address;
358
359 #[test]
364 fn test_has_enough_liquidity_user_token_matches_validator_token() {
365 let cache = AmmLiquidityCache {
366 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
367 unique_tokens: vec![address!("1111111111111111111111111111111111111111")],
368 ..Default::default()
369 })),
370 };
371
372 let provider = create_mock_provider();
373 let mut state = provider.latest().unwrap();
374
375 let user_token = address!("1111111111111111111111111111111111111111");
376 let result = cache.has_enough_liquidity(user_token, U256::from(100), &mut state);
377
378 assert!(result.is_ok());
379 assert!(
380 result.unwrap(),
381 "Should return true when user token matches validator token"
382 );
383 }
384
385 #[test]
386 fn test_has_enough_liquidity_cached_pool_sufficient() {
387 let user_token = address!("2222222222222222222222222222222222222222");
388 let validator_token = address!("3333333333333333333333333333333333333333");
389
390 let cache = AmmLiquidityCache {
391 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
392 unique_tokens: vec![validator_token],
393 cache: {
394 let mut m = HashMap::default();
395 m.insert((user_token, validator_token), U256::MAX);
396 m
397 },
398 ..Default::default()
399 })),
400 };
401
402 let provider = create_mock_provider();
403 let mut state = provider.latest().unwrap();
404
405 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
406 assert!(result.is_ok());
407 assert!(
408 result.unwrap(),
409 "Should return true for sufficient cached reserve"
410 );
411 }
412
413 #[test]
414 fn test_has_enough_liquidity_cached_pool_insufficient() {
415 let user_token = address!("2222222222222222222222222222222222222222");
416 let validator_token = address!("3333333333333333333333333333333333333333");
417
418 let cache = AmmLiquidityCache {
419 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
420 unique_tokens: vec![validator_token],
421 cache: {
422 let mut m = HashMap::default();
423 m.insert((user_token, validator_token), U256::ZERO);
424 m
425 },
426 ..Default::default()
427 })),
428 };
429
430 let provider = create_mock_provider();
431 let mut state = provider.latest().unwrap();
432
433 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
434 assert!(result.is_ok());
435 assert!(
436 !result.unwrap(),
437 "Should return false for insufficient cached reserve"
438 );
439 }
440
441 #[test]
442 fn test_has_enough_liquidity_no_unique_tokens() {
443 let cache = AmmLiquidityCache {
444 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner::default())),
445 };
446
447 let provider = create_mock_provider();
448 let mut state = provider.latest().unwrap();
449
450 let user_token = address!("1111111111111111111111111111111111111111");
451 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
452 assert!(result.is_ok());
453 assert!(
454 !result.unwrap(),
455 "Should return false when no unique tokens"
456 );
457 }
458
459 #[test]
460 fn test_has_enough_liquidity_cache_miss_insufficient() {
461 let user_token = address!("2222222222222222222222222222222222222222");
462 let validator_token = address!("3333333333333333333333333333333333333333");
463
464 let cache = AmmLiquidityCache {
465 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
466 unique_tokens: vec![validator_token],
467 cache: HashMap::default(),
468 ..Default::default()
469 })),
470 };
471
472 let provider = create_mock_provider();
473 let mut state = provider.latest().unwrap();
474
475 let result = cache.has_enough_liquidity(user_token, U256::from(1000), &mut state);
477 assert!(result.is_ok());
478 assert!(
479 !result.unwrap(),
480 "Should return false for insufficient reserve"
481 );
482 }
483
484 #[test]
489 fn test_on_new_state_early_return_no_fee_manager_account() {
490 use reth_provider::ExecutionOutcome;
491 use tempo_primitives::TempoReceipt;
492
493 let cache = AmmLiquidityCache {
494 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner::default())),
495 };
496
497 let execution_outcome: ExecutionOutcome<TempoReceipt> = ExecutionOutcome::default();
498 cache.on_new_state(&execution_outcome);
499
500 let inner = cache.inner.read();
501 assert!(inner.cache.is_empty());
502 assert!(inner.validator_preferences.is_empty());
503 }
504
505 #[test]
510 fn test_sliding_window_max_size() {
511 let mut inner = AmmLiquidityCacheInner::default();
512
513 for i in 0..LAST_SEEN_WINDOW {
514 let token = Address::new([i as u8; 20]);
515 inner.last_seen_tokens.push_back(token);
516 }
517
518 assert_eq!(inner.last_seen_tokens.len(), LAST_SEEN_WINDOW);
519
520 let new_token = Address::new([0xFF; 20]);
521 inner.last_seen_tokens.push_back(new_token);
522 if inner.last_seen_tokens.len() > LAST_SEEN_WINDOW {
523 inner.last_seen_tokens.pop_front();
524 }
525
526 assert_eq!(inner.last_seen_tokens.len(), LAST_SEEN_WINDOW);
527 assert_eq!(inner.last_seen_tokens.back(), Some(&new_token));
528 assert_eq!(inner.last_seen_tokens.front(), Some(&Address::new([1; 20])));
529 }
530
531 #[test]
532 fn test_sliding_window_validators() {
533 let mut inner = AmmLiquidityCacheInner::default();
534
535 for i in 0..LAST_SEEN_WINDOW {
536 let validator = Address::new([i as u8; 20]);
537 inner.last_seen_validators.push_back(validator);
538 }
539
540 assert_eq!(inner.last_seen_validators.len(), LAST_SEEN_WINDOW);
541
542 let new_validator = Address::new([0xFF; 20]);
543 inner.last_seen_validators.push_back(new_validator);
544 if inner.last_seen_validators.len() > LAST_SEEN_WINDOW {
545 inner.last_seen_validators.pop_front();
546 }
547
548 assert_eq!(inner.last_seen_validators.len(), LAST_SEEN_WINDOW);
549 assert_eq!(inner.last_seen_validators.back(), Some(&new_validator));
550 assert_eq!(
551 inner.last_seen_validators.front(),
552 Some(&Address::new([1; 20]))
553 );
554
555 inner.unique_validators = inner
556 .last_seen_validators
557 .iter()
558 .copied()
559 .unique()
560 .collect();
561 assert!(inner.unique_validators.contains(&new_validator));
562 }
563
564 #[test]
565 fn test_unique_tokens_deduplication() {
566 let mut inner = AmmLiquidityCacheInner::default();
567
568 let token_a = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
569 let token_b = address!("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
570
571 inner.last_seen_tokens.push_back(token_a);
572 inner.last_seen_tokens.push_back(token_b);
573 inner.last_seen_tokens.push_back(token_b);
574 inner.last_seen_tokens.push_back(token_b);
575
576 inner.unique_tokens = inner.last_seen_tokens.iter().copied().unique().collect();
577
578 assert_eq!(inner.unique_tokens.len(), 2, "duplicates must be removed");
579 assert_eq!(inner.unique_tokens[0], token_a);
580 assert_eq!(inner.unique_tokens[1], token_b);
581 }
582
583 #[test]
588 fn test_cache_insert_and_lookup() {
589 let mut inner = AmmLiquidityCacheInner::default();
590
591 let user_token = address!("1111111111111111111111111111111111111111");
592 let validator_token = address!("2222222222222222222222222222222222222222");
593 let reserve = U256::from(5000);
594
595 inner.cache.insert((user_token, validator_token), reserve);
596
597 assert_eq!(
598 inner.cache.get(&(user_token, validator_token)),
599 Some(&reserve)
600 );
601 }
602
603 #[test]
604 fn test_slot_to_pool_mapping() {
605 let mut inner = AmmLiquidityCacheInner::default();
606
607 let user_token = address!("1111111111111111111111111111111111111111");
608 let validator_token = address!("2222222222222222222222222222222222222222");
609 let slot = U256::from(12345);
610
611 inner
612 .slot_to_pool
613 .insert(slot, (user_token, validator_token));
614
615 assert_eq!(
616 inner.slot_to_pool.get(&slot),
617 Some(&(user_token, validator_token))
618 );
619 }
620
621 #[test]
622 fn test_validator_preferences_mapping() {
623 let mut inner = AmmLiquidityCacheInner::default();
624
625 let validator = address!("3333333333333333333333333333333333333333");
626 let fee_token = address!("4444444444444444444444444444444444444444");
627
628 inner.validator_preferences.insert(validator, fee_token);
629
630 assert_eq!(
631 inner.validator_preferences.get(&validator),
632 Some(&fee_token)
633 );
634 }
635
636 #[test]
637 fn test_slot_to_validator_mapping() {
638 let mut inner = AmmLiquidityCacheInner::default();
639
640 let validator = address!("3333333333333333333333333333333333333333");
641 let slot = U256::from(67890);
642
643 inner.slot_to_validator.insert(slot, validator);
644
645 assert_eq!(inner.slot_to_validator.get(&slot), Some(&validator));
646 }
647
648 #[test]
649 fn test_clear_resets_all_state() {
650 let user_token = Address::random();
651 let validator_token = Address::random();
652 let validator = Address::random();
653
654 let cache = AmmLiquidityCache {
655 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
656 cache: {
657 let mut m = HashMap::default();
658 m.insert((user_token, validator_token), U256::from(1000));
659 m
660 },
661 slot_to_pool: {
662 let mut m = U256Map::default();
663 m.insert(U256::from(1), (user_token, validator_token));
664 m
665 },
666 last_seen_tokens: VecDeque::from(vec![validator_token]),
667 unique_tokens: vec![validator_token],
668 last_seen_validators: VecDeque::from(vec![validator]),
669 unique_validators: vec![validator],
670 validator_preferences: {
671 let mut m = AddressMap::default();
672 m.insert(validator, validator_token);
673 m
674 },
675 slot_to_validator: {
676 let mut m = U256Map::default();
677 m.insert(U256::from(2), validator);
678 m
679 },
680 })),
681 };
682
683 cache.clear();
684
685 let inner = cache.inner.read();
686 assert!(inner.cache.is_empty(), "cache should be empty after clear");
687 assert!(
688 inner.slot_to_pool.is_empty(),
689 "slot_to_pool should be empty after clear"
690 );
691 assert!(
692 inner.last_seen_tokens.is_empty(),
693 "last_seen_tokens should be empty after clear"
694 );
695 assert!(
696 inner.unique_tokens.is_empty(),
697 "unique_tokens should be empty after clear"
698 );
699 assert!(
700 inner.last_seen_validators.is_empty(),
701 "last_seen_validators should be empty after clear"
702 );
703 assert!(
704 inner.unique_validators.is_empty(),
705 "unique_validators should be empty after clear"
706 );
707 assert!(
708 inner.validator_preferences.is_empty(),
709 "validator_preferences should be empty after clear"
710 );
711 assert!(
712 inner.slot_to_validator.is_empty(),
713 "slot_to_validator should be empty after clear"
714 );
715 }
716
717 #[test]
718 fn test_repopulate_clears_stale_data_and_rebuilds_from_canonical_chain() {
719 use alloy_consensus::Header;
720
721 let stale_validator = Address::random();
722 let stale_token = Address::random();
723 let stale_user_token = Address::random();
724
725 let cache = AmmLiquidityCache {
726 inner: Arc::new(RwLock::new(AmmLiquidityCacheInner {
727 cache: {
728 let mut m = HashMap::default();
729 m.insert((stale_user_token, stale_token), U256::from(9999));
730 m
731 },
732 slot_to_pool: {
733 let mut m = U256Map::default();
734 m.insert(U256::from(42), (stale_user_token, stale_token));
735 m
736 },
737 last_seen_tokens: VecDeque::from(vec![stale_token]),
738 unique_tokens: vec![stale_token],
739 last_seen_validators: VecDeque::from(vec![stale_validator]),
740 unique_validators: vec![stale_validator],
741 validator_preferences: {
742 let mut m = AddressMap::default();
743 m.insert(stale_validator, stale_token);
744 m
745 },
746 slot_to_validator: {
747 let mut m = U256Map::default();
748 m.insert(U256::from(99), stale_validator);
749 m
750 },
751 })),
752 };
753
754 {
755 let inner = cache.inner.read();
756 assert!(inner.unique_validators.contains(&stale_validator));
757 assert!(inner.unique_tokens.contains(&stale_token));
758 assert_eq!(
759 inner.cache.get(&(stale_user_token, stale_token)),
760 Some(&U256::from(9999))
761 );
762 }
763
764 let new_validator = Address::random();
765 let provider = create_mock_provider();
766 for i in 0..3u64 {
767 let header = TempoHeader {
768 inner: Header {
769 number: i,
770 beneficiary: new_validator,
771 ..Default::default()
772 },
773 ..Default::default()
774 };
775 provider.add_header(alloy_primitives::B256::random(), header);
776 }
777
778 cache
779 .repopulate(&provider)
780 .expect("repopulate should succeed");
781
782 let inner = cache.inner.read();
783
784 assert!(
785 !inner.unique_validators.contains(&stale_validator),
786 "stale validator should be gone after repopulate"
787 );
788 assert!(
789 !inner.unique_tokens.contains(&stale_token),
790 "stale token should be gone after repopulate"
791 );
792 assert!(
793 !inner.cache.contains_key(&(stale_user_token, stale_token)),
794 "stale liquidity entry should be gone after repopulate"
795 );
796 assert!(
797 inner.slot_to_pool.is_empty(),
798 "stale slot_to_pool should be gone after repopulate"
799 );
800
801 assert!(
802 inner.unique_validators.contains(&new_validator),
803 "new canonical validator should be present after repopulate"
804 );
805 assert_eq!(
806 inner.last_seen_validators.len(),
807 3,
808 "should have 3 validators from new canonical headers"
809 );
810 }
811
812 #[test]
813 fn test_is_active_validator() {
814 let active = address!("1111111111111111111111111111111111111111");
815 let inactive = address!("DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF");
816
817 let cases = [
818 (vec![active], active, true, "active validator in set"),
819 (
820 vec![active],
821 inactive,
822 false,
823 "inactive validator not in set",
824 ),
825 (vec![], active, false, "empty set"),
826 ];
827
828 for (unique_validators, query, expected, desc) in cases {
829 let cache = AmmLiquidityCache::with_unique_validators(unique_validators);
830 assert_eq!(cache.is_active_validator(&query), expected, "{desc}");
831 }
832 }
833
834 #[test]
835 fn test_track_tokens() {
836 let token_a = address!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
837 let token_b = address!("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
838
839 let cache = AmmLiquidityCache::with_unique_tokens(vec![]);
841 assert!(!cache.track_tokens(&[]));
842 assert!(cache.inner.read().unique_tokens.is_empty());
843
844 let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
846 assert!(cache.track_tokens(&[token_b]));
847 assert_eq!(cache.inner.read().unique_tokens, vec![token_a, token_b]);
848
849 let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
851 assert!(!cache.track_tokens(&[token_a]));
852 assert_eq!(cache.inner.read().unique_tokens.len(), 1);
853
854 let cache = AmmLiquidityCache::with_unique_tokens(vec![token_a]);
856 assert!(cache.track_tokens(&[token_b, token_b]));
857 assert_eq!(cache.inner.read().unique_tokens.len(), 2);
858 }
859}