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