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