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
9pub type ModuleId<'a> = &'a str;
11
12#[cosmwasm_schema::cw_serde]
14pub struct ModuleInfo {
15 pub provider: String,
17 pub name: String,
19 pub version: ModuleVersion,
21}
22
23const MAX_LENGTH: usize = 64;
24
25fn 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 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 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 Version::parse(ver)?;
211 Ok(())
212 }
213 }
214 }
215}
216
217impl 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 ModuleInitMsg {
311 fixed_init: Some(_),
312 root_init: Some(_),
313 } => to_binary(&self),
314 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#[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}