Skip to main content

dpp/data_contract/config/
mod.rs

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