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