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
18pub trait Currency: Display + FromStr<Err = GeneralError> + Default + PartialEq + Eq {
20 const NAME: &'static str;
22
23 const SCALE: usize;
25
26 fn is<C: Currency>() -> bool {
28 Self::NAME == C::NAME
29 }
30
31 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#[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#[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#[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 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 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 captures.get(2).is_none() {
133 value *= BigInt::from(10).pow(C::SCALE as u32);
134 }
135
136 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 pub fn new_base<T: Into<U256>>(value: T) -> Self {
165 Self(value.into() * U256::exp10(C::SCALE), C::default())
166 }
167
168 pub fn zero() -> Self {
170 Self(U256::zero(), C::default())
171 }
172
173 pub fn is_zero(&self) -> bool {
175 self.0.is_zero()
176 }
177
178 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 pub fn amount_in_base_units(&self) -> String {
192 let dec = self.base_amount();
193 let str = dec.to_plain_string();
194
195 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 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}