Skip to main content

nym_contracts_common/
types.rs

1// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use cosmwasm_schema::cw_serde;
5use cosmwasm_std::OverflowError;
6use cosmwasm_std::Uint128;
7use cosmwasm_std::{Decimal, Fraction};
8use serde::de::Error;
9use serde::{Deserialize, Deserializer};
10use std::fmt::{self, Display, Formatter};
11use std::ops::{Deref, Mul};
12use std::str::FromStr;
13use thiserror::Error;
14
15/// Ed25519 public key strinfified into base58.
16pub type IdentityKey = String;
17pub type IdentityKeyRef<'a> = &'a str;
18
19pub fn truncate_decimal(amount: Decimal) -> Uint128 {
20    Uint128::new(1).mul_floor(amount)
21}
22
23#[derive(Error, Debug)]
24pub enum ContractsCommonError {
25    #[error("Provided percent value ({0}) is greater than 100%")]
26    InvalidPercent(String),
27
28    #[error("{source}")]
29    StdErr {
30        #[from]
31        source: cosmwasm_std::StdError,
32    },
33}
34
35/// Percent represents a value between 0 and 100%
36/// (i.e. between 0.0 and 1.0)
37#[cw_serde]
38#[derive(Copy, Default, PartialOrd, Ord, Eq)]
39pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
40
41impl Percent {
42    pub fn new(value: Decimal) -> Result<Self, ContractsCommonError> {
43        if value > Decimal::one() {
44            Err(ContractsCommonError::InvalidPercent(value.to_string()))
45        } else {
46            Ok(Percent(value))
47        }
48    }
49
50    pub fn is_zero(&self) -> bool {
51        self.0 == Decimal::zero()
52    }
53
54    pub fn is_hundred(&self) -> bool {
55        self == &Self::hundred()
56    }
57
58    pub const fn zero() -> Self {
59        Self(Decimal::zero())
60    }
61
62    pub const fn hundred() -> Self {
63        Self(Decimal::one())
64    }
65
66    pub fn from_percentage_value(value: u64) -> Result<Self, ContractsCommonError> {
67        Percent::new(Decimal::percent(value))
68    }
69
70    pub fn value(&self) -> Decimal {
71        self.0
72    }
73
74    pub fn round_to_integer(&self) -> u8 {
75        let hundred = Decimal::from_ratio(100u32, 1u32);
76        // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
77        truncate_decimal(hundred * self.0).u128() as u8
78    }
79
80    pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> {
81        self.0.checked_pow(exp).map(Percent)
82    }
83
84    // truncate provided percent to only have 2 decimal places,
85    // e.g. convert "0.1234567" into "0.12"
86    // the purpose of it is to reduce storage space, in particular for the performance contract
87    // since that extra precision gains us nothing
88    #[must_use = "this returns the result of the operation, without modifying the original"]
89    pub fn round_to_two_decimal_places(&self) -> Self {
90        let raw = self.0;
91
92        const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18
93        const THRESHOLD: Decimal = Decimal::permille(5); // 0.005
94
95        // in case it ever changes since it's not exposed in the public API
96        debug_assert_eq!(
97            DECIMAL_FRACTIONAL,
98            Uint128::new(10).pow(Decimal::DECIMAL_PLACES)
99        );
100
101        let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL;
102
103        #[allow(clippy::unwrap_used)]
104        let floored = Decimal::from_atomics(int, 2).unwrap();
105        let diff = raw - floored;
106        let rounded = if diff >= THRESHOLD {
107            // ceil
108            floored + Decimal::percent(1)
109        } else {
110            floored
111        };
112        Percent(rounded)
113    }
114
115    #[must_use = "this returns the result of the operation, without modifying the original"]
116    pub fn average(&self, other: &Self) -> Self {
117        let sum = self.0 + other.0;
118        let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2));
119        Percent(inner)
120    }
121}
122
123impl Display for Percent {
124    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
125        let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0;
126        write!(f, "{adjusted}%")
127    }
128}
129
130impl FromStr for Percent {
131    type Err = ContractsCommonError;
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        Percent::new(Decimal::from_str(s)?)
135    }
136}
137
138impl Mul<Decimal> for Percent {
139    type Output = Decimal;
140
141    fn mul(self, rhs: Decimal) -> Self::Output {
142        self.0 * rhs
143    }
144}
145
146impl Mul<Percent> for Decimal {
147    type Output = Decimal;
148
149    fn mul(self, rhs: Percent) -> Self::Output {
150        rhs * self
151    }
152}
153
154impl Fraction<Uint128> for Percent {
155    fn numerator(&self) -> Uint128 {
156        self.0.numerator()
157    }
158
159    fn denominator(&self) -> Uint128 {
160        self.0.denominator()
161    }
162
163    fn inv(&self) -> Option<Self> {
164        Percent::new(self.0.inv()?).ok()
165    }
166}
167
168impl Deref for Percent {
169    type Target = Decimal;
170
171    fn deref(&self) -> &Self::Target {
172        &self.0
173    }
174}
175
176// this is not implemented via From traits due to its naive nature and loss of precision
177#[cfg(feature = "naive_float")]
178pub trait NaiveFloat {
179    fn naive_to_f64(&self) -> f64;
180
181    fn naive_try_from_f64(val: f64) -> Result<Self, ContractsCommonError>
182    where
183        Self: Sized;
184}
185
186#[cfg(feature = "naive_float")]
187impl NaiveFloat for Decimal {
188    fn naive_to_f64(&self) -> f64 {
189        use cosmwasm_std::Fraction;
190
191        // note: this conversion loses precision with too many decimal places,
192        // but for the purposes of displaying basic performance, that's not an issue
193        self.numerator().u128() as f64 / self.denominator().u128() as f64
194    }
195
196    fn naive_try_from_f64(val: f64) -> Result<Self, ContractsCommonError>
197    where
198        Self: Sized,
199    {
200        // we are only interested in positive values between 0 and 1
201        if !(0. ..=1.).contains(&val) {
202            return Err(ContractsCommonError::InvalidPercent(val.to_string()));
203        }
204
205        fn gcd(mut x: u64, mut y: u64) -> u64 {
206            while y > 0 {
207                let rem = x % y;
208                x = y;
209                y = rem;
210            }
211
212            x
213        }
214
215        fn to_rational(x: f64) -> (u64, u64) {
216            let log = x.log2().floor();
217            if log >= 0.0 {
218                (x as u64, 1)
219            } else {
220                let num: u64 = (x / f64::EPSILON) as _;
221                let den: u64 = (1.0 / f64::EPSILON) as _;
222                let gcd = gcd(num, den);
223                (num / gcd, den / gcd)
224            }
225        }
226
227        let (n, d) = to_rational(val);
228        Ok(Decimal::from_ratio(n, d))
229    }
230}
231
232#[cfg(feature = "naive_float")]
233impl NaiveFloat for Percent {
234    fn naive_to_f64(&self) -> f64 {
235        self.0.naive_to_f64()
236    }
237
238    fn naive_try_from_f64(val: f64) -> Result<Self, ContractsCommonError>
239    where
240        Self: Sized,
241    {
242        Percent::new(Decimal::naive_try_from_f64(val)?)
243    }
244}
245
246// implement custom Deserialize because we want to validate Percent has the correct range
247fn de_decimal_percent<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
248where
249    D: Deserializer<'de>,
250{
251    let v = Decimal::deserialize(deserializer)?;
252    if v > Decimal::one() {
253        Err(D::Error::custom(
254            "provided decimal percent is larger than 100%",
255        ))
256    } else {
257        Ok(v)
258    }
259}
260
261fn default_unknown() -> String {
262    "unknown".to_string()
263}
264
265// TODO: there's no reason this couldn't be used for proper binaries, but in that case
266// perhaps the struct should get renamed and moved to a "more" common crate
267#[cw_serde]
268#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
269pub struct ContractBuildInformation {
270    /// Provides the name of the binary, i.e. the content of `CARGO_PKG_NAME` environmental variable.
271    #[serde(default = "default_unknown")]
272    pub contract_name: String,
273
274    // VERGEN_BUILD_TIMESTAMP
275    /// Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`.
276    pub build_timestamp: String,
277
278    // VERGEN_BUILD_SEMVER
279    /// Provides the build version, for example `0.1.0-9-g46f83e1`.
280    pub build_version: String,
281
282    // VERGEN_GIT_SHA
283    /// Provides the hash of the commit that was used for the build, for example `46f83e112520533338245862d366f6a02cef07d4`.
284    pub commit_sha: String,
285
286    // VERGEN_GIT_COMMIT_TIMESTAMP
287    /// Provides the timestamp of the commit that was used for the build, for example `2021-02-23T08:08:02-05:00`.
288    pub commit_timestamp: String,
289
290    // VERGEN_GIT_BRANCH
291    /// Provides the name of the git branch that was used for the build, for example `master`.
292    pub commit_branch: String,
293
294    // VERGEN_RUSTC_SEMVER
295    /// Provides the rustc version that was used for the build, for example `1.52.0-nightly`.
296    pub rustc_version: String,
297
298    // VERGEN_CARGO_DEBUG
299    /// Provides the cargo debug mode that was used for the build.
300    #[serde(default = "default_unknown")]
301    pub cargo_debug: String,
302
303    // VERGEN_CARGO_OPT_LEVEL
304    /// Provides the opt value set by cargo during the build
305    #[serde(default = "default_unknown")]
306    pub cargo_opt_level: String,
307}
308
309impl ContractBuildInformation {
310    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
311        ContractBuildInformation {
312            contract_name: name.into(),
313            build_version: version.into(),
314            build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
315            commit_sha: option_env!("VERGEN_GIT_SHA")
316                .unwrap_or("UNKNOWN")
317                .to_string(),
318            commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
319                .unwrap_or("UNKNOWN")
320                .to_string(),
321            commit_branch: option_env!("VERGEN_GIT_BRANCH")
322                .unwrap_or("UNKNOWN")
323                .to_string(),
324            rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
325            cargo_debug: env!("VERGEN_CARGO_DEBUG").to_string(),
326            cargo_opt_level: env!("VERGEN_CARGO_OPT_LEVEL").to_string(),
327        }
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334
335    #[test]
336    fn percent_serde() {
337        let valid_value = Percent::from_percentage_value(80).unwrap();
338        let serialized = serde_json::to_string(&valid_value).unwrap();
339
340        let deserialized: Percent = serde_json::from_str(&serialized).unwrap();
341        assert_eq!(valid_value, deserialized);
342
343        let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""];
344        for invalid_value in invalid_values {
345            assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err())
346        }
347        assert_eq!(
348            serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(),
349            Percent::from_percentage_value(95).unwrap()
350        )
351    }
352
353    #[test]
354    fn percent_to_absolute_integer() {
355        let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap();
356        assert_eq!(p.round_to_integer(), 0);
357
358        let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap();
359        assert_eq!(p.round_to_integer(), 0);
360
361        let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap();
362        assert_eq!(p.round_to_integer(), 1);
363
364        let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap();
365        assert_eq!(p.round_to_integer(), 45);
366
367        let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap();
368        assert_eq!(p.round_to_integer(), 99);
369
370        let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap();
371        assert_eq!(p.round_to_integer(), 100);
372    }
373
374    #[test]
375    #[cfg(feature = "naive_float")]
376    fn naive_float_conversion() {
377        // around 15 decimal places is the maximum precision we can handle
378        // which is still way more than enough for what we use it for
379        let float: f64 = "0.546295475423853".parse().unwrap();
380        let percent: Percent = "0.546295475423853".parse().unwrap();
381
382        assert_eq!(float, percent.naive_to_f64());
383
384        let epsilon = Decimal::from_ratio(1u64, 1000000000000000u64);
385        let converted = Percent::naive_try_from_f64(float).unwrap();
386
387        assert!(converted.0 - converted.0 < epsilon);
388    }
389
390    #[test]
391    fn rounding_percent() {
392        let test_cases = vec![
393            ("0", "0"),
394            ("0.1", "0.1"),
395            ("0.12", "0.12"),
396            ("0.12", "0.123"),
397            ("0.12", "0.123456789"),
398            ("0.13", "0.125"),
399            ("0.13", "0.126"),
400            ("0.13", "0.126436545676"),
401            ("0.99", "0.99"),
402            ("0.99", "0.994"),
403            ("1", "0.999"),
404            ("1", "0.995"),
405        ];
406        for (expected, input) in test_cases {
407            let expected: Percent = expected.parse().unwrap();
408            let pre_truncated: Percent = input.parse().unwrap();
409            assert_eq!(expected, pre_truncated.round_to_two_decimal_places())
410        }
411    }
412
413    #[test]
414    fn calculating_average() -> anyhow::Result<()> {
415        fn p(raw: &str) -> Percent {
416            raw.parse().unwrap()
417        }
418
419        assert_eq!(p("0.1").average(&p("0.1")), p("0.1"));
420        assert_eq!(p("0.1").average(&p("0.2")), p("0.15"));
421        assert_eq!(p("1").average(&p("0")), p("0.5"));
422        assert_eq!(p("0.123").average(&p("0.456")), p("0.2895"));
423
424        Ok(())
425    }
426}