cw_denom/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3#[cfg(test)]
4mod integration_tests;
5
6use std::fmt::{self};
7
8use cosmwasm_schema::cw_serde;
9use cosmwasm_std::{
10    to_json_binary, Addr, BankMsg, Coin, CosmosMsg, CustomQuery, Deps, QuerierWrapper, StdError,
11    StdResult, Uint128, WasmMsg,
12};
13
14use thiserror::Error;
15
16#[derive(Error, Debug, PartialEq)]
17pub enum DenomError {
18    #[error(transparent)]
19    Std(#[from] StdError),
20
21    #[error("invalid cw20 - did not respond to `TokenInfo` query: {err}")]
22    InvalidCw20 { err: StdError },
23
24    #[error("invalid native denom. length must be between in [3, 128], got ({len})")]
25    NativeDenomLength { len: usize },
26
27    #[error("expected alphabetic ascii character in native denomination")]
28    NonAlphabeticAscii,
29
30    #[error("invalid character ({c}) in native denom")]
31    InvalidCharacter { c: char },
32}
33
34/// A denom that has been checked to point to a valid asset. This enum
35/// should never be constructed literally and should always be built
36/// by calling `into_checked` on an `UncheckedDenom` instance.
37#[cw_serde]
38pub enum CheckedDenom {
39    /// A native (bank module) asset.
40    Native(String),
41    /// A cw20 asset.
42    Cw20(Addr),
43}
44
45/// A denom that has not been checked to confirm it points to a valid
46/// asset.
47#[cw_serde]
48pub enum UncheckedDenom {
49    /// A native (bank module) asset.
50    Native(String),
51    /// A cw20 asset.
52    Cw20(String),
53}
54
55impl UncheckedDenom {
56    /// Converts an unchecked denomination into a checked one. In the
57    /// case of native denominations, it is checked that the
58    /// denomination is valid according to the [default SDK rules]. In
59    /// the case of cw20 denominations the it is checked that the
60    /// specified address is valid and that that address responds to a
61    /// `TokenInfo` query without erroring and returns a valid
62    /// `cw20::TokenInfoResponse`.
63    ///
64    /// [default SDK rules]: https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867
65    pub fn into_checked(self, deps: Deps) -> Result<CheckedDenom, DenomError> {
66        match self {
67            Self::Native(denom) => validate_native_denom(denom),
68            Self::Cw20(addr) => {
69                let addr = deps.api.addr_validate(&addr)?;
70                let _info: cw20::TokenInfoResponse = deps
71                    .querier
72                    .query_wasm_smart(addr.clone(), &cw20::Cw20QueryMsg::TokenInfo {})
73                    .map_err(|err| DenomError::InvalidCw20 { err })?;
74                Ok(CheckedDenom::Cw20(addr))
75            }
76        }
77    }
78}
79
80impl CheckedDenom {
81    /// Is the `CheckedDenom` this cw20?
82    ///
83    /// # Example
84    ///
85    /// ```
86    /// use cosmwasm_std::{Addr, coin};
87    /// use cw_denom::CheckedDenom;
88    ///
89    /// let cw20 = Addr::unchecked("fleesp");
90    /// assert!(CheckedDenom::Cw20(Addr::unchecked("fleesp")).is_cw20(&cw20));
91    /// assert!(!CheckedDenom::Native("fleesp".to_string()).is_cw20(&cw20));
92    /// ```
93    pub fn is_cw20(&self, cw20: &Addr) -> bool {
94        match self {
95            CheckedDenom::Native(_) => false,
96            CheckedDenom::Cw20(a) => a == cw20,
97        }
98    }
99
100    /// Is the `CheckedDenom` this native denom?
101    ///
102    /// # Example
103    ///
104    /// ```
105    /// use cosmwasm_std::{Addr, coin};
106    /// use cw_denom::CheckedDenom;
107    ///
108    /// let coin = coin(10, "floob");
109    /// assert!(CheckedDenom::Native("floob".to_string()).is_native(&coin.denom));
110    /// assert!(!CheckedDenom::Cw20(Addr::unchecked("floob")).is_native(&coin.denom));
111    /// ```
112    pub fn is_native(&self, denom: &str) -> bool {
113        match self {
114            CheckedDenom::Native(n) => n == denom,
115            CheckedDenom::Cw20(_) => false,
116        }
117    }
118
119    /// Queries WHO's balance for the denomination.
120    pub fn query_balance<C: CustomQuery>(
121        &self,
122        querier: &QuerierWrapper<C>,
123        who: &Addr,
124    ) -> StdResult<Uint128> {
125        match self {
126            CheckedDenom::Native(denom) => Ok(querier.query_balance(who, denom)?.amount),
127            CheckedDenom::Cw20(address) => {
128                let balance: cw20::BalanceResponse = querier.query_wasm_smart(
129                    address,
130                    &cw20::Cw20QueryMsg::Balance {
131                        address: who.to_string(),
132                    },
133                )?;
134                Ok(balance.balance)
135            }
136        }
137    }
138
139    /// Gets a `CosmosMsg` that, when executed, will transfer AMOUNT
140    /// tokens to WHO. AMOUNT being zero will cause the message
141    /// execution to fail.
142    pub fn get_transfer_to_message(&self, who: &Addr, amount: Uint128) -> StdResult<CosmosMsg> {
143        Ok(match self {
144            CheckedDenom::Native(denom) => BankMsg::Send {
145                to_address: who.to_string(),
146                amount: vec![Coin {
147                    amount,
148                    denom: denom.to_string(),
149                }],
150            }
151            .into(),
152            CheckedDenom::Cw20(address) => WasmMsg::Execute {
153                contract_addr: address.to_string(),
154                msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer {
155                    recipient: who.to_string(),
156                    amount,
157                })?,
158                funds: vec![],
159            }
160            .into(),
161        })
162    }
163}
164
165/// Follows cosmos SDK validation logic. Specifically, the regex
166/// string `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}`.
167///
168/// <https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867>
169pub fn validate_native_denom(denom: String) -> Result<CheckedDenom, DenomError> {
170    if denom.len() < 3 || denom.len() > 128 {
171        return Err(DenomError::NativeDenomLength { len: denom.len() });
172    }
173    let mut chars = denom.chars();
174    // Really this means that a non utf-8 character is in here, but
175    // non-ascii is also correct.
176    let first = chars.next().ok_or(DenomError::NonAlphabeticAscii)?;
177    if !first.is_ascii_alphabetic() {
178        return Err(DenomError::NonAlphabeticAscii);
179    }
180
181    for c in chars {
182        if !(c.is_ascii_alphanumeric() || c == '/' || c == ':' || c == '.' || c == '_' || c == '-')
183        {
184            return Err(DenomError::InvalidCharacter { c });
185        }
186    }
187
188    Ok(CheckedDenom::Native(denom))
189}
190
191// Useful for returning these in response objects when updating the
192// config or doing a withdrawal.
193impl fmt::Display for CheckedDenom {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        match self {
196            Self::Native(inner) => write!(f, "{inner}"),
197            Self::Cw20(inner) => write!(f, "{inner}"),
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use cosmwasm_std::{
205        testing::{mock_dependencies, MockQuerier},
206        to_json_binary, Addr, ContractResult, QuerierResult, StdError, SystemError, Uint128,
207        WasmQuery,
208    };
209
210    use super::*;
211
212    const CW20_ADDR: &str = "cw20";
213
214    fn token_info_mock_querier(works: bool) -> impl Fn(&WasmQuery) -> QuerierResult {
215        move |query: &WasmQuery| -> QuerierResult {
216            match query {
217                WasmQuery::Smart { contract_addr, .. } => {
218                    if *contract_addr == CW20_ADDR {
219                        if works {
220                            QuerierResult::Ok(ContractResult::Ok(
221                                to_json_binary(&cw20::TokenInfoResponse {
222                                    name: "coin".to_string(),
223                                    symbol: "symbol".to_string(),
224                                    decimals: 6,
225                                    total_supply: Uint128::new(10),
226                                })
227                                .unwrap(),
228                            ))
229                        } else {
230                            QuerierResult::Err(SystemError::NoSuchContract {
231                                addr: CW20_ADDR.to_string(),
232                            })
233                        }
234                    } else {
235                        unimplemented!()
236                    }
237                }
238                _ => unimplemented!(),
239            }
240        }
241    }
242
243    #[test]
244    fn test_into_checked_cw20_valid() {
245        let mut querier = MockQuerier::default();
246        querier.update_wasm(token_info_mock_querier(true));
247
248        let mut deps = mock_dependencies();
249        deps.querier = querier;
250
251        let unchecked = UncheckedDenom::Cw20(CW20_ADDR.to_string());
252        let checked = unchecked.into_checked(deps.as_ref()).unwrap();
253
254        assert_eq!(checked, CheckedDenom::Cw20(Addr::unchecked(CW20_ADDR)))
255    }
256
257    #[test]
258    fn test_into_checked_cw20_invalid() {
259        let mut querier = MockQuerier::default();
260        querier.update_wasm(token_info_mock_querier(false));
261
262        let mut deps = mock_dependencies();
263        deps.querier = querier;
264
265        let unchecked = UncheckedDenom::Cw20(CW20_ADDR.to_string());
266        let err = unchecked.into_checked(deps.as_ref()).unwrap_err();
267        assert_eq!(
268            err,
269            DenomError::InvalidCw20 {
270                err: StdError::GenericErr {
271                    msg: format!("Querier system error: No such contract: {CW20_ADDR}",)
272                }
273            }
274        )
275    }
276
277    #[test]
278    fn test_into_checked_cw20_addr_invalid() {
279        let mut querier = MockQuerier::default();
280        querier.update_wasm(token_info_mock_querier(true));
281
282        let mut deps = mock_dependencies();
283        deps.querier = querier;
284
285        let unchecked = UncheckedDenom::Cw20("HasCapitalsSoShouldNotValidate".to_string());
286        let err = unchecked.into_checked(deps.as_ref()).unwrap_err();
287        assert_eq!(
288            err,
289            DenomError::Std(StdError::GenericErr {
290                msg: "Invalid input: address not normalized".to_string()
291            })
292        )
293    }
294
295    #[test]
296    fn test_validate_native_denom_invalid() {
297        let invalids = [
298            // Too short.
299            "ab".to_string(),
300            // Too long.
301            (0..129).map(|_| "a").collect::<String>(),
302            // Starts with non alphabetic character.
303            "1abc".to_string(),
304            // Contains invalid character.
305            "abc~d".to_string(),
306            // Too short, also empty.
307            "".to_string(),
308            // Weird unicode start.
309            "🥵abc".to_string(),
310            // Weird unocide in non-head position.
311            "ab:12🥵a".to_string(),
312            // Comma is not a valid seperator.
313            "ab,cd".to_string(),
314        ];
315
316        for invalid in invalids {
317            assert!(validate_native_denom(invalid).is_err())
318        }
319
320        // Check that we're getting the errors we expect.
321        assert_eq!(
322            validate_native_denom("".to_string()),
323            Err(DenomError::NativeDenomLength { len: 0 })
324        );
325        // Should check length before contents for better runtime.
326        assert_eq!(
327            validate_native_denom("1".to_string()),
328            Err(DenomError::NativeDenomLength { len: 1 })
329        );
330        assert_eq!(
331            validate_native_denom("🥵abc".to_string()),
332            Err(DenomError::NonAlphabeticAscii)
333        );
334        // The regex that the SDK specifies works on ASCII characters
335        // (not unicode classes), so this emoji has a "length" that is
336        // greater than one (counted in terms of ASCII characters). As
337        // such, we expect to fail on character validation and not
338        // length.
339        assert_eq!(
340            validate_native_denom("🥵".to_string()),
341            Err(DenomError::NonAlphabeticAscii)
342        );
343        assert_eq!(
344            validate_native_denom("a🥵abc".to_string()),
345            Err(DenomError::InvalidCharacter { c: '🥵' })
346        );
347    }
348
349    #[test]
350    fn test_validate_native_denom_valid() {
351        let valids = [
352            "ujuno",
353            "uosmo",
354            "IBC/A59A9C955F1AB8B76671B00C1A0482C64A6590352944BB5880E5122358F7E1CE",
355            "wasm.juno123/channel-1/badkids",
356        ];
357        for valid in valids {
358            validate_native_denom(valid.to_string()).unwrap();
359        }
360    }
361
362    #[test]
363    fn test_display() {
364        let denom = CheckedDenom::Native("hello".to_string());
365        assert_eq!(denom.to_string(), "hello".to_string());
366        let denom = CheckedDenom::Cw20(Addr::unchecked("hello"));
367        assert_eq!(denom.to_string(), "hello".to_string());
368    }
369}