Skip to main content

fynd_core/derived/
store.rs

1//! Typed storage for derived data.
2
3use std::{any::Any, collections::HashMap, str::FromStr, sync::Arc};
4
5use tokio::sync::RwLock;
6use tycho_simulation::tycho_common::models::Address;
7
8use super::{
9    computation::{ComputationId, DerivedComputation, FailedItem, FailedItemError},
10    computations::{PoolDepthComputation, SpotPriceComputation, TokenGasPriceComputation},
11    types::{
12        PoolDepthKey, PoolDepths, SpotPriceKey, SpotPrices, TokenGasPriceKey, TokenGasPrices,
13        TokenPricesWithDeps,
14    },
15};
16use crate::derived::SharedDerivedDataRef;
17
18/// A computed value paired with the block it was computed for.
19#[derive(Debug)]
20struct ComputedValue<T> {
21    data: T,
22    block: u64,
23}
24
25/// A type-erased computation output paired with the block it was computed for.
26struct ComputedSlot {
27    data: Box<dyn Any + Send + Sync>,
28    block: u64,
29}
30
31impl std::fmt::Debug for ComputedSlot {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("ComputedSlot")
34            .field("block", &self.block)
35            .finish_non_exhaustive()
36    }
37}
38
39/// Typed storage for derived data computations.
40///
41/// Computation outputs live in a type-keyed slot map written erased and read back
42/// typed by the per-computation getters below. The persistent failure maps stay
43/// typed because their merge logic is specific to each keyed output.
44#[derive(Debug, Default)]
45pub struct DerivedData {
46    /// Computation outputs keyed by [`ComputationId`], stored type-erased.
47    slots: HashMap<ComputationId, ComputedSlot>,
48    /// Persistent failure map: key → (block, error). Merged on incremental runs, replaced on full.
49    token_prices_failed: HashMap<TokenGasPriceKey, (u64, FailedItemError)>,
50    /// Token prices with path dependency tracking for incremental computation.
51    token_prices_deps: Option<ComputedValue<TokenPricesWithDeps>>,
52    /// Persistent failure map: key → (block, error). Merged on incremental runs, replaced on full.
53    pool_depths_failed: HashMap<PoolDepthKey, (u64, FailedItemError)>,
54    /// Persistent failure map: key → (block, error). Merged on incremental runs, replaced on full.
55    spot_prices_failed: HashMap<SpotPriceKey, (u64, FailedItemError)>,
56}
57
58/// Parses `"component_id/token_in/token_out"` into a typed `(ComponentId, Address, Address)` key.
59fn parse_pair_key(s: &str) -> Option<(String, Address, Address)> {
60    let mut parts = s.rsplitn(3, '/');
61    let token_out_str = parts.next()?;
62    let token_in_str = parts.next()?;
63    let component_id = parts.next()?;
64    let token_in = Address::from_str(token_in_str).ok()?;
65    let token_out = Address::from_str(token_out_str).ok()?;
66    Some((component_id.to_string(), token_in, token_out))
67}
68
69impl DerivedData {
70    /// Creates an empty store.
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Creates a new shared derived data store for async computation tests that is wrapped in an
76    /// `Arc<RwLock<>>`.
77    pub fn new_shared() -> SharedDerivedDataRef {
78        Arc::new(RwLock::new(Self::new()))
79    }
80
81    /// Stores a computation's output under its id, type-erased, replacing any prior value.
82    pub(crate) fn set_output<T: Any + Send + Sync>(
83        &mut self,
84        id: ComputationId,
85        data: T,
86        block: u64,
87    ) {
88        self.slots
89            .insert(id, ComputedSlot { data: Box::new(data), block });
90    }
91
92    /// Returns the output stored under `id` downcast to `T`, or `None` if absent.
93    ///
94    /// Each id maps to a single output type, so reading an existing slot as the wrong `T`
95    /// is a programmer error: it trips a debug assertion and otherwise returns `None`.
96    pub(crate) fn output<T: Any>(&self, id: ComputationId) -> Option<&T> {
97        let slot = self.slots.get(id)?;
98        debug_assert!(slot.data.is::<T>(), "derived output {id} read as the wrong type");
99        slot.data.downcast_ref::<T>()
100    }
101
102    /// Returns the block at which the output under `id` was last computed.
103    pub(crate) fn output_block(&self, id: ComputationId) -> Option<u64> {
104        self.slots
105            .get(id)
106            .map(|slot| slot.block)
107    }
108
109    /// Removes the output stored under `id`.
110    fn clear_output(&mut self, id: ComputationId) {
111        self.slots.remove(id);
112    }
113
114    /// Returns `true` if all derived data types has been computed at least once.
115    pub fn derived_data_ready(&self) -> bool {
116        self.token_prices_block().is_some() &&
117            self.token_prices_deps_block().is_some() &&
118            self.pool_depths_block().is_some() &&
119            self.spot_prices_block().is_some()
120    }
121
122    // -------------------------------------------------------------------------
123    // Token Prices
124    // -------------------------------------------------------------------------
125
126    /// Returns token prices if computed.
127    pub fn token_prices(&self) -> Option<&TokenGasPrices> {
128        self.output(TokenGasPriceComputation::ID)
129    }
130
131    /// Returns the block at which token prices were last computed.
132    pub fn token_prices_block(&self) -> Option<u64> {
133        self.output_block(TokenGasPriceComputation::ID)
134    }
135
136    /// Sets token prices, merging failures for incremental runs.
137    ///
138    /// For full recomputes, the failure map is replaced entirely. For incremental runs,
139    /// failures are merged: existing entries for keys that now succeed are removed, new
140    /// failures are inserted, and entries for keys not attempted this run are preserved.
141    pub fn set_token_prices(
142        &mut self,
143        prices: TokenGasPrices,
144        failed_items: Vec<FailedItem>,
145        block: u64,
146        is_full_recompute: bool,
147    ) {
148        let new_failures: HashMap<TokenGasPriceKey, (u64, FailedItemError)> = failed_items
149            .into_iter()
150            .filter_map(|f| {
151                Address::from_str(&f.key)
152                    .ok()
153                    .map(|k| (k, (block, f.error)))
154            })
155            .collect();
156
157        if is_full_recompute {
158            self.token_prices_failed = new_failures;
159        } else {
160            self.token_prices_failed
161                .retain(|k, _| !prices.contains_key(k));
162            self.token_prices_failed
163                .extend(new_failures);
164        }
165
166        self.set_output(TokenGasPriceComputation::ID, prices, block);
167    }
168
169    /// Returns `(block, error)` for this token address if it failed in a past
170    /// computation, or `None` if it succeeded or was not attempted.
171    pub fn token_price_failure(&self, key: &TokenGasPriceKey) -> Option<(u64, &FailedItemError)> {
172        self.token_prices_failed
173            .get(key)
174            .map(|(block, error)| (*block, error))
175    }
176
177    /// Clears token prices and their failure map.
178    pub fn clear_token_prices(&mut self) {
179        self.clear_output(TokenGasPriceComputation::ID);
180        self.token_prices_failed.clear();
181    }
182
183    // -------------------------------------------------------------------------
184    // Token Prices with Dependencies (for incremental computation)
185    // -------------------------------------------------------------------------
186
187    /// Returns token prices with path dependencies if computed.
188    pub fn token_prices_deps(&self) -> Option<&TokenPricesWithDeps> {
189        self.token_prices_deps
190            .as_ref()
191            .map(|v| &v.data)
192    }
193
194    /// Returns the block at which token prices with dependencies were last computed.
195    pub fn token_prices_deps_block(&self) -> Option<u64> {
196        self.token_prices_deps
197            .as_ref()
198            .map(|v| v.block)
199    }
200
201    /// Sets token prices with path dependencies.
202    pub fn set_token_prices_deps(&mut self, prices: TokenPricesWithDeps, block: u64) {
203        self.token_prices_deps = Some(ComputedValue { data: prices, block });
204    }
205
206    /// Clears token prices with dependencies.
207    pub fn clear_token_prices_deps(&mut self) {
208        self.token_prices_deps = None;
209    }
210
211    // -------------------------------------------------------------------------
212    // Pool Depths
213    // -------------------------------------------------------------------------
214
215    /// Returns pool depths if computed.
216    pub fn pool_depths(&self) -> Option<&PoolDepths> {
217        self.output(PoolDepthComputation::ID)
218    }
219
220    /// Returns the block at which pool depths were last computed.
221    pub fn pool_depths_block(&self) -> Option<u64> {
222        self.output_block(PoolDepthComputation::ID)
223    }
224
225    /// Sets pool depths, merging failures for incremental runs.
226    ///
227    /// For full recomputes, the failure map is replaced entirely. For incremental runs,
228    /// failures are merged: existing entries for keys that now succeed are removed, new
229    /// failures are inserted, and entries for keys not attempted this run are preserved.
230    pub fn set_pool_depths(
231        &mut self,
232        depths: PoolDepths,
233        failed_items: Vec<FailedItem>,
234        block: u64,
235        is_full_recompute: bool,
236    ) {
237        let new_failures: HashMap<PoolDepthKey, (u64, FailedItemError)> = failed_items
238            .into_iter()
239            .filter_map(|f| parse_pair_key(&f.key).map(|k| (k, (block, f.error))))
240            .collect();
241
242        if is_full_recompute {
243            self.pool_depths_failed = new_failures;
244        } else {
245            self.pool_depths_failed
246                .retain(|k, _| !depths.contains_key(k));
247            self.pool_depths_failed
248                .extend(new_failures);
249        }
250
251        self.set_output(PoolDepthComputation::ID, depths, block);
252    }
253
254    /// Returns `(block, error)` for this key if it failed in a past pool depth
255    /// computation, or `None` if it succeeded or was not attempted.
256    ///
257    /// Key format: `(component_id, token_in, token_out)`
258    pub fn pool_depth_failure(&self, key: &PoolDepthKey) -> Option<(u64, &FailedItemError)> {
259        self.pool_depths_failed
260            .get(key)
261            .map(|(block, error)| (*block, error))
262    }
263
264    /// Clears pool depths and their failure map.
265    pub fn clear_pool_depths(&mut self) {
266        self.clear_output(PoolDepthComputation::ID);
267        self.pool_depths_failed.clear();
268    }
269
270    // -------------------------------------------------------------------------
271    // Spot Prices
272    // -------------------------------------------------------------------------
273
274    /// Returns spot prices if computed.
275    pub fn spot_prices(&self) -> Option<&SpotPrices> {
276        self.output(SpotPriceComputation::ID)
277    }
278
279    /// Returns the block at which spot prices were last computed.
280    pub fn spot_prices_block(&self) -> Option<u64> {
281        self.output_block(SpotPriceComputation::ID)
282    }
283
284    /// Sets spot prices, merging failures for incremental runs.
285    ///
286    /// For full recomputes, the failure map is replaced entirely. For incremental runs,
287    /// failures are merged: existing entries for keys that now succeed are removed, new
288    /// failures are inserted, and entries for keys not attempted this run are preserved.
289    pub fn set_spot_prices(
290        &mut self,
291        prices: SpotPrices,
292        failed_items: Vec<FailedItem>,
293        block: u64,
294        is_full_recompute: bool,
295    ) {
296        let new_failures: HashMap<SpotPriceKey, (u64, FailedItemError)> = failed_items
297            .into_iter()
298            .filter_map(|f| parse_pair_key(&f.key).map(|k| (k, (block, f.error))))
299            .collect();
300
301        if is_full_recompute {
302            self.spot_prices_failed = new_failures;
303        } else {
304            self.spot_prices_failed
305                .retain(|k, _| !prices.contains_key(k));
306            self.spot_prices_failed
307                .extend(new_failures);
308        }
309
310        self.set_output(SpotPriceComputation::ID, prices, block);
311    }
312
313    /// Returns `(block, error)` for this key if it failed in a past spot price
314    /// computation, or `None` if it succeeded or was not attempted.
315    ///
316    /// Key format: `(component_id, token_in, token_out)`
317    pub fn spot_price_failure(&self, key: &SpotPriceKey) -> Option<(u64, &FailedItemError)> {
318        self.spot_prices_failed
319            .get(key)
320            .map(|(block, error)| (*block, error))
321    }
322
323    /// Clears spot prices and their failure map.
324    pub fn clear_spot_prices(&mut self) {
325        self.clear_output(SpotPriceComputation::ID);
326        self.spot_prices_failed.clear();
327    }
328
329    // -------------------------------------------------------------------------
330    // Bulk Operations
331    // -------------------------------------------------------------------------
332
333    /// Clears all stored data, including all failure maps.
334    pub fn clear_all(&mut self) {
335        self.slots.clear();
336        self.token_prices_failed.clear();
337        self.token_prices_deps = None;
338        self.pool_depths_failed.clear();
339        self.spot_prices_failed.clear();
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::{algorithm::test_utils::addr, derived::types::SpotPrices};
347
348    fn failed(key: &str, error: FailedItemError) -> FailedItem {
349        FailedItem { key: key.to_string(), error }
350    }
351
352    fn pair_key(comp: &str, b_in: u8, b_out: u8) -> SpotPriceKey {
353        (comp.to_string(), addr(b_in), addr(b_out))
354    }
355
356    #[test]
357    fn test_token_prices_block_tracks_independently() {
358        let mut store = DerivedData::new();
359        assert_eq!(store.token_prices_block(), None);
360
361        store.set_token_prices(Default::default(), vec![], 42, true);
362        assert_eq!(store.token_prices_block(), Some(42));
363
364        // Other computations not set yet
365        assert_eq!(store.spot_prices_block(), None);
366        assert_eq!(store.pool_depths_block(), None);
367    }
368
369    #[test]
370    fn test_spot_prices_block_tracks_independently() {
371        let mut store = DerivedData::new();
372        store.set_spot_prices(Default::default(), vec![], 10, true);
373        assert_eq!(store.spot_prices_block(), Some(10));
374        assert_eq!(store.token_prices_block(), None);
375    }
376
377    #[test]
378    fn test_pool_depths_block_tracks_independently() {
379        let mut store = DerivedData::new();
380        store.set_pool_depths(Default::default(), vec![], 7, true);
381        assert_eq!(store.pool_depths_block(), Some(7));
382        assert_eq!(store.token_prices_block(), None);
383    }
384
385    #[test]
386    fn test_derived_data_ready() {
387        let mut store = DerivedData::new();
388        assert!(!store.derived_data_ready());
389
390        store.set_spot_prices(Default::default(), vec![], 5, true);
391        assert!(!store.derived_data_ready());
392
393        store.set_token_prices(Default::default(), vec![], 10, true);
394        assert!(!store.derived_data_ready());
395
396        store.set_token_prices_deps(Default::default(), 10);
397        assert!(!store.derived_data_ready());
398
399        store.set_pool_depths(Default::default(), vec![], 9, true);
400        assert!(store.derived_data_ready());
401    }
402
403    #[test]
404    fn test_clear_all_resets_all_fields() {
405        let mut store = DerivedData::new();
406        store.set_token_prices(Default::default(), vec![], 1, true);
407        store.set_spot_prices(Default::default(), vec![], 1, true);
408        store.set_pool_depths(Default::default(), vec![], 1, true);
409
410        store.clear_all();
411
412        assert!(store.token_prices().is_none());
413        assert!(store.spot_prices().is_none());
414        assert!(store.pool_depths().is_none());
415        assert!(!store.derived_data_ready());
416    }
417
418    #[test]
419    fn test_token_price_failure_stored_with_block() {
420        let token_addr = addr(0xab);
421        let key_str = format!("{token_addr}");
422        let mut store = DerivedData::new();
423        store.set_token_prices(
424            Default::default(),
425            vec![failed(&key_str, FailedItemError::SimulationFailed("sim error".into()))],
426            42,
427            true,
428        );
429        assert_eq!(
430            store.token_price_failure(&token_addr),
431            Some((42, &FailedItemError::SimulationFailed("sim error".into())))
432        );
433        assert_eq!(store.token_price_failure(&addr(0xcd)), None);
434    }
435
436    #[test]
437    fn test_spot_price_failure_stored_with_block() {
438        let key = pair_key("pool1", 0x01, 0x02);
439        let key_str = format!("pool1/{}/{}", addr(0x01), addr(0x02));
440        let mut store = DerivedData::new();
441        store.set_spot_prices(
442            Default::default(),
443            vec![failed(&key_str, FailedItemError::SimulationFailed("sim error".into()))],
444            10,
445            true,
446        );
447        assert_eq!(
448            store.spot_price_failure(&key),
449            Some((10, &FailedItemError::SimulationFailed("sim error".into())))
450        );
451        assert_eq!(store.spot_price_failure(&pair_key("pool1", 0x01, 0x03)), None);
452    }
453
454    #[test]
455    fn test_pool_depth_failure_stored_with_block() {
456        let key: PoolDepthKey = pair_key("pool1", 0x01, 0x02);
457        let key_str = format!("pool1/{}/{}", addr(0x01), addr(0x02));
458        let mut store = DerivedData::new();
459        store.set_pool_depths(
460            Default::default(),
461            vec![failed(&key_str, FailedItemError::SimulationFailed("depth error".into()))],
462            7,
463            true,
464        );
465        assert_eq!(
466            store.pool_depth_failure(&key),
467            Some((7, &FailedItemError::SimulationFailed("depth error".into())))
468        );
469        assert_eq!(store.pool_depth_failure(&pair_key("pool2", 0x01, 0x02)), None);
470    }
471
472    #[test]
473    fn test_rerunning_with_empty_failures_clears_old_reasons() {
474        let key = pair_key("pool1", 0x01, 0x02);
475        let key_str = format!("pool1/{}/{}", addr(0x01), addr(0x02));
476        let mut store = DerivedData::new();
477        store.set_spot_prices(
478            Default::default(),
479            vec![failed(&key_str, FailedItemError::MissingSimulationState)],
480            1,
481            true,
482        );
483        assert!(store.spot_price_failure(&key).is_some());
484
485        // Full re-run with no failures clears the map
486        store.set_spot_prices(Default::default(), vec![], 2, true);
487        assert_eq!(store.spot_price_failure(&key), None);
488    }
489
490    #[test]
491    fn test_clear_token_prices_clears_failure_map() {
492        let token_addr = addr(0xab);
493        let key_str = format!("{token_addr}");
494        let mut store = DerivedData::new();
495        store.set_token_prices(
496            Default::default(),
497            vec![failed(&key_str, FailedItemError::AllSimulationPathsFailed)],
498            1,
499            true,
500        );
501        store.clear_token_prices();
502        assert_eq!(store.token_price_failure(&token_addr), None);
503    }
504
505    #[test]
506    fn test_clear_spot_prices_clears_failure_map() {
507        let key = pair_key("pool1", 0x01, 0x02);
508        let key_str = format!("pool1/{}/{}", addr(0x01), addr(0x02));
509        let mut store = DerivedData::new();
510        store.set_spot_prices(
511            Default::default(),
512            vec![failed(&key_str, FailedItemError::MissingSimulationState)],
513            1,
514            true,
515        );
516        store.clear_spot_prices();
517        assert_eq!(store.spot_price_failure(&key), None);
518    }
519
520    #[test]
521    fn test_clear_pool_depths_clears_failure_map() {
522        let key: PoolDepthKey = pair_key("pool1", 0x01, 0x02);
523        let key_str = format!("pool1/{}/{}", addr(0x01), addr(0x02));
524        let mut store = DerivedData::new();
525        store.set_pool_depths(
526            Default::default(),
527            vec![failed(&key_str, FailedItemError::MissingSpotPrice)],
528            1,
529            true,
530        );
531        store.clear_pool_depths();
532        assert_eq!(store.pool_depth_failure(&key), None);
533    }
534
535    #[test]
536    fn test_incremental_run_preserves_failures_for_unattempted_items() {
537        let key_a = pair_key("pool_a", 0x01, 0x02);
538        let key_a_str = format!("pool_a/{}/{}", addr(0x01), addr(0x02));
539        let key_b = pair_key("pool_b", 0x03, 0x04);
540        let key_b_str = format!("pool_b/{}/{}", addr(0x03), addr(0x04));
541
542        let mut store = DerivedData::new();
543
544        // Full recompute at block 10: both keys fail
545        store.set_spot_prices(
546            Default::default(),
547            vec![
548                failed(&key_a_str, FailedItemError::MissingSimulationState),
549                failed(&key_b_str, FailedItemError::MissingTokenMetadata),
550            ],
551            10,
552            true,
553        );
554        assert_eq!(
555            store.spot_price_failure(&key_a),
556            Some((10, &FailedItemError::MissingSimulationState))
557        );
558        assert_eq!(
559            store.spot_price_failure(&key_b),
560            Some((10, &FailedItemError::MissingTokenMetadata))
561        );
562
563        // Incremental run at block 11: only pool_b is attempted and succeeds
564        let mut prices = SpotPrices::default();
565        prices.insert(key_b.clone(), 1.0);
566        store.set_spot_prices(prices, vec![], 11, false);
567
568        // pool_a was not attempted — failure is preserved from block 10
569        assert_eq!(
570            store.spot_price_failure(&key_a),
571            Some((10, &FailedItemError::MissingSimulationState))
572        );
573        // pool_b succeeded — failure is cleared
574        assert_eq!(store.spot_price_failure(&key_b), None);
575    }
576
577    #[test]
578    fn test_incremental_run_updates_block_on_repeated_failure() {
579        let key = pair_key("pool_a", 0x01, 0x02);
580        let key_str = format!("pool_a/{}/{}", addr(0x01), addr(0x02));
581
582        let mut store = DerivedData::new();
583
584        store.set_spot_prices(
585            Default::default(),
586            vec![failed(&key_str, FailedItemError::MissingSimulationState)],
587            10,
588            true,
589        );
590        assert_eq!(
591            store.spot_price_failure(&key),
592            Some((10, &FailedItemError::MissingSimulationState))
593        );
594
595        // Incremental run at block 11: pool_a fails again with a new error
596        store.set_spot_prices(
597            Default::default(),
598            vec![failed(&key_str, FailedItemError::MissingTokenMetadata)],
599            11,
600            false,
601        );
602        assert_eq!(
603            store.spot_price_failure(&key),
604            Some((11, &FailedItemError::MissingTokenMetadata))
605        );
606    }
607
608    #[test]
609    fn test_clear_all_clears_all_failure_maps() {
610        let token_addr = addr(0xab);
611        let token_str = format!("{token_addr}");
612        let pair = pair_key("pool1", 0x01, 0x02);
613        let pair_str = format!("pool1/{}/{}", addr(0x01), addr(0x02));
614
615        let mut store = DerivedData::new();
616        store.set_token_prices(
617            Default::default(),
618            vec![failed(&token_str, FailedItemError::AllSimulationPathsFailed)],
619            1,
620            true,
621        );
622        store.set_spot_prices(
623            Default::default(),
624            vec![failed(&pair_str, FailedItemError::MissingSimulationState)],
625            1,
626            true,
627        );
628        store.set_pool_depths(
629            Default::default(),
630            vec![failed(&pair_str, FailedItemError::MissingSpotPrice)],
631            1,
632            true,
633        );
634
635        store.clear_all();
636
637        assert_eq!(store.token_price_failure(&token_addr), None);
638        assert_eq!(store.spot_price_failure(&pair), None);
639        assert_eq!(store.pool_depth_failure(&pair), None);
640    }
641}