1use crate::data_contract::DataContractFactory;
2use crate::prelude::*;
3use crate::ProtocolError;
4use std::collections::{BTreeMap, BTreeSet};
5
6use crate::data_contract::accessors::v0::DataContractV0Setters;
7use crate::data_contract::config::v1::DataContractConfigSettersV1;
8use crate::data_contract::config::DataContractConfig;
9pub use data_contracts::*;
10use platform_version::version::PlatformVersion;
11
12pub trait ConfigurationForSystemContract {
13 fn configuration_in_platform_version(
14 &self,
15 version: &PlatformVersion,
16 ) -> Result<DataContractConfig, ProtocolError>;
17}
18
19impl ConfigurationForSystemContract for SystemDataContract {
20 fn configuration_in_platform_version(
21 &self,
22 platform_version: &PlatformVersion,
23 ) -> Result<DataContractConfig, ProtocolError> {
24 match self {
25 SystemDataContract::Withdrawals => {
26 let mut config = DataContractConfig::default_for_version(platform_version)?;
27 config.set_sized_integer_types_enabled(false);
28 Ok(config)
29 }
30 SystemDataContract::MasternodeRewards => {
31 let mut config = DataContractConfig::default_for_version(platform_version)?;
32 config.set_sized_integer_types_enabled(false);
33 Ok(config)
34 }
35 SystemDataContract::FeatureFlags => {
40 DataContractConfig::default_for_version(platform_version)
41 }
42 SystemDataContract::DPNS => {
43 let mut config = DataContractConfig::default_for_version(platform_version)?;
44 config.set_sized_integer_types_enabled(false);
45 Ok(config)
46 }
47 SystemDataContract::Dashpay => {
48 let mut config = DataContractConfig::default_for_version(platform_version)?;
49 config.set_sized_integer_types_enabled(false);
50 Ok(config)
51 }
52 SystemDataContract::WalletUtils => {
53 let mut config = DataContractConfig::default_for_version(platform_version)?;
54 config.set_sized_integer_types_enabled(false);
55 Ok(config)
56 }
57 SystemDataContract::TokenHistory => {
58 let mut config = DataContractConfig::default_for_version(platform_version)?;
59 config.set_sized_integer_types_enabled(true);
60 Ok(config)
61 }
62 SystemDataContract::KeywordSearch => {
63 let mut config = DataContractConfig::default_for_version(platform_version)?;
64 config.set_sized_integer_types_enabled(true);
65 Ok(config)
66 }
67 SystemDataContract::DocumentHistory => {
68 let mut config = DataContractConfig::default_for_version(platform_version)?;
69 config.set_sized_integer_types_enabled(true);
70 Ok(config)
71 }
72 SystemDataContract::AppConnect => {
73 let mut config = DataContractConfig::default_for_version(platform_version)?;
74 config.set_sized_integer_types_enabled(true);
75 Ok(config)
76 }
77 SystemDataContract::ModerationCharters => {
78 let mut config = DataContractConfig::default_for_version(platform_version)?;
79 config.set_sized_integer_types_enabled(true);
80 Ok(config)
81 }
82 }
83 }
84}
85
86fn create_data_contract(
94 factory: &DataContractFactory,
95 system_contract: SystemDataContract,
96 platform_version: &PlatformVersion,
97) -> Result<DataContract, ProtocolError> {
98 let DataContractSource {
99 id_bytes,
100 owner_id_bytes,
101 version,
102 definitions,
103 document_schemas,
104 } = system_contract
105 .source(platform_version)
106 .map_err(|e| ProtocolError::Generic(e.to_string()))?;
107
108 let mut data_contract = factory.create_with_id(
109 Identifier::from(id_bytes),
110 Identifier::from(owner_id_bytes),
111 0,
112 document_schemas.into(),
113 Some(system_contract.configuration_in_platform_version(platform_version)?),
114 definitions.map(|def| def.into()),
115 )?;
116
117 data_contract.data_contract_mut().set_version(version);
118
119 Ok(data_contract.data_contract_owned())
120}
121
122pub fn load_system_data_contract(
123 system_contract: SystemDataContract,
124 platform_version: &PlatformVersion,
125) -> Result<DataContract, ProtocolError> {
126 let factory = DataContractFactory::new(platform_version.protocol_version)?;
127
128 create_data_contract(&factory, system_contract, platform_version)
129}
130
131pub fn load_system_data_contracts(
132 system_contracts: BTreeSet<SystemDataContract>,
133 platform_version: &PlatformVersion,
134) -> Result<BTreeMap<SystemDataContract, DataContract>, ProtocolError> {
135 let factory = DataContractFactory::new(platform_version.protocol_version)?;
136
137 system_contracts
138 .into_iter()
139 .map(|system_contract| {
140 let data_contract = create_data_contract(&factory, system_contract, platform_version)?;
141
142 Ok((system_contract, data_contract))
143 })
144 .collect()
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::data_contract::serialized_version::DataContractInSerializationFormat;
151 use crate::serialization::PlatformSerializableWithPlatformVersion;
152 use platform_version::TryIntoPlatformVersioned;
153 #[cfg(feature = "validation")]
159 #[test]
160 fn should_fit_every_system_contract_under_the_contract_size_caps() {
161 use crate::serialization::PlatformSerializableWithPlatformVersion;
162 use platform_version::version::PLATFORM_VERSIONS;
163
164 for platform_version in PLATFORM_VERSIONS.iter() {
165 let contract_versions = &platform_version.system_data_contracts;
166 for system_contract in SystemDataContract::ALL {
167 let absent = match system_contract {
169 SystemDataContract::FeatureFlags => true,
171 SystemDataContract::DocumentHistory => platform_version.protocol_version < 13,
174 SystemDataContract::AppConnect => contract_versions.app_connect == 0,
175 SystemDataContract::ModerationCharters => {
176 contract_versions.moderation_charters == 0
177 }
178 SystemDataContract::Withdrawals
179 | SystemDataContract::MasternodeRewards
180 | SystemDataContract::DPNS
181 | SystemDataContract::Dashpay
182 | SystemDataContract::WalletUtils
183 | SystemDataContract::TokenHistory
184 | SystemDataContract::KeywordSearch => false,
185 };
186 if absent {
187 continue;
188 }
189 let contract = load_system_data_contract(system_contract, platform_version)
190 .unwrap_or_else(|error| {
191 panic!(
192 "{system_contract:?} does not load at protocol version {}: {error}",
193 platform_version.protocol_version
194 )
195 });
196 let origin = format!(
197 "{system_contract:?} at protocol version {}",
198 platform_version.protocol_version
199 );
200 let carried_size = contract
201 .serialize_to_bytes_with_platform_version(platform_version)
202 .expect("expected the system contract to serialize")
203 .len();
204 assert!(
205 carried_size
206 <= platform_version.dpp.contract_versions.max_serialized_size as usize,
207 "{origin} serializes to {carried_size} bytes"
208 );
209 let stored_size = contract
210 .serialize_to_storage_bytes(platform_version)
211 .expect("expected the system contract to serialize for storage")
212 .len();
213 if let Some(max_compiled_size) =
214 platform_version.system_limits.max_compiled_contract_size
215 {
216 assert!(
217 stored_size <= max_compiled_size as usize,
218 "{origin} is stored in {stored_size} bytes"
219 );
220 }
221 }
222 }
223 }
224
225 #[test]
226 fn test_load_system_data_contract_v8_vs_v9() {
227 let contract_1 = load_system_data_contract(
228 SystemDataContract::TokenHistory,
229 PlatformVersion::get(8).unwrap(),
230 )
231 .expect("data_contract");
232 let contract_2 = load_system_data_contract(
233 SystemDataContract::TokenHistory,
234 PlatformVersion::get(9).unwrap(),
235 )
236 .expect("data_contract");
237 assert_ne!(contract_1, contract_2);
238 }
239
240 #[test]
241 fn serialize_withdrawal_contract_v1_vs_v9() {
242 let contract_1 = load_system_data_contract(
243 SystemDataContract::Withdrawals,
244 PlatformVersion::get(1).unwrap(),
245 )
246 .expect("data_contract");
247 let contract_2 = load_system_data_contract(
248 SystemDataContract::Withdrawals,
249 PlatformVersion::get(9).unwrap(),
250 )
251 .expect("data_contract");
252
253 assert_ne!(contract_1, contract_2);
254 let v1_ser: DataContractInSerializationFormat = contract_1
255 .clone()
256 .try_into_platform_versioned(PlatformVersion::get(1).unwrap())
257 .expect("expected to serialize");
258 let v2_ser: DataContractInSerializationFormat = contract_2
259 .clone()
260 .try_into_platform_versioned(PlatformVersion::get(1).unwrap())
261 .expect("expected to serialize");
262 assert_eq!(v1_ser, v2_ser);
263
264 let v1_bytes = contract_1
265 .serialize_to_bytes_with_platform_version(PlatformVersion::get(1).unwrap())
266 .expect("expected to serialize");
267 let v8_bytes = contract_1
268 .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
269 .expect("expected to serialize");
270 let v9_bytes = contract_1
271 .serialize_to_bytes_with_platform_version(PlatformVersion::get(9).unwrap())
272 .expect("expected to serialize");
273 assert_eq!(v1_bytes.len(), 1747);
274 assert_eq!(v8_bytes.len(), 1747);
275 assert_eq!(v9_bytes.len(), 1757); let v1_bytes = contract_2
278 .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
279 .expect("expected to serialize");
280 let v8_bytes = contract_2
281 .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
282 .expect("expected to serialize");
283 let v9_bytes = contract_2
284 .serialize_to_bytes_with_platform_version(PlatformVersion::get(9).unwrap())
285 .expect("expected to serialize");
286 assert_eq!(v1_bytes.len(), 1747);
287 assert_eq!(v8_bytes.len(), 1747);
288 assert_eq!(v9_bytes.len(), 1758); }
290}
291
292#[cfg(all(test, feature = "app-connect-contract", feature = "validation"))]
293mod app_connect_tests {
294 use super::*;
295 use crate::data_contract::accessors::v0::DataContractV0Getters;
296 use crate::data_contract::document_type::random_document::CreateRandomDocument;
297 use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0;
298 use crate::document::{Document, DocumentV0Getters, DocumentV0Setters};
299 use platform_value::Value;
300
301 fn response(contract: &DataContract) -> Document {
302 let mut document = contract
303 .document_type_for_name("loginKeyResponse")
304 .expect("response type")
305 .random_document(Some(42), PlatformVersion::latest())
306 .expect("response document");
307 document.set_properties(BTreeMap::from([
308 (
309 "appEphemeralPubKeyHash".into(),
310 Value::Bytes(vec![0x11; 20]),
311 ),
312 ("walletEphemeralPubKey".into(), Value::Bytes(vec![0x22; 33])),
313 ("encryptedPayload".into(), Value::Bytes(vec![0x33; 60])),
314 ]));
315 document
316 }
317
318 #[test]
319 fn should_validate_app_connect_response_lengths() {
320 let platform_version = PlatformVersion::latest();
321 let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version)
322 .expect("system contract");
323 for (property, length, expected) in [
324 ("appEphemeralPubKeyHash", 19, false),
325 ("appEphemeralPubKeyHash", 20, true),
326 ("appEphemeralPubKeyHash", 21, false),
327 ("walletEphemeralPubKey", 32, false),
328 ("walletEphemeralPubKey", 33, true),
329 ("walletEphemeralPubKey", 34, false),
330 ("encryptedPayload", 59, false),
331 ("encryptedPayload", 60, true),
332 ("encryptedPayload", 572, true),
333 ("encryptedPayload", 573, false),
334 ] {
335 let mut document = response(&contract);
336 document.set(property, Value::Bytes(vec![0x44; length]));
337 let result = contract
338 .validate_document("loginKeyResponse", &document, platform_version)
339 .expect("validation executes");
340 assert_eq!(
341 result.is_valid(),
342 expected,
343 "{property}: {length} bytes: {result:?}"
344 );
345 }
346 }
347
348 #[test]
349 fn should_require_only_the_three_app_connect_response_properties() {
350 let platform_version = PlatformVersion::latest();
351 let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version)
352 .expect("system contract");
353 assert_eq!(contract.document_types().len(), 1);
354 for property in [
355 "appEphemeralPubKeyHash",
356 "walletEphemeralPubKey",
357 "encryptedPayload",
358 ] {
359 let mut document = response(&contract);
360 document.properties_mut().remove(property);
361 assert!(
362 !contract
363 .validate_document("loginKeyResponse", &document, platform_version)
364 .expect("validation executes")
365 .is_valid(),
366 "{property} is required"
367 );
368 }
369 let mut document = response(&contract);
370 document.set("contractId", Value::Bytes(vec![0x55; 32]));
371 assert!(
372 !contract
373 .validate_document("loginKeyResponse", &document, platform_version)
374 .expect("validation executes")
375 .is_valid(),
376 "the former contractId field is not admitted"
377 );
378 }
379}
380
381#[cfg(all(test, feature = "moderation-charters-contract", feature = "validation"))]
382mod moderation_charters_tests {
383 use super::*;
384 use crate::consensus::basic::document::PropertyConstraintViolation;
385 use crate::consensus::basic::BasicError;
386 use crate::consensus::ConsensusError;
387 use crate::data_contract::accessors::v0::DataContractV0Getters;
388 use crate::data_contract::document_type::accessors::{
389 DocumentTypeV0Getters, DocumentTypeV2Getters,
390 };
391 use crate::data_contract::document_type::random_document::CreateRandomDocument;
392 use crate::data_contract::document_type::{
393 ContestedIndexResolution, ContractReferenceModeration, DistinctFrom,
394 DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentType, EncryptedForRecipient,
395 EncryptionScheme, KeyReferenceIdentityProperty, LookupKeySource, PropertyReference,
396 StringPropertySizes,
397 };
398 use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0;
399 use crate::document::{Document, DocumentV0Getters, DocumentV0Setters};
400 use crate::identity::Purpose;
401 use crate::moderation_charter::{
402 property_names, ElectedCharter, ModerationCharterRewardSplit, SubmittedCharter,
403 ADDED_MODERATOR_DOCUMENT_TYPE_NAME, ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
404 JOIN_REQUEST_DOCUMENT_TYPE_NAME, MODERATION_CHARTERS_CONTRACT_ID,
405 REASON_DOCUMENT_TYPE_NAME, REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
406 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
407 };
408 use platform_value::{Identifier, Value};
409
410 fn contract() -> DataContract {
411 load_system_data_contract(
412 SystemDataContract::ModerationCharters,
413 PlatformVersion::latest(),
414 )
415 .expect("the moderation charters contract loads")
416 }
417
418 fn document_type<'a>(contract: &'a DataContract, name: &str) -> &'a DocumentType {
419 contract
420 .document_types()
421 .get(name)
422 .unwrap_or_else(|| panic!("the {name} type"))
423 }
424
425 fn reference<'a>(
426 contract: &'a DataContract,
427 type_name: &str,
428 property: &str,
429 ) -> PropertyReference<'a> {
430 document_type(contract, type_name)
431 .flattened_properties()
432 .get(property)
433 .unwrap_or_else(|| panic!("{type_name}.{property}"))
434 .property_type
435 .reference()
436 .unwrap_or_else(|| panic!("{type_name}.{property} declares a reference"))
437 }
438
439 fn proposal() -> SubmittedCharter {
440 SubmittedCharter {
441 target_contract_id: Identifier::from([9u8; 32]),
442 description: "We remove spam and doxing within a day.".to_string(),
443 reasons: vec![Identifier::from([3u8; 32]), Identifier::from([4u8; 32])],
444 moderators_share: None,
445 reward_split: ModerationCharterRewardSplit {
446 leader: 10,
447 equal: 40,
448 actions: 50,
449 },
450 }
451 }
452
453 fn document_with(
454 contract: &DataContract,
455 type_name: &str,
456 properties: std::collections::BTreeMap<String, Value>,
457 ) -> Document {
458 let mut document = document_type(contract, type_name)
459 .random_document(Some(42), PlatformVersion::latest())
460 .expect("a random document");
461 document.set_properties(properties);
462 document
463 }
464
465 fn schema_validation(
466 contract: &DataContract,
467 type_name: &str,
468 document: &Document,
469 ) -> Vec<ConsensusError> {
470 contract
471 .validate_document(type_name, document, PlatformVersion::latest())
472 .expect("validation executes")
473 .errors
474 }
475
476 #[test]
480 fn should_elect_as_many_members_as_the_system_limit_says() {
481 let contract = contract();
482 let members = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
483 .schema()
484 .get_optional_value("properties")
485 .ok()
486 .flatten()
487 .and_then(|properties| properties.get_optional_value("members").ok().flatten())
488 .and_then(|members| {
489 members
490 .get_optional_integer::<u16>("maxItems")
491 .ok()
492 .flatten()
493 })
494 .expect("electedCharter.members declares maxItems");
495 assert_eq!(
496 members,
497 PlatformVersion::latest()
498 .system_limits
499 .max_moderation_charter_elected_members
500 );
501 }
502
503 #[test]
504 fn should_spell_the_same_id_in_the_crate_and_in_dpp() {
505 assert_eq!(
506 moderation_charters_contract::ID,
507 MODERATION_CHARTERS_CONTRACT_ID
508 );
509 assert_eq!(contract().id(), MODERATION_CHARTERS_CONTRACT_ID);
510 }
511
512 #[test]
516 fn should_parse_every_document_type_under_the_published_id() {
517 let contract = contract();
518 let mut names: Vec<&str> = contract
519 .document_types()
520 .keys()
521 .map(String::as_str)
522 .collect();
523 names.sort();
524 assert_eq!(
525 names,
526 vec![
527 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
528 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
529 JOIN_REQUEST_DOCUMENT_TYPE_NAME,
530 REASON_DOCUMENT_TYPE_NAME,
531 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
532 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
533 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
534 ]
535 );
536 for name in names {
537 let document_type = document_type(&contract, name);
538 assert_eq!(
539 document_type.data_contract_id(),
540 MODERATION_CHARTERS_CONTRACT_ID
541 );
542 assert!(!document_type.documents_mutable(), "{name} is immutable");
543 assert_eq!(
547 document_type.documents_can_be_deleted(),
548 [
549 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
550 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
551 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
552 ]
553 .contains(&name),
554 "{name}: only the team changes can be deleted"
555 );
556 }
557 }
558
559 #[test]
560 fn should_declare_only_the_elected_charter_as_a_no_locking_contest() {
561 let contract = contract();
562 for name in [
563 REASON_DOCUMENT_TYPE_NAME,
564 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
565 JOIN_REQUEST_DOCUMENT_TYPE_NAME,
566 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
567 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
568 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
569 ] {
570 assert!(
571 document_type(&contract, name)
572 .find_contested_index()
573 .is_none(),
574 "{name} is not contested"
575 );
576 }
577 let index = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
578 .find_contested_index()
579 .expect("the elected charter type has a contested index");
580 assert_eq!(index.name, "byTargetContract");
581 assert!(index.unique);
582 assert_eq!(
583 index
584 .properties
585 .iter()
586 .map(|p| p.name.as_str())
587 .collect::<Vec<_>>(),
588 vec![property_names::TARGET_CONTRACT_ID]
589 );
590 assert_eq!(
591 index
592 .contested_index
593 .as_ref()
594 .expect("contested")
595 .resolution,
596 ContestedIndexResolution::MasternodeVoteNoLocking
597 );
598 }
599
600 #[test]
601 fn should_let_teams_form_before_the_election_opens() {
602 let contract = contract();
603 let moderation =
604 |type_name| match reference(&contract, type_name, property_names::TARGET_CONTRACT_ID) {
605 PropertyReference::Value(DocumentPropertyReferenceTarget::Contract {
606 contract_requirements,
607 }) => contract_requirements.moderation,
608 other => panic!("{type_name}.targetContractId: {other:?}"),
609 };
610 assert_eq!(
611 moderation(SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME),
612 Some(ContractReferenceModeration::Elected)
613 );
614 assert_eq!(
615 moderation(ELECTED_CHARTER_DOCUMENT_TYPE_NAME),
616 Some(ContractReferenceModeration::ElectionOpen)
617 );
618 }
619
620 #[test]
621 fn should_list_reasons_as_references_to_reason_documents() {
622 let contract = contract();
623 match reference(
624 &contract,
625 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
626 property_names::REASONS,
627 ) {
628 PropertyReference::Elements {
629 target:
630 DocumentPropertyReferenceTarget::PermanentDocument {
631 contract_id: None,
632 document_type_name,
633 ..
634 },
635 max_items,
636 } => {
637 assert_eq!(document_type_name, REASON_DOCUMENT_TYPE_NAME);
638 assert_eq!(max_items, 64);
639 }
640 other => panic!("reasons: {other:?}"),
641 }
642 }
643
644 #[test]
645 fn should_address_a_join_request_to_the_leader_under_bound_keys() {
646 let contract = contract();
647 match reference(
648 &contract,
649 JOIN_REQUEST_DOCUMENT_TYPE_NAME,
650 "submittedCharterId",
651 ) {
652 PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
653 document_type_name,
654 property_agreement,
655 ..
656 }) => {
657 assert_eq!(document_type_name, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
658 assert_eq!(
659 property_agreement.get("recipientId").map(String::as_str),
660 Some("$ownerId"),
661 "the recipient is the proposal's owner"
662 );
663 }
664 other => panic!("submittedCharterId: {other:?}"),
665 }
666 match reference(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME, "recipientId") {
667 PropertyReference::Value(DocumentPropertyReferenceTarget::IdentityPublicKey {
668 key_id_property,
669 key_requirements,
670 }) => {
671 assert_eq!(key_id_property, "recipientKeyId");
672 assert_eq!(key_requirements.purpose, Some(Purpose::DECRYPTION));
673 assert_eq!(
674 key_requirements.bound_to.as_deref(),
675 Some(SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME)
676 );
677 }
678 other => panic!("recipientId: {other:?}"),
679 }
680 match reference(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME, "senderKeyId") {
681 PropertyReference::KeyId(key_reference) => {
682 assert_eq!(
683 key_reference.identity_property,
684 KeyReferenceIdentityProperty::OwnerId
685 );
686 assert_eq!(
687 key_reference.key_requirements.purpose,
688 Some(Purpose::ENCRYPTION)
689 );
690 assert_eq!(
691 key_reference.key_requirements.bound_to.as_deref(),
692 Some(JOIN_REQUEST_DOCUMENT_TYPE_NAME)
693 );
694 }
695 other => panic!("senderKeyId: {other:?}"),
696 }
697 let encrypted_for = document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME)
698 .flattened_properties()
699 .get("encryptedMessage")
700 .and_then(|property| property.encrypted_for.clone())
701 .expect("the message declares its envelope");
702 assert_eq!(
703 encrypted_for.recipient,
704 EncryptedForRecipient::Property("recipientId".to_string())
705 );
706 assert_eq!(encrypted_for.recipient_key, "recipientKeyId");
707 assert_eq!(encrypted_for.sender_key, "senderKeyId");
708 assert_eq!(
709 encrypted_for.scheme,
710 EncryptionScheme::EcdhSecp256k1Aes256Cbc
711 );
712 }
713
714 #[test]
715 fn should_open_the_contest_only_from_the_leaders_own_proposal() {
716 let contract = contract();
717 match reference(
718 &contract,
719 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
720 property_names::SUBMITTED_CHARTER_ID,
721 ) {
722 PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
723 document_type_name,
724 property_agreement,
725 ..
726 }) => {
727 assert_eq!(document_type_name, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
728 assert_eq!(
729 property_agreement.get("$ownerId").map(String::as_str),
730 Some("$ownerId")
731 );
732 assert_eq!(
733 property_agreement
734 .get(property_names::TARGET_CONTRACT_ID)
735 .map(String::as_str),
736 Some(property_names::TARGET_CONTRACT_ID)
737 );
738 }
739 other => panic!("submittedCharterId: {other:?}"),
740 }
741 }
742
743 #[test]
744 fn should_choose_members_only_from_the_proposals_join_requests() {
745 let contract = contract();
746 let members = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
747 .flattened_properties()
748 .get(property_names::MEMBERS)
749 .expect("members")
750 .clone();
751 match members.property_type.reference() {
752 Some(PropertyReference::Elements {
753 target:
754 DocumentPropertyReferenceTarget::PermanentDocumentLookup {
755 document_type_name,
756 lookup,
757 ..
758 },
759 max_items,
760 }) => {
761 assert_eq!(document_type_name, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
762 assert_eq!(max_items, 15);
763 assert_eq!(
764 lookup.resolve_index(
765 document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME).as_ref()
766 ),
767 Ok("bySubmittedCharter".to_string())
768 );
769 assert_eq!(
770 lookup.keys.get(property_names::SUBMITTED_CHARTER_ID),
771 Some(&LookupKeySource::Property(
772 property_names::SUBMITTED_CHARTER_ID.to_string()
773 ))
774 );
775 assert_eq!(
776 lookup.keys.get("$ownerId"),
777 Some(&LookupKeySource::ReferenceValue)
778 );
779 }
780 other => panic!("members: {other:?}"),
781 }
782 let DocumentPropertyType::TypedArray(typed_array) = &members.property_type else {
783 panic!("members is a typed array");
784 };
785 assert!(typed_array.unique_items);
786 assert_eq!(typed_array.min_items.unwrap_or_default(), 0);
787 assert_eq!(
788 members.distinct_from,
789 Some(DistinctFrom::OwnerId),
790 "the leader cannot list themself"
791 );
792 let join_request = document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
793 let index = join_request
794 .indexes()
795 .get("bySubmittedCharter")
796 .expect("the join request's unique index");
797 assert!(index.unique);
798 assert!(
799 !join_request.documents_transferable().is_transferable(),
800 "a lookup may key on $ownerId only on a type that cannot change hands"
801 );
802 }
803
804 #[test]
805 fn should_round_trip_a_proposal_through_the_system_contract() {
806 let contract = contract();
807 let proposal = proposal();
808 let document = document_with(
809 &contract,
810 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
811 proposal.to_document_properties(),
812 );
813 assert_eq!(
814 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
815 vec![],
816 "the encoded proposal passes the schema"
817 );
818 let read = SubmittedCharter::from_document_properties(document.properties())
819 .into_data()
820 .expect("the proposal reads");
821 assert_eq!(read, proposal);
822 }
823
824 #[test]
828 fn should_hold_a_proposal_reward_split_to_one_hundred() {
829 let contract = contract();
830 let submitted_charter = document_type(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
831 assert_eq!(
832 submitted_charter
833 .property_constraints()
834 .keys()
835 .collect::<Vec<_>>(),
836 ["rewardSplitIsWhole"]
837 );
838 let judge = |leader: u8, equal: u8, actions: u8| {
839 let mut proposal = proposal();
840 proposal.reward_split = ModerationCharterRewardSplit {
841 leader,
842 equal,
843 actions,
844 };
845 let document = document_with(
846 &contract,
847 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
848 proposal.to_document_properties(),
849 );
850 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
851 };
852 assert_eq!(judge(10, 40, 50), vec![]);
853 assert_eq!(judge(100, 0, 0), vec![]);
854 for (leader, equal, actions) in [(10, 40, 40), (40, 40, 40), (0, 0, 0)] {
855 let errors = judge(leader, equal, actions);
856 assert!(
857 matches!(
858 errors.as_slice(),
859 [ConsensusError::BasicError(
860 BasicError::DocumentPropertyConstraintViolatedError(e)
861 )] if e.constraint() == "rewardSplitIsWhole"
862 && e.violation() == PropertyConstraintViolation::NotMet
863 ),
864 "{leader} + {equal} + {actions}: {errors:?}"
865 );
866 }
867 }
868
869 #[test]
870 fn should_round_trip_an_elected_charter_through_the_system_contract() {
871 let contract = contract();
872 let charter = ElectedCharter {
873 target_contract_id: Identifier::from([9u8; 32]),
874 submitted_charter_id: Identifier::from([7u8; 32]),
875 members: vec![Identifier::from([2u8; 32]), Identifier::from([5u8; 32])],
876 };
877 let document = document_with(
878 &contract,
879 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
880 charter.to_document_properties(),
881 );
882 assert_eq!(
883 schema_validation(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
884 vec![],
885 "the encoded elected charter passes the schema"
886 );
887 let read = ElectedCharter::from_document_properties(document.properties())
888 .into_data()
889 .expect("the elected charter reads back");
890 assert_eq!(read, charter);
891 }
892
893 #[test]
894 fn should_refuse_through_the_schema_what_it_can_express() {
895 let contract = contract();
896 let too_many =
897 |count: u8| Value::Array((0..count).map(|i| Value::Identifier([i; 32])).collect());
898 for (type_name, property, value, expected_keyword) in [
899 (
900 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
901 property_names::DESCRIPTION,
902 Value::Text("a".repeat(4097)),
903 "maxLength",
904 ),
905 (
906 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
907 property_names::DESCRIPTION,
908 Value::Text(String::new()),
909 "minLength",
910 ),
911 (
912 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
913 property_names::REASONS,
914 too_many(65),
915 "maxItems",
916 ),
917 (
918 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
919 property_names::REASONS,
920 Value::Array(vec![Value::Identifier([3; 32]), Value::Identifier([3; 32])]),
921 "uniqueItems",
922 ),
923 (
924 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
925 property_names::MODERATORS_SHARE,
926 Value::U8(101),
927 "maximum",
928 ),
929 (
930 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
931 property_names::MEMBERS,
932 too_many(16),
933 "maxItems",
934 ),
935 ] {
936 let properties = if type_name == SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME {
937 proposal().to_document_properties()
938 } else {
939 ElectedCharter {
940 target_contract_id: Identifier::from([9u8; 32]),
941 submitted_charter_id: Identifier::from([7u8; 32]),
942 members: vec![],
943 }
944 .to_document_properties()
945 };
946 let mut document = document_with(&contract, type_name, properties);
947 document.set(property, value);
948 let errors = schema_validation(&contract, type_name, &document);
949 let message = format!("{errors:?}");
950 assert!(
951 !errors.is_empty() && message.contains(expected_keyword),
952 "{type_name}.{property}: expected a {expected_keyword} error, got {message}"
953 );
954 }
955 for property in [
956 property_names::TARGET_CONTRACT_ID,
957 property_names::DESCRIPTION,
958 property_names::REASONS,
959 property_names::REWARD_SPLIT,
960 ] {
961 let mut document = document_with(
962 &contract,
963 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
964 proposal().to_document_properties(),
965 );
966 document.properties_mut().remove(property);
967 assert!(
968 !schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
969 .is_empty(),
970 "{property} is required"
971 );
972 }
973 }
974
975 #[test]
979 fn should_refuse_a_description_over_4096_bytes_within_4096_characters() {
980 let contract = contract();
981 assert_eq!(
982 document_type(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME)
983 .flattened_properties()
984 .get(property_names::DESCRIPTION)
985 .expect("the description")
986 .property_type,
987 DocumentPropertyType::String(StringPropertySizes {
988 min_length: Some(1),
989 max_length: Some(4096),
990 max_bytes: Some(4096),
991 })
992 );
993 let with_description = |description: String| {
994 document_with(
995 &contract,
996 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
997 SubmittedCharter {
998 description,
999 ..proposal()
1000 }
1001 .to_document_properties(),
1002 )
1003 };
1004
1005 for description in ["a".repeat(4096), "é".repeat(2048)] {
1007 let document = with_description(description);
1008 assert_eq!(
1009 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
1010 vec![]
1011 );
1012 }
1013
1014 let document = with_description("é".repeat(2049));
1016 assert!(matches!(
1017 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
1018 .as_slice(),
1019 [ConsensusError::BasicError(BasicError::DocumentPropertyMaxBytesExceededError(e))]
1020 if e.property() == property_names::DESCRIPTION
1021 && e.byte_length() == 4098
1022 && e.max_bytes() == 4096
1023 ));
1024 }
1025
1026 #[test]
1030 fn should_let_only_the_leader_change_the_team_and_only_a_member_resign() {
1031 let contract = contract();
1032 let charter_agreement =
1033 |type_name| match reference(&contract, type_name, property_names::ELECTED_CHARTER_ID) {
1034 PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
1035 document_type_name,
1036 property_agreement,
1037 ..
1038 }) => {
1039 assert_eq!(document_type_name, ELECTED_CHARTER_DOCUMENT_TYPE_NAME);
1040 property_agreement.clone()
1041 }
1042 other => panic!("{type_name}.electedCharterId: {other:?}"),
1043 };
1044
1045 let added = charter_agreement(ADDED_MODERATOR_DOCUMENT_TYPE_NAME);
1046 assert_eq!(added.get("$ownerId").map(String::as_str), Some("$ownerId"));
1047 assert_eq!(
1048 added
1049 .get(property_names::SUBMITTED_CHARTER_ID)
1050 .map(String::as_str),
1051 Some(property_names::SUBMITTED_CHARTER_ID)
1052 );
1053 let removed = charter_agreement(REMOVED_MODERATOR_DOCUMENT_TYPE_NAME);
1054 assert_eq!(
1055 removed.get("$ownerId").map(String::as_str),
1056 Some("$ownerId")
1057 );
1058 let resignation = charter_agreement(RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME);
1059 assert_eq!(
1060 resignation.get("recipientId").map(String::as_str),
1061 Some("$ownerId"),
1062 "a resignation is addressed to the charter's leader"
1063 );
1064
1065 match reference(
1066 &contract,
1067 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1068 property_names::MEMBER_ID,
1069 ) {
1070 PropertyReference::Value(
1071 DocumentPropertyReferenceTarget::PermanentDocumentLookup {
1072 document_type_name,
1073 lookup,
1074 ..
1075 },
1076 ) => {
1077 assert_eq!(document_type_name, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
1078 assert_eq!(
1079 lookup.resolve_index(
1080 document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME).as_ref()
1081 ),
1082 Ok("bySubmittedCharter".to_string())
1083 );
1084 assert_eq!(
1085 lookup.keys.get("$ownerId"),
1086 Some(&LookupKeySource::ReferenceValue)
1087 );
1088 }
1089 other => panic!("addedModerator.memberId: {other:?}"),
1090 }
1091 match reference(
1094 &contract,
1095 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1096 property_names::MEMBER_ID,
1097 ) {
1098 PropertyReference::Value(DocumentPropertyReferenceTarget::ListElement(listed)) => {
1099 assert_eq!(
1100 listed.document_type_name,
1101 ELECTED_CHARTER_DOCUMENT_TYPE_NAME
1102 );
1103 assert_eq!(listed.in_list, property_names::MEMBERS);
1104 assert_eq!(
1105 listed.document_id_property(),
1106 Some(property_names::ELECTED_CHARTER_ID)
1107 );
1108 }
1109 other => panic!("removedModerator.memberId: {other:?}"),
1110 }
1111 for type_name in [
1112 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1113 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1114 ] {
1115 let member = document_type(&contract, type_name)
1116 .flattened_properties()
1117 .get(property_names::MEMBER_ID)
1118 .expect("memberId");
1119 assert_eq!(
1120 member.distinct_from,
1121 Some(DistinctFrom::OwnerId),
1122 "{type_name}: the leader is not a member"
1123 );
1124 }
1125
1126 for (type_name, index_name, member_property) in [
1127 (
1128 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1129 "byElectedCharterMember",
1130 property_names::MEMBER_ID,
1131 ),
1132 (
1133 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1134 "byElectedCharterMember",
1135 property_names::MEMBER_ID,
1136 ),
1137 (
1138 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
1139 "byElectedCharterOwner",
1140 "$ownerId",
1141 ),
1142 ] {
1143 let index = document_type(&contract, type_name)
1144 .indexes()
1145 .get(index_name)
1146 .unwrap_or_else(|| panic!("{type_name}.{index_name}"));
1147 assert!(index.unique, "{type_name}: once per member and charter");
1148 assert_eq!(
1149 index
1150 .properties
1151 .iter()
1152 .map(|p| p.name.as_str())
1153 .collect::<Vec<_>>(),
1154 vec![property_names::ELECTED_CHARTER_ID, member_property]
1155 );
1156 }
1157 }
1158
1159 #[test]
1164 fn should_let_only_a_team_member_ask_to_leave() {
1165 let contract = contract();
1166 let resignation = document_type(&contract, RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME);
1167 assert!(resignation.documents_can_be_deleted());
1168
1169 let Some(DocumentPropertyReferenceTarget::AnyOf(operands)) = resignation.owner_reference()
1170 else {
1171 panic!(
1172 "the writer must meet any of the membership targets: {:?}",
1173 resignation.owner_reference()
1174 );
1175 };
1176 match operands.operands() {
1177 [DocumentPropertyReferenceTarget::ListElement(listed), DocumentPropertyReferenceTarget::DeletableDocumentLookup {
1178 document_type_name,
1179 lookup,
1180 ..
1181 }] => {
1182 assert_eq!(
1183 listed.document_type_name,
1184 ELECTED_CHARTER_DOCUMENT_TYPE_NAME
1185 );
1186 assert_eq!(listed.in_list, property_names::MEMBERS);
1187 assert_eq!(
1188 listed.document_id_property(),
1189 Some(property_names::ELECTED_CHARTER_ID)
1190 );
1191 assert_eq!(document_type_name, ADDED_MODERATOR_DOCUMENT_TYPE_NAME);
1192 assert_eq!(
1193 lookup.resolve_index(
1194 document_type(&contract, ADDED_MODERATOR_DOCUMENT_TYPE_NAME).as_ref()
1195 ),
1196 Ok("byElectedCharterMember".to_string())
1197 );
1198 assert_eq!(
1199 lookup.keys.get(property_names::MEMBER_ID),
1200 Some(&LookupKeySource::ReferenceValue)
1201 );
1202 }
1203 other => panic!("resignation membership operands: {other:?}"),
1204 }
1205
1206 let encrypted_for = resignation
1207 .flattened_properties()
1208 .get("encryptedMessage")
1209 .and_then(|property| property.encrypted_for.clone())
1210 .expect("the message declares its envelope");
1211 assert_eq!(
1212 encrypted_for.recipient,
1213 EncryptedForRecipient::Property("recipientId".to_string())
1214 );
1215 match reference(
1216 &contract,
1217 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
1218 "senderKeyId",
1219 ) {
1220 PropertyReference::KeyId(key_reference) => {
1221 assert_eq!(
1222 key_reference.key_requirements.bound_to.as_deref(),
1223 Some(JOIN_REQUEST_DOCUMENT_TYPE_NAME),
1224 "the member's encryption key is the one its join request used"
1225 );
1226 }
1227 other => panic!("senderKeyId: {other:?}"),
1228 }
1229 }
1230}
1231
1232#[cfg(test)]
1233mod dpns_v3_tests {
1234 use super::*;
1235 use crate::data_contract::accessors::v0::DataContractV0Getters;
1236 use crate::data_contract::document_type::accessors::{
1237 DocumentTypeV0Getters, DocumentTypeV2Getters,
1238 };
1239 use crate::data_contract::document_type::property_constraints::{
1240 AggregateBinding, AggregateKind, AggregateRead, ConstraintComparison, ConstraintExpression,
1241 EqualityKind, PropertyConstraint,
1242 };
1243 use crate::data_contract::document_type::{
1244 ChooseFirstOrder, DocumentReferenceKind, DocumentTypeRef, HashFunction, LookupChooseFirst,
1245 LookupHashKey, LookupKeyParam, LookupKeySource,
1246 };
1247 use crate::util::hash::hash_double;
1248 use platform_value::{Identifier, Value};
1249 use std::collections::BTreeMap;
1250
1251 fn dpns() -> DataContract {
1253 load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest())
1254 .expect("DPNS v3 registers under full validation")
1255 }
1256
1257 fn document_type<'a>(contract: &'a DataContract, name: &str) -> DocumentTypeRef<'a> {
1258 contract
1259 .document_type_for_name(name)
1260 .unwrap_or_else(|_| panic!("the {name} type"))
1261 }
1262
1263 fn name_params() -> Vec<LookupKeyParam> {
1264 vec![
1265 LookupKeyParam::Property("normalizedLabel".to_string()),
1266 LookupKeyParam::Const(".".to_string()),
1267 LookupKeyParam::Property("normalizedParentDomainName".to_string()),
1268 ]
1269 }
1270
1271 #[test]
1276 fn should_find_the_creators_own_preorder_chosen_first_among_those_of_the_name() {
1277 let contract = dpns();
1278 let domain = document_type(&contract, "domain");
1279 let declaration = domain
1280 .creator_reference()
1281 .and_then(|target| target.as_any_document_reference())
1282 .expect("the domain's creatorRefersTo is a document reference");
1283 assert_eq!(declaration.document_type_name, "preorder");
1284 assert_eq!(declaration.kind, DocumentReferenceKind::Deletable);
1285 let lookup = declaration.lookup.expect("found by findBy");
1286 assert_eq!(lookup.keys.get("$ownerId"), Some(&LookupKeySource::OwnerId));
1287 assert_eq!(
1288 lookup.keys.get("nameHash"),
1289 Some(&LookupKeySource::Hash(LookupHashKey {
1290 function: HashFunction::Sha256d,
1291 params: name_params(),
1292 }))
1293 );
1294 assert_eq!(lookup.minimum_age_blocks, Some(1));
1295 assert_eq!(
1296 lookup.choose_first,
1297 Some(LookupChooseFirst {
1298 among: "nameHash".to_string(),
1299 valid_when_property: "ownerNameHash".to_string(),
1300 valid_when: LookupHashKey {
1301 function: HashFunction::Sha256d,
1302 params: [vec![LookupKeyParam::Writer], name_params()].concat(),
1303 },
1304 by: vec![ChooseFirstOrder {
1305 property: "$createdAtBlockHeight".to_string(),
1306 ascending: true,
1307 }],
1308 })
1309 );
1310 assert!(lookup.consume);
1311
1312 let salt = domain
1313 .flattened_properties()
1314 .get("preorderSalt")
1315 .expect("preorderSalt stays declared");
1316 assert!(salt.transient);
1317 assert!(salt.property_type.reference().is_none());
1318 assert!(salt.revealed_reference.is_none());
1319 assert!(!domain.required_fields().contains("preorderSalt"));
1320 }
1321
1322 #[test]
1326 fn should_hash_the_normalized_name_and_the_creator_with_it() {
1327 let contract = dpns();
1328 let domain = document_type(&contract, "domain");
1329 let lookup = domain
1330 .creator_reference()
1331 .and_then(|target| target.as_any_document_reference())
1332 .and_then(|declaration| declaration.lookup)
1333 .expect("the creator reference is found by findBy");
1334 let name_key = domain
1335 .creator_reference()
1336 .and_then(|target| target.lookup_hash_key())
1337 .expect("the creator reference holds a findBy function");
1338 let owner_name_key = &lookup
1339 .choose_first
1340 .as_ref()
1341 .expect("the creator reference chooses the first preorder")
1342 .valid_when;
1343 let creator = Identifier::from([3; 32]);
1344 let data = BTreeMap::from([
1345 (
1346 "normalizedLabel".to_string(),
1347 Value::Text("b0b".to_string()),
1348 ),
1349 (
1350 "normalizedParentDomainName".to_string(),
1351 Value::Text("dash".to_string()),
1352 ),
1353 ]);
1354
1355 let (name_hash, _) = name_key
1356 .key_value(domain, None, creator, &data)
1357 .expect("the name preimage assembles");
1358 let (owner_name_hash, _) = owner_name_key
1359 .key_value(domain, None, creator, &data)
1360 .expect("the owner and name preimage assembles");
1361
1362 assert_eq!(name_hash, hash_double(b"b0b.dash"));
1363 let mut preimage = creator.to_buffer().to_vec();
1364 preimage.extend(b"b0b.dash");
1365 assert_eq!(owner_name_hash, hash_double(preimage));
1366 }
1367
1368 #[test]
1371 fn should_expire_preorders_after_an_hour_and_index_them_by_name_hash() {
1372 let contract = dpns();
1373 let preorder = document_type(&contract, "preorder");
1374 assert_eq!(preorder.documents_ttl_seconds(), Some(3600));
1375
1376 let index_properties = |name: &str| {
1377 let index = preorder
1378 .indexes()
1379 .get(name)
1380 .unwrap_or_else(|| panic!("the {name} index"));
1381 (
1382 index
1383 .properties
1384 .iter()
1385 .map(|property| property.name.clone())
1386 .collect::<Vec<_>>(),
1387 index.unique,
1388 )
1389 };
1390 assert_eq!(
1391 index_properties("ownerAndNameHash"),
1392 (vec!["$ownerId".to_string(), "nameHash".to_string()], true)
1393 );
1394 assert_eq!(
1395 index_properties("nameHashAndHeight"),
1396 (
1397 vec!["nameHash".to_string(), "$createdAtBlockHeight".to_string()],
1398 false
1399 )
1400 );
1401 }
1402
1403 #[test]
1407 fn should_hold_a_name_to_the_preorders_a_reveal_reads() {
1408 let contract = dpns();
1409 let preorder = document_type(&contract, "preorder");
1410 let index = preorder
1411 .indexes()
1412 .get("nameHash")
1413 .expect("the nameHash index");
1414 assert!(index.countable.is_countable());
1415 assert!(!index.unique);
1416
1417 let cap = preorder
1418 .property_constraints()
1419 .get("atMostAThousandPerName")
1420 .expect("the per-name preorder cap");
1421 let by_name_hash = AggregateRead {
1422 kind: AggregateKind::Count,
1423 document_type: "preorder".to_string(),
1424 filter: [(
1425 "nameHash".to_string(),
1426 AggregateBinding::Property {
1427 path: "nameHash".to_string(),
1428 kind: Some(EqualityKind::Bytes),
1429 },
1430 )]
1431 .into(),
1432 of_own_type: true,
1433 };
1434 assert_eq!(
1435 cap,
1436 &PropertyConstraint::Compare {
1437 comparison: ConstraintComparison::LessThanOrEqual,
1438 left: ConstraintExpression::Aggregate(by_name_hash.clone()),
1439 right: ConstraintExpression::Value(i128::from(
1440 PlatformVersion::latest()
1441 .system_limits
1442 .max_choose_first_candidates
1443 )),
1444 }
1445 );
1446 assert_eq!(
1447 by_name_hash
1448 .answering_index(&preorder)
1449 .map(|index| index.name.as_str()),
1450 Some("nameHash")
1451 );
1452 }
1453}