1use std::collections::HashSet;
2
3use crate::block::block_info::BlockInfo;
4use crate::consensus::state::state_error::StateError;
5use crate::consensus::state::token::PreProgrammedDistributionTimestampInPastError;
6use crate::data_contract::accessors::v0::DataContractV0Getters;
7
8use crate::consensus::basic::data_contract::{
9 DuplicateKeywordsError, IncompatibleDataContractSchemaError, InvalidDataContractVersionError,
10 InvalidDescriptionLengthError, InvalidKeywordCharacterError, InvalidKeywordLengthError,
11 TooManyKeywordsError,
12};
13use crate::consensus::state::data_contract::data_contract_update_action_not_allowed_error::DataContractUpdateActionNotAllowedError;
14use crate::consensus::state::data_contract::data_contract_update_permission_error::DataContractUpdatePermissionError;
15use crate::consensus::state::data_contract::document_type_update_error::DocumentTypeUpdateError;
16use crate::data_contract::accessors::v1::DataContractV1Getters;
17use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters;
18use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters;
19use crate::data_contract::associated_token::token_pre_programmed_distribution::accessors::v0::TokenPreProgrammedDistributionV0Methods;
20use crate::data_contract::document_type::schema::validate_schema_compatibility;
21use crate::data_contract::schema::DataContractSchemaMethodsV0;
22use crate::data_contract::DataContract;
23use crate::validation::SimpleConsensusValidationResult;
24use crate::ProtocolError;
25use platform_value::Value;
26use platform_version::version::PlatformVersion;
27use serde_json::json;
28
29pub trait DataContractUpdateValidationMethodsV0 {
30 fn validate_update(
31 &self,
32 data_contract: &DataContract,
33 block_info: &BlockInfo,
34 platform_version: &PlatformVersion,
35 ) -> Result<SimpleConsensusValidationResult, ProtocolError>;
36}
37
38impl DataContract {
39 #[inline(always)]
40 pub(super) fn validate_update_v0(
41 &self,
42 new_data_contract: &DataContract,
43 block_info: &BlockInfo,
44 platform_version: &PlatformVersion,
45 ) -> Result<SimpleConsensusValidationResult, ProtocolError> {
46 if self.owner_id() != new_data_contract.owner_id() {
48 return Ok(SimpleConsensusValidationResult::new_with_error(
49 DataContractUpdatePermissionError::new(self.id(), new_data_contract.owner_id())
50 .into(),
51 ));
52 }
53
54 let new_version = new_data_contract.version();
59 let old_version = self.version();
60 if new_version < old_version || new_version - old_version != 1 {
61 return Ok(SimpleConsensusValidationResult::new_with_error(
62 InvalidDataContractVersionError::new(old_version + 1, new_version).into(),
63 ));
64 }
65
66 let config_validation_result = self.config().validate_update(
81 new_data_contract.config(),
82 self.id(),
83 platform_version,
84 )?;
85
86 if !config_validation_result.is_valid() {
87 return Ok(SimpleConsensusValidationResult::new_with_errors(
88 config_validation_result.errors,
89 ));
90 }
91
92 for (document_type_name, old_document_type) in self.document_types() {
95 let Some(new_document_type) =
97 new_data_contract.document_type_optional_for_name(document_type_name)
98 else {
99 return Ok(SimpleConsensusValidationResult::new_with_error(
100 DocumentTypeUpdateError::new(
101 self.id(),
102 document_type_name,
103 "document type can't be removed",
104 )
105 .into(),
106 ));
107 };
108
109 let validate_update_result = old_document_type
111 .as_ref()
112 .validate_update(new_document_type, platform_version)?;
113
114 if !validate_update_result.is_valid() {
115 return Ok(SimpleConsensusValidationResult::new_with_errors(
116 validate_update_result.errors,
117 ));
118 }
119 }
120
121 if let Some(old_defs_map) = self.schema_defs() {
123 let Some(new_defs_map) = new_data_contract.schema_defs() else {
125 return Ok(SimpleConsensusValidationResult::new_with_error(
126 IncompatibleDataContractSchemaError::new(
127 self.id(),
128 "remove".to_string(),
129 "/$defs".to_string(),
130 )
131 .into(),
132 ));
133 };
134
135 if old_defs_map != new_defs_map {
138 let old_defs_json = Value::from(old_defs_map)
140 .try_into_validating_json()
141 .map_err(ProtocolError::ValueError)?;
142
143 let new_defs_json = Value::from(new_defs_map)
144 .try_into_validating_json()
145 .map_err(ProtocolError::ValueError)?;
146
147 let old_defs_schema = json!({
148 "$defs": old_defs_json
149 });
150
151 let new_defs_schema = json!({
152 "$defs": new_defs_json
153 });
154
155 let compatibility_validation_result = validate_schema_compatibility(
159 &old_defs_schema,
160 &new_defs_schema,
161 platform_version,
162 )?;
163
164 if !compatibility_validation_result.is_valid() {
165 let errors = compatibility_validation_result
166 .errors
167 .into_iter()
168 .map(|operation| {
169 IncompatibleDataContractSchemaError::new(
170 self.id(),
171 operation.name,
172 operation.path,
173 )
174 .into()
175 })
176 .collect();
177
178 return Ok(SimpleConsensusValidationResult::new_with_errors(errors));
179 }
180 }
181 }
182
183 if self.groups() != new_data_contract.groups() {
184 for old_group_position in self.groups().keys() {
186 if !new_data_contract.groups().contains_key(old_group_position) {
187 return Ok(SimpleConsensusValidationResult::new_with_error(
188 DataContractUpdateActionNotAllowedError::new(
189 self.id(),
190 "remove group".to_string(),
191 )
192 .into(),
193 ));
194 }
195 }
196
197 for (old_group_position, old_group) in self.groups() {
199 if let Some(new_group) = new_data_contract.groups().get(old_group_position) {
200 if old_group != new_group {
201 return Ok(SimpleConsensusValidationResult::new_with_error(
202 DataContractUpdateActionNotAllowedError::new(
203 self.id(),
204 format!(
205 "change group at position {} is not allowed",
206 old_group_position
207 ),
208 )
209 .into(),
210 ));
211 }
212 }
213 }
214 }
215
216 if self.tokens() != new_data_contract.tokens() {
217 for (token_position, old_token_config) in self.tokens() {
218 if !new_data_contract.tokens().contains_key(token_position) {
220 return Ok(SimpleConsensusValidationResult::new_with_error(
221 DataContractUpdateActionNotAllowedError::new(
222 self.id(),
223 format!("remove token at position {}", token_position),
224 )
225 .into(),
226 ));
227 }
228
229 if let Some(new_token_config) = new_data_contract.tokens().get(token_position) {
231 if old_token_config != new_token_config {
232 return Ok(SimpleConsensusValidationResult::new_with_error(
233 DataContractUpdateActionNotAllowedError::new(
234 self.id(),
235 format!("update token at position {}", token_position),
236 )
237 .into(),
238 ));
239 }
240 }
241 }
242
243 for (token_contract_position, token_configuration) in new_data_contract.tokens() {
245 if !self.tokens().contains_key(token_contract_position) {
246 if let Some(distribution) = token_configuration
247 .distribution_rules()
248 .pre_programmed_distribution()
249 {
250 if let Some((timestamp, _)) = distribution.distributions().iter().next() {
251 if timestamp < &block_info.time_ms {
252 return Ok(SimpleConsensusValidationResult::new_with_error(
253 StateError::PreProgrammedDistributionTimestampInPastError(
254 PreProgrammedDistributionTimestampInPastError::new(
255 new_data_contract.id(),
256 *token_contract_position,
257 *timestamp,
258 block_info.time_ms,
259 ),
260 )
261 .into(),
262 ));
263 }
264 }
265 }
266 }
267 }
268 }
269
270 if self.keywords() != new_data_contract.keywords() {
271 if new_data_contract.keywords().len() > 50 {
273 return Ok(SimpleConsensusValidationResult::new_with_error(
274 TooManyKeywordsError::new(self.id(), new_data_contract.keywords().len() as u8)
275 .into(),
276 ));
277 }
278
279 let mut seen_keywords = HashSet::new();
281 for keyword in new_data_contract.keywords() {
282 if keyword.len() < 3 || keyword.len() > 50 {
284 return Ok(SimpleConsensusValidationResult::new_with_error(
285 InvalidKeywordLengthError::new(self.id(), keyword.to_string()).into(),
286 ));
287 }
288
289 if !keyword
290 .chars()
291 .all(|c| !c.is_control() && !c.is_whitespace())
292 {
293 return Ok(SimpleConsensusValidationResult::new_with_error(
295 InvalidKeywordCharacterError::new(
296 new_data_contract.id(),
297 keyword.to_string(),
298 )
299 .into(),
300 ));
301 }
302
303 if !seen_keywords.insert(keyword) {
305 return Ok(SimpleConsensusValidationResult::new_with_error(
306 DuplicateKeywordsError::new(self.id(), keyword.to_string()).into(),
307 ));
308 }
309 }
310 }
311
312 if self.description() != new_data_contract.description() {
313 if let Some(description) = new_data_contract.description() {
315 let char_count = description.chars().count();
316 if !(3..=100).contains(&char_count) {
317 return Ok(SimpleConsensusValidationResult::new_with_error(
318 InvalidDescriptionLengthError::new(self.id(), description.to_string())
319 .into(),
320 ));
321 }
322 }
323 }
324
325 Ok(SimpleConsensusValidationResult::new())
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::consensus::basic::basic_error::BasicError;
333 use crate::consensus::state::state_error::StateError;
334 use crate::consensus::ConsensusError;
335 use crate::data_contract::config::v0::DataContractConfigSettersV0;
336 use crate::prelude::IdentityNonce;
337 use crate::tests::fixtures::get_data_contract_fixture;
338 use assert_matches::assert_matches;
339 use platform_value::platform_value;
340 use platform_value::Identifier;
341
342 mod validate_update {
343 use std::collections::BTreeMap;
344
345 use super::*;
346 use crate::data_contract::accessors::v0::DataContractV0Setters;
347 use crate::data_contract::accessors::v1::DataContractV1Setters;
348 use crate::data_contract::associated_token::token_configuration::accessors::v0::{
349 TokenConfigurationV0Getters, TokenConfigurationV0Setters,
350 };
351 use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0;
352 use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0;
353 use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention;
354 use crate::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0;
355 use crate::data_contract::associated_token::token_configuration_localization::TokenConfigurationLocalization;
356 use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters;
357 use crate::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0;
358 use crate::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution;
359 use crate::data_contract::document_type::DocumentTypeMutRef;
360 use crate::data_contract::group::accessors::v0::{GroupV0Getters, GroupV0Setters};
361 use crate::data_contract::group::v0::GroupV0;
362 use crate::data_contract::group::Group;
363 use crate::data_contract::TokenConfiguration;
364 use crate::identity::accessors::IdentityGettersV0;
365 use crate::prelude::Identity;
366
367 #[test]
368 fn should_return_invalid_result_if_owner_id_is_not_the_same() {
369 let platform_version = PlatformVersion::latest();
370
371 let old_data_contract = get_data_contract_fixture(
372 None,
373 IdentityNonce::default(),
374 platform_version.protocol_version,
375 )
376 .data_contract_owned();
377
378 let mut new_data_contract = old_data_contract.clone();
379
380 new_data_contract.set_owner_id(Identifier::random());
381
382 let result = old_data_contract
383 .validate_update(&new_data_contract, &BlockInfo::default(), platform_version)
384 .expect("failed validate update");
385
386 assert_matches!(
387 result.errors.as_slice(),
388 [ConsensusError::StateError(
389 StateError::DataContractUpdatePermissionError(e)
390 )] if *e.data_contract_id() == old_data_contract.id() && *e.identity_id() == new_data_contract.owner_id()
391 );
392 }
393
394 #[test]
395 fn should_return_invalid_result_if_contract_version_is_not_greater_for_one() {
396 let platform_version = PlatformVersion::latest();
397
398 let old_data_contract = get_data_contract_fixture(
399 None,
400 IdentityNonce::default(),
401 platform_version.protocol_version,
402 )
403 .data_contract_owned();
404
405 let new_data_contract = old_data_contract.clone();
406
407 let result = old_data_contract
408 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
409 .expect("failed validate update");
410
411 assert_matches!(
412 result.errors.as_slice(),
413 [ConsensusError::BasicError(
414 BasicError::InvalidDataContractVersionError(e)
415 )] if e.expected_version() == old_data_contract.version() + 1 && e.version() == new_data_contract.version()
416 );
417 }
418
419 #[test]
420 fn should_return_invalid_result_if_config_was_updated() {
421 let platform_version = PlatformVersion::latest();
422
423 let old_data_contract = get_data_contract_fixture(
424 None,
425 IdentityNonce::default(),
426 platform_version.protocol_version,
427 )
428 .data_contract_owned();
429
430 let mut new_data_contract = old_data_contract.clone();
431
432 new_data_contract.set_version(old_data_contract.version() + 1);
433 new_data_contract.config_mut().set_readonly(true);
434
435 let result = old_data_contract
436 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
437 .expect("failed validate update");
438
439 assert_matches!(
440 result.errors.as_slice(),
441 [ConsensusError::StateError(
442 StateError::DataContractConfigUpdateError(e)
443 )] if e.additional_message() == "contract can not be changed to readonly"
444 );
445 }
446
447 #[test]
448 fn should_return_invalid_result_when_document_type_is_removed() {
449 let platform_version = PlatformVersion::latest();
450
451 let old_data_contract = get_data_contract_fixture(
452 None,
453 IdentityNonce::default(),
454 platform_version.protocol_version,
455 )
456 .data_contract_owned();
457
458 let mut new_data_contract = old_data_contract.clone();
459
460 new_data_contract.set_version(old_data_contract.version() + 1);
461 new_data_contract
462 .document_types_mut()
463 .remove("niceDocument");
464
465 let result = old_data_contract
466 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
467 .expect("failed validate update");
468
469 assert_matches!(
470 result.errors.as_slice(),
471 [ConsensusError::StateError(
472 StateError::DocumentTypeUpdateError(e)
473 )] if e.additional_message() == "document type can't be removed"
474 );
475 }
476
477 #[test]
478 fn should_return_invalid_result_when_document_type_has_incompatible_change() {
479 let platform_version = PlatformVersion::latest();
480
481 let old_data_contract = get_data_contract_fixture(
482 None,
483 IdentityNonce::default(),
484 platform_version.protocol_version,
485 )
486 .data_contract_owned();
487
488 let mut new_data_contract = old_data_contract.clone();
489
490 new_data_contract.set_version(old_data_contract.version() + 1);
491
492 match new_data_contract
493 .document_types_mut()
494 .get_mut("niceDocument")
495 .unwrap()
496 .as_mut_ref()
497 {
498 DocumentTypeMutRef::V0(dt) => dt.documents_mutable = false,
499 DocumentTypeMutRef::V1(dt) => dt.documents_mutable = false,
500 }
501
502 let result = old_data_contract
503 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
504 .expect("failed validate update");
505
506 assert_matches!(
507 result.errors.as_slice(),
508 [ConsensusError::StateError(
509 StateError::DocumentTypeUpdateError(e)
510 )] if e.additional_message() == "document type can not change whether its documents are mutable: changing from true to false"
511 );
512 }
513
514 #[test]
515 fn should_return_invalid_result_when_defs_is_removed() {
516 let platform_version = PlatformVersion::latest();
517
518 let mut old_data_contract = get_data_contract_fixture(
519 None,
520 IdentityNonce::default(),
521 platform_version.protocol_version,
522 )
523 .data_contract_owned();
524
525 old_data_contract
527 .document_types_mut()
528 .remove("prettyDocument");
529
530 let mut new_data_contract = old_data_contract.clone();
531
532 new_data_contract.set_version(old_data_contract.version() + 1);
533 new_data_contract
534 .set_schema_defs(None, false, &mut Vec::new(), platform_version)
535 .expect("failed to set schema defs");
536
537 let result = old_data_contract
538 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
539 .expect("failed validate update");
540
541 assert_matches!(
542 result.errors.as_slice(),
543 [ConsensusError::BasicError(
544 BasicError::IncompatibleDataContractSchemaError(e)
545 )] if e.operation() == "remove" && e.field_path() == "/$defs"
546 );
547 }
548
549 #[test]
550 fn should_return_invalid_result_when_updated_defs_is_incompatible() {
551 let platform_version = PlatformVersion::latest();
552
553 let old_data_contract = get_data_contract_fixture(
554 None,
555 IdentityNonce::default(),
556 platform_version.protocol_version,
557 )
558 .data_contract_owned();
559
560 let mut new_data_contract = old_data_contract.clone();
561
562 let incompatible_defs_value = platform_value!({
563 "lastName": {
564 "type": "number",
565 },
566 });
567 let incompatible_defs = incompatible_defs_value
568 .into_btree_string_map()
569 .expect("should convert to map");
570
571 new_data_contract.set_version(old_data_contract.version() + 1);
572 new_data_contract
573 .set_schema_defs(
574 Some(incompatible_defs),
575 false,
576 &mut Vec::new(),
577 platform_version,
578 )
579 .expect("failed to set schema defs");
580
581 let result = old_data_contract
582 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
583 .expect("failed validate update");
584
585 assert_matches!(
586 result.errors.as_slice(),
587 [ConsensusError::BasicError(
588 BasicError::IncompatibleDataContractSchemaError(e)
589 )] if e.operation() == "replace" && e.field_path() == "/$defs/lastName/type"
590 );
591 }
592
593 #[test]
594 fn should_pass_when_all_changes_are_compatible() {
595 let platform_version = PlatformVersion::latest();
596
597 let old_data_contract = get_data_contract_fixture(
598 None,
599 IdentityNonce::default(),
600 platform_version.protocol_version,
601 )
602 .data_contract_owned();
603
604 let mut new_data_contract = old_data_contract.clone();
605
606 new_data_contract.set_version(old_data_contract.version() + 1);
607
608 let result = old_data_contract
609 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
610 .expect("failed validate update");
611
612 assert!(result.is_valid());
613 }
614
615 #[test]
622 fn should_return_invalid_result_when_group_is_removed() {
623 let platform_version = PlatformVersion::latest();
624
625 let identity_1 = Identity::random_identity(3, Some(14), platform_version)
626 .expect("expected a platform identity");
627 let identity_1_id = identity_1.id();
628 let identity_2 = Identity::random_identity(3, Some(506), platform_version)
629 .expect("expected a platform identity");
630 let identity_2_id = identity_2.id();
631
632 let mut old_data_contract =
633 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
634 old_data_contract.set_groups(BTreeMap::from([(
635 0,
636 Group::V0(GroupV0 {
637 members: [(identity_1_id, 1), (identity_2_id, 1)].into(),
638 required_power: 2,
639 }),
640 )]));
641
642 let mut new_data_contract = old_data_contract.clone();
644 new_data_contract.set_version(old_data_contract.version() + 1);
645
646 let first_group_pos = *old_data_contract
648 .groups()
649 .keys()
650 .next()
651 .expect("fixture must have at least one group");
652 new_data_contract
653 .groups_mut()
654 .unwrap()
655 .remove(&first_group_pos);
656
657 let result = old_data_contract
658 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
659 .expect("failed validate update");
660
661 assert_matches!(
662 result.errors.as_slice(),
663 [ConsensusError::StateError(
664 StateError::DataContractUpdateActionNotAllowedError(e)
665 )] if e.action() == "remove group"
666 );
667 }
668
669 #[test]
670 fn should_return_invalid_result_when_group_is_changed() {
671 let platform_version = PlatformVersion::latest();
672
673 let identity_1 = Identity::random_identity(3, Some(14), platform_version)
674 .expect("expected a platform identity");
675 let identity_1_id = identity_1.id();
676 let identity_2 = Identity::random_identity(3, Some(506), platform_version)
677 .expect("expected a platform identity");
678 let identity_2_id = identity_2.id();
679
680 let mut old_data_contract =
681 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
682 old_data_contract.set_groups(BTreeMap::from([(
683 0,
684 Group::V0(GroupV0 {
685 members: [(identity_1_id, 1), (identity_2_id, 1)].into(),
686 required_power: 2,
687 }),
688 )]));
689
690 let mut new_data_contract = old_data_contract.clone();
692 new_data_contract.set_version(old_data_contract.version() + 1);
693
694 let first_group_pos = *new_data_contract
697 .groups()
698 .keys()
699 .next()
700 .expect("fixture must have at least one group");
701 let mut altered_group = new_data_contract
702 .groups()
703 .get(&first_group_pos)
704 .cloned()
705 .expect("group must exist");
706 altered_group.set_required_power(altered_group.required_power() + 1);
708 new_data_contract
709 .groups_mut()
710 .unwrap()
711 .insert(first_group_pos, altered_group);
712
713 let result = old_data_contract
714 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
715 .expect("failed validate update");
716
717 assert_matches!(
718 result.errors.as_slice(),
719 [ConsensusError::StateError(
720 StateError::DataContractUpdateActionNotAllowedError(e)
721 )] if e.action() == format!(
722 "change group at position {} is not allowed",
723 first_group_pos
724 )
725 );
726 }
727
728 #[test]
735 fn should_return_invalid_result_when_token_is_removed() {
736 let platform_version = PlatformVersion::latest();
737
738 let mut old_data_contract =
739 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
740 old_data_contract.set_tokens(BTreeMap::from([(
741 0,
742 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()),
743 )]));
744
745 let mut new_data_contract = old_data_contract.clone();
746 new_data_contract.set_version(old_data_contract.version() + 1);
747
748 let first_token_pos = *old_data_contract
750 .tokens()
751 .keys()
752 .next()
753 .expect("fixture must have at least one token");
754 new_data_contract
755 .tokens_mut()
756 .unwrap()
757 .remove(&first_token_pos);
758
759 let result = old_data_contract
760 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
761 .expect("failed validate update");
762
763 assert_matches!(
764 result.errors.as_slice(),
765 [ConsensusError::StateError(
766 StateError::DataContractUpdateActionNotAllowedError(e)
767 )] if e.action() == format!("remove token at position {}", first_token_pos)
768 );
769 }
770
771 #[test]
772 fn should_return_invalid_result_when_token_is_updated() {
773 let platform_version = PlatformVersion::latest();
774
775 let mut old_data_contract =
776 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
777 old_data_contract.set_tokens(BTreeMap::from([(
778 0,
779 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()),
780 )]));
781
782 let mut new_data_contract = old_data_contract.clone();
783 new_data_contract.set_version(old_data_contract.version() + 1);
784
785 let first_token_pos = *new_data_contract
787 .tokens()
788 .keys()
789 .next()
790 .expect("fixture must have at least one token");
791 let mut altered_token_cfg = new_data_contract
792 .tokens()
793 .get(&first_token_pos)
794 .cloned()
795 .expect("token must exist");
796 altered_token_cfg.set_base_supply(altered_token_cfg.base_supply() + 1);
798 new_data_contract
799 .tokens_mut()
800 .unwrap()
801 .insert(first_token_pos, altered_token_cfg);
802
803 let result = old_data_contract
804 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
805 .expect("failed validate update");
806
807 assert_matches!(
808 result.errors.as_slice(),
809 [ConsensusError::StateError(
810 StateError::DataContractUpdateActionNotAllowedError(e)
811 )] if e.action() == format!("update token at position {}", first_token_pos)
812 );
813 }
814
815 #[test]
816 fn should_return_invalid_result_when_token_is_added_with_past_timestamp() {
817 let platform_version = PlatformVersion::latest();
818
819 let mut old_data_contract =
820 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
821 let mut token_cfg =
822 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
823 token_cfg.set_conventions(TokenConfigurationConvention::V0(
824 TokenConfigurationConventionV0 {
825 localizations: BTreeMap::from([(
826 "en".to_string(),
827 TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 {
828 should_capitalize: false,
829 singular_form: "test".to_string(),
830 plural_form: "tests".to_string(),
831 }),
832 )]),
833 decimals: 8,
834 },
835 ));
836 old_data_contract.set_tokens(BTreeMap::from([(0, token_cfg)]));
837
838 let mut new_data_contract = old_data_contract.clone();
839 new_data_contract.set_version(old_data_contract.version() + 1);
840
841 let existing_cfg = new_data_contract
843 .tokens()
844 .values()
845 .next()
846 .expect("fixture must have at least one token")
847 .clone();
848 let new_position = old_data_contract
849 .tokens()
850 .keys()
851 .max()
852 .expect("fixture must have at least one token")
853 + 1;
854 let mut new_token_cfg = existing_cfg.clone();
855 new_token_cfg
856 .distribution_rules_mut()
857 .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0(
858 TokenPreProgrammedDistributionV0 {
859 distributions: BTreeMap::from([(
860 0,
861 BTreeMap::from([(new_data_contract.owner_id(), 100)]),
862 )]),
863 },
864 )));
865 new_data_contract
866 .tokens_mut()
867 .unwrap()
868 .insert(new_position, new_token_cfg);
869
870 let result = old_data_contract
871 .validate_update_v0(
872 &new_data_contract,
873 &BlockInfo::default_with_time(100000),
874 platform_version,
875 )
876 .expect("failed validate update");
877
878 assert_matches!(
879 result.errors.as_slice(),
880 [ConsensusError::StateError(
881 StateError::PreProgrammedDistributionTimestampInPastError(e)
882 )] if e.token_position() == new_position
883 );
884 }
885
886 #[test]
887 fn should_pass_when_a_well_formed_new_token_is_added() {
888 let platform_version = PlatformVersion::latest();
889
890 let old_data_contract =
891 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
892
893 let mut new_data_contract = old_data_contract.clone();
894 new_data_contract.set_version(old_data_contract.version() + 1);
895
896 let valid_token_cfg = {
898 let mut cfg =
899 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
900
901 cfg.set_base_supply(1_000_000); cfg.set_conventions(TokenConfigurationConvention::V0(
904 TokenConfigurationConventionV0 {
905 localizations: BTreeMap::from([(
906 "en".to_string(),
907 TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 {
908 should_capitalize: true,
909 singular_form: "credit".to_string(),
910 plural_form: "credits".to_string(),
911 }),
912 )]),
913 decimals: 8,
914 },
915 ));
916
917 cfg
918 };
919
920 new_data_contract
922 .tokens_mut()
923 .unwrap()
924 .insert(0, valid_token_cfg);
925
926 let result = old_data_contract
927 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
928 .expect("failed validate update");
929
930 assert!(result.is_valid(), "well‑formed token should be accepted");
931 }
932
933 #[test]
940 fn should_pass_when_groups_and_tokens_unchanged() {
941 let platform_version = PlatformVersion::latest();
942
943 let old_data_contract =
944 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
945
946 let mut new_data_contract = old_data_contract.clone();
947 new_data_contract.set_version(old_data_contract.version() + 1);
948
949 let result = old_data_contract
950 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
951 .expect("failed validate update");
952
953 assert!(result.is_valid());
954 }
955 }
956}