odos_sdk/types/slippage.rs
1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use std::fmt;
6
7use serde::{Deserialize, Serialize};
8
9/// Type-safe slippage percentage with validation
10///
11/// Ensures slippage values are within valid ranges and provides convenient
12/// constructors for both percentage and basis point representations.
13///
14/// # Examples
15///
16/// ```rust
17/// use odos_sdk::Slippage;
18///
19/// // Create from percentage (0.5% slippage)
20/// let slippage = Slippage::percent(0.5)?;
21/// assert_eq!(slippage.as_percent(), 0.5);
22///
23/// // Create from basis points (50 bps = 0.5%)
24/// let slippage = Slippage::bps(50)?;
25/// assert_eq!(slippage.as_percent(), 0.5);
26/// assert_eq!(slippage.as_bps(), 50);
27///
28/// // Validation prevents invalid values
29/// assert!(Slippage::percent(150.0).is_err()); // > 100%
30/// assert!(Slippage::percent(-1.0).is_err()); // < 0%
31/// assert!(Slippage::bps(15000).is_err()); // > 10000 bps
32/// # Ok::<(), Box<dyn std::error::Error>>(())
33/// ```
34#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct Slippage(f64);
37
38impl Slippage {
39 /// Create slippage from percentage value
40 ///
41 /// # Arguments
42 ///
43 /// * `percent` - Slippage as percentage (e.g., 0.5 for 0.5%, 1.0 for 1%)
44 ///
45 /// # Returns
46 ///
47 /// * `Ok(Slippage)` - Valid slippage value
48 /// * `Err(String)` - If percentage is < 0 or > 100
49 ///
50 /// # Examples
51 ///
52 /// ```rust
53 /// use odos_sdk::Slippage;
54 ///
55 /// let slippage = Slippage::percent(0.5)?; // 0.5%
56 /// assert_eq!(slippage.as_percent(), 0.5);
57 ///
58 /// let slippage = Slippage::percent(1.0)?; // 1%
59 /// assert_eq!(slippage.as_percent(), 1.0);
60 ///
61 /// // Validation
62 /// assert!(Slippage::percent(150.0).is_err());
63 /// assert!(Slippage::percent(-0.1).is_err());
64 /// # Ok::<(), Box<dyn std::error::Error>>(())
65 /// ```
66 pub fn percent(percent: f64) -> Result<Self, String> {
67 if percent < 0.0 {
68 return Err(format!("Slippage percentage cannot be negative: {percent}"));
69 }
70 if percent > 100.0 {
71 return Err(format!("Slippage percentage cannot exceed 100%: {percent}"));
72 }
73 Ok(Self(percent))
74 }
75
76 /// Create slippage from basis points
77 ///
78 /// # Arguments
79 ///
80 /// * `bps` - Slippage in basis points (e.g., 50 for 0.5%, 100 for 1%)
81 ///
82 /// # Returns
83 ///
84 /// * `Ok(Slippage)` - Valid slippage value
85 /// * `Err(String)` - If basis points > 10000 (100%)
86 ///
87 /// # Examples
88 ///
89 /// ```rust
90 /// use odos_sdk::Slippage;
91 ///
92 /// let slippage = Slippage::bps(50)?; // 50 bps = 0.5%
93 /// assert_eq!(slippage.as_bps(), 50);
94 /// assert_eq!(slippage.as_percent(), 0.5);
95 ///
96 /// let slippage = Slippage::bps(100)?; // 100 bps = 1%
97 /// assert_eq!(slippage.as_bps(), 100);
98 /// assert_eq!(slippage.as_percent(), 1.0);
99 ///
100 /// // Validation
101 /// assert!(Slippage::bps(15000).is_err());
102 /// # Ok::<(), Box<dyn std::error::Error>>(())
103 /// ```
104 pub fn bps(bps: u16) -> Result<Self, String> {
105 if bps > 10000 {
106 return Err(format!(
107 "Slippage basis points cannot exceed 10000 (100%): {bps}"
108 ));
109 }
110 Ok(Self(bps as f64 / 100.0))
111 }
112
113 /// Get slippage value as percentage
114 ///
115 /// # Examples
116 ///
117 /// ```rust
118 /// use odos_sdk::Slippage;
119 ///
120 /// let slippage = Slippage::percent(0.5)?;
121 /// assert_eq!(slippage.as_percent(), 0.5);
122 /// # Ok::<(), Box<dyn std::error::Error>>(())
123 /// ```
124 pub fn as_percent(&self) -> f64 {
125 self.0
126 }
127
128 /// Get slippage value as basis points
129 ///
130 /// # Examples
131 ///
132 /// ```rust
133 /// use odos_sdk::Slippage;
134 ///
135 /// let slippage = Slippage::percent(0.5)?;
136 /// assert_eq!(slippage.as_bps(), 50);
137 ///
138 /// let slippage = Slippage::bps(100)?;
139 /// assert_eq!(slippage.as_bps(), 100);
140 /// # Ok::<(), Box<dyn std::error::Error>>(())
141 /// ```
142 pub fn as_bps(&self) -> u16 {
143 (self.0 * 100.0).round() as u16
144 }
145
146 /// Common slippage values for convenience
147 pub const ZERO: Result<Self, &'static str> = Ok(Self(0.0));
148
149 /// 0.1% slippage (10 basis points)
150 pub fn low() -> Self {
151 Self(0.1)
152 }
153
154 /// 0.5% slippage (50 basis points) - recommended for most swaps
155 pub fn standard() -> Self {
156 Self(0.5)
157 }
158
159 /// 1% slippage (100 basis points)
160 pub fn medium() -> Self {
161 Self(1.0)
162 }
163
164 /// 3% slippage (300 basis points) - high slippage for volatile pairs
165 pub fn high() -> Self {
166 Self(3.0)
167 }
168}
169
170impl fmt::Display for Slippage {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(f, "{:.2}%", self.0)
173 }
174}
175
176impl From<Slippage> for f64 {
177 fn from(slippage: Slippage) -> Self {
178 slippage.0
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_percent_constructor() {
188 let slippage = Slippage::percent(0.5).unwrap();
189 assert_eq!(slippage.as_percent(), 0.5);
190
191 let slippage = Slippage::percent(1.0).unwrap();
192 assert_eq!(slippage.as_percent(), 1.0);
193
194 let slippage = Slippage::percent(100.0).unwrap();
195 assert_eq!(slippage.as_percent(), 100.0);
196
197 // Edge case: 0%
198 let slippage = Slippage::percent(0.0).unwrap();
199 assert_eq!(slippage.as_percent(), 0.0);
200 }
201
202 #[test]
203 fn test_percent_validation() {
204 // Too high
205 assert!(Slippage::percent(100.1).is_err());
206 assert!(Slippage::percent(150.0).is_err());
207
208 // Negative
209 assert!(Slippage::percent(-0.1).is_err());
210 assert!(Slippage::percent(-50.0).is_err());
211 }
212
213 #[test]
214 fn test_bps_constructor() {
215 let slippage = Slippage::bps(50).unwrap();
216 assert_eq!(slippage.as_bps(), 50);
217 assert_eq!(slippage.as_percent(), 0.5);
218
219 let slippage = Slippage::bps(100).unwrap();
220 assert_eq!(slippage.as_bps(), 100);
221 assert_eq!(slippage.as_percent(), 1.0);
222
223 let slippage = Slippage::bps(10000).unwrap();
224 assert_eq!(slippage.as_bps(), 10000);
225 assert_eq!(slippage.as_percent(), 100.0);
226
227 // Edge case: 0 bps
228 let slippage = Slippage::bps(0).unwrap();
229 assert_eq!(slippage.as_bps(), 0);
230 assert_eq!(slippage.as_percent(), 0.0);
231 }
232
233 #[test]
234 fn test_bps_validation() {
235 // Too high
236 assert!(Slippage::bps(10001).is_err());
237 assert!(Slippage::bps(15000).is_err());
238 assert!(Slippage::bps(u16::MAX).is_err());
239 }
240
241 #[test]
242 fn test_convenience_methods() {
243 assert_eq!(Slippage::low().as_percent(), 0.1);
244 assert_eq!(Slippage::standard().as_percent(), 0.5);
245 assert_eq!(Slippage::medium().as_percent(), 1.0);
246 assert_eq!(Slippage::high().as_percent(), 3.0);
247 }
248
249 #[test]
250 fn test_display() {
251 let slippage = Slippage::percent(0.5).unwrap();
252 assert_eq!(format!("{slippage}"), "0.50%");
253
254 let slippage = Slippage::bps(100).unwrap();
255 assert_eq!(format!("{slippage}"), "1.00%");
256
257 let slippage = Slippage::high();
258 assert_eq!(format!("{slippage}"), "3.00%");
259 }
260
261 #[test]
262 fn test_conversions() {
263 let slippage = Slippage::percent(0.5).unwrap();
264
265 // as_percent
266 assert_eq!(slippage.as_percent(), 0.5);
267
268 // as_bps
269 assert_eq!(slippage.as_bps(), 50);
270
271 // Into f64
272 let value: f64 = slippage.into();
273 assert_eq!(value, 0.5);
274 }
275
276 #[test]
277 fn test_serialization() {
278 let slippage = Slippage::percent(0.5).unwrap();
279
280 // Serialize
281 let json = serde_json::to_string(&slippage).unwrap();
282 assert_eq!(json, "0.5");
283
284 // Deserialize
285 let deserialized: Slippage = serde_json::from_str(&json).unwrap();
286 assert_eq!(deserialized.as_percent(), 0.5);
287 }
288
289 #[test]
290 fn test_equality_and_ordering() {
291 let s1 = Slippage::percent(0.5).unwrap();
292 let s2 = Slippage::bps(50).unwrap();
293 let s3 = Slippage::percent(1.0).unwrap();
294
295 assert_eq!(s1, s2);
296 assert_ne!(s1, s3);
297 assert!(s1 < s3);
298 assert!(s3 > s1);
299 }
300}