cw_asset/
asset_info.rs

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/// Represents the type of an fungible asset.
15///
16/// Each **asset info** instance can be one of three variants:
17///
18/// - Native SDK coins. To create an **asset info** instance of this type,
19///   provide the denomination.
20/// - CW20 tokens. To create an **asset info** instance of this type, provide
21///   the contract address.
22#[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    /// Create an **asset info** instance of the _native_ variant by providing
32    /// the coin's denomination.
33    ///
34    /// ```rust
35    /// use cw_asset::AssetInfo;
36    ///
37    /// let info = AssetInfo::native("uusd");
38    /// ```
39    pub fn native<A: Into<String>>(denom: A) -> Self {
40        AssetInfoBase::Native(denom.into())
41    }
42
43    /// Create an **asset info** instance of the _CW20_ variant
44    ///
45    /// ```rust
46    /// use cosmwasm_std::Addr;
47    /// use cw_asset::AssetInfo;
48    ///
49    /// let info = AssetInfo::cw20(Addr::unchecked("token_addr"));
50    /// ```
51    pub fn cw20<A: Into<T>>(contract_addr: A) -> Self {
52        AssetInfoBase::Cw20(contract_addr.into())
53    }
54}
55
56/// Represents an **asset info** instance that may contain unverified data; to
57/// be used in messages.
58pub type AssetInfoUnchecked = AssetInfoBase<String>;
59
60/// Represents an **asset info** instance containing only verified data; to be
61/// saved in contract storage.
62pub type AssetInfo = AssetInfoBase<Addr>;
63
64impl AssetInfo {
65    /// Return the `denom` or `addr` wrapped within [AssetInfo]
66    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    /// Validate data contained in an _unchecked_ **asset info** instance;
126    /// return a new _checked_ **asset info** instance:
127    ///
128    /// - For CW20 tokens, assert the contract address is valid;
129    /// - For SDK coins, assert that the denom is included in a given whitelist;
130    ///   skip if the whitelist is not provided.
131    ///
132    ///
133    /// ```rust
134    /// use cosmwasm_std::{Addr, Api, StdResult};
135    /// use cw_asset::{AssetInfo, AssetInfoUnchecked};
136    ///
137    /// fn validate_asset_info(api: &dyn Api, info_unchecked: &AssetInfoUnchecked) {
138    ///     match info_unchecked.check(api, Some(&["uatom", "uluna"])) {
139    ///         Ok(info) => println!("asset info is valid: {}", info.to_string()),
140    ///         Err(err) => println!("asset is invalid! reason: {}", err),
141    ///     }
142    /// }
143    /// ```
144    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    /// Query an address' balance of the asset
179    ///
180    /// ```rust
181    /// use cosmwasm_std::{Addr, Deps, Uint128};
182    /// use cw_asset::{AssetError, AssetInfo};
183    ///
184    /// fn query_uusd_balance(deps: Deps, account_addr: &Addr) -> Result<Uint128, AssetError> {
185    ///     let info = AssetInfo::native("uusd");
186    ///     info.query_balance(&deps.querier, "account_addr")
187    /// }
188    /// ```
189    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    /// Implemented as private function to prevent from_str from being called on AssetInfo
217    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        // ignore length prefix
276        // we're allowed to do this because we set the key's namespace ourselves
277        // in PrimaryKey (first key)
278        value.drain(0..2);
279
280        // parse the bytes into an utf8 string
281        let s = String::from_utf8(value)?;
282
283        // cast the AssetError to StdError::ParseError
284        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//------------------------------------------------------------------------------
295// Tests
296//------------------------------------------------------------------------------
297
298#[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        // --- Ascending ---
559
560        // no bound
561        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        // bound on min
577        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                // ("native1".to_string(), 10), - out of bound
591                ("native2".to_string(), 20),
592                ("native3".to_string(), 30)
593            ],
594            native_ascending
595        );
596
597        // --- Descending ---
598
599        // no bound
600        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        // bound on max
616        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                // ("native3".to_string(), 30), - out of bound
630                ("native2".to_string(), 20),
631                ("native1".to_string(), 10)
632            ],
633            native_descending
634        );
635    }
636}