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