1use core::{
2 fmt::{self, Display},
3 ops::{Add, Div, Mul, Not, Sub},
4};
5
6use defuse_num_utils::{CheckedAdd, CheckedMulDiv, CheckedSub};
7use thiserror::Error as ThisError;
8
9#[cfg_attr(
11 feature = "borsh",
12 derive(::borsh::BorshSerialize),
13 cfg_attr(feature = "abi", derive(::borsh::BorshSchema))
14)]
15#[cfg_attr(
16 feature = "serde",
17 derive(::serde::Serialize, ::serde::Deserialize),
18 cfg_attr(feature = "abi", derive(::schemars::JsonSchema)),
19 serde(try_from = "u32")
20)]
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
22pub struct Pips(u32);
23
24impl Pips {
25 pub const ZERO: Self = Self(0);
26 pub const ONE_PIP: Self = Self(1);
27 pub const ONE_BIP: Self = Self(Self::ONE_PIP.as_pips() * 100);
28 pub const ONE_PERCENT: Self = Self(Self::ONE_BIP.as_pips() * 100);
29 pub const MAX: Self = Self(Self::ONE_PERCENT.as_pips() * 100);
30
31 #[inline]
32 pub const fn from_pips(pips: u32) -> Option<Self> {
33 if pips > Self::MAX.as_pips() {
34 return None;
35 }
36 Some(Self(pips))
37 }
38
39 #[inline]
40 pub const fn from_bips(bips: u32) -> Option<Self> {
41 Self::ONE_BIP.checked_mul(bips)
42 }
43
44 #[inline]
45 pub const fn from_percent(percent: u32) -> Option<Self> {
46 Self::ONE_PERCENT.checked_mul(percent)
47 }
48
49 #[inline]
50 pub const fn as_pips(self) -> u32 {
51 self.0
52 }
53
54 #[inline]
55 pub const fn as_bips(self) -> u32 {
56 self.as_pips() / Self::ONE_BIP.as_pips()
57 }
58
59 #[inline]
60 pub const fn as_percent(self) -> u32 {
61 self.as_pips() / Self::ONE_PERCENT.as_pips()
62 }
63
64 #[inline]
65 pub const fn is_zero(&self) -> bool {
66 self.0 == 0
67 }
68
69 #[inline]
70 pub fn as_f64(self) -> f64 {
71 f64::from(self.as_pips()) / f64::from(Self::MAX.as_pips())
72 }
73
74 pub const fn checked_add(self, rhs: Self) -> Option<Self> {
75 let Some(pips) = self.as_pips().checked_add(rhs.as_pips()) else {
76 return None;
77 };
78 Self::from_pips(pips)
79 }
80
81 pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
82 let Some(pips) = self.as_pips().checked_sub(rhs.as_pips()) else {
83 return None;
84 };
85 Self::from_pips(pips)
86 }
87
88 #[inline]
89 pub const fn checked_mul(self, rhs: u32) -> Option<Self> {
90 let Some(pips) = self.as_pips().checked_mul(rhs) else {
91 return None;
92 };
93 Self::from_pips(pips)
94 }
95
96 #[inline]
97 pub const fn checked_div(self, rhs: u32) -> Option<Self> {
98 let Some(pips) = self.as_pips().checked_div(rhs) else {
99 return None;
100 };
101 Some(Self(pips))
102 }
103
104 #[must_use]
105 #[inline]
106 pub const fn invert(self) -> Self {
107 Self(Self::MAX.as_pips() - self.as_pips())
108 }
109
110 #[inline]
111 pub fn fee(self, amount: u128) -> u128 {
112 amount
113 .checked_mul_div(self.as_pips().into(), Self::MAX.as_pips().into())
114 .unwrap_or_else(|| unreachable!())
115 }
116
117 #[inline]
118 pub fn fee_ceil(self, amount: u128) -> u128 {
119 amount
120 .checked_mul_div_ceil(self.as_pips().into(), Self::MAX.as_pips().into())
121 .unwrap_or_else(|| unreachable!())
122 }
123}
124
125impl CheckedAdd for Pips {
126 #[inline]
127 fn checked_add(self, rhs: Self) -> Option<Self> {
128 self.checked_add(rhs)
129 }
130}
131
132impl Add for Pips {
133 type Output = Self;
134
135 #[inline]
136 fn add(self, rhs: Self) -> Self::Output {
137 self.checked_add(rhs).unwrap()
138 }
139}
140
141impl CheckedSub for Pips {
142 #[inline]
143 fn checked_sub(self, rhs: Self) -> Option<Self> {
144 self.checked_sub(rhs)
145 }
146}
147
148impl Sub for Pips {
149 type Output = Self;
150
151 #[inline]
152 fn sub(self, rhs: Self) -> Self::Output {
153 self.checked_sub(rhs).unwrap()
154 }
155}
156
157impl Mul<u32> for Pips {
158 type Output = Self;
159
160 #[inline]
161 fn mul(self, rhs: u32) -> Self::Output {
162 self.checked_mul(rhs).unwrap()
163 }
164}
165
166impl Div<u32> for Pips {
167 type Output = Self;
168
169 #[inline]
170 fn div(self, rhs: u32) -> Self::Output {
171 self.checked_div(rhs).unwrap()
172 }
173}
174
175impl Not for Pips {
176 type Output = Self;
177
178 fn not(self) -> Self::Output {
179 self.invert()
180 }
181}
182
183impl Display for Pips {
184 #[inline]
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 write!(f, "{:.4}%", self.as_f64() * 100f64)
187 }
188}
189
190impl TryFrom<u32> for Pips {
191 type Error = PipsOutOfRange;
192
193 #[inline]
194 fn try_from(pips: u32) -> Result<Self, Self::Error> {
195 Self::from_pips(pips).ok_or(PipsOutOfRange)
196 }
197}
198
199#[derive(Debug, ThisError)]
200#[error("out of range: 0..={}", Pips::MAX.as_pips())]
201pub struct PipsOutOfRange;
202
203#[cfg(feature = "borsh")]
204impl ::borsh::BorshDeserialize for Pips {
205 fn deserialize_reader<R: std::io::Read>(reader: &mut R) -> std::io::Result<Self> {
206 let pips = u32::deserialize_reader(reader)?;
207 Self::from_pips(pips).ok_or_else(|| {
208 std::io::Error::new(
209 std::io::ErrorKind::InvalidData,
210 format!("pips: {pips} is {PipsOutOfRange}"),
211 )
212 })
213 }
214}