1use std::collections::HashMap;
20
21use serde::{Deserialize, Serialize};
22
23type Selector = [u8; 4];
25
26#[derive(Debug, Clone, Copy)]
30pub struct GasLimits;
31
32impl GasLimits {
33 pub const ETH_TRANSFER: u64 = 21_000;
35 pub const APPROVE: u64 = 60_000;
37 pub const OPEN_TAKER: u64 = 700_000;
39 pub const OPEN_MAKER: u64 = 800_000;
41 pub const CLOSE_POSITION: u64 = 600_000;
43 pub const ADJUST_NOTIONAL: u64 = 500_000;
45 pub const ADJUST_MARGIN: u64 = 500_000;
47 pub const TRANSFER: u64 = 65_000;
49}
50
51#[derive(Debug)]
60pub struct GasLimitCache {
61 estimates: HashMap<Selector, CachedEstimate>,
62 ttl_ms: u64,
63 buffer: f64,
65}
66
67#[derive(Debug, Clone, Copy)]
68struct CachedEstimate {
69 gas_limit: u64,
70 cached_at_ms: u64,
71}
72
73const DEFAULT_ESTIMATE_TTL_MS: u64 = 3_600_000;
75
76const DEFAULT_ESTIMATE_BUFFER: f64 = 1.2;
78
79impl GasLimitCache {
80 pub fn new() -> Self {
82 Self {
83 estimates: HashMap::new(),
84 ttl_ms: DEFAULT_ESTIMATE_TTL_MS,
85 buffer: DEFAULT_ESTIMATE_BUFFER,
86 }
87 }
88
89 pub fn with_config(ttl_ms: u64, buffer: f64) -> Self {
91 Self {
92 estimates: HashMap::new(),
93 ttl_ms,
94 buffer,
95 }
96 }
97
98 pub fn get(&self, selector: &Selector, now_ms: u64) -> Option<u64> {
102 let entry = self.estimates.get(selector)?;
103 if now_ms.saturating_sub(entry.cached_at_ms) < self.ttl_ms {
104 Some(entry.gas_limit)
105 } else {
106 None
107 }
108 }
109
110 pub fn put(&mut self, selector: Selector, raw_estimate: u64, now_ms: u64) {
112 let buffered = (raw_estimate as f64 * self.buffer) as u64;
113 self.estimates.insert(
114 selector,
115 CachedEstimate {
116 gas_limit: buffered,
117 cached_at_ms: now_ms,
118 },
119 );
120 }
121
122 pub fn set_ttl(&mut self, ttl_ms: u64) {
124 self.ttl_ms = ttl_ms;
125 }
126}
127
128impl Default for GasLimitCache {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
136pub enum Urgency {
137 Low,
139 Normal,
141 High,
143 Critical,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149pub struct GasFees {
150 pub base_fee: u64,
152 pub max_priority_fee_per_gas: u64,
154 pub max_fee_per_gas: u64,
156 pub updated_at_ms: u64,
158}
159
160#[derive(Debug)]
166pub struct FeeCache {
167 current: Option<GasFees>,
168 ttl_ms: u64,
169 default_priority_fee: u64,
170}
171
172impl FeeCache {
173 pub fn new(ttl_ms: u64, default_priority_fee: u64) -> Self {
178 Self {
179 current: None,
180 ttl_ms,
181 default_priority_fee,
182 }
183 }
184
185 pub fn update(&mut self, base_fee: u64, now_ms: u64) {
187 tracing::debug!(base_fee, "gas cache updated");
188 self.current = Some(GasFees {
189 base_fee,
190 max_priority_fee_per_gas: self.default_priority_fee,
191 max_fee_per_gas: 2u64
193 .saturating_mul(base_fee)
194 .saturating_add(self.default_priority_fee),
195 updated_at_ms: now_ms,
196 });
197 }
198
199 #[inline]
201 pub fn is_valid(&self, now_ms: u64) -> bool {
202 self.current
203 .map(|f| now_ms.saturating_sub(f.updated_at_ms) < self.ttl_ms)
204 .unwrap_or(false)
205 }
206
207 #[inline]
209 pub fn get(&self, now_ms: u64) -> Option<&GasFees> {
210 self.current
211 .as_ref()
212 .filter(|f| now_ms.saturating_sub(f.updated_at_ms) < self.ttl_ms)
213 }
214
215 pub fn set_ttl(&mut self, ttl_ms: u64) {
221 self.ttl_ms = ttl_ms;
222 }
223
224 #[inline]
226 pub fn base_fee(&self) -> Option<u64> {
227 self.current.map(|f| f.base_fee)
228 }
229
230 #[inline]
238 pub fn fees_for(&self, urgency: Urgency, now_ms: u64) -> Option<GasFees> {
239 let base = self.get(now_ms)?;
240 let bf = base.base_fee;
241 let pf = self.default_priority_fee;
242
243 let (max_fee, priority) = match urgency {
244 Urgency::Low => (bf.saturating_add(pf), pf),
245 Urgency::Normal => (2u64.saturating_mul(bf).saturating_add(pf), pf),
246 Urgency::High => (
247 3u64.saturating_mul(bf)
248 .saturating_add(2u64.saturating_mul(pf)),
249 2u64.saturating_mul(pf),
250 ),
251 Urgency::Critical => (
252 4u64.saturating_mul(bf)
253 .saturating_add(5u64.saturating_mul(pf)),
254 5u64.saturating_mul(pf),
255 ),
256 };
257
258 Some(GasFees {
259 base_fee: bf,
260 max_priority_fee_per_gas: priority,
261 max_fee_per_gas: max_fee,
262 updated_at_ms: base.updated_at_ms,
263 })
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 const BASE: u64 = 50_000_000; const TIP: u64 = 1_000_000_000; fn cache_with_fees(now_ms: u64) -> FeeCache {
275 let mut c = FeeCache::new(2000, TIP);
276 c.update(BASE, now_ms);
277 c
278 }
279
280 #[test]
281 fn empty_cache_is_invalid() {
282 let c = FeeCache::new(2000, TIP);
283 assert!(!c.is_valid(0));
284 assert!(c.get(0).is_none());
285 assert!(c.fees_for(Urgency::Normal, 0).is_none());
286 }
287
288 #[test]
289 fn update_makes_cache_valid() {
290 let c = cache_with_fees(1000);
291 assert!(c.is_valid(1000));
292 assert!(c.is_valid(2999)); }
294
295 #[test]
296 fn cache_expires_after_ttl() {
297 let c = cache_with_fees(1000);
298 assert!(c.is_valid(2999));
299 assert!(!c.is_valid(3000)); assert!(!c.is_valid(5000));
301 }
302
303 #[test]
304 fn low_urgency_fees() {
305 let c = cache_with_fees(0);
306 let f = c.fees_for(Urgency::Low, 0).unwrap();
307 assert_eq!(f.max_fee_per_gas, BASE + TIP);
308 assert_eq!(f.max_priority_fee_per_gas, TIP);
309 assert_eq!(f.base_fee, BASE);
310 }
311
312 #[test]
313 fn normal_urgency_fees() {
314 let c = cache_with_fees(0);
315 let f = c.fees_for(Urgency::Normal, 0).unwrap();
316 assert_eq!(f.max_fee_per_gas, 2 * BASE + TIP);
317 assert_eq!(f.max_priority_fee_per_gas, TIP);
318 }
319
320 #[test]
321 fn high_urgency_fees() {
322 let c = cache_with_fees(0);
323 let f = c.fees_for(Urgency::High, 0).unwrap();
324 assert_eq!(f.max_fee_per_gas, 3 * BASE + 2 * TIP);
325 assert_eq!(f.max_priority_fee_per_gas, 2 * TIP);
326 }
327
328 #[test]
329 fn critical_urgency_fees() {
330 let c = cache_with_fees(0);
331 let f = c.fees_for(Urgency::Critical, 0).unwrap();
332 assert_eq!(f.max_fee_per_gas, 4 * BASE + 5 * TIP);
333 assert_eq!(f.max_priority_fee_per_gas, 5 * TIP);
334 }
335
336 #[test]
337 fn urgency_ordering() {
338 let c = cache_with_fees(0);
339 let low = c.fees_for(Urgency::Low, 0).unwrap().max_fee_per_gas;
340 let normal = c.fees_for(Urgency::Normal, 0).unwrap().max_fee_per_gas;
341 let high = c.fees_for(Urgency::High, 0).unwrap().max_fee_per_gas;
342 let critical = c.fees_for(Urgency::Critical, 0).unwrap().max_fee_per_gas;
343 assert!(low < normal);
344 assert!(normal < high);
345 assert!(high < critical);
346 }
347
348 #[test]
349 fn fees_for_stale_returns_none() {
350 let c = cache_with_fees(0);
351 assert!(c.fees_for(Urgency::Normal, 3000).is_none());
352 }
353
354 #[test]
355 fn update_replaces_old_fees() {
356 let mut c = cache_with_fees(0);
357 c.update(100_000_000, 5000); let f = c.fees_for(Urgency::Low, 5000).unwrap();
359 assert_eq!(f.base_fee, 100_000_000);
360 }
361
362 #[test]
363 fn saturating_arithmetic_on_huge_values() {
364 let mut c = FeeCache::new(2000, u64::MAX / 2);
365 c.update(u64::MAX / 2, 0);
366 let f = c.fees_for(Urgency::Critical, 0).unwrap();
368 assert_eq!(f.max_fee_per_gas, u64::MAX);
369 }
370
371 #[test]
372 fn preserves_timestamp_across_urgency() {
373 let c = cache_with_fees(42);
374 for urgency in [
375 Urgency::Low,
376 Urgency::Normal,
377 Urgency::High,
378 Urgency::Critical,
379 ] {
380 let f = c.fees_for(urgency, 42).unwrap();
381 assert_eq!(f.updated_at_ms, 42);
382 }
383 }
384
385 #[test]
386 #[allow(clippy::assertions_on_constants)]
387 fn gas_limits_are_reasonable() {
388 assert!(GasLimits::APPROVE > 20_000 && GasLimits::APPROVE < 200_000);
390 assert!(GasLimits::OPEN_TAKER > 200_000 && GasLimits::OPEN_TAKER < 2_000_000);
391 assert!(GasLimits::CLOSE_POSITION > 100_000 && GasLimits::CLOSE_POSITION < 2_000_000);
392 assert!(GasLimits::OPEN_MAKER > GasLimits::OPEN_TAKER);
394 }
395
396 #[test]
399 fn estimate_cache_applies_buffer_and_expires() {
400 let mut cache = GasLimitCache::with_config(1000, 1.5);
401 let selector = [0x01, 0x02, 0x03, 0x04];
402
403 assert!(cache.get(&selector, 0).is_none());
404
405 cache.put(selector, 100_000, 0);
406 assert_eq!(cache.get(&selector, 0), Some(150_000)); assert_eq!(cache.get(&selector, 999), Some(150_000)); assert!(cache.get(&selector, 1000).is_none()); }
410
411 #[test]
412 fn estimate_cache_selectors_are_independent() {
413 let mut cache = GasLimitCache::new();
414 let open = [0xAA, 0xBB, 0xCC, 0xDD];
415 let close = [0x11, 0x22, 0x33, 0x44];
416
417 cache.put(open, 500_000, 0);
418 cache.put(close, 800_000, 0);
419
420 assert_eq!(cache.get(&open, 0), Some(600_000));
421 assert_eq!(cache.get(&close, 0), Some(960_000));
422 }
423}