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