Skip to main content

fynd_core/derived/
store.rs

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