Skip to main content

dpp/document/specialized_document_factory/v0/
mod.rs

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
38/// Factory for creating documents
39pub 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>, //IdentityID/ContractID -> nonce
200    ) -> Result<BatchTransition, ProtocolError> {
201        let platform_version = PlatformVersion::get(self.protocol_version)?;
202        // TODO: Use struct
203        #[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    //
317    // pub fn create_from_buffer(
318    //     &self,
319    //     buffer: impl AsRef<[u8]>,
320    // ) -> Result<ExtendedDocument, ProtocolError> {
321    //     let document = <ExtendedDocument as PlatformDeserializable>::deserialize(buffer.as_ref())
322    //         .map_err(|e| {
323    //             ConsensusError::BasicError(BasicError::SerializedObjectParsingError(
324    //                 SerializedObjectParsingError::new(format!("Decode protocol entity: {:#?}", e)),
325    //             ))
326    //         })?;
327    //     self.create_from_object(document.to_value()?).await
328    // }
329    //
330    // pub fn create_from_object(
331    //     &self,
332    //     raw_document: Value,
333    // ) -> Result<ExtendedDocument, ProtocolError> {
334    //     ExtendedDocument::from_untrusted_platform_value(raw_document, data_contract)
335    // }
336    // //
337    // // async fn validate_data_contract_for_extended_document(
338    // //     &self,
339    // //     raw_document: &Value,
340    // //     options: FactoryOptions,
341    // // ) -> Result<DataContract, ProtocolError> {
342    // //     let result = self
343    // //         .data_contract_fetcher_and_validator
344    // //         .validate_extended(raw_document)
345    // //         .await?;
346    // //
347    // //     if !result.is_valid() {
348    // //         return Err(ProtocolError::Document(Box::new(
349    // //             DocumentError::InvalidDocumentError {
350    // //                 errors: result.errors,
351    // //                 raw_document: raw_document.clone(),
352    // //             },
353    // //         )));
354    // //     }
355    // //     let data_contract = result
356    // //         .into_data()
357    // //         .context("Validator didn't return Data Contract. This shouldn't happen")?;
358    // //
359    // //     if !options.skip_validation {
360    // //         let result = self
361    // //             .document_validator
362    // //             .validate_extended(raw_document, &data_contract)?;
363    // //         if !result.is_valid() {
364    // //             return Err(ProtocolError::Document(Box::new(
365    // //                 DocumentError::InvalidDocumentError {
366    // //                     errors: result.errors,
367    // //                     raw_document: raw_document.clone(),
368    // //                 },
369    // //             )));
370    // //         }
371    // //     }
372    // //
373    // //     Ok(data_contract)
374    // // }
375    //
376    #[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>, //IdentityID/ContractID -> nonce
380        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                    //we need to have revisions
387                    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>, //IdentityID/ContractID -> nonce
426        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        // let mut raw_transitions = vec![];
467        // for (document, document_type) in documents {
468        //     if !document_type.documents_mutable() {
469        //         return Err(DocumentError::TryingToReplaceImmutableDocument {
470        //             document: Box::new(document),
471        //         }
472        //         .into());
473        //     }
474        //     let Some(document_revision) = document.revision() else {
475        //         return Err(DocumentError::RevisionAbsentError {
476        //             document: Box::new(document),
477        //         }.into());
478        //     };
479        //     let mut map = document.to_map_value()?;
480        //
481        //     map.retain(|key, _| {
482        //         !key.starts_with('$') || DOCUMENT_REPLACE_KEYS_TO_STAY.contains(&key.as_str())
483        //     });
484        //     map.insert(
485        //         PROPERTY_ACTION.to_string(),
486        //         Value::U8(DocumentTransitionActionType::Replace as u8),
487        //     );
488        //     let new_revision = document_revision + 1;
489        //     map.insert(PROPERTY_REVISION.to_string(), Value::U64(new_revision));
490        //
491        //     // If document have an originally set `updatedAt`
492        //     // we should update it then
493        //     let contains_updated_at = document_type
494        //         .required_fields()
495        //         .contains(PROPERTY_UPDATED_AT);
496        //
497        //     if contains_updated_at {
498        //         let now = Utc::now().timestamp_millis() as TimestampMillis;
499        //         map.insert(PROPERTY_UPDATED_AT.to_string(), Value::U64(now));
500        //     }
501        //
502        //     raw_transitions.push(map.into());
503        // }
504        // Ok(raw_transitions)
505    }
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>, //IdentityID/ContractID -> nonce
511        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    /// A deterministic entropy generator for tests
563    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        // Just verify it was created without panic
591        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    // ----- Extended coverage -----
727
728    #[test]
729    fn new_with_invalid_protocol_version_still_constructs() {
730        // `new` does not validate version; errors surface only during creation.
731        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        // Two documents for the same type with the same owner+entropy should get identical IDs.
780        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        // Identifier is derived from (contract_id, owner_id, type_name, entropy); all match → same.
799        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        // indexedDocument requires createdAt/updatedAt to be set. Provide explicit values.
810        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        // the doc should at least have a non-empty id
828        assert_ne!(doc.id().as_slice(), &[0u8; 32][..]);
829    }
830
831    // ----- State transition tests -----
832
833    #[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            // nonce started at 0 and incremented to 1
876            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            // empty outer iter
886            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            // noTimeDocument is mutable by default → revision is Some(1)
940            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            // Remove revision to trigger RevisionAbsentError.
966            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            // Invalid initial revision: must be INITIAL_REVISION (1).
993            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            // For mutable documents, revision is required.
1020            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            // produce a second doc with distinct id via different entropy/type path —
1095            // the entropy comes from the factory, but owner is same; we reuse same doc type
1096            // but set distinct revision-preserving id manually not needed, because nonce counter
1097            // is keyed by (owner, contract).
1098            let mut d2 = build_document(&factory, owner_id, "noTimeDocument");
1099            // ensure distinct id so the two can legitimately coexist
1100            d2.set_id(Identifier::from([0xEEu8; 32]));
1101
1102            let mut nonce_counter = BTreeMap::new();
1103            // pre-seed a nonce so we can assert the post-value is base + 2
1104            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}