abstract_os/objects/
module.rs

1use super::module_reference::ModuleReference;
2use crate::{error::AbstractOsError, AbstractResult};
3use cosmwasm_std::{to_binary, Binary, StdError, StdResult};
4use cw2::ContractVersion;
5use cw_semver::Version;
6use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
7use std::fmt::{self, Display};
8
9/// ID of the module
10pub type ModuleId<'a> = &'a str;
11
12/// Stores the provider, name, and version of an Abstract module.
13#[cosmwasm_schema::cw_serde]
14pub struct ModuleInfo {
15    /// Provider of the module
16    pub provider: String,
17    /// Name of the contract
18    pub name: String,
19    /// Version of the module
20    pub version: ModuleVersion,
21}
22
23const MAX_LENGTH: usize = 64;
24
25/// Validate attributes of a [`ModuleInfo`].
26/// We use the same conventions as Rust package names.
27/// See https://github.com/rust-lang/api-guidelines/discussions/29
28fn validate_name(name: &str) -> AbstractResult<()> {
29    if name.is_empty() {
30        return Err(AbstractOsError::FormattingError {
31            object: "module name".into(),
32            expected: "with content".into(),
33            actual: "empty".to_string(),
34        });
35    }
36    if name.len() > MAX_LENGTH {
37        return Err(AbstractOsError::FormattingError {
38            object: "module name".into(),
39            expected: "at most 64 characters".into(),
40            actual: name.len().to_string(),
41        });
42    }
43    if name.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-') {
44        return Err(AbstractOsError::FormattingError {
45            object: "module name".into(),
46            expected: "alphanumeric characters and hyphens".into(),
47            actual: name.to_string(),
48        });
49    }
50
51    if name != name.to_lowercase() {
52        return Err(AbstractOsError::FormattingError {
53            object: "module name".into(),
54            expected: name.to_ascii_lowercase(),
55            actual: name.to_string(),
56        });
57    }
58    Ok(())
59}
60
61impl ModuleInfo {
62    pub fn from_id(id: &str, version: ModuleVersion) -> AbstractResult<Self> {
63        let split: Vec<&str> = id.split(':').collect();
64        if split.len() != 2 {
65            return Err(AbstractOsError::FormattingError {
66                object: "contract id".into(),
67                expected: "provider:contract_name".to_string(),
68                actual: id.to_string(),
69            });
70        }
71        Ok(ModuleInfo {
72            provider: split[0].to_lowercase(),
73            name: split[1].to_lowercase(),
74            version,
75        })
76    }
77    pub fn from_id_latest(id: &str) -> AbstractResult<Self> {
78        Self::from_id(id, ModuleVersion::Latest)
79    }
80
81    pub fn validate(&self) -> AbstractResult<()> {
82        validate_name(&self.provider)?;
83        validate_name(&self.name)?;
84        self.version.validate().map_err(|e| {
85            StdError::generic_err(format!("Invalid version for module {}: {}", self.id(), e))
86        })?;
87        Ok(())
88    }
89
90    pub fn id(&self) -> String {
91        format!("{}:{}", self.provider, self.name)
92    }
93
94    pub fn id_with_version(&self) -> String {
95        format!("{}:{}", self.id(), self.version)
96    }
97
98    pub fn assert_version_variant(&self) -> AbstractResult<()> {
99        match &self.version {
100            ModuleVersion::Latest => Err(AbstractOsError::Assert(
101                "Module version must be set to a specific version".into(),
102            )),
103            ModuleVersion::Version(ver) => {
104                // assert version parses correctly
105                semver::Version::parse(ver)?;
106                Ok(())
107            }
108        }
109    }
110}
111
112impl<'a> PrimaryKey<'a> for &ModuleInfo {
113    type Prefix = (String, String);
114
115    type SubPrefix = String;
116
117    /// Possibly change to ModuleVersion in future by implementing PrimaryKey
118    type Suffix = String;
119
120    type SuperSuffix = (String, String);
121
122    fn key(&self) -> Vec<cw_storage_plus::Key> {
123        let mut keys = self.provider.key();
124        keys.extend(self.name.key());
125        let temp = match &self.version {
126            ModuleVersion::Latest => "latest".key(),
127            ModuleVersion::Version(ver) => ver.key(),
128        };
129        keys.extend(temp);
130        keys
131    }
132}
133
134impl<'a> Prefixer<'a> for &ModuleInfo {
135    fn prefix(&self) -> Vec<Key> {
136        let mut res = self.provider.prefix();
137        res.extend(self.name.prefix().into_iter());
138        res.extend(self.version.prefix().into_iter());
139        res
140    }
141}
142
143impl<'a> Prefixer<'a> for ModuleVersion {
144    fn prefix(&self) -> Vec<Key> {
145        let self_as_bytes = match &self {
146            ModuleVersion::Latest => "latest".as_bytes(),
147            ModuleVersion::Version(ver) => ver.as_bytes(),
148        };
149        vec![Key::Ref(self_as_bytes)]
150    }
151}
152
153impl KeyDeserialize for &ModuleInfo {
154    type Output = ModuleInfo;
155
156    #[inline(always)]
157    fn from_vec(mut value: Vec<u8>) -> StdResult<Self::Output> {
158        let mut prov_name_ver = value.split_off(2);
159        let prov_len = parse_length(&value)?;
160        let mut len_name_ver = prov_name_ver.split_off(prov_len);
161
162        let mut name_ver = len_name_ver.split_off(2);
163        let ver_len = parse_length(&len_name_ver)?;
164        let ver = name_ver.split_off(ver_len);
165
166        Ok(ModuleInfo {
167            provider: String::from_vec(prov_name_ver)?,
168            name: String::from_vec(name_ver)?,
169            version: ModuleVersion::from_vec(ver)?,
170        })
171    }
172}
173
174impl KeyDeserialize for ModuleVersion {
175    type Output = ModuleVersion;
176
177    #[inline(always)]
178    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
179        let val = String::from_utf8(value).map_err(StdError::invalid_utf8)?;
180        if &val == "latest" {
181            Ok(Self::Latest)
182        } else {
183            Ok(Self::Version(val))
184        }
185    }
186}
187
188#[inline(always)]
189fn parse_length(value: &[u8]) -> StdResult<usize> {
190    Ok(u16::from_be_bytes(
191        value
192            .try_into()
193            .map_err(|_| StdError::generic_err("Could not read 2 byte length"))?,
194    )
195    .into())
196}
197
198#[cosmwasm_schema::cw_serde]
199pub enum ModuleVersion {
200    Latest,
201    Version(String),
202}
203
204impl ModuleVersion {
205    pub fn validate(&self) -> AbstractResult<()> {
206        match &self {
207            ModuleVersion::Latest => Ok(()),
208            ModuleVersion::Version(ver) => {
209                // assert version parses correctly
210                Version::parse(ver)?;
211                Ok(())
212            }
213        }
214    }
215}
216
217// Do not change!!
218impl Display for ModuleVersion {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        let print_str = match self {
221            ModuleVersion::Latest => "latest".to_string(),
222            ModuleVersion::Version(ver) => ver.to_owned(),
223        };
224        f.write_str(&print_str)
225    }
226}
227
228impl<T> From<T> for ModuleVersion
229where
230    T: Into<String>,
231{
232    fn from(ver: T) -> Self {
233        Self::Version(ver.into())
234    }
235}
236
237impl fmt::Display for ModuleInfo {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        write!(
240            f,
241            "{} provided by {} with version {}",
242            self.name, self.provider, self.version,
243        )
244    }
245}
246
247impl TryInto<Version> for ModuleVersion {
248    type Error = AbstractOsError;
249
250    fn try_into(self) -> AbstractResult<Version> {
251        match self {
252            ModuleVersion::Latest => Err(AbstractOsError::MissingVersion("module".to_string())),
253            ModuleVersion::Version(ver) => {
254                let version = Version::parse(&ver)?;
255                Ok(version)
256            }
257        }
258    }
259}
260
261impl TryFrom<ContractVersion> for ModuleInfo {
262    type Error = AbstractOsError;
263
264    fn try_from(value: ContractVersion) -> Result<Self, Self::Error> {
265        let split: Vec<&str> = value.contract.split(':').collect();
266        if split.len() != 2 {
267            return Err(AbstractOsError::FormattingError {
268                object: "contract id".to_string(),
269                expected: "provider:contract_name".into(),
270                actual: value.contract,
271            });
272        }
273        Ok(ModuleInfo {
274            provider: split[0].to_lowercase(),
275            name: split[1].to_lowercase(),
276            version: ModuleVersion::Version(value.version),
277        })
278    }
279}
280
281#[cosmwasm_schema::cw_serde]
282pub struct Module {
283    pub info: ModuleInfo,
284    pub reference: ModuleReference,
285}
286
287impl fmt::Display for Module {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        write!(f, "info: {}, reference: {:?}", self.info, self.reference)
290    }
291}
292
293impl From<(ModuleInfo, ModuleReference)> for Module {
294    fn from((info, reference): (ModuleInfo, ModuleReference)) -> Self {
295        Self { info, reference }
296    }
297}
298
299#[cosmwasm_schema::cw_serde]
300
301pub struct ModuleInitMsg {
302    pub fixed_init: Option<Binary>,
303    pub root_init: Option<Binary>,
304}
305
306impl ModuleInitMsg {
307    pub fn format(self) -> AbstractResult<Binary> {
308        match self {
309            // If both set, receiving contract must handle it using the ModuleInitMsg
310            ModuleInitMsg {
311                fixed_init: Some(_),
312                root_init: Some(_),
313            } => to_binary(&self),
314            // If not, we can simplify by only sending the custom or fixed message.
315            ModuleInitMsg {
316                fixed_init: None,
317                root_init: Some(r),
318            } => Ok(r),
319            ModuleInitMsg {
320                fixed_init: Some(f),
321                root_init: None,
322            } => Ok(f),
323            ModuleInitMsg {
324                fixed_init: None,
325                root_init: None,
326            } => Err(StdError::generic_err("No init msg set for this module")),
327        }
328        .map_err(Into::into)
329    }
330}
331
332//--------------------------------------------------------------------------------------------------
333// Tests
334//--------------------------------------------------------------------------------------------------
335
336#[cfg(test)]
337mod test {
338    use super::*;
339    use cosmwasm_std::{testing::mock_dependencies, Addr, Order};
340    use cw_storage_plus::Map;
341    use speculoos::prelude::*;
342
343    mod storage_plus {
344        use super::*;
345
346        fn mock_key() -> ModuleInfo {
347            ModuleInfo {
348                provider: "abstract".to_string(),
349                name: "rocket-ship".to_string(),
350                version: ModuleVersion::Version("1.9.9".into()),
351            }
352        }
353
354        fn mock_keys() -> (ModuleInfo, ModuleInfo, ModuleInfo, ModuleInfo) {
355            (
356                ModuleInfo {
357                    provider: "abstract".to_string(),
358                    name: "boat".to_string(),
359                    version: ModuleVersion::Version("1.9.9".into()),
360                },
361                ModuleInfo {
362                    provider: "abstract".to_string(),
363                    name: "rocket-ship".to_string(),
364                    version: ModuleVersion::Version("1.0.0".into()),
365                },
366                ModuleInfo {
367                    provider: "abstract".to_string(),
368                    name: "rocket-ship".to_string(),
369                    version: ModuleVersion::Version("2.0.0".into()),
370                },
371                ModuleInfo {
372                    provider: "astroport".to_string(),
373                    name: "liquidity-pool".to_string(),
374                    version: ModuleVersion::Version("10.5.7".into()),
375                },
376            )
377        }
378
379        #[test]
380        fn storage_key_works() {
381            let mut deps = mock_dependencies();
382            let key = mock_key();
383            let map: Map<&ModuleInfo, u64> = Map::new("map");
384
385            map.save(deps.as_mut().storage, &key, &42069).unwrap();
386
387            assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
388
389            let items = map
390                .range(deps.as_ref().storage, None, None, Order::Ascending)
391                .map(|item| item.unwrap())
392                .collect::<Vec<_>>();
393
394            assert_eq!(items.len(), 1);
395            assert_eq!(items[0], (key, 42069));
396        }
397
398        #[test]
399        fn storage_key_with_overlapping_name_provider() {
400            let mut deps = mock_dependencies();
401            let info1 = ModuleInfo {
402                provider: "abstract".to_string(),
403                name: "ans".to_string(),
404                version: ModuleVersion::Version("1.9.9".into()),
405            };
406
407            let _key1 = (&info1).joined_key();
408
409            let info2 = ModuleInfo {
410                provider: "abs".to_string(),
411                name: "tractans".to_string(),
412                version: ModuleVersion::Version("1.9.9".into()),
413            };
414
415            let _key2 = (&info2).joined_key();
416
417            let map: Map<&ModuleInfo, u64> = Map::new("map");
418
419            map.save(deps.as_mut().storage, &info1, &42069).unwrap();
420            map.save(deps.as_mut().storage, &info2, &69420).unwrap();
421
422            assert_that!(map
423                .keys_raw(&deps.storage, None, None, Order::Ascending)
424                .collect::<Vec<_>>())
425            .has_length(2);
426        }
427
428        #[test]
429        fn composite_key_works() {
430            let mut deps = mock_dependencies();
431            let key = mock_key();
432            let map: Map<(&ModuleInfo, Addr), u64> = Map::new("map");
433
434            map.save(
435                deps.as_mut().storage,
436                (&key, Addr::unchecked("larry")),
437                &42069,
438            )
439            .unwrap();
440
441            map.save(
442                deps.as_mut().storage,
443                (&key, Addr::unchecked("jake")),
444                &69420,
445            )
446            .unwrap();
447
448            let items = map
449                .prefix(&key)
450                .range(deps.as_ref().storage, None, None, Order::Ascending)
451                .map(|item| item.unwrap())
452                .collect::<Vec<_>>();
453
454            assert_eq!(items.len(), 2);
455            assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
456            assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
457        }
458
459        #[test]
460        fn partial_key_works() {
461            let mut deps = mock_dependencies();
462            let (key1, key2, key3, key4) = mock_keys();
463            let map: Map<&ModuleInfo, u64> = Map::new("map");
464
465            map.save(deps.as_mut().storage, &key1, &42069).unwrap();
466
467            map.save(deps.as_mut().storage, &key2, &69420).unwrap();
468
469            map.save(deps.as_mut().storage, &key3, &999).unwrap();
470
471            map.save(deps.as_mut().storage, &key4, &13).unwrap();
472
473            let items = map
474                .sub_prefix("abstract".to_string())
475                .range(deps.as_ref().storage, None, None, Order::Ascending)
476                .map(|item| item.unwrap())
477                .collect::<Vec<_>>();
478
479            assert_eq!(items.len(), 3);
480            assert_eq!(items[0], (("boat".to_string(), "1.9.9".to_string()), 42069));
481            assert_eq!(
482                items[1],
483                (("rocket-ship".to_string(), "1.0.0".to_string()), 69420)
484            );
485
486            assert_eq!(
487                items[2],
488                (("rocket-ship".to_string(), "2.0.0".to_string()), 999)
489            );
490
491            let items = map
492                .sub_prefix("astroport".to_string())
493                .range(deps.as_ref().storage, None, None, Order::Ascending)
494                .map(|item| item.unwrap())
495                .collect::<Vec<_>>();
496
497            assert_eq!(items.len(), 1);
498            assert_eq!(
499                items[0],
500                (("liquidity-pool".to_string(), "10.5.7".to_string()), 13)
501            );
502        }
503
504        #[test]
505        fn partial_key_versions_works() {
506            let mut deps = mock_dependencies();
507            let (key1, key2, key3, key4) = mock_keys();
508            let map: Map<&ModuleInfo, u64> = Map::new("map");
509
510            map.save(deps.as_mut().storage, &key1, &42069).unwrap();
511
512            map.save(deps.as_mut().storage, &key2, &69420).unwrap();
513
514            map.save(deps.as_mut().storage, &key3, &999).unwrap();
515
516            map.save(deps.as_mut().storage, &key4, &13).unwrap();
517
518            let items = map
519                .prefix(("abstract".to_string(), "rocket-ship".to_string()))
520                .range(deps.as_ref().storage, None, None, Order::Ascending)
521                .map(|item| item.unwrap())
522                .collect::<Vec<_>>();
523
524            assert_eq!(items.len(), 2);
525            assert_eq!(items[0], ("1.0.0".to_string(), 69420));
526
527            assert_eq!(items[1], ("2.0.0".to_string(), 999));
528        }
529    }
530
531    mod module_info {
532        use super::*;
533
534        #[test]
535        fn validate_with_empty_name() {
536            let info = ModuleInfo {
537                provider: "abstract".to_string(),
538                name: "".to_string(),
539                version: ModuleVersion::Version("1.9.9".into()),
540            };
541
542            assert_that!(info.validate())
543                .is_err()
544                .matches(|e| e.to_string().contains("empty"));
545        }
546
547        #[test]
548        fn validate_with_empty_provider() {
549            let info = ModuleInfo {
550                provider: "".to_string(),
551                name: "ans".to_string(),
552                version: ModuleVersion::Version("1.9.9".into()),
553            };
554
555            assert_that!(info.validate())
556                .is_err()
557                .matches(|e| e.to_string().contains("empty"));
558        }
559
560        use rstest::rstest;
561
562        #[rstest]
563        #[case("ans_host")]
564        #[case("ans:host")]
565        #[case("ans-host&")]
566        fn validate_fails_with_non_alphanumeric(#[case] name: &str) {
567            let info = ModuleInfo {
568                provider: "abstract".to_string(),
569                name: name.to_string(),
570                version: ModuleVersion::Version("1.9.9".into()),
571            };
572
573            assert_that!(info.validate())
574                .is_err()
575                .matches(|e| e.to_string().contains("alphanumeric"));
576        }
577
578        #[rstest]
579        #[case("lmao")]
580        #[case("bad-")]
581        fn validate_with_bad_versions(#[case] version: &str) {
582            let info = ModuleInfo {
583                provider: "abstract".to_string(),
584                name: "ans".to_string(),
585                version: ModuleVersion::Version(version.into()),
586            };
587
588            assert_that!(info.validate())
589                .is_err()
590                .matches(|e| e.to_string().contains("Invalid version"));
591        }
592
593        #[test]
594        fn id() {
595            let info = ModuleInfo {
596                name: "name".to_string(),
597                provider: "provider".to_string(),
598                version: ModuleVersion::Version("1.0.0".into()),
599            };
600
601            let expected = "provider:name".to_string();
602
603            assert_that!(info.id()).is_equal_to(expected);
604        }
605
606        #[test]
607        fn id_with_version() {
608            let info = ModuleInfo {
609                name: "name".to_string(),
610                provider: "provider".to_string(),
611                version: ModuleVersion::Version("1.0.0".into()),
612            };
613
614            let expected = "provider:name:1.0.0".to_string();
615
616            assert_that!(info.id_with_version()).is_equal_to(expected);
617        }
618    }
619
620    mod module_version {
621        use super::*;
622
623        #[test]
624        fn try_into_version_happy_path() {
625            let version = ModuleVersion::Version("1.0.0".into());
626
627            let expected: Version = "1.0.0".to_string().parse().unwrap();
628
629            let actual: Version = version.try_into().unwrap();
630
631            assert_that!(actual).is_equal_to(expected);
632        }
633
634        #[test]
635        fn try_into_version_with_latest() {
636            let version = ModuleVersion::Latest;
637
638            let actual: Result<Version, _> = version.try_into();
639
640            assert_that!(actual).is_err();
641        }
642    }
643}