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(
64 rename = "$moderatedAt",
65 default,
66 skip_serializing_if = "Option::is_none"
67 )]
68 pub moderated_at: Option<TimestampMillis>,
69 #[serde(
70 rename = "$moderatedBy",
71 default,
72 skip_serializing_if = "Option::is_none"
73 )]
74 pub moderated_by: Option<Identifier>,
75
76 #[serde(
79 rename = "$contractVersion",
80 default,
81 skip_serializing_if = "Option::is_none"
82 )]
83 pub contract_version: Option<u32>,
84}
85
86#[cfg(feature = "cbor")]
87impl TryFrom<DocumentV0> for DocumentForCbor {
88 type Error = ProtocolError;
89
90 fn try_from(value: DocumentV0) -> Result<Self, Self::Error> {
91 let DocumentV0 {
92 id,
93 properties,
94 owner_id,
95 revision,
96 created_at,
97 updated_at,
98 transferred_at,
99 created_at_block_height,
100 updated_at_block_height,
101 transferred_at_block_height,
102 created_at_core_block_height,
103 updated_at_core_block_height,
104 transferred_at_core_block_height,
105 creator_id,
106 moderated_at,
107 moderated_by,
108 contract_version,
109 } = value;
110 Ok(DocumentForCbor {
111 contract_version,
112 id: id.to_buffer(),
113 properties: Value::convert_to_cbor_map(properties)
114 .map_err(ProtocolError::ValueError)?,
115 owner_id: owner_id.to_buffer(),
116 revision,
117 created_at,
118 updated_at,
119 transferred_at,
120 created_at_block_height,
121 updated_at_block_height,
122 transferred_at_block_height,
123 created_at_core_block_height,
124 updated_at_core_block_height,
125 transferred_at_core_block_height,
126 creator_id,
127 moderated_at,
128 moderated_by,
129 })
130 }
131}
132
133impl DocumentV0 {
134 fn from_map(
137 mut document_map: BTreeMap<String, Value>,
138 document_id: Option<[u8; 32]>,
139 owner_id: Option<[u8; 32]>,
140 ) -> Result<Self, ProtocolError> {
141 let owner_id = match owner_id {
142 None => document_map
143 .remove_hash256_bytes(property_names::OWNER_ID)
144 .map_err(ProtocolError::ValueError)?,
145 Some(owner_id) => owner_id,
146 };
147
148 let id = match document_id {
149 None => document_map
150 .remove_hash256_bytes(property_names::ID)
151 .map_err(ProtocolError::ValueError)?,
152 Some(document_id) => document_id,
153 };
154
155 let revision = document_map.remove_optional_integer(property_names::REVISION)?;
156
157 let created_at = document_map.remove_optional_integer(property_names::CREATED_AT)?;
158 let updated_at = document_map.remove_optional_integer(property_names::UPDATED_AT)?;
159 let transferred_at =
160 document_map.remove_optional_integer(property_names::TRANSFERRED_AT)?;
161 let created_at_block_height =
162 document_map.remove_optional_integer(property_names::CREATED_AT_BLOCK_HEIGHT)?;
163 let updated_at_block_height =
164 document_map.remove_optional_integer(property_names::UPDATED_AT_BLOCK_HEIGHT)?;
165 let transferred_at_block_height =
166 document_map.remove_optional_integer(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)?;
167 let created_at_core_block_height =
168 document_map.remove_optional_integer(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)?;
169 let updated_at_core_block_height =
170 document_map.remove_optional_integer(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)?;
171 let transferred_at_core_block_height = document_map
172 .remove_optional_integer(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)?;
173
174 let creator_id = document_map
175 .remove_optional_identifier(property_names::CREATOR_ID)
176 .map_err(ProtocolError::ValueError)?;
177
178 let contract_version =
179 document_map.remove_optional_integer(property_names::CONTRACT_VERSION)?;
180
181 let moderated_at = document_map.remove_optional_integer(property_names::MODERATED_AT)?;
182 let moderated_by = document_map
183 .remove_optional_identifier(property_names::MODERATED_BY)
184 .map_err(ProtocolError::ValueError)?;
185
186 Ok(DocumentV0 {
188 contract_version,
189 properties: document_map,
190 owner_id: Identifier::new(owner_id),
191 id: Identifier::new(id),
192 revision,
193 created_at,
194 updated_at,
195 transferred_at,
196 created_at_block_height,
197 updated_at_block_height,
198 transferred_at_block_height,
199 created_at_core_block_height,
200 updated_at_core_block_height,
201 transferred_at_core_block_height,
202 creator_id,
203 moderated_at,
204 moderated_by,
205 })
206 }
207}
208
209impl DocumentCborMethodsV0 for DocumentV0 {
210 fn from_cbor(
213 document_cbor: &[u8],
214 document_id: Option<[u8; 32]>,
215 owner_id: Option<[u8; 32]>,
216 _platform_version: &PlatformVersion,
217 ) -> Result<Self, ProtocolError> {
218 let document_cbor_map: BTreeMap<String, CborValue> =
221 ciborium::de::from_reader(document_cbor).map_err(|_| {
222 ProtocolError::InvalidCBOR(
223 "unable to decode document for document call".to_string(),
224 )
225 })?;
226 let document_map: BTreeMap<String, Value> =
227 Value::convert_from_cbor_map(document_cbor_map).map_err(ProtocolError::ValueError)?;
228 Self::from_map(document_map, document_id, owner_id)
229 }
230
231 fn to_cbor_value(&self) -> Result<CborValue, ProtocolError> {
232 let value = platform_value::to_value(self).map_err(ProtocolError::ValueError)?;
238 value.try_into().map_err(ProtocolError::ValueError)
239 }
240
241 fn to_cbor(&self) -> Result<Vec<u8>, ProtocolError> {
243 let mut buffer: Vec<u8> = Vec::new();
244 buffer.write_varint(0).map_err(|_| {
245 ProtocolError::EncodingError("error writing protocol version".to_string())
246 })?;
247 let cbor_document = DocumentForCbor::try_from(self.clone())?;
248 ciborium::ser::into_writer(&cbor_document, &mut buffer).map_err(|_| {
249 ProtocolError::EncodingError("unable to serialize into cbor".to_string())
250 })?;
251 Ok(buffer)
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use crate::data_contract::accessors::v0::DataContractV0Getters;
259 use crate::data_contract::document_type::random_document::CreateRandomDocument;
260 use crate::document::serialization_traits::DocumentCborMethodsV0;
261 use crate::document::DocumentV0Getters;
262 use crate::tests::json_document::json_document_to_contract;
263 use platform_version::version::PlatformVersion;
264
265 fn make_document_v0_with_timestamps() -> DocumentV0 {
266 let id = Identifier::new([1u8; 32]);
267 let owner_id = Identifier::new([2u8; 32]);
268 let mut properties = BTreeMap::new();
269 properties.insert("name".to_string(), Value::Text("Alice".to_string()));
270 properties.insert("age".to_string(), Value::U64(30));
271 DocumentV0 {
272 contract_version: None,
273 id,
274 owner_id,
275 properties,
276 revision: Some(1),
277 created_at: Some(1_700_000_000_000),
278 updated_at: Some(1_700_000_100_000),
279 transferred_at: None,
280 created_at_block_height: Some(100),
281 updated_at_block_height: Some(200),
282 transferred_at_block_height: None,
283 created_at_core_block_height: Some(50),
284 updated_at_core_block_height: Some(60),
285 transferred_at_core_block_height: None,
286 creator_id: None,
287 moderated_at: None,
288 moderated_by: None,
289 }
290 }
291
292 #[test]
297 fn cbor_round_trip_preserves_contract_version_stamp() {
298 use crate::document::Document;
299
300 let platform_version = PlatformVersion::latest();
301 let mut document = make_document_v0_with_timestamps();
302 document.contract_version = Some(7);
303
304 let cbor = document.to_cbor().expect("expected to serialize to cbor");
305 let restored = Document::from_cbor(&cbor, None, None, platform_version)
306 .expect("expected to deserialize from cbor");
307
308 let Document::V0(restored) = restored;
309 assert_eq!(restored.contract_version, Some(7));
310 assert_eq!(restored.id, document.id);
311 assert_eq!(restored.revision, document.revision);
312 assert_eq!(
315 restored.properties.get("name"),
316 document.properties.get("name")
317 );
318
319 let unstamped = make_document_v0_with_timestamps();
322 let unstamped_cbor = unstamped.to_cbor().expect("expected to serialize to cbor");
323 let restored_unstamped = Document::from_cbor(&unstamped_cbor, None, None, platform_version)
324 .expect("expected to deserialize from cbor");
325 let Document::V0(restored_unstamped) = restored_unstamped;
326 assert_eq!(restored_unstamped.contract_version, None);
327 }
328
329 #[test]
330 fn cbor_round_trip_with_random_dashpay_profile() {
331 let platform_version = PlatformVersion::latest();
332 let contract = json_document_to_contract(
333 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
334 false,
335 platform_version,
336 )
337 .expect("expected to load dashpay contract");
338
339 let document_type = contract
340 .document_type_for_name("profile")
341 .expect("expected profile document type");
342
343 for seed in 0..10u64 {
344 let document = document_type
345 .random_document(Some(seed), platform_version)
346 .expect("expected random document");
347
348 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
350 let recovered =
351 crate::document::Document::from_cbor(&cbor_bytes, None, None, platform_version)
352 .expect("from_cbor should succeed");
353
354 assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}");
355 assert_eq!(
356 document.owner_id(),
357 recovered.owner_id(),
358 "owner_id mismatch for seed {seed}"
359 );
360 assert_eq!(
361 document.revision(),
362 recovered.revision(),
363 "revision mismatch for seed {seed}"
364 );
365 assert_eq!(
366 document.properties(),
367 recovered.properties(),
368 "properties mismatch for seed {seed}"
369 );
370 }
371 }
372
373 #[test]
374 fn cbor_round_trip_with_explicit_ids_overrides_embedded_ids() {
375 let platform_version = PlatformVersion::latest();
376 let contract = json_document_to_contract(
377 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
378 false,
379 platform_version,
380 )
381 .expect("expected to load dashpay contract");
382
383 let document_type = contract
384 .document_type_for_name("profile")
385 .expect("expected profile document type");
386
387 let document = document_type
388 .random_document(Some(42), platform_version)
389 .expect("expected random document");
390
391 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
392
393 let override_id = [0xAA; 32];
394 let override_owner = [0xBB; 32];
395
396 let recovered = crate::document::Document::from_cbor(
397 &cbor_bytes,
398 Some(override_id),
399 Some(override_owner),
400 platform_version,
401 )
402 .expect("from_cbor with explicit ids should succeed");
403
404 assert_eq!(
405 recovered.id(),
406 Identifier::new(override_id),
407 "explicit document_id should override the one in CBOR"
408 );
409 assert_eq!(
410 recovered.owner_id(),
411 Identifier::new(override_owner),
412 "explicit owner_id should override the one in CBOR"
413 );
414 }
415
416 #[test]
421 fn to_cbor_value_returns_map_for_document_with_properties() {
422 let doc = make_document_v0_with_timestamps();
423 let cbor_val = doc.to_cbor_value().expect("to_cbor_value should succeed");
424 assert!(
426 cbor_val.is_map(),
427 "CBOR value of a document should be a Map, got {:?}",
428 cbor_val
429 );
430 }
431
432 #[test]
437 fn to_cbor_starts_with_version_zero_varint() {
438 let doc = make_document_v0_with_timestamps();
439 let cbor_bytes = doc.to_cbor().expect("to_cbor should succeed");
440 assert!(!cbor_bytes.is_empty(), "CBOR output should not be empty");
442 assert_eq!(
443 cbor_bytes[0], 0,
444 "first byte should be varint(0) for version"
445 );
446 }
447
448 #[test]
453 fn from_cbor_rejects_invalid_cbor_bytes() {
454 let platform_version = PlatformVersion::latest();
455 let garbage = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01];
456 let result = DocumentV0::from_cbor(&garbage, None, None, platform_version);
457 assert!(
458 result.is_err(),
459 "from_cbor should fail on invalid CBOR bytes"
460 );
461 }
462
463 #[test]
468 fn document_for_cbor_preserves_all_fields() {
469 let doc = make_document_v0_with_timestamps();
470 let cbor_doc = DocumentForCbor::try_from(doc.clone()).expect("TryFrom should succeed");
471 assert_eq!(cbor_doc.id, doc.id.to_buffer());
472 assert_eq!(cbor_doc.owner_id, doc.owner_id.to_buffer());
473 assert_eq!(cbor_doc.revision, doc.revision);
474 assert_eq!(cbor_doc.created_at, doc.created_at);
475 assert_eq!(cbor_doc.updated_at, doc.updated_at);
476 assert_eq!(cbor_doc.transferred_at, doc.transferred_at);
477 assert_eq!(
478 cbor_doc.created_at_block_height,
479 doc.created_at_block_height
480 );
481 assert_eq!(
482 cbor_doc.updated_at_block_height,
483 doc.updated_at_block_height
484 );
485 assert_eq!(
486 cbor_doc.transferred_at_block_height,
487 doc.transferred_at_block_height
488 );
489 assert_eq!(
490 cbor_doc.created_at_core_block_height,
491 doc.created_at_core_block_height
492 );
493 assert_eq!(
494 cbor_doc.updated_at_core_block_height,
495 doc.updated_at_core_block_height
496 );
497 assert_eq!(
498 cbor_doc.transferred_at_core_block_height,
499 doc.transferred_at_core_block_height
500 );
501 }
502
503 #[test]
508 fn from_map_extracts_system_fields_and_leaves_properties() {
509 let id_bytes = [3u8; 32];
510 let owner_bytes = [4u8; 32];
511
512 let mut map = BTreeMap::new();
513 map.insert(property_names::ID.to_string(), Value::Bytes32(id_bytes));
514 map.insert(
515 property_names::OWNER_ID.to_string(),
516 Value::Bytes32(owner_bytes),
517 );
518 map.insert(property_names::REVISION.to_string(), Value::U64(5));
519 map.insert(
520 property_names::CREATED_AT.to_string(),
521 Value::U64(1_000_000),
522 );
523 map.insert(
524 property_names::UPDATED_AT.to_string(),
525 Value::U64(2_000_000),
526 );
527 map.insert("customField".to_string(), Value::Text("hello".to_string()));
528
529 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
530
531 assert_eq!(doc.id, Identifier::new(id_bytes));
532 assert_eq!(doc.owner_id, Identifier::new(owner_bytes));
533 assert_eq!(doc.revision, Some(5));
534 assert_eq!(doc.created_at, Some(1_000_000));
535 assert_eq!(doc.updated_at, Some(2_000_000));
536 assert_eq!(
538 doc.properties.get("customField"),
539 Some(&Value::Text("hello".to_string()))
540 );
541 assert!(!doc.properties.contains_key(property_names::ID));
543 assert!(!doc.properties.contains_key(property_names::OWNER_ID));
544 assert!(!doc.properties.contains_key(property_names::REVISION));
545 }
546
547 #[test]
548 fn from_map_with_explicit_ids_overrides_map_ids() {
549 let map_id = [10u8; 32];
550 let map_owner = [11u8; 32];
551 let override_id = [20u8; 32];
552 let override_owner = [21u8; 32];
553
554 let mut map = BTreeMap::new();
555 map.insert(property_names::ID.to_string(), Value::Bytes32(map_id));
556 map.insert(
557 property_names::OWNER_ID.to_string(),
558 Value::Bytes32(map_owner),
559 );
560
561 let doc = DocumentV0::from_map(map, Some(override_id), Some(override_owner))
562 .expect("from_map should succeed");
563
564 assert_eq!(
565 doc.id,
566 Identifier::new(override_id),
567 "explicit document_id should take precedence"
568 );
569 assert_eq!(
570 doc.owner_id,
571 Identifier::new(override_owner),
572 "explicit owner_id should take precedence"
573 );
574 }
575
576 #[test]
585 fn from_map_missing_id_fails_when_not_provided() {
586 let mut map = BTreeMap::new();
589 map.insert(
590 property_names::OWNER_ID.to_string(),
591 Value::Bytes32([4u8; 32]),
592 );
593
594 let result = DocumentV0::from_map(map, None, None);
595 assert!(
596 result.is_err(),
597 "from_map without $id or explicit document_id should fail"
598 );
599 }
600
601 #[test]
602 fn from_map_missing_owner_id_fails_when_not_provided() {
603 let mut map = BTreeMap::new();
604 map.insert(property_names::ID.to_string(), Value::Bytes32([3u8; 32]));
605
606 let result = DocumentV0::from_map(map, None, None);
607 assert!(
608 result.is_err(),
609 "from_map without $ownerId or explicit owner_id should fail"
610 );
611 }
612
613 #[test]
618 fn from_map_extracts_creator_id_when_present_as_identifier() {
619 let creator = Identifier::new([0xCD; 32]);
620 let mut map = BTreeMap::new();
621 map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32]));
622 map.insert(
623 property_names::OWNER_ID.to_string(),
624 Value::Bytes32([2u8; 32]),
625 );
626 map.insert(
627 property_names::CREATOR_ID.to_string(),
628 Value::Identifier(creator.to_buffer()),
629 );
630
631 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
632 assert_eq!(doc.creator_id, Some(creator));
633 }
634
635 #[test]
636 fn from_map_creator_id_missing_stays_none() {
637 let mut map = BTreeMap::new();
638 map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32]));
639 map.insert(
640 property_names::OWNER_ID.to_string(),
641 Value::Bytes32([2u8; 32]),
642 );
643
644 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
645 assert_eq!(doc.creator_id, None);
646 }
647
648 #[test]
653 fn from_cbor_rejects_empty_buffer() {
654 let platform_version = PlatformVersion::latest();
655 let result = DocumentV0::from_cbor(&[], None, None, platform_version);
656 assert!(
657 result.is_err(),
658 "from_cbor should fail on an empty input buffer"
659 );
660 }
661
662 #[test]
663 fn from_cbor_rejects_truncated_map_bytes() {
664 let platform_version = PlatformVersion::latest();
666 let result = DocumentV0::from_cbor(&[0xA1], None, None, platform_version);
667 assert!(
668 result.is_err(),
669 "from_cbor should fail on a truncated map prefix"
670 );
671 }
672
673 #[test]
678 fn cbor_round_trip_via_to_cbor_and_from_cbor_preserves_fields() {
679 let platform_version = PlatformVersion::latest();
680 let doc = make_document_v0_with_timestamps();
681
682 let bytes = doc.to_cbor().expect("to_cbor succeeds");
683 let recovered = crate::document::Document::from_cbor(&bytes, None, None, platform_version)
684 .expect("from_cbor succeeds");
685 assert_eq!(doc.id, recovered.id());
686 assert_eq!(doc.owner_id, recovered.owner_id());
687 assert_eq!(doc.revision, recovered.revision());
688 }
689
690 #[test]
696 fn document_for_cbor_preserves_user_properties() {
697 let doc = make_document_v0_with_timestamps();
698 let cbor = DocumentForCbor::try_from(doc.clone()).expect("try_from succeeds");
699 assert!(cbor.properties.contains_key("name"));
701 assert!(cbor.properties.contains_key("age"));
702 }
703
704 #[test]
705 fn from_map_with_all_timestamp_variants() {
706 let mut map = BTreeMap::new();
707 map.insert(property_names::ID.to_string(), Value::Bytes32([5u8; 32]));
708 map.insert(
709 property_names::OWNER_ID.to_string(),
710 Value::Bytes32([6u8; 32]),
711 );
712 map.insert(
713 property_names::CREATED_AT_BLOCK_HEIGHT.to_string(),
714 Value::U64(100),
715 );
716 map.insert(
717 property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(),
718 Value::U64(200),
719 );
720 map.insert(
721 property_names::TRANSFERRED_AT.to_string(),
722 Value::U64(3_000_000),
723 );
724 map.insert(
725 property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(),
726 Value::U64(300),
727 );
728 map.insert(
729 property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(),
730 Value::U32(50),
731 );
732 map.insert(
733 property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(),
734 Value::U32(60),
735 );
736 map.insert(
737 property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(),
738 Value::U32(70),
739 );
740
741 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
742
743 assert_eq!(doc.created_at_block_height, Some(100));
744 assert_eq!(doc.updated_at_block_height, Some(200));
745 assert_eq!(doc.transferred_at, Some(3_000_000));
746 assert_eq!(doc.transferred_at_block_height, Some(300));
747 assert_eq!(doc.created_at_core_block_height, Some(50));
748 assert_eq!(doc.updated_at_core_block_height, Some(60));
749 assert_eq!(doc.transferred_at_core_block_height, Some(70));
750 }
751}