apollo_utils/
assets.rs

1use apollo_cw_asset::{Asset, AssetInfo, AssetList};
2use cosmwasm_std::{
3    attr, to_binary, Addr, Api, Coin, CosmosMsg, Env, Event, MessageInfo, Response, StdError,
4    StdResult, WasmMsg,
5};
6use cw20::{Cw20Coin, Cw20ExecuteMsg};
7
8/// Create an AssetList from a `Vec<Coin>` and an optional `Vec<Cw20Coin>`.
9/// Removes duplicates from each of the inputs.
10pub fn to_asset_list(
11    api: &dyn Api,
12    coins: Option<&Vec<Coin>>,
13    cw20s: Option<&Vec<Cw20Coin>>,
14) -> StdResult<AssetList> {
15    let mut assets = AssetList::new();
16
17    if let Some(coins) = coins {
18        for coin in coins {
19            assets.add(&coin.into())?;
20        }
21    }
22
23    if let Some(cw20s) = cw20s {
24        for cw20 in cw20s {
25            assets.add(&Asset::new(
26                AssetInfo::Cw20(api.addr_validate(&cw20.address)?),
27                cw20.amount,
28            ))?;
29        }
30    }
31    Ok(assets)
32}
33
34/// Converts an `AssetList` into a `Vec<Coin>` and a `Vec<Cw20Coin>`.
35pub fn separate_natives_and_cw20s(assets: &AssetList) -> (Vec<Coin>, Vec<Cw20Coin>) {
36    let mut coins = vec![];
37    let mut cw20s = vec![];
38
39    for asset in assets.into_iter() {
40        match &asset.info {
41            AssetInfo::Native(token) => {
42                coins.push(Coin {
43                    denom: token.to_string(),
44                    amount: asset.amount,
45                });
46            }
47            AssetInfo::Cw20(addr) => {
48                cw20s.push(Cw20Coin {
49                    address: addr.to_string(),
50                    amount: asset.amount,
51                });
52            }
53        }
54    }
55
56    // Cosmos SDK coins need to be sorted and currently wasmd does not sort
57    // CosmWasm coins when converting into SDK coins.
58    coins.sort_by(|a, b| a.denom.cmp(&b.denom));
59
60    (coins, cw20s)
61}
62
63/// Assert that a specific native token in the form of an `Asset` was sent to
64/// the contract.
65pub fn assert_native_token_received(info: &MessageInfo, asset: &Asset) -> StdResult<()> {
66    let coin: Coin = asset.try_into()?;
67
68    if !info.funds.contains(&coin) {
69        return Err(StdError::generic_err(format!(
70            "Assert native token received failed for asset: {}",
71            asset
72        )));
73    }
74    Ok(())
75}
76
77/// Assert that all assets in the `AssetList` are native tokens, and that all of
78/// them were also sent in the correct amount in `info.funds`.
79/// Does not error if there are additional native tokens in `info.funds` that
80/// are not in the `AssetList`.
81///
82/// ### Returns
83/// Returns a `Vec<Coin>` with all the native tokens in `info.funds`.
84///
85/// ### Errors
86/// Returns an error if any of the assets in the `AssetList` are not native
87/// tokens.
88/// Returns an error if any of the native tokens in the `AssetList` were not
89/// sent in `info.funds`.
90pub fn assert_native_tokens_received(
91    info: &MessageInfo,
92    assets: &AssetList,
93) -> StdResult<Vec<Coin>> {
94    let coins = assert_only_native_coins(assets)?;
95    for coin in &coins {
96        if !info.funds.contains(&coin) {
97            return Err(StdError::generic_err(format!(
98                "Assert native token received failed for asset: {}",
99                coin
100            )));
101        }
102    }
103    Ok(info.funds.clone())
104}
105
106/// Calls TransferFrom on an Asset if it is a Cw20. If it is a native we just
107/// assert that the native token was already sent to the contract.
108///
109/// ### Returns
110/// Returns a response with the transfer_from message if the asset is a Cw20.
111/// Returns an empty response if the asset is a native token.
112pub fn receive_asset(info: &MessageInfo, env: &Env, asset: &Asset) -> StdResult<Response> {
113    let event = Event::new("apollo/utils/assets").add_attributes(vec![
114        attr("action", "receive_asset"),
115        attr("asset", asset.to_string()),
116    ]);
117    match &asset.info {
118        AssetInfo::Cw20(_coin) => {
119            let msg =
120                asset.transfer_from_msg(info.sender.clone(), env.contract.address.to_string())?;
121            Ok(Response::new().add_message(msg).add_event(event))
122        }
123        AssetInfo::Native(_token) => {
124            //Here we just assert that the native token was sent with the contract call
125            assert_native_token_received(info, asset)?;
126            Ok(Response::new().add_event(event))
127        }
128    }
129}
130
131/// Returns an `Option` with a [`CosmosMsg`] that transfers the asset
132/// to `env.contract.address`. If the asset is a native token, it checks
133/// the that the funds were recieved in `info.funds` and returns `None`.
134fn receive_asset_msg(info: &MessageInfo, env: &Env, asset: &Asset) -> StdResult<Option<CosmosMsg>> {
135    match &asset.info {
136        AssetInfo::Cw20(_coin) => {
137            Some(asset.transfer_from_msg(info.sender.clone(), env.contract.address.to_string()))
138                .transpose()
139        }
140        AssetInfo::Native(_token) => {
141            //Here we just assert that the native token was sent with the contract call
142            assert_native_token_received(info, asset)?;
143            Ok(None)
144        }
145    }
146}
147
148/// Verifies that all native tokens were a sent in `info.funds` and returns
149/// a `Response` with a messages that transfers all Cw20 tokens to
150/// `env.contract.address`.
151pub fn receive_assets(info: &MessageInfo, env: &Env, assets: &AssetList) -> StdResult<Response> {
152    let event = Event::new("apollo/utils/assets").add_attributes(vec![
153        attr("action", "receive_assets"),
154        attr("assets", assets.to_string()),
155    ]);
156    let msgs = assets
157        .into_iter()
158        .map(|asset| receive_asset_msg(info, env, asset))
159        .collect::<StdResult<Vec<Option<_>>>>()?
160        .into_iter()
161        .filter_map(|msg| msg)
162        .collect::<Vec<_>>();
163
164    Ok(Response::new().add_messages(msgs).add_event(event))
165}
166
167/// Assert that all assets in the `AssetList` are native tokens.
168///
169/// ### Returns
170/// Returns an error if any of the assets are not native tokens.
171/// Returns a `StdResult<Vec<Coin>>` containing the assets as coins if they are
172/// all native tokens.
173pub fn assert_only_native_coins(assets: &AssetList) -> StdResult<Vec<Coin>> {
174    assets
175        .into_iter()
176        .map(assert_native_coin)
177        .collect::<StdResult<Vec<Coin>>>()
178}
179
180/// Assert that an asset is a native token.
181///
182/// ### Returns
183/// Returns an error if the asset is not a native token.
184/// Returns a `StdResult<Coin>` containing the asset as a coin if it is a native
185/// token.
186pub fn assert_native_coin(asset: &Asset) -> StdResult<Coin> {
187    match asset.info {
188        AssetInfo::Native(_) => asset.try_into(),
189        _ => Err(StdError::generic_err("Asset is not a native token")),
190    }
191}
192
193/// Assert that an AssetInfo is a native token.
194///
195/// ### Returns
196/// Returns an error if the AssetInfo is not a native token.
197/// Returns a `StdResult<String>` containing the denom if it is a native token.
198pub fn assert_native_asset_info(asset_info: &AssetInfo) -> StdResult<String> {
199    match asset_info {
200        AssetInfo::Native(denom) => Ok(denom.clone()),
201        _ => Err(StdError::generic_err("AssetInfo is not a native token")),
202    }
203}
204
205/// Merge duplicates of assets in an `AssetList`.
206///
207/// ### Returns
208/// Returns the asset list with all duplicates merged.
209pub fn merge_assets<'a, A: Into<&'a AssetList>>(assets: A) -> StdResult<AssetList> {
210    let asset_list = assets.into();
211    let mut merged = AssetList::new();
212    for asset in asset_list {
213        merged.add(asset)?;
214    }
215    Ok(merged)
216}
217
218/// Separate native tokens and Cw20's in an `AssetList` and return messages
219/// for increasing allowance for the Cw20's.
220///
221/// ### Returns
222/// Returns a `StdResult<(Vec<CosmosMsg>, Vec<Coin>)>` containing the messages
223/// for increasing allowance and the native tokens.
224pub fn increase_allowance_msgs(
225    env: &Env,
226    assets: &AssetList,
227    recipient: Addr,
228) -> StdResult<(Vec<CosmosMsg>, Vec<Coin>)> {
229    let (funds, cw20s) = separate_natives_and_cw20s(assets);
230    let msgs: Vec<CosmosMsg> = cw20s
231        .into_iter()
232        .map(|x| {
233            Ok(CosmosMsg::Wasm(WasmMsg::Execute {
234                contract_addr: x.address,
235                msg: to_binary(&Cw20ExecuteMsg::IncreaseAllowance {
236                    spender: recipient.to_string(),
237                    amount: x.amount,
238                    expires: Some(cw20::Expiration::AtHeight(env.block.height + 1)),
239                })?,
240                funds: vec![],
241            }))
242        })
243        .collect::<StdResult<Vec<_>>>()?;
244    Ok((msgs, funds))
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use apollo_cw_asset::{Asset, AssetInfo, AssetInfoBase, AssetList};
251    use cosmwasm_std::testing::{mock_env, mock_info, MockApi};
252    use cosmwasm_std::CosmosMsg::Wasm;
253    use cosmwasm_std::ReplyOn::Never;
254    use cosmwasm_std::StdError::GenericErr;
255    use cosmwasm_std::WasmMsg::Execute;
256    use cosmwasm_std::{to_binary, Addr, Coin, SubMsg, Uint128};
257    use cw20::{Cw20ExecuteMsg, Expiration};
258    use test_case::test_case;
259
260    #[test_case(
261        vec![Coin::new(1000, "uosmo"), Coin::new(1000, "uatom")].into(),
262        vec![Coin::new(1000, "uosmo"), Coin::new(1000, "uatom")]
263        => Ok(());
264        "Only native tokens, all sent")]
265    #[test_case(
266        vec![Coin::new(1000, "uosmo"), Coin::new(1000, "uatom")].into(),
267        vec![Coin::new(1000, "uosmo"), Coin::new(10, "uatom")]
268        => Err(StdError::generic_err("Assert native token received failed for asset: 1000uatom"));
269        "Only native tokens, some not sent")]
270    #[test_case(
271        vec![Coin::new(1000, "uosmo"), Coin::new(1000, "uatom")].into(),
272        vec![Coin::new(1000, "uosmo")]
273        => Err(StdError::generic_err("Assert native token received failed for asset: 1000uatom"));
274        "Only native tokens, one missing coin")]
275    #[test_case(
276        vec![Asset::new(AssetInfo::Native("uosmo".into()), 1000u128), Asset::new(AssetInfo::cw20(Addr::unchecked("apollo")), 1000u128)].into(),
277        vec![Coin::new(1000, "uosmo")]
278        => Err(StdError::generic_err("Asset is not a native token"));
279        "Mixed native and cw20 tokens")]
280    #[test_case(
281        AssetList::new(),
282        vec![]
283        => Ok(());
284        "Empty asset list, empty funds")]
285    #[test_case(
286        vec![Coin::new(1000, "uosmo")].into(),
287        vec![]
288        => Err(StdError::generic_err("Assert native token received failed for asset: 1000uosmo"));
289        "1 native token in asset list, empty funds")]
290    #[test_case(
291        AssetList::new(),
292        vec![Coin::new(1000, "uosmo")]
293        => Ok(());
294        "Empty asset list, 1 native token in funds")]
295    fn test_assert_native_tokens_received(assets: AssetList, funds: Vec<Coin>) -> StdResult<()> {
296        let info = mock_info("addr", &funds);
297        assert_native_tokens_received(&info, &assets)?;
298        Ok(())
299    }
300
301    #[test]
302    fn test_receive_asset_cw20() {
303        let funds = vec![Coin::new(1000, "uosmo")];
304        let info = mock_info("addr", &funds);
305        let env = mock_env();
306        let asset = Asset::new(AssetInfo::cw20(Addr::unchecked("apollo")), 1000u128);
307        let result = receive_asset(&info, &env, &asset);
308        assert!(result.is_ok());
309        let response = result.unwrap();
310
311        let expected_events = vec![Event::new("apollo/utils/assets").add_attributes(vec![
312            attr("action", "receive_asset"),
313            attr("asset", "apollo:1000"),
314        ])];
315
316        let expected_messages = vec![SubMsg {
317            id: 0,
318            msg: Wasm(Execute {
319                contract_addr: String::from("apollo"),
320                msg: to_binary(
321                    &(Cw20ExecuteMsg::TransferFrom {
322                        owner: String::from("addr"),
323                        recipient: String::from("cosmos2contract"),
324                        amount: Uint128::new(1000),
325                    }),
326                )
327                .unwrap(),
328                funds: vec![],
329            }),
330            gas_limit: None,
331            reply_on: Never,
332        }];
333        assert_eq!(response.messages.len(), 1);
334        assert_eq!(response.messages[0], expected_messages[0]);
335        assert_eq!(response.events.len(), 1);
336        assert_eq!(response.events, expected_events);
337    }
338
339    #[test]
340    fn test_receive_asset_native() {
341        let funds = vec![Coin::new(1000, "uosmo")];
342        let info = mock_info("addr", &funds);
343        let env = mock_env();
344        let asset = Asset::new(AssetInfo::Native("uosmo".into()), 1000u128);
345        let result = receive_asset(&info, &env, &asset);
346        assert!(result.is_ok());
347        let response = result.unwrap();
348        assert_eq!(response.messages.len(), 0);
349        assert_eq!(response.events.len(), 1);
350    }
351
352    #[test_case(
353        Asset {
354            info: AssetInfoBase::Native(String::from("uosmo")),
355            amount: Uint128::new(10),
356        },vec![Coin::new(10, "uosmo")] => Ok(());
357        "Native token received")]
358    #[test_case(
359        Asset {
360            info: AssetInfoBase::Native(String::from("uion")),
361            amount: Uint128::new(10),
362        },vec![Coin::new(10, "uosmo")] => Err(GenericErr { msg: String::from("Assert native token received failed for asset: uion:10") });
363            "Native token not received")]
364    #[test_case(
365        Asset {
366            info: AssetInfoBase::Native(String::from("uosmo")),
367            amount: Uint128::new(10),
368        },vec![Coin::new(20, "uosmo")] => Err(GenericErr { msg: String::from("Assert native token received failed for asset: uosmo:10") });
369                "Native token quantity mismatch")]
370    fn test_assert_native_token_received(asset: Asset, funds: Vec<Coin>) -> StdResult<()> {
371        let info = MessageInfo {
372            funds: funds,
373            sender: Addr::unchecked("sender"),
374        };
375        assert_native_token_received(&info, &asset)
376    }
377
378    #[test]
379    fn test_separate_natives_and_cw20s() {
380        let api = MockApi::default();
381        let coins = &vec![
382            Coin::new(10, "uosmo"),
383            Coin::new(20, "uatom"),
384            Coin::new(10, "uion"),
385        ];
386        let cw20s = &vec![
387            Cw20Coin {
388                address: "osmo1".to_owned(),
389                amount: Uint128::new(100),
390            },
391            Cw20Coin {
392                address: "osmo2".to_owned(),
393                amount: Uint128::new(200),
394            },
395            Cw20Coin {
396                address: "osmo3".to_owned(),
397                amount: Uint128::new(300),
398            },
399        ];
400
401        let asset_list = to_asset_list(&api, Some(coins), Some(cw20s)).unwrap();
402        let (separated_coins, separated_cw20s) = separate_natives_and_cw20s(&asset_list);
403
404        assert_eq!(separated_coins.len(), coins.len());
405        assert_eq!(separated_cw20s.len(), cw20s.len());
406        for coin in coins.iter() {
407            assert!(separated_coins.contains(coin));
408        }
409        for cw20 in cw20s.iter() {
410            assert!(separated_cw20s.contains(cw20));
411        }
412    }
413
414    #[test]
415    fn test_separate_natives_and_cw20s_with_empty_inputs() {
416        let api = MockApi::default();
417        let coins = None;
418        let cw20s = None;
419
420        let empty_asset_list = to_asset_list(&api, coins, cw20s).unwrap();
421        let (coins, cw20s) = separate_natives_and_cw20s(&empty_asset_list);
422
423        assert!(coins.len() == 0);
424        assert!(cw20s.len() == 0);
425    }
426
427    #[test]
428    fn test_to_asset_list() {
429        let api = MockApi::default();
430        let coins = &vec![
431            Coin::new(10, "uosmo"),
432            Coin::new(20, "uatom"),
433            Coin::new(10, "uion"),
434        ];
435        let cw20s = &vec![
436            Cw20Coin {
437                address: "osmo1".to_owned(),
438                amount: Uint128::new(100),
439            },
440            Cw20Coin {
441                address: "osmo2".to_owned(),
442                amount: Uint128::new(200),
443            },
444            Cw20Coin {
445                address: "osmo3".to_owned(),
446                amount: Uint128::new(300),
447            },
448        ];
449
450        let assets = to_asset_list(&api, Some(coins), Some(cw20s)).unwrap();
451
452        assert_eq!(assets.len(), coins.len() + cw20s.len());
453        for coin in coins.iter() {
454            assert!(assets
455                .find(&AssetInfo::Native(coin.denom.to_owned()))
456                .is_some());
457        }
458        for cw20 in cw20s.iter() {
459            assert!(assets
460                .find(&AssetInfo::Cw20(api.addr_validate(&cw20.address).unwrap()))
461                .is_some());
462        }
463    }
464
465    #[test]
466    fn test_to_asset_list_with_empty_inputs() {
467        let api = MockApi::default();
468        let coins = None;
469        let cw20s = None;
470
471        let assets = to_asset_list(&api, coins, cw20s).unwrap();
472
473        assert!(assets.len() == 0);
474    }
475
476    #[test]
477    fn test_merge_assets_with_no_duplicates() {
478        let mut asset_list = AssetList::new();
479
480        asset_list
481            .add(
482                &(Asset {
483                    info: AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 1"))),
484                    amount: Uint128::new(100),
485                }),
486            )
487            .unwrap();
488        asset_list
489            .add(
490                &(Asset {
491                    info: AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 2"))),
492                    amount: Uint128::new(200),
493                }),
494            )
495            .unwrap();
496        asset_list
497            .add(
498                &(Asset {
499                    info: AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 3"))),
500                    amount: Uint128::new(300),
501                }),
502            )
503            .unwrap();
504
505        let merged_assets = merge_assets(&asset_list).unwrap();
506
507        assert_eq!(merged_assets.len(), 3);
508        assert_eq!(
509            merged_assets.to_vec()[0].info,
510            AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 1")))
511        );
512        assert_eq!(merged_assets.to_vec()[0].amount, Uint128::new(100));
513        assert_eq!(
514            merged_assets.to_vec()[1].info,
515            AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 2")))
516        );
517        assert_eq!(merged_assets.to_vec()[1].amount, Uint128::new(200));
518        assert_eq!(
519            merged_assets.to_vec()[2].info,
520            AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 3")))
521        );
522        assert_eq!(merged_assets.to_vec()[2].amount, Uint128::new(300));
523    }
524
525    #[test]
526    fn test_merge_assets_with_duplicates() {
527        let mut asset_list = AssetList::new();
528        asset_list
529            .add(
530                &(Asset {
531                    info: AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 1"))),
532                    amount: Uint128::new(100),
533                }),
534            )
535            .unwrap();
536        asset_list
537            .add(
538                &(Asset {
539                    info: AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 1"))),
540                    amount: Uint128::new(200),
541                }),
542            )
543            .unwrap();
544        let merged_assets = merge_assets(&asset_list).unwrap();
545
546        assert_eq!(merged_assets.len(), 1);
547        assert_eq!(
548            merged_assets.to_vec()[0].info,
549            AssetInfoBase::Cw20(Addr::unchecked(String::from("Asset 1")))
550        );
551        assert_eq!(merged_assets.to_vec()[0].amount, Uint128::new(300));
552    }
553
554    #[test]
555    fn test_increase_allowance_msgs() {
556        let env = mock_env();
557        let spender = Addr::unchecked(String::from("spender"));
558
559        let assets = AssetList::from(vec![
560            Asset::new(AssetInfo::Native("uatom".to_string()), Uint128::new(100)),
561            Asset::new(
562                AssetInfo::Cw20(Addr::unchecked("cw20".to_string())),
563                Uint128::new(200),
564            ),
565        ]);
566        let (increase_allowance_msgs, funds) =
567            increase_allowance_msgs(&env, &assets, spender.clone()).unwrap();
568
569        assert_eq!(increase_allowance_msgs.len(), 1);
570        assert_eq!(
571            increase_allowance_msgs[0],
572            CosmosMsg::Wasm(WasmMsg::Execute {
573                contract_addr: "cw20".to_string(),
574                funds: vec![],
575                msg: to_binary(&Cw20ExecuteMsg::IncreaseAllowance {
576                    spender: spender.to_string(),
577                    amount: Uint128::new(200),
578                    expires: Some(Expiration::AtHeight(env.block.height + 1)),
579                })
580                .unwrap(),
581            })
582        );
583        assert_eq!(funds.len(), 1);
584        assert_eq!(funds[0].amount, Uint128::new(100));
585        assert_eq!(funds[0].denom, "uatom");
586    }
587}