1use crate::consensus::basic::document::InvalidDocumentTypeError;
2use crate::data_contract::accessors::v0::DataContractV0Getters;
3use crate::data_contract::document_type::accessors::DocumentTypeV0Getters;
4use crate::data_contract::document_type::DocumentTypeRef;
5use crate::data_contract::errors::DataContractError;
6use crate::data_contract::DataContract;
7use crate::document::errors::DocumentError;
8use crate::document::{Document, DocumentV0Getters, DocumentV0Setters, INITIAL_REVISION};
9use chrono::Utc;
10use std::collections::BTreeMap;
11
12use crate::util::entropy_generator::{DefaultEntropyGenerator, EntropyGenerator};
13use crate::version::PlatformVersion;
14use crate::ProtocolError;
15
16use platform_value::{Bytes32, Identifier, Value};
17
18use crate::data_contract::document_type::methods::DocumentTypeV0Methods;
19use crate::document::document_methods::DocumentMethodsV0;
20#[cfg(feature = "extended-document")]
21use crate::document::{
22 extended_document::v0::ExtendedDocumentV0,
23 ExtendedDocument, serialization_traits::DocumentPlatformConversionMethodsV0,
24};
25use crate::prelude::{BlockHeight, CoreBlockHeight, TimestampMillis};
26#[cfg(feature = "state-transitions")]
27use crate::state_transition::batch_transition::{
28 batched_transition::{
29 document_transition_action_type::DocumentTransitionActionType, DocumentCreateTransition,
30 DocumentDeleteTransition, DocumentReplaceTransition,
31 },
32 BatchTransition, BatchTransitionV0,
33};
34use itertools::Itertools;
35#[cfg(feature = "state-transitions")]
36use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition;
37use crate::tokens::token_payment_info::TokenPaymentInfo;
38
39pub struct DocumentFactoryV0 {
41 protocol_version: u32,
42 entropy_generator: Box<dyn EntropyGenerator>,
43}
44
45impl DocumentFactoryV0 {
46 pub fn new(protocol_version: u32) -> Self {
47 DocumentFactoryV0 {
48 protocol_version,
49 entropy_generator: Box::new(DefaultEntropyGenerator),
50 }
51 }
52
53 pub fn new_with_entropy_generator(
54 protocol_version: u32,
55 entropy_generator: Box<dyn EntropyGenerator>,
56 ) -> Self {
57 DocumentFactoryV0 {
58 protocol_version,
59 entropy_generator,
60 }
61 }
62
63 pub fn create_document(
64 &self,
65 data_contract: &DataContract,
66 owner_id: Identifier,
67 block_time: BlockHeight,
68 core_block_height: CoreBlockHeight,
69 document_type_name: String,
70 data: Value,
71 ) -> Result<Document, ProtocolError> {
72 let platform_version = PlatformVersion::get(self.protocol_version)?;
73 if !data_contract.has_document_type_for_name(&document_type_name) {
74 return Err(DataContractError::InvalidDocumentTypeError(
75 InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
76 )
77 .into());
78 }
79
80 let document_entropy = self.entropy_generator.generate()?;
81
82 let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
83
84 document_type.create_document_from_data(
85 data,
86 owner_id,
87 block_time,
88 core_block_height,
89 document_entropy,
90 platform_version,
91 )
92 }
93
94 pub fn create_document_without_time_based_properties(
95 &self,
96 data_contract: &DataContract,
97 owner_id: Identifier,
98 document_type_name: String,
99 data: Value,
100 ) -> Result<Document, ProtocolError> {
101 let platform_version = PlatformVersion::get(self.protocol_version)?;
102 if !data_contract.has_document_type_for_name(&document_type_name) {
103 return Err(DataContractError::InvalidDocumentTypeError(
104 InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
105 )
106 .into());
107 }
108
109 let document_entropy = self.entropy_generator.generate()?;
110
111 let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
112
113 document_type.create_document_from_data(
114 data,
115 owner_id,
116 0,
117 0,
118 document_entropy,
119 platform_version,
120 )
121 }
122
123 #[cfg(feature = "extended-document")]
124 pub fn create_extended_document(
125 &self,
126 data_contract: &DataContract,
127 owner_id: Identifier,
128 document_type_name: String,
129 data: Value,
130 ) -> Result<ExtendedDocument, ProtocolError> {
131 let platform_version = PlatformVersion::get(self.protocol_version)?;
132 if !data_contract.has_document_type_for_name(&document_type_name) {
133 return Err(DataContractError::InvalidDocumentTypeError(
134 InvalidDocumentTypeError::new(document_type_name, data_contract.id()),
135 )
136 .into());
137 }
138
139 let document_entropy = self.entropy_generator.generate()?;
140
141 let document_type = data_contract.document_type_for_name(document_type_name.as_str())?;
142
143 let document = document_type.create_document_from_data(
145 data,
146 owner_id,
147 0,
148 0,
149 document_entropy,
150 platform_version,
151 )?;
152
153 let extended_document = match platform_version
154 .dpp
155 .document_versions
156 .extended_document_structure_version
157 {
158 0 => Ok(ExtendedDocumentV0 {
159 document_type_name,
160 data_contract_id: data_contract.id(),
161 document,
162 data_contract: data_contract.clone(),
163 metadata: None,
164 entropy: Bytes32::new(document_entropy),
165 token_payment_info: None,
166 }
167 .into()),
168 version => Err(ProtocolError::UnknownVersionMismatch {
169 method: "DocumentFactory::create_extended_document".to_string(),
170 known_versions: vec![0],
171 received: version,
172 }),
173 }?;
174
175 Ok(extended_document)
176 }
177 #[cfg(feature = "state-transitions")]
178 pub fn create_state_transition<'a>(
179 &self,
180 documents_iter: impl IntoIterator<
181 Item = (
182 DocumentTransitionActionType,
183 Vec<(
184 Document,
185 DocumentTypeRef<'a>,
186 Bytes32,
187 Option<TokenPaymentInfo>,
188 )>,
189 ),
190 >,
191 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, ) -> Result<BatchTransition, ProtocolError> {
193 let platform_version = PlatformVersion::get(self.protocol_version)?;
194 #[allow(clippy::type_complexity)]
196 let documents: Vec<(
197 DocumentTransitionActionType,
198 Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
199 )> = documents_iter.into_iter().collect();
200 let mut flattened_documents_iter = documents.iter().flat_map(|(_, v)| v).peekable();
201
202 let Some((first_document, _, _, _)) = flattened_documents_iter.peek() else {
203 return Err(DocumentError::NoDocumentsSuppliedError.into());
204 };
205
206 let owner_id = first_document.owner_id();
207
208 let is_the_same_owner =
209 flattened_documents_iter.all(|(document, _, _, _)| document.owner_id() == owner_id);
210 if !is_the_same_owner {
211 return Err(DocumentError::MismatchOwnerIdsError {
212 documents: documents
213 .into_iter()
214 .flat_map(|(_, v)| {
215 v.into_iter()
216 .map(|(document, _, _, _)| document)
217 .collect::<Vec<_>>()
218 })
219 .collect(),
220 }
221 .into());
222 }
223
224 let transitions: Vec<_> = documents
225 .into_iter()
226 .map(|(action, documents)| match action {
227 DocumentTransitionActionType::Create => {
228 Self::document_create_transitions(documents, nonce_counter, platform_version)
229 }
230 DocumentTransitionActionType::Delete => Self::document_delete_transitions(
231 documents
232 .into_iter()
233 .map(|(document, document_type, _, token_payment_info)| {
234 (document, document_type, token_payment_info)
235 })
236 .collect(),
237 nonce_counter,
238 platform_version,
239 ),
240 DocumentTransitionActionType::Replace => Self::document_replace_transitions(
241 documents
242 .into_iter()
243 .map(|(document, document_type, _, token_payment_info)| {
244 (document, document_type, token_payment_info)
245 })
246 .collect(),
247 nonce_counter,
248 platform_version,
249 ),
250 _ => Err(ProtocolError::InvalidStateTransitionType(
251 "action type not accounted for".to_string(),
252 )),
253 })
254 .collect::<Result<Vec<_>, ProtocolError>>()?
255 .into_iter()
256 .flatten()
257 .collect();
258
259 if transitions.is_empty() {
260 return Err(DocumentError::NoDocumentsSuppliedError.into());
261 }
262
263 Ok(BatchTransitionV0 {
264 owner_id,
265 transitions,
266 user_fee_increase: 0,
267 signature_public_key_id: 0,
268 signature: Default::default(),
269 }
270 .into())
271 }
272
273 #[cfg(feature = "extended-document")]
274 pub fn create_extended_from_document_buffer(
275 &self,
276 buffer: &[u8],
277 document_type_name: &str,
278 data_contract: &DataContract,
279 platform_version: &PlatformVersion,
280 ) -> Result<ExtendedDocument, ProtocolError> {
281 let document_type = data_contract.document_type_for_name(document_type_name)?;
282
283 let document = Document::from_bytes(buffer, document_type, platform_version)?;
284
285 match platform_version
286 .dpp
287 .document_versions
288 .extended_document_structure_version
289 {
290 0 => Ok(ExtendedDocumentV0 {
291 document_type_name: document_type_name.to_string(),
292 data_contract_id: data_contract.id(),
293 document,
294 data_contract: data_contract.clone(),
295 metadata: None,
296 entropy: Bytes32::default(),
297 token_payment_info: None,
298 }
299 .into()),
300 version => Err(ProtocolError::UnknownVersionMismatch {
301 method: "DocumentFactory::create_extended_from_document_buffer".to_string(),
302 known_versions: vec![0],
303 received: version,
304 }),
305 }
306 }
307 #[cfg(feature = "state-transitions")]
368 fn document_create_transitions(
369 documents: Vec<(Document, DocumentTypeRef, Bytes32, Option<TokenPaymentInfo>)>,
370 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
372 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
373 documents
374 .into_iter()
375 .map(|(document, document_type, entropy, token_payment_info)| {
376 if document_type.documents_mutable() {
377 let Some(revision) = document.revision() else {
379 return Err(DocumentError::RevisionAbsentError {
380 document: Box::new(document),
381 }
382 .into());
383 };
384 if revision != INITIAL_REVISION {
385 return Err(DocumentError::InvalidInitialRevisionError {
386 document: Box::new(document),
387 }
388 .into());
389 }
390 }
391 let nonce = nonce_counter
392 .entry((document.owner_id(), document_type.data_contract_id()))
393 .or_default();
394
395 let transition = DocumentCreateTransition::from_document(
396 document,
397 document_type,
398 entropy.to_buffer(),
399 token_payment_info,
400 *nonce,
401 platform_version,
402 None,
403 None,
404 )?;
405
406 *nonce += 1;
407
408 Ok(transition.into())
409 })
410 .collect()
411 }
412
413 #[cfg(feature = "state-transitions")]
414 fn document_replace_transitions(
415 documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
416 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
418 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
419 documents
420 .into_iter()
421 .map(|(mut document, document_type, token_payment_info)| {
422 if !document_type.documents_mutable() {
423 return Err(DocumentError::TryingToReplaceImmutableDocument {
424 document: Box::new(document),
425 }
426 .into());
427 }
428 if document.revision().is_none() {
429 return Err(DocumentError::RevisionAbsentError {
430 document: Box::new(document),
431 }
432 .into());
433 };
434
435 document.increment_revision()?;
436 document.set_updated_at(Some(Utc::now().timestamp_millis() as TimestampMillis));
437
438 let nonce = nonce_counter
439 .entry((document.owner_id(), document_type.data_contract_id()))
440 .or_default();
441
442 let transition = DocumentReplaceTransition::from_document(
443 document,
444 document_type,
445 token_payment_info,
446 *nonce,
447 platform_version,
448 None,
449 None,
450 )?;
451
452 *nonce += 1;
453
454 Ok(transition.into())
455 })
456 .collect()
457 }
497
498 #[cfg(feature = "state-transitions")]
499 fn document_delete_transitions(
500 documents: Vec<(Document, DocumentTypeRef, Option<TokenPaymentInfo>)>,
501 nonce_counter: &mut BTreeMap<(Identifier, Identifier), u64>, platform_version: &PlatformVersion,
503 ) -> Result<Vec<DocumentTransition>, ProtocolError> {
504 documents
505 .into_iter()
506 .map(|(document, document_type, token_payment_info)| {
507 if !document_type.documents_can_be_deleted() {
508 return Err(DocumentError::TryingToDeleteIndelibleDocument {
509 document: Box::new(document),
510 }
511 .into());
512 }
513 let Some(_document_revision) = document.revision() else {
514 return Err(DocumentError::RevisionAbsentError {
515 document: Box::new(document),
516 }
517 .into());
518 };
519
520 let nonce = nonce_counter
521 .entry((document.owner_id(), document_type.data_contract_id()))
522 .or_default();
523 let transition = DocumentDeleteTransition::from_document(
524 document,
525 document_type,
526 token_payment_info,
527 *nonce,
528 platform_version,
529 None,
530 None,
531 )?;
532
533 *nonce += 1;
534
535 Ok(transition.into())
536 })
537 .collect()
538 }
539
540 fn is_ownership_the_same<'a>(ids: impl IntoIterator<Item = &'a Identifier>) -> bool {
541 ids.into_iter().all_equal()
542 }
543}
544
545#[cfg(test)]
546mod test {
547 use data_contracts::SystemDataContract;
548 use platform_version::version::PlatformVersion;
549 use std::collections::BTreeMap;
550
551 use crate::data_contract::accessors::v0::DataContractV0Getters;
552 use crate::document::document_factory::DocumentFactoryV0;
553 use crate::document::{Document, DocumentV0};
554 use crate::identifier::Identifier;
555 use crate::system_data_contracts::load_system_data_contract;
556
557 #[test]
558 fn delete_immutable_but_deletable_documents() {
560 let dpns_contract =
562 load_system_data_contract(SystemDataContract::DPNS, PlatformVersion::latest()).unwrap();
563 let document_type = dpns_contract
564 .document_type_borrowed_for_name("preorder")
565 .unwrap();
566 let document_type_ref = document_type.as_ref();
567
568 let document_id = Identifier::random();
570 let owner_id = Identifier::random();
571 let mut properties = BTreeMap::new();
572 properties.insert(
573 "saltedDomainHash".to_string(),
574 platform_value::Value::Array(vec![]),
575 );
576 let document_v0 = DocumentV0 {
577 id: document_id,
578 owner_id,
579 properties,
580 revision: Some(1),
581 created_at: None,
582 updated_at: None,
583 transferred_at: None,
584 created_at_block_height: None,
585 updated_at_block_height: None,
586 transferred_at_block_height: None,
587 created_at_core_block_height: None,
588 updated_at_core_block_height: None,
589 transferred_at_core_block_height: None,
590 creator_id: None,
591 };
592 let document = Document::V0(document_v0);
593
594 let documents = vec![(document, document_type_ref, None)];
596 let mut nonce_counter = BTreeMap::new();
597 let platform_version = PlatformVersion::latest();
598
599 let result = DocumentFactoryV0::document_delete_transitions(
601 documents,
602 &mut nonce_counter,
603 platform_version,
604 );
605
606 assert!(result.is_ok(), "The function should succeed");
608 let transitions = result.unwrap();
609 assert_eq!(transitions.len(), 1, "There should be one transition");
610 }
611}