Skip to main content

dpp/data_contract/serialized_version/
mod.rs

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