dpp/document/
mod.rs

1pub use fields::{property_names, IDENTIFIER_FIELDS};
2
3mod accessors;
4#[cfg(feature = "client")]
5mod document_facade;
6#[cfg(feature = "factories")]
7pub mod document_factory;
8pub mod document_methods;
9mod document_patch;
10pub mod errors;
11#[cfg(feature = "extended-document")]
12pub mod extended_document;
13mod fields;
14pub mod generate_document_id;
15pub mod serialization_traits;
16#[cfg(feature = "factories")]
17pub mod specialized_document_factory;
18pub mod transfer;
19mod v0;
20
21pub use accessors::*;
22pub use v0::*;
23
24#[cfg(feature = "extended-document")]
25pub use extended_document::property_names as extended_document_property_names;
26#[cfg(feature = "extended-document")]
27pub use extended_document::ExtendedDocument;
28#[cfg(feature = "extended-document")]
29pub use extended_document::IDENTIFIER_FIELDS as EXTENDED_DOCUMENT_IDENTIFIER_FIELDS;
30
31/// the initial revision of newly created document
32pub const INITIAL_REVISION: u64 = 1;
33
34use crate::data_contract::document_type::DocumentTypeRef;
35use crate::data_contract::DataContract;
36use crate::document::document_methods::{
37    DocumentGetRawForContractV0, DocumentGetRawForDocumentTypeV0, DocumentHashV0Method,
38    DocumentIsEqualIgnoringTimestampsV0, DocumentMethodsV0,
39};
40use crate::document::errors::DocumentError;
41use crate::version::PlatformVersion;
42use crate::ProtocolError;
43use derive_more::From;
44
45use std::fmt;
46use std::fmt::Formatter;
47
48#[derive(Clone, Debug, PartialEq, From)]
49#[cfg_attr(
50    any(feature = "serde-conversion", feature = "serde-conversion"),
51    derive(serde::Serialize, serde::Deserialize),
52    serde(tag = "$formatVersion")
53)]
54pub enum Document {
55    #[cfg_attr(
56        any(feature = "serde-conversion", feature = "serde-conversion"),
57        serde(rename = "0")
58    )]
59    V0(DocumentV0),
60}
61
62impl fmt::Display for Document {
63    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
64        match self {
65            Document::V0(v0) => {
66                write!(f, "v0 : {} ", v0)?;
67            }
68        }
69        Ok(())
70    }
71}
72
73impl DocumentMethodsV0 for Document {
74    /// Return a value given the path to its key and the document type for a contract.
75    fn get_raw_for_contract(
76        &self,
77        key: &str,
78        document_type_name: &str,
79        contract: &DataContract,
80        owner_id: Option<[u8; 32]>,
81        platform_version: &PlatformVersion,
82    ) -> Result<Option<Vec<u8>>, ProtocolError> {
83        match self {
84            Document::V0(document_v0) => {
85                match platform_version
86                    .dpp
87                    .document_versions
88                    .document_method_versions
89                    .get_raw_for_contract
90                {
91                    0 => document_v0.get_raw_for_contract_v0(
92                        key,
93                        document_type_name,
94                        contract,
95                        owner_id,
96                        platform_version,
97                    ),
98                    version => Err(ProtocolError::UnknownVersionMismatch {
99                        method: "DocumentMethodV0::get_raw_for_contract".to_string(),
100                        known_versions: vec![0],
101                        received: version,
102                    }),
103                }
104            }
105        }
106    }
107
108    /// Return a value given the path to its key for a document type.
109    fn get_raw_for_document_type(
110        &self,
111        key_path: &str,
112        document_type: DocumentTypeRef,
113        owner_id: Option<[u8; 32]>,
114        platform_version: &PlatformVersion,
115    ) -> Result<Option<Vec<u8>>, ProtocolError> {
116        match self {
117            Document::V0(document_v0) => {
118                match platform_version
119                    .dpp
120                    .document_versions
121                    .document_method_versions
122                    .get_raw_for_document_type
123                {
124                    0 => document_v0.get_raw_for_document_type_v0(
125                        key_path,
126                        document_type,
127                        owner_id,
128                        platform_version,
129                    ),
130                    version => Err(ProtocolError::UnknownVersionMismatch {
131                        method: "DocumentMethodV0::get_raw_for_document_type".to_string(),
132                        known_versions: vec![0],
133                        received: version,
134                    }),
135                }
136            }
137        }
138    }
139
140    fn hash(
141        &self,
142        contract: &DataContract,
143        document_type: DocumentTypeRef,
144        platform_version: &PlatformVersion,
145    ) -> Result<Vec<u8>, ProtocolError> {
146        match self {
147            Document::V0(document_v0) => {
148                match platform_version
149                    .dpp
150                    .document_versions
151                    .document_method_versions
152                    .hash
153                {
154                    0 => document_v0.hash_v0(contract, document_type, platform_version),
155                    version => Err(ProtocolError::UnknownVersionMismatch {
156                        method: "DocumentMethodV0::hash".to_string(),
157                        known_versions: vec![0],
158                        received: version,
159                    }),
160                }
161            }
162        }
163    }
164
165    fn increment_revision(&mut self) -> Result<(), ProtocolError> {
166        let Some(revision) = self.revision() else {
167            return Err(ProtocolError::Document(Box::new(
168                DocumentError::DocumentNoRevisionError {
169                    document: Box::new(self.clone()),
170                },
171            )));
172        };
173
174        let new_revision = revision
175            .checked_add(1)
176            .ok_or(ProtocolError::Overflow("overflow when adding 1"))?;
177
178        self.set_revision(Some(new_revision));
179
180        Ok(())
181    }
182
183    fn is_equal_ignoring_time_based_fields(
184        &self,
185        rhs: &Self,
186        also_ignore_fields: Option<Vec<&str>>,
187        platform_version: &PlatformVersion,
188    ) -> Result<bool, ProtocolError> {
189        match (self, rhs) {
190            (Document::V0(document_v0), Document::V0(rhs_v0)) => {
191                match platform_version
192                    .dpp
193                    .document_versions
194                    .document_method_versions
195                    .is_equal_ignoring_timestamps
196                {
197                    0 => Ok(document_v0
198                        .is_equal_ignoring_time_based_fields_v0(rhs_v0, also_ignore_fields)),
199                    version => Err(ProtocolError::UnknownVersionMismatch {
200                        method: "DocumentMethodV0::is_equal_ignoring_time_based_fields".to_string(),
201                        known_versions: vec![0],
202                        received: version,
203                    }),
204                }
205            }
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::data_contract::accessors::v0::DataContractV0Getters;
214    use crate::data_contract::document_type::random_document::CreateRandomDocument;
215    use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
216    use crate::tests::json_document::json_document_to_contract;
217
218    use regex::Regex;
219
220    #[test]
221    fn test_document_display() {
222        let platform_version = PlatformVersion::first();
223        let contract = json_document_to_contract(
224            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
225            false,
226            platform_version,
227        )
228        .expect("expected to get contract");
229
230        let document_type = contract
231            .document_type_for_name("profile")
232            .expect("expected to get profile document type");
233        let document = document_type
234            .random_document(Some(3333), platform_version)
235            .expect("expected to get a random document");
236
237        let document_string = format!("{}", document);
238        let pattern = r"v\d+ : id:45ZNwGcxeMpLpYmiVEKKBKXbZfinrhjZLkau1GWizPFX owner_id:2vq574DjKi7ZD8kJ6dMHxT5wu6ZKD2bW5xKAyKAGW7qZ created_at:(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) updated_at:(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) avatarUrl:string y8RD1DbW18RuyblDX7hx\[...\(670\)\] displayName:string y94Itl6mn1yBE publicMessage:string SvAQrzsslj0ESc15GQBQ\[...\(105\)\] .*";
239        let re = Regex::new(pattern).unwrap();
240        assert!(
241            re.is_match(document_string.as_str()),
242            "pattern: {} does not match {}",
243            pattern,
244            document_string
245        );
246    }
247
248    #[test]
249    fn test_serialization_and_deserialization() {
250        let platform_version = PlatformVersion::latest();
251        let contract = json_document_to_contract(
252            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
253            false,
254            platform_version,
255        )
256        .expect("expected to get contract");
257
258        let document_type = contract
259            .document_type_for_name("domain")
260            .expect("expected to get document type");
261        for _ in 0..20 {
262            let document = document_type
263                .random_document(None, platform_version)
264                .expect("expected a document");
265            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
266                &document,
267                document_type,
268                &contract,
269                platform_version,
270            )
271            .expect("should serialize");
272            let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
273                .expect("expected to deserialize domain document");
274        }
275    }
276
277    #[test]
278    fn test_serialize_deserialize_over_different_versions_of_document_type() {
279        let platform_version = PlatformVersion::latest();
280        let contract = json_document_to_contract(
281            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
282            false,
283            platform_version,
284        )
285        .expect("expected to get contract");
286
287        let updated_contract = json_document_to_contract(
288            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract-update-v2-test.json",
289            false,
290            platform_version,
291        )
292        .expect("expected to get contract");
293
294        let document_type = contract
295            .document_type_for_name("domain")
296            .expect("expected to get document type");
297
298        let updated_document_type = updated_contract
299            .document_type_for_name("domain")
300            .expect("expected to get document type");
301
302        // let's test from a document created in the old version, and we try to deserialize it in the new version
303        for _ in 0..20 {
304            let document = document_type
305                .random_document(None, platform_version)
306                .expect("expected a document");
307            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
308                &document,
309                document_type,
310                &contract,
311                platform_version,
312            )
313            .expect("should serialize");
314            let _deserialized =
315                Document::from_bytes(&serialized, updated_document_type, platform_version)
316                    .expect("expected to deserialize domain document");
317        }
318
319        // let's test from a document created in the new version, and we try to deserialize it with the old version
320        for _ in 0..20 {
321            let document = updated_document_type
322                .random_document(None, platform_version)
323                .expect("expected a document");
324            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
325                &document,
326                document_type,
327                &contract,
328                platform_version,
329            )
330            .expect("should serialize");
331            let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
332                .expect("expected to deserialize domain document");
333        }
334    }
335
336    // ================================================================
337    //  Display impl tests for Document
338    // ================================================================
339
340    #[test]
341    fn display_document_with_no_properties() {
342        let doc = Document::V0(DocumentV0 {
343            id: platform_value::Identifier::new([0xAA; 32]),
344            owner_id: platform_value::Identifier::new([0xBB; 32]),
345            properties: Default::default(),
346            revision: None,
347            created_at: None,
348            updated_at: None,
349            transferred_at: None,
350            created_at_block_height: None,
351            updated_at_block_height: None,
352            transferred_at_block_height: None,
353            created_at_core_block_height: None,
354            updated_at_core_block_height: None,
355            transferred_at_core_block_height: None,
356            creator_id: None,
357        });
358
359        let s = format!("{}", doc);
360        assert!(
361            s.contains("no properties"),
362            "should say 'no properties' when the BTreeMap is empty, got: {}",
363            s
364        );
365    }
366
367    #[test]
368    fn display_document_shows_transferred_at_fields() {
369        let doc = Document::V0(DocumentV0 {
370            id: platform_value::Identifier::new([1u8; 32]),
371            owner_id: platform_value::Identifier::new([2u8; 32]),
372            properties: Default::default(),
373            revision: None,
374            created_at: None,
375            updated_at: None,
376            transferred_at: Some(1_700_000_000_000),
377            created_at_block_height: None,
378            updated_at_block_height: None,
379            transferred_at_block_height: Some(500),
380            created_at_core_block_height: None,
381            updated_at_core_block_height: None,
382            transferred_at_core_block_height: Some(42),
383            creator_id: None,
384        });
385
386        let s = format!("{}", doc);
387        assert!(
388            s.contains("transferred_at:"),
389            "should contain transferred_at, got: {}",
390            s
391        );
392        assert!(
393            s.contains("transferred_at_block_height:500"),
394            "should contain transferred_at_block_height:500, got: {}",
395            s
396        );
397        assert!(
398            s.contains("transferred_at_core_block_height:42"),
399            "should contain transferred_at_core_block_height:42, got: {}",
400            s
401        );
402    }
403
404    #[test]
405    fn display_document_shows_creator_id() {
406        let creator = platform_value::Identifier::new([0xCC; 32]);
407        let doc = Document::V0(DocumentV0 {
408            id: platform_value::Identifier::new([1u8; 32]),
409            owner_id: platform_value::Identifier::new([2u8; 32]),
410            properties: Default::default(),
411            revision: None,
412            created_at: None,
413            updated_at: None,
414            transferred_at: None,
415            created_at_block_height: None,
416            updated_at_block_height: None,
417            transferred_at_block_height: None,
418            created_at_core_block_height: None,
419            updated_at_core_block_height: None,
420            transferred_at_core_block_height: None,
421            creator_id: Some(creator),
422        });
423
424        let s = format!("{}", doc);
425        assert!(
426            s.contains("creator_id:"),
427            "should contain creator_id, got: {}",
428            s
429        );
430    }
431
432    #[test]
433    fn display_document_shows_block_height_fields() {
434        let doc = Document::V0(DocumentV0 {
435            id: platform_value::Identifier::new([1u8; 32]),
436            owner_id: platform_value::Identifier::new([2u8; 32]),
437            properties: Default::default(),
438            revision: None,
439            created_at: None,
440            updated_at: None,
441            transferred_at: None,
442            created_at_block_height: Some(100),
443            updated_at_block_height: Some(200),
444            transferred_at_block_height: None,
445            created_at_core_block_height: Some(50),
446            updated_at_core_block_height: Some(60),
447            transferred_at_core_block_height: None,
448            creator_id: None,
449        });
450
451        let s = format!("{}", doc);
452        assert!(s.contains("created_at_block_height:100"), "got: {}", s);
453        assert!(s.contains("updated_at_block_height:200"), "got: {}", s);
454        assert!(s.contains("created_at_core_block_height:50"), "got: {}", s);
455        assert!(s.contains("updated_at_core_block_height:60"), "got: {}", s);
456    }
457
458    // ================================================================
459    //  Version dispatch: increment_revision
460    // ================================================================
461
462    #[test]
463    fn increment_revision_works_on_mutable_document() {
464        let mut doc = Document::V0(DocumentV0 {
465            id: platform_value::Identifier::new([1u8; 32]),
466            owner_id: platform_value::Identifier::new([2u8; 32]),
467            properties: Default::default(),
468            revision: Some(1),
469            created_at: None,
470            updated_at: None,
471            transferred_at: None,
472            created_at_block_height: None,
473            updated_at_block_height: None,
474            transferred_at_block_height: None,
475            created_at_core_block_height: None,
476            updated_at_core_block_height: None,
477            transferred_at_core_block_height: None,
478            creator_id: None,
479        });
480
481        doc.increment_revision()
482            .expect("increment_revision should succeed");
483        assert_eq!(doc.revision(), Some(2));
484    }
485
486    #[test]
487    fn increment_revision_fails_when_no_revision() {
488        let mut doc = Document::V0(DocumentV0 {
489            id: platform_value::Identifier::new([1u8; 32]),
490            owner_id: platform_value::Identifier::new([2u8; 32]),
491            properties: Default::default(),
492            revision: None,
493            created_at: None,
494            updated_at: None,
495            transferred_at: None,
496            created_at_block_height: None,
497            updated_at_block_height: None,
498            transferred_at_block_height: None,
499            created_at_core_block_height: None,
500            updated_at_core_block_height: None,
501            transferred_at_core_block_height: None,
502            creator_id: None,
503        });
504
505        let result = doc.increment_revision();
506        assert!(
507            result.is_err(),
508            "increment_revision should fail when revision is None"
509        );
510    }
511
512    // ================================================================
513    //  Version dispatch: is_equal_ignoring_time_based_fields
514    // ================================================================
515
516    #[test]
517    fn is_equal_ignoring_time_based_fields_dispatches_correctly() {
518        let platform_version = PlatformVersion::latest();
519        let contract = json_document_to_contract(
520            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
521            false,
522            platform_version,
523        )
524        .expect("expected to get contract");
525
526        let document_type = contract
527            .document_type_for_name("profile")
528            .expect("expected to get profile document type");
529
530        let doc1 = document_type
531            .random_document(Some(42), platform_version)
532            .expect("expected random document");
533
534        let mut doc2 = doc1.clone();
535        // Change timestamps
536        doc2.set_created_at(Some(9_999_999));
537        doc2.set_updated_at(Some(8_888_888));
538
539        let result = doc1
540            .is_equal_ignoring_time_based_fields(&doc2, None, platform_version)
541            .expect("should succeed");
542        assert!(
543            result,
544            "same document with different timestamps should be equal ignoring time fields"
545        );
546    }
547
548    // ================================================================
549    //  Version dispatch: get_raw_for_contract
550    // ================================================================
551
552    #[test]
553    fn get_raw_for_contract_dispatches_to_v0() {
554        let platform_version = PlatformVersion::latest();
555        let contract = json_document_to_contract(
556            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
557            false,
558            platform_version,
559        )
560        .expect("expected to get contract");
561
562        let document_type = contract
563            .document_type_for_name("profile")
564            .expect("expected to get profile document type");
565
566        let document = document_type
567            .random_document(Some(7), platform_version)
568            .expect("expected random document");
569
570        let raw_id = document
571            .get_raw_for_contract("$id", "profile", &contract, None, platform_version)
572            .expect("should succeed");
573        assert_eq!(raw_id, Some(document.id().to_vec()));
574    }
575
576    // ================================================================
577    //  Version dispatch: hash
578    // ================================================================
579
580    #[test]
581    fn document_hash_is_deterministic() {
582        let platform_version = PlatformVersion::latest();
583        let contract = json_document_to_contract(
584            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
585            false,
586            platform_version,
587        )
588        .expect("expected to get contract");
589
590        let document_type = contract
591            .document_type_for_name("profile")
592            .expect("expected to get profile document type");
593
594        let document = document_type
595            .random_document(Some(42), platform_version)
596            .expect("expected random document");
597
598        let hash1 = document
599            .hash(&contract, document_type, platform_version)
600            .expect("hash should succeed");
601        let hash2 = document
602            .hash(&contract, document_type, platform_version)
603            .expect("hash should succeed");
604        assert_eq!(hash1, hash2, "hash should be deterministic");
605        assert!(!hash1.is_empty(), "hash should not be empty");
606    }
607}