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)]
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    /// A deterministic entropy generator for tests
562    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        // Just verify it was created without panic
590        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}