abstract_std/objects/
truncated_chain_id.rs1use 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)]
14pub struct TruncatedChainId(String);
17
18impl TruncatedChainId {
19 pub fn new(env: &Env) -> Self {
21 let chain_id = &env.block.chain_id;
22 Self::from_chain_id(chain_id)
23 }
24
25 pub fn from_chain_id(chain_id: &str) -> Self {
27 let parts = chain_id.rsplitn(2, '-');
31 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 pub fn verify(&self) -> AbstractResult<()> {
43 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 } 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 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 pub(crate) fn _from_str(value: &str) -> Self {
79 Self(value.to_string())
80 }
81
82 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 #[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}