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)]
554#[allow(clippy::type_complexity)]
555mod tests {
556 use super::*;
557 use crate::document::DocumentV0Getters;
558 use crate::tests::fixtures::get_data_contract_fixture;
559 use crate::util::entropy_generator::EntropyGenerator;
560 use platform_value::platform_value;
561
562 struct TestEntropyGenerator;
564
565 impl EntropyGenerator for TestEntropyGenerator {
566 fn generate(&self) -> anyhow::Result<[u8; 32]> {
567 Ok([1u8; 32])
568 }
569 }
570
571 fn setup_factory() -> (SpecializedDocumentFactoryV0, DataContract) {
572 let platform_version = PlatformVersion::latest();
573 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
574 let data_contract = created.data_contract_owned();
575 let factory = SpecializedDocumentFactoryV0::new_with_entropy_generator(
576 platform_version.protocol_version,
577 data_contract.clone(),
578 Box::new(TestEntropyGenerator),
579 );
580 (factory, data_contract)
581 }
582
583 #[test]
584 fn new_creates_factory_with_default_entropy() {
585 let platform_version = PlatformVersion::latest();
586 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
587 let data_contract = created.data_contract_owned();
588 let factory =
589 SpecializedDocumentFactoryV0::new(platform_version.protocol_version, data_contract);
590 assert_eq!(factory.protocol_version, platform_version.protocol_version);
592 }
593
594 #[test]
595 fn create_document_with_valid_type_succeeds() {
596 let (factory, data_contract) = setup_factory();
597 let owner_id = Identifier::from([10u8; 32]);
598
599 let data = platform_value!({
600 "firstName": "John",
601 "lastName": "Doe",
602 });
603
604 let result = factory.create_document(
605 &data_contract,
606 owner_id,
607 100,
608 50,
609 "indexedDocument".to_string(),
610 data,
611 );
612
613 let doc = result.expect("should create document");
614 assert_eq!(doc.owner_id(), owner_id);
615 }
616
617 #[test]
618 fn create_document_with_invalid_type_fails() {
619 let (factory, data_contract) = setup_factory();
620 let owner_id = Identifier::from([10u8; 32]);
621
622 let result = factory.create_document(
623 &data_contract,
624 owner_id,
625 100,
626 50,
627 "nonExistentDocType".to_string(),
628 Value::Null,
629 );
630
631 assert!(result.is_err());
632 }
633
634 #[test]
635 fn create_document_without_time_based_properties_succeeds() {
636 let (factory, _) = setup_factory();
637 let owner_id = Identifier::from([20u8; 32]);
638
639 let data = platform_value!({
640 "name": "test",
641 });
642
643 let result = factory.create_document_without_time_based_properties(
644 owner_id,
645 "noTimeDocument".to_string(),
646 data,
647 );
648
649 let doc = result.expect("should create document without time properties");
650 assert_eq!(doc.owner_id(), owner_id);
651 }
652
653 #[test]
654 fn create_document_without_time_based_properties_invalid_type_fails() {
655 let (factory, _) = setup_factory();
656 let owner_id = Identifier::from([20u8; 32]);
657
658 let result = factory.create_document_without_time_based_properties(
659 owner_id,
660 "nonExistentDocType".to_string(),
661 Value::Null,
662 );
663
664 assert!(result.is_err());
665 }
666
667 #[cfg(feature = "extended-document")]
668 #[test]
669 fn create_extended_document_succeeds() {
670 let (factory, _) = setup_factory();
671 let owner_id = Identifier::from([30u8; 32]);
672
673 let data = platform_value!({
674 "name": "test",
675 });
676
677 let result = factory.create_extended_document(owner_id, "noTimeDocument".to_string(), data);
678
679 assert!(result.is_ok());
680 }
681
682 #[cfg(feature = "extended-document")]
683 #[test]
684 fn create_extended_document_invalid_type_fails() {
685 let (factory, _) = setup_factory();
686 let owner_id = Identifier::from([30u8; 32]);
687
688 let result = factory.create_extended_document(
689 owner_id,
690 "nonExistentDocType".to_string(),
691 Value::Null,
692 );
693
694 assert!(result.is_err());
695 }
696
697 #[test]
698 fn is_ownership_the_same_with_same_ids() {
699 let id = Identifier::from([1u8; 32]);
700 assert!(SpecializedDocumentFactoryV0::is_ownership_the_same([
701 &id, &id, &id
702 ]));
703 }
704
705 #[test]
706 fn is_ownership_the_same_with_different_ids() {
707 let id1 = Identifier::from([1u8; 32]);
708 let id2 = Identifier::from([2u8; 32]);
709 assert!(!SpecializedDocumentFactoryV0::is_ownership_the_same([
710 &id1, &id2
711 ]));
712 }
713
714 #[test]
715 fn is_ownership_the_same_with_single_id() {
716 let id = Identifier::from([1u8; 32]);
717 assert!(SpecializedDocumentFactoryV0::is_ownership_the_same([&id]));
718 }
719
720 #[test]
721 fn is_ownership_the_same_with_empty_iter() {
722 let ids: Vec<&Identifier> = vec![];
723 assert!(SpecializedDocumentFactoryV0::is_ownership_the_same(ids));
724 }
725
726 #[test]
729 fn new_with_invalid_protocol_version_still_constructs() {
730 let platform_version = PlatformVersion::latest();
732 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
733 let factory = SpecializedDocumentFactoryV0::new(u32::MAX, created.data_contract_owned());
734 assert_eq!(factory.protocol_version, u32::MAX);
735 }
736
737 #[test]
738 fn create_document_bad_protocol_version_returns_error() {
739 let platform_version = PlatformVersion::latest();
740 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
741 let data_contract = created.data_contract_owned();
742 let factory = SpecializedDocumentFactoryV0::new_with_entropy_generator(
743 u32::MAX,
744 data_contract.clone(),
745 Box::new(TestEntropyGenerator),
746 );
747
748 let result = factory.create_document(
749 &data_contract,
750 Identifier::from([1u8; 32]),
751 0,
752 0,
753 "noTimeDocument".to_string(),
754 Value::Null,
755 );
756 assert!(result.is_err());
757 }
758
759 #[test]
760 fn create_document_without_time_bad_protocol_version_returns_error() {
761 let platform_version = PlatformVersion::latest();
762 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
763 let factory = SpecializedDocumentFactoryV0::new_with_entropy_generator(
764 u32::MAX,
765 created.data_contract_owned(),
766 Box::new(TestEntropyGenerator),
767 );
768
769 let result = factory.create_document_without_time_based_properties(
770 Identifier::from([1u8; 32]),
771 "noTimeDocument".to_string(),
772 Value::Null,
773 );
774 assert!(result.is_err());
775 }
776
777 #[test]
778 fn create_document_uses_entropy_generator_for_deterministic_id() {
779 let (factory, _) = setup_factory();
781 let owner_id = Identifier::from([42u8; 32]);
782
783 let d1 = factory
784 .create_document_without_time_based_properties(
785 owner_id,
786 "noTimeDocument".to_string(),
787 platform_value!({ "name": "a" }),
788 )
789 .unwrap();
790 let d2 = factory
791 .create_document_without_time_based_properties(
792 owner_id,
793 "noTimeDocument".to_string(),
794 platform_value!({ "name": "b" }),
795 )
796 .unwrap();
797
798 assert_eq!(d1.id(), d2.id());
800 }
801
802 #[test]
803 fn create_document_uses_given_time_based_properties() {
804 let (factory, data_contract) = setup_factory();
805 let owner_id = Identifier::from([50u8; 32]);
806 let block_time = 12345u64;
807 let core_block_height = 67u32;
808
809 let data = platform_value!({
811 "firstName": "Alice",
812 "lastName": "Liddell",
813 });
814
815 let doc = factory
816 .create_document(
817 &data_contract,
818 owner_id,
819 block_time,
820 core_block_height,
821 "indexedDocument".to_string(),
822 data,
823 )
824 .unwrap();
825
826 assert_eq!(doc.owner_id(), owner_id);
827 assert_ne!(doc.id().as_slice(), &[0u8; 32][..]);
829 }
830
831 #[cfg(feature = "state-transitions")]
834 mod state_transition_tests {
835 use super::*;
836 use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0;
837 use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionType;
838 use crate::state_transition::StateTransitionOwned;
839
840 fn build_document(
841 factory: &SpecializedDocumentFactoryV0,
842 owner: Identifier,
843 type_name: &str,
844 ) -> Document {
845 factory
846 .create_document_without_time_based_properties(
847 owner,
848 type_name.to_string(),
849 platform_value!({ "name": "foo" }),
850 )
851 .expect("document should be created")
852 }
853
854 #[test]
855 fn create_state_transition_create_action_populates_owner_and_nonce() {
856 let (factory, data_contract) = setup_factory();
857 let owner_id = Identifier::from([1u8; 32]);
858 let doc = build_document(&factory, owner_id, "noTimeDocument");
859 let doc_type = data_contract
860 .document_type_for_name("noTimeDocument")
861 .unwrap();
862
863 let mut nonce_counter: BTreeMap<(Identifier, Identifier), u64> = BTreeMap::new();
864 let entries = vec![(
865 DocumentTransitionActionType::Create,
866 vec![(doc, doc_type, Bytes32::new([2u8; 32]), None)],
867 )];
868
869 let batch = factory
870 .create_state_transition(entries, &mut nonce_counter)
871 .expect("batch transition should be created");
872
873 assert_eq!(batch.owner_id(), owner_id);
874 assert_eq!(batch.transitions_len(), 1);
875 let key = (owner_id, data_contract.id());
877 assert_eq!(*nonce_counter.get(&key).unwrap(), 1);
878 }
879
880 #[test]
881 fn create_state_transition_no_documents_returns_error() {
882 let (factory, _) = setup_factory();
883 let mut nonce_counter: BTreeMap<(Identifier, Identifier), u64> = BTreeMap::new();
884
885 let empty: Vec<(
887 DocumentTransitionActionType,
888 Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
889 )> = vec![];
890 let result = factory.create_state_transition(empty, &mut nonce_counter);
891 assert!(
892 matches!(
893 result,
894 Err(ProtocolError::Document(e)) if matches!(*e, DocumentError::NoDocumentsSuppliedError)
895 ),
896 "expected NoDocumentsSuppliedError"
897 );
898 }
899
900 #[test]
901 fn create_state_transition_mismatched_owners_returns_error() {
902 let (factory, data_contract) = setup_factory();
903 let owner_a = Identifier::from([1u8; 32]);
904 let owner_b = Identifier::from([2u8; 32]);
905 let doc_a = build_document(&factory, owner_a, "noTimeDocument");
906 let doc_b = build_document(&factory, owner_b, "noTimeDocument");
907 let doc_type = data_contract
908 .document_type_for_name("noTimeDocument")
909 .unwrap();
910
911 let mut nonce_counter = BTreeMap::new();
912 let entries = vec![(
913 DocumentTransitionActionType::Create,
914 vec![
915 (doc_a, doc_type, Bytes32::new([1u8; 32]), None),
916 (doc_b, doc_type, Bytes32::new([2u8; 32]), None),
917 ],
918 )];
919
920 let result = factory.create_state_transition(entries, &mut nonce_counter);
921 assert!(
922 matches!(
923 result,
924 Err(ProtocolError::Document(e))
925 if matches!(*e, DocumentError::MismatchOwnerIdsError { .. })
926 ),
927 "expected MismatchOwnerIdsError"
928 );
929 }
930
931 #[test]
932 fn create_state_transition_replace_on_mutable_document_increments_revision() {
933 let (factory, data_contract) = setup_factory();
934 let owner_id = Identifier::from([3u8; 32]);
935 let doc_type = data_contract
936 .document_type_for_name("noTimeDocument")
937 .unwrap();
938 let doc = build_document(&factory, owner_id, "noTimeDocument");
939 assert_eq!(doc.revision(), Some(INITIAL_REVISION));
941
942 let mut nonce_counter = BTreeMap::new();
943 let entries = vec![(
944 DocumentTransitionActionType::Replace,
945 vec![(doc, doc_type, Bytes32::default(), None)],
946 )];
947
948 let batch = factory
949 .create_state_transition(entries, &mut nonce_counter)
950 .expect("replace transition should be built");
951 assert_eq!(batch.transitions_len(), 1);
952 assert_eq!(batch.owner_id(), owner_id);
953 let key = (owner_id, data_contract.id());
954 assert_eq!(*nonce_counter.get(&key).unwrap(), 1);
955 }
956
957 #[test]
958 fn create_state_transition_replace_without_revision_returns_error() {
959 let (factory, data_contract) = setup_factory();
960 let owner_id = Identifier::from([4u8; 32]);
961 let doc_type = data_contract
962 .document_type_for_name("noTimeDocument")
963 .unwrap();
964 let mut doc = build_document(&factory, owner_id, "noTimeDocument");
965 doc.set_revision(None);
967
968 let mut nonce_counter = BTreeMap::new();
969 let entries = vec![(
970 DocumentTransitionActionType::Replace,
971 vec![(doc, doc_type, Bytes32::default(), None)],
972 )];
973 let result = factory.create_state_transition(entries, &mut nonce_counter);
974 assert!(
975 matches!(
976 result,
977 Err(ProtocolError::Document(e))
978 if matches!(*e, DocumentError::RevisionAbsentError { .. })
979 ),
980 "expected RevisionAbsentError"
981 );
982 }
983
984 #[test]
985 fn create_state_transition_create_with_wrong_initial_revision_returns_error() {
986 let (factory, data_contract) = setup_factory();
987 let owner_id = Identifier::from([5u8; 32]);
988 let doc_type = data_contract
989 .document_type_for_name("noTimeDocument")
990 .unwrap();
991 let mut doc = build_document(&factory, owner_id, "noTimeDocument");
992 doc.set_revision(Some(42));
994
995 let mut nonce_counter = BTreeMap::new();
996 let entries = vec![(
997 DocumentTransitionActionType::Create,
998 vec![(doc, doc_type, Bytes32::default(), None)],
999 )];
1000 let result = factory.create_state_transition(entries, &mut nonce_counter);
1001 assert!(
1002 matches!(
1003 result,
1004 Err(ProtocolError::Document(e))
1005 if matches!(*e, DocumentError::InvalidInitialRevisionError { .. })
1006 ),
1007 "expected InvalidInitialRevisionError"
1008 );
1009 }
1010
1011 #[test]
1012 fn create_state_transition_create_without_revision_on_mutable_returns_error() {
1013 let (factory, data_contract) = setup_factory();
1014 let owner_id = Identifier::from([6u8; 32]);
1015 let doc_type = data_contract
1016 .document_type_for_name("noTimeDocument")
1017 .unwrap();
1018 let mut doc = build_document(&factory, owner_id, "noTimeDocument");
1019 doc.set_revision(None);
1021
1022 let mut nonce_counter = BTreeMap::new();
1023 let entries = vec![(
1024 DocumentTransitionActionType::Create,
1025 vec![(doc, doc_type, Bytes32::default(), None)],
1026 )];
1027 let result = factory.create_state_transition(entries, &mut nonce_counter);
1028 assert!(
1029 matches!(
1030 result,
1031 Err(ProtocolError::Document(e))
1032 if matches!(*e, DocumentError::RevisionAbsentError { .. })
1033 ),
1034 "expected RevisionAbsentError"
1035 );
1036 }
1037
1038 #[test]
1039 fn create_state_transition_delete_with_mutable_doc() {
1040 let (factory, data_contract) = setup_factory();
1041 let owner_id = Identifier::from([7u8; 32]);
1042 let doc_type = data_contract
1043 .document_type_for_name("noTimeDocument")
1044 .unwrap();
1045 let doc = build_document(&factory, owner_id, "noTimeDocument");
1046
1047 let mut nonce_counter = BTreeMap::new();
1048 let entries = vec![(
1049 DocumentTransitionActionType::Delete,
1050 vec![(doc, doc_type, Bytes32::default(), None)],
1051 )];
1052 let batch = factory
1053 .create_state_transition(entries, &mut nonce_counter)
1054 .expect("delete transition should be built");
1055 assert_eq!(batch.transitions_len(), 1);
1056 let key = (owner_id, data_contract.id());
1057 assert_eq!(*nonce_counter.get(&key).unwrap(), 1);
1058 }
1059
1060 #[test]
1061 fn create_state_transition_delete_without_revision_returns_error() {
1062 let (factory, data_contract) = setup_factory();
1063 let owner_id = Identifier::from([8u8; 32]);
1064 let doc_type = data_contract
1065 .document_type_for_name("noTimeDocument")
1066 .unwrap();
1067 let mut doc = build_document(&factory, owner_id, "noTimeDocument");
1068 doc.set_revision(None);
1069
1070 let mut nonce_counter = BTreeMap::new();
1071 let entries = vec![(
1072 DocumentTransitionActionType::Delete,
1073 vec![(doc, doc_type, Bytes32::default(), None)],
1074 )];
1075 let result = factory.create_state_transition(entries, &mut nonce_counter);
1076 assert!(
1077 matches!(
1078 result,
1079 Err(ProtocolError::Document(e))
1080 if matches!(*e, DocumentError::RevisionAbsentError { .. })
1081 ),
1082 "expected RevisionAbsentError for delete without revision"
1083 );
1084 }
1085
1086 #[test]
1087 fn create_state_transition_nonces_increment_per_document() {
1088 let (factory, data_contract) = setup_factory();
1089 let owner_id = Identifier::from([9u8; 32]);
1090 let doc_type = data_contract
1091 .document_type_for_name("noTimeDocument")
1092 .unwrap();
1093 let d1 = build_document(&factory, owner_id, "noTimeDocument");
1094 let mut d2 = build_document(&factory, owner_id, "noTimeDocument");
1099 d2.set_id(Identifier::from([0xEEu8; 32]));
1101
1102 let mut nonce_counter = BTreeMap::new();
1103 nonce_counter.insert((owner_id, data_contract.id()), 10);
1105
1106 let entries = vec![(
1107 DocumentTransitionActionType::Create,
1108 vec![
1109 (d1, doc_type, Bytes32::new([1u8; 32]), None),
1110 (d2, doc_type, Bytes32::new([2u8; 32]), None),
1111 ],
1112 )];
1113 let _ = factory
1114 .create_state_transition(entries, &mut nonce_counter)
1115 .expect("transition should build");
1116
1117 assert_eq!(
1118 *nonce_counter.get(&(owner_id, data_contract.id())).unwrap(),
1119 12
1120 );
1121 }
1122 }
1123
1124 #[cfg(feature = "extended-document")]
1125 mod extended_document_tests {
1126 use super::*;
1127 use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
1128
1129 #[test]
1130 fn create_extended_from_document_buffer_roundtrips() {
1131 let (factory, data_contract) = setup_factory();
1132 let owner_id = Identifier::from([77u8; 32]);
1133
1134 let doc = factory
1135 .create_document_without_time_based_properties(
1136 owner_id,
1137 "noTimeDocument".to_string(),
1138 platform_value!({ "name": "bob" }),
1139 )
1140 .expect("doc should be created");
1141 let doc_type = data_contract
1142 .document_type_for_name("noTimeDocument")
1143 .unwrap();
1144
1145 let platform_version = PlatformVersion::latest();
1146 let bytes = doc
1147 .serialize(doc_type, &data_contract, platform_version)
1148 .expect("serialize");
1149
1150 let extended = factory
1151 .create_extended_from_document_buffer(
1152 bytes.as_slice(),
1153 "noTimeDocument",
1154 platform_version,
1155 )
1156 .expect("extended doc should deserialize");
1157
1158 assert_eq!(extended.data_contract_id(), data_contract.id());
1159 assert_eq!(extended.document_type_name(), "noTimeDocument");
1160 }
1161
1162 #[test]
1163 fn create_extended_from_document_buffer_invalid_type_fails() {
1164 let (factory, _) = setup_factory();
1165 let platform_version = PlatformVersion::latest();
1166 let result = factory.create_extended_from_document_buffer(
1167 &[0u8; 8],
1168 "doesNotExist",
1169 platform_version,
1170 );
1171 assert!(result.is_err());
1172 }
1173
1174 #[test]
1175 fn create_extended_from_document_buffer_bad_bytes_fails() {
1176 let (factory, _) = setup_factory();
1177 let platform_version = PlatformVersion::latest();
1178 let result = factory.create_extended_from_document_buffer(
1179 &[0xFFu8; 4],
1180 "noTimeDocument",
1181 platform_version,
1182 );
1183 assert!(result.is_err());
1184 }
1185 }
1186}