1use crate::consensus::basic::document::InvalidDocumentTypeError;
2use crate::data_contract::accessors::v0::DataContractV0Getters;
3use crate::data_contract::document_type::accessors::{
4 DocumentTypeV0Getters, DocumentTypeV2Getters,
5};
6use crate::data_contract::document_type::DocumentTypeRef;
7use crate::data_contract::errors::DataContractError;
8use crate::data_contract::DataContract;
9use crate::document::errors::DocumentError;
10use crate::document::{Document, DocumentV0Getters, DocumentV0Setters, INITIAL_REVISION};
11use chrono::Utc;
12use std::collections::BTreeMap;
13
14use crate::util::entropy_generator::{DefaultEntropyGenerator, EntropyGenerator};
15use crate::version::PlatformVersion;
16use crate::ProtocolError;
17
18use platform_value::{Bytes32, Identifier, Value};
19
20use crate::data_contract::document_type::methods::DocumentTypeV0Methods;
21use crate::document::document_methods::DocumentMethodsV0;
22#[cfg(feature = "extended-document")]
23use crate::document::{
24 extended_document::v0::ExtendedDocumentV0,
25 ExtendedDocument, serialization_traits::DocumentPlatformConversionMethodsV0,
26};
27use crate::prelude::{BlockHeight, CoreBlockHeight, TimestampMillis};
28#[cfg(feature = "state-transitions")]
29use crate::state_transition::batch_transition::{
30 batched_transition::{
31 document_transition_action_type::DocumentTransitionActionType, DocumentCreateTransition,
32 DocumentDeleteTransition, DocumentIndexOnlyDeleteTransition, DocumentReplaceTransition,
33 },
34 BatchTransition, BatchTransitionV0,
35};
36use itertools::Itertools;
37#[cfg(feature = "state-transitions")]
38use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition;
39use crate::tokens::token_payment_info::TokenPaymentInfo;
40
41pub struct DocumentFactoryV0 {
43 protocol_version: u32,
44 entropy_generator: Box<dyn EntropyGenerator>,
45}
46
47impl DocumentFactoryV0 {
48 pub fn new(protocol_version: u32) -> Self {
49 DocumentFactoryV0 {
50 protocol_version,
51 entropy_generator: Box::new(DefaultEntropyGenerator),
52 }
53 }
54
55 pub fn new_with_entropy_generator(
56 protocol_version: u32,
57 entropy_generator: Box<dyn EntropyGenerator>,
58 ) -> Self {
59 DocumentFactoryV0 {
60 protocol_version,
61 entropy_generator,
62 }
63 }
64
65 pub fn create_document(
66 &self,
67 data_contract: &DataContract,
68 owner_id: Identifier,
69 block_time: BlockHeight,
70 core_block_height: CoreBlockHeight,
71 document_type_name: String,
72 data: Value,
73 ) -> Result<Document, ProtocolError> {
74 let platform_version = PlatformVersion::get(self.protocol_version)?;
75 if !data_contract.has_document_type_for_name(&document_type_name) {
76 return Err(DataContractError::InvalidDocumentTypeError(
77 InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
78 )
79 .into());
80 }
81
82 let document_entropy = self.entropy_generator.generate()?;
83
84 let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
85
86 document_type.create_document_from_data(
87 data,
88 owner_id,
89 block_time,
90 core_block_height,
91 document_entropy,
92 platform_version,
93 )
94 }
95
96 pub fn create_document_without_time_based_properties(
97 &self,
98 data_contract: &DataContract,
99 owner_id: Identifier,
100 document_type_name: String,
101 data: Value,
102 ) -> Result<Document, ProtocolError> {
103 let platform_version = PlatformVersion::get(self.protocol_version)?;
104 if !data_contract.has_document_type_for_name(&document_type_name) {
105 return Err(DataContractError::InvalidDocumentTypeError(
106 InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
107 )
108 .into());
109 }
110
111 let document_entropy = self.entropy_generator.generate()?;
112
113 let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
114
115 document_type.create_document_from_data(
116 data,
117 owner_id,
118 0,
119 0,
120 document_entropy,
121 platform_version,
122 )
123 }
124
125 #[cfg(feature = "extended-document")]
126 pub fn create_extended_document(
127 &self,
128 data_contract: &DataContract,
129 owner_id: Identifier,
130 document_type_name: String,
131 data: Value,
132 ) -> Result<ExtendedDocument, ProtocolError> {
133 let platform_version = PlatformVersion::get(self.protocol_version)?;
134 if !data_contract.has_document_type_for_name(&document_type_name) {
135 return Err(DataContractError::InvalidDocumentTypeError(
136 InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
137 )
138 .into());
139 }
140
141 let document_entropy = self.entropy_generator.generate()?;
142
143 let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
144
145 let document = document_type.create_document_from_data(
147 data,
148 owner_id,
149 0,
150 0,
151 document_entropy,
152 platform_version,
153 )?;
154
155 let extended_document = match platform_version
156 .dpp
157 .document_versions
158 .extended_document_structure_version
159 {
160 0 => Ok(ExtendedDocumentV0 {
161 document_type_name,
162 data_contract_id: data_contract.id(),
163 document,
164 data_contract: data_contract.clone(),
165 metadata: None,
166 entropy: Bytes32::new(document_entropy),
167 token_payment_info: None,
168 }
169 .into()),
170 version => Err(ProtocolError::UnknownVersionMismatch {
171 method: "DocumentFactory::create_extended_document".to_string(),
172 known_versions: vec![0],
173 received: version,
174 }),
175 }?;
176
177 Ok(extended_document)
178 }
179 #[cfg(feature = "state-transitions")]
180 pub fn create_state_transition<'a>(
181 &self,
182 documents_iter: impl IntoIterator<
183 Item = (
184 DocumentTransitionActionType,
185 Vec<(
186 Document,
187 DocumentTypeRef<'a>,
188 Bytes32,
189 Option<TokenPaymentInfo>,
190 )>,
191 ),
192 >,
193 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, ) -> Result<BatchTransition, ProtocolError> {
195 let platform_version = PlatformVersion::get(self.protocol_version)?;
196 #[allow(clippy::type_complexity)]
198 let documents: Vec<(
199 DocumentTransitionActionType,
200 Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
201 )> = documents_iter.into_iter().collect();
202 let mut flattened_documents_iter = documents.iter().flat_map(|(_, v)| v).peekable();
203
204 let Some((first_document, _, _, _)) = flattened_documents_iter.peek() else {
205 return Err(DocumentError::NoDocumentsSuppliedError.into());
206 };
207
208 let owner_id = first_document.owner_id();
209
210 let is_the_same_owner =
211 flattened_documents_iter.all(|(document, _, _, _)| document.owner_id() == owner_id);
212 if !is_the_same_owner {
213 return Err(DocumentError::MismatchOwnerIdsError {
214 documents: documents
215 .into_iter()
216 .flat_map(|(_, v)| {
217 v.into_iter()
218 .map(|(document, _, _, _)| document)
219 .collect::<Vec<_>>()
220 })
221 .collect(),
222 }
223 .into());
224 }
225
226 let transitions: Vec<_> = documents
227 .into_iter()
228 .map(|(action, documents)| match action {
229 DocumentTransitionActionType::Create => {
230 Self::document_create_transitions(documents, nonce_counter, platform_version)
231 }
232 DocumentTransitionActionType::Delete => Self::document_delete_transitions(
233 documents
234 .into_iter()
235 .map(|(document, document_type, _, token_payment_info)| {
236 (document, document_type, token_payment_info)
237 })
238 .collect(),
239 nonce_counter,
240 platform_version,
241 ),
242 DocumentTransitionActionType::Replace => Self::document_replace_transitions(
243 documents
244 .into_iter()
245 .map(|(document, document_type, _, token_payment_info)| {
246 (document, document_type, token_payment_info)
247 })
248 .collect(),
249 nonce_counter,
250 platform_version,
251 ),
252 _ => Err(ProtocolError::InvalidStateTransitionType(
253 "action type not accounted for".to_string(),
254 )),
255 })
256 .collect::<Result<Vec<_>, ProtocolError>>()?
257 .into_iter()
258 .flatten()
259 .collect();
260
261 if transitions.is_empty() {
262 return Err(DocumentError::NoDocumentsSuppliedError.into());
263 }
264
265 Ok(BatchTransitionV0 {
266 owner_id,
267 transitions,
268 user_fee_increase: 0,
269 signature_public_key_id: 0,
270 signature: Default::default(),
271 }
272 .into())
273 }
274
275 #[cfg(feature = "extended-document")]
276 pub fn create_extended_from_document_buffer(
277 &self,
278 buffer: &[u8],
279 document_type_name: &str,
280 data_contract: &DataContract,
281 platform_version: &PlatformVersion,
282 ) -> Result<ExtendedDocument, ProtocolError> {
283 let document_type = data_contract.document_type_for_name(document_type_name)?;
284
285 let document = Document::from_bytes(buffer, document_type, platform_version)?;
286
287 match platform_version
288 .dpp
289 .document_versions
290 .extended_document_structure_version
291 {
292 0 => Ok(ExtendedDocumentV0 {
293 document_type_name: document_type_name.to_string(),
294 data_contract_id: data_contract.id(),
295 document,
296 data_contract: data_contract.clone(),
297 metadata: None,
298 entropy: Bytes32::default(),
299 token_payment_info: None,
300 }
301 .into()),
302 version => Err(ProtocolError::UnknownVersionMismatch {
303 method: "DocumentFactory::create_extended_from_document_buffer".to_string(),
304 known_versions: vec![0],
305 received: version,
306 }),
307 }
308 }
309 #[cfg(feature = "state-transitions")]
370 fn document_create_transitions(
371 documents: Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
372 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
374 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
375 documents
376 .into_iter()
377 .map(|(document, document_type, entropy, token_payment_info)| {
378 if document_type.documents_mutable() {
379 let Some(revision) = document.revision() else {
381 return Err(DocumentError::RevisionAbsentError {
382 document: Box::new(document),
383 }
384 .into());
385 };
386 if revision != INITIAL_REVISION {
387 return Err(DocumentError::InvalidInitialRevisionError {
388 document: Box::new(document),
389 }
390 .into());
391 }
392 }
393 let nonce = nonce_counter
394 .entry((document.owner_id(), document_type.data_contract_id()))
395 .or_default();
396
397 let transition = DocumentCreateTransition::from_document(
398 document,
399 document_type,
400 entropy.to_buffer(),
401 token_payment_info,
402 *nonce,
403 platform_version,
404 None,
405 None,
406 )?;
407
408 *nonce += 1;
409
410 Ok(transition.into())
411 })
412 .collect()
413 }
414
415 #[cfg(feature = "state-transitions")]
416 fn document_replace_transitions(
417 documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
418 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
420 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
421 documents
422 .into_iter()
423 .map(|(mut document, document_type, token_payment_info)| {
424 if !document_type.documents_mutable() {
425 return Err(DocumentError::TryingToReplaceImmutableDocument {
426 document: Box::new(document),
427 }
428 .into());
429 }
430 if document.revision().is_none() {
431 return Err(DocumentError::RevisionAbsentError {
432 document: Box::new(document),
433 }
434 .into());
435 };
436
437 document.increment_revision()?;
438 document.set_updated_at(Some(Utc::now().timestamp_millis() as TimestampMillis));
439
440 let nonce = nonce_counter
441 .entry((document.owner_id(), document_type.data_contract_id()))
442 .or_default();
443
444 let transition = DocumentReplaceTransition::from_document(
445 document,
446 document_type,
447 token_payment_info,
448 *nonce,
449 platform_version,
450 None,
451 None,
452 )?;
453
454 *nonce += 1;
455
456 Ok(transition.into())
457 })
458 .collect()
459 }
499
500 #[cfg(feature = "state-transitions")]
501 fn document_delete_transitions(
502 documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
503 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
505 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
506 documents
507 .into_iter()
508 .map(|(document, document_type, token_payment_info)| {
509 if !document_type.documents_can_be_deleted() {
510 return Err(DocumentError::TryingToDeleteIndelibleDocument {
511 document: Box::new(document),
512 }
513 .into());
514 }
515
516 if document_type.index_only() {
523 let nonce = nonce_counter
524 .entry((document.owner_id(), document_type.data_contract_id()))
525 .or_default();
526 let transition = DocumentIndexOnlyDeleteTransition::from_document(
527 document,
528 document_type,
529 token_payment_info,
530 *nonce,
531 platform_version,
532 None,
533 None,
534 )?;
535
536 *nonce += 1;
537
538 return Ok(transition.into());
539 }
540
541 let Some(_document_revision) = document.revision() else {
542 return Err(DocumentError::RevisionAbsentError {
543 document: Box::new(document),
544 }
545 .into());
546 };
547
548 let nonce = nonce_counter
549 .entry((document.owner_id(), document_type.data_contract_id()))
550 .or_default();
551 let transition = DocumentDeleteTransition::from_document(
552 document,
553 document_type,
554 token_payment_info,
555 *nonce,
556 platform_version,
557 None,
558 None,
559 )?;
560
561 *nonce += 1;
562
563 Ok(transition.into())
564 })
565 .collect()
566 }
567
568 fn is_ownership_the_same<'a>(ids: impl IntoIterator<Item = &'a Identifier>) -> bool {
569 ids.into_iter().all_equal()
570 }
571}
572
573#[cfg(test)]
574mod test {
575 use data_contracts::SystemDataContract;
576 use platform_value::platform_value;
577 use platform_version::version::PlatformVersion;
578 use std::collections::BTreeMap;
579
580 use crate::data_contract::accessors::v0::DataContractV0Getters;
581 use crate::data_contract::config::DataContractConfig;
582 use crate::data_contract::document_type::DocumentType;
583 use crate::document::document_factory::DocumentFactoryV0;
584 use crate::document::{Document, DocumentV0};
585 use crate::identifier::Identifier;
586 use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods;
587 use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition;
588 use crate::system_data_contracts::load_system_data_contract;
589
590 #[test]
591 fn delete_immutable_but_deletable_documents() {
593 let dpns_contract =
595 load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest()).unwrap();
596 let document_type = dpns_contract
597 .document_type_borrowed_for_name("preorder")
598 .unwrap();
599 let document_type_ref = document_type.as_ref();
600
601 let document_id = Identifier::random();
603 let owner_id = Identifier::random();
604 let mut properties = BTreeMap::new();
605 properties.insert(
606 "saltedDomainHash".to_string(),
607 platform_value::Value::Array(vec![]),
608 );
609 let document_v0 = DocumentV0 {
610 contract_version: None,
611 id: document_id,
612 owner_id,
613 properties,
614 revision: Some(1),
615 created_at: None,
616 updated_at: None,
617 transferred_at: None,
618 created_at_block_height: None,
619 updated_at_block_height: None,
620 transferred_at_block_height: None,
621 created_at_core_block_height: None,
622 updated_at_core_block_height: None,
623 transferred_at_core_block_height: None,
624 creator_id: None,
625 };
626 let document = Document::V0(document_v0);
627
628 let documents = vec![(document, document_type_ref, None)];
630 let mut nonce_counter = BTreeMap::new();
631 let platform_version = PlatformVersion::latest();
632
633 let result = DocumentFactoryV0::document_delete_transitions(
635 documents,
636 &mut nonce_counter,
637 platform_version,
638 );
639
640 assert!(result.is_ok(), "The function should succeed");
642 let transitions = result.unwrap();
643 assert_eq!(transitions.len(), 1, "There should be one transition");
644 }
645
646 #[test]
647 fn delete_index_only_documents_produces_index_only_delete_kind() {
652 let platform_version = PlatformVersion::latest();
653 let config = DataContractConfig::default_for_version(platform_version)
654 .expect("default config available");
655
656 let schema = platform_value!({
657 "type": "object",
658 "indexOnly": true,
659 "documentsMutable": false,
660 "canBeDeleted": true,
661 "properties": {
662 "hashtag": {
663 "type": "string",
664 "maxLength": 63,
665 "position": 0
666 },
667 "postId": {
668 "type": "array",
669 "byteArray": true,
670 "minItems": 32,
671 "maxItems": 32,
672 "contentMediaType": "application/x.dash.dpp.identifier",
673 "refersTo": { "type": "identity" },
674 "position": 1
675 }
676 },
677 "required": ["hashtag", "postId"],
678 "indices": [
679 {
680 "name": "byPost",
681 "properties": [{ "postId": "asc" }]
682 },
683 {
684 "name": "byLiker",
685 "properties": [{ "$ownerId": "asc" }],
686 "terminal": "postId"
687 },
688 {
689 "name": "byHashtagPost",
690 "properties": [{ "hashtag": "asc" }, { "postId": "asc" }],
691 "terminal": "$ownerId"
692 }
693 ],
694 "additionalProperties": false
695 });
696
697 let contract_id = Identifier::new([1; 32]);
698 let document_type = DocumentType::try_from_schema(
699 contract_id,
700 1,
701 config.version(),
702 "like",
703 schema,
704 None,
705 &BTreeMap::new(),
706 &config,
707 false,
708 &mut vec![],
709 platform_version,
710 )
711 .expect("the indexOnly like schema parses");
712 let document_type_ref = document_type.as_ref();
713
714 let mut properties = BTreeMap::new();
715 properties.insert(
716 "hashtag".to_string(),
717 platform_value::Value::Text("dash".to_string()),
718 );
719 properties.insert(
720 "postId".to_string(),
721 platform_value::Value::Identifier([2; 32]),
722 );
723 let document = Document::V0(DocumentV0 {
724 contract_version: None,
725 id: Identifier::random(),
726 owner_id: Identifier::random(),
727 properties,
728 revision: None,
731 created_at: None,
732 updated_at: None,
733 transferred_at: None,
734 created_at_block_height: None,
735 updated_at_block_height: None,
736 transferred_at_block_height: None,
737 created_at_core_block_height: None,
738 updated_at_core_block_height: None,
739 transferred_at_core_block_height: None,
740 creator_id: None,
741 });
742
743 let mut nonce_counter = BTreeMap::new();
744 let transitions = DocumentFactoryV0::document_delete_transitions(
745 vec![(document, document_type_ref, None)],
746 &mut nonce_counter,
747 platform_version,
748 )
749 .expect("the delete transition builds");
750
751 assert_eq!(transitions.len(), 1, "There should be one transition");
752 let DocumentTransition::IndexOnlyDelete(transition) = &transitions[0] else {
753 panic!(
754 "an indexOnly doctype must produce an indexOnlyDelete transition, got {}",
755 transitions[0]
756 );
757 };
758 let data = transition.data();
759 assert_eq!(
760 data.get("hashtag"),
761 Some(&platform_value::Value::Text("dash".to_string()))
762 );
763 assert_eq!(
764 data.get("postId"),
765 Some(&platform_value::Value::Identifier([2; 32]))
766 );
767 assert!(
768 !data.contains_key("$createdAt"),
769 "$createdAt must not ride along when the doctype does not require it"
770 );
771 }
772}