1use std::{any::type_name, fmt, str::FromStr};
2
3use cosmwasm_schema::cw_serde;
4use cosmwasm_std::{
5 to_json_binary, Addr, Api, BalanceResponse, BankQuery, QuerierWrapper, QueryRequest, StdError,
6 StdResult, Uint128, WasmQuery,
7};
8use cw20::{BalanceResponse as Cw20BalanceResponse, Cw20QueryMsg};
9use cw_address_like::AddressLike;
10use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
11
12use crate::AssetError;
13
14#[cw_serde]
23#[derive(Eq, PartialOrd, Ord, Hash)]
24#[non_exhaustive]
25pub enum AssetInfoBase<T: AddressLike> {
26 Native(String),
27 Cw20(T),
28}
29
30impl<T: AddressLike> AssetInfoBase<T> {
31 pub fn native<A: Into<String>>(denom: A) -> Self {
40 AssetInfoBase::Native(denom.into())
41 }
42
43 pub fn cw20<A: Into<T>>(contract_addr: A) -> Self {
52 AssetInfoBase::Cw20(contract_addr.into())
53 }
54}
55
56pub type AssetInfoUnchecked = AssetInfoBase<String>;
59
60pub type AssetInfo = AssetInfoBase<Addr>;
63
64impl AssetInfo {
65 pub fn inner(&self) -> String {
67 match self {
68 AssetInfoBase::Native(denom) => denom.clone(),
69 AssetInfoBase::Cw20(addr) => addr.into(),
70 }
71 }
72}
73
74impl FromStr for AssetInfoUnchecked {
75 type Err = AssetError;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 let words: Vec<&str> = s.split(':').collect();
79
80 match words[0] {
81 "native" => {
82 if words.len() != 2 {
83 return Err(AssetError::InvalidAssetInfoFormat {
84 received: s.into(),
85 should_be: "native:{denom}".into(),
86 });
87 }
88 Ok(AssetInfoUnchecked::Native(String::from(words[1])))
89 },
90 "cw20" => {
91 if words.len() != 2 {
92 return Err(AssetError::InvalidAssetInfoFormat {
93 received: s.into(),
94 should_be: "cw20:{contract_addr}".into(),
95 });
96 }
97 Ok(AssetInfoUnchecked::Cw20(String::from(words[1])))
98 },
99 ty => Err(AssetError::InvalidAssetType {
100 ty: ty.into(),
101 }),
102 }
103 }
104}
105
106impl From<AssetInfo> for AssetInfoUnchecked {
107 fn from(asset_info: AssetInfo) -> Self {
108 match asset_info {
109 AssetInfo::Cw20(contract_addr) => AssetInfoUnchecked::Cw20(contract_addr.into()),
110 AssetInfo::Native(denom) => AssetInfoUnchecked::Native(denom),
111 }
112 }
113}
114
115impl From<&AssetInfo> for AssetInfoUnchecked {
116 fn from(asset_info: &AssetInfo) -> Self {
117 match asset_info {
118 AssetInfo::Cw20(contract_addr) => AssetInfoUnchecked::Cw20(contract_addr.into()),
119 AssetInfo::Native(denom) => AssetInfoUnchecked::Native(denom.into()),
120 }
121 }
122}
123
124impl AssetInfoUnchecked {
125 pub fn check(
145 &self,
146 api: &dyn Api,
147 optional_whitelist: Option<&[&str]>,
148 ) -> Result<AssetInfo, AssetError> {
149 match self {
150 AssetInfoUnchecked::Native(denom) => {
151 if let Some(whitelist) = optional_whitelist {
152 if !whitelist.contains(&&denom[..]) {
153 return Err(AssetError::UnacceptedDenom {
154 denom: denom.clone(),
155 whitelist: whitelist.join("|"),
156 });
157 }
158 }
159 Ok(AssetInfo::Native(denom.clone()))
160 },
161 AssetInfoUnchecked::Cw20(contract_addr) => {
162 Ok(AssetInfo::Cw20(api.addr_validate(contract_addr)?))
163 },
164 }
165 }
166}
167
168impl fmt::Display for AssetInfo {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 AssetInfo::Cw20(contract_addr) => write!(f, "cw20:{contract_addr}"),
172 AssetInfo::Native(denom) => write!(f, "native:{denom}"),
173 }
174 }
175}
176
177impl AssetInfo {
178 pub fn query_balance<T: Into<String>>(
190 &self,
191 querier: &QuerierWrapper,
192 address: T,
193 ) -> Result<Uint128, AssetError> {
194 match self {
195 AssetInfo::Native(denom) => {
196 let response: BalanceResponse =
197 querier.query(&QueryRequest::Bank(BankQuery::Balance {
198 address: address.into(),
199 denom: denom.clone(),
200 }))?;
201 Ok(response.amount.amount)
202 },
203 AssetInfo::Cw20(contract_addr) => {
204 let response: Cw20BalanceResponse =
205 querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
206 contract_addr: contract_addr.into(),
207 msg: to_json_binary(&Cw20QueryMsg::Balance {
208 address: address.into(),
209 })?,
210 }))?;
211 Ok(response.balance)
212 },
213 }
214 }
215
216 fn from_str(s: &str) -> Result<Self, AssetError> {
218 let words: Vec<&str> = s.split(':').collect();
219
220 match words[0] {
221 "native" => {
222 if words.len() != 2 {
223 return Err(AssetError::InvalidAssetInfoFormat {
224 received: s.into(),
225 should_be: "native:{denom}".into(),
226 });
227 }
228 Ok(AssetInfo::Native(String::from(words[1])))
229 },
230 "cw20" => {
231 if words.len() != 2 {
232 return Err(AssetError::InvalidAssetInfoFormat {
233 received: s.into(),
234 should_be: "cw20:{contract_addr}".into(),
235 });
236 }
237 Ok(AssetInfo::Cw20(Addr::unchecked(words[1])))
238 },
239 ty => Err(AssetError::InvalidAssetType {
240 ty: ty.into(),
241 }),
242 }
243 }
244}
245
246impl<'a> PrimaryKey<'a> for &AssetInfo {
247 type Prefix = String;
248 type SubPrefix = ();
249 type Suffix = String;
250 type SuperSuffix = Self;
251
252 fn key(&self) -> Vec<Key> {
253 let mut keys = vec![];
254 match &self {
255 AssetInfo::Cw20(addr) => {
256 keys.extend("cw20:".key());
257 keys.extend(addr.key());
258 },
259 AssetInfo::Native(denom) => {
260 keys.extend("native:".key());
261 keys.extend(denom.key());
262 },
263 };
264 keys
265 }
266}
267
268impl KeyDeserialize for &AssetInfo {
269 const KEY_ELEMS: u16 = 1;
270
271 type Output = AssetInfo;
272
273 #[inline(always)]
274 fn from_vec(mut value: Vec<u8>) -> StdResult<Self::Output> {
275 value.drain(0..2);
279
280 let s = String::from_utf8(value)?;
282
283 AssetInfo::from_str(&s).map_err(|err| StdError::parse_err(type_name::<Self::Output>(), err))
285 }
286}
287
288impl<'a> Prefixer<'a> for &AssetInfo {
289 fn prefix(&self) -> Vec<Key> {
290 self.key()
291 }
292}
293
294#[cfg(test)]
299mod test {
300 use std::collections::{BTreeMap, HashMap};
301
302 use cosmwasm_std::{testing::MockApi, Coin};
303
304 use super::{super::testing::mock_dependencies, *};
305
306 #[test]
307 fn creating_instances() {
308 let info = AssetInfo::cw20(Addr::unchecked("mock_token"));
309 assert_eq!(info, AssetInfo::Cw20(Addr::unchecked("mock_token")));
310
311 let info = AssetInfo::native("uusd");
312 assert_eq!(info, AssetInfo::Native(String::from("uusd")));
313 }
314
315 #[test]
316 fn comparing() {
317 let uluna = AssetInfo::native("uluna");
318 let uusd = AssetInfo::native("uusd");
319 let astro = AssetInfo::cw20(Addr::unchecked("astro_token"));
320 let mars = AssetInfo::cw20(Addr::unchecked("mars_token"));
321
322 assert!(uluna != uusd);
323 assert!(uluna != astro);
324 assert!(astro != mars);
325 assert!(uluna == uluna.clone());
326 assert!(astro == astro.clone());
327 }
328
329 #[test]
330 fn from_string() {
331 let s = "";
332 assert_eq!(
333 AssetInfoUnchecked::from_str(s),
334 Err(AssetError::InvalidAssetType {
335 ty: "".into()
336 }),
337 );
338
339 let s = "native:uusd:12345";
340 assert_eq!(
341 AssetInfoUnchecked::from_str(s),
342 Err(AssetError::InvalidAssetInfoFormat {
343 received: s.into(),
344 should_be: "native:{denom}".into(),
345 }),
346 );
347
348 let s = "cw721:galactic_punk";
349 assert_eq!(
350 AssetInfoUnchecked::from_str(s),
351 Err(AssetError::InvalidAssetType {
352 ty: "cw721".into(),
353 })
354 );
355
356 let s = "native:uusd";
357 assert_eq!(AssetInfoUnchecked::from_str(s).unwrap(), AssetInfoUnchecked::native("uusd"),);
358
359 let s = "cw20:mock_token";
360 assert_eq!(
361 AssetInfoUnchecked::from_str(s).unwrap(),
362 AssetInfoUnchecked::cw20("mock_token"),
363 );
364 }
365
366 #[test]
367 fn to_string() {
368 let info = AssetInfo::native("uusd");
369 assert_eq!(info.to_string(), String::from("native:uusd"));
370
371 let info = AssetInfo::cw20(Addr::unchecked("mock_token"));
372 assert_eq!(info.to_string(), String::from("cw20:mock_token"));
373 }
374
375 #[test]
376 fn checking() {
377 let api = MockApi::default();
378 let token_addr = api.addr_make("mock_token");
379
380 let checked = AssetInfo::cw20(token_addr);
381 let unchecked: AssetInfoUnchecked = checked.clone().into();
382 assert_eq!(unchecked.check(&api, None).unwrap(), checked);
383
384 let checked = AssetInfo::native("uusd");
385 let unchecked: AssetInfoUnchecked = checked.clone().into();
386 assert_eq!(unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])).unwrap(), checked);
387
388 let unchecked = AssetInfoUnchecked::native("uatom");
389 assert_eq!(
390 unchecked.check(&api, Some(&["uusd", "uluna", "uosmo"])),
391 Err(AssetError::UnacceptedDenom {
392 denom: "uatom".into(),
393 whitelist: "uusd|uluna|uosmo".into(),
394 }),
395 );
396 }
397
398 #[test]
399 fn checking_uppercase() {
400 let api = MockApi::default();
401 let mut token_addr = api.addr_make("mock_token");
402 token_addr = Addr::unchecked(token_addr.into_string().to_uppercase());
403
404 let unchecked = AssetInfoUnchecked::cw20(token_addr);
405 assert_eq!(
406 unchecked.check(&api, None).unwrap_err(),
407 StdError::generic_err("Invalid input: address not normalized").into(),
408 );
409 }
410
411 #[test]
412 fn querying_balance() {
413 let mut deps = mock_dependencies();
414 deps.querier.set_base_balances("alice", &[Coin::new(12345u128, "uusd")]);
415 deps.querier.set_cw20_balance("mock_token", "bob", 67890);
416
417 let info1 = AssetInfo::native("uusd");
418 let balance1 = info1.query_balance(&deps.as_ref().querier, "alice").unwrap();
419 assert_eq!(balance1, Uint128::new(12345));
420
421 let info2 = AssetInfo::cw20(Addr::unchecked("mock_token"));
422 let balance2 = info2.query_balance(&deps.as_ref().querier, "bob").unwrap();
423 assert_eq!(balance2, Uint128::new(67890));
424 }
425
426 use cosmwasm_std::{Addr, Order};
427 use cw_storage_plus::{Bound, Map};
428
429 fn mock_key() -> AssetInfo {
430 AssetInfo::native("uusd")
431 }
432
433 fn mock_keys() -> (AssetInfo, AssetInfo, AssetInfo) {
434 (
435 AssetInfo::native("uusd"),
436 AssetInfo::cw20(Addr::unchecked("mock_token")),
437 AssetInfo::cw20(Addr::unchecked("mock_token2")),
438 )
439 }
440
441 #[test]
442 fn storage_key_works() {
443 let mut deps = mock_dependencies();
444 let key = mock_key();
445 let map: Map<&AssetInfo, u64> = Map::new("map");
446
447 map.save(deps.as_mut().storage, &key, &42069).unwrap();
448
449 assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
450
451 let items = map
452 .range(deps.as_ref().storage, None, None, Order::Ascending)
453 .map(|item| item.unwrap())
454 .collect::<Vec<_>>();
455
456 assert_eq!(items.len(), 1);
457 assert_eq!(items[0], (key, 42069));
458 }
459
460 #[test]
461 fn composite_key_works() {
462 let mut deps = mock_dependencies();
463 let key = mock_key();
464 let map: Map<(&AssetInfo, Addr), u64> = Map::new("map");
465
466 map.save(deps.as_mut().storage, (&key, Addr::unchecked("larry")), &42069).unwrap();
467
468 map.save(deps.as_mut().storage, (&key, Addr::unchecked("jake")), &69420).unwrap();
469
470 let items = map
471 .prefix(&key)
472 .range(deps.as_ref().storage, None, None, Order::Ascending)
473 .map(|item| item.unwrap())
474 .collect::<Vec<_>>();
475
476 assert_eq!(items.len(), 2);
477 assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
478 assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
479 }
480
481 #[test]
482 fn triple_asset_key_works() {
483 let mut deps = mock_dependencies();
484 let map: Map<(&AssetInfo, &AssetInfo, &AssetInfo), u64> = Map::new("map");
485
486 let (key1, key2, key3) = mock_keys();
487 map.save(deps.as_mut().storage, (&key1, &key2, &key3), &42069).unwrap();
488 map.save(deps.as_mut().storage, (&key1, &key1, &key2), &11).unwrap();
489 map.save(deps.as_mut().storage, (&key1, &key1, &key3), &69420).unwrap();
490
491 let items = map
492 .prefix((&key1, &key1))
493 .range(deps.as_ref().storage, None, None, Order::Ascending)
494 .map(|item| item.unwrap())
495 .collect::<Vec<_>>();
496 assert_eq!(items.len(), 2);
497 assert_eq!(items[1], (key3.clone(), 69420));
498 assert_eq!(items[0], (key2.clone(), 11));
499
500 let val1 = map.load(deps.as_ref().storage, (&key1, &key2, &key3)).unwrap();
501 assert_eq!(val1, 42069);
502 }
503
504 #[test]
505 fn std_maps_asset_info() {
506 let mut map: HashMap<AssetInfo, u64> = HashMap::new();
507
508 let asset_cw20 = AssetInfo::cw20(Addr::unchecked("cosmwasm1"));
509 let asset_native = AssetInfo::native(Addr::unchecked("native1"));
510 let asset_fake_native = AssetInfo::native(Addr::unchecked("cosmwasm1"));
511
512 map.insert(asset_cw20.clone(), 1);
513 map.insert(asset_native.clone(), 2);
514 map.insert(asset_fake_native.clone(), 3);
515
516 assert_eq!(&1, map.get(&asset_cw20).unwrap());
517 assert_eq!(&2, map.get(&asset_native).unwrap());
518 assert_eq!(&3, map.get(&asset_fake_native).unwrap());
519
520 let mut map: BTreeMap<AssetInfo, u64> = BTreeMap::new();
521
522 map.insert(asset_cw20.clone(), 1);
523 map.insert(asset_native.clone(), 2);
524 map.insert(asset_fake_native.clone(), 3);
525
526 assert_eq!(&1, map.get(&asset_cw20).unwrap());
527 assert_eq!(&2, map.get(&asset_native).unwrap());
528 assert_eq!(&3, map.get(&asset_fake_native).unwrap());
529 }
530
531 #[test]
532 fn inner() {
533 assert_eq!(AssetInfo::native("denom").inner(), "denom".to_string());
534 assert_eq!(AssetInfo::cw20(Addr::unchecked("addr")).inner(), "addr".to_string())
535 }
536
537 #[test]
538 fn prefix() {
539 let mut deps = mock_dependencies();
540 let map: Map<&AssetInfo, u64> = Map::new("map");
541
542 let asset_cw20_1 = AssetInfo::cw20(Addr::unchecked("cosmwasm1"));
543 let asset_cw20_2 = AssetInfo::cw20(Addr::unchecked("cosmwasm2"));
544 let asset_cw20_3 = AssetInfo::cw20(Addr::unchecked("cosmwasm3"));
545
546 let asset_native_1 = AssetInfo::native(Addr::unchecked("native1"));
547 let asset_native_2 = AssetInfo::native(Addr::unchecked("native2"));
548 let asset_native_3 = AssetInfo::native(Addr::unchecked("native3"));
549
550 map.save(deps.as_mut().storage, &asset_cw20_1, &1).unwrap();
551 map.save(deps.as_mut().storage, &asset_cw20_3, &3).unwrap();
552 map.save(deps.as_mut().storage, &asset_cw20_2, &2).unwrap();
553
554 map.save(deps.as_mut().storage, &asset_native_2, &20).unwrap();
555 map.save(deps.as_mut().storage, &asset_native_3, &30).unwrap();
556 map.save(deps.as_mut().storage, &asset_native_1, &10).unwrap();
557
558 let cw20_ascending = map
562 .prefix("cw20:".to_string())
563 .range(deps.as_ref().storage, None, None, Order::Ascending)
564 .collect::<StdResult<Vec<(String, u64)>>>()
565 .unwrap();
566
567 assert_eq!(
568 vec![
569 ("cosmwasm1".to_string(), 1),
570 ("cosmwasm2".to_string(), 2),
571 ("cosmwasm3".to_string(), 3)
572 ],
573 cw20_ascending
574 );
575
576 let native_ascending = map
578 .prefix("native:".to_string())
579 .range(
580 deps.as_ref().storage,
581 Some(Bound::exclusive(asset_native_1.inner())),
582 None,
583 Order::Ascending,
584 )
585 .collect::<StdResult<Vec<(String, u64)>>>()
586 .unwrap();
587
588 assert_eq!(
589 vec![
590 ("native2".to_string(), 20),
592 ("native3".to_string(), 30)
593 ],
594 native_ascending
595 );
596
597 let cw20_descending = map
601 .prefix("cw20:".to_string())
602 .range(deps.as_ref().storage, None, None, Order::Descending)
603 .collect::<StdResult<Vec<(String, u64)>>>()
604 .unwrap();
605
606 assert_eq!(
607 vec![
608 ("cosmwasm3".to_string(), 3),
609 ("cosmwasm2".to_string(), 2),
610 ("cosmwasm1".to_string(), 1)
611 ],
612 cw20_descending
613 );
614
615 let native_descending = map
617 .prefix("native:".to_string())
618 .range(
619 deps.as_ref().storage,
620 None,
621 Some(Bound::exclusive(asset_native_3.inner())),
622 Order::Descending,
623 )
624 .collect::<StdResult<Vec<(String, u64)>>>()
625 .unwrap();
626
627 assert_eq!(
628 vec![
629 ("native2".to_string(), 20),
631 ("native1".to_string(), 10)
632 ],
633 native_descending
634 );
635 }
636}