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#[cw_serde]
38pub enum CheckedDenom {
39 Native(String),
41 Cw20(Addr),
43}
44
45#[cw_serde]
48pub enum UncheckedDenom {
49 Native(String),
51 Cw20(String),
53}
54
55impl UncheckedDenom {
56 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 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 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 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 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
165pub 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 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
191impl 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 "ab".to_string(),
300 (0..129).map(|_| "a").collect::<String>(),
302 "1abc".to_string(),
304 "abc~d".to_string(),
306 "".to_string(),
308 "🥵abc".to_string(),
310 "ab:12🥵a".to_string(),
312 "ab,cd".to_string(),
314 ];
315
316 for invalid in invalids {
317 assert!(validate_native_denom(invalid).is_err())
318 }
319
320 assert_eq!(
322 validate_native_denom("".to_string()),
323 Err(DenomError::NativeDenomLength { len: 0 })
324 );
325 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 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}