1mod v0;
2
3use crate::data_contract::DataContract;
4use std::collections::BTreeMap;
5
6use crate::version::PlatformVersion;
7use crate::ProtocolError;
8use derive_more::From;
9use platform_value::{Bytes32, Identifier, Value};
10
11use crate::data_contract::document_type::DocumentTypeRef;
12use crate::document::Document;
13#[cfg(feature = "extended-document")]
14use crate::document::ExtendedDocument;
15#[cfg(feature = "state-transitions")]
16use crate::state_transition::batch_transition::{
17 batched_transition::document_transition_action_type::DocumentTransitionActionType,
18 BatchTransition,
19};
20use crate::tokens::token_payment_info::TokenPaymentInfo;
21use crate::util::entropy_generator::EntropyGenerator;
22pub use v0::DocumentFactoryV0;
23
24#[derive(From)]
37pub enum DocumentFactory {
38 V0(DocumentFactoryV0),
40}
41
42impl DocumentFactory {
43 pub fn new(protocol_version: u32) -> Result<Self, ProtocolError> {
45 let platform_version = PlatformVersion::get(protocol_version)?;
46 match platform_version
47 .dpp
48 .factory_versions
49 .document_factory_structure_version
50 {
51 0 => Ok(DocumentFactoryV0::new(protocol_version).into()),
52 version => Err(ProtocolError::UnknownVersionMismatch {
53 method: "DocumentFactory::new".to_string(),
54 known_versions: vec![0],
55 received: version,
56 }),
57 }
58 }
59
60 pub fn new_with_entropy_generator(
61 protocol_version: u32,
62 entropy_generator: Box<dyn EntropyGenerator>,
63 ) -> Result<Self, ProtocolError> {
64 let platform_version = PlatformVersion::get(protocol_version)?;
65 match platform_version
66 .dpp
67 .factory_versions
68 .document_factory_structure_version
69 {
70 0 => Ok(DocumentFactoryV0::new_with_entropy_generator(
71 protocol_version,
72 entropy_generator,
73 )
74 .into()),
75 version => Err(ProtocolError::UnknownVersionMismatch {
76 method: "DocumentFactory::new_with_entropy_generator".to_string(),
77 known_versions: vec![0],
78 received: version,
79 }),
80 }
81 }
82
83 pub fn create_document(
84 &self,
85 data_contract: &DataContract,
86 owner_id: Identifier,
87 document_type_name: String,
88 data: Value,
89 ) -> Result<Document, ProtocolError> {
90 match self {
91 DocumentFactory::V0(v0) => v0.create_document_without_time_based_properties(
92 data_contract,
93 owner_id,
94 document_type_name,
95 data,
96 ),
97 }
98 }
99
100 #[cfg(feature = "extended-document")]
101 pub fn create_extended_document(
102 &self,
103 data_contract: &DataContract,
104 owner_id: Identifier,
105 document_type_name: String,
106 data: Value,
107 ) -> Result<ExtendedDocument, ProtocolError> {
108 match self {
109 DocumentFactory::V0(v0) => {
110 v0.create_extended_document(data_contract, owner_id, document_type_name, data)
111 }
112 }
113 }
114
115 #[cfg(feature = "state-transitions")]
116 pub fn create_state_transition<'a>(
117 &self,
118 documents_iter: impl IntoIterator<
119 Item = (
120 DocumentTransitionActionType,
121 Vec<(
122 Document,
123 DocumentTypeRef<'a>,
124 Bytes32,
125 Option<TokenPaymentInfo>,
126 )>,
127 ),
128 >,
129 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, ) -> Result<BatchTransition, ProtocolError> {
131 match self {
132 DocumentFactory::V0(v0) => v0.create_state_transition(documents_iter, nonce_counter),
133 }
134 }
135
136 #[cfg(feature = "extended-document")]
137 pub fn create_extended_from_document_buffer(
138 &self,
139 buffer: &[u8],
140 document_type_name: &str,
141 data_contract: &DataContract,
142 platform_version: &PlatformVersion,
143 ) -> Result<ExtendedDocument, ProtocolError> {
144 match self {
145 DocumentFactory::V0(v0) => v0.create_extended_from_document_buffer(
146 buffer,
147 document_type_name,
148 data_contract,
149 platform_version,
150 ),
151 }
152 }
153}
154
155#[cfg(test)]
156#[allow(clippy::type_complexity)]
157mod tests {
158 use super::*;
159 use crate::data_contract::accessors::v0::DataContractV0Getters;
160 use crate::document::DocumentV0Getters;
161 use crate::tests::fixtures::get_data_contract_fixture;
162 use crate::util::entropy_generator::EntropyGenerator;
163 use platform_value::platform_value;
164 use platform_version::version::PlatformVersion;
165
166 struct TestEntropyGenerator;
168
169 impl EntropyGenerator for TestEntropyGenerator {
170 fn generate(&self) -> anyhow::Result<[u8; 32]> {
171 Ok([7u8; 32])
172 }
173 }
174
175 struct FailingEntropyGenerator;
178
179 impl EntropyGenerator for FailingEntropyGenerator {
180 fn generate(&self) -> anyhow::Result<[u8; 32]> {
181 Err(anyhow::anyhow!("synthetic entropy failure"))
182 }
183 }
184
185 fn setup_factory() -> (DocumentFactory, DataContract) {
186 let platform_version = PlatformVersion::latest();
187 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
188 let data_contract = created.data_contract_owned();
189 let factory = DocumentFactory::new_with_entropy_generator(
190 platform_version.protocol_version,
191 Box::new(TestEntropyGenerator),
192 )
193 .expect("factory construction should succeed");
194 (factory, data_contract)
195 }
196
197 #[test]
200 fn new_with_bad_protocol_version_returns_error() {
201 let result = DocumentFactory::new(u32::MAX);
203 assert!(
204 matches!(result, Err(ProtocolError::PlatformVersionError(_))),
205 "expected PlatformVersionError, got {:?}",
206 result.err()
207 );
208 }
209
210 #[test]
211 fn new_with_zero_protocol_version_returns_error() {
212 let result = DocumentFactory::new(0);
214 assert!(result.is_err(), "expected error for version 0");
215 }
216
217 #[test]
218 fn new_with_entropy_generator_bad_version_returns_error() {
219 let result =
220 DocumentFactory::new_with_entropy_generator(u32::MAX, Box::new(TestEntropyGenerator));
221 assert!(result.is_err());
222 }
223
224 #[test]
225 fn new_with_entropy_generator_valid_version_succeeds() {
226 let platform_version = PlatformVersion::latest();
227 let result = DocumentFactory::new_with_entropy_generator(
228 platform_version.protocol_version,
229 Box::new(TestEntropyGenerator),
230 );
231 assert!(result.is_ok());
232 assert!(matches!(result.unwrap(), DocumentFactory::V0(_)));
233 }
234
235 #[test]
236 fn new_variant_is_v0() {
237 let platform_version = PlatformVersion::latest();
238 let factory = DocumentFactory::new(platform_version.protocol_version).unwrap();
239 match factory {
240 DocumentFactory::V0(_) => {}
241 }
242 }
243
244 #[test]
247 fn create_document_with_invalid_type_returns_error() {
248 let (factory, data_contract) = setup_factory();
249 let owner_id = Identifier::from([0xAAu8; 32]);
250
251 let result = factory.create_document(
252 &data_contract,
253 owner_id,
254 "nonExistentDocType".to_string(),
255 Value::Null,
256 );
257
258 assert!(result.is_err(), "expected error for unknown type");
260 }
261
262 #[test]
263 fn create_document_with_failing_entropy_returns_error() {
264 let platform_version = PlatformVersion::latest();
266 let created = get_data_contract_fixture(None, 0, platform_version.protocol_version);
267 let data_contract = created.data_contract_owned();
268 let factory = DocumentFactory::new_with_entropy_generator(
269 platform_version.protocol_version,
270 Box::new(FailingEntropyGenerator),
271 )
272 .unwrap();
273
274 let owner_id = Identifier::from([0x11u8; 32]);
275 let result = factory.create_document(
276 &data_contract,
277 owner_id,
278 "noTimeDocument".to_string(),
279 platform_value!({ "name": "x" }),
280 );
281 assert!(result.is_err(), "failing entropy generator should surface");
282 }
283
284 #[test]
285 fn create_document_happy_path_has_zero_time_based_props() {
286 let (factory, data_contract) = setup_factory();
287 let owner_id = Identifier::from([0xBBu8; 32]);
288
289 let doc = factory
290 .create_document(
291 &data_contract,
292 owner_id,
293 "noTimeDocument".to_string(),
294 platform_value!({ "name": "widget" }),
295 )
296 .expect("document should be created");
297
298 assert_eq!(doc.owner_id(), owner_id);
301 assert_eq!(doc.created_at(), None);
302 assert_eq!(doc.updated_at(), None);
303 }
304
305 #[cfg(feature = "extended-document")]
308 #[test]
309 fn create_extended_document_with_invalid_type_returns_error() {
310 let (factory, data_contract) = setup_factory();
311 let owner_id = Identifier::from([0xCCu8; 32]);
312
313 let result = factory.create_extended_document(
314 &data_contract,
315 owner_id,
316 "bogusTypeName".to_string(),
317 Value::Null,
318 );
319 assert!(result.is_err());
320 }
321
322 #[cfg(feature = "extended-document")]
323 #[test]
324 fn create_extended_document_happy_path() {
325 let (factory, data_contract) = setup_factory();
326 let owner_id = Identifier::from([0xDDu8; 32]);
327
328 let result = factory.create_extended_document(
329 &data_contract,
330 owner_id,
331 "noTimeDocument".to_string(),
332 platform_value!({ "name": "z" }),
333 );
334 assert!(result.is_ok(), "extended document creation should succeed");
335 let ext = result.unwrap();
336 assert_eq!(ext.data_contract_id(), data_contract.id());
337 assert_eq!(ext.document_type_name(), "noTimeDocument");
338 assert_eq!(ext.entropy().to_buffer(), [7u8; 32]);
340 }
341
342 #[cfg(feature = "extended-document")]
345 #[test]
346 fn create_extended_from_document_buffer_roundtrips() {
347 use crate::document::serialization_traits::DocumentPlatformConversionMethodsV0;
348 let (factory, data_contract) = setup_factory();
349 let owner_id = Identifier::from([0x55u8; 32]);
350 let platform_version = PlatformVersion::latest();
351
352 let doc = factory
353 .create_document(
354 &data_contract,
355 owner_id,
356 "noTimeDocument".to_string(),
357 platform_value!({ "name": "abc" }),
358 )
359 .expect("doc should be created");
360
361 let doc_type = data_contract
362 .document_type_for_name("noTimeDocument")
363 .unwrap();
364 let bytes = doc
365 .serialize(doc_type, &data_contract, platform_version)
366 .expect("serialize");
367
368 let ext = factory
369 .create_extended_from_document_buffer(
370 bytes.as_slice(),
371 "noTimeDocument",
372 &data_contract,
373 platform_version,
374 )
375 .expect("extended doc should be parsed");
376
377 assert_eq!(ext.data_contract_id(), data_contract.id());
378 assert_eq!(ext.document_type_name(), "noTimeDocument");
379 assert_eq!(ext.entropy(), &Bytes32::default());
381 }
382
383 #[cfg(feature = "extended-document")]
384 #[test]
385 fn create_extended_from_document_buffer_invalid_type_fails() {
386 let (factory, data_contract) = setup_factory();
387 let platform_version = PlatformVersion::latest();
388
389 let result = factory.create_extended_from_document_buffer(
390 &[0u8; 16],
391 "thisTypeDoesNotExist",
392 &data_contract,
393 platform_version,
394 );
395 assert!(result.is_err(), "unknown doc type should surface error");
396 }
397
398 #[cfg(feature = "extended-document")]
399 #[test]
400 fn create_extended_from_document_buffer_malformed_bytes_fails() {
401 let (factory, data_contract) = setup_factory();
402 let platform_version = PlatformVersion::latest();
403
404 let result = factory.create_extended_from_document_buffer(
406 &[0xFFu8; 6],
407 "noTimeDocument",
408 &data_contract,
409 platform_version,
410 );
411 assert!(result.is_err(), "malformed buffer should fail to decode");
412 }
413
414 #[cfg(feature = "state-transitions")]
417 mod state_transition_tests {
418 use super::*;
419 use crate::document::errors::DocumentError;
420 use crate::document::{DocumentV0Setters, INITIAL_REVISION};
421 use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0;
422 use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition_action_type::DocumentTransitionActionType;
423 use crate::state_transition::StateTransitionOwned;
424
425 fn build_doc(
426 factory: &DocumentFactory,
427 data_contract: &DataContract,
428 owner: Identifier,
429 type_name: &str,
430 ) -> Document {
431 factory
432 .create_document(
433 data_contract,
434 owner,
435 type_name.to_string(),
436 platform_value!({ "name": "x" }),
437 )
438 .expect("doc should build")
439 }
440
441 #[test]
442 fn create_state_transition_empty_iter_returns_error() {
443 let (factory, _) = setup_factory();
444 let mut nonce_counter: BTreeMap<(Identifier, Identifier), u64> = BTreeMap::new();
445 let empty: Vec<(
446 DocumentTransitionActionType,
447 Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
448 )> = vec![];
449
450 let result = factory.create_state_transition(empty, &mut nonce_counter);
451 assert!(
452 matches!(
453 result,
454 Err(ProtocolError::Document(e)) if matches!(*e, DocumentError::NoDocumentsSuppliedError)
455 ),
456 "expected NoDocumentsSuppliedError"
457 );
458 }
459
460 #[test]
461 fn create_state_transition_outer_iter_has_empty_inner_returns_error() {
462 let (factory, _) = setup_factory();
464 let mut nonce_counter = BTreeMap::new();
465 let entries = vec![(DocumentTransitionActionType::Create, vec![])];
466 let result = factory.create_state_transition(entries, &mut nonce_counter);
467 assert!(
468 matches!(
469 result,
470 Err(ProtocolError::Document(e)) if matches!(*e, DocumentError::NoDocumentsSuppliedError)
471 ),
472 "expected NoDocumentsSuppliedError"
473 );
474 }
475
476 #[test]
477 fn create_state_transition_mismatched_owner_returns_error() {
478 let (factory, data_contract) = setup_factory();
479 let doc_type = data_contract
480 .document_type_for_name("noTimeDocument")
481 .unwrap();
482 let owner_a = Identifier::from([0x01u8; 32]);
483 let owner_b = Identifier::from([0x02u8; 32]);
484 let doc_a = build_doc(&factory, &data_contract, owner_a, "noTimeDocument");
485 let doc_b = build_doc(&factory, &data_contract, owner_b, "noTimeDocument");
486
487 let mut nonce_counter = BTreeMap::new();
488 let entries = vec![(
489 DocumentTransitionActionType::Create,
490 vec![
491 (doc_a, doc_type, Bytes32::new([1u8; 32]), None),
492 (doc_b, doc_type, Bytes32::new([2u8; 32]), None),
493 ],
494 )];
495 let result = factory.create_state_transition(entries, &mut nonce_counter);
496 assert!(
497 matches!(
498 result,
499 Err(ProtocolError::Document(e))
500 if matches!(*e, DocumentError::MismatchOwnerIdsError { .. })
501 ),
502 "expected MismatchOwnerIdsError"
503 );
504 }
505
506 #[test]
507 fn create_state_transition_create_wrong_initial_revision_errors() {
508 let (factory, data_contract) = setup_factory();
509 let doc_type = data_contract
510 .document_type_for_name("noTimeDocument")
511 .unwrap();
512 let owner = Identifier::from([0x05u8; 32]);
513 let mut doc = build_doc(&factory, &data_contract, owner, "noTimeDocument");
514 doc.set_revision(Some(9999));
515
516 let mut nonce_counter = BTreeMap::new();
517 let entries = vec![(
518 DocumentTransitionActionType::Create,
519 vec![(doc, doc_type, Bytes32::default(), None)],
520 )];
521 let result = factory.create_state_transition(entries, &mut nonce_counter);
522 assert!(
523 matches!(
524 result,
525 Err(ProtocolError::Document(e))
526 if matches!(*e, DocumentError::InvalidInitialRevisionError { .. })
527 ),
528 "expected InvalidInitialRevisionError"
529 );
530 }
531
532 #[test]
533 fn create_state_transition_create_missing_revision_on_mutable_errors() {
534 let (factory, data_contract) = setup_factory();
535 let doc_type = data_contract
536 .document_type_for_name("noTimeDocument")
537 .unwrap();
538 let owner = Identifier::from([0x06u8; 32]);
539 let mut doc = build_doc(&factory, &data_contract, owner, "noTimeDocument");
540 doc.set_revision(None);
541
542 let mut nonce_counter = BTreeMap::new();
543 let entries = vec![(
544 DocumentTransitionActionType::Create,
545 vec![(doc, doc_type, Bytes32::default(), None)],
546 )];
547 let result = factory.create_state_transition(entries, &mut nonce_counter);
548 assert!(
549 matches!(
550 result,
551 Err(ProtocolError::Document(e))
552 if matches!(*e, DocumentError::RevisionAbsentError { .. })
553 ),
554 "expected RevisionAbsentError"
555 );
556 }
557
558 #[test]
559 fn create_state_transition_replace_missing_revision_errors() {
560 let (factory, data_contract) = setup_factory();
561 let doc_type = data_contract
562 .document_type_for_name("noTimeDocument")
563 .unwrap();
564 let owner = Identifier::from([0x07u8; 32]);
565 let mut doc = build_doc(&factory, &data_contract, owner, "noTimeDocument");
566 doc.set_revision(None);
567
568 let mut nonce_counter = BTreeMap::new();
569 let entries = vec![(
570 DocumentTransitionActionType::Replace,
571 vec![(doc, doc_type, Bytes32::default(), None)],
572 )];
573 let result = factory.create_state_transition(entries, &mut nonce_counter);
574 assert!(
575 matches!(
576 result,
577 Err(ProtocolError::Document(e))
578 if matches!(*e, DocumentError::RevisionAbsentError { .. })
579 ),
580 "expected RevisionAbsentError for replace"
581 );
582 }
583
584 #[test]
585 fn create_state_transition_delete_missing_revision_errors() {
586 let (factory, data_contract) = setup_factory();
587 let doc_type = data_contract
588 .document_type_for_name("noTimeDocument")
589 .unwrap();
590 let owner = Identifier::from([0x08u8; 32]);
591 let mut doc = build_doc(&factory, &data_contract, owner, "noTimeDocument");
592 doc.set_revision(None);
593
594 let mut nonce_counter = BTreeMap::new();
595 let entries = vec![(
596 DocumentTransitionActionType::Delete,
597 vec![(doc, doc_type, Bytes32::default(), None)],
598 )];
599 let result = factory.create_state_transition(entries, &mut nonce_counter);
600 assert!(
601 matches!(
602 result,
603 Err(ProtocolError::Document(e))
604 if matches!(*e, DocumentError::RevisionAbsentError { .. })
605 ),
606 "expected RevisionAbsentError for delete"
607 );
608 }
609
610 #[test]
611 fn create_state_transition_create_increments_nonce() {
612 let (factory, data_contract) = setup_factory();
613 let doc_type = data_contract
614 .document_type_for_name("noTimeDocument")
615 .unwrap();
616 let owner = Identifier::from([0x20u8; 32]);
617 let doc = build_doc(&factory, &data_contract, owner, "noTimeDocument");
618
619 let mut nonce_counter = BTreeMap::new();
620 nonce_counter.insert((owner, data_contract.id()), 7);
621
622 let entries = vec![(
623 DocumentTransitionActionType::Create,
624 vec![(doc, doc_type, Bytes32::default(), None)],
625 )];
626 let batch = factory
627 .create_state_transition(entries, &mut nonce_counter)
628 .expect("should build");
629 assert_eq!(batch.owner_id(), owner);
630 assert_eq!(batch.transitions_len(), 1);
631 assert_eq!(*nonce_counter.get(&(owner, data_contract.id())).unwrap(), 8);
633 }
634
635 #[test]
636 fn create_state_transition_mix_actions_combines_transitions() {
637 let (factory, data_contract) = setup_factory();
638 let doc_type = data_contract
639 .document_type_for_name("noTimeDocument")
640 .unwrap();
641 let owner = Identifier::from([0x30u8; 32]);
642
643 let mut c1 = build_doc(&factory, &data_contract, owner, "noTimeDocument");
645 c1.set_id(Identifier::from([0xAAu8; 32]));
646 let mut c2 = build_doc(&factory, &data_contract, owner, "noTimeDocument");
647 c2.set_id(Identifier::from([0xBBu8; 32]));
648
649 let mut r1 = build_doc(&factory, &data_contract, owner, "noTimeDocument");
651 r1.set_id(Identifier::from([0xCCu8; 32]));
652 assert_eq!(r1.revision(), Some(INITIAL_REVISION));
653
654 let mut nonce_counter = BTreeMap::new();
655 let entries = vec![
656 (
657 DocumentTransitionActionType::Create,
658 vec![
659 (c1, doc_type, Bytes32::new([0x01; 32]), None),
660 (c2, doc_type, Bytes32::new([0x02; 32]), None),
661 ],
662 ),
663 (
664 DocumentTransitionActionType::Replace,
665 vec![(r1, doc_type, Bytes32::default(), None)],
666 ),
667 ];
668 let batch = factory
669 .create_state_transition(entries, &mut nonce_counter)
670 .expect("mixed batch should build");
671 assert_eq!(batch.transitions_len(), 3);
672 assert_eq!(*nonce_counter.get(&(owner, data_contract.id())).unwrap(), 3);
674 }
675 }
676}
677
678