From 25c58a01378c964c051fb14e01242b1c1480f6e0 Mon Sep 17 00:00:00 2001 From: larry <26318510+larry0x@users.noreply.github.com> Date: Mon, 14 Nov 2022 15:50:18 +0000 Subject: [PATCH] add `Coins` struct --- packages/std/src/coins.rs | 246 ++++++++++++++++++++++++++++++++++++++ packages/std/src/lib.rs | 2 + 2 files changed, 248 insertions(+) create mode 100644 packages/std/src/coins.rs diff --git a/packages/std/src/coins.rs b/packages/std/src/coins.rs new file mode 100644 index 0000000000..826982d7ab --- /dev/null +++ b/packages/std/src/coins.rs @@ -0,0 +1,246 @@ +use std::any::type_name; +use std::collections::BTreeMap; +use std::fmt; +use std::str::FromStr; + +use crate::{Coin, StdError, StdResult, Uint128}; + +/// A collection of coins, similar to Cosmos SDK's `sdk.Coins` struct. +/// +/// Differently from `sdk.Coins`, which is a vector of `sdk.Coin`, here we +/// implement Coins as a BTreeMap that maps from coin denoms to amounts. +/// This has a number of advantages: +/// +/// - coins are naturally sorted alphabetically by denom +/// - duplicate denoms are automatically removed +/// - cheaper for searching/inserting/deleting: O(log(n)) compared to O(n) +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Coins(BTreeMap); + +// Casting a Vec to Coins. +// The Vec can be out of order, but must not contain duplicate denoms or zero amounts. +impl TryFrom> for Coins { + type Error = StdError; + + fn try_from(vec: Vec) -> StdResult { + let vec_len = vec.len(); + + let map = vec + .into_iter() + .filter(|coin| !coin.amount.is_zero()) + .map(|coin| (coin.denom, coin.amount)) + .collect::>(); + + // the map having a different length from the vec means the vec must either + // 1) contain duplicate denoms, or 2) contain zero amounts + if map.len() != vec_len { + return Err(StdError::parse_err( + type_name::(), + "duplicate denoms or zero amount", + )); + } + + Ok(Self(map)) + } +} + +impl TryFrom<&[Coin]> for Coins { + type Error = StdError; + + fn try_from(slice: &[Coin]) -> StdResult { + slice.to_vec().try_into() + } +} + +impl FromStr for Coins { + type Err = StdError; + + fn from_str(s: &str) -> StdResult { + // Parse a string into a `Coin`. + // + // Parsing the string with regex doesn't work, because the resulting + // wasm binary would be too big from including the `regex` library. + // + // We opt for the following solution: enumerate characters in the string, + // and break before the first non-number character. Split the string at + // that index. + // + // This assumes the denom never starts with a number, which is the case: + // https://github.com/cosmos/cosmos-sdk/blob/v0.46.0/types/coin.go#L854-L856 + let parse_coin_str = |s: &str| -> StdResult { + for (i, c) in s.chars().enumerate() { + if !c.is_ascii_digit() { + let amount = Uint128::from_str(&s[..i])?; + let denom = String::from(&s[i..]); + return Ok(Coin { amount, denom }); + } + } + + Err(StdError::parse_err( + type_name::(), + format!("invalid coin string: {s}"), + )) + }; + + s.split(',') + .into_iter() + .map(parse_coin_str) + .collect::>>()? + .try_into() + } +} + +impl fmt::Display for Coins { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let s = self + .0 + .iter() + .map(|(denom, amount)| format!("{amount}{denom}")) + .collect::>() + .join(","); + write!(f, "{s}") + } +} + +impl Coins { + /// Cast to Vec, while NOT consuming the original object + pub fn to_vec(&self) -> Vec { + self.0 + .iter() + .map(|(denom, amount)| Coin { + denom: denom.clone(), + amount: *amount, + }) + .collect() + } + + /// Cast to Vec, consuming the original object + pub fn into_vec(self) -> Vec { + self.0 + .into_iter() + .map(|(denom, amount)| Coin { denom, amount }) + .collect() + } + + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return the denoms as a vector of strings. + /// The vector is guaranteed to not contain duplicates and sorted alphabetically. + pub fn denoms(&self) -> Vec { + self.0.keys().cloned().collect() + } + + pub fn add(&mut self, coin: &Coin) -> StdResult<()> { + let amount = self + .0 + .entry(coin.denom.clone()) + .or_insert_with(Uint128::zero); + *amount = amount.checked_add(coin.amount)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::coin; + + /// Sort a Vec by denom alphabetically + fn sort_by_denom(vec: &mut [Coin]) { + vec.sort_by(|a, b| a.denom.cmp(&b.denom)); + } + + /// Returns a mockup Vec. In this example, the coins are not in order + fn mock_vec() -> Vec { + vec![ + coin(12345, "uatom"), + coin(69420, "ibc/1234ABCD"), + coin(88888, "factory/osmo1234abcd/subdenom"), + ] + } + + /// Return a mockup Coins that contains the same coins as in `mock_vec` + fn mock_coins() -> Coins { + let mut coins = Coins::default(); + for coin in mock_vec() { + coins.add(&coin).unwrap(); + } + coins + } + + #[test] + fn casting_vec() { + let mut vec = mock_vec(); + let coins = mock_coins(); + + // &[Coin] --> Coins + assert_eq!(Coins::try_from(vec.as_slice()).unwrap(), coins); + // Vec --> Coins + assert_eq!(Coins::try_from(vec.clone()).unwrap(), coins); + + sort_by_denom(&mut vec); + + // &Coins --> Vec + // NOTE: the returned vec should be sorted + assert_eq!(coins.to_vec(), vec); + // Coins --> Vec + // NOTE: the returned vec should be sorted + assert_eq!(coins.into_vec(), vec); + } + + #[test] + fn casting_str() { + // not in order + let s1 = "88888factory/osmo1234abcd/subdenom,12345uatom,69420ibc/1234ABCD"; + // in order + let s2 = "88888factory/osmo1234abcd/subdenom,69420ibc/1234ABCD,12345uatom"; + + let coins = mock_coins(); + + // &str --> Coins + // NOTE: should generate the same Coins, regardless of input order + assert_eq!(Coins::from_str(s1).unwrap(), coins); + assert_eq!(Coins::from_str(s2).unwrap(), coins); + + // Coins --> String + // NOTE: the generated string should be sorted + assert_eq!(coins.to_string(), s2); + } + + #[test] + fn handling_duplicates() { + // create a Vec that contains duplicate denoms + let mut vec = mock_vec(); + vec.push(coin(67890, "uatom")); + + let err = Coins::try_from(vec).unwrap_err(); + assert!(err.to_string().contains("duplicate denoms")); + } + + #[test] + fn handling_zero_amount() { + // create a Vec that contains zero amounts + let mut vec = mock_vec(); + vec[0].amount = Uint128::zero(); + + let err = Coins::try_from(vec).unwrap_err(); + assert!(err.to_string().contains("zero amount")); + } + + #[test] + fn length() { + let coins = Coins::default(); + assert_eq!(coins.len(), 0); + assert!(coins.is_empty()); + + let coins = mock_coins(); + assert_eq!(coins.len(), 3); + assert!(!coins.is_empty()); + } +} diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index 1f37bed1d6..dfac262c01 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -6,6 +6,7 @@ mod addresses; mod assertions; mod binary; mod coin; +mod coins; mod conversion; mod deps; mod errors; @@ -28,6 +29,7 @@ mod types; pub use crate::addresses::{Addr, CanonicalAddr}; pub use crate::binary::Binary; pub use crate::coin::{coin, coins, has_coins, Coin}; +pub use crate::coins::Coins; pub use crate::deps::{Deps, DepsMut, OwnedDeps}; pub use crate::errors::{ CheckedFromRatioError, CheckedMultiplyRatioError, ConversionOverflowError, DivideByZeroError,