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
63#[cfg(feature = "cbor")]
64impl TryFrom<DocumentV0> for DocumentForCbor {
65 type Error = ProtocolError;
66
67 fn try_from(value: DocumentV0) -> Result<Self, Self::Error> {
68 let DocumentV0 {
69 id,
70 properties,
71 owner_id,
72 revision,
73 created_at,
74 updated_at,
75 transferred_at,
76 created_at_block_height,
77 updated_at_block_height,
78 transferred_at_block_height,
79 created_at_core_block_height,
80 updated_at_core_block_height,
81 transferred_at_core_block_height,
82 creator_id,
83 } = value;
84 Ok(DocumentForCbor {
85 id: id.to_buffer(),
86 properties: Value::convert_to_cbor_map(properties)
87 .map_err(ProtocolError::ValueError)?,
88 owner_id: owner_id.to_buffer(),
89 revision,
90 created_at,
91 updated_at,
92 transferred_at,
93 created_at_block_height,
94 updated_at_block_height,
95 transferred_at_block_height,
96 created_at_core_block_height,
97 updated_at_core_block_height,
98 transferred_at_core_block_height,
99 creator_id,
100 })
101 }
102}
103
104impl DocumentV0 {
105 fn from_map(
108 mut document_map: BTreeMap<String, Value>,
109 document_id: Option<[u8; 32]>,
110 owner_id: Option<[u8; 32]>,
111 ) -> Result<Self, ProtocolError> {
112 let owner_id = match owner_id {
113 None => document_map
114 .remove_hash256_bytes(property_names::OWNER_ID)
115 .map_err(ProtocolError::ValueError)?,
116 Some(owner_id) => owner_id,
117 };
118
119 let id = match document_id {
120 None => document_map
121 .remove_hash256_bytes(property_names::ID)
122 .map_err(ProtocolError::ValueError)?,
123 Some(document_id) => document_id,
124 };
125
126 let revision = document_map.remove_optional_integer(property_names::REVISION)?;
127
128 let created_at = document_map.remove_optional_integer(property_names::CREATED_AT)?;
129 let updated_at = document_map.remove_optional_integer(property_names::UPDATED_AT)?;
130 let transferred_at =
131 document_map.remove_optional_integer(property_names::TRANSFERRED_AT)?;
132 let created_at_block_height =
133 document_map.remove_optional_integer(property_names::CREATED_AT_BLOCK_HEIGHT)?;
134 let updated_at_block_height =
135 document_map.remove_optional_integer(property_names::UPDATED_AT_BLOCK_HEIGHT)?;
136 let transferred_at_block_height =
137 document_map.remove_optional_integer(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)?;
138 let created_at_core_block_height =
139 document_map.remove_optional_integer(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)?;
140 let updated_at_core_block_height =
141 document_map.remove_optional_integer(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)?;
142 let transferred_at_core_block_height = document_map
143 .remove_optional_integer(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)?;
144
145 let creator_id = document_map
146 .remove_optional_identifier(property_names::CREATOR_ID)
147 .map_err(ProtocolError::ValueError)?;
148
149 Ok(DocumentV0 {
151 properties: document_map,
152 owner_id: Identifier::new(owner_id),
153 id: Identifier::new(id),
154 revision,
155 created_at,
156 updated_at,
157 transferred_at,
158 created_at_block_height,
159 updated_at_block_height,
160 transferred_at_block_height,
161 created_at_core_block_height,
162 updated_at_core_block_height,
163 transferred_at_core_block_height,
164 creator_id,
165 })
166 }
167}
168
169impl DocumentCborMethodsV0 for DocumentV0 {
170 fn from_cbor(
173 document_cbor: &[u8],
174 document_id: Option<[u8; 32]>,
175 owner_id: Option<[u8; 32]>,
176 _platform_version: &PlatformVersion,
177 ) -> Result<Self, ProtocolError> {
178 let document_cbor_map: BTreeMap<String, CborValue> =
181 ciborium::de::from_reader(document_cbor).map_err(|_| {
182 ProtocolError::InvalidCBOR(
183 "unable to decode document for document call".to_string(),
184 )
185 })?;
186 let document_map: BTreeMap<String, Value> =
187 Value::convert_from_cbor_map(document_cbor_map).map_err(ProtocolError::ValueError)?;
188 Self::from_map(document_map, document_id, owner_id)
189 }
190
191 fn to_cbor_value(&self) -> Result<CborValue, ProtocolError> {
192 let value = platform_value::to_value(self).map_err(ProtocolError::ValueError)?;
198 value.try_into().map_err(ProtocolError::ValueError)
199 }
200
201 fn to_cbor(&self) -> Result<Vec<u8>, ProtocolError> {
203 let mut buffer: Vec<u8> = Vec::new();
204 buffer.write_varint(0).map_err(|_| {
205 ProtocolError::EncodingError("error writing protocol version".to_string())
206 })?;
207 let cbor_document = DocumentForCbor::try_from(self.clone())?;
208 ciborium::ser::into_writer(&cbor_document, &mut buffer).map_err(|_| {
209 ProtocolError::EncodingError("unable to serialize into cbor".to_string())
210 })?;
211 Ok(buffer)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::data_contract::accessors::v0::DataContractV0Getters;
219 use crate::data_contract::document_type::random_document::CreateRandomDocument;
220 use crate::document::serialization_traits::DocumentCborMethodsV0;
221 use crate::document::DocumentV0Getters;
222 use crate::tests::json_document::json_document_to_contract;
223 use platform_version::version::PlatformVersion;
224
225 fn make_document_v0_with_timestamps() -> DocumentV0 {
226 let id = Identifier::new([1u8; 32]);
227 let owner_id = Identifier::new([2u8; 32]);
228 let mut properties = BTreeMap::new();
229 properties.insert("name".to_string(), Value::Text("Alice".to_string()));
230 properties.insert("age".to_string(), Value::U64(30));
231 DocumentV0 {
232 id,
233 owner_id,
234 properties,
235 revision: Some(1),
236 created_at: Some(1_700_000_000_000),
237 updated_at: Some(1_700_000_100_000),
238 transferred_at: None,
239 created_at_block_height: Some(100),
240 updated_at_block_height: Some(200),
241 transferred_at_block_height: None,
242 created_at_core_block_height: Some(50),
243 updated_at_core_block_height: Some(60),
244 transferred_at_core_block_height: None,
245 creator_id: None,
246 }
247 }
248
249 #[test]
254 fn cbor_round_trip_with_random_dashpay_profile() {
255 let platform_version = PlatformVersion::latest();
256 let contract = json_document_to_contract(
257 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
258 false,
259 platform_version,
260 )
261 .expect("expected to load dashpay contract");
262
263 let document_type = contract
264 .document_type_for_name("profile")
265 .expect("expected profile document type");
266
267 for seed in 0..10u64 {
268 let document = document_type
269 .random_document(Some(seed), platform_version)
270 .expect("expected random document");
271
272 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
274 let recovered =
275 crate::document::Document::from_cbor(&cbor_bytes, None, None, platform_version)
276 .expect("from_cbor should succeed");
277
278 assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}");
279 assert_eq!(
280 document.owner_id(),
281 recovered.owner_id(),
282 "owner_id mismatch for seed {seed}"
283 );
284 assert_eq!(
285 document.revision(),
286 recovered.revision(),
287 "revision mismatch for seed {seed}"
288 );
289 assert_eq!(
290 document.properties(),
291 recovered.properties(),
292 "properties mismatch for seed {seed}"
293 );
294 }
295 }
296
297 #[test]
298 fn cbor_round_trip_with_explicit_ids_overrides_embedded_ids() {
299 let platform_version = PlatformVersion::latest();
300 let contract = json_document_to_contract(
301 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
302 false,
303 platform_version,
304 )
305 .expect("expected to load dashpay contract");
306
307 let document_type = contract
308 .document_type_for_name("profile")
309 .expect("expected profile document type");
310
311 let document = document_type
312 .random_document(Some(42), platform_version)
313 .expect("expected random document");
314
315 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
316
317 let override_id = [0xAA; 32];
318 let override_owner = [0xBB; 32];
319
320 let recovered = crate::document::Document::from_cbor(
321 &cbor_bytes,
322 Some(override_id),
323 Some(override_owner),
324 platform_version,
325 )
326 .expect("from_cbor with explicit ids should succeed");
327
328 assert_eq!(
329 recovered.id(),
330 Identifier::new(override_id),
331 "explicit document_id should override the one in CBOR"
332 );
333 assert_eq!(
334 recovered.owner_id(),
335 Identifier::new(override_owner),
336 "explicit owner_id should override the one in CBOR"
337 );
338 }
339
340 #[test]
345 fn to_cbor_value_returns_map_for_document_with_properties() {
346 let doc = make_document_v0_with_timestamps();
347 let cbor_val = doc.to_cbor_value().expect("to_cbor_value should succeed");
348 assert!(
350 cbor_val.is_map(),
351 "CBOR value of a document should be a Map, got {:?}",
352 cbor_val
353 );
354 }
355
356 #[test]
361 fn to_cbor_starts_with_version_zero_varint() {
362 let doc = make_document_v0_with_timestamps();
363 let cbor_bytes = doc.to_cbor().expect("to_cbor should succeed");
364 assert!(!cbor_bytes.is_empty(), "CBOR output should not be empty");
366 assert_eq!(
367 cbor_bytes[0], 0,
368 "first byte should be varint(0) for version"
369 );
370 }
371
372 #[test]
377 fn from_cbor_rejects_invalid_cbor_bytes() {
378 let platform_version = PlatformVersion::latest();
379 let garbage = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01];
380 let result = DocumentV0::from_cbor(&garbage, None, None, platform_version);
381 assert!(
382 result.is_err(),
383 "from_cbor should fail on invalid CBOR bytes"
384 );
385 }
386
387 #[test]
392 fn document_for_cbor_preserves_all_fields() {
393 let doc = make_document_v0_with_timestamps();
394 let cbor_doc = DocumentForCbor::try_from(doc.clone()).expect("TryFrom should succeed");
395 assert_eq!(cbor_doc.id, doc.id.to_buffer());
396 assert_eq!(cbor_doc.owner_id, doc.owner_id.to_buffer());
397 assert_eq!(cbor_doc.revision, doc.revision);
398 assert_eq!(cbor_doc.created_at, doc.created_at);
399 assert_eq!(cbor_doc.updated_at, doc.updated_at);
400 assert_eq!(cbor_doc.transferred_at, doc.transferred_at);
401 assert_eq!(
402 cbor_doc.created_at_block_height,
403 doc.created_at_block_height
404 );
405 assert_eq!(
406 cbor_doc.updated_at_block_height,
407 doc.updated_at_block_height
408 );
409 assert_eq!(
410 cbor_doc.transferred_at_block_height,
411 doc.transferred_at_block_height
412 );
413 assert_eq!(
414 cbor_doc.created_at_core_block_height,
415 doc.created_at_core_block_height
416 );
417 assert_eq!(
418 cbor_doc.updated_at_core_block_height,
419 doc.updated_at_core_block_height
420 );
421 assert_eq!(
422 cbor_doc.transferred_at_core_block_height,
423 doc.transferred_at_core_block_height
424 );
425 }
426
427 #[test]
432 fn from_map_extracts_system_fields_and_leaves_properties() {
433 let id_bytes = [3u8; 32];
434 let owner_bytes = [4u8; 32];
435
436 let mut map = BTreeMap::new();
437 map.insert(property_names::ID.to_string(), Value::Bytes32(id_bytes));
438 map.insert(
439 property_names::OWNER_ID.to_string(),
440 Value::Bytes32(owner_bytes),
441 );
442 map.insert(property_names::REVISION.to_string(), Value::U64(5));
443 map.insert(
444 property_names::CREATED_AT.to_string(),
445 Value::U64(1_000_000),
446 );
447 map.insert(
448 property_names::UPDATED_AT.to_string(),
449 Value::U64(2_000_000),
450 );
451 map.insert("customField".to_string(), Value::Text("hello".to_string()));
452
453 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
454
455 assert_eq!(doc.id, Identifier::new(id_bytes));
456 assert_eq!(doc.owner_id, Identifier::new(owner_bytes));
457 assert_eq!(doc.revision, Some(5));
458 assert_eq!(doc.created_at, Some(1_000_000));
459 assert_eq!(doc.updated_at, Some(2_000_000));
460 assert_eq!(
462 doc.properties.get("customField"),
463 Some(&Value::Text("hello".to_string()))
464 );
465 assert!(!doc.properties.contains_key(property_names::ID));
467 assert!(!doc.properties.contains_key(property_names::OWNER_ID));
468 assert!(!doc.properties.contains_key(property_names::REVISION));
469 }
470
471 #[test]
472 fn from_map_with_explicit_ids_overrides_map_ids() {
473 let map_id = [10u8; 32];
474 let map_owner = [11u8; 32];
475 let override_id = [20u8; 32];
476 let override_owner = [21u8; 32];
477
478 let mut map = BTreeMap::new();
479 map.insert(property_names::ID.to_string(), Value::Bytes32(map_id));
480 map.insert(
481 property_names::OWNER_ID.to_string(),
482 Value::Bytes32(map_owner),
483 );
484
485 let doc = DocumentV0::from_map(map, Some(override_id), Some(override_owner))
486 .expect("from_map should succeed");
487
488 assert_eq!(
489 doc.id,
490 Identifier::new(override_id),
491 "explicit document_id should take precedence"
492 );
493 assert_eq!(
494 doc.owner_id,
495 Identifier::new(override_owner),
496 "explicit owner_id should take precedence"
497 );
498 }
499
500 #[test]
509 fn from_map_missing_id_fails_when_not_provided() {
510 let mut map = BTreeMap::new();
513 map.insert(
514 property_names::OWNER_ID.to_string(),
515 Value::Bytes32([4u8; 32]),
516 );
517
518 let result = DocumentV0::from_map(map, None, None);
519 assert!(
520 result.is_err(),
521 "from_map without $id or explicit document_id should fail"
522 );
523 }
524
525 #[test]
526 fn from_map_missing_owner_id_fails_when_not_provided() {
527 let mut map = BTreeMap::new();
528 map.insert(property_names::ID.to_string(), Value::Bytes32([3u8; 32]));
529
530 let result = DocumentV0::from_map(map, None, None);
531 assert!(
532 result.is_err(),
533 "from_map without $ownerId or explicit owner_id should fail"
534 );
535 }
536
537 #[test]
542 fn from_map_extracts_creator_id_when_present_as_identifier() {
543 let creator = Identifier::new([0xCD; 32]);
544 let mut map = BTreeMap::new();
545 map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32]));
546 map.insert(
547 property_names::OWNER_ID.to_string(),
548 Value::Bytes32([2u8; 32]),
549 );
550 map.insert(
551 property_names::CREATOR_ID.to_string(),
552 Value::Identifier(creator.to_buffer()),
553 );
554
555 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
556 assert_eq!(doc.creator_id, Some(creator));
557 }
558
559 #[test]
560 fn from_map_creator_id_missing_stays_none() {
561 let mut map = BTreeMap::new();
562 map.insert(property_names::ID.to_string(), Value::Bytes32([1u8; 32]));
563 map.insert(
564 property_names::OWNER_ID.to_string(),
565 Value::Bytes32([2u8; 32]),
566 );
567
568 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
569 assert_eq!(doc.creator_id, None);
570 }
571
572 #[test]
577 fn from_cbor_rejects_empty_buffer() {
578 let platform_version = PlatformVersion::latest();
579 let result = DocumentV0::from_cbor(&[], None, None, platform_version);
580 assert!(
581 result.is_err(),
582 "from_cbor should fail on an empty input buffer"
583 );
584 }
585
586 #[test]
587 fn from_cbor_rejects_truncated_map_bytes() {
588 let platform_version = PlatformVersion::latest();
590 let result = DocumentV0::from_cbor(&[0xA1], None, None, platform_version);
591 assert!(
592 result.is_err(),
593 "from_cbor should fail on a truncated map prefix"
594 );
595 }
596
597 #[test]
602 fn cbor_round_trip_via_to_cbor_and_from_cbor_preserves_fields() {
603 let platform_version = PlatformVersion::latest();
604 let doc = make_document_v0_with_timestamps();
605
606 let bytes = doc.to_cbor().expect("to_cbor succeeds");
607 let recovered = crate::document::Document::from_cbor(&bytes, None, None, platform_version)
608 .expect("from_cbor succeeds");
609 assert_eq!(doc.id, recovered.id());
610 assert_eq!(doc.owner_id, recovered.owner_id());
611 assert_eq!(doc.revision, recovered.revision());
612 }
613
614 #[test]
620 fn document_for_cbor_preserves_user_properties() {
621 let doc = make_document_v0_with_timestamps();
622 let cbor = DocumentForCbor::try_from(doc.clone()).expect("try_from succeeds");
623 assert!(cbor.properties.contains_key("name"));
625 assert!(cbor.properties.contains_key("age"));
626 }
627
628 #[test]
629 fn from_map_with_all_timestamp_variants() {
630 let mut map = BTreeMap::new();
631 map.insert(property_names::ID.to_string(), Value::Bytes32([5u8; 32]));
632 map.insert(
633 property_names::OWNER_ID.to_string(),
634 Value::Bytes32([6u8; 32]),
635 );
636 map.insert(
637 property_names::CREATED_AT_BLOCK_HEIGHT.to_string(),
638 Value::U64(100),
639 );
640 map.insert(
641 property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(),
642 Value::U64(200),
643 );
644 map.insert(
645 property_names::TRANSFERRED_AT.to_string(),
646 Value::U64(3_000_000),
647 );
648 map.insert(
649 property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(),
650 Value::U64(300),
651 );
652 map.insert(
653 property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(),
654 Value::U32(50),
655 );
656 map.insert(
657 property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(),
658 Value::U32(60),
659 );
660 map.insert(
661 property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(),
662 Value::U32(70),
663 );
664
665 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
666
667 assert_eq!(doc.created_at_block_height, Some(100));
668 assert_eq!(doc.updated_at_block_height, Some(200));
669 assert_eq!(doc.transferred_at, Some(3_000_000));
670 assert_eq!(doc.transferred_at_block_height, Some(300));
671 assert_eq!(doc.created_at_core_block_height, Some(50));
672 assert_eq!(doc.updated_at_core_block_height, Some(60));
673 assert_eq!(doc.transferred_at_core_block_height, Some(70));
674 }
675}