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