diff --git a/CHANGELOG.md b/CHANGELOG.md index 377b65cc2a..4ee07964b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ and this project adheres to - cosmwasm-std: Implement `Not` for `Uint{64,128,256}` ([#1799]). - cosmwasm-std: Implement `into_empty` for `QuerierWrapper`, `Deps` and `DepsMut`. +- cosmwasm-std: Add iterators for `Coins` ([#1806]). [#1799]: https://github.com/CosmWasm/cosmwasm/pull/1799 +[#1806]: https://github.com/CosmWasm/cosmwasm/pull/1806 ### Changed diff --git a/packages/std/src/coins.rs b/packages/std/src/coins.rs index 5a0f05a7f3..44e72e1330 100644 --- a/packages/std/src/coins.rs +++ b/packages/std/src/coins.rs @@ -2,20 +2,21 @@ use alloc::collections::BTreeMap; use core::fmt; use core::str::FromStr; -use crate::{errors::CoinsError, Coin, StdError, StdResult, Uint128}; -use crate::{OverflowError, OverflowOperation}; +use crate::{ + errors::CoinsError, Coin, OverflowError, OverflowOperation, 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. +/// implement Coins as a BTreeMap that maps from coin denoms to `Coin`. /// 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); +pub struct Coins(BTreeMap); /// Casting a Vec to Coins. /// The Vec can be out of order, but must not contain duplicate denoms. @@ -26,13 +27,13 @@ impl TryFrom> for Coins { fn try_from(vec: Vec) -> Result { let mut map = BTreeMap::new(); - for Coin { amount, denom } in vec { - if amount.is_zero() { + for coin in vec { + if coin.amount.is_zero() { continue; } // if the insertion returns a previous value, we have a duplicate denom - if map.insert(denom, amount).is_some() { + if map.insert(coin.denom.clone(), coin).is_some() { return Err(CoinsError::DuplicateDenom); } } @@ -91,8 +92,8 @@ 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}")) + .values() + .map(|coin| coin.to_string()) .collect::>() .join(","); write!(f, "{s}") @@ -102,21 +103,12 @@ impl fmt::Display for Coins { impl Coins { /// Conversion 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() + self.0.values().cloned().collect() } /// Conversion to Vec, consuming the original object pub fn into_vec(self) -> Vec { - self.0 - .into_iter() - .map(|(denom, amount)| Coin { denom, amount }) - .collect() + self.0.into_values().collect() } /// Returns the number of different denoms in this collection. @@ -137,7 +129,10 @@ impl Coins { /// Returns the amount of the given denom or zero if the denom is not present. pub fn amount_of(&self, denom: &str) -> Uint128 { - self.0.get(denom).copied().unwrap_or_else(Uint128::zero) + self.0 + .get(denom) + .map(|c| c.amount) + .unwrap_or_else(Uint128::zero) } /// Returns the amount of the given denom if and only if this collection contains only @@ -161,7 +156,7 @@ impl Coins { /// ``` pub fn contains_only(&self, denom: &str) -> Option { if self.len() == 1 { - self.0.get(denom).copied() + self.0.get(denom).map(|c| c.amount) } else { None } @@ -174,8 +169,15 @@ impl Coins { return Ok(()); } - let amount = self.0.entry(coin.denom).or_insert_with(Uint128::zero); - *amount = amount.checked_add(coin.amount)?; + // if the coin is not present yet, insert it, otherwise add to existing amount + match self.0.get_mut(&coin.denom) { + None => { + self.0.insert(coin.denom.clone(), coin); + } + Some(existing) => { + existing.amount = existing.amount.checked_add(coin.amount)?; + } + } Ok(()) } @@ -183,10 +185,10 @@ impl Coins { /// Errors in case of overflow or if the denom is not present. pub fn sub(&mut self, coin: Coin) -> StdResult<()> { match self.0.get_mut(&coin.denom) { - Some(v) => { - *v = v.checked_sub(coin.amount)?; + Some(existing) => { + existing.amount = existing.amount.checked_sub(coin.amount)?; // make sure to remove zero coin - if v.is_zero() { + if existing.amount.is_zero() { self.0.remove(&coin.denom); } } @@ -206,6 +208,105 @@ impl Coins { Ok(()) } + + /// Returns an iterator over the coins. + /// + /// # Examples + /// + /// ``` + /// # use cosmwasm_std::{coin, Coin, Coins, Uint128}; + /// let mut coins = Coins::default(); + /// coins.add(coin(500, "uluna")).unwrap(); + /// coins.add(coin(1000, "uatom")).unwrap(); + /// let mut iterator = coins.iter(); + /// + /// let uatom = iterator.next().unwrap(); + /// assert_eq!(uatom.denom, "uatom"); + /// assert_eq!(uatom.amount.u128(), 1000); + /// + /// let uluna = iterator.next().unwrap(); + /// assert_eq!(uluna.denom, "uluna"); + /// assert_eq!(uluna.amount.u128(), 500); + /// + /// assert_eq!(iterator.next(), None); + /// ``` + pub fn iter(&self) -> CoinsIter<'_> { + CoinsIter(self.0.iter()) + } +} + +impl IntoIterator for Coins { + type Item = Coin; + type IntoIter = CoinsIntoIter; + + fn into_iter(self) -> Self::IntoIter { + CoinsIntoIter(self.0.into_iter()) + } +} + +impl<'a> IntoIterator for &'a Coins { + type Item = &'a Coin; + type IntoIter = CoinsIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +#[derive(Debug)] +pub struct CoinsIntoIter(alloc::collections::btree_map::IntoIter); + +impl Iterator for CoinsIntoIter { + type Item = Coin; + + fn next(&mut self) -> Option { + self.0.next().map(|(_, coin)| coin) + } + + fn size_hint(&self) -> (usize, Option) { + // Since btree_map::IntoIter implements ExactSizeIterator, this is guaranteed to return the exact length + self.0.size_hint() + } +} + +impl DoubleEndedIterator for CoinsIntoIter { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(_, coin)| coin) + } +} + +impl ExactSizeIterator for CoinsIntoIter { + fn len(&self) -> usize { + self.0.len() + } +} + +#[derive(Debug)] +pub struct CoinsIter<'a>(alloc::collections::btree_map::Iter<'a, String, Coin>); + +impl<'a> Iterator for CoinsIter<'a> { + type Item = &'a Coin; + + fn next(&mut self) -> Option { + self.0.next().map(|(_, coin)| coin) + } + + fn size_hint(&self) -> (usize, Option) { + // Since btree_map::Iter implements ExactSizeIterator, this is guaranteed to return the exact length + self.0.size_hint() + } +} + +impl<'a> DoubleEndedIterator for CoinsIter<'a> { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(_, coin)| coin) + } +} + +impl<'a> ExactSizeIterator for CoinsIter<'a> { + fn len(&self) -> usize { + self.0.len() + } } #[cfg(test)] @@ -389,4 +490,40 @@ mod tests { assert_eq!(coins.len(), 1); assert_eq!(coins.amount_of("uatom").u128(), 12345); } + + #[test] + fn exact_size_iterator() { + let coins = mock_coins(); + let iter = coins.iter(); + assert_eq!(iter.len(), 3); + assert_eq!(iter.size_hint(), (3, Some(3))); + + let iter = coins.into_iter(); + assert_eq!(iter.len(), 3); + assert_eq!(iter.size_hint(), (3, Some(3))); + } + + #[test] + fn can_iterate_owned() { + let coins = mock_coins(); + let mut moved = Coins::default(); + for c in coins { + moved.add(c).unwrap(); + } + assert_eq!(moved.len(), 3); + + assert!(mock_coins().into_iter().eq(mock_coins().to_vec())); + } + + #[test] + fn can_iterate_borrowed() { + let coins = mock_coins(); + assert!(coins + .iter() + .map(|c| &c.denom) + .eq(coins.to_vec().iter().map(|c| &c.denom))); + + // can still use the coins afterwards + assert_eq!(coins.amount_of("uatom").u128(), 12345); + } }