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 DocumentTypeMutRef::V2(dt) => dt.documents_mutable = false,
501 }
502
503 let result = old_data_contract
504 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
505 .expect("failed validate update");
506
507 assert_matches!(
508 result.errors.as_slice(),
509 [ConsensusError::StateError(
510 StateError::DocumentTypeUpdateError(e)
511 )] if e.additional_message() == "document type can not change whether its documents are mutable: changing from true to false"
512 );
513 }
514
515 #[test]
516 fn should_return_invalid_result_when_defs_is_removed() {
517 let platform_version = PlatformVersion::latest();
518
519 let mut old_data_contract = get_data_contract_fixture(
520 None,
521 IdentityNonce::default(),
522 platform_version.protocol_version,
523 )
524 .data_contract_owned();
525
526 old_data_contract
528 .document_types_mut()
529 .remove("prettyDocument");
530
531 let mut new_data_contract = old_data_contract.clone();
532
533 new_data_contract.set_version(old_data_contract.version() + 1);
534 new_data_contract
535 .set_schema_defs(None, false, &mut Vec::new(), platform_version)
536 .expect("failed to set schema defs");
537
538 let result = old_data_contract
539 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
540 .expect("failed validate update");
541
542 assert_matches!(
543 result.errors.as_slice(),
544 [ConsensusError::BasicError(
545 BasicError::IncompatibleDataContractSchemaError(e)
546 )] if e.operation() == "remove" && e.field_path() == "/$defs"
547 );
548 }
549
550 #[test]
551 fn should_return_invalid_result_when_updated_defs_is_incompatible() {
552 let platform_version = PlatformVersion::latest();
553
554 let old_data_contract = get_data_contract_fixture(
555 None,
556 IdentityNonce::default(),
557 platform_version.protocol_version,
558 )
559 .data_contract_owned();
560
561 let mut new_data_contract = old_data_contract.clone();
562
563 let incompatible_defs_value = platform_value!({
564 "lastName": {
565 "type": "number",
566 },
567 });
568 let incompatible_defs = incompatible_defs_value
569 .into_btree_string_map()
570 .expect("should convert to map");
571
572 new_data_contract.set_version(old_data_contract.version() + 1);
573 new_data_contract
574 .set_schema_defs(
575 Some(incompatible_defs),
576 false,
577 &mut Vec::new(),
578 platform_version,
579 )
580 .expect("failed to set schema defs");
581
582 let result = old_data_contract
583 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
584 .expect("failed validate update");
585
586 assert_matches!(
587 result.errors.as_slice(),
588 [ConsensusError::BasicError(
589 BasicError::IncompatibleDataContractSchemaError(e)
590 )] if e.operation() == "replace" && e.field_path() == "/$defs/lastName/type"
591 );
592 }
593
594 #[test]
595 fn should_pass_when_all_changes_are_compatible() {
596 let platform_version = PlatformVersion::latest();
597
598 let old_data_contract = get_data_contract_fixture(
599 None,
600 IdentityNonce::default(),
601 platform_version.protocol_version,
602 )
603 .data_contract_owned();
604
605 let mut new_data_contract = old_data_contract.clone();
606
607 new_data_contract.set_version(old_data_contract.version() + 1);
608
609 let result = old_data_contract
610 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
611 .expect("failed validate update");
612
613 assert!(result.is_valid());
614 }
615
616 #[test]
623 fn should_return_invalid_result_when_group_is_removed() {
624 let platform_version = PlatformVersion::latest();
625
626 let identity_1 = Identity::random_identity(3, Some(14), platform_version)
627 .expect("expected a platform identity");
628 let identity_1_id = identity_1.id();
629 let identity_2 = Identity::random_identity(3, Some(506), platform_version)
630 .expect("expected a platform identity");
631 let identity_2_id = identity_2.id();
632
633 let mut old_data_contract =
634 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
635 old_data_contract.set_groups(BTreeMap::from([(
636 0,
637 Group::V0(GroupV0 {
638 members: [(identity_1_id, 1), (identity_2_id, 1)].into(),
639 required_power: 2,
640 }),
641 )]));
642
643 let mut new_data_contract = old_data_contract.clone();
645 new_data_contract.set_version(old_data_contract.version() + 1);
646
647 let first_group_pos = *old_data_contract
649 .groups()
650 .keys()
651 .next()
652 .expect("fixture must have at least one group");
653 new_data_contract
654 .groups_mut()
655 .unwrap()
656 .remove(&first_group_pos);
657
658 let result = old_data_contract
659 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
660 .expect("failed validate update");
661
662 assert_matches!(
663 result.errors.as_slice(),
664 [ConsensusError::StateError(
665 StateError::DataContractUpdateActionNotAllowedError(e)
666 )] if e.action() == "remove group"
667 );
668 }
669
670 #[test]
671 fn should_return_invalid_result_when_group_is_changed() {
672 let platform_version = PlatformVersion::latest();
673
674 let identity_1 = Identity::random_identity(3, Some(14), platform_version)
675 .expect("expected a platform identity");
676 let identity_1_id = identity_1.id();
677 let identity_2 = Identity::random_identity(3, Some(506), platform_version)
678 .expect("expected a platform identity");
679 let identity_2_id = identity_2.id();
680
681 let mut old_data_contract =
682 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
683 old_data_contract.set_groups(BTreeMap::from([(
684 0,
685 Group::V0(GroupV0 {
686 members: [(identity_1_id, 1), (identity_2_id, 1)].into(),
687 required_power: 2,
688 }),
689 )]));
690
691 let mut new_data_contract = old_data_contract.clone();
693 new_data_contract.set_version(old_data_contract.version() + 1);
694
695 let first_group_pos = *new_data_contract
698 .groups()
699 .keys()
700 .next()
701 .expect("fixture must have at least one group");
702 let mut altered_group = new_data_contract
703 .groups()
704 .get(&first_group_pos)
705 .cloned()
706 .expect("group must exist");
707 altered_group.set_required_power(altered_group.required_power() + 1);
709 new_data_contract
710 .groups_mut()
711 .unwrap()
712 .insert(first_group_pos, altered_group);
713
714 let result = old_data_contract
715 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
716 .expect("failed validate update");
717
718 assert_matches!(
719 result.errors.as_slice(),
720 [ConsensusError::StateError(
721 StateError::DataContractUpdateActionNotAllowedError(e)
722 )] if e.action() == format!(
723 "change group at position {} is not allowed",
724 first_group_pos
725 )
726 );
727 }
728
729 #[test]
736 fn should_return_invalid_result_when_token_is_removed() {
737 let platform_version = PlatformVersion::latest();
738
739 let mut old_data_contract =
740 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
741 old_data_contract.set_tokens(BTreeMap::from([(
742 0,
743 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()),
744 )]));
745
746 let mut new_data_contract = old_data_contract.clone();
747 new_data_contract.set_version(old_data_contract.version() + 1);
748
749 let first_token_pos = *old_data_contract
751 .tokens()
752 .keys()
753 .next()
754 .expect("fixture must have at least one token");
755 new_data_contract
756 .tokens_mut()
757 .unwrap()
758 .remove(&first_token_pos);
759
760 let result = old_data_contract
761 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
762 .expect("failed validate update");
763
764 assert_matches!(
765 result.errors.as_slice(),
766 [ConsensusError::StateError(
767 StateError::DataContractUpdateActionNotAllowedError(e)
768 )] if e.action() == format!("remove token at position {}", first_token_pos)
769 );
770 }
771
772 #[test]
773 fn should_return_invalid_result_when_token_is_updated() {
774 let platform_version = PlatformVersion::latest();
775
776 let mut old_data_contract =
777 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
778 old_data_contract.set_tokens(BTreeMap::from([(
779 0,
780 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()),
781 )]));
782
783 let mut new_data_contract = old_data_contract.clone();
784 new_data_contract.set_version(old_data_contract.version() + 1);
785
786 let first_token_pos = *new_data_contract
788 .tokens()
789 .keys()
790 .next()
791 .expect("fixture must have at least one token");
792 let mut altered_token_cfg = new_data_contract
793 .tokens()
794 .get(&first_token_pos)
795 .cloned()
796 .expect("token must exist");
797 altered_token_cfg.set_base_supply(altered_token_cfg.base_supply() + 1);
799 new_data_contract
800 .tokens_mut()
801 .unwrap()
802 .insert(first_token_pos, altered_token_cfg);
803
804 let result = old_data_contract
805 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
806 .expect("failed validate update");
807
808 assert_matches!(
809 result.errors.as_slice(),
810 [ConsensusError::StateError(
811 StateError::DataContractUpdateActionNotAllowedError(e)
812 )] if e.action() == format!("update token at position {}", first_token_pos)
813 );
814 }
815
816 #[test]
817 fn should_return_invalid_result_when_token_is_added_with_past_timestamp() {
818 let platform_version = PlatformVersion::latest();
819
820 let mut old_data_contract =
821 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
822 let mut token_cfg =
823 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
824 token_cfg.set_conventions(TokenConfigurationConvention::V0(
825 TokenConfigurationConventionV0 {
826 localizations: BTreeMap::from([(
827 "en".to_string(),
828 TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 {
829 should_capitalize: false,
830 singular_form: "test".to_string(),
831 plural_form: "tests".to_string(),
832 }),
833 )]),
834 decimals: 8,
835 },
836 ));
837 old_data_contract.set_tokens(BTreeMap::from([(0, token_cfg)]));
838
839 let mut new_data_contract = old_data_contract.clone();
840 new_data_contract.set_version(old_data_contract.version() + 1);
841
842 let existing_cfg = new_data_contract
844 .tokens()
845 .values()
846 .next()
847 .expect("fixture must have at least one token")
848 .clone();
849 let new_position = old_data_contract
850 .tokens()
851 .keys()
852 .max()
853 .expect("fixture must have at least one token")
854 + 1;
855 let mut new_token_cfg = existing_cfg.clone();
856 new_token_cfg
857 .distribution_rules_mut()
858 .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0(
859 TokenPreProgrammedDistributionV0 {
860 distributions: BTreeMap::from([(
861 0,
862 BTreeMap::from([(new_data_contract.owner_id(), 100)]),
863 )]),
864 },
865 )));
866 new_data_contract
867 .tokens_mut()
868 .unwrap()
869 .insert(new_position, new_token_cfg);
870
871 let result = old_data_contract
872 .validate_update_v0(
873 &new_data_contract,
874 &BlockInfo::default_with_time(100000),
875 platform_version,
876 )
877 .expect("failed validate update");
878
879 assert_matches!(
880 result.errors.as_slice(),
881 [ConsensusError::StateError(
882 StateError::PreProgrammedDistributionTimestampInPastError(e)
883 )] if e.token_position() == new_position
884 );
885 }
886
887 #[test]
888 fn should_pass_when_a_well_formed_new_token_is_added() {
889 let platform_version = PlatformVersion::latest();
890
891 let old_data_contract =
892 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
893
894 let mut new_data_contract = old_data_contract.clone();
895 new_data_contract.set_version(old_data_contract.version() + 1);
896
897 let valid_token_cfg = {
899 let mut cfg =
900 TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive());
901
902 cfg.set_base_supply(1_000_000); cfg.set_conventions(TokenConfigurationConvention::V0(
905 TokenConfigurationConventionV0 {
906 localizations: BTreeMap::from([(
907 "en".to_string(),
908 TokenConfigurationLocalization::V0(TokenConfigurationLocalizationV0 {
909 should_capitalize: true,
910 singular_form: "credit".to_string(),
911 plural_form: "credits".to_string(),
912 }),
913 )]),
914 decimals: 8,
915 },
916 ));
917
918 cfg
919 };
920
921 new_data_contract
923 .tokens_mut()
924 .unwrap()
925 .insert(0, valid_token_cfg);
926
927 let result = old_data_contract
928 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
929 .expect("failed validate update");
930
931 assert!(result.is_valid(), "well‑formed token should be accepted");
932 }
933
934 #[test]
941 fn should_pass_when_groups_and_tokens_unchanged() {
942 let platform_version = PlatformVersion::latest();
943
944 let old_data_contract =
945 get_data_contract_fixture(None, IdentityNonce::default(), 9).data_contract_owned();
946
947 let mut new_data_contract = old_data_contract.clone();
948 new_data_contract.set_version(old_data_contract.version() + 1);
949
950 let result = old_data_contract
951 .validate_update_v0(&new_data_contract, &BlockInfo::default(), platform_version)
952 .expect("failed validate update");
953
954 assert!(result.is_valid());
955 }
956 }
957}