Skip to main content

dpp/data_contract/config/
mod.rs

1mod fields;
2mod methods;
3pub mod moderation;
4pub mod v0;
5pub mod v1;
6pub mod v2;
7
8use crate::data_contract::config::moderation::ContractModerationConfig;
9use crate::data_contract::config::v1::{
10    DataContractConfigGettersV1, DataContractConfigSettersV1, DataContractConfigV1,
11};
12use crate::data_contract::config::v2::{DataContractConfigGettersV2, DataContractConfigV2};
13use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements;
14#[cfg(feature = "json-conversion")]
15use crate::serialization::JsonConvertible;
16#[cfg(feature = "value-conversion")]
17use crate::serialization::ValueConvertible;
18use crate::version::PlatformVersion;
19use crate::ProtocolError;
20use bincode::{Decode, DecodeUntrusted, Encode};
21use derive_more::From;
22pub use fields::*;
23use platform_value::Value;
24use serde::{Deserialize, Serialize};
25use std::collections::BTreeMap;
26use v0::{DataContractConfigGettersV0, DataContractConfigSettersV0, DataContractConfigV0};
27
28#[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
29#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))]
30#[derive(
31    Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq, Eq, From, DecodeUntrusted,
32)]
33#[serde(tag = "$formatVersion")]
34pub enum DataContractConfig {
35    #[serde(rename = "0")]
36    V0(DataContractConfigV0),
37    #[serde(rename = "1")]
38    V1(DataContractConfigV1),
39    /// Protocol version 14: V1 plus the optional contract moderation declaration.
40    #[serde(rename = "2")]
41    V2(DataContractConfigV2),
42}
43
44impl DataContractConfig {
45    pub fn version(&self) -> u16 {
46        match self {
47            DataContractConfig::V0(_) => 0,
48            DataContractConfig::V1(_) => 1,
49            DataContractConfig::V2(_) => 2,
50        }
51    }
52
53    pub fn default_for_version(
54        platform_version: &PlatformVersion,
55    ) -> Result<DataContractConfig, ProtocolError> {
56        match platform_version
57            .dpp
58            .contract_versions
59            .config
60            .default_current_version
61        {
62            0 => Ok(DataContractConfigV0::default().into()),
63            1 => Ok(DataContractConfigV1::default().into()),
64            2 => Ok(DataContractConfigV2::default().into()),
65            version => Err(ProtocolError::UnknownVersionMismatch {
66                method: "DataContractConfig::default_for_version".to_string(),
67                known_versions: vec![0, 1, 2],
68                received: version,
69            }),
70        }
71    }
72
73    /// The same config declaring `moderation`, raised to V2 when it is lower. A V0 config keeps
74    /// V0's defaults for the V1 fields.
75    pub fn with_moderation(self, moderation: Option<ContractModerationConfig>) -> Self {
76        let mut v2: DataContractConfigV2 = match self {
77            DataContractConfig::V0(v0) => DataContractConfigV1 {
78                can_be_deleted: v0.can_be_deleted,
79                readonly: v0.readonly,
80                keeps_history: v0.keeps_history,
81                documents_keep_history_contract_default: v0.documents_keep_history_contract_default,
82                documents_mutable_contract_default: v0.documents_mutable_contract_default,
83                documents_can_be_deleted_contract_default: v0
84                    .documents_can_be_deleted_contract_default,
85                requires_identity_encryption_bounded_key: v0
86                    .requires_identity_encryption_bounded_key,
87                requires_identity_decryption_bounded_key: v0
88                    .requires_identity_decryption_bounded_key,
89                sized_integer_types: false,
90            }
91            .into(),
92            DataContractConfig::V1(v1) => v1.into(),
93            DataContractConfig::V2(v2) => v2,
94        };
95        v2.moderation = moderation;
96        DataContractConfig::V2(v2)
97    }
98
99    /// Refuses a config that declares moderation where the platform version cannot carry the
100    /// declaration (before protocol version 14). Lowering it would quietly store an unmoderated
101    /// contract, and moderation can never be turned on by an update.
102    pub fn ensure_admitted_by_platform_version(
103        &self,
104        platform_version: &PlatformVersion,
105    ) -> Result<(), ProtocolError> {
106        if self.moderation().is_some()
107            && platform_version.dpp.contract_versions.config.max_version < 2
108        {
109            return Err(ProtocolError::NotSupported(format!(
110                "contract moderation is not supported at protocol version {}",
111                platform_version.protocol_version
112            )));
113        }
114        Ok(())
115    }
116
117    /// Adjusts the current `DataContractConfig` to be valid for the provided platform version.
118    ///
119    /// The config version follows the platform version, never the config's content: a config
120    /// is lowered only where the platform version does not admit it (V1 to V0 below protocol
121    /// version 9, V2 to V1 below protocol version 14). From protocol version 14 every new
122    /// contract carries a V2 config, moderated or not. Lowering a V2 would drop a moderation
123    /// declaration, so a contract is only serialized once
124    /// [`ensure_admitted_by_platform_version`](Self::ensure_admitted_by_platform_version) has
125    /// refused that case. Consensus never sees it either way: a contract create or update
126    /// carrying a V2 config is inactive before protocol version 14
127    /// (`StateTransition::active_version_range`).
128    pub fn config_valid_for_platform_version(
129        self,
130        platform_version: &PlatformVersion,
131    ) -> DataContractConfig {
132        let max_version = platform_version.dpp.contract_versions.config.max_version;
133        match self {
134            DataContractConfig::V0(v0) => DataContractConfig::V0(v0),
135            DataContractConfig::V1(v1) => {
136                if max_version == 0 {
137                    DataContractConfig::V0(v1.into())
138                } else {
139                    DataContractConfig::V1(v1)
140                }
141            }
142            DataContractConfig::V2(v2) => {
143                if max_version < 2 {
144                    DataContractConfig::V1(v2.into())
145                        .config_valid_for_platform_version(platform_version)
146                } else {
147                    DataContractConfig::V2(v2)
148                }
149            }
150        }
151    }
152
153    /// A config version below 2 has no `moderation` key and would parse a value around one,
154    /// dropping it. Refused instead, like lowering a moderated config.
155    fn refuse_moderation_the_version_can_not_parse(
156        declares_moderation: bool,
157        platform_version: &PlatformVersion,
158    ) -> Result<(), ProtocolError> {
159        if declares_moderation
160            && platform_version
161                .dpp
162                .contract_versions
163                .config
164                .default_current_version
165                < 2
166        {
167            return Err(ProtocolError::NotSupported(format!(
168                "contract moderation is not supported at protocol version {}",
169                platform_version.protocol_version
170            )));
171        }
172        Ok(())
173    }
174
175    /// **KEEP-AS-EXCEPTION** in the JSON/Value canonical-trait migration —
176    /// this is a context-aware constructor, not a parallel conversion path:
177    /// it dispatches the config variant on `platform_version` (the input map
178    /// carries no `$formatVersion` tag in the contract-creation flow), so
179    /// canonical `ValueConvertible::from_object` cannot replace it.
180    pub fn from_value(
181        value: Value,
182        platform_version: &PlatformVersion,
183    ) -> Result<DataContractConfig, ProtocolError> {
184        if let Value::Map(map) = &value {
185            let declares_moderation = map.iter().any(|(key, value)| {
186                key.as_text() == Some(property::MODERATION) && !value.is_null()
187            });
188            Self::refuse_moderation_the_version_can_not_parse(
189                declares_moderation,
190                platform_version,
191            )?;
192        }
193        match platform_version
194            .dpp
195            .contract_versions
196            .config
197            .default_current_version
198        {
199            0 => {
200                let config: DataContractConfigV0 = platform_value::from_value(value)?;
201                Ok(config.into())
202            }
203            1 => {
204                let config: DataContractConfigV1 = platform_value::from_value(value)?;
205                Ok(config.into())
206            }
207            2 => {
208                let config: DataContractConfigV2 = platform_value::from_value(value)?;
209                Ok(config.into())
210            }
211            version => Err(ProtocolError::UnknownVersionMismatch {
212                method: "DataContractConfig::from_value".to_string(),
213                known_versions: vec![0, 1, 2],
214                received: version,
215            }),
216        }
217    }
218
219    // TODO: Remove, it's not using
220    /// Retrieve contract configuration properties.
221    ///
222    /// This method takes a BTreeMap representing a contract and retrieves
223    /// the configuration properties based on the values found in the map.
224    ///
225    /// The process of retrieving contract configuration properties is versioned,
226    /// and the version is determined by the platform version parameter.
227    /// If the version is not supported, an error is returned.
228    ///
229    /// # Parameters
230    ///
231    /// * `contract`: BTreeMap representing the contract.
232    /// * `platform_version`: The platform version being used.
233    ///
234    /// # Returns
235    ///
236    /// * `Result<ContractConfig, ProtocolError>`: On success, a ContractConfig.
237    ///   On failure, a ProtocolError.
238    pub(in crate::data_contract) fn get_contract_configuration_properties(
239        contract: &BTreeMap<String, Value>,
240        platform_version: &PlatformVersion,
241    ) -> Result<DataContractConfig, ProtocolError> {
242        Self::refuse_moderation_the_version_can_not_parse(
243            contract
244                .get(property::MODERATION)
245                .is_some_and(|value| !value.is_null()),
246            platform_version,
247        )?;
248        match platform_version
249            .dpp
250            .contract_versions
251            .config
252            .default_current_version
253        {
254            0 => Ok(
255                DataContractConfigV0::get_contract_configuration_properties_v0(contract)?.into(),
256            ),
257            1 => Ok(
258                DataContractConfigV1::get_contract_configuration_properties_v1(contract)?.into(),
259            ),
260            2 => Ok(
261                DataContractConfigV2::get_contract_configuration_properties_v2(contract)?.into(),
262            ),
263            version => Err(ProtocolError::UnknownVersionMismatch {
264                method: "DataContractConfig::get_contract_configuration_properties".to_string(),
265                known_versions: vec![0, 1, 2],
266                received: version,
267            }),
268        }
269    }
270}
271
272impl DataContractConfigGettersV0 for DataContractConfig {
273    fn can_be_deleted(&self) -> bool {
274        match self {
275            DataContractConfig::V0(v0) => v0.can_be_deleted,
276            DataContractConfig::V1(v1) => v1.can_be_deleted,
277            DataContractConfig::V2(v2) => v2.can_be_deleted,
278        }
279    }
280
281    fn readonly(&self) -> bool {
282        match self {
283            DataContractConfig::V0(v0) => v0.readonly,
284            DataContractConfig::V1(v1) => v1.readonly,
285            DataContractConfig::V2(v2) => v2.readonly,
286        }
287    }
288
289    fn keeps_history(&self) -> bool {
290        match self {
291            DataContractConfig::V0(v0) => v0.keeps_history,
292            DataContractConfig::V1(v1) => v1.keeps_history,
293            DataContractConfig::V2(v2) => v2.keeps_history,
294        }
295    }
296
297    fn documents_keep_history_contract_default(&self) -> bool {
298        match self {
299            DataContractConfig::V0(v0) => v0.documents_keep_history_contract_default,
300            DataContractConfig::V1(v1) => v1.documents_keep_history_contract_default,
301            DataContractConfig::V2(v2) => v2.documents_keep_history_contract_default,
302        }
303    }
304
305    fn documents_mutable_contract_default(&self) -> bool {
306        match self {
307            DataContractConfig::V0(v0) => v0.documents_mutable_contract_default,
308            DataContractConfig::V1(v1) => v1.documents_mutable_contract_default,
309            DataContractConfig::V2(v2) => v2.documents_mutable_contract_default,
310        }
311    }
312
313    fn documents_can_be_deleted_contract_default(&self) -> bool {
314        match self {
315            DataContractConfig::V0(v0) => v0.documents_can_be_deleted_contract_default,
316            DataContractConfig::V1(v1) => v1.documents_can_be_deleted_contract_default,
317            DataContractConfig::V2(v2) => v2.documents_can_be_deleted_contract_default,
318        }
319    }
320
321    /// Encryption key storage requirements
322    fn requires_identity_encryption_bounded_key(&self) -> Option<StorageKeyRequirements> {
323        match self {
324            DataContractConfig::V0(v0) => v0.requires_identity_encryption_bounded_key,
325            DataContractConfig::V1(v1) => v1.requires_identity_encryption_bounded_key,
326            DataContractConfig::V2(v2) => v2.requires_identity_encryption_bounded_key,
327        }
328    }
329
330    /// Decryption key storage requirements
331    fn requires_identity_decryption_bounded_key(&self) -> Option<StorageKeyRequirements> {
332        match self {
333            DataContractConfig::V0(v0) => v0.requires_identity_decryption_bounded_key,
334            DataContractConfig::V1(v1) => v1.requires_identity_decryption_bounded_key,
335            DataContractConfig::V2(v2) => v2.requires_identity_decryption_bounded_key,
336        }
337    }
338}
339
340impl DataContractConfigSettersV0 for DataContractConfig {
341    fn set_can_be_deleted(&mut self, value: bool) {
342        match self {
343            DataContractConfig::V0(v0) => v0.can_be_deleted = value,
344            DataContractConfig::V1(v1) => v1.can_be_deleted = value,
345            DataContractConfig::V2(v2) => v2.can_be_deleted = value,
346        }
347    }
348
349    fn set_readonly(&mut self, value: bool) {
350        match self {
351            DataContractConfig::V0(v0) => v0.readonly = value,
352            DataContractConfig::V1(v1) => v1.readonly = value,
353            DataContractConfig::V2(v2) => v2.readonly = value,
354        }
355    }
356
357    fn set_keeps_history(&mut self, value: bool) {
358        match self {
359            DataContractConfig::V0(v0) => v0.keeps_history = value,
360            DataContractConfig::V1(v1) => v1.keeps_history = value,
361            DataContractConfig::V2(v2) => v2.keeps_history = value,
362        }
363    }
364
365    fn set_documents_keep_history_contract_default(&mut self, value: bool) {
366        match self {
367            DataContractConfig::V0(v0) => v0.documents_keep_history_contract_default = value,
368            DataContractConfig::V1(v1) => v1.documents_keep_history_contract_default = value,
369            DataContractConfig::V2(v2) => v2.documents_keep_history_contract_default = value,
370        }
371    }
372
373    fn set_documents_can_be_deleted_contract_default(&mut self, value: bool) {
374        match self {
375            DataContractConfig::V0(v0) => v0.documents_can_be_deleted_contract_default = value,
376            DataContractConfig::V1(v1) => v1.documents_can_be_deleted_contract_default = value,
377            DataContractConfig::V2(v2) => v2.documents_can_be_deleted_contract_default = value,
378        }
379    }
380
381    fn set_documents_mutable_contract_default(&mut self, value: bool) {
382        match self {
383            DataContractConfig::V0(v0) => v0.documents_mutable_contract_default = value,
384            DataContractConfig::V1(v1) => v1.documents_mutable_contract_default = value,
385            DataContractConfig::V2(v2) => v2.documents_mutable_contract_default = value,
386        }
387    }
388
389    fn set_requires_identity_encryption_bounded_key(
390        &mut self,
391        value: Option<StorageKeyRequirements>,
392    ) {
393        match self {
394            DataContractConfig::V0(v0) => v0.requires_identity_encryption_bounded_key = value,
395            DataContractConfig::V1(v1) => v1.requires_identity_encryption_bounded_key = value,
396            DataContractConfig::V2(v2) => v2.requires_identity_encryption_bounded_key = value,
397        }
398    }
399
400    fn set_requires_identity_decryption_bounded_key(
401        &mut self,
402        value: Option<StorageKeyRequirements>,
403    ) {
404        match self {
405            DataContractConfig::V0(v0) => v0.requires_identity_decryption_bounded_key = value,
406            DataContractConfig::V1(v1) => v1.requires_identity_decryption_bounded_key = value,
407            DataContractConfig::V2(v2) => v2.requires_identity_decryption_bounded_key = value,
408        }
409    }
410}
411
412impl DataContractConfigGettersV1 for DataContractConfig {
413    fn sized_integer_types(&self) -> bool {
414        match self {
415            DataContractConfig::V0(_) => false,
416            DataContractConfig::V1(v1) => v1.sized_integer_types,
417            DataContractConfig::V2(v2) => v2.sized_integer_types,
418        }
419    }
420}
421
422impl DataContractConfigSettersV1 for DataContractConfig {
423    fn set_sized_integer_types_enabled(&mut self, enable: bool) {
424        match self {
425            DataContractConfig::V0(_) => {}
426            DataContractConfig::V1(v1) => v1.sized_integer_types = enable,
427            DataContractConfig::V2(v2) => v2.sized_integer_types = enable,
428        }
429    }
430}
431
432impl DataContractConfigGettersV2 for DataContractConfig {
433    fn moderation(&self) -> Option<&ContractModerationConfig> {
434        match self {
435            DataContractConfig::V0(_) | DataContractConfig::V1(_) => None,
436            DataContractConfig::V2(v2) => v2.moderation.as_ref(),
437        }
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use crate::data_contract::config::v0::DataContractConfigV0;
445    use crate::data_contract::config::v1::DataContractConfigV1;
446    use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements;
447    use platform_version::version::PlatformVersion;
448
449    mod default_for_version {
450        use super::*;
451
452        #[test]
453        fn default_for_latest_platform_version() {
454            let platform_version = PlatformVersion::latest();
455            let config = DataContractConfig::default_for_version(platform_version)
456                .expect("should create config for latest version");
457
458            // Latest platform version uses contract config V1
459            let expected_version = platform_version
460                .dpp
461                .contract_versions
462                .config
463                .default_current_version;
464
465            assert_eq!(config.version(), expected_version);
466        }
467
468        #[test]
469        fn default_for_first_platform_version() {
470            let platform_version = PlatformVersion::first();
471            let config = DataContractConfig::default_for_version(platform_version)
472                .expect("should create config for first version");
473
474            let expected_version = platform_version
475                .dpp
476                .contract_versions
477                .config
478                .default_current_version;
479
480            assert_eq!(config.version(), expected_version);
481        }
482    }
483
484    mod version_method {
485        use super::*;
486
487        #[test]
488        fn v0_reports_version_0() {
489            let config = DataContractConfig::V0(DataContractConfigV0::default());
490            assert_eq!(config.version(), 0);
491        }
492
493        #[test]
494        fn v1_reports_version_1() {
495            let config = DataContractConfig::V1(DataContractConfigV1::default());
496            assert_eq!(config.version(), 1);
497        }
498    }
499
500    mod from_conversions {
501        use super::*;
502
503        #[test]
504        fn v0_into_config() {
505            let v0 = DataContractConfigV0::default();
506            let config: DataContractConfig = v0.into();
507            assert_eq!(config.version(), 0);
508        }
509
510        #[test]
511        fn v1_into_config() {
512            let v1 = DataContractConfigV1::default();
513            let config: DataContractConfig = v1.into();
514            assert_eq!(config.version(), 1);
515        }
516
517        #[test]
518        fn v1_to_v0_conversion_preserves_fields() {
519            let v1 = DataContractConfigV1 {
520                can_be_deleted: true,
521                readonly: true,
522                keeps_history: true,
523                documents_keep_history_contract_default: true,
524                documents_mutable_contract_default: false,
525                documents_can_be_deleted_contract_default: false,
526                requires_identity_encryption_bounded_key: None,
527                requires_identity_decryption_bounded_key: None,
528                sized_integer_types: true,
529            };
530            let v0: DataContractConfigV0 = v1.into();
531            assert!(v0.can_be_deleted);
532            assert!(v0.readonly);
533            assert!(v0.keeps_history);
534            assert!(v0.documents_keep_history_contract_default);
535            assert!(!v0.documents_mutable_contract_default);
536            assert!(!v0.documents_can_be_deleted_contract_default);
537        }
538    }
539
540    mod getters_v0 {
541        use super::*;
542
543        #[test]
544        fn default_v0_getter_values() {
545            let config = DataContractConfig::V0(DataContractConfigV0::default());
546            assert_eq!(config.can_be_deleted(), DEFAULT_CONTRACT_CAN_BE_DELETED);
547            assert_eq!(config.readonly(), !DEFAULT_CONTRACT_MUTABILITY);
548            assert_eq!(config.keeps_history(), DEFAULT_CONTRACT_KEEPS_HISTORY);
549            assert_eq!(
550                config.documents_keep_history_contract_default(),
551                DEFAULT_CONTRACT_DOCUMENTS_KEEPS_HISTORY
552            );
553            assert_eq!(
554                config.documents_mutable_contract_default(),
555                DEFAULT_CONTRACT_DOCUMENT_MUTABILITY
556            );
557            assert_eq!(
558                config.documents_can_be_deleted_contract_default(),
559                DEFAULT_CONTRACT_DOCUMENTS_CAN_BE_DELETED
560            );
561            assert!(config.requires_identity_encryption_bounded_key().is_none());
562            assert!(config.requires_identity_decryption_bounded_key().is_none());
563        }
564
565        #[test]
566        fn default_v1_getter_values() {
567            let config = DataContractConfig::V1(DataContractConfigV1::default());
568            assert_eq!(config.can_be_deleted(), DEFAULT_CONTRACT_CAN_BE_DELETED);
569            assert_eq!(config.readonly(), !DEFAULT_CONTRACT_MUTABILITY);
570            assert_eq!(config.keeps_history(), DEFAULT_CONTRACT_KEEPS_HISTORY);
571            assert_eq!(
572                config.documents_keep_history_contract_default(),
573                DEFAULT_CONTRACT_DOCUMENTS_KEEPS_HISTORY
574            );
575            assert_eq!(
576                config.documents_mutable_contract_default(),
577                DEFAULT_CONTRACT_DOCUMENT_MUTABILITY
578            );
579            assert_eq!(
580                config.documents_can_be_deleted_contract_default(),
581                DEFAULT_CONTRACT_DOCUMENTS_CAN_BE_DELETED
582            );
583        }
584    }
585
586    mod setters_v0 {
587        use super::*;
588
589        #[test]
590        fn set_can_be_deleted_on_v0() {
591            let mut config = DataContractConfig::V0(DataContractConfigV0::default());
592            config.set_can_be_deleted(true);
593            assert!(config.can_be_deleted());
594            config.set_can_be_deleted(false);
595            assert!(!config.can_be_deleted());
596        }
597
598        #[test]
599        fn set_readonly_on_v1() {
600            let mut config = DataContractConfig::V1(DataContractConfigV1::default());
601            config.set_readonly(true);
602            assert!(config.readonly());
603            config.set_readonly(false);
604            assert!(!config.readonly());
605        }
606
607        #[test]
608        fn set_keeps_history() {
609            let mut config = DataContractConfig::V0(DataContractConfigV0::default());
610            config.set_keeps_history(true);
611            assert!(config.keeps_history());
612        }
613
614        #[test]
615        fn set_documents_keep_history() {
616            let mut config = DataContractConfig::V1(DataContractConfigV1::default());
617            config.set_documents_keep_history_contract_default(true);
618            assert!(config.documents_keep_history_contract_default());
619        }
620
621        #[test]
622        fn set_documents_mutable() {
623            let mut config = DataContractConfig::V0(DataContractConfigV0::default());
624            config.set_documents_mutable_contract_default(false);
625            assert!(!config.documents_mutable_contract_default());
626        }
627
628        #[test]
629        fn set_documents_can_be_deleted() {
630            let mut config = DataContractConfig::V1(DataContractConfigV1::default());
631            config.set_documents_can_be_deleted_contract_default(false);
632            assert!(!config.documents_can_be_deleted_contract_default());
633        }
634
635        #[test]
636        fn set_encryption_key_requirements() {
637            let mut config = DataContractConfig::V0(DataContractConfigV0::default());
638            config
639                .set_requires_identity_encryption_bounded_key(Some(StorageKeyRequirements::Unique));
640            assert_eq!(
641                config.requires_identity_encryption_bounded_key(),
642                Some(StorageKeyRequirements::Unique)
643            );
644        }
645
646        #[test]
647        fn set_decryption_key_requirements() {
648            let mut config = DataContractConfig::V1(DataContractConfigV1::default());
649            config
650                .set_requires_identity_decryption_bounded_key(Some(StorageKeyRequirements::Unique));
651            assert_eq!(
652                config.requires_identity_decryption_bounded_key(),
653                Some(StorageKeyRequirements::Unique)
654            );
655        }
656    }
657
658    mod getters_setters_v1 {
659        use super::*;
660
661        #[test]
662        fn sized_integer_types_default_v1() {
663            let config = DataContractConfig::V1(DataContractConfigV1::default());
664            // V1 defaults to sized_integer_types = true
665            assert!(config.sized_integer_types());
666        }
667
668        #[test]
669        fn sized_integer_types_v0_always_false() {
670            let config = DataContractConfig::V0(DataContractConfigV0::default());
671            assert!(!config.sized_integer_types());
672        }
673
674        #[test]
675        fn set_sized_integer_types_on_v1() {
676            let mut config = DataContractConfig::V1(DataContractConfigV1::default());
677            config.set_sized_integer_types_enabled(false);
678            assert!(!config.sized_integer_types());
679            config.set_sized_integer_types_enabled(true);
680            assert!(config.sized_integer_types());
681        }
682
683        #[test]
684        fn set_sized_integer_types_on_v0_is_noop() {
685            let mut config = DataContractConfig::V0(DataContractConfigV0::default());
686            config.set_sized_integer_types_enabled(true);
687            // V0 does not support sized_integer_types; should remain false
688            assert!(!config.sized_integer_types());
689        }
690    }
691
692    mod config_valid_for_platform_version {
693        use super::*;
694
695        #[test]
696        fn v0_stays_v0_regardless_of_platform() {
697            let config = DataContractConfig::V0(DataContractConfigV0::default());
698            let result = config.config_valid_for_platform_version(PlatformVersion::latest());
699            assert_eq!(result.version(), 0);
700        }
701
702        #[test]
703        fn v1_downgraded_to_v0_when_max_version_is_0() {
704            let config = DataContractConfig::V1(DataContractConfigV1 {
705                can_be_deleted: true,
706                readonly: false,
707                keeps_history: true,
708                documents_keep_history_contract_default: false,
709                documents_mutable_contract_default: true,
710                documents_can_be_deleted_contract_default: true,
711                requires_identity_encryption_bounded_key: None,
712                requires_identity_decryption_bounded_key: None,
713                sized_integer_types: true,
714            });
715
716            // Use first platform version which has config max_version = 0
717            let platform_version = PlatformVersion::first();
718            if platform_version.dpp.contract_versions.config.max_version == 0 {
719                let result = config.config_valid_for_platform_version(platform_version);
720                assert_eq!(result.version(), 0);
721                // The converted V0 should preserve basic fields
722                assert!(result.can_be_deleted());
723            }
724        }
725
726        #[test]
727        fn v2_follows_the_platform_version_whatever_it_declares() {
728            let moderated = DataContractConfig::V2(DataContractConfigV2 {
729                moderation: Some(ContractModerationConfig {
730                    banlist: true,
731                    suspensions: false,
732                    moderators: Default::default(),
733                    warnings: false,
734                }),
735                ..DataContractConfigV2::default()
736            });
737            let unmoderated = DataContractConfig::V2(DataContractConfigV2::default());
738
739            // Protocol version 14 admits V2, so both stay V2: the version is the platform
740            // version's, not a function of whether moderation is declared.
741            let latest = PlatformVersion::latest();
742            for config in [moderated.clone(), unmoderated.clone()] {
743                assert_eq!(
744                    config.config_valid_for_platform_version(latest).version(),
745                    2
746                );
747            }
748            assert_eq!(
749                DataContractConfig::default_for_version(latest)
750                    .expect("default config")
751                    .version(),
752                2
753            );
754
755            // Protocol version 13 does not, so both are lowered to V1.
756            let v13 = PlatformVersion::get(13).expect("protocol version 13");
757            for config in [moderated, unmoderated] {
758                assert_eq!(config.config_valid_for_platform_version(v13).version(), 1);
759            }
760        }
761
762        #[test]
763        fn a_moderation_declaration_is_refused_where_the_version_can_not_carry_it() {
764            let moderated = DataContractConfig::V2(DataContractConfigV2 {
765                moderation: Some(ContractModerationConfig {
766                    banlist: true,
767                    suspensions: false,
768                    moderators: Default::default(),
769                    warnings: false,
770                }),
771                ..DataContractConfigV2::default()
772            });
773            let unmoderated = DataContractConfig::V2(DataContractConfigV2::default());
774            let latest = PlatformVersion::latest();
775            let v13 = PlatformVersion::get(13).expect("protocol version 13");
776
777            // Lowering would drop the declaration for good, so it is refused, not dropped.
778            assert!(moderated
779                .ensure_admitted_by_platform_version(latest)
780                .is_ok());
781            assert!(matches!(
782                moderated.ensure_admitted_by_platform_version(v13),
783                Err(ProtocolError::NotSupported(_))
784            ));
785            assert!(unmoderated.ensure_admitted_by_platform_version(v13).is_ok());
786
787            // The same for a value parsed by a config version that has no such key.
788            let value = platform_value::platform_value!({
789                "moderation": { "banlist": true },
790            });
791            assert!(matches!(
792                DataContractConfig::from_value(value.clone(), v13),
793                Err(ProtocolError::NotSupported(_))
794            ));
795            assert!(DataContractConfig::from_value(value, latest)
796                .expect("from_value")
797                .moderation()
798                .is_some());
799        }
800
801        #[test]
802        fn v1_stays_v1_when_max_version_is_1_or_higher() {
803            let config = DataContractConfig::V1(DataContractConfigV1::default());
804            let platform_version = PlatformVersion::latest();
805            if platform_version.dpp.contract_versions.config.max_version >= 1 {
806                let result = config.config_valid_for_platform_version(platform_version);
807                assert_eq!(result.version(), 1);
808            }
809        }
810    }
811
812    /// V0's `get_contract_configuration_properties_v0` has a historical
813    /// copy-paste quirk: the decryption bounded-key field is parsed from
814    /// the `requiresIdentityEncryptionBoundedKey` property (not the matching
815    /// DECRYPTION one). This is part of V0 protocol behavior and MUST NOT be
816    /// changed — altering it would fork the chain. V1 parses correctly; see
817    /// `v1/mod.rs`. These tests lock the V0 behavior in place so the quirk
818    /// is not silently "fixed" by a future well-intentioned refactor.
819    mod get_contract_configuration_properties_v0_consensus_lock {
820        use super::*;
821        use crate::data_contract::config::property::{
822            REQUIRES_IDENTITY_DECRYPTION_BOUNDED_KEY, REQUIRES_IDENTITY_ENCRYPTION_BOUNDED_KEY,
823        };
824        use platform_value::Value;
825        use std::collections::BTreeMap;
826
827        /// When the ENCRYPTION property is set, V0 applies that value to
828        /// BOTH the encryption and decryption fields — because the parser
829        /// reads both from the same key.
830        #[test]
831        fn encryption_property_populates_both_fields() {
832            let mut map: BTreeMap<String, Value> = BTreeMap::new();
833            map.insert(
834                REQUIRES_IDENTITY_ENCRYPTION_BOUNDED_KEY.to_string(),
835                Value::U8(StorageKeyRequirements::Unique as u8),
836            );
837
838            let config = DataContractConfigV0::get_contract_configuration_properties_v0(&map)
839                .expect("should parse V0 config");
840
841            assert_eq!(
842                config.requires_identity_encryption_bounded_key,
843                Some(StorageKeyRequirements::Unique)
844            );
845            assert_eq!(
846                config.requires_identity_decryption_bounded_key,
847                Some(StorageKeyRequirements::Unique),
848                "V0 consensus quirk: decryption field is read from the ENCRYPTION key"
849            );
850        }
851
852        /// When ONLY the DECRYPTION property is set, V0 ignores it entirely
853        /// — neither field is populated, because V0 never reads the
854        /// DECRYPTION key.
855        #[test]
856        fn decryption_property_is_ignored_by_v0() {
857            let mut map: BTreeMap<String, Value> = BTreeMap::new();
858            map.insert(
859                REQUIRES_IDENTITY_DECRYPTION_BOUNDED_KEY.to_string(),
860                Value::U8(StorageKeyRequirements::MultipleReferenceToLatest as u8),
861            );
862
863            let config = DataContractConfigV0::get_contract_configuration_properties_v0(&map)
864                .expect("should parse V0 config");
865
866            assert!(
867                config.requires_identity_encryption_bounded_key.is_none(),
868                "V0 does not read the DECRYPTION property at all"
869            );
870            assert!(
871                config.requires_identity_decryption_bounded_key.is_none(),
872                "V0 consensus quirk: decryption field is NOT sourced from the DECRYPTION key"
873            );
874        }
875
876        /// When BOTH properties are set, the ENCRYPTION value wins for both
877        /// fields; the DECRYPTION property is ignored.
878        #[test]
879        fn encryption_wins_when_both_properties_set() {
880            let mut map: BTreeMap<String, Value> = BTreeMap::new();
881            map.insert(
882                REQUIRES_IDENTITY_ENCRYPTION_BOUNDED_KEY.to_string(),
883                Value::U8(StorageKeyRequirements::Unique as u8),
884            );
885            map.insert(
886                REQUIRES_IDENTITY_DECRYPTION_BOUNDED_KEY.to_string(),
887                Value::U8(StorageKeyRequirements::Multiple as u8),
888            );
889
890            let config = DataContractConfigV0::get_contract_configuration_properties_v0(&map)
891                .expect("should parse V0 config");
892
893            assert_eq!(
894                config.requires_identity_encryption_bounded_key,
895                Some(StorageKeyRequirements::Unique)
896            );
897            assert_eq!(
898                config.requires_identity_decryption_bounded_key,
899                Some(StorageKeyRequirements::Unique),
900                "V0 consensus quirk: the DECRYPTION property is ignored"
901            );
902        }
903
904        /// Sanity check: with neither property set, both fields stay `None`.
905        #[test]
906        fn neither_property_set_leaves_both_none() {
907            let map: BTreeMap<String, Value> = BTreeMap::new();
908            let config = DataContractConfigV0::get_contract_configuration_properties_v0(&map)
909                .expect("should parse V0 config with defaults");
910            assert!(config.requires_identity_encryption_bounded_key.is_none());
911            assert!(config.requires_identity_decryption_bounded_key.is_none());
912        }
913    }
914
915    mod bincode_roundtrip {
916        use super::*;
917        use bincode::config;
918
919        #[test]
920        fn v0_bincode_roundtrip_preserves_fields() {
921            let cfg = config::standard();
922            let original = DataContractConfig::V0(DataContractConfigV0 {
923                can_be_deleted: true,
924                readonly: true,
925                keeps_history: true,
926                documents_keep_history_contract_default: true,
927                documents_mutable_contract_default: false,
928                documents_can_be_deleted_contract_default: false,
929                requires_identity_encryption_bounded_key: Some(StorageKeyRequirements::Unique),
930                requires_identity_decryption_bounded_key: None,
931            });
932            let bytes = bincode::encode_to_vec(&original, cfg).expect("encode");
933            let (decoded, _): (DataContractConfig, _) =
934                bincode::decode_from_slice(&bytes, cfg).expect("decode");
935            assert_eq!(decoded, original);
936        }
937
938        #[test]
939        fn v1_bincode_roundtrip_preserves_sized_integer_types() {
940            let cfg = config::standard();
941            let original = DataContractConfig::V1(DataContractConfigV1 {
942                can_be_deleted: false,
943                readonly: false,
944                keeps_history: false,
945                documents_keep_history_contract_default: false,
946                documents_mutable_contract_default: true,
947                documents_can_be_deleted_contract_default: true,
948                requires_identity_encryption_bounded_key: None,
949                requires_identity_decryption_bounded_key: None,
950                sized_integer_types: false,
951            });
952            let bytes = bincode::encode_to_vec(&original, cfg).expect("encode");
953            let (decoded, _): (DataContractConfig, _) =
954                bincode::decode_from_slice(&bytes, cfg).expect("decode");
955            assert_eq!(decoded, original);
956            // And sized_integer_types is correctly false on the decoded copy
957            assert!(!decoded.sized_integer_types());
958        }
959    }
960
961    mod from_value_tests {
962        use super::*;
963        use platform_value::platform_value;
964
965        #[test]
966        fn from_value_yields_default_for_empty_object() {
967            // Empty object -> defaults; succeeds on the latest platform version
968            let value = platform_value!({});
969            let platform_version = PlatformVersion::latest();
970            let cfg = DataContractConfig::from_value(value, platform_version)
971                .expect("empty object should deserialize to defaults");
972            // All booleans should match defaults
973            assert_eq!(cfg.can_be_deleted(), DEFAULT_CONTRACT_CAN_BE_DELETED);
974        }
975    }
976
977    mod get_contract_configuration_properties_tests {
978        use super::*;
979        use platform_value::Value;
980        use std::collections::BTreeMap;
981
982        fn make_contract_map(can_be_deleted: bool, readonly: bool) -> BTreeMap<String, Value> {
983            let mut m = BTreeMap::new();
984            m.insert(
985                property::CAN_BE_DELETED.to_string(),
986                Value::Bool(can_be_deleted),
987            );
988            m.insert(property::READONLY.to_string(), Value::Bool(readonly));
989            m
990        }
991
992        #[test]
993        fn reads_booleans_from_map() {
994            let platform_version = PlatformVersion::latest();
995            // Use distinct values for both fields so a key mix-up (reading
996            // one field from the other's key) would fail the assertion.
997            for (can_be_deleted, readonly) in [(true, false), (false, true)] {
998                let contract = make_contract_map(can_be_deleted, readonly);
999                let cfg = DataContractConfig::get_contract_configuration_properties(
1000                    &contract,
1001                    platform_version,
1002                )
1003                .expect("should parse config from map");
1004                assert_eq!(cfg.can_be_deleted(), can_be_deleted);
1005                assert_eq!(cfg.readonly(), readonly);
1006            }
1007        }
1008
1009        #[test]
1010        fn missing_keys_fall_back_to_defaults() {
1011            let platform_version = PlatformVersion::latest();
1012            let empty: BTreeMap<String, Value> = BTreeMap::new();
1013            let cfg =
1014                DataContractConfig::get_contract_configuration_properties(&empty, platform_version)
1015                    .expect("should parse empty contract map");
1016            // Defaults preserved
1017            assert_eq!(cfg.can_be_deleted(), DEFAULT_CONTRACT_CAN_BE_DELETED);
1018            assert_eq!(cfg.keeps_history(), DEFAULT_CONTRACT_KEEPS_HISTORY);
1019        }
1020
1021        #[test]
1022        fn non_bool_value_errors() {
1023            let platform_version = PlatformVersion::latest();
1024            let mut m: BTreeMap<String, Value> = BTreeMap::new();
1025            m.insert(
1026                property::CAN_BE_DELETED.to_string(),
1027                Value::Text("not-a-bool".to_string()),
1028            );
1029            let result =
1030                DataContractConfig::get_contract_configuration_properties(&m, platform_version);
1031            assert!(result.is_err());
1032        }
1033    }
1034}
1035
1036#[cfg(all(
1037    test,
1038    feature = "json-conversion",
1039    feature = "value-conversion",
1040    feature = "serde-conversion"
1041))]
1042mod json_convertible_tests {
1043    use super::*;
1044    use crate::data_contract::config::v0::DataContractConfigV0;
1045    use crate::data_contract::storage_requirements::keys_for_document_type::StorageKeyRequirements;
1046    use platform_value::platform_value;
1047    use serde_json::json;
1048
1049    /// Non-default values per field so the wire-shape assertion catches any
1050    /// silent zero-out / flip on round-trip.
1051    fn fixture() -> DataContractConfig {
1052        DataContractConfig::V0(DataContractConfigV0 {
1053            can_be_deleted: true,
1054            readonly: true,
1055            keeps_history: true,
1056            documents_keep_history_contract_default: true,
1057            documents_mutable_contract_default: false,
1058            documents_can_be_deleted_contract_default: false,
1059            requires_identity_encryption_bounded_key: Some(StorageKeyRequirements::Unique),
1060            requires_identity_decryption_bounded_key: Some(StorageKeyRequirements::Multiple),
1061        })
1062    }
1063
1064    #[test]
1065    fn json_round_trip_with_full_wire_shape() {
1066        use crate::serialization::JsonConvertible;
1067        let original = fixture();
1068        let json = original.to_json().expect("to_json");
1069        // `requiresIdentity{En,De}cryptionBoundedKey` are `Option<StorageKeyRequirements>`
1070        // where `StorageKeyRequirements` is `#[repr(u8)]` with `Serialize_repr`
1071        // (Unique = 0, Multiple = 1). JSON has only one number type, so the
1072        // u8-ness of these fields is erased on the wire — the Value-path
1073        // assertion below uses `0u8` / `1u8` to lock in the sized variant.
1074        assert_eq!(
1075            json,
1076            json!({
1077                "$formatVersion": "0",
1078                "canBeDeleted": true,
1079                "readonly": true,
1080                "keepsHistory": true,
1081                "documentsKeepHistoryContractDefault": true,
1082                "documentsMutableContractDefault": false,
1083                "documentsCanBeDeletedContractDefault": false,
1084                "requiresIdentityEncryptionBoundedKey": 0,
1085                "requiresIdentityDecryptionBoundedKey": 1,
1086            })
1087        );
1088        let recovered = DataContractConfig::from_json(json).expect("from_json");
1089        assert_eq!(original, recovered);
1090    }
1091
1092    #[test]
1093    fn value_round_trip_with_full_wire_shape() {
1094        use crate::serialization::ValueConvertible;
1095        let original = fixture();
1096        let value = original.to_object().expect("to_object");
1097        // `0u8` / `1u8`: `StorageKeyRequirements` is `#[repr(u8)]`, and
1098        // platform_value preserves sized variants (`Value::U8`, not `Value::U64`).
1099        assert_eq!(
1100            value,
1101            platform_value!({
1102                "$formatVersion": "0",
1103                "canBeDeleted": true,
1104                "readonly": true,
1105                "keepsHistory": true,
1106                "documentsKeepHistoryContractDefault": true,
1107                "documentsMutableContractDefault": false,
1108                "documentsCanBeDeletedContractDefault": false,
1109                "requiresIdentityEncryptionBoundedKey": 0u8,
1110                "requiresIdentityDecryptionBoundedKey": 1u8,
1111            })
1112        );
1113        let recovered = DataContractConfig::from_object(value).expect("from_object");
1114        assert_eq!(original, recovered);
1115    }
1116}