hopr_primitive_types/
balance.rs

1use std::{
2    fmt::{Display, Formatter},
3    ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign},
4    str::FromStr,
5};
6
7use bigdecimal::{
8    BigDecimal,
9    num_bigint::{BigInt, BigUint, ToBigInt},
10};
11
12use crate::{
13    errors::GeneralError,
14    prelude::{IntoEndian, U256},
15    traits::UnitaryFloatOps,
16};
17
18/// Represents a general currency - like a token or a coin.
19pub trait Currency: Display + FromStr<Err = GeneralError> + Default + PartialEq + Eq {
20    /// Name of the currency.
21    const NAME: &'static str;
22
23    /// Base unit exponent used for the currency.
24    const SCALE: usize;
25
26    /// Checks if this currency is the same as the one given in the template argument.
27    fn is<C: Currency>() -> bool {
28        Self::NAME == C::NAME
29    }
30
31    /// Returns `Ok(())` if the given string is equal to the currency name.
32    fn name_matches(s: &str) -> Result<(), GeneralError> {
33        if s.eq_ignore_ascii_case(Self::NAME) {
34            Ok(())
35        } else {
36            Err(GeneralError::ParseError("invalid currency name".into()))
37        }
38    }
39}
40
41/// Represents wxHOPR token [`Currency`].
42#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct WxHOPR;
45
46impl Display for WxHOPR {
47    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48        write!(f, "{}", Self::NAME)
49    }
50}
51
52impl FromStr for WxHOPR {
53    type Err = GeneralError;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        Self::name_matches(s).map(|_| Self)
57    }
58}
59
60impl Currency for WxHOPR {
61    const NAME: &'static str = "wxHOPR";
62    const SCALE: usize = 18;
63}
64
65/// Represents xDai coin [`Currency`].
66#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68pub struct XDai;
69
70impl Display for XDai {
71    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
72        write!(f, "{}", Self::NAME)
73    }
74}
75
76impl FromStr for XDai {
77    type Err = GeneralError;
78
79    fn from_str(s: &str) -> Result<Self, Self::Err> {
80        Self::name_matches(s).map(|_| Self)
81    }
82}
83
84impl Currency for XDai {
85    const NAME: &'static str = "xDai";
86    const SCALE: usize = 18;
87}
88
89/// Represents a non-negative balance of some [`Currency`].
90///
91/// The value is internally always stored in `wei` but always printed in human-readable format.
92///
93/// All arithmetic on this type is implicitly saturating at bounds given by [`U256`].
94#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub struct Balance<C: Currency>(U256, C);
97
98const WEI_PREFIX: &str = "wei";
99
100lazy_static::lazy_static! {
101    static ref BALANCE_REGEX: regex::Regex = regex::Regex::new(&format!("^([\\d\\s.]*\\d)\\s+({WEI_PREFIX}[_\\s]?)?([A-Za-z]+)$")).unwrap();
102}
103
104impl<C: Currency> Display for Balance<C> {
105    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
106        write!(f, "{} {}", self.amount_in_base_units(), self.1)
107    }
108}
109
110impl<C: Currency> std::fmt::Debug for Balance<C> {
111    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112        // Intentionally same as Display
113        write!(f, "{} {}", self.amount_in_base_units(), self.1)
114    }
115}
116
117impl<C: Currency> FromStr for Balance<C> {
118    type Err = GeneralError;
119
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        let captures = BALANCE_REGEX
122            .captures(s)
123            .ok_or(GeneralError::ParseError("cannot parse balance".into()))?;
124
125        // Fail-fast if the currency name is not valid
126        let currency = C::from_str(&captures[3])?;
127
128        let mut value = BigDecimal::from_str(&captures[1].replace(' ', ""))
129            .map_err(|_| GeneralError::ParseError("invalid balance value".into()))?;
130
131        // If the value is not given in wei, it must be multiplied by the scale
132        if captures.get(2).is_none() {
133            value *= BigInt::from(10).pow(C::SCALE as u32);
134        }
135
136        // This discards any excess fractional digits after 10e-SCALE
137        let biguint_val = value
138            .to_bigint()
139            .and_then(|b| b.to_biguint())
140            .expect("conversion to big unsigned integer never fails");
141
142        if biguint_val > BigUint::from_bytes_be(&U256::max_value().to_be_bytes()) {
143            return Err(GeneralError::ParseError("balance value out of bounds".into()));
144        }
145
146        Ok(Self(U256::from_be_bytes(biguint_val.to_bytes_be()), currency))
147    }
148}
149
150impl<C: Currency, T: Into<U256>> From<T> for Balance<C> {
151    fn from(value: T) -> Self {
152        Self(value.into(), C::default())
153    }
154}
155
156impl<C: Currency> AsRef<U256> for Balance<C> {
157    fn as_ref(&self) -> &U256 {
158        &self.0
159    }
160}
161
162impl<C: Currency> Balance<C> {
163    /// Creates new balance in base units, instead of `wei`.
164    pub fn new_base<T: Into<U256>>(value: T) -> Self {
165        Self(value.into() * U256::exp10(C::SCALE), C::default())
166    }
167
168    /// Zero balance.
169    pub fn zero() -> Self {
170        Self(U256::zero(), C::default())
171    }
172
173    /// Checks if the balance is zero.
174    pub fn is_zero(&self) -> bool {
175        self.0.is_zero()
176    }
177
178    /// Gets the amount in `wei`.
179    pub fn amount(&self) -> U256 {
180        self.0
181    }
182
183    fn base_amount(&self) -> BigDecimal {
184        BigDecimal::from_biguint(
185            bigdecimal::num_bigint::BigUint::from_bytes_be(&self.0.to_be_bytes()),
186            C::SCALE as i64,
187        )
188    }
189
190    /// Returns the amount in base units (human-readable), not in `wei`.
191    pub fn amount_in_base_units(&self) -> String {
192        let dec = self.base_amount();
193        let str = dec.to_plain_string();
194
195        // Trim excess zeroes if any
196        if dec.fractional_digit_count() > 0 {
197            str.trim_end_matches('0').trim_end_matches('.').to_owned()
198        } else {
199            str
200        }
201    }
202
203    /// Prints the balance formated in `wei` units.
204    pub fn format_in_wei(&self) -> String {
205        format!("{} {} {}", self.0, WEI_PREFIX, self.1)
206    }
207}
208
209impl<C: Currency> IntoEndian<32> for Balance<C> {
210    fn from_be_bytes<T: AsRef<[u8]>>(bytes: T) -> Self {
211        Self(U256::from_be_bytes(bytes.as_ref()), C::default())
212    }
213
214    fn from_le_bytes<T: AsRef<[u8]>>(bytes: T) -> Self {
215        Self(U256::from_le_bytes(bytes.as_ref()), C::default())
216    }
217
218    fn to_le_bytes(self) -> [u8; 32] {
219        self.0.to_le_bytes()
220    }
221
222    fn to_be_bytes(self) -> [u8; 32] {
223        self.0.to_be_bytes()
224    }
225}
226
227impl<C: Currency> Add for Balance<C> {
228    type Output = Self;
229
230    fn add(self, rhs: Self) -> Self::Output {
231        Self(self.0.saturating_add(rhs.0), C::default())
232    }
233}
234
235impl<C: Currency, T: Into<U256>> Add<T> for Balance<C> {
236    type Output = Self;
237
238    fn add(self, rhs: T) -> Self::Output {
239        Self(self.0.saturating_add(rhs.into()), C::default())
240    }
241}
242
243impl<C: Currency> AddAssign for Balance<C> {
244    fn add_assign(&mut self, rhs: Self) {
245        self.0 = self.0.saturating_add(rhs.0);
246    }
247}
248
249impl<C: Currency, T: Into<U256>> AddAssign<T> for Balance<C> {
250    fn add_assign(&mut self, rhs: T) {
251        self.0 = self.0.saturating_add(rhs.into());
252    }
253}
254
255impl<C: Currency> Sub for Balance<C> {
256    type Output = Self;
257
258    fn sub(self, rhs: Self) -> Self::Output {
259        Self(self.0.saturating_sub(rhs.0), C::default())
260    }
261}
262
263impl<C: Currency, T: Into<U256>> Sub<T> for Balance<C> {
264    type Output = Self;
265
266    fn sub(self, rhs: T) -> Self::Output {
267        Self(self.0.saturating_sub(rhs.into()), C::default())
268    }
269}
270
271impl<C: Currency> SubAssign for Balance<C> {
272    fn sub_assign(&mut self, rhs: Self) {
273        self.0 = self.0.saturating_sub(rhs.0);
274    }
275}
276
277impl<C: Currency, T: Into<U256>> SubAssign<T> for Balance<C> {
278    fn sub_assign(&mut self, rhs: T) {
279        self.0 = self.0.saturating_sub(rhs.into());
280    }
281}
282
283impl<C: Currency> Mul for Balance<C> {
284    type Output = Self;
285
286    fn mul(self, rhs: Self) -> Self::Output {
287        Self(self.0.saturating_mul(rhs.0), C::default())
288    }
289}
290
291impl<C: Currency, T: Into<U256>> Mul<T> for Balance<C> {
292    type Output = Self;
293
294    fn mul(self, rhs: T) -> Self::Output {
295        Self(self.0.saturating_mul(rhs.into()), C::default())
296    }
297}
298
299impl<C: Currency> MulAssign for Balance<C> {
300    fn mul_assign(&mut self, rhs: Self) {
301        self.0 = self.0.saturating_mul(rhs.0);
302    }
303}
304
305impl<C: Currency, T: Into<U256>> MulAssign<T> for Balance<C> {
306    fn mul_assign(&mut self, rhs: T) {
307        self.0 = self.0.saturating_mul(rhs.into());
308    }
309}
310
311impl<C: Currency> std::iter::Sum for Balance<C> {
312    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
313        iter.fold(Self::zero(), |acc, x| acc + x)
314    }
315}
316
317impl<C: Currency> UnitaryFloatOps for Balance<C> {
318    fn mul_f64(&self, rhs: f64) -> crate::errors::Result<Self> {
319        self.0.mul_f64(rhs).map(|x| Self(x, C::default()))
320    }
321
322    fn div_f64(&self, rhs: f64) -> crate::errors::Result<Self> {
323        self.0.div_f64(rhs).map(|x| Self(x, C::default()))
324    }
325}
326
327pub type HoprBalance = Balance<WxHOPR>;
328
329pub type XDaiBalance = Balance<XDai>;
330
331#[cfg(test)]
332mod tests {
333    use std::ops::Div;
334
335    use super::*;
336    use crate::primitives::U256;
337
338    #[test]
339    fn balance_is_not_zero_when_not_zero() {
340        assert!(!HoprBalance::from(1).is_zero())
341    }
342
343    #[test]
344    fn balance_zero_is_zero() {
345        assert_eq!(HoprBalance::zero(), HoprBalance::from(0));
346        assert!(HoprBalance::zero().is_zero());
347        assert!(HoprBalance::zero().amount().is_zero());
348    }
349
350    #[test]
351    fn balance_should_have_zero_default() {
352        assert_eq!(HoprBalance::default(), HoprBalance::zero());
353        assert!(HoprBalance::default().is_zero());
354    }
355
356    #[test]
357    fn balance_should_saturate_on_bounds() {
358        let b1 = HoprBalance::from(U256::max_value());
359        let b2 = b1 + HoprBalance::from(1);
360        assert_eq!(b1.amount(), U256::max_value());
361        assert!(!b2.is_zero());
362        assert_eq!(b2.amount(), U256::max_value());
363
364        let b1 = HoprBalance::zero();
365        let b2 = b1 - HoprBalance::from(1);
366        assert_eq!(b1.amount(), U256::zero());
367        assert!(b2.is_zero());
368        assert_eq!(b2.amount(), U256::zero());
369
370        let b1 = HoprBalance::from(U256::max_value());
371        let b2 = b1 * HoprBalance::from(2);
372        assert_eq!(b1.amount(), U256::max_value());
373        assert!(!b2.is_zero());
374        assert_eq!(b2.amount(), U256::max_value());
375    }
376
377    #[test]
378    fn balance_should_print_different_units() {
379        let b1: HoprBalance = 10.into();
380        let b2: XDaiBalance = 10.into();
381
382        assert_ne!(b1.to_string(), b2.to_string());
383    }
384
385    #[test]
386    fn balance_parsing_should_fail_with_invalid_units() {
387        assert!(HoprBalance::from_str("10").is_err());
388        assert!(HoprBalance::from_str("10 wei").is_err());
389        assert!(HoprBalance::from_str("10 wai wxHOPR").is_err());
390        assert!(HoprBalance::from_str("10 wxxHOPR").is_err());
391    }
392
393    #[test]
394    fn balance_parsing_should_fail_when_out_of_bounds() {
395        let too_big = primitive_types::U512::from(U256::max_value()) + 1;
396        assert!(HoprBalance::from_str(&format!("{too_big} wei wxHOPR")).is_err());
397
398        let too_big =
399            (primitive_types::U512::from(U256::max_value()) + 1).div(primitive_types::U512::exp10(WxHOPR::SCALE)) + 1;
400        assert!(HoprBalance::from_str(&format!("{too_big} wxHOPR")).is_err());
401    }
402
403    #[test]
404    fn balance_should_discard_excess_fractional_digits() -> anyhow::Result<()> {
405        let balance: HoprBalance = "1.12345678901234567891 wxHOPR".parse()?;
406        assert_eq!("1.123456789012345678 wxHOPR", balance.to_string());
407
408        let balance: HoprBalance = "123.12345678901234567891 wxHOPR".parse()?;
409        assert_eq!("123.123456789012345678 wxHOPR", balance.to_string());
410
411        let balance: HoprBalance = "1.12345678901234567891 wei wxHOPR".parse()?;
412        assert_eq!("0.000000000000000001 wxHOPR", balance.to_string());
413
414        Ok(())
415    }
416
417    #[test]
418    fn balance_should_not_parse_from_different_units() {
419        assert!(HoprBalance::from_str(&XDaiBalance::from(10).to_string()).is_err());
420    }
421
422    #[test]
423    fn balance_should_translate_from_non_wei_units() {
424        let balance = HoprBalance::new_base(10);
425        assert_eq!(balance.amount(), U256::from(10) * U256::exp10(WxHOPR::SCALE));
426        assert_eq!(balance.amount_in_base_units(), "10");
427    }
428
429    #[test]
430    fn balance_should_parse_from_non_wei_string() -> anyhow::Result<()> {
431        let balance = HoprBalance::from_str("5 wxHOPR")?;
432        assert_eq!(balance, Balance::new_base(5));
433
434        let balance = HoprBalance::from_str("5 wxhopr")?;
435        assert_eq!(balance, Balance::new_base(5));
436
437        let balance = HoprBalance::from_str(".5 wxHOPR")?;
438        assert_eq!(balance.amount(), U256::from(5) * U256::exp10(WxHOPR::SCALE - 1));
439
440        let balance = HoprBalance::from_str(" .5 wxHOPR")?;
441        assert_eq!(balance.amount(), U256::from(5) * U256::exp10(WxHOPR::SCALE - 1));
442
443        let balance = HoprBalance::from_str("0.5 wxHOPR")?;
444        assert_eq!(balance.amount(), U256::from(5) * U256::exp10(WxHOPR::SCALE - 1));
445
446        let balance = HoprBalance::from_str("0. 5 wxHOPR")?;
447        assert_eq!(balance.amount(), U256::from(5) * U256::exp10(WxHOPR::SCALE - 1));
448
449        let balance = HoprBalance::from_str("0. 50 wxHOPR")?;
450        assert_eq!(balance.amount(), U256::from(5) * U256::exp10(WxHOPR::SCALE - 1));
451
452        let balance = HoprBalance::from_str("0. 5 0 wxHOPR")?;
453        assert_eq!(balance.amount(), U256::from(5) * U256::exp10(WxHOPR::SCALE - 1));
454
455        Ok(())
456    }
457
458    #[test]
459    fn balance_should_parse_from_wei_string() -> anyhow::Result<()> {
460        let balance = HoprBalance::from_str("5 weiwxHOPR")?;
461        assert_eq!(balance.amount(), 5.into());
462
463        let balance = HoprBalance::from_str(" 5 weiwxHOPR")?;
464        assert_eq!(balance.amount(), 5.into());
465
466        let balance = HoprBalance::from_str("5 000 weiwxHOPR")?;
467        assert_eq!(balance.amount(), 5000.into());
468
469        let balance = HoprBalance::from_str("5 0 0 0 weiwxHOPR")?;
470        assert_eq!(balance.amount(), 5000.into());
471
472        let balance = HoprBalance::from_str("5 wei_wxHOPR")?;
473        assert_eq!(balance.amount(), 5.into());
474
475        let balance = HoprBalance::from_str("5 wei wxHOPR")?;
476        assert_eq!(balance.amount(), 5.into());
477
478        let balance = HoprBalance::from_str("5 wei wxhopr")?;
479        assert_eq!(balance.amount(), 5.into());
480
481        Ok(())
482    }
483
484    #[test]
485    fn balance_should_parse_from_formatted_string() -> anyhow::Result<()> {
486        let balance = HoprBalance::from_str("5.0123 wxHOPR")?;
487        assert_eq!(balance.amount(), U256::from(50123) * U256::exp10(WxHOPR::SCALE - 4));
488
489        let balance = HoprBalance::from_str("5.001 weiwxHOPR")?;
490        assert_eq!(balance.amount(), 5.into());
491
492        let balance = HoprBalance::from_str("5.00 weiwxHOPR")?;
493        assert_eq!(balance.amount(), 5.into());
494
495        let balance = HoprBalance::from_str("5.00 wei_wxHOPR")?;
496        assert_eq!(balance.amount(), 5.into());
497
498        Ok(())
499    }
500
501    #[test]
502    fn balance_should_have_consistent_display_from_str() -> anyhow::Result<()> {
503        let balance_1 = HoprBalance::from(10);
504        let balance_2 = HoprBalance::from_str(&balance_1.to_string())?;
505
506        assert_eq!(balance_1, balance_2);
507
508        Ok(())
509    }
510
511    #[test]
512    fn balance_should_have_consistent_formatted_string_from_str() -> anyhow::Result<()> {
513        let balance_1 = HoprBalance::from(10);
514        let balance_2 = HoprBalance::from_str(&balance_1.format_in_wei())?;
515
516        assert_eq!(balance_1, balance_2);
517
518        Ok(())
519    }
520
521    #[test]
522    fn balance_test_formatted_string() -> anyhow::Result<()> {
523        let base = U256::from(123) * U256::exp10(WxHOPR::SCALE - 2);
524
525        let b1 = format!("{base} wei_wxHOPR");
526        let b1: HoprBalance = b1.parse()?;
527
528        let b2 = b1.mul(100);
529
530        let b3 = format!("{} wei_wxHOPR", base / 1000);
531        let b3: HoprBalance = b3.parse()?;
532
533        let b4 = format!("{} wei_wxHOPR", base / 10);
534        let b4: HoprBalance = b4.parse()?;
535
536        assert_eq!("1.23 wxHOPR", b1.to_string());
537        assert_eq!("123 wxHOPR", b2.to_string());
538        assert_eq!("0.00123 wxHOPR", b3.to_string());
539        assert_eq!("0.123 wxHOPR", b4.to_string());
540
541        Ok(())
542    }
543
544    #[test]
545    fn balance_should_sum_in_interator_correctly() {
546        let sum = vec![HoprBalance::from(1), HoprBalance::from(2), HoprBalance::from(3)]
547            .into_iter()
548            .sum::<HoprBalance>();
549
550        assert_eq!(sum, HoprBalance::from(6));
551    }
552}