1use crate::consensus::basic::document::InvalidDocumentTypeError;
2use crate::data_contract::accessors::v0::DataContractV0Getters;
3use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
4use crate::data_contract::document_type::DocumentTypeRef;
5use crate::data_contract::errors::DataContractError;
6use crate::data_contract::DataContract;
7use crate::document::errors::DocumentError;
8use crate::document::{Document, DocumentV0Getters, DocumentV0Setters, INITIAL_REVISION};
9use chrono::Utc;
10use std::collections::BTreeMap;
11
12use crate::util::entropy_generator::{DefaultEntropyGenerator, EntropyGenerator};
13use crate::version::PlatformVersion;
14use crate::ProtocolError;
15
16use platform_value::{Bytes32, Identifier, Value};
17
18use crate::data_contract::document_type::methods::DocumentTypeV0Methods;
19#[cfg(feature = "extended-document")]
20use crate::document::{
21 extended_document::v0::ExtendedDocumentV0,
22 ExtendedDocument, serialization_traits::DocumentPlatformConversionMethodsV0,
23};
24use crate::prelude::{BlockHeight, CoreBlockHeight, TimestampMillis};
25#[cfg(feature = "state-transitions")]
26use crate::state_transition::batch_transition::{
27 batched_transition::{
28 document_transition_action_type::DocumentTransitionActionType, DocumentCreateTransition,
29 DocumentDeleteTransition, DocumentReplaceTransition,
30 },
31 BatchTransition, BatchTransitionV0,
32};
33use itertools::Itertools;
34#[cfg(feature = "state-transitions")]
35use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition;
36use crate::tokens::token_payment_info::TokenPaymentInfo;
37
38pub struct SpecializedDocumentFactoryV0 {
40 protocol_version: u32,
41 pub(super) data_contract: DataContract,
42 entropy_generator: Box<dyn EntropyGenerator>,
43}
44
45impl SpecializedDocumentFactoryV0 {
46 pub fn new(protocol_version: u32, data_contract: DataContract) -> Self {
47 SpecializedDocumentFactoryV0 {
48 protocol_version,
49 data_contract,
50 entropy_generator: Box::new(DefaultEntropyGenerator),
51 }
52 }
53
54 pub fn new_with_entropy_generator(
55 protocol_version: u32,
56 data_contract: DataContract,
57 entropy_generator: Box<dyn EntropyGenerator>,
58 ) -> Self {
59 SpecializedDocumentFactoryV0 {
60 protocol_version,
61 data_contract,
62 entropy_generator,
63 }
64 }
65
66 pub fn create_document(
67 &self,
68 data_contract: &DataContract,
69 owner_id: Identifier,
70 block_time: BlockHeight,
71 core_block_height: CoreBlockHeight,
72 document_type_name: String,
73 data: Value,
74 ) -> Result<Document, ProtocolError> {
75 let platform_version = PlatformVersion::get(self.protocol_version)?;
76 if !data_contract.has_document_type_for_name(&document_type_name) {
77 return Err(DataContractError::InvalidDocumentTypeError(
78 InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
79 )
80 .into());
81 }
82
83 let document_entropy = self.entropy_generator.generate()?;
84
85 let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
86
87 document_type.create_document_from_data(
88 data,
89 owner_id,
90 block_time,
91 core_block_height,
92 document_entropy,
93 platform_version,
94 )
95 }
96 pub fn create_document_without_time_based_properties(
97 &self,
98 owner_id: Identifier,
99 document_type_name: String,
100 data: Value,
101 ) -> Result<Document, ProtocolError> {
102 let platform_version = PlatformVersion::get(self.protocol_version)?;
103 if !self
104 .data_contract
105 .has_document_type_for_name(&document_type_name)
106 {
107 return Err(DataContractError::InvalidDocumentTypeError(
108 InvalidDocumentTypeError::new(document_type_name, self.data_contract.id()),
109 )
110 .into());
111 }
112
113 let document_entropy = self.entropy_generator.generate()?;
114
115 let document_type = self
116 .data_contract
117 .document_type_for_name(document_type_name.as_str())?;
118
119 document_type.create_document_from_data(
120 data,
121 owner_id,
122 0,
123 0,
124 document_entropy,
125 platform_version,
126 )
127 }
128 #[cfg(feature = "extended-document")]
129 pub fn create_extended_document(
130 &self,
131 owner_id: Identifier,
132 document_type_name: String,
133 data: Value,
134 ) -> Result<ExtendedDocument, ProtocolError> {
135 let platform_version = PlatformVersion::get(self.protocol_version)?;
136 if !self
137 .data_contract
138 .has_document_type_for_name(&document_type_name)
139 {
140 return Err(DataContractError::InvalidDocumentTypeError(
141 InvalidDocumentTypeError::new(document_type_name, self.data_contract.id()),
142 )
143 .into());
144 }
145
146 let document_entropy = self.entropy_generator.generate()?;
147
148 let document_type = self
149 .data_contract
150 .document_type_for_name(document_type_name.as_str())?;
151
152 let document = document_type.create_document_from_data(
153 data,
154 owner_id,
155 0,
156 0,
157 document_entropy,
158 platform_version,
159 )?;
160
161 let extended_document = match platform_version
162 .dpp
163 .document_versions
164 .extended_document_structure_version
165 {
166 0 => Ok(ExtendedDocumentV0 {
167 document_type_name,
168 data_contract_id: self.data_contract.id(),
169 document,
170 data_contract: self.data_contract.clone(),
171 metadata: None,
172 entropy: Bytes32::new(document_entropy),
173 token_payment_info: None,
174 }
175 .into()),
176 version => Err(ProtocolError::UnknownVersionMismatch {
177 method: "DocumentFactory::create_extended_document".to_string(),
178 known_versions: vec![0],
179 received: version,
180 }),
181 }?;
182
183 Ok(extended_document)
184 }
185 #[cfg(feature = "state-transitions")]
186 pub fn create_state_transition<'a>(
187 &self,
188 documents_iter: impl IntoIterator<
189 Item = (
190 DocumentTransitionActionType,
191 Vec<(
192 Document,
193 DocumentTypeRef<'a>,
194 Bytes32,
195 Option<TokenPaymentInfo>,
196 )>,
197 ),
198 >,
199 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, ) -> Result<BatchTransition, ProtocolError> {
201 let platform_version = PlatformVersion::get(self.protocol_version)?;
202 #[allow(clippy::type_complexity)]
204 let documents: Vec<(
205 DocumentTransitionActionType,
206 Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
207 )> = documents_iter.into_iter().collect();
208 let mut flattened_documents_iter = documents.iter().flat_map(|(_, v)| v).peekable();
209
210 let Some((first_document, _, _, _)) = flattened_documents_iter.peek() else {
211 return Err(DocumentError::NoDocumentsSuppliedError.into());
212 };
213
214 let owner_id = first_document.owner_id();
215
216 let is_the_same_owner =
217 flattened_documents_iter.all(|(document, _, _, _)| document.owner_id() == owner_id);
218 if !is_the_same_owner {
219 return Err(DocumentError::MismatchOwnerIdsError {
220 documents: documents
221 .into_iter()
222 .flat_map(|(_, v)| {
223 v.into_iter()
224 .map(|(document, _, _, _)| document)
225 .collect::<Vec<_>>()
226 })
227 .collect(),
228 }
229 .into());
230 }
231
232 let transitions: Vec<_> = documents
233 .into_iter()
234 .map(|(action, documents)| match action {
235 DocumentTransitionActionType::Create => {
236 Self::document_create_transitions(documents, nonce_counter, platform_version)
237 }
238 DocumentTransitionActionType::Delete => Self::document_delete_transitions(
239 documents
240 .into_iter()
241 .map(|(document, document_type, _, token_payment_info)| {
242 (document, document_type, token_payment_info)
243 })
244 .collect(),
245 nonce_counter,
246 platform_version,
247 ),
248 DocumentTransitionActionType::Replace => Self::document_replace_transitions(
249 documents
250 .into_iter()
251 .map(|(document, document_type, _, token_payment_info)| {
252 (document, document_type, token_payment_info)
253 })
254 .collect(),
255 nonce_counter,
256 platform_version,
257 ),
258 _ => Err(ProtocolError::InvalidStateTransitionType(
259 "action type not accounted for".to_string(),
260 )),
261 })
262 .collect::<Result<Vec<_>, ProtocolError>>()?
263 .into_iter()
264 .flatten()
265 .collect();
266
267 if transitions.is_empty() {
268 return Err(DocumentError::NoDocumentsSuppliedError.into());
269 }
270
271 Ok(BatchTransitionV0 {
272 owner_id,
273 transitions,
274 user_fee_increase: 0,
275 signature_public_key_id: 0,
276 signature: Default::default(),
277 }
278 .into())
279 }
280
281 #[cfg(feature = "extended-document")]
282 pub fn create_extended_from_document_buffer(
283 &self,
284 buffer: &[u8],
285 document_type_name: &str,
286 platform_version: &PlatformVersion,
287 ) -> Result<ExtendedDocument, ProtocolError> {
288 let document_type = self
289 .data_contract
290 .document_type_for_name(document_type_name)?;
291
292 let document = Document::from_bytes(buffer, document_type, platform_version)?;
293
294 match platform_version
295 .dpp
296 .document_versions
297 .extended_document_structure_version
298 {
299 0 => Ok(ExtendedDocumentV0 {
300 document_type_name: document_type_name.to_string(),
301 data_contract_id: self.data_contract.id(),
302 document,
303 data_contract: self.data_contract.clone(),
304 metadata: None,
305 entropy: Bytes32::default(),
306 token_payment_info: None,
307 }
308 .into()),
309 version => Err(ProtocolError::UnknownVersionMismatch {
310 method: "DocumentFactory::create_extended_from_document_buffer".to_string(),
311 known_versions: vec![0],
312 received: version,
313 }),
314 }
315 }
316 #[cfg(feature = "state-transitions")]
377 fn document_create_transitions(
378 documents: Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
379 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
381 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
382 documents
383 .into_iter()
384 .map(|(document, document_type, entropy, token_payment_info)| {
385 if document_type.documents_mutable() {
386 let Some(revision) = document.revision() else {
388 return Err(DocumentError::RevisionAbsentError {
389 document: Box::new(document),
390 }
391 .into());
392 };
393 if revision != INITIAL_REVISION {
394 return Err(DocumentError::InvalidInitialRevisionError {
395 document: Box::new(document),
396 }
397 .into());
398 }
399 }
400 let nonce = nonce_counter
401 .entry((document.owner_id(), document_type.data_contract_id()))
402 .or_default();
403
404 let transition = DocumentCreateTransition::from_document(
405 document,
406 document_type,
407 entropy.to_buffer(),
408 token_payment_info,
409 *nonce,
410 platform_version,
411 None,
412 None,
413 )?;
414
415 *nonce += 1;
416
417 Ok(transition.into())
418 })
419 .collect()
420 }
421
422 #[cfg(feature = "state-transitions")]
423 fn document_replace_transitions(
424 documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
425 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
427 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
428 documents
429 .into_iter()
430 .map(|(mut document, document_type, token_payment_info)| {
431 if !document_type.documents_mutable() {
432 return Err(DocumentError::TryingToReplaceImmutableDocument {
433 document: Box::new(document),
434 }
435 .into());
436 }
437 if document.revision().is_none() {
438 return Err(DocumentError::RevisionAbsentError {
439 document: Box::new(document),
440 }
441 .into());
442 };
443
444 document.set_revision(document.revision().map(|revision| revision + 1));
445 document.set_updated_at(Some(Utc::now().timestamp_millis() as TimestampMillis));
446
447 let nonce = nonce_counter
448 .entry((document.owner_id(), document_type.data_contract_id()))
449 .or_default();
450
451 let transition = DocumentReplaceTransition::from_document(
452 document,
453 document_type,
454 token_payment_info,
455 *nonce,
456 platform_version,
457 None,
458 None,
459 )?;
460
461 *nonce += 1;
462
463 Ok(transition.into())
464 })
465 .collect()
466 }
506
507 #[cfg(feature = "state-transitions")]
508 fn document_delete_transitions(
509 documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
510 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
512 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
513 documents
514 .into_iter()
515 .map(|(document, document_type, token_payment_info)| {
516 if !document_type.documents_can_be_deleted() {
517 return Err(DocumentError::TryingToDeleteIndelibleDocument {
518 document: Box::new(document),
519 }
520 .into());
521 }
522 let Some(_document_revision) = document.revision() else {
523 return Err(DocumentError::RevisionAbsentError {
524 document: Box::new(document),
525 }
526 .into());
527 };
528 let nonce = nonce_counter
529 .entry((document.owner_id(), document_type.data_contract_id()))
530 .or_default();
531 let transition = DocumentDeleteTransition::from_document(
532 document,
533 document_type,
534 token_payment_info,
535 *nonce,
536 platform_version,
537 None,
538 None,
539 )?;
540
541 *nonce += 1;
542
543 Ok(transition.into())
544 })
545 .collect()
546 }
547
548 fn is_ownership_the_same<'a>(ids: impl IntoIterator<Item = &'a Identifier>) -> bool {
549 ids.into_iter().all_equal()
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::document::DocumentV0Getters;
557 use crate::tests::fixtures::get_data_contract_fixture;
558 use crate::util::entropy_generator::EntropyGenerator;
559 use platform_value::platform_value;
560
561 struct TestEntropyGenerator;
563
564 impl EntropyGenerator for TestEntropyGenerator {
565 fn generate(&self) -> anyhow::Result<[u8; 32]> {
566 Ok([1u8; 32])
567 }
568 }
569
570 fn setup_factory() -> (SpecializedDocumentFactoryV0, DataContract) {
571 let platform_version = PlatformVersion::latest();
572 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
573 let data_contract = created.data_contract_owned();
574 let factory = SpecializedDocumentFactoryV0::new_with_entropy_generator(
575 platform_version.protocol_version,
576 data_contract.clone(),
577 Box::new(TestEntropyGenerator),
578 );
579 (factory, data_contract)
580 }
581
582 #[test]
583 fn new_creates_factory_with_default_entropy() {
584 let platform_version = PlatformVersion::latest();
585 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
586 let data_contract = created.data_contract_owned();
587 let factory =
588 SpecializedDocumentFactoryV0::new(platform_version.protocol_version, data_contract);
589 assert_eq!(factory.protocol_version, platform_version.protocol_version);
591 }
592
593 #[test]
594 fn create_document_with_valid_type_succeeds() {
595 let (factory, data_contract) = setup_factory();
596 let owner_id = Identifier::from([10u8; 32]);
597
598 let data = platform_value!({
599 "firstName": "John",
600 "lastName": "Doe",
601 });
602
603 let result = factory.create_document(
604 &data_contract,
605 owner_id,
606 100,
607 50,
608 "indexedDocument".to_string(),
609 data,
610 );
611
612 let doc = result.expect("should create document");
613 assert_eq!(doc.owner_id(), owner_id);
614 }
615
616 #[test]
617 fn create_document_with_invalid_type_fails() {
618 let (factory, data_contract) = setup_factory();
619 let owner_id = Identifier::from([10u8; 32]);
620
621 let result = factory.create_document(
622 &data_contract,
623 owner_id,
624 100,
625 50,
626 "nonExistentDocType".to_string(),
627 Value::Null,
628 );
629
630 assert!(result.is_err());
631 }
632
633 #[test]
634 fn create_document_without_time_based_properties_succeeds() {
635 let (factory, _) = setup_factory();
636 let owner_id = Identifier::from([20u8; 32]);
637
638 let data = platform_value!({
639 "name": "test",
640 });
641
642 let result = factory.create_document_without_time_based_properties(
643 owner_id,
644 "noTimeDocument".to_string(),
645 data,
646 );
647
648 let doc = result.expect("should create document without time properties");
649 assert_eq!(doc.owner_id(), owner_id);
650 }
651
652 #[test]
653 fn create_document_without_time_based_properties_invalid_type_fails() {
654 let (factory, _) = setup_factory();
655 let owner_id = Identifier::from([20u8; 32]);
656
657 let result = factory.create_document_without_time_based_properties(
658 owner_id,
659 "nonExistentDocType".to_string(),
660 Value::Null,
661 );
662
663 assert!(result.is_err());
664 }
665
666 #[cfg(feature = "extended-document")]
667 #[test]
668 fn create_extended_document_succeeds() {
669 let (factory, _) = setup_factory();
670 let owner_id = Identifier::from([30u8; 32]);
671
672 let data = platform_value!({
673 "name": "test",
674 });
675
676 let result = factory.create_extended_document(owner_id, "noTimeDocument".to_string(), data);
677
678 assert!(result.is_ok());
679 }
680
681 #[cfg(feature = "extended-document")]
682 #[test]
683 fn create_extended_document_invalid_type_fails() {
684 let (factory, _) = setup_factory();
685 let owner_id = Identifier::from([30u8; 32]);
686
687 let result = factory.create_extended_document(
688 owner_id,
689 "nonExistentDocType".to_string(),
690 Value::Null,
691 );
692
693 assert!(result.is_err());
694 }
695
696 #[test]
697 fn is_ownership_the_same_with_same_ids() {
698 let id = Identifier::from([1u8; 32]);
699 assert!(SpecializedDocumentFactoryV0::is_ownership_the_same([
700 &id, &id, &id
701 ]));
702 }
703
704 #[test]
705 fn is_ownership_the_same_with_different_ids() {
706 let id1 = Identifier::from([1u8; 32]);
707 let id2 = Identifier::from([2u8; 32]);
708 assert!(!SpecializedDocumentFactoryV0::is_ownership_the_same([
709 &id1, &id2
710 ]));
711 }
712
713 #[test]
714 fn is_ownership_the_same_with_single_id() {
715 let id = Identifier::from([1u8; 32]);
716 assert!(SpecializedDocumentFactoryV0::is_ownership_the_same([&id]));
717 }
718
719 #[test]
720 fn is_ownership_the_same_with_empty_iter() {
721 let ids: Vec<&Identifier> = vec![];
722 assert!(SpecializedDocumentFactoryV0::is_ownership_the_same(ids));
723 }
724}