Skip to main content

sui_sdk_types/
object.rs

1use std::collections::BTreeMap;
2
3use super::Address;
4use super::Digest;
5use super::Identifier;
6use super::StructTag;
7
8pub type Version = u64;
9
10/// Reference to an object
11///
12/// Contains sufficient information to uniquely identify a specific object.
13///
14/// # BCS
15///
16/// The BCS serialized form for this type is defined by the following ABNF:
17///
18/// ```text
19/// object-ref = address u64 digest
20/// ```
21#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
22#[cfg_attr(
23    feature = "serde",
24    derive(serde_derive::Serialize, serde_derive::Deserialize)
25)]
26#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
27pub struct ObjectReference {
28    /// The object id of this object.
29    object_id: Address,
30    /// The version of this object.
31    version: Version,
32    /// The digest of this object.
33    digest: Digest,
34}
35
36impl ObjectReference {
37    /// Creates a new object reference from the object's id, version, and digest.
38    pub fn new(object_id: Address, version: Version, digest: Digest) -> Self {
39        Self {
40            object_id,
41            version,
42            digest,
43        }
44    }
45
46    /// Returns a reference to the object id that this ObjectReference is referring to.
47    pub fn object_id(&self) -> &Address {
48        &self.object_id
49    }
50
51    /// Returns the version of the object that this ObjectReference is referring to.
52    pub fn version(&self) -> Version {
53        self.version
54    }
55
56    /// Returns the digest of the object that this ObjectReference is referring to.
57    pub fn digest(&self) -> &Digest {
58        &self.digest
59    }
60
61    /// Construct a synthetic coin reservation reference.
62    ///
63    /// The reservation ref encodes an address-balance reservation for coin
64    /// usage so that the server can reserve the specified `balance` of the
65    /// sender's address balance and transparently create a `Coin<T>` without
66    /// requiring actual coin objects.
67    ///
68    /// See the [coin reservation protocol][cr] for the full specification.
69    ///
70    /// [cr]: https://github.com/mystenlabs/sui/blob/main/crates/sui-types/src/coin_reservation.rs
71    #[cfg(all(feature = "hash", feature = "serde"))]
72    #[cfg_attr(doc_cfg, doc(cfg(all(feature = "hash", feature = "serde"))))]
73    pub fn coin_reservation(
74        coin_type: &crate::StructTag,
75        balance: u64,
76        epoch: u64,
77        chain_id: Digest,
78        owner: Address,
79    ) -> Self {
80        use super::Identifier;
81
82        // Derive the accumulator object ID for (owner, Balance<SUI>).
83        //
84        // The parent is the accumulator root object (0xacc). The key is
85        // the BCS-serialized owner address. The key type tag is
86        // `accumulator::Key<balance::Balance<SUI>>`.
87        let accumulator_root = const { Address::from_static("0xacc") };
88
89        let balance_sui_type = StructTag::new(
90            Address::TWO,
91            Identifier::from_static("balance"),
92            Identifier::from_static("Balance"),
93            vec![coin_type.clone().into()],
94        );
95        let key_type_tag: super::TypeTag = StructTag::new(
96            Address::TWO,
97            Identifier::from_static("accumulator"),
98            Identifier::from_static("Key"),
99            vec![balance_sui_type.into()],
100        )
101        .into();
102
103        let object_id = accumulator_root.derive_dynamic_child_id(&key_type_tag, owner.as_ref());
104
105        // XOR-mask the object ID with the chain identifier to prevent
106        // cross-chain replay.
107        let masked_id = {
108            let id_bytes = object_id.into_inner();
109            let mask_bytes = chain_id.into_inner();
110            let mut masked = [0u8; 32];
111            for i in 0..32 {
112                masked[i] = id_bytes[i] ^ mask_bytes[i];
113            }
114            Address::new(masked)
115        };
116
117        // Construct the magic digest:
118        //   bytes  0-7:  reservation balance (LE u64)
119        //   bytes  8-11: epoch ID (LE u32)
120        //   bytes 12-31: magic constant [0xac; 20]
121        let digest = {
122            let mut bytes = [0u8; 32];
123            bytes[0..8].copy_from_slice(&balance.to_le_bytes());
124            bytes[8..12].copy_from_slice(&(epoch as u32).to_le_bytes());
125            bytes[12..32].copy_from_slice(&[0xac; 20]);
126            Digest::new(bytes)
127        };
128
129        Self::new(masked_id, 0, digest)
130    }
131
132    /// Returns a 3-tuple containing the object id, version, and digest.
133    pub fn into_parts(self) -> (Address, Version, Digest) {
134        let Self {
135            object_id,
136            version,
137            digest,
138        } = self;
139
140        (object_id, version, digest)
141    }
142}
143
144/// Enum of different types of ownership for an object.
145///
146/// # BCS
147///
148/// The BCS serialized form for this type is defined by the following ABNF:
149///
150/// ```text
151/// owner = owner-address / owner-object / owner-shared / owner-immutable
152///
153/// owner-address   = %x00 address
154/// owner-object    = %x01 address
155/// owner-shared    = %x02 u64
156/// owner-immutable = %x03
157/// ```
158#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
159#[cfg_attr(
160    feature = "serde",
161    derive(serde_derive::Serialize, serde_derive::Deserialize),
162    serde(rename_all = "lowercase")
163)]
164#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
165#[non_exhaustive]
166pub enum Owner {
167    /// Object is exclusively owned by a single address, and is mutable.
168    Address(Address),
169    /// Object is exclusively owned by a single object, and is mutable.
170    Object(Address),
171    /// Object is shared, can be used by any address, and is mutable.
172    Shared(
173        /// The version at which the object became shared
174        Version,
175    ),
176    /// Object is immutable, and hence ownership doesn't matter.
177    Immutable,
178
179    /// Object is exclusively owned by a single address and sequenced via consensus.
180    ConsensusAddress {
181        /// The version at which the object most recently became a consensus object.
182        /// This serves the same function as `initial_shared_version`, except it may change
183        /// if the object's Owner type changes.
184        start_version: Version,
185
186        /// The owner of the object.
187        owner: Address,
188    },
189}
190
191/// Object data, either a package or struct
192///
193/// # BCS
194///
195/// The BCS serialized form for this type is defined by the following ABNF:
196///
197/// ```text
198/// object-data = object-data-struct / object-data-package
199///
200/// object-data-struct  = %x00 object-move-struct
201/// object-data-package = %x01 object-move-package
202/// ```
203#[derive(Clone, Debug, PartialEq, Eq, Hash)]
204#[cfg_attr(
205    feature = "serde",
206    derive(serde_derive::Serialize, serde_derive::Deserialize)
207)]
208#[allow(clippy::large_enum_variant)]
209#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
210//TODO think about hiding this type and not exposing it
211pub enum ObjectData {
212    /// An object whose governing logic lives in a published Move module
213    Struct(MoveStruct),
214    /// Map from each module name to raw serialized Move module bytes
215    Package(MovePackage),
216    // ... Sui "native" types go here
217}
218
219/// A move package
220///
221/// # BCS
222///
223/// The BCS serialized form for this type is defined by the following ABNF:
224///
225/// ```text
226/// object-move-package = address u64 move-modules type-origin-table linkage-table
227///
228/// move-modules = map (identifier bytes)
229/// type-origin-table = vector type-origin
230/// linkage-table = map (address upgrade-info)
231/// ```
232#[derive(Eq, PartialEq, Debug, Clone, Hash)]
233#[cfg_attr(
234    feature = "serde",
235    derive(serde_derive::Serialize, serde_derive::Deserialize)
236)]
237#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
238pub struct MovePackage {
239    /// Address or Id of this package
240    pub id: Address,
241
242    /// Most move packages are uniquely identified by their ID (i.e. there is only one version per
243    /// ID), but the version is still stored because one package may be an upgrade of another (at a
244    /// different ID), in which case its version will be one greater than the version of the
245    /// upgraded package.
246    ///
247    /// Framework packages are an exception to this rule -- all versions of the framework packages
248    /// exist at the same ID, at increasing versions.
249    ///
250    /// In all cases, packages are referred to by move calls using just their ID, and they are
251    /// always loaded at their latest version.
252    pub version: Version,
253
254    /// Set of modules defined by this package
255    #[cfg_attr(
256        feature = "serde",
257        serde(with = "::serde_with::As::<BTreeMap<::serde_with::Same, ::serde_with::Bytes>>")
258    )]
259    #[cfg_attr(
260        feature = "proptest",
261        strategy(
262            proptest::collection::btree_map(proptest::arbitrary::any::<Identifier>(), proptest::collection::vec(proptest::arbitrary::any::<u8>(), 0..=1024), 0..=5)
263        )
264    )]
265    pub modules: BTreeMap<Identifier, Vec<u8>>,
266
267    /// Maps struct/module to a package version where it was first defined, stored as a vector for
268    /// simple serialization and deserialization.
269    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(0..=1).lift()))]
270    pub type_origin_table: Vec<TypeOrigin>,
271
272    /// For each dependency, maps original package ID to the info about the (upgraded) dependency
273    /// version that this package is using
274    #[cfg_attr(
275        feature = "proptest",
276        strategy(
277            proptest::collection::btree_map(proptest::arbitrary::any::<Address>(), proptest::arbitrary::any::<UpgradeInfo>(), 0..=5)
278        )
279    )]
280    pub linkage_table: BTreeMap<Address, UpgradeInfo>,
281}
282
283/// Identifies a struct and the module it was defined in
284///
285/// # BCS
286///
287/// The BCS serialized form for this type is defined by the following ABNF:
288///
289/// ```text
290/// type-origin = identifier identifier address
291/// ```
292#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
293#[cfg_attr(
294    feature = "serde",
295    derive(serde_derive::Serialize, serde_derive::Deserialize)
296)]
297#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
298pub struct TypeOrigin {
299    pub module_name: Identifier,
300    pub struct_name: Identifier,
301    pub package: Address,
302}
303
304/// Upgraded package info for the linkage table
305///
306/// # BCS
307///
308/// The BCS serialized form for this type is defined by the following ABNF:
309///
310/// ```text
311/// upgrade-info = address u64
312/// ```
313#[derive(Eq, PartialEq, Debug, Clone, Hash)]
314#[cfg_attr(
315    feature = "serde",
316    derive(serde_derive::Serialize, serde_derive::Deserialize)
317)]
318#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
319pub struct UpgradeInfo {
320    /// Id of the upgraded packages
321    pub upgraded_id: Address,
322    /// Version of the upgraded package
323    pub upgraded_version: Version,
324}
325
326/// A move struct
327///
328/// # BCS
329///
330/// The BCS serialized form for this type is defined by the following ABNF:
331///
332/// ```text
333/// object-move-struct = compressed-struct-tag bool u64 object-contents
334///
335/// compressed-struct-tag = other-struct-type / gas-coin-type / staked-sui-type / coin-type
336/// other-struct-type     = %x00 struct-tag
337/// gas-coin-type         = %x01
338/// staked-sui-type       = %x02
339/// coin-type             = %x03 type-tag
340///
341/// ; first 32 bytes of the contents are the object's address
342/// object-contents = uleb128 (address *OCTET) ; length followed by contents
343/// ```
344#[derive(Eq, PartialEq, Debug, Clone, Hash)]
345#[cfg_attr(feature = "serde", derive(serde_derive::Serialize))]
346#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
347pub struct MoveStruct {
348    /// The type of this object
349    #[cfg_attr(
350        feature = "serde",
351        serde(with = "::serde_with::As::<serialization::BinaryMoveStructType>")
352    )]
353    pub(crate) type_: StructTag,
354
355    /// DEPRECATED this field is no longer used to determine whether a tx can transfer this
356    /// object. Instead, it is always calculated from the objects type when loaded in execution
357    has_public_transfer: bool,
358
359    /// Number that increases each time a tx takes this object as a mutable input
360    /// This is a lamport timestamp, not a sequentially increasing version
361    version: Version,
362
363    /// BCS bytes of a Move struct value
364    #[cfg_attr(
365        feature = "serde",
366        serde(with = "crate::_serde::ReadableBase64Encoded")
367    )]
368    #[cfg_attr(feature = "proptest", any(proptest::collection::size_range(32..=1024).lift()))]
369    pub(crate) contents: Vec<u8>,
370}
371
372impl MoveStruct {
373    /// Construct a move struct
374    pub fn new(
375        type_: StructTag,
376        has_public_transfer: bool,
377        version: Version,
378        contents: Vec<u8>,
379    ) -> Option<Self> {
380        id_opt(&contents).map(|_| Self {
381            type_,
382            has_public_transfer,
383            version,
384            contents,
385        })
386    }
387
388    /// Return the type of the struct
389    pub fn object_type(&self) -> &StructTag {
390        &self.type_
391    }
392
393    /// Return if this object can be publicly transferred
394    ///
395    /// DEPRECATED
396    ///
397    /// This field is no longer used to determine whether a tx can transfer this object. Instead,
398    /// it is always calculated from the objects type when loaded in execution.
399    #[doc(hidden)]
400    pub fn has_public_transfer(&self) -> bool {
401        self.has_public_transfer
402    }
403
404    /// Return the version of this object
405    pub fn version(&self) -> Version {
406        self.version
407    }
408
409    /// Return the raw contents of this struct
410    pub fn contents(&self) -> &[u8] {
411        &self.contents
412    }
413
414    /// Return the ObjectId of this object
415    pub fn object_id(&self) -> Address {
416        id_opt(self.contents()).unwrap()
417    }
418}
419
420/// Type of a Sui object
421#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Debug)]
422pub enum ObjectType {
423    /// Move package containing one or more bytecode modules
424    Package,
425    /// A Move struct of the given type
426    Struct(StructTag),
427}
428
429/// An object on the sui blockchain
430///
431/// # BCS
432///
433/// The BCS serialized form for this type is defined by the following ABNF:
434///
435/// ```text
436/// object = object-data owner digest u64
437/// ```
438#[derive(Clone, Debug, PartialEq, Eq)]
439#[cfg_attr(
440    feature = "serde",
441    derive(serde_derive::Serialize, serde_derive::Deserialize)
442)]
443#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
444pub struct Object {
445    /// The meat of the object
446    pub(crate) data: ObjectData,
447
448    /// The owner that unlocks this object
449    owner: Owner,
450
451    /// The digest of the transaction that created or last mutated this object
452    previous_transaction: Digest,
453
454    /// The amount of SUI we would rebate if this object gets deleted.
455    /// This number is re-calculated each time the object is mutated based on
456    /// the present storage gas price.
457    storage_rebate: u64,
458}
459
460impl Object {
461    /// Build an object
462    pub fn new(
463        data: ObjectData,
464        owner: Owner,
465        previous_transaction: Digest,
466        storage_rebate: u64,
467    ) -> Self {
468        Self {
469            data,
470            owner,
471            previous_transaction,
472            storage_rebate,
473        }
474    }
475
476    /// Return this object's id
477    pub fn object_id(&self) -> Address {
478        match &self.data {
479            ObjectData::Struct(struct_) => id_opt(&struct_.contents).unwrap(),
480            ObjectData::Package(package) => package.id,
481        }
482    }
483
484    /// Return this object's version
485    pub fn version(&self) -> Version {
486        match &self.data {
487            ObjectData::Struct(struct_) => struct_.version,
488            ObjectData::Package(package) => package.version,
489        }
490    }
491
492    /// Return this object's type
493    pub fn object_type(&self) -> ObjectType {
494        match &self.data {
495            ObjectData::Struct(struct_) => ObjectType::Struct(struct_.type_.clone()),
496            ObjectData::Package(_) => ObjectType::Package,
497        }
498    }
499
500    /// Try to interpret this object as a move struct
501    pub fn as_struct(&self) -> Option<&MoveStruct> {
502        match &self.data {
503            ObjectData::Struct(struct_) => Some(struct_),
504            _ => None,
505        }
506    }
507
508    /// Return this object's owner
509    pub fn owner(&self) -> &Owner {
510        &self.owner
511    }
512
513    /// Return this object's data
514    pub fn data(&self) -> &ObjectData {
515        &self.data
516    }
517
518    /// Return the digest of the transaction that last modified this object
519    pub fn previous_transaction(&self) -> Digest {
520        self.previous_transaction
521    }
522
523    /// Return the storage rebate locked in this object
524    ///
525    /// Storage rebates are credited to the gas coin used in a transaction that deletes this
526    /// object.
527    pub fn storage_rebate(&self) -> u64 {
528        self.storage_rebate
529    }
530}
531
532fn id_opt(contents: &[u8]) -> Option<Address> {
533    if Address::LENGTH > contents.len() {
534        return None;
535    }
536
537    Some(Address::from_bytes(&contents[..Address::LENGTH]).unwrap())
538}
539
540/// An object part of the initial chain state
541///
542/// `GenesisObject`'s are included as a part of genesis, the initial checkpoint/transaction, that
543/// initializes the state of the blockchain.
544///
545/// # BCS
546///
547/// The BCS serialized form for this type is defined by the following ABNF:
548///
549/// ```text
550/// genesis-object = object-data owner
551/// ```
552#[derive(Clone, Debug, PartialEq, Eq)]
553#[cfg_attr(feature = "proptest", derive(test_strategy::Arbitrary))]
554pub struct GenesisObject {
555    data: ObjectData,
556    owner: Owner,
557}
558
559impl GenesisObject {
560    pub fn new(data: ObjectData, owner: Owner) -> Self {
561        Self { data, owner }
562    }
563
564    pub fn object_id(&self) -> Address {
565        match &self.data {
566            ObjectData::Struct(struct_) => id_opt(&struct_.contents).unwrap(),
567            ObjectData::Package(package) => package.id,
568        }
569    }
570
571    pub fn version(&self) -> Version {
572        match &self.data {
573            ObjectData::Struct(struct_) => struct_.version,
574            ObjectData::Package(package) => package.version,
575        }
576    }
577
578    pub fn object_type(&self) -> ObjectType {
579        match &self.data {
580            ObjectData::Struct(struct_) => ObjectType::Struct(struct_.type_.clone()),
581            ObjectData::Package(_) => ObjectType::Package,
582        }
583    }
584
585    pub fn owner(&self) -> &Owner {
586        &self.owner
587    }
588
589    pub fn data(&self) -> &ObjectData {
590        &self.data
591    }
592}
593
594//TODO improve ser/de to do borrowing to avoid clones where possible
595#[cfg(feature = "serde")]
596#[cfg_attr(doc_cfg, doc(cfg(feature = "serde")))]
597mod serialization {
598    use serde::Deserialize;
599    use serde::Deserializer;
600    use serde::Serialize;
601    use serde::Serializer;
602    use serde_with::DeserializeAs;
603    use serde_with::SerializeAs;
604
605    use super::*;
606    use crate::TypeTag;
607
608    /// Wrapper around StructTag with a space-efficient representation for common types like coins
609    /// The StructTag for a gas coin is 84 bytes, so using 1 byte instead is a win.
610    /// The inner representation is private to prevent incorrectly constructing an `Other` instead of
611    /// one of the specialized variants, e.g. `Other(GasCoin::type_())` instead of `GasCoin`
612    #[derive(serde_derive::Deserialize)]
613    enum MoveStructType {
614        /// A type that is not `0x2::coin::Coin<T>`
615        Other(StructTag),
616        /// A SUI coin (i.e., `0x2::coin::Coin<0x2::sui::SUI>`)
617        GasCoin,
618        /// A record of a staked SUI coin (i.e., `0x3::staking_pool::StakedSui`)
619        StakedSui,
620        /// A non-SUI coin type (i.e., `0x2::coin::Coin<T> where T != 0x2::sui::SUI`)
621        Coin(TypeTag),
622        /// A SUI balance accumulator field
623        /// (i.e., `0x2::dynamic_field::Field<0x2::accumulator::Key<0x2::balance::Balance<0x2::sui::SUI>>, 0x2::accumulator::U128>`)
624        SuiBalanceAccumulatorField,
625        /// A non-SUI balance accumulator field
626        /// (i.e., `0x2::dynamic_field::Field<0x2::accumulator::Key<0x2::balance::Balance<T>>, 0x2::accumulator::U128>`
627        /// where T != 0x2::sui::SUI)
628        BalanceAccumulatorField(TypeTag),
629    }
630
631    /// See `MoveStructType`
632    #[derive(serde_derive::Serialize)]
633    enum MoveStructTypeRef<'a> {
634        /// A type that is not `0x2::coin::Coin<T>`
635        Other(&'a StructTag),
636        /// A SUI coin (i.e., `0x2::coin::Coin<0x2::sui::SUI>`)
637        GasCoin,
638        /// A record of a staked SUI coin (i.e., `0x3::staking_pool::StakedSui`)
639        StakedSui,
640        /// A non-SUI coin type (i.e., `0x2::coin::Coin<T> where T != 0x2::sui::SUI`)
641        Coin(&'a TypeTag),
642        /// A SUI balance accumulator field
643        /// (i.e., `0x2::dynamic_field::Field<0x2::accumulator::Key<0x2::balance::Balance<0x2::sui::SUI>>, 0x2::accumulator::U128>`)
644        SuiBalanceAccumulatorField,
645        /// A non-SUI balance accumulator field
646        /// (i.e., `0x2::dynamic_field::Field<0x2::accumulator::Key<0x2::balance::Balance<T>>, 0x2::accumulator::U128>`
647        /// where T != 0x2::sui::SUI)
648        BalanceAccumulatorField(&'a TypeTag),
649    }
650
651    impl MoveStructType {
652        fn into_struct_tag(self) -> StructTag {
653            match self {
654                MoveStructType::Other(tag) => tag,
655                MoveStructType::GasCoin => StructTag::gas_coin(),
656                MoveStructType::StakedSui => StructTag::staked_sui(),
657                MoveStructType::Coin(type_tag) => StructTag::coin(type_tag),
658                MoveStructType::SuiBalanceAccumulatorField => {
659                    StructTag::balance_accumulator_field(StructTag::sui().into())
660                }
661                MoveStructType::BalanceAccumulatorField(type_tag) => {
662                    StructTag::balance_accumulator_field(type_tag)
663                }
664            }
665        }
666
667        /// Wire-format variant index. Used to compare a parsed
668        /// `MoveStructType` against the canonical encoding produced by
669        /// `MoveStructTypeRef::from_struct_tag`.
670        fn variant_index(&self) -> u8 {
671            match self {
672                Self::Other(_) => 0,
673                Self::GasCoin => 1,
674                Self::StakedSui => 2,
675                Self::Coin(_) => 3,
676                Self::SuiBalanceAccumulatorField => 4,
677                Self::BalanceAccumulatorField(_) => 5,
678            }
679        }
680    }
681
682    impl<'a> MoveStructTypeRef<'a> {
683        fn from_struct_tag(s: &'a StructTag) -> Self {
684            let address = s.address();
685            let module = s.module();
686            let name = s.name();
687            let type_params = s.type_params();
688
689            if let Some(coin_type) = s.is_coin() {
690                if let TypeTag::Struct(s_inner) = coin_type
691                    && s_inner.is_gas()
692                {
693                    Self::GasCoin
694                } else {
695                    Self::Coin(coin_type)
696                }
697            } else if address == &Address::THREE
698                && module == "staking_pool"
699                && name == "StakedSui"
700                && type_params.is_empty()
701            {
702                Self::StakedSui
703            } else if let Some(coin_type) = s.is_balance_accumulator_field() {
704                if let TypeTag::Struct(s_inner) = coin_type
705                    && s_inner.is_gas()
706                {
707                    Self::SuiBalanceAccumulatorField
708                } else {
709                    Self::BalanceAccumulatorField(coin_type)
710                }
711            } else {
712                Self::Other(s)
713            }
714        }
715
716        fn variant_index(&self) -> u8 {
717            match self {
718                Self::Other(_) => 0,
719                Self::GasCoin => 1,
720                Self::StakedSui => 2,
721                Self::Coin(_) => 3,
722                Self::SuiBalanceAccumulatorField => 4,
723                Self::BalanceAccumulatorField(_) => 5,
724            }
725        }
726    }
727
728    pub(super) struct BinaryMoveStructType;
729
730    impl SerializeAs<StructTag> for BinaryMoveStructType {
731        fn serialize_as<S>(source: &StructTag, serializer: S) -> Result<S::Ok, S::Error>
732        where
733            S: Serializer,
734        {
735            let move_object_type = MoveStructTypeRef::from_struct_tag(source);
736            move_object_type.serialize(serializer)
737        }
738    }
739
740    impl<'de> DeserializeAs<'de, StructTag> for BinaryMoveStructType {
741        fn deserialize_as<D>(deserializer: D) -> Result<StructTag, D::Error>
742        where
743            D: Deserializer<'de>,
744        {
745            // Enforce that the wire form matches the canonical encoding for
746            // the resulting `StructTag`. Without this, the same logical
747            // value could be reached from multiple BCS byte strings (e.g.
748            // `Other(0x2::coin::Coin<0x2::sui::SUI>)` vs `GasCoin`), but
749            // re-serializing via `MoveStructTypeRef::from_struct_tag` would
750            // always emit the specialized form, breaking byte-faithful
751            // round-trips and any downstream digest computed over them.
752            let parsed = MoveStructType::deserialize(deserializer)?;
753            let parsed_idx = parsed.variant_index();
754            let tag = parsed.into_struct_tag();
755            let canonical_idx = MoveStructTypeRef::from_struct_tag(&tag).variant_index();
756            if parsed_idx != canonical_idx {
757                return Err(serde::de::Error::custom(format!(
758                    "non-canonical MoveStructType encoding: variant {parsed_idx} \
759                     would be re-encoded as variant {canonical_idx}",
760                )));
761            }
762            Ok(tag)
763        }
764    }
765
766    /// Mirror of `MoveStruct`'s fields used solely as a deserialization
767    /// target. Lets us validate `contents` after the wire decode but before
768    /// the value reaches the public type, where `object_id()` would otherwise
769    /// panic on truncated input.
770    #[derive(serde_derive::Deserialize)]
771    struct RawMoveStruct {
772        #[serde(with = "::serde_with::As::<BinaryMoveStructType>")]
773        type_: StructTag,
774        has_public_transfer: bool,
775        version: Version,
776        #[serde(with = "crate::_serde::ReadableBase64Encoded")]
777        contents: Vec<u8>,
778    }
779
780    impl<'de> Deserialize<'de> for MoveStruct {
781        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
782        where
783            D: Deserializer<'de>,
784        {
785            let RawMoveStruct {
786                type_,
787                has_public_transfer,
788                version,
789                contents,
790            } = RawMoveStruct::deserialize(deserializer)?;
791
792            if contents.len() < Address::LENGTH {
793                return Err(serde::de::Error::custom(format!(
794                    "MoveStruct contents must be at least {} bytes (object id), got {}",
795                    Address::LENGTH,
796                    contents.len(),
797                )));
798            }
799
800            Ok(MoveStruct {
801                type_,
802                has_public_transfer,
803                version,
804                contents,
805            })
806        }
807    }
808
809    #[derive(serde_derive::Serialize, serde_derive::Deserialize)]
810    enum BinaryGenesisObject {
811        RawObject { data: ObjectData, owner: Owner },
812    }
813
814    impl Serialize for GenesisObject {
815        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
816        where
817            S: Serializer,
818        {
819            let binary = BinaryGenesisObject::RawObject {
820                data: self.data.clone(),
821                owner: self.owner,
822            };
823            binary.serialize(serializer)
824        }
825    }
826
827    impl<'de> Deserialize<'de> for GenesisObject {
828        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
829        where
830            D: Deserializer<'de>,
831        {
832            let BinaryGenesisObject::RawObject { data, owner } =
833                Deserialize::deserialize(deserializer)?;
834
835            Ok(GenesisObject { data, owner })
836        }
837    }
838
839    #[cfg(test)]
840    mod test {
841        use crate::bcs::FromBcs;
842        use crate::bcs::ToBcs;
843        use crate::object::Object;
844
845        #[cfg(target_arch = "wasm32")]
846        use wasm_bindgen_test::wasm_bindgen_test as test;
847
848        #[test]
849        fn object_fixture() {
850            const SUI_COIN: &[u8] = &[
851                0, 1, 1, 32, 79, 43, 0, 0, 0, 0, 0, 40, 35, 95, 175, 213, 151, 87, 206, 190, 35,
852                131, 79, 35, 254, 22, 15, 181, 40, 108, 28, 77, 68, 229, 107, 254, 191, 160, 196,
853                186, 42, 2, 122, 53, 52, 133, 199, 58, 0, 0, 0, 0, 0, 79, 255, 208, 0, 85, 34, 190,
854                75, 192, 41, 114, 76, 127, 15, 110, 215, 9, 58, 107, 243, 160, 155, 144, 230, 47,
855                97, 220, 21, 24, 30, 26, 62, 32, 17, 197, 192, 38, 64, 173, 142, 143, 49, 111, 15,
856                211, 92, 84, 48, 160, 243, 102, 229, 253, 251, 137, 210, 101, 119, 173, 228, 51,
857                141, 20, 15, 85, 96, 19, 15, 0, 0, 0, 0, 0,
858            ];
859
860            const SUI_STAKE: &[u8] = &[
861                0, 2, 1, 154, 1, 52, 5, 0, 0, 0, 0, 80, 3, 112, 71, 231, 166, 234, 205, 164, 99,
862                237, 29, 56, 97, 170, 21, 96, 105, 158, 227, 122, 22, 251, 60, 162, 12, 97, 151,
863                218, 71, 253, 231, 239, 116, 138, 12, 233, 128, 195, 128, 77, 33, 38, 122, 77, 53,
864                154, 197, 198, 75, 212, 12, 182, 163, 224, 42, 82, 123, 69, 248, 40, 207, 143, 211,
865                13, 106, 1, 0, 0, 0, 0, 0, 0, 59, 81, 183, 246, 112, 0, 0, 0, 0, 79, 255, 208, 0,
866                85, 34, 190, 75, 192, 41, 114, 76, 127, 15, 110, 215, 9, 58, 107, 243, 160, 155,
867                144, 230, 47, 97, 220, 21, 24, 30, 26, 62, 32, 247, 239, 248, 71, 247, 102, 190,
868                149, 232, 153, 138, 67, 169, 209, 203, 29, 255, 215, 223, 57, 159, 44, 40, 218,
869                166, 13, 80, 71, 14, 188, 232, 68, 0, 0, 0, 0, 0, 0, 0, 0,
870            ];
871
872            const NFT: &[u8] = &[
873                0, 0, 97, 201, 195, 159, 216, 97, 133, 173, 96, 215, 56, 212, 229, 43, 208, 139,
874                218, 7, 29, 54, 106, 205, 224, 126, 7, 195, 145, 106, 45, 117, 168, 22, 12, 100,
875                105, 115, 116, 114, 105, 98, 117, 116, 105, 111, 110, 11, 68, 69, 69, 80, 87, 114,
876                97, 112, 112, 101, 114, 0, 0, 124, 24, 223, 4, 0, 0, 0, 0, 40, 31, 8, 18, 84, 38,
877                164, 252, 84, 115, 250, 246, 137, 132, 128, 186, 156, 36, 62, 18, 140, 21, 4, 90,
878                209, 105, 85, 84, 92, 214, 97, 81, 207, 64, 194, 198, 208, 21, 0, 0, 0, 0, 79, 255,
879                208, 0, 85, 34, 190, 75, 192, 41, 114, 76, 127, 15, 110, 215, 9, 58, 107, 243, 160,
880                155, 144, 230, 47, 97, 220, 21, 24, 30, 26, 62, 32, 170, 4, 94, 114, 207, 155, 31,
881                80, 62, 254, 220, 206, 240, 218, 83, 54, 204, 197, 255, 239, 41, 66, 199, 150, 56,
882                189, 86, 217, 166, 216, 128, 241, 64, 205, 21, 0, 0, 0, 0, 0,
883            ];
884
885            const FUD_COIN: &[u8] = &[
886                0, 3, 7, 118, 203, 129, 155, 1, 171, 237, 80, 43, 238, 138, 112, 43, 76, 45, 84,
887                117, 50, 193, 47, 37, 0, 28, 157, 234, 121, 90, 94, 99, 28, 38, 241, 3, 102, 117,
888                100, 3, 70, 85, 68, 0, 1, 193, 89, 252, 3, 0, 0, 0, 0, 40, 33, 214, 90, 11, 56,
889                243, 115, 10, 250, 121, 250, 28, 34, 237, 104, 130, 148, 40, 130, 29, 248, 137,
890                244, 27, 138, 94, 150, 28, 182, 104, 162, 185, 0, 152, 247, 62, 93, 1, 0, 0, 0, 42,
891                95, 32, 226, 13, 31, 128, 91, 188, 127, 235, 12, 75, 73, 116, 112, 3, 227, 244,
892                126, 59, 81, 214, 118, 144, 243, 195, 17, 82, 216, 119, 170, 32, 239, 247, 71, 249,
893                241, 98, 133, 53, 46, 37, 100, 242, 94, 231, 241, 184, 8, 69, 192, 69, 67, 1, 116,
894                251, 229, 226, 99, 119, 79, 255, 71, 43, 64, 242, 19, 0, 0, 0, 0, 0,
895            ];
896
897            const BULLSHARK_PACKAGE: &[u8] = &[
898                1, 135, 35, 29, 28, 138, 126, 114, 145, 204, 122, 145, 8, 244, 199, 188, 26, 10,
899                28, 14, 182, 55, 91, 91, 97, 10, 245, 202, 35, 223, 14, 140, 86, 1, 0, 0, 0, 0, 0,
900                0, 0, 1, 9, 98, 117, 108, 108, 115, 104, 97, 114, 107, 162, 6, 161, 28, 235, 11, 6,
901                0, 0, 0, 10, 1, 0, 12, 2, 12, 36, 3, 48, 61, 4, 109, 12, 5, 121, 137, 1, 7, 130, 2,
902                239, 1, 8, 241, 3, 96, 6, 209, 4, 82, 10, 163, 5, 5, 12, 168, 5, 75, 0, 7, 1, 16,
903                2, 9, 2, 21, 2, 22, 2, 23, 0, 0, 2, 0, 1, 3, 7, 1, 0, 0, 2, 1, 12, 1, 0, 1, 2, 2,
904                12, 1, 0, 1, 2, 4, 12, 1, 0, 1, 4, 5, 2, 0, 5, 6, 7, 0, 0, 12, 0, 1, 0, 0, 13, 2,
905                1, 0, 0, 8, 3, 1, 0, 1, 20, 7, 8, 1, 0, 2, 8, 18, 19, 1, 0, 2, 10, 10, 11, 1, 2, 2,
906                14, 17, 1, 1, 0, 3, 17, 7, 1, 1, 12, 3, 18, 16, 1, 1, 12, 4, 19, 13, 14, 0, 5, 15,
907                5, 6, 0, 3, 6, 5, 9, 7, 12, 8, 15, 6, 9, 4, 9, 2, 8, 0, 7, 8, 5, 0, 4, 7, 11, 4, 1,
908                8, 0, 3, 5, 7, 8, 5, 2, 7, 11, 4, 1, 8, 0, 11, 2, 1, 8, 0, 2, 11, 3, 1, 8, 0, 11,
909                4, 1, 8, 0, 1, 10, 2, 1, 8, 6, 1, 9, 0, 1, 11, 1, 1, 9, 0, 1, 8, 0, 7, 9, 0, 2, 10,
910                2, 10, 2, 10, 2, 11, 1, 1, 8, 6, 7, 8, 5, 2, 11, 4, 1, 9, 0, 11, 3, 1, 9, 0, 1, 11,
911                3, 1, 8, 0, 1, 6, 8, 5, 1, 5, 1, 11, 4, 1, 8, 0, 2, 9, 0, 5, 4, 7, 11, 4, 1, 9, 0,
912                3, 5, 7, 8, 5, 2, 7, 11, 4, 1, 9, 0, 11, 2, 1, 9, 0, 1, 3, 9, 66, 85, 76, 76, 83,
913                72, 65, 82, 75, 4, 67, 111, 105, 110, 12, 67, 111, 105, 110, 77, 101, 116, 97, 100,
914                97, 116, 97, 6, 79, 112, 116, 105, 111, 110, 11, 84, 114, 101, 97, 115, 117, 114,
915                121, 67, 97, 112, 9, 84, 120, 67, 111, 110, 116, 101, 120, 116, 3, 85, 114, 108, 9,
916                98, 117, 108, 108, 115, 104, 97, 114, 107, 4, 98, 117, 114, 110, 4, 99, 111, 105,
917                110, 15, 99, 114, 101, 97, 116, 101, 95, 99, 117, 114, 114, 101, 110, 99, 121, 11,
918                100, 117, 109, 109, 121, 95, 102, 105, 101, 108, 100, 4, 105, 110, 105, 116, 4,
919                109, 105, 110, 116, 17, 109, 105, 110, 116, 95, 97, 110, 100, 95, 116, 114, 97,
920                110, 115, 102, 101, 114, 21, 110, 101, 119, 95, 117, 110, 115, 97, 102, 101, 95,
921                102, 114, 111, 109, 95, 98, 121, 116, 101, 115, 6, 111, 112, 116, 105, 111, 110,
922                20, 112, 117, 98, 108, 105, 99, 95, 102, 114, 101, 101, 122, 101, 95, 111, 98, 106,
923                101, 99, 116, 15, 112, 117, 98, 108, 105, 99, 95, 116, 114, 97, 110, 115, 102, 101,
924                114, 6, 115, 101, 110, 100, 101, 114, 4, 115, 111, 109, 101, 8, 116, 114, 97, 110,
925                115, 102, 101, 114, 10, 116, 120, 95, 99, 111, 110, 116, 101, 120, 116, 3, 117,
926                114, 108, 135, 35, 29, 28, 138, 126, 114, 145, 204, 122, 145, 8, 244, 199, 188, 26,
927                10, 28, 14, 182, 55, 91, 91, 97, 10, 245, 202, 35, 223, 14, 140, 86, 0, 0, 0, 0, 0,
928                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
929                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
930                0, 0, 2, 10, 2, 10, 9, 66, 85, 76, 76, 83, 72, 65, 82, 75, 10, 2, 20, 19, 66, 117,
931                108, 108, 32, 83, 104, 97, 114, 107, 32, 83, 117, 105, 70, 114, 101, 110, 115, 10,
932                2, 1, 0, 10, 2, 39, 38, 104, 116, 116, 112, 115, 58, 47, 47, 105, 46, 105, 98, 98,
933                46, 99, 111, 47, 104, 87, 89, 50, 87, 53, 120, 47, 98, 117, 108, 108, 115, 104, 97,
934                114, 107, 46, 112, 110, 103, 0, 2, 1, 11, 1, 0, 0, 0, 0, 4, 20, 11, 0, 49, 6, 7, 0,
935                7, 1, 7, 2, 7, 3, 17, 10, 56, 0, 10, 1, 56, 1, 12, 2, 12, 3, 11, 2, 56, 2, 11, 3,
936                11, 1, 46, 17, 9, 56, 3, 2, 1, 1, 4, 0, 1, 6, 11, 0, 11, 1, 11, 2, 11, 3, 56, 4, 2,
937                2, 1, 4, 0, 1, 5, 11, 0, 11, 1, 56, 5, 1, 2, 0, 1, 9, 98, 117, 108, 108, 115, 104,
938                97, 114, 107, 9, 66, 85, 76, 76, 83, 72, 65, 82, 75, 135, 35, 29, 28, 138, 126,
939                114, 145, 204, 122, 145, 8, 244, 199, 188, 26, 10, 28, 14, 182, 55, 91, 91, 97, 10,
940                245, 202, 35, 223, 14, 140, 86, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
941                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
942                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0,
943                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
944                0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
945                0, 0, 0, 0, 0, 0, 0, 2, 4, 0, 0, 0, 0, 0, 0, 0, 3, 32, 87, 145, 191, 231, 147, 185,
946                46, 159, 240, 181, 95, 126, 236, 65, 154, 55, 16, 196, 229, 218, 47, 59, 99, 197,
947                13, 89, 18, 159, 205, 129, 112, 131, 112, 192, 126, 0, 0, 0, 0, 0,
948            ];
949
950            for fixture in [SUI_COIN, SUI_STAKE, NFT, FUD_COIN, BULLSHARK_PACKAGE] {
951                let object: Object = bcs::from_bytes(fixture).unwrap();
952                assert_eq!(bcs::to_bytes(&object).unwrap(), fixture);
953
954                let json = serde_json::to_string_pretty(&object).unwrap();
955                println!("{json}");
956                assert_eq!(object, serde_json::from_str(&json).unwrap());
957            }
958        }
959
960        // Test to ensure we properly serialize and deserialize the new MoveStructType variants for
961        // address balances
962        #[test]
963        fn address_balance_objects() {
964            let non_sui_address_balance_type = "AAUHIRSU0QWQjjOQW/wFv4O24+PddAa8JQ45YwhB+i7EfzgGY29pbl9hBkNPSU5fQQAAEAAAAAAAAABQCSgWThHEQ1NqPKQQsXVKd/yFD0FSDYUtrvXx1xt+jIk0Bsyk3bbd4hLE1MDxwok6jzp0k3365HVXhJgmi+4vjcQJAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACswgmPj1SN1ZkPLtiVtVs0XD3QCgS/YYUFBh9Q6p4b+zoJAAAAAAAAAAAA==";
965
966            let non_sui = Object::from_bcs_base64(non_sui_address_balance_type).unwrap();
967            assert_eq!(
968                non_sui.to_bcs_base64().unwrap(),
969                non_sui_address_balance_type
970            );
971
972            let sui_address_balance_type = "AAQAAgAAAAAAAABQlJ321C1hKFc15SQmGZUdTDrwVh7xQ46GoV2zEnFK88b/JOPl1wGyhHd/R1itnNXhAzGoyXuDHuOL3V34auvxf+gDAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACswgjRbaObiwu6bn07xewfd3V9iFJfbhcaWy7K6YgNsZdKkAAAAAAAAAAA==";
973
974            let sui = Object::from_bcs_base64(sui_address_balance_type).unwrap();
975            assert_eq!(sui.to_bcs_base64().unwrap(), sui_address_balance_type);
976        }
977
978        // Regression test: deserializing a `MoveStruct` whose `contents` is
979        // shorter than the 32-byte object id used to make `object_id()`
980        // panic via `id_opt(...).unwrap()`. Both BCS and JSON paths must
981        // reject the truncated input rather than produce a value that will
982        // panic on the next `object_id()` call.
983        #[test]
984        fn truncated_move_struct_contents_is_rejected() {
985            // BCS layout: ObjectData::Struct(0x00) | GasCoin tag (0x01) |
986            // has_public_transfer (0x00) | version u64 (8 bytes) |
987            // contents uleb128 length (10) | contents (10 bytes) |
988            // Owner::Immutable (0x03) | digest (1+32 bytes) |
989            // storage_rebate u64 (8 bytes).
990            let mut bytes = Vec::new();
991            bytes.push(0x00);
992            bytes.push(0x01);
993            bytes.push(0x00);
994            bytes.extend_from_slice(&[0u8; 8]);
995            bytes.push(0x0a);
996            bytes.extend_from_slice(&[0u8; 10]);
997            bytes.push(0x03);
998            bytes.push(0x20);
999            bytes.extend_from_slice(&[0u8; 32]);
1000            bytes.extend_from_slice(&[0u8; 8]);
1001
1002            let bcs_err = bcs::from_bytes::<Object>(&bytes).unwrap_err();
1003            assert!(
1004                bcs_err.to_string().contains("MoveStruct contents"),
1005                "unexpected BCS error: {bcs_err}"
1006            );
1007
1008            let json = serde_json::json!({
1009                "data": {
1010                    "Struct": {
1011                        "type_": "GasCoin",
1012                        "has_public_transfer": false,
1013                        "version": 0,
1014                        "contents": "AAAAAAAAAAAAAA=="
1015                    }
1016                },
1017                "owner": "immutable",
1018                "previous_transaction":
1019                    "11111111111111111111111111111111",
1020                "storage_rebate": 0,
1021            });
1022            let json_err = serde_json::from_value::<Object>(json).unwrap_err();
1023            assert!(
1024                json_err.to_string().contains("MoveStruct contents"),
1025                "unexpected JSON error: {json_err}"
1026            );
1027        }
1028
1029        // Regression test: a non-canonical `MoveStructType` wire form (e.g.
1030        // `Other(GasCoin's StructTag)`) used to deserialize successfully and
1031        // then re-serialize as the specialized variant, producing different
1032        // BCS bytes from the same logical value. Each non-canonical encoding
1033        // must now be rejected at deserialization.
1034        #[test]
1035        fn non_canonical_move_struct_type_is_rejected() {
1036            use crate::StructTag;
1037            use crate::TypeTag;
1038
1039            // Build a complete `Object` BCS payload around a
1040            // caller-supplied `type_` byte sequence. Everything else
1041            // (contents, owner, digest, rebate) is canonical and valid so
1042            // the only possible failure is the `type_` canonicalization
1043            // check.
1044            fn object_bytes_with_type(type_bytes: &[u8]) -> Vec<u8> {
1045                let mut bytes = Vec::new();
1046                bytes.push(0x00); // ObjectData::Struct
1047                bytes.extend_from_slice(type_bytes);
1048                bytes.push(0x00); // has_public_transfer
1049                bytes.extend_from_slice(&[0u8; 8]); // version
1050                bytes.push(0x20); // contents uleb128 length = 32
1051                bytes.extend_from_slice(&[0u8; 32]); // contents (valid id)
1052                bytes.push(0x03); // Owner::Immutable
1053                bytes.push(0x20); // digest length prefix
1054                bytes.extend_from_slice(&[0u8; 32]); // digest
1055                bytes.extend_from_slice(&[0u8; 8]); // storage_rebate
1056                bytes
1057            }
1058
1059            // BCS encodes enum tags as uleb128, which is a single byte for
1060            // values 0-127. Variant indices used here:
1061            //   0 = Other(StructTag)
1062            //   3 = Coin(TypeTag)
1063            //   5 = BalanceAccumulatorField(TypeTag)
1064            let cases: Vec<(&str, Vec<u8>)> = vec![
1065                (
1066                    "Other(GasCoin tag)",
1067                    [&[0u8][..], &bcs::to_bytes(&StructTag::gas_coin()).unwrap()].concat(),
1068                ),
1069                (
1070                    "Other(StakedSui tag)",
1071                    [
1072                        &[0u8][..],
1073                        &bcs::to_bytes(&StructTag::staked_sui()).unwrap(),
1074                    ]
1075                    .concat(),
1076                ),
1077                (
1078                    "Other(Coin<non-SUI> tag)",
1079                    [
1080                        &[0u8][..],
1081                        &bcs::to_bytes(&StructTag::coin(TypeTag::U64)).unwrap(),
1082                    ]
1083                    .concat(),
1084                ),
1085                (
1086                    "Other(SuiBalanceAccumulatorField tag)",
1087                    [
1088                        &[0u8][..],
1089                        &bcs::to_bytes(&StructTag::balance_accumulator_field(
1090                            StructTag::sui().into(),
1091                        ))
1092                        .unwrap(),
1093                    ]
1094                    .concat(),
1095                ),
1096                (
1097                    "Coin(SUI's TypeTag)",
1098                    [
1099                        &[3u8][..],
1100                        &bcs::to_bytes(&TypeTag::from(StructTag::sui())).unwrap(),
1101                    ]
1102                    .concat(),
1103                ),
1104                (
1105                    "BalanceAccumulatorField(SUI's TypeTag)",
1106                    [
1107                        &[5u8][..],
1108                        &bcs::to_bytes(&TypeTag::from(StructTag::sui())).unwrap(),
1109                    ]
1110                    .concat(),
1111                ),
1112            ];
1113
1114            for (label, type_bytes) in cases {
1115                let bytes = object_bytes_with_type(&type_bytes);
1116                let err = bcs::from_bytes::<Object>(&bytes).unwrap_err();
1117                assert!(
1118                    err.to_string().contains("non-canonical MoveStructType"),
1119                    "{label}: unexpected error: {err}"
1120                );
1121            }
1122        }
1123    }
1124}