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::{
9 DocumentCborMethodsV0, DocumentPlatformValueMethodsV0,
10};
11use crate::document::v0::DocumentV0;
12use crate::version::PlatformVersion;
13use ciborium::Value as CborValue;
14use integer_encoding::VarIntWriter;
15use platform_value::btreemap_extensions::BTreeValueRemoveFromMapHelper;
16use platform_value::{Identifier, Value};
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeMap;
19use std::convert::{TryFrom, TryInto};
20
21#[cfg(feature = "cbor")]
22#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
23pub struct DocumentForCbor {
24 #[serde(rename = "$id")]
26 pub id: [u8; 32],
27
28 #[serde(flatten)]
30 pub properties: BTreeMap<String, CborValue>,
31
32 #[serde(rename = "$ownerId")]
34 pub owner_id: [u8; 32],
35
36 #[serde(rename = "$revision")]
38 pub revision: Option<Revision>,
39
40 #[serde(rename = "$createdAt")]
41 pub created_at: Option<TimestampMillis>,
42 #[serde(rename = "$updatedAt")]
43 pub updated_at: Option<TimestampMillis>,
44 #[serde(rename = "$transferredAt")]
45 pub transferred_at: Option<TimestampMillis>,
46
47 #[serde(rename = "$createdAtBlockHeight")]
48 pub created_at_block_height: Option<BlockHeight>,
49 #[serde(rename = "$updatedAtBlockHeight")]
50 pub updated_at_block_height: Option<BlockHeight>,
51 #[serde(rename = "$transferredAtBlockHeight")]
52 pub transferred_at_block_height: Option<BlockHeight>,
53
54 #[serde(rename = "$createdAtCoreBlockHeight")]
55 pub created_at_core_block_height: Option<CoreBlockHeight>,
56 #[serde(rename = "$updatedAtCoreBlockHeight")]
57 pub updated_at_core_block_height: Option<CoreBlockHeight>,
58 #[serde(rename = "$transferredAtCoreBlockHeight")]
59 pub transferred_at_core_block_height: Option<CoreBlockHeight>,
60
61 #[serde(rename = "$creatorId")]
62 pub creator_id: Option<Identifier>,
63}
64
65#[cfg(feature = "cbor")]
66impl TryFrom<DocumentV0> for DocumentForCbor {
67 type Error = ProtocolError;
68
69 fn try_from(value: DocumentV0) -> Result<Self, Self::Error> {
70 let DocumentV0 {
71 id,
72 properties,
73 owner_id,
74 revision,
75 created_at,
76 updated_at,
77 transferred_at,
78 created_at_block_height,
79 updated_at_block_height,
80 transferred_at_block_height,
81 created_at_core_block_height,
82 updated_at_core_block_height,
83 transferred_at_core_block_height,
84 creator_id,
85 } = value;
86 Ok(DocumentForCbor {
87 id: id.to_buffer(),
88 properties: Value::convert_to_cbor_map(properties)
89 .map_err(ProtocolError::ValueError)?,
90 owner_id: owner_id.to_buffer(),
91 revision,
92 created_at,
93 updated_at,
94 transferred_at,
95 created_at_block_height,
96 updated_at_block_height,
97 transferred_at_block_height,
98 created_at_core_block_height,
99 updated_at_core_block_height,
100 transferred_at_core_block_height,
101 creator_id,
102 })
103 }
104}
105
106impl DocumentV0 {
107 fn from_map(
110 mut document_map: BTreeMap<String, Value>,
111 document_id: Option<[u8; 32]>,
112 owner_id: Option<[u8; 32]>,
113 ) -> Result<Self, ProtocolError> {
114 let owner_id = match owner_id {
115 None => document_map
116 .remove_hash256_bytes(property_names::OWNER_ID)
117 .map_err(ProtocolError::ValueError)?,
118 Some(owner_id) => owner_id,
119 };
120
121 let id = match document_id {
122 None => document_map
123 .remove_hash256_bytes(property_names::ID)
124 .map_err(ProtocolError::ValueError)?,
125 Some(document_id) => document_id,
126 };
127
128 let revision = document_map.remove_optional_integer(property_names::REVISION)?;
129
130 let created_at = document_map.remove_optional_integer(property_names::CREATED_AT)?;
131 let updated_at = document_map.remove_optional_integer(property_names::UPDATED_AT)?;
132 let transferred_at =
133 document_map.remove_optional_integer(property_names::TRANSFERRED_AT)?;
134 let created_at_block_height =
135 document_map.remove_optional_integer(property_names::CREATED_AT_BLOCK_HEIGHT)?;
136 let updated_at_block_height =
137 document_map.remove_optional_integer(property_names::UPDATED_AT_BLOCK_HEIGHT)?;
138 let transferred_at_block_height =
139 document_map.remove_optional_integer(property_names::TRANSFERRED_AT_BLOCK_HEIGHT)?;
140 let created_at_core_block_height =
141 document_map.remove_optional_integer(property_names::CREATED_AT_CORE_BLOCK_HEIGHT)?;
142 let updated_at_core_block_height =
143 document_map.remove_optional_integer(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT)?;
144 let transferred_at_core_block_height = document_map
145 .remove_optional_integer(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT)?;
146
147 let creator_id = document_map
148 .remove_optional_identifier(property_names::CREATOR_ID)
149 .map_err(ProtocolError::ValueError)?;
150
151 Ok(DocumentV0 {
153 properties: document_map,
154 owner_id: Identifier::new(owner_id),
155 id: Identifier::new(id),
156 revision,
157 created_at,
158 updated_at,
159 transferred_at,
160 created_at_block_height,
161 updated_at_block_height,
162 transferred_at_block_height,
163 created_at_core_block_height,
164 updated_at_core_block_height,
165 transferred_at_core_block_height,
166 creator_id,
167 })
168 }
169}
170
171impl DocumentCborMethodsV0 for DocumentV0 {
172 fn from_cbor(
175 document_cbor: &[u8],
176 document_id: Option<[u8; 32]>,
177 owner_id: Option<[u8; 32]>,
178 _platform_version: &PlatformVersion,
179 ) -> Result<Self, ProtocolError> {
180 let document_cbor_map: BTreeMap<String, CborValue> =
183 ciborium::de::from_reader(document_cbor).map_err(|_| {
184 ProtocolError::InvalidCBOR(
185 "unable to decode document for document call".to_string(),
186 )
187 })?;
188 let document_map: BTreeMap<String, Value> =
189 Value::convert_from_cbor_map(document_cbor_map).map_err(ProtocolError::ValueError)?;
190 Self::from_map(document_map, document_id, owner_id)
191 }
192
193 fn to_cbor_value(&self) -> Result<CborValue, ProtocolError> {
194 self.to_object()
195 .map(|v| v.try_into().map_err(ProtocolError::ValueError))?
196 }
197
198 fn to_cbor(&self) -> Result<Vec<u8>, ProtocolError> {
200 let mut buffer: Vec<u8> = Vec::new();
201 buffer.write_varint(0).map_err(|_| {
202 ProtocolError::EncodingError("error writing protocol version".to_string())
203 })?;
204 let cbor_document = DocumentForCbor::try_from(self.clone())?;
205 ciborium::ser::into_writer(&cbor_document, &mut buffer).map_err(|_| {
206 ProtocolError::EncodingError("unable to serialize into cbor".to_string())
207 })?;
208 Ok(buffer)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::data_contract::accessors::v0::DataContractV0Getters;
216 use crate::data_contract::document_type::random_document::CreateRandomDocument;
217 use crate::document::serialization_traits::DocumentCborMethodsV0;
218 use crate::document::DocumentV0Getters;
219 use crate::tests::json_document::json_document_to_contract;
220 use platform_version::version::PlatformVersion;
221
222 fn make_document_v0_with_timestamps() -> DocumentV0 {
223 let id = Identifier::new([1u8; 32]);
224 let owner_id = Identifier::new([2u8; 32]);
225 let mut properties = BTreeMap::new();
226 properties.insert("name".to_string(), Value::Text("Alice".to_string()));
227 properties.insert("age".to_string(), Value::U64(30));
228 DocumentV0 {
229 id,
230 owner_id,
231 properties,
232 revision: Some(1),
233 created_at: Some(1_700_000_000_000),
234 updated_at: Some(1_700_000_100_000),
235 transferred_at: None,
236 created_at_block_height: Some(100),
237 updated_at_block_height: Some(200),
238 transferred_at_block_height: None,
239 created_at_core_block_height: Some(50),
240 updated_at_core_block_height: Some(60),
241 transferred_at_core_block_height: None,
242 creator_id: None,
243 }
244 }
245
246 #[test]
251 fn cbor_round_trip_with_random_dashpay_profile() {
252 let platform_version = PlatformVersion::latest();
253 let contract = json_document_to_contract(
254 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
255 false,
256 platform_version,
257 )
258 .expect("expected to load dashpay contract");
259
260 let document_type = contract
261 .document_type_for_name("profile")
262 .expect("expected profile document type");
263
264 for seed in 0..10u64 {
265 let document = document_type
266 .random_document(Some(seed), platform_version)
267 .expect("expected random document");
268
269 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
271 let recovered =
272 crate::document::Document::from_cbor(&cbor_bytes, None, None, platform_version)
273 .expect("from_cbor should succeed");
274
275 assert_eq!(document.id(), recovered.id(), "id mismatch for seed {seed}");
276 assert_eq!(
277 document.owner_id(),
278 recovered.owner_id(),
279 "owner_id mismatch for seed {seed}"
280 );
281 assert_eq!(
282 document.revision(),
283 recovered.revision(),
284 "revision mismatch for seed {seed}"
285 );
286 assert_eq!(
287 document.properties(),
288 recovered.properties(),
289 "properties mismatch for seed {seed}"
290 );
291 }
292 }
293
294 #[test]
295 fn cbor_round_trip_with_explicit_ids_overrides_embedded_ids() {
296 let platform_version = PlatformVersion::latest();
297 let contract = json_document_to_contract(
298 "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
299 false,
300 platform_version,
301 )
302 .expect("expected to load dashpay contract");
303
304 let document_type = contract
305 .document_type_for_name("profile")
306 .expect("expected profile document type");
307
308 let document = document_type
309 .random_document(Some(42), platform_version)
310 .expect("expected random document");
311
312 let cbor_bytes = document.to_cbor().expect("to_cbor should succeed");
313
314 let override_id = [0xAA; 32];
315 let override_owner = [0xBB; 32];
316
317 let recovered = crate::document::Document::from_cbor(
318 &cbor_bytes,
319 Some(override_id),
320 Some(override_owner),
321 platform_version,
322 )
323 .expect("from_cbor with explicit ids should succeed");
324
325 assert_eq!(
326 recovered.id(),
327 Identifier::new(override_id),
328 "explicit document_id should override the one in CBOR"
329 );
330 assert_eq!(
331 recovered.owner_id(),
332 Identifier::new(override_owner),
333 "explicit owner_id should override the one in CBOR"
334 );
335 }
336
337 #[test]
342 fn to_cbor_value_returns_map_for_document_with_properties() {
343 let doc = make_document_v0_with_timestamps();
344 let cbor_val = doc.to_cbor_value().expect("to_cbor_value should succeed");
345 assert!(
347 cbor_val.is_map(),
348 "CBOR value of a document should be a Map, got {:?}",
349 cbor_val
350 );
351 }
352
353 #[test]
358 fn to_cbor_starts_with_version_zero_varint() {
359 let doc = make_document_v0_with_timestamps();
360 let cbor_bytes = doc.to_cbor().expect("to_cbor should succeed");
361 assert!(!cbor_bytes.is_empty(), "CBOR output should not be empty");
363 assert_eq!(
364 cbor_bytes[0], 0,
365 "first byte should be varint(0) for version"
366 );
367 }
368
369 #[test]
374 fn from_cbor_rejects_invalid_cbor_bytes() {
375 let platform_version = PlatformVersion::latest();
376 let garbage = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01];
377 let result = DocumentV0::from_cbor(&garbage, None, None, platform_version);
378 assert!(
379 result.is_err(),
380 "from_cbor should fail on invalid CBOR bytes"
381 );
382 }
383
384 #[test]
389 fn document_for_cbor_preserves_all_fields() {
390 let doc = make_document_v0_with_timestamps();
391 let cbor_doc = DocumentForCbor::try_from(doc.clone()).expect("TryFrom should succeed");
392 assert_eq!(cbor_doc.id, doc.id.to_buffer());
393 assert_eq!(cbor_doc.owner_id, doc.owner_id.to_buffer());
394 assert_eq!(cbor_doc.revision, doc.revision);
395 assert_eq!(cbor_doc.created_at, doc.created_at);
396 assert_eq!(cbor_doc.updated_at, doc.updated_at);
397 assert_eq!(cbor_doc.transferred_at, doc.transferred_at);
398 assert_eq!(
399 cbor_doc.created_at_block_height,
400 doc.created_at_block_height
401 );
402 assert_eq!(
403 cbor_doc.updated_at_block_height,
404 doc.updated_at_block_height
405 );
406 assert_eq!(
407 cbor_doc.transferred_at_block_height,
408 doc.transferred_at_block_height
409 );
410 assert_eq!(
411 cbor_doc.created_at_core_block_height,
412 doc.created_at_core_block_height
413 );
414 assert_eq!(
415 cbor_doc.updated_at_core_block_height,
416 doc.updated_at_core_block_height
417 );
418 assert_eq!(
419 cbor_doc.transferred_at_core_block_height,
420 doc.transferred_at_core_block_height
421 );
422 }
423
424 #[test]
429 fn from_map_extracts_system_fields_and_leaves_properties() {
430 let id_bytes = [3u8; 32];
431 let owner_bytes = [4u8; 32];
432
433 let mut map = BTreeMap::new();
434 map.insert(property_names::ID.to_string(), Value::Bytes32(id_bytes));
435 map.insert(
436 property_names::OWNER_ID.to_string(),
437 Value::Bytes32(owner_bytes),
438 );
439 map.insert(property_names::REVISION.to_string(), Value::U64(5));
440 map.insert(
441 property_names::CREATED_AT.to_string(),
442 Value::U64(1_000_000),
443 );
444 map.insert(
445 property_names::UPDATED_AT.to_string(),
446 Value::U64(2_000_000),
447 );
448 map.insert("customField".to_string(), Value::Text("hello".to_string()));
449
450 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
451
452 assert_eq!(doc.id, Identifier::new(id_bytes));
453 assert_eq!(doc.owner_id, Identifier::new(owner_bytes));
454 assert_eq!(doc.revision, Some(5));
455 assert_eq!(doc.created_at, Some(1_000_000));
456 assert_eq!(doc.updated_at, Some(2_000_000));
457 assert_eq!(
459 doc.properties.get("customField"),
460 Some(&Value::Text("hello".to_string()))
461 );
462 assert!(!doc.properties.contains_key(property_names::ID));
464 assert!(!doc.properties.contains_key(property_names::OWNER_ID));
465 assert!(!doc.properties.contains_key(property_names::REVISION));
466 }
467
468 #[test]
469 fn from_map_with_explicit_ids_overrides_map_ids() {
470 let map_id = [10u8; 32];
471 let map_owner = [11u8; 32];
472 let override_id = [20u8; 32];
473 let override_owner = [21u8; 32];
474
475 let mut map = BTreeMap::new();
476 map.insert(property_names::ID.to_string(), Value::Bytes32(map_id));
477 map.insert(
478 property_names::OWNER_ID.to_string(),
479 Value::Bytes32(map_owner),
480 );
481
482 let doc = DocumentV0::from_map(map, Some(override_id), Some(override_owner))
483 .expect("from_map should succeed");
484
485 assert_eq!(
486 doc.id,
487 Identifier::new(override_id),
488 "explicit document_id should take precedence"
489 );
490 assert_eq!(
491 doc.owner_id,
492 Identifier::new(override_owner),
493 "explicit owner_id should take precedence"
494 );
495 }
496
497 #[test]
502 fn from_map_with_all_timestamp_variants() {
503 let mut map = BTreeMap::new();
504 map.insert(property_names::ID.to_string(), Value::Bytes32([5u8; 32]));
505 map.insert(
506 property_names::OWNER_ID.to_string(),
507 Value::Bytes32([6u8; 32]),
508 );
509 map.insert(
510 property_names::CREATED_AT_BLOCK_HEIGHT.to_string(),
511 Value::U64(100),
512 );
513 map.insert(
514 property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(),
515 Value::U64(200),
516 );
517 map.insert(
518 property_names::TRANSFERRED_AT.to_string(),
519 Value::U64(3_000_000),
520 );
521 map.insert(
522 property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(),
523 Value::U64(300),
524 );
525 map.insert(
526 property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(),
527 Value::U32(50),
528 );
529 map.insert(
530 property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(),
531 Value::U32(60),
532 );
533 map.insert(
534 property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(),
535 Value::U32(70),
536 );
537
538 let doc = DocumentV0::from_map(map, None, None).expect("from_map should succeed");
539
540 assert_eq!(doc.created_at_block_height, Some(100));
541 assert_eq!(doc.updated_at_block_height, Some(200));
542 assert_eq!(doc.transferred_at, Some(3_000_000));
543 assert_eq!(doc.transferred_at_block_height, Some(300));
544 assert_eq!(doc.created_at_core_block_height, Some(50));
545 assert_eq!(doc.updated_at_core_block_height, Some(60));
546 assert_eq!(doc.transferred_at_core_block_height, Some(70));
547 }
548}