1#[derive(Debug, Clone, Copy)]
23pub struct GasLimits;
24
25impl GasLimits {
26 pub const APPROVE: u64 = 60_000;
28 pub const OPEN_TAKER: u64 = 700_000;
30 pub const OPEN_MAKER: u64 = 800_000;
32 pub const CLOSE_POSITION: u64 = 600_000;
34 pub const ADJUST_NOTIONAL: u64 = 350_000;
36 pub const ADJUST_MARGIN: u64 = 250_000;
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Urgency {
43 Low,
45 Normal,
47 High,
49 Critical,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct GasFees {
56 pub base_fee: u64,
58 pub max_priority_fee_per_gas: u64,
60 pub max_fee_per_gas: u64,
62 pub updated_at_ms: u64,
64}
65
66#[derive(Debug)]
72pub struct GasCache {
73 current: Option<GasFees>,
74 ttl_ms: u64,
75 default_priority_fee: u64,
76}
77
78impl GasCache {
79 pub fn new(ttl_ms: u64, default_priority_fee: u64) -> Self {
84 Self {
85 current: None,
86 ttl_ms,
87 default_priority_fee,
88 }
89 }
90
91 pub fn update(&mut self, base_fee: u64, now_ms: u64) {
93 self.current = Some(GasFees {
94 base_fee,
95 max_priority_fee_per_gas: self.default_priority_fee,
96 max_fee_per_gas: 2u64
98 .saturating_mul(base_fee)
99 .saturating_add(self.default_priority_fee),
100 updated_at_ms: now_ms,
101 });
102 }
103
104 #[inline]
106 pub fn is_valid(&self, now_ms: u64) -> bool {
107 self.current
108 .map(|f| now_ms.saturating_sub(f.updated_at_ms) < self.ttl_ms)
109 .unwrap_or(false)
110 }
111
112 #[inline]
114 pub fn get(&self, now_ms: u64) -> Option<&GasFees> {
115 self.current
116 .as_ref()
117 .filter(|f| now_ms.saturating_sub(f.updated_at_ms) < self.ttl_ms)
118 }
119
120 #[inline]
128 pub fn fees_for(&self, urgency: Urgency, now_ms: u64) -> Option<GasFees> {
129 let base = self.get(now_ms)?;
130 let bf = base.base_fee;
131 let pf = self.default_priority_fee;
132
133 let (max_fee, priority) = match urgency {
134 Urgency::Low => (bf.saturating_add(pf), pf),
135 Urgency::Normal => (2u64.saturating_mul(bf).saturating_add(pf), pf),
136 Urgency::High => (
137 3u64.saturating_mul(bf)
138 .saturating_add(2u64.saturating_mul(pf)),
139 2u64.saturating_mul(pf),
140 ),
141 Urgency::Critical => (
142 4u64.saturating_mul(bf)
143 .saturating_add(5u64.saturating_mul(pf)),
144 5u64.saturating_mul(pf),
145 ),
146 };
147
148 Some(GasFees {
149 base_fee: bf,
150 max_priority_fee_per_gas: priority,
151 max_fee_per_gas: max_fee,
152 updated_at_ms: base.updated_at_ms,
153 })
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 const BASE: u64 = 50_000_000; const TIP: u64 = 1_000_000_000; fn cache_with_fees(now_ms: u64) -> GasCache {
165 let mut c = GasCache::new(2000, TIP);
166 c.update(BASE, now_ms);
167 c
168 }
169
170 #[test]
171 fn empty_cache_is_invalid() {
172 let c = GasCache::new(2000, TIP);
173 assert!(!c.is_valid(0));
174 assert!(c.get(0).is_none());
175 assert!(c.fees_for(Urgency::Normal, 0).is_none());
176 }
177
178 #[test]
179 fn update_makes_cache_valid() {
180 let c = cache_with_fees(1000);
181 assert!(c.is_valid(1000));
182 assert!(c.is_valid(2999)); }
184
185 #[test]
186 fn cache_expires_after_ttl() {
187 let c = cache_with_fees(1000);
188 assert!(c.is_valid(2999));
189 assert!(!c.is_valid(3000)); assert!(!c.is_valid(5000));
191 }
192
193 #[test]
194 fn low_urgency_fees() {
195 let c = cache_with_fees(0);
196 let f = c.fees_for(Urgency::Low, 0).unwrap();
197 assert_eq!(f.max_fee_per_gas, BASE + TIP);
198 assert_eq!(f.max_priority_fee_per_gas, TIP);
199 assert_eq!(f.base_fee, BASE);
200 }
201
202 #[test]
203 fn normal_urgency_fees() {
204 let c = cache_with_fees(0);
205 let f = c.fees_for(Urgency::Normal, 0).unwrap();
206 assert_eq!(f.max_fee_per_gas, 2 * BASE + TIP);
207 assert_eq!(f.max_priority_fee_per_gas, TIP);
208 }
209
210 #[test]
211 fn high_urgency_fees() {
212 let c = cache_with_fees(0);
213 let f = c.fees_for(Urgency::High, 0).unwrap();
214 assert_eq!(f.max_fee_per_gas, 3 * BASE + 2 * TIP);
215 assert_eq!(f.max_priority_fee_per_gas, 2 * TIP);
216 }
217
218 #[test]
219 fn critical_urgency_fees() {
220 let c = cache_with_fees(0);
221 let f = c.fees_for(Urgency::Critical, 0).unwrap();
222 assert_eq!(f.max_fee_per_gas, 4 * BASE + 5 * TIP);
223 assert_eq!(f.max_priority_fee_per_gas, 5 * TIP);
224 }
225
226 #[test]
227 fn urgency_ordering() {
228 let c = cache_with_fees(0);
229 let low = c.fees_for(Urgency::Low, 0).unwrap().max_fee_per_gas;
230 let normal = c.fees_for(Urgency::Normal, 0).unwrap().max_fee_per_gas;
231 let high = c.fees_for(Urgency::High, 0).unwrap().max_fee_per_gas;
232 let critical = c.fees_for(Urgency::Critical, 0).unwrap().max_fee_per_gas;
233 assert!(low < normal);
234 assert!(normal < high);
235 assert!(high < critical);
236 }
237
238 #[test]
239 fn fees_for_stale_returns_none() {
240 let c = cache_with_fees(0);
241 assert!(c.fees_for(Urgency::Normal, 3000).is_none());
242 }
243
244 #[test]
245 fn update_replaces_old_fees() {
246 let mut c = cache_with_fees(0);
247 c.update(100_000_000, 5000); let f = c.fees_for(Urgency::Low, 5000).unwrap();
249 assert_eq!(f.base_fee, 100_000_000);
250 }
251
252 #[test]
253 fn saturating_arithmetic_on_huge_values() {
254 let mut c = GasCache::new(2000, u64::MAX / 2);
255 c.update(u64::MAX / 2, 0);
256 let f = c.fees_for(Urgency::Critical, 0).unwrap();
258 assert_eq!(f.max_fee_per_gas, u64::MAX);
259 }
260
261 #[test]
262 fn preserves_timestamp_across_urgency() {
263 let c = cache_with_fees(42);
264 for urgency in [
265 Urgency::Low,
266 Urgency::Normal,
267 Urgency::High,
268 Urgency::Critical,
269 ] {
270 let f = c.fees_for(urgency, 42).unwrap();
271 assert_eq!(f.updated_at_ms, 42);
272 }
273 }
274
275 #[test]
276 #[allow(clippy::assertions_on_constants)]
277 fn gas_limits_are_reasonable() {
278 assert!(GasLimits::APPROVE > 20_000 && GasLimits::APPROVE < 200_000);
280 assert!(GasLimits::OPEN_TAKER > 200_000 && GasLimits::OPEN_TAKER < 2_000_000);
281 assert!(GasLimits::CLOSE_POSITION > 100_000 && GasLimits::CLOSE_POSITION < 2_000_000);
282 assert!(GasLimits::OPEN_MAKER > GasLimits::OPEN_TAKER);
284 }
285}