Skip to main content

dpp/document/
mod.rs

1pub use fields::{property_names, IDENTIFIER_FIELDS};
2
3mod accessors;
4pub mod document_event;
5#[cfg(feature = "client")]
6mod document_facade;
7#[cfg(feature = "factories")]
8pub mod document_factory;
9pub mod document_methods;
10mod document_patch;
11pub mod errors;
12#[cfg(feature = "extended-document")]
13pub mod extended_document;
14mod fields;
15pub mod generate_document_id;
16pub mod serialization_traits;
17#[cfg(feature = "factories")]
18pub mod specialized_document_factory;
19pub mod transfer;
20mod v0;
21
22pub use accessors::*;
23pub use v0::*;
24
25#[cfg(feature = "extended-document")]
26pub use extended_document::property_names as extended_document_property_names;
27#[cfg(feature = "extended-document")]
28pub use extended_document::ExtendedDocument;
29#[cfg(feature = "extended-document")]
30pub use extended_document::IDENTIFIER_FIELDS as EXTENDED_DOCUMENT_IDENTIFIER_FIELDS;
31
32/// the initial revision of newly created document
33pub const INITIAL_REVISION: u64 = 1;
34
35use crate::data_contract::document_type::DocumentTypeRef;
36use crate::data_contract::DataContract;
37use crate::document::document_methods::{
38    DocumentGetRawForContractV0, DocumentGetRawForDocumentTypeV0, DocumentHashV0Method,
39    DocumentIsEqualIgnoringTimestampsV0, DocumentMethodsV0,
40};
41use crate::document::errors::DocumentError;
42use crate::version::PlatformVersion;
43use crate::ProtocolError;
44use derive_more::From;
45
46use std::fmt;
47use std::fmt::Formatter;
48
49#[derive(Clone, Debug, PartialEq, From)]
50#[cfg_attr(
51    feature = "serde-conversion",
52    derive(serde::Serialize, serde::Deserialize),
53    serde(tag = "$formatVersion")
54)]
55pub enum Document {
56    #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
57    V0(DocumentV0),
58}
59
60#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
61impl crate::serialization::JsonConvertible for Document {}
62
63#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
64impl crate::serialization::ValueConvertible for Document {}
65
66impl fmt::Display for Document {
67    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
68        match self {
69            Document::V0(v0) => {
70                write!(f, "v0 : {} ", v0)?;
71            }
72        }
73        Ok(())
74    }
75}
76
77impl DocumentMethodsV0 for Document {
78    /// Return a value given the path to its key and the document type for a contract.
79    fn get_raw_for_contract(
80        &self,
81        key: &str,
82        document_type_name: &str,
83        contract: &DataContract,
84        owner_id: Option<[u8; 32]>,
85        platform_version: &PlatformVersion,
86    ) -> Result<Option<Vec<u8>>, ProtocolError> {
87        match self {
88            Document::V0(document_v0) => {
89                match platform_version
90                    .dpp
91                    .document_versions
92                    .document_method_versions
93                    .get_raw_for_contract
94                {
95                    0 => document_v0.get_raw_for_contract_v0(
96                        key,
97                        document_type_name,
98                        contract,
99                        owner_id,
100                        platform_version,
101                    ),
102                    version => Err(ProtocolError::UnknownVersionMismatch {
103                        method: "DocumentMethodV0::get_raw_for_contract".to_string(),
104                        known_versions: vec![0],
105                        received: version,
106                    }),
107                }
108            }
109        }
110    }
111
112    /// Return a value given the path to its key for a document type.
113    fn get_raw_for_document_type(
114        &self,
115        key_path: &str,
116        document_type: DocumentTypeRef,
117        owner_id: Option<[u8; 32]>,
118        platform_version: &PlatformVersion,
119    ) -> Result<Option<Vec<u8>>, ProtocolError> {
120        match self {
121            Document::V0(document_v0) => {
122                match platform_version
123                    .dpp
124                    .document_versions
125                    .document_method_versions
126                    .get_raw_for_document_type
127                {
128                    0 => document_v0.get_raw_for_document_type_v0(
129                        key_path,
130                        document_type,
131                        owner_id,
132                        platform_version,
133                    ),
134                    version => Err(ProtocolError::UnknownVersionMismatch {
135                        method: "DocumentMethodV0::get_raw_for_document_type".to_string(),
136                        known_versions: vec![0],
137                        received: version,
138                    }),
139                }
140            }
141        }
142    }
143
144    fn hash(
145        &self,
146        contract: &DataContract,
147        document_type: DocumentTypeRef,
148        platform_version: &PlatformVersion,
149    ) -> Result<Vec<u8>, ProtocolError> {
150        match self {
151            Document::V0(document_v0) => {
152                match platform_version
153                    .dpp
154                    .document_versions
155                    .document_method_versions
156                    .hash
157                {
158                    0 => document_v0.hash_v0(contract, document_type, platform_version),
159                    version => Err(ProtocolError::UnknownVersionMismatch {
160                        method: "DocumentMethodV0::hash".to_string(),
161                        known_versions: vec![0],
162                        received: version,
163                    }),
164                }
165            }
166        }
167    }
168
169    fn increment_revision(&mut self) -> Result<(), ProtocolError> {
170        let Some(revision) = self.revision() else {
171            return Err(ProtocolError::Document(Box::new(
172                DocumentError::DocumentNoRevisionError {
173                    document: Box::new(self.clone()),
174                },
175            )));
176        };
177
178        let new_revision = revision
179            .checked_add(1)
180            .ok_or(ProtocolError::Overflow("overflow when adding 1"))?;
181
182        self.set_revision(Some(new_revision));
183
184        Ok(())
185    }
186
187    fn is_equal_ignoring_time_based_fields(
188        &self,
189        rhs: &Self,
190        also_ignore_fields: Option<Vec<&str>>,
191        platform_version: &PlatformVersion,
192    ) -> Result<bool, ProtocolError> {
193        match (self, rhs) {
194            (Document::V0(document_v0), Document::V0(rhs_v0)) => {
195                match platform_version
196                    .dpp
197                    .document_versions
198                    .document_method_versions
199                    .is_equal_ignoring_timestamps
200                {
201                    0 => Ok(document_v0
202                        .is_equal_ignoring_time_based_fields_v0(rhs_v0, also_ignore_fields)),
203                    version => Err(ProtocolError::UnknownVersionMismatch {
204                        method: "DocumentMethodV0::is_equal_ignoring_time_based_fields".to_string(),
205                        known_versions: vec![0],
206                        received: version,
207                    }),
208                }
209            }
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::data_contract::accessors::v0::DataContractV0Getters;
218    use crate::data_contract::document_type::random_document::CreateRandomDocument;
219    use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
220    use crate::tests::json_document::json_document_to_contract;
221
222    use regex::Regex;
223
224    #[test]
225    fn test_document_display() {
226        let platform_version = PlatformVersion::first();
227        let contract = json_document_to_contract(
228            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
229            false,
230            platform_version,
231        )
232        .expect("expected to get contract");
233
234        let document_type = contract
235            .document_type_for_name("profile")
236            .expect("expected to get profile document type");
237        let document = document_type
238            .random_document(Some(3333), platform_version)
239            .expect("expected to get a random document");
240
241        let document_string = format!("{}", document);
242        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\)\] .*";
243        let re = Regex::new(pattern).unwrap();
244        assert!(
245            re.is_match(document_string.as_str()),
246            "pattern: {} does not match {}",
247            pattern,
248            document_string
249        );
250    }
251
252    #[test]
253    fn test_serialization_and_deserialization() {
254        let platform_version = PlatformVersion::latest();
255        let contract = json_document_to_contract(
256            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
257            false,
258            platform_version,
259        )
260        .expect("expected to get contract");
261
262        let document_type = contract
263            .document_type_for_name("domain")
264            .expect("expected to get document type");
265        for _ in 0..20 {
266            let document = document_type
267                .random_document(None, platform_version)
268                .expect("expected a document");
269            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
270                &document,
271                document_type,
272                &contract,
273                platform_version,
274            )
275            .expect("should serialize");
276            let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
277                .expect("expected to deserialize domain document");
278        }
279    }
280
281    #[test]
282    fn test_serialize_deserialize_over_different_versions_of_document_type() {
283        let platform_version = PlatformVersion::latest();
284        let contract = json_document_to_contract(
285            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract.json",
286            false,
287            platform_version,
288        )
289        .expect("expected to get contract");
290
291        let updated_contract = json_document_to_contract(
292            "../rs-drive/tests/supporting_files/contract/dpns/dpns-contract-update-v2-test.json",
293            false,
294            platform_version,
295        )
296        .expect("expected to get contract");
297
298        let document_type = contract
299            .document_type_for_name("domain")
300            .expect("expected to get document type");
301
302        let updated_document_type = updated_contract
303            .document_type_for_name("domain")
304            .expect("expected to get document type");
305
306        // let's test from a document created in the old version, and we try to deserialize it in the new version
307        for _ in 0..20 {
308            let document = document_type
309                .random_document(None, platform_version)
310                .expect("expected a document");
311            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
312                &document,
313                document_type,
314                &contract,
315                platform_version,
316            )
317            .expect("should serialize");
318            let _deserialized =
319                Document::from_bytes(&serialized, updated_document_type, platform_version)
320                    .expect("expected to deserialize domain document");
321        }
322
323        // let's test from a document created in the new version, and we try to deserialize it with the old version
324        for _ in 0..20 {
325            let document = updated_document_type
326                .random_document(None, platform_version)
327                .expect("expected a document");
328            let serialized = <Document as DocumentPlatformConversionMethodsV0>::serialize(
329                &document,
330                document_type,
331                &contract,
332                platform_version,
333            )
334            .expect("should serialize");
335            let _deserialized = Document::from_bytes(&serialized, document_type, platform_version)
336                .expect("expected to deserialize domain document");
337        }
338    }
339
340    // ================================================================
341    //  Display impl tests for Document
342    // ================================================================
343
344    #[test]
345    fn display_document_with_no_properties() {
346        let doc = Document::V0(DocumentV0 {
347            contract_version: None,
348            id: platform_value::Identifier::new([0xAA; 32]),
349            owner_id: platform_value::Identifier::new([0xBB; 32]),
350            properties: Default::default(),
351            revision: None,
352            created_at: None,
353            updated_at: None,
354            transferred_at: None,
355            created_at_block_height: None,
356            updated_at_block_height: None,
357            transferred_at_block_height: None,
358            created_at_core_block_height: None,
359            updated_at_core_block_height: None,
360            transferred_at_core_block_height: None,
361            creator_id: None,
362        });
363
364        let s = format!("{}", doc);
365        assert!(
366            s.contains("no properties"),
367            "should say 'no properties' when the BTreeMap is empty, got: {}",
368            s
369        );
370    }
371
372    #[test]
373    fn display_document_shows_transferred_at_fields() {
374        let doc = Document::V0(DocumentV0 {
375            contract_version: None,
376            id: platform_value::Identifier::new([1u8; 32]),
377            owner_id: platform_value::Identifier::new([2u8; 32]),
378            properties: Default::default(),
379            revision: None,
380            created_at: None,
381            updated_at: None,
382            transferred_at: Some(1_700_000_000_000),
383            created_at_block_height: None,
384            updated_at_block_height: None,
385            transferred_at_block_height: Some(500),
386            created_at_core_block_height: None,
387            updated_at_core_block_height: None,
388            transferred_at_core_block_height: Some(42),
389            creator_id: None,
390        });
391
392        let s = format!("{}", doc);
393        assert!(
394            s.contains("transferred_at:"),
395            "should contain transferred_at, got: {}",
396            s
397        );
398        assert!(
399            s.contains("transferred_at_block_height:500"),
400            "should contain transferred_at_block_height:500, got: {}",
401            s
402        );
403        assert!(
404            s.contains("transferred_at_core_block_height:42"),
405            "should contain transferred_at_core_block_height:42, got: {}",
406            s
407        );
408    }
409
410    #[test]
411    fn display_document_shows_creator_id() {
412        let creator = platform_value::Identifier::new([0xCC; 32]);
413        let doc = Document::V0(DocumentV0 {
414            contract_version: None,
415            id: platform_value::Identifier::new([1u8; 32]),
416            owner_id: platform_value::Identifier::new([2u8; 32]),
417            properties: Default::default(),
418            revision: None,
419            created_at: None,
420            updated_at: None,
421            transferred_at: None,
422            created_at_block_height: None,
423            updated_at_block_height: None,
424            transferred_at_block_height: None,
425            created_at_core_block_height: None,
426            updated_at_core_block_height: None,
427            transferred_at_core_block_height: None,
428            creator_id: Some(creator),
429        });
430
431        let s = format!("{}", doc);
432        assert!(
433            s.contains("creator_id:"),
434            "should contain creator_id, got: {}",
435            s
436        );
437    }
438
439    #[test]
440    fn display_document_shows_block_height_fields() {
441        let doc = Document::V0(DocumentV0 {
442            contract_version: None,
443            id: platform_value::Identifier::new([1u8; 32]),
444            owner_id: platform_value::Identifier::new([2u8; 32]),
445            properties: Default::default(),
446            revision: None,
447            created_at: None,
448            updated_at: None,
449            transferred_at: None,
450            created_at_block_height: Some(100),
451            updated_at_block_height: Some(200),
452            transferred_at_block_height: None,
453            created_at_core_block_height: Some(50),
454            updated_at_core_block_height: Some(60),
455            transferred_at_core_block_height: None,
456            creator_id: None,
457        });
458
459        let s = format!("{}", doc);
460        assert!(s.contains("created_at_block_height:100"), "got: {}", s);
461        assert!(s.contains("updated_at_block_height:200"), "got: {}", s);
462        assert!(s.contains("created_at_core_block_height:50"), "got: {}", s);
463        assert!(s.contains("updated_at_core_block_height:60"), "got: {}", s);
464    }
465
466    // ================================================================
467    //  Version dispatch: increment_revision
468    // ================================================================
469
470    #[test]
471    fn increment_revision_works_on_mutable_document() {
472        let mut doc = Document::V0(DocumentV0 {
473            contract_version: None,
474            id: platform_value::Identifier::new([1u8; 32]),
475            owner_id: platform_value::Identifier::new([2u8; 32]),
476            properties: Default::default(),
477            revision: Some(1),
478            created_at: None,
479            updated_at: None,
480            transferred_at: None,
481            created_at_block_height: None,
482            updated_at_block_height: None,
483            transferred_at_block_height: None,
484            created_at_core_block_height: None,
485            updated_at_core_block_height: None,
486            transferred_at_core_block_height: None,
487            creator_id: None,
488        });
489
490        doc.increment_revision()
491            .expect("increment_revision should succeed");
492        assert_eq!(doc.revision(), Some(2));
493    }
494
495    #[test]
496    fn increment_revision_fails_when_no_revision() {
497        let mut doc = Document::V0(DocumentV0 {
498            contract_version: None,
499            id: platform_value::Identifier::new([1u8; 32]),
500            owner_id: platform_value::Identifier::new([2u8; 32]),
501            properties: Default::default(),
502            revision: None,
503            created_at: None,
504            updated_at: None,
505            transferred_at: None,
506            created_at_block_height: None,
507            updated_at_block_height: None,
508            transferred_at_block_height: None,
509            created_at_core_block_height: None,
510            updated_at_core_block_height: None,
511            transferred_at_core_block_height: None,
512            creator_id: None,
513        });
514
515        let result = doc.increment_revision();
516        assert!(
517            result.is_err(),
518            "increment_revision should fail when revision is None"
519        );
520    }
521
522    // ================================================================
523    //  Version dispatch: is_equal_ignoring_time_based_fields
524    // ================================================================
525
526    #[test]
527    fn is_equal_ignoring_time_based_fields_dispatches_correctly() {
528        let platform_version = PlatformVersion::latest();
529        let contract = json_document_to_contract(
530            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
531            false,
532            platform_version,
533        )
534        .expect("expected to get contract");
535
536        let document_type = contract
537            .document_type_for_name("profile")
538            .expect("expected to get profile document type");
539
540        let doc1 = document_type
541            .random_document(Some(42), platform_version)
542            .expect("expected random document");
543
544        let mut doc2 = doc1.clone();
545        // Change timestamps
546        doc2.set_created_at(Some(9_999_999));
547        doc2.set_updated_at(Some(8_888_888));
548
549        let result = doc1
550            .is_equal_ignoring_time_based_fields(&doc2, None, platform_version)
551            .expect("should succeed");
552        assert!(
553            result,
554            "same document with different timestamps should be equal ignoring time fields"
555        );
556    }
557
558    // ================================================================
559    //  Version dispatch: get_raw_for_contract
560    // ================================================================
561
562    #[test]
563    fn get_raw_for_contract_dispatches_to_v0() {
564        let platform_version = PlatformVersion::latest();
565        let contract = json_document_to_contract(
566            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
567            false,
568            platform_version,
569        )
570        .expect("expected to get contract");
571
572        let document_type = contract
573            .document_type_for_name("profile")
574            .expect("expected to get profile document type");
575
576        let document = document_type
577            .random_document(Some(7), platform_version)
578            .expect("expected random document");
579
580        let raw_id = document
581            .get_raw_for_contract("$id", "profile", &contract, None, platform_version)
582            .expect("should succeed");
583        assert_eq!(raw_id, Some(document.id().to_vec()));
584    }
585
586    // ================================================================
587    //  Version dispatch: hash
588    // ================================================================
589
590    #[test]
591    fn document_hash_is_deterministic() {
592        let platform_version = PlatformVersion::latest();
593        let contract = json_document_to_contract(
594            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
595            false,
596            platform_version,
597        )
598        .expect("expected to get contract");
599
600        let document_type = contract
601            .document_type_for_name("profile")
602            .expect("expected to get profile document type");
603
604        let document = document_type
605            .random_document(Some(42), platform_version)
606            .expect("expected random document");
607
608        let hash1 = document
609            .hash(&contract, document_type, platform_version)
610            .expect("hash should succeed");
611        let hash2 = document
612            .hash(&contract, document_type, platform_version)
613            .expect("hash should succeed");
614        assert_eq!(hash1, hash2, "hash should be deterministic");
615        assert!(!hash1.is_empty(), "hash should not be empty");
616    }
617
618    // ================================================================
619    //  increment_revision: overflow from Revision::MAX surfaces an
620    //  Overflow ProtocolError (not a silent saturate — this is the
621    //  Document-enum path which uses checked_add).
622    // ================================================================
623
624    #[test]
625    fn increment_revision_errors_on_overflow() {
626        let mut doc = Document::V0(DocumentV0 {
627            contract_version: None,
628            id: platform_value::Identifier::new([1u8; 32]),
629            owner_id: platform_value::Identifier::new([2u8; 32]),
630            properties: Default::default(),
631            revision: Some(crate::prelude::Revision::MAX),
632            created_at: None,
633            updated_at: None,
634            transferred_at: None,
635            created_at_block_height: None,
636            updated_at_block_height: None,
637            transferred_at_block_height: None,
638            created_at_core_block_height: None,
639            updated_at_core_block_height: None,
640            transferred_at_core_block_height: None,
641            creator_id: None,
642        });
643        let err = doc.increment_revision().expect_err("MAX + 1 must overflow");
644        match err {
645            ProtocolError::Overflow(_) => {}
646            other => panic!("expected ProtocolError::Overflow, got {:?}", other),
647        }
648    }
649
650    // ================================================================
651    //  From<DocumentV0> for Document produces a V0 variant.
652    // ================================================================
653
654    #[test]
655    fn from_document_v0_produces_v0_variant() {
656        let v0 = DocumentV0 {
657            contract_version: None,
658            id: platform_value::Identifier::new([1u8; 32]),
659            owner_id: platform_value::Identifier::new([2u8; 32]),
660            properties: Default::default(),
661            revision: Some(7),
662            created_at: None,
663            updated_at: None,
664            transferred_at: None,
665            created_at_block_height: None,
666            updated_at_block_height: None,
667            transferred_at_block_height: None,
668            created_at_core_block_height: None,
669            updated_at_core_block_height: None,
670            transferred_at_core_block_height: None,
671            creator_id: None,
672        };
673        let document: Document = v0.clone().into();
674        match document {
675            Document::V0(inner) => assert_eq!(inner, v0),
676        }
677    }
678
679    // ================================================================
680    //  Document Display forwards to DocumentV0 Display with a version
681    //  prefix.
682    // ================================================================
683
684    #[test]
685    fn document_display_has_version_prefix() {
686        let doc = Document::V0(DocumentV0 {
687            contract_version: None,
688            id: platform_value::Identifier::new([1u8; 32]),
689            owner_id: platform_value::Identifier::new([2u8; 32]),
690            properties: Default::default(),
691            revision: None,
692            created_at: None,
693            updated_at: None,
694            transferred_at: None,
695            created_at_block_height: None,
696            updated_at_block_height: None,
697            transferred_at_block_height: None,
698            created_at_core_block_height: None,
699            updated_at_core_block_height: None,
700            transferred_at_core_block_height: None,
701            creator_id: None,
702        });
703        let s = format!("{}", doc);
704        assert!(
705            s.starts_with("v0 : "),
706            "Display should prefix with version, got: {s}"
707        );
708    }
709
710    // ================================================================
711    //  get_raw_for_document_type dispatches via platform version 0
712    //  to the V0 implementation.
713    // ================================================================
714
715    #[test]
716    fn get_raw_for_document_type_dispatch_path_returns_id() {
717        let platform_version = PlatformVersion::latest();
718        let contract = json_document_to_contract(
719            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
720            false,
721            platform_version,
722        )
723        .expect("expected contract");
724        let document_type = contract
725            .document_type_for_name("profile")
726            .expect("expected document type");
727
728        let document = document_type
729            .random_document(Some(11), platform_version)
730            .expect("expected random document");
731
732        let raw = document
733            .get_raw_for_document_type("$id", document_type, None, platform_version)
734            .expect("should succeed");
735        assert_eq!(raw, Some(document.id().to_vec()));
736    }
737}
738
739#[cfg(all(
740    test,
741    feature = "json-conversion",
742    feature = "value-conversion",
743    feature = "serde-conversion"
744))]
745mod json_convertible_tests {
746    use super::*;
747
748    use platform_value::{platform_value, Identifier};
749    use serde_json::json;
750    use std::collections::BTreeMap;
751
752    fn fixture() -> Document {
753        Document::V0(DocumentV0 {
754            contract_version: None,
755            id: Identifier::new([0xa1; 32]),
756            owner_id: Identifier::new([0xb2; 32]),
757            properties: BTreeMap::new(),
758            revision: Some(2),
759            created_at: Some(1_700_000_000_000),
760            updated_at: Some(1_700_000_001_000),
761            transferred_at: None,
762            created_at_block_height: Some(100),
763            updated_at_block_height: Some(101),
764            transferred_at_block_height: None,
765            created_at_core_block_height: Some(50),
766            updated_at_core_block_height: Some(51),
767            transferred_at_core_block_height: None,
768            creator_id: Some(Identifier::new([0xc3; 32])),
769        })
770    }
771
772    #[test]
773    fn json_round_trip_with_full_wire_shape() {
774        use crate::serialization::JsonConvertible;
775        let original = fixture();
776        let json = original.to_json().expect("to_json");
777        // Sized-int fields whose JSON wire encoding loses size info:
778        // `$revision`/`$createdAt`/`$updatedAt`/`$createdAtBlockHeight`/
779        // `$updatedAtBlockHeight` (u64), `$createdAtCoreBlockHeight`/
780        // `$updatedAtCoreBlockHeight` (u32). The value-path locks variants
781        // via explicit suffixes. `properties` is flattened into the document
782        // root; for an empty `BTreeMap`, no extra keys appear.
783        assert_eq!(
784            json,
785            json!({
786                "$formatVersion": "0",
787                "$id": Identifier::new([0xa1; 32]),
788                "$ownerId": Identifier::new([0xb2; 32]),
789                "$revision": 2,
790                "$createdAt": 1_700_000_000_000u64,
791                "$updatedAt": 1_700_000_001_000u64,
792                "$transferredAt": serde_json::Value::Null,
793                "$createdAtBlockHeight": 100,
794                "$updatedAtBlockHeight": 101,
795                "$transferredAtBlockHeight": serde_json::Value::Null,
796                "$createdAtCoreBlockHeight": 50,
797                "$updatedAtCoreBlockHeight": 51,
798                "$transferredAtCoreBlockHeight": serde_json::Value::Null,
799                "$creatorId": Identifier::new([0xc3; 32]),
800            })
801        );
802        let recovered = Document::from_json(json).expect("from_json");
803        assert_eq!(original, recovered);
804    }
805
806    #[test]
807    fn value_round_trip_with_full_wire_shape() {
808        use crate::serialization::ValueConvertible;
809        let original = fixture();
810        let value = original.to_object().expect("to_object");
811        // Explicit suffixes lock in sized variants: revision / *At /
812        // *AtBlockHeight are u64; *AtCoreBlockHeight are u32.
813        assert_eq!(
814            value,
815            platform_value!({
816                "$formatVersion": "0",
817                "$id": Identifier::new([0xa1; 32]),
818                "$ownerId": Identifier::new([0xb2; 32]),
819                "$revision": 2u64,
820                "$createdAt": 1_700_000_000_000u64,
821                "$updatedAt": 1_700_000_001_000u64,
822                "$transferredAt": platform_value::Value::Null,
823                "$createdAtBlockHeight": 100u64,
824                "$updatedAtBlockHeight": 101u64,
825                "$transferredAtBlockHeight": platform_value::Value::Null,
826                "$createdAtCoreBlockHeight": 50u32,
827                "$updatedAtCoreBlockHeight": 51u32,
828                "$transferredAtCoreBlockHeight": platform_value::Value::Null,
829                "$creatorId": Identifier::new([0xc3; 32]),
830            })
831        );
832        let recovered = Document::from_object(value).expect("from_object");
833        assert_eq!(original, recovered);
834    }
835}