apollo_utils/
coins.rs

1use cosmwasm_std::{Coin, StdError, StdResult, Uint128};
2use regex::Regex;
3
4/// Parse coins from string in format {amount}{denom}
5pub fn coin_from_str(s: &str) -> Coin {
6    // Find index of first non-digit character
7    let idx = s
8        .char_indices()
9        .find(|(_, c)| !c.is_digit(10))
10        .map(|(idx, _)| idx)
11        .unwrap_or(s.len());
12
13    // Parse amount and denom from string
14    let amount: Uint128 = s[..idx].parse::<u128>().unwrap().into();
15    let denom = s[idx..].to_string();
16
17    Coin { denom, amount }
18}
19
20/// Validate string as a valid CosmosSDK denom according to regex
21/// `r"^[a-zA-Z][a-zA-Z0-9/:._-]{2,127}$"`. See https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867
22pub fn validate_denom(input: &str) -> StdResult<()> {
23    let re = Regex::new(r"^[a-zA-Z][a-zA-Z0-9/:._-]{2,127}$").unwrap();
24
25    if re.is_match(input) {
26        Ok(())
27    } else {
28        Err(StdError::generic_err(
29            "Provided string is not a valid CosmosSDK denom.",
30        ))
31    }
32}
33
34#[cfg(test)]
35mod tests {
36    use test_case::test_case;
37
38    use super::*;
39
40    #[test]
41    fn test_coin_from_sdk_str() {
42        let coin = coin_from_str("100000000000000000000gamm/pool/1");
43
44        assert_eq!(coin.amount, Uint128::from(100000000000000000000u128));
45        assert_eq!(coin.denom, "gamm/pool/1");
46    }
47
48    #[test_case("gamm/pool/1" => Ok(()); "valid osmosis LP denom")]
49    #[test_case("uatom" => Ok(()); "valid uatom denom")]
50    #[test_case("ibc/C140AFD542AE77BD7DCC83F13FDD8C5E5BB8C4929785E6EC2F4C636F98F17901" => Ok(()); "valid IBC denom")]
51    #[test_case("IBC/C140AFD542AE77BD7DCC83F13FDD8C5E5BB8C4929785E6EC2F4C636F98F17901" => Ok(()); "valid IBC denom capital IBC")]
52    #[test_case("factory/osmo1g3kmqpp8608szfp0pdag3r6z85npph7wmccat8lgl3mp407kv73qlj7qwp/VaultToken/1/14d/ATOM/OSMO" => Ok(()); "valid token factory denom")]
53    #[test_case("test:test/test-test.test_test" => Ok(()); "all valid separators")]
54    #[test_case("test test" => Err(StdError::generic_err("Provided string is not a valid CosmosSDK denom.")); "invalid separator space")]
55    #[test_case("test/test " => Err(StdError::generic_err("Provided string is not a valid CosmosSDK denom.")); "trailing space")]
56    #[test_case(" test/test" => Err(StdError::generic_err("Provided string is not a valid CosmosSDK denom.")); "leading space")]
57    #[test_case("/test/test" => Err(StdError::generic_err("Provided string is not a valid CosmosSDK denom.")); "leading separator")]
58    #[test_case("2test/test" => Err(StdError::generic_err("Provided string is not a valid CosmosSDK denom.")); "leading number")]
59    #[test_case("te" => Err(StdError::generic_err("Provided string is not a valid CosmosSDK denom.")); "too short")]
60    #[test_case("tes" => Ok(()); "min length")]
61    #[test_case("t//" => Ok(()); "min length with two consecutive separators")]
62    #[test_case("testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest" => Ok(()); "max length")]
63    #[test_case("testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttestt" => Err(StdError::generic_err("Provided string is not a valid CosmosSDK denom.")); "too long")]
64    fn test_validate_denom(input: &str) -> StdResult<()> {
65        validate_denom(input)
66    }
67}