#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
#[cfg(test)]
mod integration_tests;
use std::fmt::{self};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
to_binary, Addr, BankMsg, Coin, CosmosMsg, CustomQuery, Deps, QuerierWrapper, StdError,
StdResult, Uint128, WasmMsg,
};
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum DenomError {
#[error(transparent)]
Std(#[from] StdError),
#[error("invalid cw20 - did not respond to `TokenInfo` query: {err}")]
InvalidCw20 { err: StdError },
#[error("invalid native denom. length must be between in [3, 128], got ({len})")]
NativeDenomLength { len: usize },
#[error("expected alphabetic ascii character in native denomination")]
NonAlphabeticAscii,
#[error("invalid character ({c}) in native denom")]
InvalidCharacter { c: char },
}
#[cw_serde]
pub enum CheckedDenom {
Native(String),
Cw20(Addr),
}
#[cw_serde]
pub enum UncheckedDenom {
Native(String),
Cw20(String),
}
impl UncheckedDenom {
pub fn into_checked(self, deps: Deps) -> Result<CheckedDenom, DenomError> {
match self {
Self::Native(denom) => validate_native_denom(denom),
Self::Cw20(addr) => {
let addr = deps.api.addr_validate(&addr)?;
let _info: cw20::TokenInfoResponse = deps
.querier
.query_wasm_smart(addr.clone(), &cw20::Cw20QueryMsg::TokenInfo {})
.map_err(|err| DenomError::InvalidCw20 { err })?;
Ok(CheckedDenom::Cw20(addr))
}
}
}
}
impl CheckedDenom {
pub fn is_cw20(&self, cw20: &Addr) -> bool {
match self {
CheckedDenom::Native(_) => false,
CheckedDenom::Cw20(a) => a == cw20,
}
}
pub fn is_native(&self, denom: &str) -> bool {
match self {
CheckedDenom::Native(n) => n == denom,
CheckedDenom::Cw20(_) => false,
}
}
pub fn query_balance<C: CustomQuery>(
&self,
querier: &QuerierWrapper<C>,
who: &Addr,
) -> StdResult<Uint128> {
match self {
CheckedDenom::Native(denom) => Ok(querier.query_balance(who, denom)?.amount),
CheckedDenom::Cw20(address) => {
let balance: cw20::BalanceResponse = querier.query_wasm_smart(
address,
&cw20::Cw20QueryMsg::Balance {
address: who.to_string(),
},
)?;
Ok(balance.balance)
}
}
}
pub fn get_transfer_to_message(&self, who: &Addr, amount: Uint128) -> StdResult<CosmosMsg> {
Ok(match self {
CheckedDenom::Native(denom) => BankMsg::Send {
to_address: who.to_string(),
amount: vec![Coin {
amount,
denom: denom.to_string(),
}],
}
.into(),
CheckedDenom::Cw20(address) => WasmMsg::Execute {
contract_addr: address.to_string(),
msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
recipient: who.to_string(),
amount,
})?,
funds: vec![],
}
.into(),
})
}
}
pub fn validate_native_denom(denom: String) -> Result<CheckedDenom, DenomError> {
if denom.len() < 3 || denom.len() > 128 {
return Err(DenomError::NativeDenomLength { len: denom.len() });
}
let mut chars = denom.chars();
let first = chars.next().ok_or(DenomError::NonAlphabeticAscii)?;
if !first.is_ascii_alphabetic() {
return Err(DenomError::NonAlphabeticAscii);
}
for c in chars {
if !(c.is_ascii_alphanumeric() || c == '/' || c == ':' || c == '.' || c == '_' || c == '-')
{
return Err(DenomError::InvalidCharacter { c });
}
}
Ok(CheckedDenom::Native(denom))
}
impl fmt::Display for CheckedDenom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Native(inner) => write!(f, "{inner}"),
Self::Cw20(inner) => write!(f, "{inner}"),
}
}
}
#[cfg(test)]
mod tests {
use cosmwasm_std::{
testing::{mock_dependencies, MockQuerier},
to_binary, Addr, ContractResult, QuerierResult, StdError, SystemError, Uint128, WasmQuery,
};
use super::*;
const CW20_ADDR: &str = "cw20";
fn token_info_mock_querier(works: bool) -> impl Fn(&WasmQuery) -> QuerierResult {
move |query: &WasmQuery| -> QuerierResult {
match query {
WasmQuery::Smart { contract_addr, .. } => {
if *contract_addr == CW20_ADDR {
if works {
QuerierResult::Ok(ContractResult::Ok(
to_binary(&cw20::TokenInfoResponse {
name: "coin".to_string(),
symbol: "symbol".to_string(),
decimals: 6,
total_supply: Uint128::new(10),
})
.unwrap(),
))
} else {
QuerierResult::Err(SystemError::NoSuchContract {
addr: CW20_ADDR.to_string(),
})
}
} else {
unimplemented!()
}
}
_ => unimplemented!(),
}
}
}
#[test]
fn test_into_checked_cw20_valid() {
let mut querier = MockQuerier::default();
querier.update_wasm(token_info_mock_querier(true));
let mut deps = mock_dependencies();
deps.querier = querier;
let unchecked = UncheckedDenom::Cw20(CW20_ADDR.to_string());
let checked = unchecked.into_checked(deps.as_ref()).unwrap();
assert_eq!(checked, CheckedDenom::Cw20(Addr::unchecked(CW20_ADDR)))
}
#[test]
fn test_into_checked_cw20_invalid() {
let mut querier = MockQuerier::default();
querier.update_wasm(token_info_mock_querier(false));
let mut deps = mock_dependencies();
deps.querier = querier;
let unchecked = UncheckedDenom::Cw20(CW20_ADDR.to_string());
let err = unchecked.into_checked(deps.as_ref()).unwrap_err();
assert_eq!(
err,
DenomError::InvalidCw20 {
err: StdError::GenericErr {
msg: format!("Querier system error: No such contract: {CW20_ADDR}",)
}
}
)
}
#[test]
fn test_into_checked_cw20_addr_invalid() {
let mut querier = MockQuerier::default();
querier.update_wasm(token_info_mock_querier(true));
let mut deps = mock_dependencies();
deps.querier = querier;
let unchecked = UncheckedDenom::Cw20("HasCapitalsSoShouldNotValidate".to_string());
let err = unchecked.into_checked(deps.as_ref()).unwrap_err();
assert_eq!(
err,
DenomError::Std(StdError::GenericErr {
msg: "Invalid input: address not normalized".to_string()
})
)
}
#[test]
fn test_validate_native_denom_invalid() {
let invalids = [
"ab".to_string(),
(0..129).map(|_| "a").collect::<String>(),
"1abc".to_string(),
"abc~d".to_string(),
"".to_string(),
"🥵abc".to_string(),
"ab:12🥵a".to_string(),
"ab,cd".to_string(),
];
for invalid in invalids {
assert!(validate_native_denom(invalid).is_err())
}
assert_eq!(
validate_native_denom("".to_string()),
Err(DenomError::NativeDenomLength { len: 0 })
);
assert_eq!(
validate_native_denom("1".to_string()),
Err(DenomError::NativeDenomLength { len: 1 })
);
assert_eq!(
validate_native_denom("🥵abc".to_string()),
Err(DenomError::NonAlphabeticAscii)
);
assert_eq!(
validate_native_denom("🥵".to_string()),
Err(DenomError::NonAlphabeticAscii)
);
assert_eq!(
validate_native_denom("a🥵abc".to_string()),
Err(DenomError::InvalidCharacter { c: '🥵' })
);
}
#[test]
fn test_validate_native_denom_valid() {
let valids = [
"ujuno",
"uosmo",
"IBC/A59A9C955F1AB8B76671B00C1A0482C64A6590352944BB5880E5122358F7E1CE",
"wasm.juno123/channel-1/badkids",
];
for valid in valids {
validate_native_denom(valid.to_string()).unwrap();
}
}
#[test]
fn test_display() {
let denom = CheckedDenom::Native("hello".to_string());
assert_eq!(denom.to_string(), "hello".to_string());
let denom = CheckedDenom::Cw20(Addr::unchecked("hello"));
assert_eq!(denom.to_string(), "hello".to_string());
}
}