Skip to main content

dpp/data_contract/serialized_version/
mod.rs

1use super::EMPTY_KEYWORDS;
2use crate::data_contract::associated_token::token_configuration::TokenConfiguration;
3use crate::data_contract::config::DataContractConfig;
4use crate::data_contract::group::Group;
5use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0;
6use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1;
7use crate::data_contract::v0::DataContractV0;
8use crate::data_contract::v1::DataContractV1;
9use crate::data_contract::{
10    DataContract, DefinitionName, DocumentName, GroupContractPosition, TokenContractPosition,
11    EMPTY_GROUPS, EMPTY_TOKENS,
12};
13#[cfg(feature = "json-conversion")]
14use crate::serialization::JsonConvertible;
15#[cfg(feature = "value-conversion")]
16use crate::serialization::ValueConvertible;
17use crate::validation::operations::ProtocolValidationOperation;
18use crate::version::PlatformVersion;
19use crate::ProtocolError;
20use bincode::{Decode, Encode};
21use derive_more::From;
22use platform_value::{Identifier, Value};
23use platform_version::{IntoPlatformVersioned, TryFromPlatformVersioned};
24use platform_versioning::PlatformVersioned;
25#[cfg(feature = "serde-conversion")]
26use serde::{Deserialize, Serialize};
27use std::collections::BTreeMap;
28use std::fmt;
29
30pub(in crate::data_contract) mod v0;
31pub(in crate::data_contract) mod v1;
32
33pub mod property_names {
34    pub const ID: &str = "id";
35    pub const OWNER_ID: &str = "ownerId";
36    pub const VERSION: &str = "version";
37    pub const DEFINITIONS: &str = "$defs";
38}
39
40pub const CONTRACT_DESERIALIZATION_LIMIT: usize = 15000;
41
42/// Represents a field mismatch between two `DataContractInSerializationFormat::V1`
43/// variants, or indicates a format version mismatch.
44///
45/// Used to diagnose why two data contracts are not considered equal
46/// when ignoring auto-generated fields.
47#[derive(Debug, PartialEq, Eq, Clone, Copy)]
48pub enum DataContractMismatch {
49    /// The `id` fields are not equal.
50    Id,
51    /// The `config` fields are not equal.
52    Config,
53    /// The `version` fields are not equal.
54    Version,
55    /// The `owner_id` fields are not equal.
56    OwnerId,
57    /// The `schema_defs` fields are not equal.
58    SchemaDefs,
59    /// The `document_schemas` fields are not equal.
60    DocumentSchemas,
61    /// The `groups` fields are not equal.
62    Groups,
63    /// The `tokens` fields are not equal.
64    Tokens,
65    /// The `keywords` fields are not equal.
66    Keywords,
67    /// The `description` fields are not equal.
68    Description,
69    /// The two variants are of different serialization formats (e.g., V0 vs V1).
70    FormatVersionMismatch,
71    /// The two variants are different in V0.
72    V0Mismatch,
73}
74
75impl fmt::Display for DataContractMismatch {
76    /// Formats the enum into a human-readable string describing the mismatch.
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        let description = match self {
79            DataContractMismatch::Id => "ID fields differ",
80            DataContractMismatch::Config => "Config fields differ",
81            DataContractMismatch::Version => "Version fields differ",
82            DataContractMismatch::OwnerId => "Owner ID fields differ",
83            DataContractMismatch::SchemaDefs => "Schema definitions differ",
84            DataContractMismatch::DocumentSchemas => "Document schemas differ",
85            DataContractMismatch::Groups => "Groups differ",
86            DataContractMismatch::Tokens => "Tokens differ",
87            DataContractMismatch::Keywords => "Keywords differ",
88            DataContractMismatch::Description => "Description fields differ",
89            DataContractMismatch::FormatVersionMismatch => {
90                "Serialization format versions differ (e.g., V0 vs V1)"
91            }
92            DataContractMismatch::V0Mismatch => "V0 versions differ",
93        };
94        write!(f, "{}", description)
95    }
96}
97
98#[cfg_attr(
99    all(feature = "json-conversion", feature = "serde-conversion"),
100    derive(JsonConvertible)
101)]
102#[cfg_attr(
103    all(feature = "value-conversion", feature = "serde-conversion"),
104    derive(ValueConvertible)
105)]
106#[derive(Debug, Clone, Encode, Decode, PartialEq, PlatformVersioned, From)]
107#[cfg_attr(
108    feature = "serde-conversion",
109    derive(Serialize, Deserialize),
110    serde(tag = "$formatVersion")
111)]
112pub enum DataContractInSerializationFormat {
113    #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))]
114    V0(DataContractInSerializationFormatV0),
115    #[cfg_attr(feature = "serde-conversion", serde(rename = "1"))]
116    V1(DataContractInSerializationFormatV1),
117}
118
119impl DataContractInSerializationFormat {
120    /// Returns the unique identifier for the data contract.
121    pub fn id(&self) -> Identifier {
122        match self {
123            DataContractInSerializationFormat::V0(v0) => v0.id,
124            DataContractInSerializationFormat::V1(v1) => v1.id,
125        }
126    }
127
128    /// Returns the owner identifier for the data contract.
129    pub fn owner_id(&self) -> Identifier {
130        match self {
131            DataContractInSerializationFormat::V0(v0) => v0.owner_id,
132            DataContractInSerializationFormat::V1(v1) => v1.owner_id,
133        }
134    }
135
136    pub fn document_schemas(&self) -> &BTreeMap<DocumentName, Value> {
137        match self {
138            DataContractInSerializationFormat::V0(v0) => &v0.document_schemas,
139            DataContractInSerializationFormat::V1(v1) => &v1.document_schemas,
140        }
141    }
142
143    pub fn document_schemas_mut(&mut self) -> &mut BTreeMap<DocumentName, Value> {
144        match self {
145            DataContractInSerializationFormat::V0(v0) => &mut v0.document_schemas,
146            DataContractInSerializationFormat::V1(v1) => &mut v1.document_schemas,
147        }
148    }
149
150    pub fn schema_defs(&self) -> Option<&BTreeMap<DefinitionName, Value>> {
151        match self {
152            DataContractInSerializationFormat::V0(v0) => v0.schema_defs.as_ref(),
153            DataContractInSerializationFormat::V1(v1) => v1.schema_defs.as_ref(),
154        }
155    }
156
157    pub fn version(&self) -> u32 {
158        match self {
159            DataContractInSerializationFormat::V0(v0) => v0.version,
160            DataContractInSerializationFormat::V1(v1) => v1.version,
161        }
162    }
163
164    /// Returns the config for the data contract.
165    pub fn config(&self) -> &DataContractConfig {
166        match self {
167            DataContractInSerializationFormat::V0(v0) => &v0.config,
168            DataContractInSerializationFormat::V1(v1) => &v1.config,
169        }
170    }
171
172    pub fn groups(&self) -> &BTreeMap<GroupContractPosition, Group> {
173        match self {
174            DataContractInSerializationFormat::V0(_) => &EMPTY_GROUPS,
175            DataContractInSerializationFormat::V1(v1) => &v1.groups,
176        }
177    }
178    pub fn tokens(&self) -> &BTreeMap<TokenContractPosition, TokenConfiguration> {
179        match self {
180            DataContractInSerializationFormat::V0(_) => &EMPTY_TOKENS,
181            DataContractInSerializationFormat::V1(v1) => &v1.tokens,
182        }
183    }
184
185    pub fn keywords(&self) -> &Vec<String> {
186        match self {
187            DataContractInSerializationFormat::V0(_) => &EMPTY_KEYWORDS,
188            DataContractInSerializationFormat::V1(v1) => &v1.keywords,
189        }
190    }
191
192    pub fn description(&self) -> &Option<String> {
193        match self {
194            DataContractInSerializationFormat::V0(_) => &None,
195            DataContractInSerializationFormat::V1(v1) => &v1.description,
196        }
197    }
198
199    /// Compares `self` to another `DataContractInSerializationFormat` instance
200    /// and returns the first mismatching field, if any.
201    ///
202    /// This comparison ignores auto-generated fields and is only sensitive to
203    /// significant differences in contract content. For V0 formats, any difference
204    /// results in a generic mismatch. For differing format versions (V0 vs V1),
205    /// a `FormatVersionMismatch` is returned.
206    ///
207    /// # Returns
208    ///
209    /// - `None` if the contracts are equal according to the relevant fields.
210    /// - `Some(DataContractMismatch)` indicating the first field where they differ.
211    pub fn first_mismatch(&self, other: &Self) -> Option<DataContractMismatch> {
212        match (self, other) {
213            (
214                DataContractInSerializationFormat::V0(v0_self),
215                DataContractInSerializationFormat::V0(v0_other),
216            ) => {
217                if v0_self != v0_other {
218                    Some(DataContractMismatch::V0Mismatch)
219                } else {
220                    None
221                }
222            }
223            (
224                DataContractInSerializationFormat::V1(v1_self),
225                DataContractInSerializationFormat::V1(v1_other),
226            ) => {
227                if v1_self.id != v1_other.id {
228                    Some(DataContractMismatch::Id)
229                } else if v1_self.config != v1_other.config {
230                    Some(DataContractMismatch::Config)
231                } else if v1_self.version != v1_other.version {
232                    Some(DataContractMismatch::Version)
233                } else if v1_self.owner_id != v1_other.owner_id {
234                    Some(DataContractMismatch::OwnerId)
235                } else if v1_self.schema_defs != v1_other.schema_defs {
236                    Some(DataContractMismatch::SchemaDefs)
237                } else if v1_self.document_schemas != v1_other.document_schemas {
238                    Some(DataContractMismatch::DocumentSchemas)
239                } else if v1_self.groups != v1_other.groups {
240                    Some(DataContractMismatch::Groups)
241                } else if v1_self.tokens != v1_other.tokens {
242                    Some(DataContractMismatch::Tokens)
243                } else if v1_self.keywords.len() != v1_other.keywords.len()
244                    || v1_self
245                        .keywords
246                        .iter()
247                        .zip(v1_other.keywords.iter())
248                        .any(|(a, b)| a.to_lowercase() != b.to_lowercase())
249                {
250                    Some(DataContractMismatch::Keywords)
251                } else if v1_self.description != v1_other.description {
252                    Some(DataContractMismatch::Description)
253                } else {
254                    None
255                }
256            }
257            _ => Some(DataContractMismatch::FormatVersionMismatch),
258        }
259    }
260}
261
262impl TryFromPlatformVersioned<DataContractV0> for DataContractInSerializationFormat {
263    type Error = ProtocolError;
264
265    fn try_from_platform_versioned(
266        value: DataContractV0,
267        platform_version: &PlatformVersion,
268    ) -> Result<Self, Self::Error> {
269        match platform_version
270            .dpp
271            .contract_versions
272            .contract_serialization_version
273            .default_current_version
274        {
275            0 => {
276                let v0_format: DataContractInSerializationFormatV0 =
277                    DataContract::V0(value).into_platform_versioned(platform_version);
278                Ok(v0_format.into())
279            }
280            1 => {
281                let v1_format: DataContractInSerializationFormatV1 =
282                    DataContract::V0(value).into_platform_versioned(platform_version);
283                Ok(v1_format.into())
284            }
285            version => Err(ProtocolError::UnknownVersionMismatch {
286                method: "DataContract::serialize_to_default_current_version".to_string(),
287                known_versions: vec![0, 1],
288                received: version,
289            }),
290        }
291    }
292}
293
294impl TryFromPlatformVersioned<&DataContractV0> for DataContractInSerializationFormat {
295    type Error = ProtocolError;
296
297    fn try_from_platform_versioned(
298        value: &DataContractV0,
299        platform_version: &PlatformVersion,
300    ) -> Result<Self, Self::Error> {
301        match platform_version
302            .dpp
303            .contract_versions
304            .contract_serialization_version
305            .default_current_version
306        {
307            0 => {
308                let v0_format: DataContractInSerializationFormatV0 =
309                    DataContract::V0(value.to_owned()).into_platform_versioned(platform_version);
310                Ok(v0_format.into())
311            }
312            1 => {
313                let v1_format: DataContractInSerializationFormatV1 =
314                    DataContract::V0(value.to_owned()).into_platform_versioned(platform_version);
315                Ok(v1_format.into())
316            }
317            version => Err(ProtocolError::UnknownVersionMismatch {
318                method: "DataContract::serialize_to_default_current_version".to_string(),
319                known_versions: vec![0, 1],
320                received: version,
321            }),
322        }
323    }
324}
325
326impl TryFromPlatformVersioned<DataContractV1> for DataContractInSerializationFormat {
327    type Error = ProtocolError;
328
329    fn try_from_platform_versioned(
330        value: DataContractV1,
331        platform_version: &PlatformVersion,
332    ) -> Result<Self, Self::Error> {
333        match platform_version
334            .dpp
335            .contract_versions
336            .contract_serialization_version
337            .default_current_version
338        {
339            0 => {
340                let v0_format: DataContractInSerializationFormatV0 =
341                    DataContract::V1(value).into_platform_versioned(platform_version);
342                Ok(v0_format.into())
343            }
344            1 => {
345                let v1_format: DataContractInSerializationFormatV1 =
346                    DataContract::V1(value).into_platform_versioned(platform_version);
347                Ok(v1_format.into())
348            }
349            version => Err(ProtocolError::UnknownVersionMismatch {
350                method: "DataContract::serialize_to_default_current_version".to_string(),
351                known_versions: vec![0, 1],
352                received: version,
353            }),
354        }
355    }
356}
357
358impl TryFromPlatformVersioned<&DataContractV1> for DataContractInSerializationFormat {
359    type Error = ProtocolError;
360
361    fn try_from_platform_versioned(
362        value: &DataContractV1,
363        platform_version: &PlatformVersion,
364    ) -> Result<Self, Self::Error> {
365        match platform_version
366            .dpp
367            .contract_versions
368            .contract_serialization_version
369            .default_current_version
370        {
371            0 => {
372                let v0_format: DataContractInSerializationFormatV0 =
373                    DataContract::V1(value.to_owned()).into_platform_versioned(platform_version);
374                Ok(v0_format.into())
375            }
376            1 => {
377                let v1_format: DataContractInSerializationFormatV1 =
378                    DataContract::V1(value.to_owned()).into_platform_versioned(platform_version);
379                Ok(v1_format.into())
380            }
381            version => Err(ProtocolError::UnknownVersionMismatch {
382                method: "DataContract::serialize_to_default_current_version".to_string(),
383                known_versions: vec![0, 1],
384                received: version,
385            }),
386        }
387    }
388}
389
390impl TryFromPlatformVersioned<&DataContract> for DataContractInSerializationFormat {
391    type Error = ProtocolError;
392
393    fn try_from_platform_versioned(
394        value: &DataContract,
395        platform_version: &PlatformVersion,
396    ) -> Result<Self, Self::Error> {
397        match platform_version
398            .dpp
399            .contract_versions
400            .contract_serialization_version
401            .default_current_version
402        {
403            0 => {
404                let v0_format: DataContractInSerializationFormatV0 =
405                    value.clone().into_platform_versioned(platform_version);
406                Ok(v0_format.into())
407            }
408            1 => {
409                let v1_format: DataContractInSerializationFormatV1 =
410                    value.clone().into_platform_versioned(platform_version);
411                Ok(v1_format.into())
412            }
413            version => Err(ProtocolError::UnknownVersionMismatch {
414                method: "DataContract::serialize_to_default_current_version".to_string(),
415                known_versions: vec![0, 1],
416                received: version,
417            }),
418        }
419    }
420}
421
422impl TryFromPlatformVersioned<DataContract> for DataContractInSerializationFormat {
423    type Error = ProtocolError;
424
425    fn try_from_platform_versioned(
426        value: DataContract,
427        platform_version: &PlatformVersion,
428    ) -> Result<Self, Self::Error> {
429        match platform_version
430            .dpp
431            .contract_versions
432            .contract_serialization_version
433            .default_current_version
434        {
435            0 => {
436                let v0_format: DataContractInSerializationFormatV0 =
437                    value.into_platform_versioned(platform_version);
438                Ok(v0_format.into())
439            }
440            1 => {
441                let v1_format: DataContractInSerializationFormatV1 =
442                    value.into_platform_versioned(platform_version);
443                Ok(v1_format.into())
444            }
445            version => Err(ProtocolError::UnknownVersionMismatch {
446                method: "DataContract::serialize_consume_to_default_current_version".to_string(),
447                known_versions: vec![0, 1],
448                received: version,
449            }),
450        }
451    }
452}
453
454impl DataContract {
455    pub fn try_from_platform_versioned(
456        value: DataContractInSerializationFormat,
457        full_validation: bool,
458        validation_operations: &mut Vec<ProtocolValidationOperation>,
459        platform_version: &PlatformVersion,
460    ) -> Result<Self, ProtocolError> {
461        match platform_version
462            .dpp
463            .contract_versions
464            .contract_structure_version
465        {
466            0 => DataContractV0::try_from_platform_versioned(
467                value,
468                full_validation,
469                validation_operations,
470                platform_version,
471            )
472            .map(|contract| contract.into()),
473            1 => DataContractV1::try_from_platform_versioned(
474                value,
475                full_validation,
476                validation_operations,
477                platform_version,
478            )
479            .map(|contract| contract.into()),
480            version => Err(ProtocolError::UnknownVersionMismatch {
481                method: "DataContract::try_from_platform_versioned".to_string(),
482                known_versions: vec![0, 1],
483                received: version,
484            }),
485        }
486    }
487}
488
489#[cfg(test)]
490mod tests {
491    use super::*;
492    use crate::data_contract::config::v0::DataContractConfigV0;
493    use crate::data_contract::config::v1::DataContractConfigV1;
494    use crate::data_contract::group::v0::GroupV0;
495    use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0;
496    use crate::data_contract::serialized_version::v1::DataContractInSerializationFormatV1;
497    use platform_value::Identifier;
498    use std::collections::BTreeMap;
499
500    /// Helper to create a default V0 serialization format.
501    fn make_v0() -> DataContractInSerializationFormatV0 {
502        DataContractInSerializationFormatV0 {
503            id: Identifier::default(),
504            config: DataContractConfig::V0(DataContractConfigV0::default()),
505            version: 1,
506            owner_id: Identifier::default(),
507            schema_defs: None,
508            document_schemas: BTreeMap::new(),
509        }
510    }
511
512    /// Helper to create a default V1 serialization format.
513    fn make_v1() -> DataContractInSerializationFormatV1 {
514        DataContractInSerializationFormatV1 {
515            id: Identifier::default(),
516            config: DataContractConfig::V1(DataContractConfigV1::default()),
517            version: 1,
518            owner_id: Identifier::default(),
519            schema_defs: None,
520            document_schemas: BTreeMap::new(),
521            created_at: None,
522            updated_at: None,
523            created_at_block_height: None,
524            updated_at_block_height: None,
525            created_at_epoch: None,
526            updated_at_epoch: None,
527            groups: BTreeMap::new(),
528            tokens: BTreeMap::new(),
529            keywords: vec![],
530            description: None,
531        }
532    }
533
534    // -----------------------------------------------------------------------
535    // first_mismatch: V0-V0
536    // -----------------------------------------------------------------------
537
538    #[test]
539    fn first_mismatch_v0_v0_identical_returns_none() {
540        let a = DataContractInSerializationFormat::V0(make_v0());
541        let b = DataContractInSerializationFormat::V0(make_v0());
542        assert_eq!(a.first_mismatch(&b), None);
543    }
544
545    #[test]
546    fn first_mismatch_v0_v0_different_id() {
547        let mut v0_b = make_v0();
548        v0_b.id = Identifier::from([1u8; 32]);
549        let a = DataContractInSerializationFormat::V0(make_v0());
550        let b = DataContractInSerializationFormat::V0(v0_b);
551        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch));
552    }
553
554    #[test]
555    fn first_mismatch_v0_v0_different_config() {
556        let mut v0_b = make_v0();
557        let mut cfg = DataContractConfigV0::default();
558        cfg.readonly = !cfg.readonly;
559        v0_b.config = DataContractConfig::V0(cfg);
560        let a = DataContractInSerializationFormat::V0(make_v0());
561        let b = DataContractInSerializationFormat::V0(v0_b);
562        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch));
563    }
564
565    #[test]
566    fn first_mismatch_v0_v0_different_version() {
567        let mut v0_b = make_v0();
568        v0_b.version = 99;
569        let a = DataContractInSerializationFormat::V0(make_v0());
570        let b = DataContractInSerializationFormat::V0(v0_b);
571        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch));
572    }
573
574    #[test]
575    fn first_mismatch_v0_v0_different_owner_id() {
576        let mut v0_b = make_v0();
577        v0_b.owner_id = Identifier::from([2u8; 32]);
578        let a = DataContractInSerializationFormat::V0(make_v0());
579        let b = DataContractInSerializationFormat::V0(v0_b);
580        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch));
581    }
582
583    #[test]
584    fn first_mismatch_v0_v0_different_document_schemas() {
585        let mut v0_b = make_v0();
586        v0_b.document_schemas
587            .insert("doc".to_string(), Value::Bool(true));
588        let a = DataContractInSerializationFormat::V0(make_v0());
589        let b = DataContractInSerializationFormat::V0(v0_b);
590        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::V0Mismatch));
591    }
592
593    // -----------------------------------------------------------------------
594    // first_mismatch: format mismatch (V0 vs V1)
595    // -----------------------------------------------------------------------
596
597    #[test]
598    fn first_mismatch_v0_v1_returns_format_version_mismatch() {
599        let a = DataContractInSerializationFormat::V0(make_v0());
600        let b = DataContractInSerializationFormat::V1(make_v1());
601        assert_eq!(
602            a.first_mismatch(&b),
603            Some(DataContractMismatch::FormatVersionMismatch)
604        );
605    }
606
607    #[test]
608    fn first_mismatch_v1_v0_returns_format_version_mismatch() {
609        let a = DataContractInSerializationFormat::V1(make_v1());
610        let b = DataContractInSerializationFormat::V0(make_v0());
611        assert_eq!(
612            a.first_mismatch(&b),
613            Some(DataContractMismatch::FormatVersionMismatch)
614        );
615    }
616
617    // -----------------------------------------------------------------------
618    // first_mismatch: V1-V1 identical
619    // -----------------------------------------------------------------------
620
621    #[test]
622    fn first_mismatch_v1_v1_identical_returns_none() {
623        let a = DataContractInSerializationFormat::V1(make_v1());
624        let b = DataContractInSerializationFormat::V1(make_v1());
625        assert_eq!(a.first_mismatch(&b), None);
626    }
627
628    // -----------------------------------------------------------------------
629    // first_mismatch: V1-V1 field-by-field mismatches
630    // -----------------------------------------------------------------------
631
632    #[test]
633    fn first_mismatch_v1_v1_different_id() {
634        let mut v1_b = make_v1();
635        v1_b.id = Identifier::from([1u8; 32]);
636        let a = DataContractInSerializationFormat::V1(make_v1());
637        let b = DataContractInSerializationFormat::V1(v1_b);
638        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Id));
639    }
640
641    #[test]
642    fn first_mismatch_v1_v1_different_config() {
643        let mut v1_b = make_v1();
644        let mut cfg = DataContractConfigV1::default();
645        cfg.readonly = !cfg.readonly;
646        v1_b.config = DataContractConfig::V1(cfg);
647        let a = DataContractInSerializationFormat::V1(make_v1());
648        let b = DataContractInSerializationFormat::V1(v1_b);
649        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Config));
650    }
651
652    #[test]
653    fn first_mismatch_v1_v1_different_version() {
654        let mut v1_b = make_v1();
655        v1_b.version = 42;
656        let a = DataContractInSerializationFormat::V1(make_v1());
657        let b = DataContractInSerializationFormat::V1(v1_b);
658        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Version));
659    }
660
661    #[test]
662    fn first_mismatch_v1_v1_different_owner_id() {
663        let mut v1_b = make_v1();
664        v1_b.owner_id = Identifier::from([3u8; 32]);
665        let a = DataContractInSerializationFormat::V1(make_v1());
666        let b = DataContractInSerializationFormat::V1(v1_b);
667        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::OwnerId));
668    }
669
670    #[test]
671    fn first_mismatch_v1_v1_different_schema_defs() {
672        let mut v1_b = make_v1();
673        let mut defs = BTreeMap::new();
674        defs.insert("someDef".to_string(), Value::Bool(true));
675        v1_b.schema_defs = Some(defs);
676        let a = DataContractInSerializationFormat::V1(make_v1());
677        let b = DataContractInSerializationFormat::V1(v1_b);
678        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::SchemaDefs));
679    }
680
681    #[test]
682    fn first_mismatch_v1_v1_different_document_schemas() {
683        let mut v1_b = make_v1();
684        v1_b.document_schemas
685            .insert("doc".to_string(), Value::U64(1));
686        let a = DataContractInSerializationFormat::V1(make_v1());
687        let b = DataContractInSerializationFormat::V1(v1_b);
688        assert_eq!(
689            a.first_mismatch(&b),
690            Some(DataContractMismatch::DocumentSchemas)
691        );
692    }
693
694    #[test]
695    fn first_mismatch_v1_v1_different_groups() {
696        let mut v1_b = make_v1();
697        v1_b.groups.insert(
698            0,
699            Group::V0(GroupV0 {
700                members: Default::default(),
701                required_power: 1,
702            }),
703        );
704        let a = DataContractInSerializationFormat::V1(make_v1());
705        let b = DataContractInSerializationFormat::V1(v1_b);
706        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Groups));
707    }
708
709    #[test]
710    fn first_mismatch_v1_v1_different_tokens() {
711        let mut v1_b = make_v1();
712        v1_b.tokens.insert(
713            0,
714            TokenConfiguration::V0(
715                crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0::default_most_restrictive(),
716            ),
717        );
718        let a = DataContractInSerializationFormat::V1(make_v1());
719        let b = DataContractInSerializationFormat::V1(v1_b);
720        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Tokens));
721    }
722
723    #[test]
724    fn first_mismatch_v1_v1_different_keywords() {
725        let mut v1_b = make_v1();
726        v1_b.keywords = vec!["test".to_string()];
727        let a = DataContractInSerializationFormat::V1(make_v1());
728        let b = DataContractInSerializationFormat::V1(v1_b);
729        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Keywords));
730    }
731
732    #[test]
733    fn first_mismatch_v1_v1_keywords_case_insensitive_match() {
734        let mut v1_a = make_v1();
735        v1_a.keywords = vec!["Test".to_string()];
736        let mut v1_b = make_v1();
737        v1_b.keywords = vec!["test".to_string()];
738        let a = DataContractInSerializationFormat::V1(v1_a);
739        let b = DataContractInSerializationFormat::V1(v1_b);
740        // The comparison uses to_lowercase, so "Test" and "test" should match
741        assert_eq!(a.first_mismatch(&b), None);
742    }
743
744    #[test]
745    fn first_mismatch_v1_v1_keywords_different_length() {
746        let mut v1_a = make_v1();
747        v1_a.keywords = vec!["a".to_string()];
748        let mut v1_b = make_v1();
749        v1_b.keywords = vec!["a".to_string(), "b".to_string()];
750        let a = DataContractInSerializationFormat::V1(v1_a);
751        let b = DataContractInSerializationFormat::V1(v1_b);
752        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Keywords));
753    }
754
755    #[test]
756    fn first_mismatch_v1_v1_different_description() {
757        let mut v1_b = make_v1();
758        v1_b.description = Some("a description".to_string());
759        let a = DataContractInSerializationFormat::V1(make_v1());
760        let b = DataContractInSerializationFormat::V1(v1_b);
761        assert_eq!(
762            a.first_mismatch(&b),
763            Some(DataContractMismatch::Description)
764        );
765    }
766
767    // -----------------------------------------------------------------------
768    // first_mismatch: priority ordering in V1 (id detected before config, etc.)
769    // -----------------------------------------------------------------------
770
771    #[test]
772    fn first_mismatch_v1_v1_id_takes_priority_over_config() {
773        let mut v1_b = make_v1();
774        v1_b.id = Identifier::from([5u8; 32]);
775        let mut cfg = DataContractConfigV1::default();
776        cfg.readonly = !cfg.readonly;
777        v1_b.config = DataContractConfig::V1(cfg);
778        let a = DataContractInSerializationFormat::V1(make_v1());
779        let b = DataContractInSerializationFormat::V1(v1_b);
780        // Id is checked before config
781        assert_eq!(a.first_mismatch(&b), Some(DataContractMismatch::Id));
782    }
783
784    // -----------------------------------------------------------------------
785    // DataContractMismatch Display
786    // -----------------------------------------------------------------------
787
788    #[test]
789    fn data_contract_mismatch_display() {
790        assert_eq!(format!("{}", DataContractMismatch::Id), "ID fields differ");
791        assert_eq!(
792            format!("{}", DataContractMismatch::FormatVersionMismatch),
793            "Serialization format versions differ (e.g., V0 vs V1)"
794        );
795        assert_eq!(
796            format!("{}", DataContractMismatch::V0Mismatch),
797            "V0 versions differ"
798        );
799        assert_eq!(format!("{}", DataContractMismatch::Tokens), "Tokens differ");
800        assert_eq!(
801            format!("{}", DataContractMismatch::Keywords),
802            "Keywords differ"
803        );
804        assert_eq!(
805            format!("{}", DataContractMismatch::Description),
806            "Description fields differ"
807        );
808    }
809
810    // -----------------------------------------------------------------------
811    // Accessor methods
812    // -----------------------------------------------------------------------
813
814    #[test]
815    fn accessor_id_v0() {
816        let v0 = make_v0();
817        let expected_id = v0.id;
818        let format = DataContractInSerializationFormat::V0(v0);
819        assert_eq!(format.id(), expected_id);
820    }
821
822    #[test]
823    fn accessor_id_v1() {
824        let v1 = make_v1();
825        let expected_id = v1.id;
826        let format = DataContractInSerializationFormat::V1(v1);
827        assert_eq!(format.id(), expected_id);
828    }
829
830    #[test]
831    fn accessor_owner_id_v0() {
832        let mut v0 = make_v0();
833        v0.owner_id = Identifier::from([7u8; 32]);
834        let expected = v0.owner_id;
835        let format = DataContractInSerializationFormat::V0(v0);
836        assert_eq!(format.owner_id(), expected);
837    }
838
839    #[test]
840    fn accessor_version_v0() {
841        let mut v0 = make_v0();
842        v0.version = 10;
843        let format = DataContractInSerializationFormat::V0(v0);
844        assert_eq!(format.version(), 10);
845    }
846
847    #[test]
848    fn accessor_version_v1() {
849        let mut v1 = make_v1();
850        v1.version = 20;
851        let format = DataContractInSerializationFormat::V1(v1);
852        assert_eq!(format.version(), 20);
853    }
854
855    #[test]
856    fn accessor_groups_v0_returns_empty() {
857        let format = DataContractInSerializationFormat::V0(make_v0());
858        assert!(format.groups().is_empty());
859    }
860
861    #[test]
862    fn accessor_tokens_v0_returns_empty() {
863        let format = DataContractInSerializationFormat::V0(make_v0());
864        assert!(format.tokens().is_empty());
865    }
866
867    #[test]
868    fn accessor_keywords_v0_returns_empty() {
869        let format = DataContractInSerializationFormat::V0(make_v0());
870        assert!(format.keywords().is_empty());
871    }
872
873    #[test]
874    fn accessor_description_v0_returns_none() {
875        let format = DataContractInSerializationFormat::V0(make_v0());
876        assert_eq!(format.description(), &None);
877    }
878
879    #[test]
880    fn accessor_keywords_v1() {
881        let mut v1 = make_v1();
882        v1.keywords = vec!["hello".to_string()];
883        let format = DataContractInSerializationFormat::V1(v1);
884        assert_eq!(format.keywords(), &vec!["hello".to_string()]);
885    }
886
887    #[test]
888    fn accessor_description_v1_some() {
889        let mut v1 = make_v1();
890        v1.description = Some("desc".to_string());
891        let format = DataContractInSerializationFormat::V1(v1);
892        assert_eq!(format.description(), &Some("desc".to_string()));
893    }
894
895    #[test]
896    fn accessor_document_schemas_v0() {
897        let mut v0 = make_v0();
898        v0.document_schemas
899            .insert("note".to_string(), Value::Bool(true));
900        let format = DataContractInSerializationFormat::V0(v0);
901        assert_eq!(format.document_schemas().len(), 1);
902        assert!(format.document_schemas().contains_key("note"));
903    }
904
905    #[test]
906    fn accessor_schema_defs_v0_none() {
907        let format = DataContractInSerializationFormat::V0(make_v0());
908        assert!(format.schema_defs().is_none());
909    }
910
911    #[test]
912    fn accessor_schema_defs_v1_some() {
913        let mut v1 = make_v1();
914        let mut defs = BTreeMap::new();
915        defs.insert("def1".to_string(), Value::Null);
916        v1.schema_defs = Some(defs);
917        let format = DataContractInSerializationFormat::V1(v1);
918        assert!(format.schema_defs().is_some());
919        assert!(format.schema_defs().unwrap().contains_key("def1"));
920    }
921
922    // -----------------------------------------------------------------------
923    // TryFromPlatformVersioned: DataContractV0 -> DataContractInSerializationFormat
924    // -----------------------------------------------------------------------
925
926    #[test]
927    fn try_from_platform_versioned_data_contract_v0_version_0() {
928        let platform_version = PlatformVersion::first();
929        // V1 contract versions use default_current_version: 0
930        let v0 = DataContractV0 {
931            id: Identifier::from([10u8; 32]),
932            config: DataContractConfig::V0(DataContractConfigV0::default()),
933            version: 1,
934            owner_id: Identifier::from([20u8; 32]),
935            schema_defs: None,
936            document_types: BTreeMap::new(),
937            metadata: None,
938        };
939        let result = DataContractInSerializationFormat::try_from_platform_versioned(
940            v0.clone(),
941            platform_version,
942        );
943        assert!(result.is_ok());
944        let format = result.unwrap();
945        assert!(matches!(format, DataContractInSerializationFormat::V0(_)));
946        assert_eq!(format.id(), Identifier::from([10u8; 32]));
947        assert_eq!(format.owner_id(), Identifier::from([20u8; 32]));
948    }
949
950    #[test]
951    fn try_from_platform_versioned_data_contract_v0_ref_version_0() {
952        let platform_version = PlatformVersion::first();
953        let v0 = DataContractV0 {
954            id: Identifier::from([11u8; 32]),
955            config: DataContractConfig::V0(DataContractConfigV0::default()),
956            version: 2,
957            owner_id: Identifier::from([22u8; 32]),
958            schema_defs: None,
959            document_types: BTreeMap::new(),
960            metadata: None,
961        };
962        let result =
963            DataContractInSerializationFormat::try_from_platform_versioned(&v0, platform_version);
964        assert!(result.is_ok());
965        let format = result.unwrap();
966        assert!(matches!(format, DataContractInSerializationFormat::V0(_)));
967        assert_eq!(format.version(), 2);
968    }
969
970    #[test]
971    fn try_from_platform_versioned_data_contract_v0_version_1() {
972        let platform_version = PlatformVersion::latest();
973        // Latest uses default_current_version: 1
974        let v0 = DataContractV0 {
975            id: Identifier::from([10u8; 32]),
976            config: DataContractConfig::V0(DataContractConfigV0::default()),
977            version: 1,
978            owner_id: Identifier::from([20u8; 32]),
979            schema_defs: None,
980            document_types: BTreeMap::new(),
981            metadata: None,
982        };
983        let result = DataContractInSerializationFormat::try_from_platform_versioned(
984            v0.clone(),
985            platform_version,
986        );
987        assert!(result.is_ok());
988        let format = result.unwrap();
989        assert!(matches!(format, DataContractInSerializationFormat::V1(_)));
990    }
991
992    // -----------------------------------------------------------------------
993    // TryFromPlatformVersioned: DataContractV1 -> DataContractInSerializationFormat
994    // -----------------------------------------------------------------------
995
996    #[test]
997    fn try_from_platform_versioned_data_contract_v1_version_0() {
998        let platform_version = PlatformVersion::first();
999        let v1 = DataContractV1 {
1000            id: Identifier::from([10u8; 32]),
1001            config: DataContractConfig::V0(DataContractConfigV0::default()),
1002            version: 1,
1003            owner_id: Identifier::from([20u8; 32]),
1004            schema_defs: None,
1005            document_types: BTreeMap::new(),
1006            created_at: None,
1007            updated_at: None,
1008            created_at_block_height: None,
1009            updated_at_block_height: None,
1010            created_at_epoch: None,
1011            updated_at_epoch: None,
1012            groups: BTreeMap::new(),
1013            tokens: BTreeMap::new(),
1014            keywords: vec![],
1015            description: None,
1016        };
1017        let result = DataContractInSerializationFormat::try_from_platform_versioned(
1018            v1.clone(),
1019            platform_version,
1020        );
1021        assert!(result.is_ok());
1022        let format = result.unwrap();
1023        assert!(matches!(format, DataContractInSerializationFormat::V0(_)));
1024    }
1025
1026    #[test]
1027    fn try_from_platform_versioned_data_contract_v1_version_1() {
1028        let platform_version = PlatformVersion::latest();
1029        let v1 = DataContractV1 {
1030            id: Identifier::from([10u8; 32]),
1031            config: DataContractConfig::V1(DataContractConfigV1::default()),
1032            version: 1,
1033            owner_id: Identifier::from([20u8; 32]),
1034            schema_defs: None,
1035            document_types: BTreeMap::new(),
1036            created_at: None,
1037            updated_at: None,
1038            created_at_block_height: None,
1039            updated_at_block_height: None,
1040            created_at_epoch: None,
1041            updated_at_epoch: None,
1042            groups: BTreeMap::new(),
1043            tokens: BTreeMap::new(),
1044            keywords: vec![],
1045            description: None,
1046        };
1047        let result = DataContractInSerializationFormat::try_from_platform_versioned(
1048            v1.clone(),
1049            platform_version,
1050        );
1051        assert!(result.is_ok());
1052        let format = result.unwrap();
1053        assert!(matches!(format, DataContractInSerializationFormat::V1(_)));
1054    }
1055
1056    #[test]
1057    fn try_from_platform_versioned_data_contract_v1_ref_version_1() {
1058        let platform_version = PlatformVersion::latest();
1059        let v1 = DataContractV1 {
1060            id: Identifier::from([10u8; 32]),
1061            config: DataContractConfig::V1(DataContractConfigV1::default()),
1062            version: 3,
1063            owner_id: Identifier::from([20u8; 32]),
1064            schema_defs: None,
1065            document_types: BTreeMap::new(),
1066            created_at: None,
1067            updated_at: None,
1068            created_at_block_height: None,
1069            updated_at_block_height: None,
1070            created_at_epoch: None,
1071            updated_at_epoch: None,
1072            groups: BTreeMap::new(),
1073            tokens: BTreeMap::new(),
1074            keywords: vec![],
1075            description: None,
1076        };
1077        let result =
1078            DataContractInSerializationFormat::try_from_platform_versioned(&v1, platform_version);
1079        assert!(result.is_ok());
1080        let format = result.unwrap();
1081        assert!(matches!(format, DataContractInSerializationFormat::V1(_)));
1082        assert_eq!(format.version(), 3);
1083    }
1084
1085    // -----------------------------------------------------------------------
1086    // TryFromPlatformVersioned: DataContract -> DataContractInSerializationFormat
1087    // -----------------------------------------------------------------------
1088
1089    #[test]
1090    fn try_from_platform_versioned_data_contract_ref_version_0() {
1091        let platform_version = PlatformVersion::first();
1092        let contract = DataContract::V0(DataContractV0 {
1093            id: Identifier::from([10u8; 32]),
1094            config: DataContractConfig::V0(DataContractConfigV0::default()),
1095            version: 1,
1096            owner_id: Identifier::from([20u8; 32]),
1097            schema_defs: None,
1098            document_types: BTreeMap::new(),
1099            metadata: None,
1100        });
1101        let result = DataContractInSerializationFormat::try_from_platform_versioned(
1102            &contract,
1103            platform_version,
1104        );
1105        assert!(result.is_ok());
1106        assert!(matches!(
1107            result.unwrap(),
1108            DataContractInSerializationFormat::V0(_)
1109        ));
1110    }
1111
1112    #[test]
1113    fn try_from_platform_versioned_data_contract_owned_version_1() {
1114        let platform_version = PlatformVersion::latest();
1115        let contract = DataContract::V0(DataContractV0 {
1116            id: Identifier::from([10u8; 32]),
1117            config: DataContractConfig::V0(DataContractConfigV0::default()),
1118            version: 1,
1119            owner_id: Identifier::from([20u8; 32]),
1120            schema_defs: None,
1121            document_types: BTreeMap::new(),
1122            metadata: None,
1123        });
1124        let result = DataContractInSerializationFormat::try_from_platform_versioned(
1125            contract,
1126            platform_version,
1127        );
1128        assert!(result.is_ok());
1129        assert!(matches!(
1130            result.unwrap(),
1131            DataContractInSerializationFormat::V1(_)
1132        ));
1133    }
1134
1135    // -----------------------------------------------------------------------
1136    // Verify serialization version routing
1137    // -----------------------------------------------------------------------
1138
1139    #[test]
1140    fn first_platform_version_uses_serialization_version_0() {
1141        let pv = PlatformVersion::first();
1142        assert_eq!(
1143            pv.dpp
1144                .contract_versions
1145                .contract_serialization_version
1146                .default_current_version,
1147            0
1148        );
1149    }
1150
1151    #[test]
1152    fn latest_platform_version_uses_serialization_version_1() {
1153        let pv = PlatformVersion::latest();
1154        assert_eq!(
1155            pv.dpp
1156                .contract_versions
1157                .contract_serialization_version
1158                .default_current_version,
1159            1
1160        );
1161    }
1162
1163    #[test]
1164    fn first_platform_version_uses_contract_structure_0() {
1165        let pv = PlatformVersion::first();
1166        assert_eq!(pv.dpp.contract_versions.contract_structure_version, 0);
1167    }
1168
1169    #[test]
1170    fn latest_platform_version_uses_contract_structure_1() {
1171        let pv = PlatformVersion::latest();
1172        assert_eq!(pv.dpp.contract_versions.contract_structure_version, 1);
1173    }
1174}
1175
1176#[cfg(all(
1177    test,
1178    feature = "json-conversion",
1179    feature = "value-conversion",
1180    feature = "serde-conversion"
1181))]
1182mod json_convertible_tests {
1183    use super::*;
1184    use crate::data_contract::config::v0::DataContractConfigV0;
1185    use crate::data_contract::config::DataContractConfig;
1186    use crate::data_contract::serialized_version::v0::DataContractInSerializationFormatV0;
1187    use platform_value::Identifier;
1188    use std::collections::BTreeMap;
1189
1190    fn fixture() -> DataContractInSerializationFormat {
1191        DataContractInSerializationFormat::V0(DataContractInSerializationFormatV0 {
1192            id: Identifier::new([0xa1; 32]),
1193            config: DataContractConfig::V0(DataContractConfigV0::default()),
1194            version: 1,
1195            owner_id: Identifier::new([0xb2; 32]),
1196            schema_defs: None,
1197            document_schemas: BTreeMap::new(),
1198        })
1199    }
1200
1201    #[test]
1202    fn json_round_trip_with_full_wire_shape() {
1203        use crate::serialization::JsonConvertible;
1204        use serde_json::json;
1205        let original = fixture();
1206        let json = original.to_json().expect("to_json");
1207        // Tier 3 envelope-only: `DataContractInSerializationFormat` embeds a
1208        // versioned `DataContractConfig` and arbitrary `document_schemas` /
1209        // `schema_defs` Values. The full inline expansion is verified for the
1210        // `DataContractConfig` in its own module. We still pin the top-level
1211        // envelope keys + their types here so that any silent drop / rename /
1212        // re-keying at this layer would fail the test.
1213        assert_eq!(json["$formatVersion"], "0");
1214        assert_eq!(
1215            json["id"],
1216            json!("Bswb3UyeD1pUTaGiE6WvqwFpJZsQSEY1xhJePCDTHdvp")
1217        );
1218        assert_eq!(
1219            json["ownerId"],
1220            json!("D2ZcUbtpG5sKq7XLeB4YnpNnTGSptKCxTddoNeydzJQq")
1221        );
1222        assert_eq!(json["version"], json!(1));
1223        assert_eq!(json["schemaDefs"], json!(null));
1224        assert_eq!(json["documentSchemas"], json!({}));
1225        assert!(json.get("config").is_some(), "config envelope present");
1226        assert_eq!(json["config"]["$formatVersion"], "0");
1227        let recovered = DataContractInSerializationFormat::from_json(json).expect("from_json");
1228        assert_eq!(original, recovered);
1229    }
1230
1231    #[test]
1232    fn value_round_trip_with_full_wire_shape() {
1233        use crate::serialization::ValueConvertible;
1234        use platform_value::Value;
1235        let original = fixture();
1236        let value = original.to_object().expect("to_object");
1237        // Tier 3 envelope-only: see JSON test above. Keys remain `Identifier` /
1238        // `Map` / typed integers in non-HR mode (no base58 stringification).
1239        let map = match &value {
1240            Value::Map(m) => m,
1241            other => panic!("expected Value::Map, got {:?}", other),
1242        };
1243        let get = |k: &str| -> &Value {
1244            map.iter()
1245                .find(|(key, _)| matches!(key, Value::Text(t) if t == k))
1246                .map(|(_, v)| v)
1247                .unwrap_or_else(|| panic!("missing key {k}"))
1248        };
1249        assert_eq!(get("$formatVersion"), &Value::Text("0".to_string()));
1250        assert_eq!(get("id"), &Value::Identifier([0xa1; 32]));
1251        assert_eq!(get("ownerId"), &Value::Identifier([0xb2; 32]));
1252        assert_eq!(get("version"), &Value::U32(1));
1253        assert_eq!(get("schemaDefs"), &Value::Null);
1254        // documentSchemas: empty Map
1255        assert!(matches!(get("documentSchemas"), Value::Map(m) if m.is_empty()));
1256        // config: nested Map with its own $formatVersion="0"
1257        assert!(matches!(get("config"), Value::Map(_)));
1258        let recovered = DataContractInSerializationFormat::from_object(value).expect("from_object");
1259        assert_eq!(original, recovered);
1260    }
1261
1262    #[test]
1263    fn json_preserves_format_version_tag() {
1264        use crate::serialization::JsonConvertible;
1265        let json = fixture().to_json().expect("to_json");
1266        assert_eq!(json["$formatVersion"], "0");
1267    }
1268}