1use std::{
18 fmt::Display,
19 result::Result::Ok,
20 str::FromStr,
21};
22
23use rust_decimal::{
24 Decimal,
25 prelude::FromPrimitive,
26};
27use serde_json::json;
28use subxt::{
29 Config,
30 backend::{
31 legacy::LegacyRpcMethods,
32 rpc::RpcClient,
33 },
34};
35
36use anyhow::{
37 Context,
38 Result,
39 anyhow,
40};
41use url::Url;
42
43use crate::url_to_string;
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum BalanceVariant<Balance> {
48 Default(Balance),
50 Denominated(DenominatedBalance),
52}
53
54#[derive(Debug, Clone)]
55pub struct TokenMetadata {
56 pub token_decimals: usize,
58 pub symbol: String,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct DenominatedBalance {
64 value: Decimal,
65 unit: UnitPrefix,
66 symbol: String,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum UnitPrefix {
71 Giga,
72 Mega,
73 Kilo,
74 One,
75 Milli,
76 Micro,
77 Nano,
78}
79
80impl TokenMetadata {
81 pub async fn query<C: Config>(url: &Url) -> Result<Self> {
83 let rpc_cli = RpcClient::from_url(url_to_string(url)).await?;
84 let rpc = LegacyRpcMethods::<C>::new(rpc_cli.clone());
85 let sys_props = rpc.system_properties().await?;
86
87 let default_decimals = json!(12);
88 let default_units = json!("UNIT");
89 let token_decimals = sys_props
90 .get("tokenDecimals")
91 .unwrap_or(&default_decimals)
92 .as_u64()
93 .context("error converting decimal to u64")?
94 as usize;
95 let symbol = sys_props
96 .get("tokenSymbol")
97 .unwrap_or(&default_units)
98 .as_str()
99 .context("error converting symbol to string")?;
100 Ok(Self {
101 token_decimals,
102 symbol: symbol.to_string(),
103 })
104 }
105}
106
107impl<Balance> FromStr for BalanceVariant<Balance>
108where
109 Balance: FromStr,
110{
111 type Err = anyhow::Error;
112
113 fn from_str(input: &str) -> Result<Self, Self::Err> {
118 let input = input.replace('_', "");
119 let result = match input.parse::<Balance>() {
122 Ok(balance) => BalanceVariant::Default(balance),
123 Err(_) => BalanceVariant::Denominated(DenominatedBalance::from_str(&input)?),
124 };
125 Ok(result)
126 }
127}
128
129impl FromStr for DenominatedBalance {
130 type Err = anyhow::Error;
131
132 fn from_str(value: &str) -> Result<Self, Self::Err> {
133 let symbols = value
134 .trim_start_matches(|ch: char| ch.is_numeric() || ch == '.' || ch == ',');
135 let unit_char = symbols
136 .chars()
137 .next()
138 .context("no units or symbols present")?;
139 let unit: UnitPrefix = match unit_char {
140 'G' => UnitPrefix::Giga,
141 'M' => UnitPrefix::Mega,
142 'k' => UnitPrefix::Kilo,
143 'm' => UnitPrefix::Milli,
144 '\u{3bc}' => UnitPrefix::Micro,
145 'n' => UnitPrefix::Nano,
146 _ => UnitPrefix::One,
147 };
148 let symbol = if unit != UnitPrefix::One {
149 let (start, _) = symbols
150 .char_indices()
151 .nth(1)
152 .context("cannot find the first char's index")?;
153 symbols[start..].to_string()
154 } else {
155 String::new()
156 };
157 let value = value.trim_end_matches(|ch: char| ch.is_alphabetic());
158 let value = Decimal::from_str_exact(value)
159 .context("Error while parsing the value. Please denominate and normalize the balance first.")?
160 .normalize();
161 Ok(Self {
162 value,
163 unit,
164 symbol,
165 })
166 }
167}
168
169impl<Balance> BalanceVariant<Balance>
170where
171 Balance: From<u128> + Clone,
172{
173 pub fn denominate_balance(&self, token_metadata: &TokenMetadata) -> Result<Balance> {
211 match self {
212 BalanceVariant::Default(balance) => Ok(balance.clone()),
213 BalanceVariant::Denominated(den_balance) => {
214 let zeros: usize = (token_metadata.token_decimals as isize
215 + match den_balance.unit {
216 UnitPrefix::Giga => 9,
217 UnitPrefix::Mega => 6,
218 UnitPrefix::Kilo => 3,
219 UnitPrefix::One => 0,
220 UnitPrefix::Milli => -3,
221 UnitPrefix::Micro => -6,
222 UnitPrefix::Nano => -9,
223 })
224 .try_into()?;
225 let multiple =
226 Decimal::from_str_exact(&format!("1{}", "0".repeat(zeros)))?;
227 let fract_scale = den_balance.value.fract().scale();
228 let mantissa_difference = zeros as isize - fract_scale as isize;
229 if mantissa_difference < 0 {
230 return Err(anyhow!(
231 "Given precision of a Balance value is higher than allowed"
232 ))
233 }
234 let balance: u128 = den_balance
235 .value
236 .checked_mul(multiple)
237 .context("error while converting balance to raw format. Overflow during multiplication!")?
238 .try_into()?;
239 Ok(balance.into())
240 }
241 }
242 }
243
244 pub fn from<T: Into<u128>>(
275 value: T,
276 token_metadata: Option<&TokenMetadata>,
277 ) -> Result<Self> {
278 let n: u128 = value.into();
279
280 if let Some(token_metadata) = token_metadata {
281 if n == 0 {
282 return Ok(BalanceVariant::Denominated(DenominatedBalance {
283 value: Decimal::ZERO,
284 unit: UnitPrefix::One,
285 symbol: token_metadata.symbol.clone(),
286 }))
287 }
288
289 let number_of_digits = n.to_string().len();
290
291 let giga_units_zeros = token_metadata.token_decimals + 9;
292 let mega_units_zeros = token_metadata.token_decimals + 6;
293 let kilo_units_zeros = token_metadata.token_decimals + 3;
294 let one_unit_zeros = token_metadata.token_decimals;
295 let milli_units_zeros = token_metadata.token_decimals.checked_sub(3);
296 let micro_units_zeros = token_metadata.token_decimals.checked_sub(6);
297 let nano_units_zeros = token_metadata.token_decimals.checked_sub(9);
298
299 let unit: UnitPrefix;
300 let zeros: usize;
301 if (giga_units_zeros + 1..).contains(&number_of_digits) {
302 zeros = giga_units_zeros;
303 unit = UnitPrefix::Giga;
304 } else if (mega_units_zeros + 1..=giga_units_zeros)
305 .contains(&number_of_digits)
306 {
307 zeros = mega_units_zeros;
308 unit = UnitPrefix::Mega;
309 } else if (kilo_units_zeros + 1..=mega_units_zeros)
310 .contains(&number_of_digits)
311 {
312 zeros = kilo_units_zeros;
313 unit = UnitPrefix::Kilo;
314 } else if (one_unit_zeros + 1..=kilo_units_zeros).contains(&number_of_digits)
315 {
316 zeros = one_unit_zeros;
317 unit = UnitPrefix::One;
318 } else if milli_units_zeros.is_some()
319 && (milli_units_zeros.unwrap() + 1..=one_unit_zeros)
320 .contains(&number_of_digits)
321 {
322 zeros = match milli_units_zeros {
323 Some(val) => val,
324 None => return Err(anyhow!("the number is checked to be >= 0. qed")),
325 };
326 unit = UnitPrefix::Milli;
327 } else if milli_units_zeros.is_some()
328 && micro_units_zeros.is_some()
329 && (micro_units_zeros.unwrap() + 1..=milli_units_zeros.unwrap())
330 .contains(&number_of_digits)
331 {
332 zeros = match micro_units_zeros {
333 Some(val) => val,
334 None => return Err(anyhow!("the number is checked to be >= 0. qed")),
335 };
336 unit = UnitPrefix::Micro;
337 } else if nano_units_zeros.is_some() {
338 zeros = match nano_units_zeros {
339 Some(val) => val,
340 None => return Err(anyhow!("the number is checked to be >= 0. qed")),
341 };
342 unit = UnitPrefix::Nano;
343 } else {
344 return Err(anyhow!("Invalid denomination"))
345 }
346 let multiple = Decimal::from_str_exact(&format!("1{}", "0".repeat(zeros)))?;
347 let value = Decimal::from_u128(n)
348 .context("value can not be converted into decimal")?
349 / multiple;
350
351 let den_balance = DenominatedBalance {
352 value,
353 unit,
354 symbol: token_metadata.symbol.clone(),
355 };
356
357 Ok(BalanceVariant::Denominated(den_balance))
358 } else {
359 Ok(BalanceVariant::Default(n.into()))
360 }
361 }
362}
363
364impl<Balance> Display for BalanceVariant<Balance>
365where
366 Balance: Display + Clone,
367{
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 match self {
370 BalanceVariant::Default(balance) => f.write_str(&balance.to_string()),
371 BalanceVariant::Denominated(input) => f.write_str(&input.to_string()),
372 }
373 }
374}
375
376impl Display for DenominatedBalance {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 let prefix = match self.unit {
379 UnitPrefix::Giga => "G",
380 UnitPrefix::Mega => "M",
381 UnitPrefix::Kilo => "k",
382 UnitPrefix::One => "",
383 UnitPrefix::Milli => "m",
384 UnitPrefix::Micro => "μ",
385 UnitPrefix::Nano => "n",
386 };
387 f.write_fmt(format_args!("{}{}{}", self.value, prefix, self.symbol))
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use ink_env::{
394 DefaultEnvironment,
395 Environment,
396 };
397
398 use super::*;
399
400 #[test]
401 fn correct_balances_parses_success() {
402 assert!(
403 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
404 "500DOT"
405 )
406 .is_ok(),
407 "<500DOT> was not parsed correctly"
408 );
409 assert!(
410 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
411 "500"
412 )
413 .is_ok(),
414 "<500> was not parsed correctly"
415 );
416 assert!(
417 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
418 "1.0"
419 )
420 .is_err(),
421 "<1.0> was not parsed correctly. Units must be provided"
422 );
423 assert!(
424 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
425 "1.0DOT"
426 )
427 .is_ok(),
428 "<1.0DOt> was not parsed correctly"
429 );
430 }
431
432 #[test]
433 fn incorrect_balances() {
434 assert!(
435 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
436 "500%"
437 )
438 .is_err(),
439 "expected to fail parsing incorrect balance"
440 );
441 }
442
443 #[test]
444 fn balance_variant_denominated_success() {
445 let tm = TokenMetadata {
446 token_decimals: 10,
447 symbol: String::from("DOT"),
448 };
449 let bv =
450 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
451 "500MDOT",
452 )
453 .expect("successful parsing. qed");
454 assert!(
455 bv.denominate_balance(&tm).is_ok(),
456 "balances could not be denominated correctly"
457 );
458 }
459
460 #[test]
461 fn balance_variant_denominated_equal() {
462 let decimals = 10;
463 let tm = TokenMetadata {
464 token_decimals: decimals,
465 symbol: String::from("DOT"),
466 };
467 let balance: <DefaultEnvironment as Environment>::Balance =
468 500 * 1_000_000 * 10_000_000_000;
469 let bv =
470 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
471 "500MDOT",
472 )
473 .expect("successful parsing. qed");
474 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
475 assert_eq!(balance, balance_parsed);
476 }
477
478 #[test]
479 fn balance_variant_denominated_equal_fraction() {
480 let decimals = 10;
481 let tm = TokenMetadata {
482 token_decimals: decimals,
483 symbol: String::from("DOT"),
484 };
485 let balance: <DefaultEnvironment as Environment>::Balance =
486 5_005_000_000_000_000_000;
487 let bv = BalanceVariant::from_str("500.5MDOT").expect("successful parsing. qed");
488 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
489 assert_eq!(balance, balance_parsed);
490 }
491
492 #[test]
493 fn balance_variant_denominated_equal_small_units() {
494 let decimals = 10;
495 let tm = TokenMetadata {
496 token_decimals: decimals,
497 symbol: String::from("DOT"),
498 };
499 let balance: <DefaultEnvironment as Environment>::Balance = 5_005_000;
500 let bv = BalanceVariant::from_str("500.5μDOT").expect("successful parsing. qed");
501 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
502 assert_eq!(balance, balance_parsed);
503 }
504 #[test]
505 fn smallest_value() {
506 let decimals = 10;
507 let tm = TokenMetadata {
508 token_decimals: decimals,
509 symbol: String::from("DOT"),
510 };
511 let balance: <DefaultEnvironment as Environment>::Balance = 1;
512 let bv = BalanceVariant::from_str("0.1nDOT").expect("successful parsing. qed");
513 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
514 assert_eq!(balance, balance_parsed);
515 }
516
517 #[test]
518 fn value_less_than_precision() {
519 let decimals = 10;
522 let tm = TokenMetadata {
523 token_decimals: decimals,
524 symbol: String::from("DOT"),
525 };
526 let bv =
527 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
528 "0.01546nDOT",
529 )
530 .expect("successful parsing. qed");
531 let balance_parsed = bv.denominate_balance(&tm);
532 assert!(balance_parsed.is_err())
533 }
534
535 #[test]
536 fn giga() {
537 let decimals = 10;
538 let tm = TokenMetadata {
539 token_decimals: decimals,
540 symbol: String::from("DOT"),
541 };
542 let balance: <DefaultEnvironment as Environment>::Balance =
543 5_005_000_000_000_000_000_000;
544 let bv = BalanceVariant::from_str("500.5GDOT").expect("successful parsing. qed");
545 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
546 assert_eq!(balance, balance_parsed);
547 }
548
549 #[test]
550 fn kilo() {
551 let decimals = 10;
552 let tm = TokenMetadata {
553 token_decimals: decimals,
554 symbol: String::from("DOT"),
555 };
556 let balance: <DefaultEnvironment as Environment>::Balance = 5_005_000_000_000_000;
557 let bv = BalanceVariant::from_str("500.5kDOT").expect("successful parsing. qed");
558 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
559 assert_eq!(balance, balance_parsed);
560 }
561
562 #[test]
563 fn unit() {
564 let decimals = 10;
565 let tm = TokenMetadata {
566 token_decimals: decimals,
567 symbol: String::from("DOT"),
568 };
569 let balance: <DefaultEnvironment as Environment>::Balance = 5_005_000_000_000;
570 let bv = BalanceVariant::from_str("500.5DOT").expect("successful parsing. qed");
571 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
572 assert_eq!(balance, balance_parsed);
573 }
574
575 #[test]
576 fn milli() {
577 let decimals = 10;
578 let tm = TokenMetadata {
579 token_decimals: decimals,
580 symbol: String::from("DOT"),
581 };
582 let balance: <DefaultEnvironment as Environment>::Balance = 5_005_000_000;
583 let bv = BalanceVariant::from_str("500.5mDOT").expect("successful parsing. qed");
584 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
585 assert_eq!(balance, balance_parsed);
586 }
587 #[test]
588 fn micro() {
589 let decimals = 10;
590 let tm = TokenMetadata {
591 token_decimals: decimals,
592 symbol: String::from("DOT"),
593 };
594 let balance: <DefaultEnvironment as Environment>::Balance = 5_005_000;
595 let bv = BalanceVariant::from_str("500.5μDOT").expect("successful parsing. qed");
596 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
597 assert_eq!(balance, balance_parsed);
598 }
599 #[test]
600 fn nano() {
601 let decimals = 10;
602 let tm = TokenMetadata {
603 token_decimals: decimals,
604 symbol: String::from("DOT"),
605 };
606 let balance: <DefaultEnvironment as Environment>::Balance = 5_005;
607 let bv = BalanceVariant::from_str("500.5nDOT").expect("successful parsing. qed");
608 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
609 assert_eq!(balance, balance_parsed);
610 }
611
612 #[test]
613 fn different_digits() {
614 let decimals = 10;
615 let tm = TokenMetadata {
616 token_decimals: decimals,
617 symbol: String::from("DOT"),
618 };
619 let balance: <DefaultEnvironment as Environment>::Balance = 5_235_456_210_000_000;
620 let bv =
621 BalanceVariant::from_str("523.545621kDOT").expect("successful parsing. qed");
622 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
623 assert_eq!(balance, balance_parsed);
624 }
625
626 #[test]
627 fn non_standard_token_decimals() {
628 let decimals = 10;
629 let tm = TokenMetadata {
630 token_decimals: decimals,
631 symbol: String::from("DOT"),
632 };
633 let balance: <DefaultEnvironment as Environment>::Balance = 50_015_000_000_000;
634 let bv = BalanceVariant::from_str("5001.5DOT").expect("successful parsing. qed");
635 let balance_parsed = bv.denominate_balance(&tm).expect("successful parsing. qed");
636 assert_eq!(balance, balance_parsed);
637 }
638
639 #[test]
640 fn small_number_of_decimals_zero() {
641 let decimals = 6;
642 let tm = TokenMetadata {
643 token_decimals: decimals,
644 symbol: String::from("DOT"),
645 };
646 let bv =
647 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(
648 "0.4μDOT",
649 )
650 .expect("successful parsing. qed");
651 let balance_parsed = bv.denominate_balance(&tm);
652 assert!(balance_parsed.is_err())
653 }
654
655 #[test]
656 fn big_input_to_denominate() {
657 let s = "79_228_162_514_264_337_593_543_950_336DOT";
659 let bv =
660 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(s);
661 assert!(bv.is_err())
662 }
663
664 #[test]
665 fn big_input_to_raw() {
666 let s = "79_228_162_514_264_337_593_543_950_336";
668 let bv =
669 BalanceVariant::<<DefaultEnvironment as Environment>::Balance>::from_str(s);
670 assert!(bv.is_ok())
671 }
672
673 #[test]
674 fn convert_from_u128() {
675 let decimals = 6;
676 let tm = TokenMetadata {
677 token_decimals: decimals,
678 symbol: String::from("DOT"),
679 };
680 let balance = 532_500_000_000_u128;
681 let denominated_balance = BalanceVariant::<
682 <DefaultEnvironment as Environment>::Balance,
683 >::from(balance, Some(&tm))
684 .expect("successful conversion");
685 let sample = BalanceVariant::Denominated(DenominatedBalance {
686 value: Decimal::new(5325, 1),
687 unit: UnitPrefix::Kilo,
688 symbol: String::from("DOT"),
689 });
690 assert_eq!(sample, denominated_balance);
691 }
692
693 #[test]
694 fn convert_one_from_u128() {
695 let decimals = 10;
696 let tm = TokenMetadata {
697 token_decimals: decimals,
698 symbol: String::from("DOT"),
699 };
700 let balance = 532_500_000_000_u128;
701 let denominated_balance = BalanceVariant::<
702 <DefaultEnvironment as Environment>::Balance,
703 >::from(balance, Some(&tm))
704 .expect("successful conversion");
705 let sample = BalanceVariant::Denominated(DenominatedBalance {
706 value: Decimal::new(5325, 2),
707 unit: UnitPrefix::One,
708 symbol: String::from("DOT"),
709 });
710 assert_eq!(sample, denominated_balance);
711 }
712
713 #[test]
714 fn convert_small_from_u128() {
715 let decimals = 10;
716 let tm = TokenMetadata {
717 token_decimals: decimals,
718 symbol: String::from("DOT"),
719 };
720 let balance = 532_500_u128;
725 let denominated_balance = BalanceVariant::<
726 <DefaultEnvironment as Environment>::Balance,
727 >::from(balance, Some(&tm))
728 .expect("successful conversion");
729 let sample = BalanceVariant::Denominated(DenominatedBalance {
730 value: Decimal::new(5325, 2),
731 unit: UnitPrefix::Micro,
732 symbol: String::from("DOT"),
733 });
734 assert_eq!(sample, denominated_balance);
735 }
736}