1use std::collections::HashMap;
32
33#[derive(Debug, Clone, Copy)]
35pub struct CachedValue<T> {
36 pub value: T,
38 pub expires_at: u64,
40}
41
42impl<T> CachedValue<T> {
43 #[inline]
45 pub fn is_valid(&self, now_ts: u64) -> bool {
46 now_ts < self.expires_at
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct CachedFees {
53 pub creator_fee: f64,
55 pub insurance_fee: f64,
57 pub lp_fee: f64,
59 pub liquidation_fee: f64,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct CachedBounds {
66 pub min_margin: f64,
68 pub min_taker_leverage: f64,
70 pub max_taker_leverage: f64,
72 pub liquidation_taker_ratio: f64,
74}
75
76#[derive(Debug, Clone, Copy)]
78pub struct StateCacheConfig {
79 pub slow_ttl: u64,
81 pub fast_ttl: u64,
83}
84
85impl Default for StateCacheConfig {
86 fn default() -> Self {
87 Self {
88 slow_ttl: 60,
89 fast_ttl: 2,
90 }
91 }
92}
93
94#[derive(Debug)]
99pub struct StateCache {
100 fees: HashMap<[u8; 20], CachedValue<CachedFees>>,
102 bounds: HashMap<[u8; 20], CachedValue<CachedBounds>>,
103
104 mark_prices: HashMap<[u8; 32], CachedValue<f64>>,
106 funding_rates: HashMap<[u8; 32], CachedValue<f64>>,
107 usdc_balance: Option<CachedValue<f64>>,
108
109 slow_ttl: u64,
110 fast_ttl: u64,
111}
112
113impl StateCache {
114 pub fn new(config: StateCacheConfig) -> Self {
116 Self {
117 fees: HashMap::new(),
118 bounds: HashMap::new(),
119 mark_prices: HashMap::new(),
120 funding_rates: HashMap::new(),
121 usdc_balance: None,
122 slow_ttl: config.slow_ttl,
123 fast_ttl: config.fast_ttl,
124 }
125 }
126
127 #[inline]
131 pub fn get_fees(&self, addr: &[u8; 20], now_ts: u64) -> Option<&CachedFees> {
132 self.fees
133 .get(addr)
134 .filter(|cv| cv.is_valid(now_ts))
135 .map(|cv| &cv.value)
136 }
137
138 pub fn put_fees(&mut self, addr: [u8; 20], value: CachedFees, now_ts: u64) {
140 self.fees.insert(
141 addr,
142 CachedValue {
143 value,
144 expires_at: now_ts.saturating_add(self.slow_ttl),
145 },
146 );
147 }
148
149 #[inline]
153 pub fn get_bounds(&self, addr: &[u8; 20], now_ts: u64) -> Option<&CachedBounds> {
154 self.bounds
155 .get(addr)
156 .filter(|cv| cv.is_valid(now_ts))
157 .map(|cv| &cv.value)
158 }
159
160 pub fn put_bounds(&mut self, addr: [u8; 20], value: CachedBounds, now_ts: u64) {
162 self.bounds.insert(
163 addr,
164 CachedValue {
165 value,
166 expires_at: now_ts.saturating_add(self.slow_ttl),
167 },
168 );
169 }
170
171 #[inline]
175 pub fn get_mark_price(&self, perp_id: &[u8; 32], now_ts: u64) -> Option<f64> {
176 self.mark_prices
177 .get(perp_id)
178 .filter(|cv| cv.is_valid(now_ts))
179 .map(|cv| cv.value)
180 }
181
182 pub fn put_mark_price(&mut self, perp_id: [u8; 32], price: f64, now_ts: u64) {
184 self.mark_prices.insert(
185 perp_id,
186 CachedValue {
187 value: price,
188 expires_at: now_ts.saturating_add(self.fast_ttl),
189 },
190 );
191 }
192
193 #[inline]
197 pub fn get_funding_rate(&self, perp_id: &[u8; 32], now_ts: u64) -> Option<f64> {
198 self.funding_rates
199 .get(perp_id)
200 .filter(|cv| cv.is_valid(now_ts))
201 .map(|cv| cv.value)
202 }
203
204 pub fn put_funding_rate(&mut self, perp_id: [u8; 32], rate: f64, now_ts: u64) {
206 self.funding_rates.insert(
207 perp_id,
208 CachedValue {
209 value: rate,
210 expires_at: now_ts.saturating_add(self.fast_ttl),
211 },
212 );
213 }
214
215 #[inline]
219 pub fn get_usdc_balance(&self, now_ts: u64) -> Option<f64> {
220 self.usdc_balance
221 .filter(|cv| cv.is_valid(now_ts))
222 .map(|cv| cv.value)
223 }
224
225 pub fn put_usdc_balance(&mut self, balance: f64, now_ts: u64) {
227 self.usdc_balance = Some(CachedValue {
228 value: balance,
229 expires_at: now_ts.saturating_add(self.fast_ttl),
230 });
231 }
232
233 pub fn invalidate_fast_layer(&mut self) {
239 self.mark_prices.clear();
240 self.funding_rates.clear();
241 self.usdc_balance = None;
242 }
243
244 pub fn invalidate_all(&mut self) {
246 self.fees.clear();
247 self.bounds.clear();
248 self.invalidate_fast_layer();
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 fn sample_fees() -> CachedFees {
257 CachedFees {
258 creator_fee: 0.001,
259 insurance_fee: 0.0005,
260 lp_fee: 0.003,
261 liquidation_fee: 0.01,
262 }
263 }
264
265 fn sample_bounds() -> CachedBounds {
266 CachedBounds {
267 min_margin: 5.0,
268 min_taker_leverage: 1.0,
269 max_taker_leverage: 100.0,
270 liquidation_taker_ratio: 0.05,
271 }
272 }
273
274 #[test]
275 fn empty_cache_returns_none() {
276 let c = StateCache::new(StateCacheConfig::default());
277 assert!(c.get_fees(&[0; 20], 0).is_none());
278 assert!(c.get_bounds(&[0; 20], 0).is_none());
279 assert!(c.get_mark_price(&[0; 32], 0).is_none());
280 assert!(c.get_funding_rate(&[0; 32], 0).is_none());
281 assert!(c.get_usdc_balance(0).is_none());
282 }
283
284 #[test]
285 fn slow_layer_respects_ttl() {
286 let mut c = StateCache::new(StateCacheConfig::default()); let addr = [0xAA; 20];
288
289 c.put_fees(addr, sample_fees(), 1000);
290 assert!(c.get_fees(&addr, 1059).is_some());
292 assert!(c.get_fees(&addr, 1060).is_none());
294 }
295
296 #[test]
297 fn fast_layer_respects_ttl() {
298 let mut c = StateCache::new(StateCacheConfig::default()); let perp = [0xBB; 32];
300
301 c.put_mark_price(perp, 42000.0, 1000);
302 assert_eq!(c.get_mark_price(&perp, 1001), Some(42000.0));
303 assert!(c.get_mark_price(&perp, 1002).is_none());
304 }
305
306 #[test]
307 fn funding_rate_ttl() {
308 let mut c = StateCache::new(StateCacheConfig::default());
309 let perp = [0xCC; 32];
310
311 c.put_funding_rate(perp, 0.0001, 500);
312 assert_eq!(c.get_funding_rate(&perp, 501), Some(0.0001));
313 assert!(c.get_funding_rate(&perp, 502).is_none());
314 }
315
316 #[test]
317 fn usdc_balance_ttl() {
318 let mut c = StateCache::new(StateCacheConfig::default());
319
320 c.put_usdc_balance(10_000.0, 100);
321 assert_eq!(c.get_usdc_balance(101), Some(10_000.0));
322 assert!(c.get_usdc_balance(102).is_none());
323 }
324
325 #[test]
326 fn bounds_caching() {
327 let mut c = StateCache::new(StateCacheConfig::default());
328 let addr = [0xDD; 20];
329
330 c.put_bounds(addr, sample_bounds(), 0);
331 let b = c.get_bounds(&addr, 30).unwrap();
332 assert_eq!(b.max_taker_leverage, 100.0);
333 assert_eq!(b.min_margin, 5.0);
334 }
335
336 #[test]
337 fn invalidate_fast_preserves_slow() {
338 let mut c = StateCache::new(StateCacheConfig::default());
339 let addr = [0xAA; 20];
340 let perp = [0xBB; 32];
341
342 c.put_fees(addr, sample_fees(), 0);
343 c.put_bounds(addr, sample_bounds(), 0);
344 c.put_mark_price(perp, 42000.0, 0);
345 c.put_funding_rate(perp, 0.0001, 0);
346 c.put_usdc_balance(1000.0, 0);
347
348 c.invalidate_fast_layer();
349
350 assert!(c.get_fees(&addr, 0).is_some());
352 assert!(c.get_bounds(&addr, 0).is_some());
353
354 assert!(c.get_mark_price(&perp, 0).is_none());
356 assert!(c.get_funding_rate(&perp, 0).is_none());
357 assert!(c.get_usdc_balance(0).is_none());
358 }
359
360 #[test]
361 fn invalidate_all_clears_everything() {
362 let mut c = StateCache::new(StateCacheConfig::default());
363 let addr = [0xAA; 20];
364 let perp = [0xBB; 32];
365
366 c.put_fees(addr, sample_fees(), 0);
367 c.put_mark_price(perp, 42000.0, 0);
368
369 c.invalidate_all();
370
371 assert!(c.get_fees(&addr, 0).is_none());
372 assert!(c.get_mark_price(&perp, 0).is_none());
373 }
374
375 #[test]
376 fn overwrite_updates_value_and_ttl() {
377 let mut c = StateCache::new(StateCacheConfig::default());
378 let perp = [0xBB; 32];
379
380 c.put_mark_price(perp, 42000.0, 100);
381 c.put_mark_price(perp, 43000.0, 200);
382
383 assert_eq!(c.get_mark_price(&perp, 201), Some(43000.0));
385 assert!(c.get_mark_price(&perp, 202).is_none());
386 }
387
388 #[test]
389 fn custom_config_ttls() {
390 let config = StateCacheConfig {
391 slow_ttl: 10,
392 fast_ttl: 1,
393 };
394 let mut c = StateCache::new(config);
395 let addr = [0xAA; 20];
396 let perp = [0xBB; 32];
397
398 c.put_fees(addr, sample_fees(), 0);
399 c.put_mark_price(perp, 100.0, 0);
400
401 assert!(c.get_fees(&addr, 9).is_some());
403 assert!(c.get_fees(&addr, 10).is_none());
404
405 assert!(c.get_mark_price(&perp, 0).is_some());
407 assert!(c.get_mark_price(&perp, 1).is_none());
408 }
409
410 #[test]
411 fn different_keys_independent() {
412 let mut c = StateCache::new(StateCacheConfig::default());
413 let perp_a = [0xAA; 32];
414 let perp_b = [0xBB; 32];
415
416 c.put_mark_price(perp_a, 100.0, 0);
417 c.put_mark_price(perp_b, 200.0, 0);
418
419 assert_eq!(c.get_mark_price(&perp_a, 0), Some(100.0));
420 assert_eq!(c.get_mark_price(&perp_b, 0), Some(200.0));
421 }
422
423 #[test]
424 fn cached_value_is_valid_boundary() {
425 let cv = CachedValue {
426 value: 42,
427 expires_at: 100,
428 };
429 assert!(cv.is_valid(99)); assert!(!cv.is_valid(100)); assert!(!cv.is_valid(101)); }
433}