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 #[test]
154 fn test_load_system_data_contract_v8_vs_v9() {
155 let contract_1 = load_system_data_contract(
156 SystemDataContract::TokenHistory,
157 PlatformVersion::get(8).unwrap(),
158 )
159 .expect("data_contract");
160 let contract_2 = load_system_data_contract(
161 SystemDataContract::TokenHistory,
162 PlatformVersion::get(9).unwrap(),
163 )
164 .expect("data_contract");
165 assert_ne!(contract_1, contract_2);
166 }
167
168 #[test]
169 fn serialize_withdrawal_contract_v1_vs_v9() {
170 let contract_1 = load_system_data_contract(
171 SystemDataContract::Withdrawals,
172 PlatformVersion::get(1).unwrap(),
173 )
174 .expect("data_contract");
175 let contract_2 = load_system_data_contract(
176 SystemDataContract::Withdrawals,
177 PlatformVersion::get(9).unwrap(),
178 )
179 .expect("data_contract");
180
181 assert_ne!(contract_1, contract_2);
182 let v1_ser: DataContractInSerializationFormat = contract_1
183 .clone()
184 .try_into_platform_versioned(PlatformVersion::get(1).unwrap())
185 .expect("expected to serialize");
186 let v2_ser: DataContractInSerializationFormat = contract_2
187 .clone()
188 .try_into_platform_versioned(PlatformVersion::get(1).unwrap())
189 .expect("expected to serialize");
190 assert_eq!(v1_ser, v2_ser);
191
192 let v1_bytes = contract_1
193 .serialize_to_bytes_with_platform_version(PlatformVersion::get(1).unwrap())
194 .expect("expected to serialize");
195 let v8_bytes = contract_1
196 .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
197 .expect("expected to serialize");
198 let v9_bytes = contract_1
199 .serialize_to_bytes_with_platform_version(PlatformVersion::get(9).unwrap())
200 .expect("expected to serialize");
201 assert_eq!(v1_bytes.len(), 1747);
202 assert_eq!(v8_bytes.len(), 1747);
203 assert_eq!(v9_bytes.len(), 1757); let v1_bytes = contract_2
206 .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
207 .expect("expected to serialize");
208 let v8_bytes = contract_2
209 .serialize_to_bytes_with_platform_version(PlatformVersion::get(8).unwrap())
210 .expect("expected to serialize");
211 let v9_bytes = contract_2
212 .serialize_to_bytes_with_platform_version(PlatformVersion::get(9).unwrap())
213 .expect("expected to serialize");
214 assert_eq!(v1_bytes.len(), 1747);
215 assert_eq!(v8_bytes.len(), 1747);
216 assert_eq!(v9_bytes.len(), 1758); }
218}
219
220#[cfg(all(test, feature = "app-connect-contract", feature = "validation"))]
221mod app_connect_tests {
222 use super::*;
223 use crate::data_contract::accessors::v0::DataContractV0Getters;
224 use crate::data_contract::document_type::random_document::CreateRandomDocument;
225 use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0;
226 use crate::document::{Document, DocumentV0Getters, DocumentV0Setters};
227 use platform_value::Value;
228
229 fn response(contract: &DataContract) -> Document {
230 let mut document = contract
231 .document_type_for_name("loginKeyResponse")
232 .expect("response type")
233 .random_document(Some(42), PlatformVersion::latest())
234 .expect("response document");
235 document.set_properties(BTreeMap::from([
236 (
237 "appEphemeralPubKeyHash".into(),
238 Value::Bytes(vec![0x11; 20]),
239 ),
240 ("walletEphemeralPubKey".into(), Value::Bytes(vec![0x22; 33])),
241 ("encryptedPayload".into(), Value::Bytes(vec![0x33; 60])),
242 ]));
243 document
244 }
245
246 #[test]
247 fn should_validate_app_connect_response_lengths() {
248 let platform_version = PlatformVersion::latest();
249 let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version)
250 .expect("system contract");
251 for (property, length, expected) in [
252 ("appEphemeralPubKeyHash", 19, false),
253 ("appEphemeralPubKeyHash", 20, true),
254 ("appEphemeralPubKeyHash", 21, false),
255 ("walletEphemeralPubKey", 32, false),
256 ("walletEphemeralPubKey", 33, true),
257 ("walletEphemeralPubKey", 34, false),
258 ("encryptedPayload", 59, false),
259 ("encryptedPayload", 60, true),
260 ("encryptedPayload", 572, true),
261 ("encryptedPayload", 573, false),
262 ] {
263 let mut document = response(&contract);
264 document.set(property, Value::Bytes(vec![0x44; length]));
265 let result = contract
266 .validate_document("loginKeyResponse", &document, platform_version)
267 .expect("validation executes");
268 assert_eq!(
269 result.is_valid(),
270 expected,
271 "{property}: {length} bytes: {result:?}"
272 );
273 }
274 }
275
276 #[test]
277 fn should_require_only_the_three_app_connect_response_properties() {
278 let platform_version = PlatformVersion::latest();
279 let contract = load_system_data_contract(SystemDataContract::AppConnect, platform_version)
280 .expect("system contract");
281 assert_eq!(contract.document_types().len(), 1);
282 for property in [
283 "appEphemeralPubKeyHash",
284 "walletEphemeralPubKey",
285 "encryptedPayload",
286 ] {
287 let mut document = response(&contract);
288 document.properties_mut().remove(property);
289 assert!(
290 !contract
291 .validate_document("loginKeyResponse", &document, platform_version)
292 .expect("validation executes")
293 .is_valid(),
294 "{property} is required"
295 );
296 }
297 let mut document = response(&contract);
298 document.set("contractId", Value::Bytes(vec![0x55; 32]));
299 assert!(
300 !contract
301 .validate_document("loginKeyResponse", &document, platform_version)
302 .expect("validation executes")
303 .is_valid(),
304 "the former contractId field is not admitted"
305 );
306 }
307}
308
309#[cfg(all(test, feature = "moderation-charters-contract", feature = "validation"))]
310mod moderation_charters_tests {
311 use super::*;
312 use crate::consensus::basic::document::PropertyConstraintViolation;
313 use crate::consensus::basic::BasicError;
314 use crate::consensus::ConsensusError;
315 use crate::data_contract::accessors::v0::DataContractV0Getters;
316 use crate::data_contract::document_type::accessors::{
317 DocumentTypeV0Getters, DocumentTypeV2Getters,
318 };
319 use crate::data_contract::document_type::random_document::CreateRandomDocument;
320 use crate::data_contract::document_type::{
321 ContestedIndexResolution, ContractReferenceModeration, DistinctFrom,
322 DocumentPropertyReferenceTarget, DocumentPropertyType, DocumentType, EncryptedForRecipient,
323 EncryptionScheme, KeyReferenceIdentityProperty, LookupKeySource, PropertyReference,
324 StringPropertySizes,
325 };
326 use crate::data_contract::validate_document::DataContractDocumentValidationMethodsV0;
327 use crate::document::{Document, DocumentV0Getters, DocumentV0Setters};
328 use crate::identity::Purpose;
329 use crate::moderation_charter::{
330 property_names, ElectedCharter, ModerationCharterRewardSplit, SubmittedCharter,
331 ADDED_MODERATOR_DOCUMENT_TYPE_NAME, ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
332 JOIN_REQUEST_DOCUMENT_TYPE_NAME, MODERATION_CHARTERS_CONTRACT_ID,
333 REASON_DOCUMENT_TYPE_NAME, REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
334 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
335 };
336 use platform_value::{Identifier, Value};
337
338 fn contract() -> DataContract {
339 load_system_data_contract(
340 SystemDataContract::ModerationCharters,
341 PlatformVersion::latest(),
342 )
343 .expect("the moderation charters contract loads")
344 }
345
346 fn document_type<'a>(contract: &'a DataContract, name: &str) -> &'a DocumentType {
347 contract
348 .document_types()
349 .get(name)
350 .unwrap_or_else(|| panic!("the {name} type"))
351 }
352
353 fn reference<'a>(
354 contract: &'a DataContract,
355 type_name: &str,
356 property: &str,
357 ) -> PropertyReference<'a> {
358 document_type(contract, type_name)
359 .flattened_properties()
360 .get(property)
361 .unwrap_or_else(|| panic!("{type_name}.{property}"))
362 .property_type
363 .reference()
364 .unwrap_or_else(|| panic!("{type_name}.{property} declares a reference"))
365 }
366
367 fn proposal() -> SubmittedCharter {
368 SubmittedCharter {
369 target_contract_id: Identifier::from([9u8; 32]),
370 description: "We remove spam and doxing within a day.".to_string(),
371 reasons: vec![Identifier::from([3u8; 32]), Identifier::from([4u8; 32])],
372 moderators_share: None,
373 reward_split: ModerationCharterRewardSplit {
374 leader: 10,
375 equal: 40,
376 actions: 50,
377 },
378 }
379 }
380
381 fn document_with(
382 contract: &DataContract,
383 type_name: &str,
384 properties: std::collections::BTreeMap<String, Value>,
385 ) -> Document {
386 let mut document = document_type(contract, type_name)
387 .random_document(Some(42), PlatformVersion::latest())
388 .expect("a random document");
389 document.set_properties(properties);
390 document
391 }
392
393 fn schema_validation(
394 contract: &DataContract,
395 type_name: &str,
396 document: &Document,
397 ) -> Vec<ConsensusError> {
398 contract
399 .validate_document(type_name, document, PlatformVersion::latest())
400 .expect("validation executes")
401 .errors
402 }
403
404 #[test]
408 fn should_elect_as_many_members_as_the_system_limit_says() {
409 let contract = contract();
410 let members = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
411 .schema()
412 .get_optional_value("properties")
413 .ok()
414 .flatten()
415 .and_then(|properties| properties.get_optional_value("members").ok().flatten())
416 .and_then(|members| {
417 members
418 .get_optional_integer::<u16>("maxItems")
419 .ok()
420 .flatten()
421 })
422 .expect("electedCharter.members declares maxItems");
423 assert_eq!(
424 members,
425 PlatformVersion::latest()
426 .system_limits
427 .max_moderation_charter_elected_members
428 );
429 }
430
431 #[test]
432 fn should_spell_the_same_id_in_the_crate_and_in_dpp() {
433 assert_eq!(
434 moderation_charters_contract::ID,
435 MODERATION_CHARTERS_CONTRACT_ID
436 );
437 assert_eq!(contract().id(), MODERATION_CHARTERS_CONTRACT_ID);
438 }
439
440 #[test]
444 fn should_parse_every_document_type_under_the_published_id() {
445 let contract = contract();
446 let mut names: Vec<&str> = contract
447 .document_types()
448 .keys()
449 .map(String::as_str)
450 .collect();
451 names.sort();
452 assert_eq!(
453 names,
454 vec![
455 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
456 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
457 JOIN_REQUEST_DOCUMENT_TYPE_NAME,
458 REASON_DOCUMENT_TYPE_NAME,
459 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
460 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
461 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
462 ]
463 );
464 for name in names {
465 let document_type = document_type(&contract, name);
466 assert_eq!(
467 document_type.data_contract_id(),
468 MODERATION_CHARTERS_CONTRACT_ID
469 );
470 assert!(!document_type.documents_mutable(), "{name} is immutable");
471 assert_eq!(
475 document_type.documents_can_be_deleted(),
476 [
477 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
478 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
479 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
480 ]
481 .contains(&name),
482 "{name}: only the team changes can be deleted"
483 );
484 }
485 }
486
487 #[test]
488 fn should_declare_only_the_elected_charter_as_a_no_locking_contest() {
489 let contract = contract();
490 for name in [
491 REASON_DOCUMENT_TYPE_NAME,
492 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
493 JOIN_REQUEST_DOCUMENT_TYPE_NAME,
494 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
495 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
496 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
497 ] {
498 assert!(
499 document_type(&contract, name)
500 .find_contested_index()
501 .is_none(),
502 "{name} is not contested"
503 );
504 }
505 let index = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
506 .find_contested_index()
507 .expect("the elected charter type has a contested index");
508 assert_eq!(index.name, "byTargetContract");
509 assert!(index.unique);
510 assert_eq!(
511 index
512 .properties
513 .iter()
514 .map(|p| p.name.as_str())
515 .collect::<Vec<_>>(),
516 vec![property_names::TARGET_CONTRACT_ID]
517 );
518 assert_eq!(
519 index
520 .contested_index
521 .as_ref()
522 .expect("contested")
523 .resolution,
524 ContestedIndexResolution::MasternodeVoteNoLocking
525 );
526 }
527
528 #[test]
529 fn should_let_teams_form_before_the_election_opens() {
530 let contract = contract();
531 let moderation =
532 |type_name| match reference(&contract, type_name, property_names::TARGET_CONTRACT_ID) {
533 PropertyReference::Value(DocumentPropertyReferenceTarget::Contract {
534 contract_requirements,
535 }) => contract_requirements.moderation,
536 other => panic!("{type_name}.targetContractId: {other:?}"),
537 };
538 assert_eq!(
539 moderation(SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME),
540 Some(ContractReferenceModeration::Elected)
541 );
542 assert_eq!(
543 moderation(ELECTED_CHARTER_DOCUMENT_TYPE_NAME),
544 Some(ContractReferenceModeration::ElectionOpen)
545 );
546 }
547
548 #[test]
549 fn should_list_reasons_as_references_to_reason_documents() {
550 let contract = contract();
551 match reference(
552 &contract,
553 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
554 property_names::REASONS,
555 ) {
556 PropertyReference::Elements {
557 target:
558 DocumentPropertyReferenceTarget::PermanentDocument {
559 contract_id: None,
560 document_type_name,
561 ..
562 },
563 max_items,
564 } => {
565 assert_eq!(document_type_name, REASON_DOCUMENT_TYPE_NAME);
566 assert_eq!(max_items, 64);
567 }
568 other => panic!("reasons: {other:?}"),
569 }
570 }
571
572 #[test]
573 fn should_address_a_join_request_to_the_leader_under_bound_keys() {
574 let contract = contract();
575 match reference(
576 &contract,
577 JOIN_REQUEST_DOCUMENT_TYPE_NAME,
578 "submittedCharterId",
579 ) {
580 PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
581 document_type_name,
582 property_agreement,
583 ..
584 }) => {
585 assert_eq!(document_type_name, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
586 assert_eq!(
587 property_agreement.get("recipientId").map(String::as_str),
588 Some("$ownerId"),
589 "the recipient is the proposal's owner"
590 );
591 }
592 other => panic!("submittedCharterId: {other:?}"),
593 }
594 match reference(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME, "recipientId") {
595 PropertyReference::Value(DocumentPropertyReferenceTarget::IdentityPublicKey {
596 key_id_property,
597 key_requirements,
598 }) => {
599 assert_eq!(key_id_property, "recipientKeyId");
600 assert_eq!(key_requirements.purpose, Some(Purpose::DECRYPTION));
601 assert_eq!(
602 key_requirements.bound_to.as_deref(),
603 Some(SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME)
604 );
605 }
606 other => panic!("recipientId: {other:?}"),
607 }
608 match reference(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME, "senderKeyId") {
609 PropertyReference::KeyId(key_reference) => {
610 assert_eq!(
611 key_reference.identity_property,
612 KeyReferenceIdentityProperty::OwnerId
613 );
614 assert_eq!(
615 key_reference.key_requirements.purpose,
616 Some(Purpose::ENCRYPTION)
617 );
618 assert_eq!(
619 key_reference.key_requirements.bound_to.as_deref(),
620 Some(JOIN_REQUEST_DOCUMENT_TYPE_NAME)
621 );
622 }
623 other => panic!("senderKeyId: {other:?}"),
624 }
625 let encrypted_for = document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME)
626 .flattened_properties()
627 .get("encryptedMessage")
628 .and_then(|property| property.encrypted_for.clone())
629 .expect("the message declares its envelope");
630 assert_eq!(
631 encrypted_for.recipient,
632 EncryptedForRecipient::Property("recipientId".to_string())
633 );
634 assert_eq!(encrypted_for.recipient_key, "recipientKeyId");
635 assert_eq!(encrypted_for.sender_key, "senderKeyId");
636 assert_eq!(
637 encrypted_for.scheme,
638 EncryptionScheme::EcdhSecp256k1Aes256Cbc
639 );
640 }
641
642 #[test]
643 fn should_open_the_contest_only_from_the_leaders_own_proposal() {
644 let contract = contract();
645 match reference(
646 &contract,
647 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
648 property_names::SUBMITTED_CHARTER_ID,
649 ) {
650 PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
651 document_type_name,
652 property_agreement,
653 ..
654 }) => {
655 assert_eq!(document_type_name, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
656 assert_eq!(
657 property_agreement.get("$ownerId").map(String::as_str),
658 Some("$ownerId")
659 );
660 assert_eq!(
661 property_agreement
662 .get(property_names::TARGET_CONTRACT_ID)
663 .map(String::as_str),
664 Some(property_names::TARGET_CONTRACT_ID)
665 );
666 }
667 other => panic!("submittedCharterId: {other:?}"),
668 }
669 }
670
671 #[test]
672 fn should_choose_members_only_from_the_proposals_join_requests() {
673 let contract = contract();
674 let members = document_type(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME)
675 .flattened_properties()
676 .get(property_names::MEMBERS)
677 .expect("members")
678 .clone();
679 match members.property_type.reference() {
680 Some(PropertyReference::Elements {
681 target:
682 DocumentPropertyReferenceTarget::PermanentDocumentLookup {
683 document_type_name,
684 lookup,
685 ..
686 },
687 max_items,
688 }) => {
689 assert_eq!(document_type_name, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
690 assert_eq!(max_items, 15);
691 assert_eq!(
692 lookup.resolve_index(
693 document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME).as_ref()
694 ),
695 Ok("bySubmittedCharter".to_string())
696 );
697 assert_eq!(
698 lookup.keys.get(property_names::SUBMITTED_CHARTER_ID),
699 Some(&LookupKeySource::Property(
700 property_names::SUBMITTED_CHARTER_ID.to_string()
701 ))
702 );
703 assert_eq!(
704 lookup.keys.get("$ownerId"),
705 Some(&LookupKeySource::ReferenceValue)
706 );
707 }
708 other => panic!("members: {other:?}"),
709 }
710 let DocumentPropertyType::TypedArray(typed_array) = &members.property_type else {
711 panic!("members is a typed array");
712 };
713 assert!(typed_array.unique_items);
714 assert_eq!(typed_array.min_items.unwrap_or_default(), 0);
715 assert_eq!(
716 members.distinct_from,
717 Some(DistinctFrom::OwnerId),
718 "the leader cannot list themself"
719 );
720 let join_request = document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
721 let index = join_request
722 .indexes()
723 .get("bySubmittedCharter")
724 .expect("the join request's unique index");
725 assert!(index.unique);
726 assert!(
727 !join_request.documents_transferable().is_transferable(),
728 "a lookup may key on $ownerId only on a type that cannot change hands"
729 );
730 }
731
732 #[test]
733 fn should_round_trip_a_proposal_through_the_system_contract() {
734 let contract = contract();
735 let proposal = proposal();
736 let document = document_with(
737 &contract,
738 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
739 proposal.to_document_properties(),
740 );
741 assert_eq!(
742 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
743 vec![],
744 "the encoded proposal passes the schema"
745 );
746 let read = SubmittedCharter::from_document_properties(document.properties())
747 .into_data()
748 .expect("the proposal reads");
749 assert_eq!(read, proposal);
750 }
751
752 #[test]
756 fn should_hold_a_proposal_reward_split_to_one_hundred() {
757 let contract = contract();
758 let submitted_charter = document_type(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME);
759 assert_eq!(
760 submitted_charter
761 .property_constraints()
762 .keys()
763 .collect::<Vec<_>>(),
764 ["rewardSplitIsWhole"]
765 );
766 let judge = |leader: u8, equal: u8, actions: u8| {
767 let mut proposal = proposal();
768 proposal.reward_split = ModerationCharterRewardSplit {
769 leader,
770 equal,
771 actions,
772 };
773 let document = document_with(
774 &contract,
775 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
776 proposal.to_document_properties(),
777 );
778 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
779 };
780 assert_eq!(judge(10, 40, 50), vec![]);
781 assert_eq!(judge(100, 0, 0), vec![]);
782 for (leader, equal, actions) in [(10, 40, 40), (40, 40, 40), (0, 0, 0)] {
783 let errors = judge(leader, equal, actions);
784 assert!(
785 matches!(
786 errors.as_slice(),
787 [ConsensusError::BasicError(
788 BasicError::DocumentPropertyConstraintViolatedError(e)
789 )] if e.constraint() == "rewardSplitIsWhole"
790 && e.violation() == PropertyConstraintViolation::NotMet
791 ),
792 "{leader} + {equal} + {actions}: {errors:?}"
793 );
794 }
795 }
796
797 #[test]
798 fn should_round_trip_an_elected_charter_through_the_system_contract() {
799 let contract = contract();
800 let charter = ElectedCharter {
801 target_contract_id: Identifier::from([9u8; 32]),
802 submitted_charter_id: Identifier::from([7u8; 32]),
803 members: vec![Identifier::from([2u8; 32]), Identifier::from([5u8; 32])],
804 };
805 let document = document_with(
806 &contract,
807 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
808 charter.to_document_properties(),
809 );
810 assert_eq!(
811 schema_validation(&contract, ELECTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
812 vec![],
813 "the encoded elected charter passes the schema"
814 );
815 let read = ElectedCharter::from_document_properties(document.properties())
816 .into_data()
817 .expect("the elected charter reads back");
818 assert_eq!(read, charter);
819 }
820
821 #[test]
822 fn should_refuse_through_the_schema_what_it_can_express() {
823 let contract = contract();
824 let too_many =
825 |count: u8| Value::Array((0..count).map(|i| Value::Identifier([i; 32])).collect());
826 for (type_name, property, value, expected_keyword) in [
827 (
828 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
829 property_names::DESCRIPTION,
830 Value::Text("a".repeat(4097)),
831 "maxLength",
832 ),
833 (
834 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
835 property_names::DESCRIPTION,
836 Value::Text(String::new()),
837 "minLength",
838 ),
839 (
840 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
841 property_names::REASONS,
842 too_many(65),
843 "maxItems",
844 ),
845 (
846 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
847 property_names::REASONS,
848 Value::Array(vec![Value::Identifier([3; 32]), Value::Identifier([3; 32])]),
849 "uniqueItems",
850 ),
851 (
852 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
853 property_names::MODERATORS_SHARE,
854 Value::U8(101),
855 "maximum",
856 ),
857 (
858 ELECTED_CHARTER_DOCUMENT_TYPE_NAME,
859 property_names::MEMBERS,
860 too_many(16),
861 "maxItems",
862 ),
863 ] {
864 let properties = if type_name == SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME {
865 proposal().to_document_properties()
866 } else {
867 ElectedCharter {
868 target_contract_id: Identifier::from([9u8; 32]),
869 submitted_charter_id: Identifier::from([7u8; 32]),
870 members: vec![],
871 }
872 .to_document_properties()
873 };
874 let mut document = document_with(&contract, type_name, properties);
875 document.set(property, value);
876 let errors = schema_validation(&contract, type_name, &document);
877 let message = format!("{errors:?}");
878 assert!(
879 !errors.is_empty() && message.contains(expected_keyword),
880 "{type_name}.{property}: expected a {expected_keyword} error, got {message}"
881 );
882 }
883 for property in [
884 property_names::TARGET_CONTRACT_ID,
885 property_names::DESCRIPTION,
886 property_names::REASONS,
887 property_names::REWARD_SPLIT,
888 ] {
889 let mut document = document_with(
890 &contract,
891 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
892 proposal().to_document_properties(),
893 );
894 document.properties_mut().remove(property);
895 assert!(
896 !schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
897 .is_empty(),
898 "{property} is required"
899 );
900 }
901 }
902
903 #[test]
907 fn should_refuse_a_description_over_4096_bytes_within_4096_characters() {
908 let contract = contract();
909 assert_eq!(
910 document_type(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME)
911 .flattened_properties()
912 .get(property_names::DESCRIPTION)
913 .expect("the description")
914 .property_type,
915 DocumentPropertyType::String(StringPropertySizes {
916 min_length: Some(1),
917 max_length: Some(4096),
918 max_bytes: Some(4096),
919 })
920 );
921 let with_description = |description: String| {
922 document_with(
923 &contract,
924 SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME,
925 SubmittedCharter {
926 description,
927 ..proposal()
928 }
929 .to_document_properties(),
930 )
931 };
932
933 for description in ["a".repeat(4096), "é".repeat(2048)] {
935 let document = with_description(description);
936 assert_eq!(
937 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document),
938 vec![]
939 );
940 }
941
942 let document = with_description("é".repeat(2049));
944 assert!(matches!(
945 schema_validation(&contract, SUBMITTED_CHARTER_DOCUMENT_TYPE_NAME, &document)
946 .as_slice(),
947 [ConsensusError::BasicError(BasicError::DocumentPropertyMaxBytesExceededError(e))]
948 if e.property() == property_names::DESCRIPTION
949 && e.byte_length() == 4098
950 && e.max_bytes() == 4096
951 ));
952 }
953
954 #[test]
958 fn should_let_only_the_leader_change_the_team_and_only_a_member_resign() {
959 let contract = contract();
960 let charter_agreement =
961 |type_name| match reference(&contract, type_name, property_names::ELECTED_CHARTER_ID) {
962 PropertyReference::Value(DocumentPropertyReferenceTarget::PermanentDocument {
963 document_type_name,
964 property_agreement,
965 ..
966 }) => {
967 assert_eq!(document_type_name, ELECTED_CHARTER_DOCUMENT_TYPE_NAME);
968 property_agreement.clone()
969 }
970 other => panic!("{type_name}.electedCharterId: {other:?}"),
971 };
972
973 let added = charter_agreement(ADDED_MODERATOR_DOCUMENT_TYPE_NAME);
974 assert_eq!(added.get("$ownerId").map(String::as_str), Some("$ownerId"));
975 assert_eq!(
976 added
977 .get(property_names::SUBMITTED_CHARTER_ID)
978 .map(String::as_str),
979 Some(property_names::SUBMITTED_CHARTER_ID)
980 );
981 let removed = charter_agreement(REMOVED_MODERATOR_DOCUMENT_TYPE_NAME);
982 assert_eq!(
983 removed.get("$ownerId").map(String::as_str),
984 Some("$ownerId")
985 );
986 let resignation = charter_agreement(RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME);
987 assert_eq!(
988 resignation.get("recipientId").map(String::as_str),
989 Some("$ownerId"),
990 "a resignation is addressed to the charter's leader"
991 );
992
993 match reference(
994 &contract,
995 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
996 property_names::MEMBER_ID,
997 ) {
998 PropertyReference::Value(
999 DocumentPropertyReferenceTarget::PermanentDocumentLookup {
1000 document_type_name,
1001 lookup,
1002 ..
1003 },
1004 ) => {
1005 assert_eq!(document_type_name, JOIN_REQUEST_DOCUMENT_TYPE_NAME);
1006 assert_eq!(
1007 lookup.resolve_index(
1008 document_type(&contract, JOIN_REQUEST_DOCUMENT_TYPE_NAME).as_ref()
1009 ),
1010 Ok("bySubmittedCharter".to_string())
1011 );
1012 assert_eq!(
1013 lookup.keys.get("$ownerId"),
1014 Some(&LookupKeySource::ReferenceValue)
1015 );
1016 }
1017 other => panic!("addedModerator.memberId: {other:?}"),
1018 }
1019 match reference(
1022 &contract,
1023 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1024 property_names::MEMBER_ID,
1025 ) {
1026 PropertyReference::Value(DocumentPropertyReferenceTarget::ListElement(listed)) => {
1027 assert_eq!(
1028 listed.document_type_name,
1029 ELECTED_CHARTER_DOCUMENT_TYPE_NAME
1030 );
1031 assert_eq!(listed.in_list, property_names::MEMBERS);
1032 assert_eq!(
1033 listed.document_id_property(),
1034 Some(property_names::ELECTED_CHARTER_ID)
1035 );
1036 }
1037 other => panic!("removedModerator.memberId: {other:?}"),
1038 }
1039 for type_name in [
1040 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1041 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1042 ] {
1043 let member = document_type(&contract, type_name)
1044 .flattened_properties()
1045 .get(property_names::MEMBER_ID)
1046 .expect("memberId");
1047 assert_eq!(
1048 member.distinct_from,
1049 Some(DistinctFrom::OwnerId),
1050 "{type_name}: the leader is not a member"
1051 );
1052 }
1053
1054 for (type_name, index_name, member_property) in [
1055 (
1056 ADDED_MODERATOR_DOCUMENT_TYPE_NAME,
1057 "byElectedCharterMember",
1058 property_names::MEMBER_ID,
1059 ),
1060 (
1061 REMOVED_MODERATOR_DOCUMENT_TYPE_NAME,
1062 "byElectedCharterMember",
1063 property_names::MEMBER_ID,
1064 ),
1065 (
1066 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
1067 "byElectedCharterOwner",
1068 "$ownerId",
1069 ),
1070 ] {
1071 let index = document_type(&contract, type_name)
1072 .indexes()
1073 .get(index_name)
1074 .unwrap_or_else(|| panic!("{type_name}.{index_name}"));
1075 assert!(index.unique, "{type_name}: once per member and charter");
1076 assert_eq!(
1077 index
1078 .properties
1079 .iter()
1080 .map(|p| p.name.as_str())
1081 .collect::<Vec<_>>(),
1082 vec![property_names::ELECTED_CHARTER_ID, member_property]
1083 );
1084 }
1085 }
1086
1087 #[test]
1092 fn should_let_only_a_team_member_ask_to_leave() {
1093 let contract = contract();
1094 let resignation = document_type(&contract, RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME);
1095 assert!(resignation.documents_can_be_deleted());
1096
1097 let Some(DocumentPropertyReferenceTarget::AnyOf(operands)) = resignation.owner_reference()
1098 else {
1099 panic!(
1100 "the writer must meet any of the membership targets: {:?}",
1101 resignation.owner_reference()
1102 );
1103 };
1104 match operands.operands() {
1105 [DocumentPropertyReferenceTarget::ListElement(listed), DocumentPropertyReferenceTarget::DeletableDocumentLookup {
1106 document_type_name,
1107 lookup,
1108 ..
1109 }] => {
1110 assert_eq!(
1111 listed.document_type_name,
1112 ELECTED_CHARTER_DOCUMENT_TYPE_NAME
1113 );
1114 assert_eq!(listed.in_list, property_names::MEMBERS);
1115 assert_eq!(
1116 listed.document_id_property(),
1117 Some(property_names::ELECTED_CHARTER_ID)
1118 );
1119 assert_eq!(document_type_name, ADDED_MODERATOR_DOCUMENT_TYPE_NAME);
1120 assert_eq!(
1121 lookup.resolve_index(
1122 document_type(&contract, ADDED_MODERATOR_DOCUMENT_TYPE_NAME).as_ref()
1123 ),
1124 Ok("byElectedCharterMember".to_string())
1125 );
1126 assert_eq!(
1127 lookup.keys.get(property_names::MEMBER_ID),
1128 Some(&LookupKeySource::ReferenceValue)
1129 );
1130 }
1131 other => panic!("resignation membership operands: {other:?}"),
1132 }
1133
1134 let encrypted_for = resignation
1135 .flattened_properties()
1136 .get("encryptedMessage")
1137 .and_then(|property| property.encrypted_for.clone())
1138 .expect("the message declares its envelope");
1139 assert_eq!(
1140 encrypted_for.recipient,
1141 EncryptedForRecipient::Property("recipientId".to_string())
1142 );
1143 match reference(
1144 &contract,
1145 RESIGNATION_REQUEST_DOCUMENT_TYPE_NAME,
1146 "senderKeyId",
1147 ) {
1148 PropertyReference::KeyId(key_reference) => {
1149 assert_eq!(
1150 key_reference.key_requirements.bound_to.as_deref(),
1151 Some(JOIN_REQUEST_DOCUMENT_TYPE_NAME),
1152 "the member's encryption key is the one its join request used"
1153 );
1154 }
1155 other => panic!("senderKeyId: {other:?}"),
1156 }
1157 }
1158}