cw_utils/
balance.rs

1use std::{fmt, ops};
2
3use cosmwasm_schema::cw_serde;
4use cosmwasm_std::{Coin, OverflowError, OverflowOperation, StdError, StdResult, Uint128};
5
6// Balance wraps Vec<Coin> and provides some nice helpers. It mutates the Vec and can be
7// unwrapped when done.
8#[cw_serde]
9#[derive(Default)]
10pub struct NativeBalance(pub Vec<Coin>);
11
12impl NativeBalance {
13    pub fn into_vec(self) -> Vec<Coin> {
14        self.0
15    }
16
17    /// returns true if the list of coins has at least the required amount
18    pub fn has(&self, required: &Coin) -> bool {
19        self.0
20            .iter()
21            .find(|c| c.denom == required.denom)
22            .map(|m| m.amount >= required.amount)
23            .unwrap_or(false)
24    }
25
26    /// normalize Wallet (sorted by denom, no 0 elements, no duplicate denoms)
27    pub fn normalize(&mut self) {
28        // drop 0's
29        self.0.retain(|c| c.amount.u128() != 0);
30        // sort
31        self.0.sort_unstable_by(|a, b| a.denom.cmp(&b.denom));
32
33        // find all i where (self[i-1].denom == self[i].denom).
34        let mut dups: Vec<usize> = self
35            .0
36            .iter()
37            .enumerate()
38            .filter_map(|(i, c)| {
39                if i != 0 && c.denom == self.0[i - 1].denom {
40                    Some(i)
41                } else {
42                    None
43                }
44            })
45            .collect();
46        dups.reverse();
47
48        // we go through the dups in reverse order (to avoid shifting indexes of other ones)
49        for dup in dups {
50            let add = self.0[dup].amount;
51            self.0[dup - 1].amount += add;
52            self.0.remove(dup);
53        }
54    }
55
56    fn find(&self, denom: &str) -> Option<(usize, &Coin)> {
57        self.0.iter().enumerate().find(|(_i, c)| c.denom == denom)
58    }
59
60    /// insert_pos should only be called when denom is not in the Wallet.
61    /// it returns the position where denom should be inserted at (via splice).
62    /// It returns None if this should be appended
63    fn insert_pos(&self, denom: &str) -> Option<usize> {
64        self.0.iter().position(|c| c.denom.as_str() >= denom)
65    }
66
67    pub fn is_empty(&self) -> bool {
68        !self.0.iter().any(|x| x.amount != Uint128::zero())
69    }
70
71    /// similar to `Balance.sub`, but doesn't fail when minuend less than subtrahend
72    pub fn sub_saturating(mut self, other: Coin) -> StdResult<Self> {
73        match self.find(&other.denom) {
74            Some((i, c)) => {
75                if c.amount <= other.amount {
76                    self.0.remove(i);
77                } else {
78                    self.0[i].amount = self.0[i].amount.checked_sub(other.amount)?;
79                }
80            }
81            // error if no tokens
82            None => {
83                return Err(StdError::overflow(OverflowError::new(
84                    OverflowOperation::Sub,
85                )))
86            }
87        };
88        Ok(self)
89    }
90}
91
92impl fmt::Display for NativeBalance {
93    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
94        for c in &self.0 {
95            write!(f, "{}{}", c.denom, c.amount)?
96        }
97        Ok(())
98    }
99}
100
101impl ops::AddAssign<Coin> for NativeBalance {
102    fn add_assign(&mut self, other: Coin) {
103        match self.find(&other.denom) {
104            Some((i, c)) => {
105                self.0[i].amount = c.amount + other.amount;
106            }
107            // place this in proper sorted order
108            None => match self.insert_pos(&other.denom) {
109                Some(idx) => self.0.insert(idx, other),
110                None => self.0.push(other),
111            },
112        };
113    }
114}
115
116impl ops::Add<Coin> for NativeBalance {
117    type Output = Self;
118
119    fn add(mut self, other: Coin) -> Self {
120        self += other;
121        self
122    }
123}
124
125impl ops::AddAssign<NativeBalance> for NativeBalance {
126    fn add_assign(&mut self, other: NativeBalance) {
127        for coin in other.0.into_iter() {
128            self.add_assign(coin);
129        }
130    }
131}
132
133impl ops::Add<NativeBalance> for NativeBalance {
134    type Output = Self;
135
136    fn add(mut self, other: NativeBalance) -> Self {
137        self += other;
138        self
139    }
140}
141
142impl ops::Sub<Coin> for NativeBalance {
143    type Output = StdResult<Self>;
144
145    fn sub(mut self, other: Coin) -> StdResult<Self> {
146        match self.find(&other.denom) {
147            Some((i, c)) => {
148                let remainder = c.amount.checked_sub(other.amount)?;
149                if remainder.u128() == 0 {
150                    self.0.remove(i);
151                } else {
152                    self.0[i].amount = remainder;
153                }
154            }
155            // error if no tokens
156            None => {
157                return Err(StdError::overflow(OverflowError::new(
158                    OverflowOperation::Sub,
159                )))
160            }
161        };
162        Ok(self)
163    }
164}
165
166impl ops::Sub<Vec<Coin>> for NativeBalance {
167    type Output = StdResult<Self>;
168
169    fn sub(self, amount: Vec<Coin>) -> StdResult<Self> {
170        let mut res = self;
171        for coin in amount {
172            res = res.sub(coin.clone())?;
173        }
174        Ok(res)
175    }
176}
177
178#[cfg(test)]
179mod test {
180    use super::*;
181    use cosmwasm_std::coin;
182
183    #[test]
184    fn balance_has_works() {
185        let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]);
186
187        // less than same type
188        assert!(balance.has(&coin(777, "ETH")));
189        // equal to same type
190        assert!(balance.has(&coin(555, "BTC")));
191
192        // too high
193        assert!(!balance.has(&coin(12346, "ETH")));
194        // wrong type
195        assert!(!balance.has(&coin(456, "ETC")));
196    }
197
198    #[test]
199    fn balance_add_works() {
200        let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]);
201
202        // add an existing coin
203        let more_eth = balance.clone() + coin(54321, "ETH");
204        assert_eq!(
205            more_eth,
206            NativeBalance(vec![coin(555, "BTC"), coin(66666, "ETH")])
207        );
208
209        // add an new coin
210        let add_atom = balance + coin(777, "ATOM");
211        assert_eq!(
212            add_atom,
213            NativeBalance(vec![
214                coin(777, "ATOM"),
215                coin(555, "BTC"),
216                coin(12345, "ETH"),
217            ])
218        );
219    }
220
221    #[test]
222    fn balance_in_place_addition() {
223        let mut balance = NativeBalance(vec![coin(555, "BTC")]);
224        balance += coin(777, "ATOM");
225        assert_eq!(
226            &balance,
227            &NativeBalance(vec![coin(777, "ATOM"), coin(555, "BTC")])
228        );
229
230        balance += NativeBalance(vec![coin(666, "ETH"), coin(123, "ATOM")]);
231        assert_eq!(
232            &balance,
233            &NativeBalance(vec![coin(900, "ATOM"), coin(555, "BTC"), coin(666, "ETH")])
234        );
235
236        let sum = balance + NativeBalance(vec![coin(234, "BTC")]);
237        assert_eq!(
238            sum,
239            NativeBalance(vec![coin(900, "ATOM"), coin(789, "BTC"), coin(666, "ETH")])
240        );
241    }
242
243    #[test]
244    fn balance_subtract_works() {
245        let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]);
246
247        // subtract less than we have
248        let less_eth = (balance.clone() - coin(2345, "ETH")).unwrap();
249        assert_eq!(
250            less_eth,
251            NativeBalance(vec![coin(555, "BTC"), coin(10000, "ETH")])
252        );
253
254        // subtract all of one coin (and remove with 0 amount)
255        let no_btc = (balance.clone() - coin(555, "BTC")).unwrap();
256        assert_eq!(no_btc, NativeBalance(vec![coin(12345, "ETH")]));
257
258        // subtract more than we have
259        let underflow = balance.clone() - coin(666, "BTC");
260        assert!(underflow.is_err());
261
262        // subtract non-existent denom
263        let missing = balance - coin(1, "ATOM");
264        assert!(missing.is_err());
265    }
266
267    #[test]
268    fn balance_subtract_saturating_works() {
269        let balance = NativeBalance(vec![coin(555, "BTC"), coin(12345, "ETH")]);
270
271        // subtract less than we have
272        let less_eth = balance.clone().sub_saturating(coin(2345, "ETH")).unwrap();
273        assert_eq!(
274            less_eth,
275            NativeBalance(vec![coin(555, "BTC"), coin(10000, "ETH")])
276        );
277
278        // subtract all of one coin (and remove with 0 amount)
279        let no_btc = balance.clone().sub_saturating(coin(555, "BTC")).unwrap();
280        assert_eq!(no_btc, NativeBalance(vec![coin(12345, "ETH")]));
281
282        // subtract more than we have
283        let saturating = balance.clone().sub_saturating(coin(666, "BTC"));
284        assert!(saturating.is_ok());
285        assert_eq!(saturating.unwrap(), NativeBalance(vec![coin(12345, "ETH")]));
286
287        // subtract non-existent denom
288        let missing = balance - coin(1, "ATOM");
289        assert!(missing.is_err());
290    }
291
292    #[test]
293    fn normalize_balance() {
294        // remove 0 value items and sort
295        let mut balance = NativeBalance(vec![coin(123, "ETH"), coin(0, "BTC"), coin(8990, "ATOM")]);
296        balance.normalize();
297        assert_eq!(
298            balance,
299            NativeBalance(vec![coin(8990, "ATOM"), coin(123, "ETH")])
300        );
301
302        // merge duplicate entries of same denom
303        let mut balance = NativeBalance(vec![
304            coin(123, "ETH"),
305            coin(789, "BTC"),
306            coin(321, "ETH"),
307            coin(11, "BTC"),
308        ]);
309        balance.normalize();
310        assert_eq!(
311            balance,
312            NativeBalance(vec![coin(800, "BTC"), coin(444, "ETH")])
313        );
314    }
315}