abstract_std/objects/
truncated_chain_id.rs

1use std::str::FromStr;
2
3use cosmwasm_schema::cw_serde;
4use cosmwasm_std::{Env, StdResult};
5use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
6
7use crate::{AbstractError, AbstractResult};
8
9pub const MAX_CHAIN_NAME_LENGTH: usize = 20;
10pub const MIN_CHAIN_NAME_LENGTH: usize = 3;
11
12#[cw_serde]
13#[derive(Eq, PartialOrd, Ord)]
14/// The name of a chain, aka the chain-id without the post-fix number.
15/// ex. `cosmoshub-4` -> `cosmoshub`, `juno-1` -> `juno`
16pub struct TruncatedChainId(String);
17
18impl TruncatedChainId {
19    // Construct the chain name from the environment (chain-id)
20    pub fn new(env: &Env) -> Self {
21        let chain_id = &env.block.chain_id;
22        Self::from_chain_id(chain_id)
23    }
24
25    // Construct the chain name from the chain id
26    pub fn from_chain_id(chain_id: &str) -> Self {
27        // split on the last -
28        // `cosmos-testnet-53159`
29        // -> `cosmos-testnet` and `53159`
30        let parts = chain_id.rsplitn(2, '-');
31        // the parts should look like [53159, cosmos-tesnet] or [cosmos-testnet], because we are using rsplitn
32        Self(parts.last().unwrap().to_string())
33    }
34
35    pub fn from_string(value: String) -> AbstractResult<Self> {
36        let chain_name = Self(value);
37        chain_name.verify()?;
38        Ok(chain_name)
39    }
40
41    /// verify the formatting of the chain name
42    pub fn verify(&self) -> AbstractResult<()> {
43        // check length
44        if self.0.is_empty()
45            || self.0.len() < MIN_CHAIN_NAME_LENGTH
46            || self.0.len() > MAX_CHAIN_NAME_LENGTH
47        {
48            return Err(AbstractError::FormattingError {
49                object: "chain-seq".into(),
50                expected: format!("between {MIN_CHAIN_NAME_LENGTH} and {MAX_CHAIN_NAME_LENGTH}"),
51                actual: self.0.len().to_string(),
52            });
53        // check character set
54        } else if !self.0.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
55            return Err(crate::AbstractError::FormattingError {
56                object: "chain_name".into(),
57                expected: "chain-name".into(),
58                actual: self.0.clone(),
59            });
60        }
61        Ok(())
62    }
63
64    pub fn as_str(&self) -> &str {
65        self.0.as_str()
66    }
67
68    // used for key implementation
69    pub(crate) fn str_ref(&self) -> &String {
70        &self.0
71    }
72
73    pub fn into_string(self) -> String {
74        self.0
75    }
76
77    /// Only use this if you are sure that the string is valid (e.g. from storage)
78    pub(crate) fn _from_str(value: &str) -> Self {
79        Self(value.to_string())
80    }
81
82    /// Only use this if you are sure that the string is valid (e.g. from storage)
83    pub(crate) fn _from_string(value: String) -> Self {
84        Self(value)
85    }
86}
87
88impl std::fmt::Display for TruncatedChainId {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        write!(f, "{}", self.0)
91    }
92}
93
94impl FromStr for TruncatedChainId {
95    type Err = AbstractError;
96    fn from_str(value: &str) -> AbstractResult<Self> {
97        let chain_name = Self(value.to_string());
98        chain_name.verify()?;
99        Ok(chain_name)
100    }
101}
102
103impl PrimaryKey<'_> for &TruncatedChainId {
104    type Prefix = ();
105
106    type SubPrefix = ();
107
108    type Suffix = Self;
109
110    type SuperSuffix = Self;
111
112    fn key(&self) -> Vec<cw_storage_plus::Key> {
113        self.0.key()
114    }
115}
116
117impl Prefixer<'_> for &TruncatedChainId {
118    fn prefix(&self) -> Vec<Key> {
119        self.0.prefix()
120    }
121}
122
123impl KeyDeserialize for &TruncatedChainId {
124    type Output = TruncatedChainId;
125    const KEY_ELEMS: u16 = 1;
126
127    #[inline(always)]
128    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
129        Ok(TruncatedChainId(String::from_vec(value)?))
130    }
131}
132
133#[cfg(test)]
134mod test {
135    #![allow(clippy::needless_borrows_for_generic_args)]
136    use cosmwasm_std::testing::mock_env;
137
138    use super::*;
139
140    #[coverage_helper::test]
141    fn test_namespace() {
142        let namespace = TruncatedChainId::new(&mock_env());
143        assert_eq!(namespace.as_str(), "cosmos-testnet");
144    }
145
146    #[coverage_helper::test]
147    fn test_from_string() {
148        let namespace = TruncatedChainId::from_string("test-me".to_string()).unwrap();
149        assert_eq!(namespace.as_str(), "test-me");
150    }
151
152    #[coverage_helper::test]
153    fn test_from_str() {
154        let namespace = TruncatedChainId::from_str("test-too").unwrap();
155        assert_eq!(namespace.as_str(), "test-too");
156    }
157
158    #[coverage_helper::test]
159    fn test_to_string() {
160        let namespace = TruncatedChainId::from_str("test").unwrap();
161        assert_eq!(namespace.to_string(), "test".to_string());
162    }
163
164    #[coverage_helper::test]
165    fn test_from_str_long() {
166        let namespace = TruncatedChainId::from_str("test-a-b-c-d-e-f").unwrap();
167        assert_eq!(namespace.as_str(), "test-a-b-c-d-e-f");
168    }
169
170    #[coverage_helper::test]
171    fn string_key_works() {
172        let k = &TruncatedChainId::from_str("test-abc").unwrap();
173        let path = k.key();
174        assert_eq!(1, path.len());
175        assert_eq!(b"test-abc", path[0].as_ref());
176
177        let joined = k.joined_key();
178        assert_eq!(joined, b"test-abc")
179    }
180
181    // Failures
182
183    #[coverage_helper::test]
184    fn local_empty_fails() {
185        TruncatedChainId::from_str("").unwrap_err();
186    }
187
188    #[coverage_helper::test]
189    fn local_too_short_fails() {
190        TruncatedChainId::from_str("a").unwrap_err();
191    }
192
193    #[coverage_helper::test]
194    fn local_too_long_fails() {
195        TruncatedChainId::from_str(&"a".repeat(MAX_CHAIN_NAME_LENGTH + 1)).unwrap_err();
196    }
197
198    #[coverage_helper::test]
199    fn local_uppercase_fails() {
200        TruncatedChainId::from_str("AAAAA").unwrap_err();
201    }
202
203    #[coverage_helper::test]
204    fn local_non_alphanumeric_fails() {
205        TruncatedChainId::from_str("a_aoeuoau").unwrap_err();
206    }
207
208    #[coverage_helper::test]
209    fn from_chain_id() {
210        let normal_chain_name = TruncatedChainId::from_chain_id("juno-1");
211        assert_eq!(normal_chain_name, TruncatedChainId::_from_str("juno"));
212
213        let postfixless_chain_name = TruncatedChainId::from_chain_id("juno");
214        assert_eq!(postfixless_chain_name, TruncatedChainId::_from_str("juno"));
215    }
216}