abstract_std/objects/
module.rs

1use std::{
2    fmt::{self, Display},
3    str::FromStr,
4};
5
6use cosmwasm_std::{ensure_eq, to_json_binary, Addr, Binary, QuerierWrapper, StdError, StdResult};
7use cw2::ContractVersion;
8use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
9use semver::Version;
10
11use super::module_reference::ModuleReference;
12use crate::{
13    error::AbstractError,
14    objects::{fee::FixedFee, module_version::MODULE, namespace::Namespace},
15    AbstractResult, IBC_CLIENT,
16};
17
18/// ID of the module
19pub type ModuleId<'a> = &'a str;
20
21/// Module status
22#[cosmwasm_schema::cw_serde]
23pub enum ModuleStatus {
24    /// Modules in use
25    Registered,
26    /// Pending modules
27    Pending,
28    /// Yanked modules
29    Yanked,
30}
31
32/// Stores the namespace, name, and version of an Abstract module.
33#[cosmwasm_schema::cw_serde]
34pub struct ModuleInfo {
35    /// Namespace of the module
36    pub namespace: Namespace,
37    /// Name of the contract
38    pub name: String,
39    /// Version of the module
40    pub version: ModuleVersion,
41}
42
43impl TryFrom<ModuleInfo> for ContractVersion {
44    type Error = AbstractError;
45
46    fn try_from(value: ModuleInfo) -> Result<Self, Self::Error> {
47        let ModuleVersion::Version(version) = value.version else {
48            return Err(AbstractError::MissingVersion("module".to_owned()));
49        };
50        Ok(ContractVersion {
51            contract: format!("{}:{}", value.namespace, value.name),
52            version,
53        })
54    }
55}
56
57const MAX_LENGTH: usize = 64;
58
59/// Validate attributes of a [`ModuleInfo`].
60/// We use the same conventions as Rust package names.
61/// See <https://github.com/rust-lang/api-guidelines/discussions/29>
62pub fn validate_name(name: &str) -> AbstractResult<()> {
63    if name.is_empty() {
64        return Err(AbstractError::FormattingError {
65            object: "module name".into(),
66            expected: "with content".into(),
67            actual: "empty".to_string(),
68        });
69    }
70    if name.len() > MAX_LENGTH {
71        return Err(AbstractError::FormattingError {
72            object: "module name".into(),
73            expected: "at most 64 characters".into(),
74            actual: name.len().to_string(),
75        });
76    }
77    if name.contains(|c: char| !c.is_ascii_alphanumeric() && c != '-') {
78        return Err(AbstractError::FormattingError {
79            object: "module name".into(),
80            expected: "alphanumeric characters and hyphens".into(),
81            actual: name.to_string(),
82        });
83    }
84
85    if name != name.to_lowercase() {
86        return Err(AbstractError::FormattingError {
87            object: "module name".into(),
88            expected: name.to_ascii_lowercase(),
89            actual: name.to_string(),
90        });
91    }
92    Ok(())
93}
94
95impl ModuleInfo {
96    pub fn from_id(id: &str, version: ModuleVersion) -> AbstractResult<Self> {
97        let split: Vec<&str> = id.split(':').collect();
98        if split.len() != 2 {
99            return Err(AbstractError::FormattingError {
100                object: "contract id".into(),
101                expected: "namespace:contract_name".to_string(),
102                actual: id.to_string(),
103            });
104        }
105        Ok(ModuleInfo {
106            namespace: Namespace::try_from(split[0])?,
107            name: split[1].to_lowercase(),
108            version,
109        })
110    }
111    pub fn from_id_latest(id: &str) -> AbstractResult<Self> {
112        Self::from_id(id, ModuleVersion::Latest)
113    }
114
115    pub fn validate(&self) -> AbstractResult<()> {
116        self.namespace.validate()?;
117        validate_name(&self.name)?;
118        self.version.validate().map_err(|e| {
119            StdError::generic_err(format!("Invalid version for module {}: {}", self.id(), e))
120        })?;
121        Ok(())
122    }
123
124    pub fn id(&self) -> String {
125        format!("{}:{}", self.namespace, self.name)
126    }
127
128    pub fn id_with_version(&self) -> String {
129        format!("{}:{}", self.id(), self.version)
130    }
131
132    pub fn assert_version_variant(&self) -> AbstractResult<()> {
133        match &self.version {
134            ModuleVersion::Latest => Err(AbstractError::Assert(
135                "Module version must be set to a specific version".into(),
136            )),
137            ModuleVersion::Version(ver) => {
138                // assert version parses correctly
139                semver::Version::parse(ver)?;
140                Ok(())
141            }
142        }
143    }
144}
145
146impl PrimaryKey<'_> for &ModuleInfo {
147    /// (namespace, name)
148    type Prefix = (Namespace, String);
149
150    /// namespace
151    type SubPrefix = Namespace;
152
153    /// version
154    type Suffix = ModuleVersion;
155
156    // (name, version)
157    type SuperSuffix = (String, ModuleVersion);
158
159    fn key(&self) -> Vec<cw_storage_plus::Key> {
160        let mut keys = self.namespace.key();
161        keys.extend(self.name.key());
162        keys.extend(self.version.key());
163        keys
164    }
165}
166
167impl Prefixer<'_> for &ModuleInfo {
168    fn prefix(&self) -> Vec<Key> {
169        let mut res = self.namespace.prefix();
170        res.extend(self.name.prefix());
171        res.extend(self.version.prefix());
172        res
173    }
174}
175
176impl KeyDeserialize for &ModuleInfo {
177    type Output = ModuleInfo;
178    const KEY_ELEMS: u16 = Namespace::KEY_ELEMS + String::KEY_ELEMS + ModuleVersion::KEY_ELEMS;
179
180    #[inline(always)]
181    fn from_vec(mut value: Vec<u8>) -> StdResult<Self::Output> {
182        let mut prov_name_ver = value.split_off(2);
183        let prov_len = parse_length(&value)?;
184        let mut len_name_ver = prov_name_ver.split_off(prov_len);
185
186        let mut name_ver = len_name_ver.split_off(2);
187        let ver_len = parse_length(&len_name_ver)?;
188        let ver = name_ver.split_off(ver_len);
189
190        Ok(ModuleInfo {
191            namespace: Namespace::try_from(String::from_vec(prov_name_ver)?).map_err(|e| {
192                StdError::generic_err(format!("Invalid namespace for module: {}", e))
193            })?,
194            name: String::from_vec(name_ver)?,
195            version: ModuleVersion::from_vec(ver)?,
196        })
197    }
198}
199
200impl KeyDeserialize for ModuleVersion {
201    type Output = ModuleVersion;
202    const KEY_ELEMS: u16 = 1;
203
204    #[inline(always)]
205    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
206        let val = String::from_vec(value)?;
207        if &val == "latest" {
208            Ok(Self::Latest)
209        } else {
210            Ok(Self::Version(val))
211        }
212    }
213}
214
215#[inline(always)]
216fn parse_length(value: &[u8]) -> StdResult<usize> {
217    Ok(u16::from_be_bytes(
218        value
219            .try_into()
220            .map_err(|_| StdError::generic_err("Could not read 2 byte length"))?,
221    )
222    .into())
223}
224
225#[cosmwasm_schema::cw_serde]
226pub enum ModuleVersion {
227    Latest,
228    Version(String),
229}
230
231impl ModuleVersion {
232    pub fn validate(&self) -> AbstractResult<()> {
233        match &self {
234            ModuleVersion::Latest => Ok(()),
235            ModuleVersion::Version(ver) => {
236                // assert version parses correctly
237                Version::parse(ver)?;
238                Ok(())
239            }
240        }
241    }
242}
243
244impl FromStr for ModuleVersion {
245    type Err = AbstractError;
246
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        match s {
249            "latest" => Ok(Self::Latest),
250            _ => {
251                let v = Self::Version(s.to_owned());
252                v.validate()?;
253                Ok(v)
254            }
255        }
256    }
257}
258
259// Do not change!!
260impl Display for ModuleVersion {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        let print_str = match self {
263            ModuleVersion::Latest => "latest".to_string(),
264            ModuleVersion::Version(ver) => ver.to_owned(),
265        };
266        f.write_str(&print_str)
267    }
268}
269
270impl<T> From<T> for ModuleVersion
271where
272    T: Into<String>,
273{
274    fn from(ver: T) -> Self {
275        Self::Version(ver.into())
276    }
277}
278
279impl fmt::Display for ModuleInfo {
280    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
281        write!(
282            f,
283            "{} provided by {} with version {}",
284            self.name, self.namespace, self.version,
285        )
286    }
287}
288
289impl TryInto<Version> for ModuleVersion {
290    type Error = AbstractError;
291
292    fn try_into(self) -> AbstractResult<Version> {
293        match self {
294            ModuleVersion::Latest => Err(AbstractError::MissingVersion("module".to_string())),
295            ModuleVersion::Version(ver) => {
296                let version = Version::parse(&ver)?;
297                Ok(version)
298            }
299        }
300    }
301}
302
303impl PrimaryKey<'_> for ModuleVersion {
304    type Prefix = ();
305
306    type SubPrefix = ();
307
308    type Suffix = Self;
309
310    type SuperSuffix = Self;
311
312    fn key(&self) -> Vec<cw_storage_plus::Key> {
313        match &self {
314            ModuleVersion::Latest => "latest".key(),
315            ModuleVersion::Version(ver) => ver.key(),
316        }
317    }
318}
319
320impl Prefixer<'_> for ModuleVersion {
321    fn prefix(&self) -> Vec<Key> {
322        let self_as_bytes = match &self {
323            ModuleVersion::Latest => "latest".as_bytes(),
324            ModuleVersion::Version(ver) => ver.as_bytes(),
325        };
326        vec![Key::Ref(self_as_bytes)]
327    }
328}
329
330impl TryFrom<ContractVersion> for ModuleInfo {
331    type Error = AbstractError;
332
333    fn try_from(value: ContractVersion) -> Result<Self, Self::Error> {
334        let split: Vec<&str> = value.contract.split(':').collect();
335        if split.len() != 2 {
336            return Err(AbstractError::FormattingError {
337                object: "contract id".to_string(),
338                expected: "namespace:contract_name".into(),
339                actual: value.contract,
340            });
341        }
342        Ok(ModuleInfo {
343            namespace: Namespace::try_from(split[0])?,
344            name: split[1].to_lowercase(),
345            version: ModuleVersion::Version(value.version),
346        })
347    }
348}
349
350#[cosmwasm_schema::cw_serde]
351pub struct Module {
352    pub info: ModuleInfo,
353    pub reference: ModuleReference,
354}
355
356impl fmt::Display for Module {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        write!(f, "info: {}, reference: {:?}", self.info, self.reference)
359    }
360}
361
362impl From<(ModuleInfo, ModuleReference)> for Module {
363    fn from((info, reference): (ModuleInfo, ModuleReference)) -> Self {
364        Self { info, reference }
365    }
366}
367
368impl Module {
369    // Helper to know if this module supposed to be whitelisted on account contract
370    pub fn should_be_whitelisted(&self) -> bool {
371        match &self.reference {
372            // Standalone, Service or Native(exception for IBC Client for the ICS20 Callbacks) contracts not supposed to be whitelisted on account
373            ModuleReference::Adapter(_) | ModuleReference::App(_) => true,
374            ModuleReference::Native(_) if self.info.id() == IBC_CLIENT => true,
375            _ => false,
376        }
377    }
378}
379
380#[cosmwasm_schema::cw_serde]
381pub struct ModuleInitMsg {
382    pub fixed_init: Option<Binary>,
383    pub owner_init: Option<Binary>,
384}
385
386impl ModuleInitMsg {
387    pub fn format(self) -> AbstractResult<Binary> {
388        match self {
389            // If both set, receiving contract must handle it using the ModuleInitMsg
390            ModuleInitMsg {
391                fixed_init: Some(_),
392                owner_init: Some(_),
393            } => to_json_binary(&self),
394            // If not, we can simplify by only sending the custom or fixed message.
395            ModuleInitMsg {
396                fixed_init: None,
397                owner_init: Some(r),
398            } => Ok(r),
399            ModuleInitMsg {
400                fixed_init: Some(f),
401                owner_init: None,
402            } => Ok(f),
403            ModuleInitMsg {
404                fixed_init: None,
405                owner_init: None,
406            } => Err(StdError::generic_err("No init msg set for this module")),
407        }
408        .map_err(Into::into)
409    }
410}
411
412/// Assert that the provided module has the same data stored under the cw2 and module data keys.
413pub fn assert_module_data_validity(
414    querier: &QuerierWrapper,
415    // The module that it claims to be
416    module_claim: &Module,
417    // Optional address, if not set, skip code_id checks
418    module_address: Option<Addr>,
419) -> AbstractResult<()> {
420    // we retrieve the address information.
421    let module_address = match &module_claim.reference.unwrap_addr() {
422        Ok(addr) => addr.to_owned(),
423        Err(..) => {
424            // now we need to have a module address provided
425            let Some(addr) = module_address else {
426                // if no addr provided and module doesn't have it, just return
427                // this will be the case when registering a code-id on Registry
428                return Ok(());
429            };
430            addr
431        }
432    };
433
434    let ModuleVersion::Version(version) = &module_claim.info.version else {
435        panic!("Module version is not versioned, context setting is wrong")
436    };
437
438    // verify that the contract's data is equal to its registered data
439    let cw_2_data_res = cw2::CONTRACT.query(querier, module_address.clone());
440
441    // For standalone and service we only check the version if cw2 exists
442    if let ModuleReference::Standalone(_) | ModuleReference::Service(_) = module_claim.reference {
443        if let Ok(cw_2_data) = cw_2_data_res {
444            ensure_eq!(
445                version,
446                &cw_2_data.version,
447                AbstractError::UnequalModuleData {
448                    cw2: cw_2_data.version,
449                    module: version.to_owned()
450                }
451            );
452        }
453        return Ok(());
454    }
455    let cw_2_data = cw_2_data_res?;
456
457    // Assert that the contract name is equal to the module name
458    ensure_eq!(
459        module_claim.info.id(),
460        cw_2_data.contract,
461        AbstractError::UnequalModuleData {
462            cw2: cw_2_data.contract,
463            module: module_claim.info.id()
464        }
465    );
466
467    // Assert that the contract version is equal to the module version
468    ensure_eq!(
469        version,
470        &cw_2_data.version,
471        AbstractError::UnequalModuleData {
472            cw2: cw_2_data.version,
473            module: version.to_owned()
474        }
475    );
476    // we're done if it's not an actual module
477    match module_claim.reference {
478        ModuleReference::Account(_) | ModuleReference::Native(_) | ModuleReference::Service(_) => {
479            return Ok(())
480        }
481        _ => {}
482    }
483
484    let module_data = MODULE.query(querier, module_address)?;
485    // assert that the names are equal
486    ensure_eq!(
487        module_data.module,
488        cw_2_data.contract,
489        AbstractError::UnequalModuleData {
490            cw2: cw_2_data.contract,
491            module: module_data.module,
492        }
493    );
494    // assert that the versions are equal
495    ensure_eq!(
496        module_data.version,
497        cw_2_data.version,
498        AbstractError::UnequalModuleData {
499            cw2: cw_2_data.version,
500            module: module_data.version
501        }
502    );
503
504    Ok(())
505}
506
507/// Module Monetization
508#[cosmwasm_schema::cw_serde]
509#[non_exhaustive]
510pub enum Monetization {
511    None,
512    InstallFee(FixedFee),
513}
514
515impl Default for Monetization {
516    fn default() -> Self {
517        Self::None
518    }
519}
520
521/// Module Metadata String
522pub type ModuleMetadata = String;
523
524//--------------------------------------------------------------------------------------------------
525// Tests
526//--------------------------------------------------------------------------------------------------
527
528#[cfg(test)]
529mod test {
530    #![allow(clippy::needless_borrows_for_generic_args)]
531    use cosmwasm_std::{testing::mock_dependencies, Addr, Order};
532    use cw_storage_plus::Map;
533
534    use super::*;
535
536    mod storage_plus {
537        use super::*;
538
539        fn mock_key() -> ModuleInfo {
540            ModuleInfo {
541                namespace: Namespace::new("abstract").unwrap(),
542                name: "rocket-ship".to_string(),
543                version: ModuleVersion::Version("1.9.9".into()),
544            }
545        }
546
547        fn mock_keys() -> (ModuleInfo, ModuleInfo, ModuleInfo, ModuleInfo) {
548            (
549                ModuleInfo {
550                    namespace: Namespace::new("abstract").unwrap(),
551                    name: "boat".to_string(),
552                    version: ModuleVersion::Version("1.9.9".into()),
553                },
554                ModuleInfo {
555                    namespace: Namespace::new("abstract").unwrap(),
556                    name: "rocket-ship".to_string(),
557                    version: ModuleVersion::Version("1.0.0".into()),
558                },
559                ModuleInfo {
560                    namespace: Namespace::new("abstract").unwrap(),
561                    name: "rocket-ship".to_string(),
562                    version: ModuleVersion::Version("2.0.0".into()),
563                },
564                ModuleInfo {
565                    namespace: Namespace::new("astroport").unwrap(),
566                    name: "liquidity-pool".to_string(),
567                    version: ModuleVersion::Version("10.5.7".into()),
568                },
569            )
570        }
571
572        #[coverage_helper::test]
573        fn storage_key_works() {
574            let mut deps = mock_dependencies();
575            let key = mock_key();
576            let map: Map<&ModuleInfo, u64> = Map::new("map");
577
578            map.save(deps.as_mut().storage, &key, &42069).unwrap();
579
580            assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
581
582            let items = map
583                .range(deps.as_ref().storage, None, None, Order::Ascending)
584                .map(|item| item.unwrap())
585                .collect::<Vec<_>>();
586
587            assert_eq!(items.len(), 1);
588            assert_eq!(items[0], (key, 42069));
589        }
590
591        #[coverage_helper::test]
592        fn storage_key_with_overlapping_name_namespace() {
593            let mut deps = mock_dependencies();
594            let info1 = ModuleInfo {
595                namespace: Namespace::new("abstract").unwrap(),
596                name: "ans".to_string(),
597                version: ModuleVersion::Version("1.9.9".into()),
598            };
599
600            let _key1 = (&info1).joined_key();
601
602            let info2 = ModuleInfo {
603                namespace: Namespace::new("abs").unwrap(),
604                name: "tractans".to_string(),
605                version: ModuleVersion::Version("1.9.9".into()),
606            };
607
608            let _key2 = (&info2).joined_key();
609
610            let map: Map<&ModuleInfo, u64> = Map::new("map");
611
612            map.save(deps.as_mut().storage, &info1, &42069).unwrap();
613            map.save(deps.as_mut().storage, &info2, &69420).unwrap();
614
615            assert_eq!(
616                map.keys_raw(&deps.storage, None, None, Order::Ascending)
617                    .collect::<Vec<_>>()
618                    .len(),
619                2
620            );
621        }
622
623        #[coverage_helper::test]
624        fn composite_key_works() {
625            let mut deps = mock_dependencies();
626            let key = mock_key();
627            let map: Map<(&ModuleInfo, Addr), u64> = Map::new("map");
628
629            map.save(
630                deps.as_mut().storage,
631                (&key, Addr::unchecked("larry")),
632                &42069,
633            )
634            .unwrap();
635
636            map.save(
637                deps.as_mut().storage,
638                (&key, Addr::unchecked("jake")),
639                &69420,
640            )
641            .unwrap();
642
643            let items = map
644                .prefix(&key)
645                .range(deps.as_ref().storage, None, None, Order::Ascending)
646                .map(|item| item.unwrap())
647                .collect::<Vec<_>>();
648
649            assert_eq!(items.len(), 2);
650            assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
651            assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
652        }
653
654        #[coverage_helper::test]
655        fn partial_key_works() {
656            let mut deps = mock_dependencies();
657            let (key1, key2, key3, key4) = mock_keys();
658            let map: Map<&ModuleInfo, u64> = Map::new("map");
659
660            map.save(deps.as_mut().storage, &key1, &42069).unwrap();
661
662            map.save(deps.as_mut().storage, &key2, &69420).unwrap();
663
664            map.save(deps.as_mut().storage, &key3, &999).unwrap();
665
666            map.save(deps.as_mut().storage, &key4, &13).unwrap();
667
668            let items = map
669                .sub_prefix(Namespace::new("abstract").unwrap())
670                .range(deps.as_ref().storage, None, None, Order::Ascending)
671                .map(|item| item.unwrap())
672                .collect::<Vec<_>>();
673
674            assert_eq!(items.len(), 3);
675            assert_eq!(
676                items[0],
677                (
678                    (
679                        "boat".to_string(),
680                        ModuleVersion::Version("1.9.9".to_string())
681                    ),
682                    42069
683                )
684            );
685            assert_eq!(
686                items[1],
687                (
688                    (
689                        "rocket-ship".to_string(),
690                        ModuleVersion::Version("1.0.0".to_string())
691                    ),
692                    69420
693                )
694            );
695
696            assert_eq!(
697                items[2],
698                (
699                    (
700                        "rocket-ship".to_string(),
701                        ModuleVersion::Version("2.0.0".to_string())
702                    ),
703                    999
704                )
705            );
706
707            let items = map
708                .sub_prefix(Namespace::new("astroport").unwrap())
709                .range(deps.as_ref().storage, None, None, Order::Ascending)
710                .map(|item| item.unwrap())
711                .collect::<Vec<_>>();
712
713            assert_eq!(items.len(), 1);
714            assert_eq!(
715                items[0],
716                (
717                    (
718                        "liquidity-pool".to_string(),
719                        ModuleVersion::Version("10.5.7".to_string())
720                    ),
721                    13
722                )
723            );
724        }
725
726        #[coverage_helper::test]
727        fn partial_key_versions_works() {
728            let mut deps = mock_dependencies();
729            let (key1, key2, key3, key4) = mock_keys();
730            let map: Map<&ModuleInfo, u64> = Map::new("map");
731
732            map.save(deps.as_mut().storage, &key1, &42069).unwrap();
733
734            map.save(deps.as_mut().storage, &key2, &69420).unwrap();
735
736            map.save(deps.as_mut().storage, &key3, &999).unwrap();
737
738            map.save(deps.as_mut().storage, &key4, &13).unwrap();
739
740            let items = map
741                .prefix((
742                    Namespace::new("abstract").unwrap(),
743                    "rocket-ship".to_string(),
744                ))
745                .range(deps.as_ref().storage, None, None, Order::Ascending)
746                .map(|item| item.unwrap())
747                .collect::<Vec<_>>();
748
749            assert_eq!(items.len(), 2);
750            assert_eq!(
751                items[0],
752                (ModuleVersion::Version("1.0.0".to_string()), 69420)
753            );
754
755            assert_eq!(items[1], (ModuleVersion::Version("2.0.0".to_string()), 999));
756        }
757    }
758
759    mod module_info {
760        use super::*;
761
762        #[coverage_helper::test]
763        fn validate_with_empty_name() {
764            let info = ModuleInfo {
765                namespace: Namespace::try_from("abstract").unwrap(),
766                name: "".to_string(),
767                version: ModuleVersion::Version("1.9.9".into()),
768            };
769
770            assert!(info.validate().unwrap_err().to_string().contains("empty"));
771        }
772
773        #[coverage_helper::test]
774        fn validate_with_empty_namespace() {
775            let info = ModuleInfo {
776                namespace: Namespace::unchecked(""),
777                name: "ans".to_string(),
778                version: ModuleVersion::Version("1.9.9".into()),
779            };
780
781            assert!(info.validate().unwrap_err().to_string().contains("empty"));
782        }
783
784        use rstest::rstest;
785
786        #[rstest]
787        #[case("ans_host")]
788        #[case("ans:host")]
789        #[case("ans-host&")]
790        fn validate_fails_with_non_alphanumeric(#[case] name: &str) {
791            let info = ModuleInfo {
792                namespace: Namespace::try_from("abstract").unwrap(),
793                name: name.to_string(),
794                version: ModuleVersion::Version("1.9.9".into()),
795            };
796
797            assert!(info
798                .validate()
799                .unwrap_err()
800                .to_string()
801                .contains("alphanumeric"));
802        }
803
804        #[rstest]
805        #[case("lmao")]
806        #[case("bad-")]
807        fn validate_with_bad_versions(#[case] version: &str) {
808            let info = ModuleInfo {
809                namespace: Namespace::try_from("abstract").unwrap(),
810                name: "ans".to_string(),
811                version: ModuleVersion::Version(version.into()),
812            };
813
814            assert!(info
815                .validate()
816                .unwrap_err()
817                .to_string()
818                .contains("Invalid version"));
819        }
820
821        #[coverage_helper::test]
822        fn id() {
823            let info = ModuleInfo {
824                name: "name".to_string(),
825                namespace: Namespace::try_from("namespace").unwrap(),
826                version: ModuleVersion::Version("1.0.0".into()),
827            };
828
829            let expected = "namespace:name".to_string();
830
831            assert_eq!(info.id(), expected);
832        }
833
834        #[coverage_helper::test]
835        fn id_with_version() {
836            let info = ModuleInfo {
837                name: "name".to_string(),
838                namespace: Namespace::try_from("namespace").unwrap(),
839                version: ModuleVersion::Version("1.0.0".into()),
840            };
841
842            let expected = "namespace:name:1.0.0".to_string();
843
844            assert_eq!(info.id_with_version(), expected);
845        }
846    }
847
848    mod module_version {
849        use super::*;
850
851        #[coverage_helper::test]
852        fn try_into_version_happy_path() {
853            let version = ModuleVersion::Version("1.0.0".into());
854
855            let expected: Version = "1.0.0".to_string().parse().unwrap();
856
857            let actual: Version = version.try_into().unwrap();
858
859            assert_eq!(actual, expected);
860        }
861
862        #[coverage_helper::test]
863        fn try_into_version_with_latest() {
864            let version = ModuleVersion::Latest;
865
866            let actual: Result<Version, _> = version.try_into();
867
868            assert!(actual.is_err());
869        }
870    }
871
872    mod standalone_modules_valid {
873        use cosmwasm_std::testing::MOCK_CONTRACT_ADDR;
874
875        use super::*;
876
877        #[coverage_helper::test]
878        fn no_cw2_contract() {
879            let deps = mock_dependencies();
880            let res = assert_module_data_validity(
881                &deps.as_ref().querier,
882                &Module {
883                    info: ModuleInfo {
884                        namespace: Namespace::new("counter").unwrap(),
885                        name: "counter".to_owned(),
886                        version: ModuleVersion::Version("1.1.0".to_owned()),
887                    },
888                    reference: ModuleReference::Standalone(0),
889                },
890                Some(Addr::unchecked(MOCK_CONTRACT_ADDR)),
891            );
892            assert!(res.is_ok());
893        }
894    }
895}