1use crate::document::property_names;
2
3use crate::identity::TimestampMillis;
4use crate::prelude::{BlockHeight, CoreBlockHeight, Revision};
5
6use crate::ProtocolError;
7
8use crate::document::serialization_traits::DocumentCborMethodsV0;
9use crate::document::v0::DocumentV0;
10use crate::version::PlatformVersion;
11use ciborium::Value as CborValue;
12use integer_encoding::VarIntWriter;
13use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper;
14use platform_value::{Identifier, Value};
15use serde::{Deserialize, Serialize};
16use std::collections::BTreeMap;
17use std::convert::{TryFrom, TryInto};
18
19#[cfg(feature = "cbor")]
20#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
21pub struct DocumentForCbor {
22 #[serde(rename = "$id")]
24 pub id: [u8; 32],
25
26 #[serde(flatten)]
28 pub properties: BTreeMap<String, CborValue>,
29
30 #[serde(rename = "$ownerId")]
32 pub owner_id: [u8; 32],
33
34 #[serde(rename = "$revision")]
36 pub revision: Option<Revision>,
37
38 #[serde(rename = "$createdAt")]
39 pub created_at: Option<TimestampMillis>,
40 #[serde(rename = "$updatedAt")]
41 pub updated_at: Option<TimestampMillis>,
42 #[serde(rename = "$transferredAt")]
43 pub transferred_at: Option<TimestampMillis>,
44
45 #[serde(rename = "$createdAtBlockHeight")]
46 pub created_at_block_height: Option<BlockHeight>,
47 #[serde(rename = "$updatedAtBlockHeight")]
48 pub updated_at_block_height: Option<BlockHeight>,
49 #[serde(rename = "$transferredAtBlockHeight")]
50 pub transferred_at_block_height: Option<BlockHeight>,
51
52 #[serde(rename = "$createdAtCoreBlockHeight")]
53 pub created_at_core_block_height: Option<CoreBlockHeight>,
54 #[serde(rename = "$updatedAtCoreBlockHeight")]
55 pub updated_at_core_block_height: Option<CoreBlockHeight>,
56 #[serde(rename = "$transferredAtCoreBlockHeight")]
57 pub transferred_at_core_block_height: Option<CoreBlockHeight>,
58
59 #[serde(rename = "$creatorId")]
60 pub creator_id: Option<Identifier>,
61
62 #[serde(
65 rename = "$contractVersion",
66 default,
67 skip_serializing_if = "Option::is_none"
68 )]
69 pub contract_version: Option<u32>,
70}
71
72#[cfg(feature = "cbor")]
73impl TryFrom<DocumentV0> for DocumentForCbor {
74 type Error = ProtocolError;
75
76 fn try_from(value: DocumentV0) -> Result<Self, Self::Error> {
77 let DocumentV0 {
78 id,
79 properties,
80 owner_id,
81 revision,
82 created_at,
83 updated_at,
84 transferred_at,
85 created_at_block_height,
86 updated_at_block_height,
87 transferred_at_block_height,
88 created_at_core_block_height,
89 updated_at_core_block_height,
90 transferred_at_core_block_height,
91 creator_id,
92 contract_version,
93 } = value;
94 Ok(DocumentForCbor {
95 contract_version,
96 id: id.to_buffer(),
97 properties: Value::convert_to_cbor_map(properties)
98 .map_err(ProtocolError::ValueError)?,
99 owner_id: owner_id.to_buffer(),
100 revision,
101 created_at,
102 updated_at,
103 transferred_at,
104 created_at_block_height,
105 updated_at_block_height,
106 transferred_at_block_height,
107 created_at_core_block_height,
108 updated_at_core_block_height,
109 transferred_at_core_block_height,
110 creator_id,
111 })
112 }
113}
114
115impl DocumentV0 {
116 fn from_map(
119 mut document_map: BTreeMap<String, Value>,
120 document_id: Option<[u8; 32]>,
121 owner_id: Option<[u8; 32]>,
122 ) -> Result<Self, ProtocolError> {
123 let owner_id = match owner_id {
124 None => document_map
125 .remove_hash256_bytes(property_names::OWNER_ID)
126 .map_err(ProtocolError::ValueError)?,
127 Some(owner_id) => owner_id,
128 };
129
130 let id = match document_id {
131 None => document_map
132 .remove_hash256_bytes(property_names::ID)
133 .map_err(ProtocolError::ValueError)?,
134 Some(document_id) => document_id,
135 };
136
137 let revision = document_map.remove_optional_integer(property_names::REVISION)?;
138
139 let created_at = document_map.remove_optional_integer(property_names::CREATED_AT)?;
140 let updated_at = document_map.remove_optional_integer(property_names::UPDATED_AT)?;
141 let transferred_at =
142 document_map.remove_optional_integer(property_names::TRANSFERRED_AT)?;
143 let created_at_block_height =
144 document_map.remove_optional_integer(property_names::CREATED_AT_BLOCK_HEIGHT)?;
145 let updated_at_block_height =
146 document_map.remove_optional_integer(property_names::UPDATED_AT_BLOCK_HEIGHT)?;
147 let transferred_at_block_height =
148 document_map.remove_optional_integer(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)?;
149 let created_at_core_block_height =
150 document_map.remove_optional_integer(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)?;
151 let updated_at_core_block_height =
152 document_map.remove_optional_integer(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)?;
153 let transferred_at_core_block_height = document_map
154 .remove_optional_integer(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)?;
155
156 let creator_id = document_map
157 .remove_optional_identifier(property_names::CREATOR_ID)
158 .map_err(ProtocolError::ValueError)?;
159
160 let contract_version =
161 document_map.remove_optional_integer(property_names::CONTRACT_VERSION)?;
162
163 Ok(DocumentV0 {
165 contract_version,
166 properties: document_map,
167 owner_id: Identifier::new(owner_id),
168 id: Identifier::new(id),
169 revision,
170 created_at,
171 updated_at,
172 transferred_at,
173 created_at_block_height,
174 updated_at_block_height,
175 transferred_at_block_height,
176 created_at_core_block_height,
177 updated_at_core_block_height,
178 transferred_at_core_block_height,
179 creator_id,
180 })
181 }
182}
183
184impl DocumentCborMethodsV0 for DocumentV0 {
185 fn from_cbor(
188 document_cbor: &[u8],
189 document_id: Option<[u8; 32]>,
190 owner_id: Option<[u8; 32]>,
191 _platform_version: &PlatformVersion,
192 ) -> Result<Self, ProtocolError> {
193 let document_cbor_map: BTreeMap<String, CborValue> =
196 ciborium::de::from_reader(document_cbor).map_err(|_| {
197 ProtocolError::InvalidCBOR(
198 "unable to decode document for document call".to_string(),
199 )
200 })?;
201 let document_map: BTreeMap<String, Value> =
202 Value::convert_from_cbor_map(document_cbor_map).map_err(ProtocolError::ValueError)?;
203 Self::from_map(document_map, document_id, owner_id)
204 }
205
206 fn to_cbor_value(&self) -> Result<CborValue, ProtocolError> {
207 let value = platform_value::to_value(self).map_err(ProtocolError::ValueError)?;
213 value.try_into().map_err(ProtocolError::ValueError)
214 }
215
216 fn to_cbor(&self) -> Result<Vec<u8>, ProtocolError> {
218 let mut buffer: Vec<u8> = Vec::new();
219 buffer.write_varint(0).map_err(|_| {
220 ProtocolError::EncodingError("error writing protocol version".to_string())
221 })?;
222 let cbor_document = DocumentForCbor::try_from(self.clone())?;
223 ciborium::ser::into_writer(&cbor_document, &mut buffer).map_err(|_| {
224 ProtocolError::EncodingError("unable to serialize into cbor".to_string())
225 })?;
226 Ok(buffer)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use crate::data_contract::accessors::v0::DataContractV0Getters;
234 use crate::data_contract::document_type::random_document::CreateRandomDocument;
235 use crate::document::serialization_traits::DocumentCborMethodsV0;
236 use crate::document::DocumentV0Getters;
237 use crate::tests::json_document::json_document_to_contract;
238 use platform_version::version::PlatformVersion;
239
240 fn make_document_v0_with_timestamps() -> DocumentV0 {
241 let id = Identifier::new([1u8; 32]);
242 let owner_id = Identifier::new([2u8; 32]);
243 let mut properties = BTreeMap::new();
244 properties.insert("name".to_string(), Value::Text("Alice".to_string()));
245 properties.insert("age".to_string(), Value::U64(30));
246 DocumentV0 {
247 contract_version: None,
248 id,
249 owner_id,
250 properties,
251 revision: Some(1),
252 created_at: Some(1_700_000_000_000),
253 updated_at: Some(1_700_000_100_000),
254 transferred_at: None,
255 created_at_block_height: Some(100),
256 updated_at_block_height: Some(200),
257 transferred_at_block_height: None,
258 created_at_core_block_height: Some(50),
259 updated_at_core_block_height: Some(60),
260 transferred_at_core_block_height: None,
261 creator_id: None,
262 }
263 }
264
265 #[test]
270 fn cbor_round_trip_preserves_contract_version_stamp() {
271 use crate::document::Document;
272
273 let platform_version = PlatformVersion::latest();
274 let mut document = make_document_v0_with_timestamps();
275 document.contract_version = Some(7);
276
277 let cbor = document.to_cbor().expect("expected to serialize to cbor");
278 let restored = Document::from_cbor(&cbor, None, None, platform_version)
279 .expect("expected to deserialize from cbor");
280
281 let Document::V0(restored) = restored;
282 assert_eq!(restored.contract_version, Some(7));
283 assert_eq!(restored.id, document.id);
284 assert_eq!(restored.revision, document.revision);
285 assert_eq!(
288 restored.properties.get("name"),
289 document.properties.get("name")
290 );
291
292 let unstamped = make_document_v0_with_timestamps();
295 let unstamped_cbor = unstamped.to_cbor().expect("expected to serialize to cbor");
296 let restored_unstamped = Document::from_cbor(&unstamped_cbor, None, None, platform_version)
297 .expect("expected to deserialize from cbor");
298 let Document::V0(restored_unstamped) = restored_unstamped;
299 assert_eq!(restored_unstamped.contract_version, None);
300 }
301
302 #[test]
303 fn cbor_round_trip_with_random_dashpay_profile() {
304 let platform_version = PlatformVersion::latest();
305 let contract = json_document_to_contract(
306 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
307 false,
308 platform_version,
309 )
310 .expect("expected to load dashpay contract");
311
312 let document_type = contract
313 .document_type_for_name("profile")
314 .expect("expected profile document type");
315
316 for seed in 0..10u64 {
317 let document = document_type
318 .random_document(Some(seed), platform_version)
319 .expect("expected random document");
320
321 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
323 let recovered =
324 crate::document::Document::from_cbor(&cbor_bytes, None, None, platform_version)
325 .expect("from_cbor should succeed");
326
327 assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}");
328 assert_eq!(
329 document.owner_id(),
330 recovered.owner_id(),
331 "owner_id mismatch for seed {seed}"
332 );
333 assert_eq!(
334 document.revision(),
335 recovered.revision(),
336 "revision mismatch for seed {seed}"
337 );
338 assert_eq!(
339 document.properties(),
340 recovered.properties(),
341 "properties mismatch for seed {seed}"
342 );
343 }
344 }
345
346 #[test]
347 fn cbor_round_trip_with_explicit_ids_overrides_embedded_ids() {
348 let platform_version = PlatformVersion::latest();
349 let contract = json_document_to_contract(
350 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
351 false,
352 platform_version,
353 )
354 .expect("expected to load dashpay contract");
355
356 let document_type = contract
357 .document_type_for_name("profile")
358 .expect("expected profile document type");
359
360 let document = document_type
361 .random_document(Some(42), platform_version)
362 .expect("expected random document");
363
364 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
365
366 let override_id = [0xAA; 32];
367 let override_owner = [0xBB; 32];
368
369 let recovered = crate::document::Document::from_cbor(
370 &cbor_bytes,
371 Some(override_id),
372 Some(override_owner),
373 platform_version,
374 )
375 .expect("from_cbor with explicit ids should succeed");
376
377 assert_eq!(
378 recovered.id(),
379 Identifier::new(override_id),
380 "explicit document_id should override the one in CBOR"
381 );
382 assert_eq!(
383 recovered.owner_id(),
384 Identifier::new(override_owner),
385 "explicit owner_id should override the one in CBOR"
386 );
387 }
388
389 #[test]
394 fn to_cbor_value_returns_map_for_document_with_properties() {
395 let doc = make_document_v0_with_timestamps();
396 let cbor_val = doc.to_cbor_value().expect("to_cbor_value should succeed");
397 assert!(
399 cbor_val.is_map(),
400 "CBOR value of a document should be a Map, got {:?}",
401 cbor_val
402 );
403 }
404
405 #[test]
410 fn to_cbor_starts_with_version_zero_varint() {
411 let doc = make_document_v0_with_timestamps();
412 let cbor_bytes = doc.to_cbor().expect("to_cbor should succeed");
413 assert!(!cbor_bytes.is_empty(), "CBOR output should not be empty");
415 assert_eq!(
416 cbor_bytes[0], 0,
417 "first byte should be varint(0) for version"
418 );
419 }
420
421 #[test]
426 fn from_cbor_rejects_invalid_cbor_bytes() {
427 let platform_version = PlatformVersion::latest();
428 let garbage = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01];
429 let result = DocumentV0::from_cbor(&garbage, None, None, platform_version);
430 assert!(
431 result.is_err(),
432 "from_cbor should fail on invalid CBOR bytes"
433 );
434 }
435
436 #[test]
441 fn document_for_cbor_preserves_all_fields() {
442 let doc = make_document_v0_with_timestamps();
443 let cbor_doc = DocumentForCbor::try_from(doc.clone()).expect("TryFrom should succeed");
444 assert_eq!(cbor_doc.id, doc.id.to_buffer());
445 assert_eq!(cbor_doc.owner_id, doc.owner_id.to_buffer());
446 assert_eq!(cbor_doc.revision, doc.revision);
447 assert_eq!(cbor_doc.created_at, doc.created_at);
448 assert_eq!(cbor_doc.updated_at, doc.updated_at);
449 assert_eq!(cbor_doc.transferred_at, doc.transferred_at);
450 assert_eq!(
451 cbor_doc.created_at_block_height,
452 doc.created_at_block_height
453 );
454 assert_eq!(
455 cbor_doc.updated_at_block_height,
456 doc.updated_at_block_height
457 );
458 assert_eq!(
459 cbor_doc.transferred_at_block_height,
460 doc.transferred_at_block_height
461 );
462 assert_eq!(
463 cbor_doc.created_at_core_block_height,
464 doc.created_at_core_block_height
465 );
466 assert_eq!(
467 cbor_doc.updated_at_core_block_height,
468 doc.updated_at_core_block_height
469 );
470 assert_eq!(
471 cbor_doc.transferred_at_core_block_height,
472 doc.transferred_at_core_block_height
473 );
474 }
475
476 #[test]
481 fn from_map_extracts_system_fields_and_leaves_properties() {
482 let id_bytes = [3u8; 32];
483 let owner_bytes = [4u8; 32];
484
485 let mut map = BTreeMap::new();
486 map.insert(property_names::ID.to_string(), Value::Bytes32(id_bytes));
487 map.insert(
488 property_names::OWNER_ID.to_string(),
489 Value::Bytes32(owner_bytes),
490 );
491 map.insert(property_names::REVISION.to_string(), Value::U64(5));
492 map.insert(
493 property_names::CREATED_AT.to_string(),
494 Value::U64(1_000_000),
495 );
496 map.insert(
497 property_names::UPDATED_AT.to_string(),
498 Value::U64(2_000_000),
499 );
500 map.insert("customField".to_string(), Value::Text("hello".to_string()));
501
502 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
503
504 assert_eq!(doc.id, Identifier::new(id_bytes));
505 assert_eq!(doc.owner_id, Identifier::new(owner_bytes));
506 assert_eq!(doc.revision, Some(5));
507 assert_eq!(doc.created_at, Some(1_000_000));
508 assert_eq!(doc.updated_at, Some(2_000_000));
509 assert_eq!(
511 doc.properties.get("customField"),
512 Some(&Value::Text("hello".to_string()))
513 );
514 assert!(!doc.properties.contains_key(property_names::ID));
516 assert!(!doc.properties.contains_key(property_names::OWNER_ID));
517 assert!(!doc.properties.contains_key(property_names::REVISION));
518 }
519
520 #[test]
521 fn from_map_with_explicit_ids_overrides_map_ids() {
522 let map_id = [10u8; 32];
523 let map_owner = [11u8; 32];
524 let override_id = [20u8; 32];
525 let override_owner = [21u8; 32];
526
527 let mut map = BTreeMap::new();
528 map.insert(property_names::ID.to_string(), Value::Bytes32(map_id));
529 map.insert(
530 property_names::OWNER_ID.to_string(),
531 Value::Bytes32(map_owner),
532 );
533
534 let doc = DocumentV0::from_map(map, Some(override_id), Some(override_owner))
535 .expect("from_map should succeed");
536
537 assert_eq!(
538 doc.id,
539 Identifier::new(override_id),
540 "explicit document_id should take precedence"
541 );
542 assert_eq!(
543 doc.owner_id,
544 Identifier::new(override_owner),
545 "explicit owner_id should take precedence"
546 );
547 }
548
549 #[test]
558 fn from_map_missing_id_fails_when_not_provided() {
559 let mut map = BTreeMap::new();
562 map.insert(
563 property_names::OWNER_ID.to_string(),
564 Value::Bytes32([4u8; 32]),
565 );
566
567 let result = DocumentV0::from_map(map, None, None);
568 assert!(
569 result.is_err(),
570 "from_map without $id or explicit document_id should fail"
571 );
572 }
573
574 #[test]
575 fn from_map_missing_owner_id_fails_when_not_provided() {
576 let mut map = BTreeMap::new();
577 map.insert(property_names::ID.to_string(), Value::Bytes32([3u8; 32]));
578
579 let result = DocumentV0::from_map(map, None, None);
580 assert!(
581 result.is_err(),
582 "from_map without $ownerId or explicit owner_id should fail"
583 );
584 }
585
586 #[test]
591 fn from_map_extracts_creator_id_when_present_as_identifier() {
592 let creator = Identifier::new([0xCD; 32]);
593 let mut map = BTreeMap::new();
594 map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32]));
595 map.insert(
596 property_names::OWNER_ID.to_string(),
597 Value::Bytes32([2u8; 32]),
598 );
599 map.insert(
600 property_names::CREATOR_ID.to_string(),
601 Value::Identifier(creator.to_buffer()),
602 );
603
604 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
605 assert_eq!(doc.creator_id, Some(creator));
606 }
607
608 #[test]
609 fn from_map_creator_id_missing_stays_none() {
610 let mut map = BTreeMap::new();
611 map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32]));
612 map.insert(
613 property_names::OWNER_ID.to_string(),
614 Value::Bytes32([2u8; 32]),
615 );
616
617 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
618 assert_eq!(doc.creator_id, None);
619 }
620
621 #[test]
626 fn from_cbor_rejects_empty_buffer() {
627 let platform_version = PlatformVersion::latest();
628 let result = DocumentV0::from_cbor(&[], None, None, platform_version);
629 assert!(
630 result.is_err(),
631 "from_cbor should fail on an empty input buffer"
632 );
633 }
634
635 #[test]
636 fn from_cbor_rejects_truncated_map_bytes() {
637 let platform_version = PlatformVersion::latest();
639 let result = DocumentV0::from_cbor(&[0xA1], None, None, platform_version);
640 assert!(
641 result.is_err(),
642 "from_cbor should fail on a truncated map prefix"
643 );
644 }
645
646 #[test]
651 fn cbor_round_trip_via_to_cbor_and_from_cbor_preserves_fields() {
652 let platform_version = PlatformVersion::latest();
653 let doc = make_document_v0_with_timestamps();
654
655 let bytes = doc.to_cbor().expect("to_cbor succeeds");
656 let recovered = crate::document::Document::from_cbor(&bytes, None, None, platform_version)
657 .expect("from_cbor succeeds");
658 assert_eq!(doc.id, recovered.id());
659 assert_eq!(doc.owner_id, recovered.owner_id());
660 assert_eq!(doc.revision, recovered.revision());
661 }
662
663 #[test]
669 fn document_for_cbor_preserves_user_properties() {
670 let doc = make_document_v0_with_timestamps();
671 let cbor = DocumentForCbor::try_from(doc.clone()).expect("try_from succeeds");
672 assert!(cbor.properties.contains_key("name"));
674 assert!(cbor.properties.contains_key("age"));
675 }
676
677 #[test]
678 fn from_map_with_all_timestamp_variants() {
679 let mut map = BTreeMap::new();
680 map.insert(property_names::ID.to_string(), Value::Bytes32([5u8; 32]));
681 map.insert(
682 property_names::OWNER_ID.to_string(),
683 Value::Bytes32([6u8; 32]),
684 );
685 map.insert(
686 property_names::CREATED_AT_BLOCK_HEIGHT.to_string(),
687 Value::U64(100),
688 );
689 map.insert(
690 property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(),
691 Value::U64(200),
692 );
693 map.insert(
694 property_names::TRANSFERRED_AT.to_string(),
695 Value::U64(3_000_000),
696 );
697 map.insert(
698 property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(),
699 Value::U64(300),
700 );
701 map.insert(
702 property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(),
703 Value::U32(50),
704 );
705 map.insert(
706 property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(),
707 Value::U32(60),
708 );
709 map.insert(
710 property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(),
711 Value::U32(70),
712 );
713
714 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
715
716 assert_eq!(doc.created_at_block_height, Some(100));
717 assert_eq!(doc.updated_at_block_height, Some(200));
718 assert_eq!(doc.transferred_at, Some(3_000_000));
719 assert_eq!(doc.transferred_at_block_height, Some(300));
720 assert_eq!(doc.created_at_core_block_height, Some(50));
721 assert_eq!(doc.updated_at_core_block_height, Some(60));
722 assert_eq!(doc.transferred_at_core_block_height, Some(70));
723 }
724}