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
8pub 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
34pub 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 coins.sort_by(|a, b| a.denom.cmp(&b.denom));
59
60 (coins, cw20s)
61}
62
63pub 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
77pub 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
106pub 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 assert_native_token_received(info, asset)?;
126 Ok(Response::new().add_event(event))
127 }
128 }
129}
130
131fn 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 assert_native_token_received(info, asset)?;
143 Ok(None)
144 }
145 }
146}
147
148pub 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
167pub 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
180pub 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
193pub 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
205pub 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
218pub 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}