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