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, 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#[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#[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 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 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 captures.get(2).is_none() {
130 value *= BigInt::from(10).pow(C::SCALE as u32);
131 }
132
133 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 pub fn new_base<T: Into<U256>>(value: T) -> Self {
162 Self(value.into() * U256::exp10(C::SCALE), C::default())
163 }
164
165 pub fn zero() -> Self {
167 Self(U256::zero(), C::default())
168 }
169
170 pub fn is_zero(&self) -> bool {
172 self.0.is_zero()
173 }
174
175 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 pub fn amount_in_base_units(&self) -> String {
189 let dec = self.base_amount();
190 let str = dec.to_plain_string();
191
192 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 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}