andromeda_std/common/
mod.rs

1pub mod actions;
2pub mod context;
3pub mod denom;
4pub mod expiration;
5pub mod milliseconds;
6pub mod rates;
7pub mod reply;
8pub mod response;
9pub mod withdraw;
10
11pub use milliseconds::*;
12
13use crate::error::ContractError;
14use cosmwasm_std::{
15    ensure, has_coins, to_json_binary, BankMsg, Binary, Coin, CosmosMsg, SubMsg, Uint128,
16};
17use cw20::Cw20Coin;
18
19use serde::Serialize;
20use std::collections::BTreeMap;
21
22use cosmwasm_schema::cw_serde;
23#[cw_serde]
24pub enum OrderBy {
25    Asc,
26    Desc,
27}
28
29pub fn encode_binary<T>(val: &T) -> Result<Binary, ContractError>
30where
31    T: Serialize,
32{
33    match to_json_binary(val) {
34        Ok(encoded_val) => Ok(encoded_val),
35        Err(err) => Err(err.into()),
36    }
37}
38
39#[cw_serde]
40pub enum Funds {
41    Native(Coin),
42    Cw20(Cw20Coin),
43}
44
45impl Funds {
46    // There is probably a more idiomatic way of doing this with From and Into...
47    pub fn try_get_coin(&self) -> Result<Coin, ContractError> {
48        match self {
49            Funds::Native(coin) => Ok(coin.clone()),
50            Funds::Cw20(_) => Err(ContractError::ParsingError {
51                err: "Funds is not of type Native".to_string(),
52            }),
53        }
54    }
55}
56
57/// Merges bank messages to the same recipient to a single bank message. Any sub messages
58/// that do not contain bank messages are left as is. Note: Original order is not necessarily maintained.
59///
60/// ## Arguments
61/// * `msgs`  - The sub messages to merge.
62///
63/// Returns a Vec<SubMsg> containing the merged bank messages.
64pub fn merge_sub_msgs(msgs: Vec<SubMsg>) -> Vec<SubMsg> {
65    // BTreeMap used instead of HashMap for determinant ordering in tests. Both should work
66    // on-chain as hashmap randomness is fixed in cosmwasm. We get O(logn) instead of O(1)
67    // performance this way which is not a huge difference.
68    let mut map: BTreeMap<String, Vec<Coin>> = BTreeMap::new();
69
70    let mut merged_msgs: Vec<SubMsg> = vec![];
71    for msg in msgs.into_iter() {
72        match msg.msg {
73            CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => {
74                let current_coins = map.get(&to_address);
75                match current_coins {
76                    Some(current_coins) => {
77                        map.insert(
78                            to_address.to_owned(),
79                            merge_coins(current_coins.to_vec(), amount),
80                        );
81                    }
82                    None => {
83                        map.insert(to_address.to_owned(), amount);
84                    }
85                }
86            }
87            _ => merged_msgs.push(msg),
88        }
89    }
90
91    for (to_address, amount) in map.into_iter() {
92        merged_msgs.push(SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
93            to_address,
94            amount,
95        })));
96    }
97
98    merged_msgs
99}
100
101/// Adds coins in `coins_to_add` to `coins` by merging those of the same denom and
102/// otherwise appending.
103///
104/// ## Arguments
105/// * `coins`        - Mutable reference to a vec of coins which will be modified in-place.
106/// * `coins_to_add` - The `Vec<Coin>` to add, it is assumed that it contains no coins of the
107///                    same denom
108///
109/// Returns nothing as it is done in place.
110pub fn merge_coins(coins: Vec<Coin>, coins_to_add: Vec<Coin>) -> Vec<Coin> {
111    let mut new_coins: Vec<Coin> = if !coins.is_empty() {
112        merge_coins(vec![], coins.to_vec())
113    } else {
114        vec![]
115    };
116    // Not the most efficient algorithm (O(n * m)) but we don't expect to deal with very large arrays of Coin,
117    // typically at most 2 denoms. Even in the future there are not that many Terra native coins
118    // where this will be a problem.
119
120    for coin in coins_to_add {
121        let mut same_denom_coins = new_coins.iter_mut().filter(|c| c.denom == coin.denom);
122        if let Some(same_denom_coin) = same_denom_coins.next() {
123            same_denom_coin.amount += coin.amount
124        } else {
125            new_coins.push(coin);
126        }
127    }
128
129    new_coins
130}
131
132/// Checks if the required funds can be covered by merging the provided coins.
133///
134/// ## Arguments
135/// * `coins` - The vector of `Coin` structs representing the available coins
136/// * `required` - The vector of `Coin` structs representing the required funds
137///
138/// Returns true if the required funds can be covered by merging the available coins, false otherwise.
139pub fn has_coins_merged(coins: &[Coin], required: &[Coin]) -> bool {
140    let merged_coins = merge_coins(vec![], coins.to_vec());
141    let merged_required = merge_coins(vec![], required.to_vec());
142
143    for required_funds in merged_required {
144        if !has_coins(&merged_coins, &required_funds) {
145            return false;
146        };
147    }
148
149    true
150}
151
152/// Deducts a given amount from a vector of `Coin` structs. Alters the given vector, does not return a new vector.
153///
154/// ## Arguments
155/// * `coins` - The vector of `Coin` structs from which to deduct the given funds
156/// * `funds` - The amount to deduct
157pub fn deduct_funds(coins: &mut [Coin], funds: &Coin) -> Result<(), ContractError> {
158    let coin_amount: Vec<&mut Coin> = coins
159        .iter_mut()
160        .filter(|c| c.denom.eq(&funds.denom))
161        .collect();
162
163    let mut remainder = funds.amount;
164    for same_coin in coin_amount {
165        if same_coin.amount > remainder {
166            same_coin.amount = same_coin.amount.checked_sub(remainder)?;
167            return Ok(());
168        } else {
169            remainder = remainder.checked_sub(same_coin.amount)?;
170            same_coin.amount = Uint128::zero();
171        }
172    }
173
174    ensure!(
175        remainder == Uint128::zero(),
176        ContractError::InsufficientFunds {}
177    );
178
179    Ok(())
180}
181
182#[cfg(test)]
183mod test {
184    use cosmwasm_std::{coin, Uint128, WasmMsg};
185    use cw20::Expiration;
186
187    use super::*;
188
189    #[cw_serde]
190    struct TestStruct {
191        name: String,
192        expiration: Expiration,
193    }
194
195    #[test]
196    fn test_merge_coins() {
197        let coins = vec![coin(100, "uusd"), coin(100, "uluna")];
198        let funds_to_add = vec![
199            coin(25, "uluna"),
200            coin(50, "uusd"),
201            coin(100, "ucad"),
202            coin(50, "uluna"),
203            coin(100, "uluna"),
204            coin(100, "ucad"),
205        ];
206
207        let res = merge_coins(coins, funds_to_add);
208        assert_eq!(
209            vec![coin(150, "uusd"), coin(275, "uluna"), coin(200, "ucad")],
210            res
211        );
212    }
213
214    #[test]
215    fn test_merge_sub_messages() {
216        let sub_msgs: Vec<SubMsg> = vec![
217            SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
218                to_address: "A".to_string(),
219                amount: vec![coin(100, "uusd"), coin(50, "uluna")],
220            })),
221            SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
222                to_address: "A".to_string(),
223                amount: vec![coin(100, "uusd"), coin(50, "ukrw")],
224            })),
225            SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
226                to_address: "B".to_string(),
227                amount: vec![coin(100, "uluna")],
228            })),
229            SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
230                to_address: "B".to_string(),
231                amount: vec![coin(50, "uluna")],
232            })),
233            SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {
234                contract_addr: "C".to_string(),
235                funds: vec![],
236                msg: encode_binary(&"").unwrap(),
237            })),
238        ];
239
240        let merged_msgs = merge_sub_msgs(sub_msgs);
241        assert_eq!(
242            vec![
243                SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {
244                    contract_addr: "C".to_string(),
245                    funds: vec![],
246                    msg: encode_binary(&"").unwrap(),
247                })),
248                SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
249                    to_address: "A".to_string(),
250                    amount: vec![coin(200, "uusd"), coin(50, "uluna"), coin(50, "ukrw")],
251                })),
252                SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
253                    to_address: "B".to_string(),
254                    amount: vec![coin(150, "uluna")],
255                })),
256            ],
257            merged_msgs
258        );
259
260        assert_eq!(3, merged_msgs.len());
261    }
262
263    #[test]
264    fn test_deduct_funds() {
265        let mut funds: Vec<Coin> = vec![coin(5, "uluna"), coin(100, "uusd"), coin(100, "uluna")];
266
267        deduct_funds(&mut funds, &coin(10, "uluna")).unwrap();
268
269        assert_eq!(Uint128::zero(), funds[0].amount);
270        assert_eq!(String::from("uluna"), funds[0].denom);
271        assert_eq!(Uint128::from(95u128), funds[2].amount);
272        assert_eq!(String::from("uluna"), funds[2].denom);
273
274        let mut funds: Vec<Coin> = vec![Coin {
275            denom: String::from("uluna"),
276            amount: Uint128::from(5u64),
277        }];
278
279        let e = deduct_funds(&mut funds, &coin(10, "uluna")).unwrap_err();
280
281        assert_eq!(ContractError::InsufficientFunds {}, e);
282    }
283    #[test]
284    fn test_has_coins_merged() {
285        let available_coins: Vec<Coin> = vec![
286            coin(50, "uluna"),
287            coin(200, "uusd"),
288            coin(50, "ukrw"),
289            coin(25, "uluna"),
290            coin(25, "uluna"),
291        ];
292        let required_funds: Vec<Coin> = vec![
293            coin(50, "uluna"),
294            coin(100, "uusd"),
295            coin(50, "ukrw"),
296            coin(50, "uluna"),
297        ];
298
299        assert!(has_coins_merged(&available_coins, &required_funds));
300
301        let insufficient_funds: Vec<Coin> =
302            vec![coin(10, "uluna"), coin(100, "uusd"), coin(50, "ukrw")];
303
304        assert!(!has_coins_merged(&insufficient_funds, &required_funds));
305    }
306}