dpp/document/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;
19use crate::document::document_methods::DocumentMethodsV0;
20#[cfg(feature = "extended-document")]
21use crate::document::{
22    extended_document::v0::ExtendedDocumentV0,
23    ExtendedDocument, serialization_traits::DocumentPlatformConversionMethodsV0,
24};
25use crate::prelude::{BlockHeight, CoreBlockHeight, TimestampMillis};
26#[cfg(feature = "state-transitions")]
27use crate::state_transition::batch_transition::{
28    batched_transition::{
29        document_transition_action_type::DocumentTransitionActionType, DocumentCreateTransition,
30        DocumentDeleteTransition, DocumentReplaceTransition,
31    },
32    BatchTransition, BatchTransitionV0,
33};
34use itertools::Itertools;
35#[cfg(feature = "state-transitions")]
36use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition;
37use crate::tokens::token_payment_info::TokenPaymentInfo;
38
39/// Factory for creating documents
40pub struct DocumentFactoryV0 {
41    protocol_version: u32,
42    entropy_generator: Box<dyn EntropyGenerator>,
43}
44
45impl DocumentFactoryV0 {
46    pub fn new(protocol_version: u32) -> Self {
47        DocumentFactoryV0 {
48            protocol_version,
49            entropy_generator: Box::new(DefaultEntropyGenerator),
50        }
51    }
52
53    pub fn new_with_entropy_generator(
54        protocol_version: u32,
55        entropy_generator: Box<dyn EntropyGenerator>,
56    ) -> Self {
57        DocumentFactoryV0 {
58            protocol_version,
59            entropy_generator,
60        }
61    }
62
63    pub fn create_document(
64        &self,
65        data_contract: &DataContract,
66        owner_id: Identifier,
67        block_time: BlockHeight,
68        core_block_height: CoreBlockHeight,
69        document_type_name: String,
70        data: Value,
71    ) -> Result<Document, ProtocolError> {
72        let platform_version = PlatformVersion::get(self.protocol_version)?;
73        if !data_contract.has_document_type_for_name(&document_type_name) {
74            return Err(DataContractError::InvalidDocumentTypeError(
75                InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
76            )
77            .into());
78        }
79
80        let document_entropy = self.entropy_generator.generate()?;
81
82        let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
83
84        document_type.create_document_from_data(
85            data,
86            owner_id,
87            block_time,
88            core_block_height,
89            document_entropy,
90            platform_version,
91        )
92    }
93
94    pub fn create_document_without_time_based_properties(
95        &self,
96        data_contract: &DataContract,
97        owner_id: Identifier,
98        document_type_name: String,
99        data: Value,
100    ) -> Result<Document, ProtocolError> {
101        let platform_version = PlatformVersion::get(self.protocol_version)?;
102        if !data_contract.has_document_type_for_name(&document_type_name) {
103            return Err(DataContractError::InvalidDocumentTypeError(
104                InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
105            )
106            .into());
107        }
108
109        let document_entropy = self.entropy_generator.generate()?;
110
111        let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
112
113        document_type.create_document_from_data(
114            data,
115            owner_id,
116            0,
117            0,
118            document_entropy,
119            platform_version,
120        )
121    }
122
123    #[cfg(feature = "extended-document")]
124    pub fn create_extended_document(
125        &self,
126        data_contract: &DataContract,
127        owner_id: Identifier,
128        document_type_name: String,
129        data: Value,
130    ) -> Result<ExtendedDocument, ProtocolError> {
131        let platform_version = PlatformVersion::get(self.protocol_version)?;
132        if !data_contract.has_document_type_for_name(&document_type_name) {
133            return Err(DataContractError::InvalidDocumentTypeError(
134                InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
135            )
136            .into());
137        }
138
139        let document_entropy = self.entropy_generator.generate()?;
140
141        let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
142
143        // Extended documents are client side, so we don't need to fill in their timestamp properties
144        let document = document_type.create_document_from_data(
145            data,
146            owner_id,
147            0,
148            0,
149            document_entropy,
150            platform_version,
151        )?;
152
153        let extended_document = match platform_version
154            .dpp
155            .document_versions
156            .extended_document_structure_version
157        {
158            0 => Ok(ExtendedDocumentV0 {
159                document_type_name,
160                data_contract_id: data_contract.id(),
161                document,
162                data_contract: data_contract.clone(),
163                metadata: None,
164                entropy: Bytes32::new(document_entropy),
165                token_payment_info: None,
166            }
167            .into()),
168            version => Err(ProtocolError::UnknownVersionMismatch {
169                method: "DocumentFactory::create_extended_document".to_string(),
170                known_versions: vec![0],
171                received: version,
172            }),
173        }?;
174
175        Ok(extended_document)
176    }
177    #[cfg(feature = "state-transitions")]
178    pub fn create_state_transition<'a>(
179        &self,
180        documents_iter: impl IntoIterator<
181            Item = (
182                DocumentTransitionActionType,
183                Vec<(
184                    Document,
185                    DocumentTypeRef<'a>,
186                    Bytes32,
187                    Option<TokenPaymentInfo>,
188                )>,
189            ),
190        >,
191        nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, //IdentityID/ContractID -> nonce
192    ) -> Result<BatchTransition, ProtocolError> {
193        let platform_version = PlatformVersion::get(self.protocol_version)?;
194        // TODO: Use struct or types
195        #[allow(clippy::type_complexity)]
196        let documents: Vec<(
197            DocumentTransitionActionType,
198            Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
199        )> = documents_iter.into_iter().collect();
200        let mut flattened_documents_iter = documents.iter().flat_map(|(_, v)| v).peekable();
201
202        let Some((first_document, _, _, _)) = flattened_documents_iter.peek() else {
203            return Err(DocumentError::NoDocumentsSuppliedError.into());
204        };
205
206        let owner_id = first_document.owner_id();
207
208        let is_the_same_owner =
209            flattened_documents_iter.all(|(document, _, _, _)| document.owner_id() == owner_id);
210        if !is_the_same_owner {
211            return Err(DocumentError::MismatchOwnerIdsError {
212                documents: documents
213                    .into_iter()
214                    .flat_map(|(_, v)| {
215                        v.into_iter()
216                            .map(|(document, _, _, _)| document)
217                            .collect::<Vec<_>>()
218                    })
219                    .collect(),
220            }
221            .into());
222        }
223
224        let transitions: Vec<_> = documents
225            .into_iter()
226            .map(|(action, documents)| match action {
227                DocumentTransitionActionType::Create => {
228                    Self::document_create_transitions(documents, nonce_counter, platform_version)
229                }
230                DocumentTransitionActionType::Delete => Self::document_delete_transitions(
231                    documents
232                        .into_iter()
233                        .map(|(document, document_type, _, token_payment_info)| {
234                            (document, document_type, token_payment_info)
235                        })
236                        .collect(),
237                    nonce_counter,
238                    platform_version,
239                ),
240                DocumentTransitionActionType::Replace => Self::document_replace_transitions(
241                    documents
242                        .into_iter()
243                        .map(|(document, document_type, _, token_payment_info)| {
244                            (document, document_type, token_payment_info)
245                        })
246                        .collect(),
247                    nonce_counter,
248                    platform_version,
249                ),
250                _ => Err(ProtocolError::InvalidStateTransitionType(
251                    "action type not accounted for".to_string(),
252                )),
253            })
254            .collect::<Result<Vec<_>, ProtocolError>>()?
255            .into_iter()
256            .flatten()
257            .collect();
258
259        if transitions.is_empty() {
260            return Err(DocumentError::NoDocumentsSuppliedError.into());
261        }
262
263        Ok(BatchTransitionV0 {
264            owner_id,
265            transitions,
266            user_fee_increase: 0,
267            signature_public_key_id: 0,
268            signature: Default::default(),
269        }
270        .into())
271    }
272
273    #[cfg(feature = "extended-document")]
274    pub fn create_extended_from_document_buffer(
275        &self,
276        buffer: &[u8],
277        document_type_name: &str,
278        data_contract: &DataContract,
279        platform_version: &PlatformVersion,
280    ) -> Result<ExtendedDocument, ProtocolError> {
281        let document_type = data_contract.document_type_for_name(document_type_name)?;
282
283        let document = Document::from_bytes(buffer, document_type, platform_version)?;
284
285        match platform_version
286            .dpp
287            .document_versions
288            .extended_document_structure_version
289        {
290            0 => Ok(ExtendedDocumentV0 {
291                document_type_name: document_type_name.to_string(),
292                data_contract_id: data_contract.id(),
293                document,
294                data_contract: data_contract.clone(),
295                metadata: None,
296                entropy: Bytes32::default(),
297                token_payment_info: None,
298            }
299            .into()),
300            version => Err(ProtocolError::UnknownVersionMismatch {
301                method: "DocumentFactory::create_extended_from_document_buffer".to_string(),
302                known_versions: vec![0],
303                received: version,
304            }),
305        }
306    }
307    //
308    // pub fn create_from_buffer(
309    //     &self,
310    //     buffer: impl AsRef<[u8]>,
311    // ) -> Result<ExtendedDocument, ProtocolError> {
312    //     let document = <ExtendedDocument as PlatformDeserializable>::deserialize(buffer.as_ref())
313    //         .map_err(|e| {
314    //             ConsensusError::BasicError(BasicError::SerializedObjectParsingError(
315    //                 SerializedObjectParsingError::new(format!("Decode protocol entity: {:#?}", e)),
316    //             ))
317    //         })?;
318    //     self.create_from_object(document.to_value()?).await
319    // }
320    //
321    // pub fn create_from_object(
322    //     &self,
323    //     raw_document: Value,
324    // ) -> Result<ExtendedDocument, ProtocolError> {
325    //     ExtendedDocument::from_untrusted_platform_value(raw_document, data_contract)
326    // }
327    // //
328    // // async fn validate_data_contract_for_extended_document(
329    // //     &self,
330    // //     raw_document: &Value,
331    // //     options: FactoryOptions,
332    // // ) -> Result<DataContract, ProtocolError> {
333    // //     let result = self
334    // //         .data_contract_fetcher_and_validator
335    // //         .validate_extended(raw_document)
336    // //         .await?;
337    // //
338    // //     if !result.is_valid() {
339    // //         return Err(ProtocolError::Document(Box::new(
340    // //             DocumentError::InvalidDocumentError {
341    // //                 errors: result.errors,
342    // //                 raw_document: raw_document.clone(),
343    // //             },
344    // //         )));
345    // //     }
346    // //     let data_contract = result
347    // //         .into_data()
348    // //         .context("Validator didn't return Data Contract. This shouldn't happen")?;
349    // //
350    // //     if !options.skip_validation {
351    // //         let result = self
352    // //             .document_validator
353    // //             .validate_extended(raw_document, &data_contract)?;
354    // //         if !result.is_valid() {
355    // //             return Err(ProtocolError::Document(Box::new(
356    // //                 DocumentError::InvalidDocumentError {
357    // //                     errors: result.errors,
358    // //                     raw_document: raw_document.clone(),
359    // //                 },
360    // //             )));
361    // //         }
362    // //     }
363    // //
364    // //     Ok(data_contract)
365    // // }
366    //
367    #[cfg(feature = "state-transitions")]
368    fn document_create_transitions(
369        documents: Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
370        nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, //IdentityID/ContractID -> nonce
371        platform_version: &PlatformVersion,
372    ) -> Result<Vec<DocumentTransition>, ProtocolError> {
373        documents
374            .into_iter()
375            .map(|(document, document_type, entropy, token_payment_info)| {
376                if document_type.documents_mutable() {
377                    //we need to have revisions
378                    let Some(revision) = document.revision() else {
379                        return Err(DocumentError::RevisionAbsentError {
380                            document: Box::new(document),
381                        }
382                        .into());
383                    };
384                    if revision != INITIAL_REVISION {
385                        return Err(DocumentError::InvalidInitialRevisionError {
386                            document: Box::new(document),
387                        }
388                        .into());
389                    }
390                }
391                let nonce = nonce_counter
392                    .entry((document.owner_id(), document_type.data_contract_id()))
393                    .or_default();
394
395                let transition = DocumentCreateTransition::from_document(
396                    document,
397                    document_type,
398                    entropy.to_buffer(),
399                    token_payment_info,
400                    *nonce,
401                    platform_version,
402                    None,
403                    None,
404                )?;
405
406                *nonce += 1;
407
408                Ok(transition.into())
409            })
410            .collect()
411    }
412
413    #[cfg(feature = "state-transitions")]
414    fn document_replace_transitions(
415        documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
416        nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, //IdentityID/ContractID -> nonce
417        platform_version: &PlatformVersion,
418    ) -> Result<Vec<DocumentTransition>, ProtocolError> {
419        documents
420            .into_iter()
421            .map(|(mut document, document_type, token_payment_info)| {
422                if !document_type.documents_mutable() {
423                    return Err(DocumentError::TryingToReplaceImmutableDocument {
424                        document: Box::new(document),
425                    }
426                    .into());
427                }
428                if document.revision().is_none() {
429                    return Err(DocumentError::RevisionAbsentError {
430                        document: Box::new(document),
431                    }
432                    .into());
433                };
434
435                document.increment_revision()?;
436                document.set_updated_at(Some(Utc::now().timestamp_millis() as TimestampMillis));
437
438                let nonce = nonce_counter
439                    .entry((document.owner_id(), document_type.data_contract_id()))
440                    .or_default();
441
442                let transition = DocumentReplaceTransition::from_document(
443                    document,
444                    document_type,
445                    token_payment_info,
446                    *nonce,
447                    platform_version,
448                    None,
449                    None,
450                )?;
451
452                *nonce += 1;
453
454                Ok(transition.into())
455            })
456            .collect()
457        // let mut raw_transitions = vec![];
458        // for (document, document_type) in documents {
459        //     if !document_type.documents_mutable() {
460        //         return Err(DocumentError::TryingToReplaceImmutableDocument {
461        //             document: Box::new(document),
462        //         }
463        //         .into());
464        //     }
465        //     let Some(document_revision) = document.revision() else {
466        //         return Err(DocumentError::RevisionAbsentError {
467        //             document: Box::new(document),
468        //         }.into());
469        //     };
470        //     let mut map = document.to_map_value()?;
471        //
472        //     map.retain(|key, _| {
473        //         !key.starts_with('$') || DOCUMENT_REPLACE_KEYS_TO_STAY.contains(&key.as_str())
474        //     });
475        //     map.insert(
476        //         PROPERTY_ACTION.to_string(),
477        //         Value::U8(DocumentTransitionActionType::Replace as u8),
478        //     );
479        //     let new_revision = document_revision + 1;
480        //     map.insert(PROPERTY_REVISION.to_string(), Value::U64(new_revision));
481        //
482        //     // If document have an originally set `updatedAt`
483        //     // we should update it then
484        //     let contains_updated_at = document_type
485        //         .required_fields()
486        //         .contains(PROPERTY_UPDATED_AT);
487        //
488        //     if contains_updated_at {
489        //         let now = Utc::now().timestamp_millis() as TimestampMillis;
490        //         map.insert(PROPERTY_UPDATED_AT.to_string(), Value::U64(now));
491        //     }
492        //
493        //     raw_transitions.push(map.into());
494        // }
495        // Ok(raw_transitions)
496    }
497
498    #[cfg(feature = "state-transitions")]
499    fn document_delete_transitions(
500        documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
501        nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, //IdentityID/ContractID -> nonce
502        platform_version: &PlatformVersion,
503    ) -> Result<Vec<DocumentTransition>, ProtocolError> {
504        documents
505            .into_iter()
506            .map(|(document, document_type, token_payment_info)| {
507                if !document_type.documents_can_be_deleted() {
508                    return Err(DocumentError::TryingToDeleteIndelibleDocument {
509                        document: Box::new(document),
510                    }
511                    .into());
512                }
513                let Some(_document_revision) = document.revision() else {
514                    return Err(DocumentError::RevisionAbsentError {
515                        document: Box::new(document),
516                    }
517                    .into());
518                };
519
520                let nonce = nonce_counter
521                    .entry((document.owner_id(), document_type.data_contract_id()))
522                    .or_default();
523                let transition = DocumentDeleteTransition::from_document(
524                    document,
525                    document_type,
526                    token_payment_info,
527                    *nonce,
528                    platform_version,
529                    None,
530                    None,
531                )?;
532
533                *nonce += 1;
534
535                Ok(transition.into())
536            })
537            .collect()
538    }
539
540    fn is_ownership_the_same<'a>(ids: impl IntoIterator<Item = &'a Identifier>) -> bool {
541        ids.into_iter().all_equal()
542    }
543}
544
545#[cfg(test)]
546mod test {
547    use data_contracts::SystemDataContract;
548    use platform_version::version::PlatformVersion;
549    use std::collections::BTreeMap;
550
551    use crate::data_contract::accessors::v0::DataContractV0Getters;
552    use crate::document::document_factory::DocumentFactoryV0;
553    use crate::document::{Document, DocumentV0};
554    use crate::identifier::Identifier;
555    use crate::system_data_contracts::load_system_data_contract;
556
557    #[test]
558    /// Create a delete transition in DocumentFactoryV0 for an immutable but deletable document type
559    fn delete_immutable_but_deletable_documents() {
560        // Get a ref to the dpns preorder document type to pass to the factory
561        let dpns_contract =
562            load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest()).unwrap();
563        let document_type = dpns_contract
564            .document_type_borrowed_for_name("preorder")
565            .unwrap();
566        let document_type_ref = document_type.as_ref();
567
568        // Create a preorder document
569        let document_id = Identifier::random();
570        let owner_id = Identifier::random();
571        let mut properties = BTreeMap::new();
572        properties.insert(
573            "saltedDomainHash".to_string(),
574            platform_value::Value::Array(vec![]),
575        );
576        let document_v0 = DocumentV0 {
577            id: document_id,
578            owner_id,
579            properties,
580            revision: Some(1),
581            created_at: None,
582            updated_at: None,
583            transferred_at: None,
584            created_at_block_height: None,
585            updated_at_block_height: None,
586            transferred_at_block_height: None,
587            created_at_core_block_height: None,
588            updated_at_core_block_height: None,
589            transferred_at_core_block_height: None,
590            creator_id: None,
591        };
592        let document = Document::V0(document_v0);
593
594        // This will be passed to the factory
595        let documents = vec![(document, document_type_ref, None)];
596        let mut nonce_counter = BTreeMap::new();
597        let platform_version = PlatformVersion::latest();
598
599        // Try to create the delete transition in the factory
600        let result = DocumentFactoryV0::document_delete_transitions(
601            documents,
602            &mut nonce_counter,
603            platform_version,
604        );
605
606        // Checks
607        assert!(result.is_ok(), "The function should succeed");
608        let transitions = result.unwrap();
609        assert_eq!(transitions.len(), 1, "There should be one transition");
610    }
611}