iota_types/
base_types.rs

1// Copyright (c) 2021, Facebook, Inc. and its affiliates
2// Copyright (c) Mysten Labs, Inc.
3// Modifications Copyright (c) 2024 IOTA Stiftung
4// SPDX-License-Identifier: Apache-2.0
5
6use std::{
7    convert::{TryFrom, TryInto},
8    fmt,
9    str::FromStr,
10};
11
12use anyhow::anyhow;
13use fastcrypto::hash::HashFunction;
14use iota_protocol_config::ProtocolConfig;
15pub use iota_sdk_types::{Identifier, MoveObjectType, StructTag, TypeTag};
16use move_binary_format::{CompiledModule, file_format::SignatureToken};
17use move_bytecode_utils::resolve_struct;
18use move_core_types::{
19    account_address::AccountAddress, annotated_value as A, ident_str, identifier::IdentStr,
20};
21use serde::{
22    Deserialize, Serialize, Serializer,
23    ser::{Error, SerializeSeq},
24};
25
26use crate::{
27    MOVE_STDLIB_ADDRESS,
28    crypto::{
29        AuthorityPublicKeyBytes, DefaultHash, IotaPublicKey, IotaSignature, PublicKey,
30        SignatureScheme,
31    },
32    effects::{TransactionEffects, TransactionEffectsAPI},
33    epoch_data::EpochData,
34    error::{ExecutionError, ExecutionErrorKind, IotaError, IotaResult},
35    id::RESOLVED_IOTA_ID,
36    iota_sdk_types_conversions::struct_tag_sdk_to_core,
37    iota_serde::to_iota_struct_tag_string,
38    messages_checkpoint::CheckpointTimestamp,
39    multisig::MultiSigPublicKey,
40    object::{Object, Owner},
41    parse_iota_struct_tag,
42    signature::GenericSignature,
43    transaction::{Transaction, VerifiedTransaction},
44};
45pub use crate::{
46    committee::EpochId,
47    digests::{ObjectDigest, TransactionDigest, TransactionEffectsDigest},
48};
49
50#[cfg(test)]
51#[path = "unit_tests/base_types_tests.rs"]
52mod base_types_tests;
53
54pub use iota_sdk_types::{
55    ObjectId as ObjectID, ObjectReference as ObjectRef, Version as SequenceNumber,
56};
57
58pub type TxSequenceNumber = u64;
59
60pub type VersionNumber = SequenceNumber;
61
62/// The round number.
63pub type CommitRound = u64;
64
65pub type AuthorityName = AuthorityPublicKeyBytes;
66
67pub trait ConciseableName<'a> {
68    type ConciseTypeRef: std::fmt::Debug;
69    type ConciseType: std::fmt::Debug;
70
71    fn concise(&'a self) -> Self::ConciseTypeRef;
72    fn concise_owned(&self) -> Self::ConciseType;
73}
74
75pub type VersionDigest = (SequenceNumber, ObjectDigest);
76
77pub fn random_object_ref() -> ObjectRef {
78    ObjectRef::new(
79        ObjectID::random(),
80        SequenceNumber::default(),
81        ObjectDigest::new([0; 32]),
82    )
83}
84
85/// Whether this type is valid as a primitive (pure) transaction input.
86pub fn is_primitive_type_tag(t: &TypeTag) -> bool {
87    use TypeTag as T;
88
89    match t {
90        T::Bool | T::U8 | T::U16 | T::U32 | T::U64 | T::U128 | T::U256 | T::Address => true,
91        T::Vector(inner) => is_primitive_type_tag(inner),
92        T::Struct(st) => {
93            let resolved_struct = (
94                &AccountAddress::new(st.address().into_bytes()),
95                move_core_types::identifier::IdentStr::new(st.module().as_str()).unwrap(),
96                move_core_types::identifier::IdentStr::new(st.name().as_str()).unwrap(),
97            );
98            // is id or..
99            if resolved_struct == RESOLVED_IOTA_ID {
100                return true;
101            }
102            // is utf8 string
103            if resolved_struct == RESOLVED_UTF8_STR {
104                return true;
105            }
106            // is ascii string
107            if resolved_struct == RESOLVED_ASCII_STR {
108                return true;
109            }
110            // is option of a primitive
111            resolved_struct == RESOLVED_STD_OPTION
112                && st.type_params().len() == 1
113                && is_primitive_type_tag(&st.type_params()[0])
114        }
115        T::Signer => false,
116    }
117}
118
119/// Type of an IOTA object
120#[derive(Clone, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug)]
121pub enum ObjectType {
122    /// Move package containing one or more bytecode modules
123    Package,
124    /// A Move struct of the given type
125    Struct(MoveObjectType),
126}
127
128const PACKAGE: &str = "package";
129
130impl ObjectType {
131    pub fn is_gas_coin(&self) -> bool {
132        matches!(self, ObjectType::Struct(s) if s.is_gas_coin())
133    }
134
135    pub fn is_coin(&self) -> bool {
136        matches!(self, ObjectType::Struct(s) if s.is_coin())
137    }
138
139    pub fn is_package(&self) -> bool {
140        matches!(self, ObjectType::Package)
141    }
142}
143
144impl From<&Object> for ObjectType {
145    fn from(o: &Object) -> Self {
146        o.data
147            .object_type()
148            .map(|t| ObjectType::Struct(t.clone()))
149            .unwrap_or(ObjectType::Package)
150    }
151}
152
153impl TryFrom<ObjectType> for StructTag {
154    type Error = anyhow::Error;
155
156    fn try_from(o: ObjectType) -> Result<Self, anyhow::Error> {
157        match o {
158            ObjectType::Package => Err(anyhow!("Cannot create StructTag from Package")),
159            ObjectType::Struct(s) => Ok(s.into()),
160        }
161    }
162}
163
164impl FromStr for ObjectType {
165    type Err = anyhow::Error;
166
167    fn from_str(s: &str) -> Result<Self, Self::Err> {
168        if s.to_lowercase() == PACKAGE {
169            Ok(ObjectType::Package)
170        } else {
171            let tag = parse_iota_struct_tag(s)?;
172            Ok(ObjectType::Struct(tag.into()))
173        }
174    }
175}
176
177#[derive(Clone, Serialize, Deserialize, Ord, PartialOrd, Eq, PartialEq, Debug)]
178pub struct ObjectInfo {
179    pub object_id: ObjectID,
180    pub version: SequenceNumber,
181    pub digest: ObjectDigest,
182    pub type_: ObjectType,
183    pub owner: Owner,
184    pub previous_transaction: TransactionDigest,
185}
186
187impl ObjectInfo {
188    pub fn new(oref: &ObjectRef, o: &Object) -> Self {
189        Self {
190            object_id: oref.object_id,
191            version: oref.version,
192            digest: oref.digest,
193            type_: o.into(),
194            owner: o.owner,
195            previous_transaction: o.previous_transaction,
196        }
197    }
198
199    pub fn from_object(object: &Object) -> Self {
200        Self {
201            object_id: object.id(),
202            version: object.version(),
203            digest: object.digest(),
204            type_: object.into(),
205            owner: object.owner,
206            previous_transaction: object.previous_transaction,
207        }
208    }
209}
210
211impl From<ObjectInfo> for ObjectRef {
212    fn from(info: ObjectInfo) -> Self {
213        ObjectRef::new(info.object_id, info.version, info.digest)
214    }
215}
216
217impl From<&ObjectInfo> for ObjectRef {
218    fn from(info: &ObjectInfo) -> Self {
219        ObjectRef::new(info.object_id, info.version, info.digest)
220    }
221}
222
223pub const IOTA_ADDRESS_LENGTH: usize = ObjectID::LENGTH;
224
225pub use iota_sdk_types::Address as IotaAddress;
226
227pub fn address_from_iota_pub_key<T: IotaPublicKey>(pk: &T) -> IotaAddress {
228    let mut hasher = DefaultHash::default();
229    T::SIGNATURE_SCHEME.update_hasher_with_flag(&mut hasher);
230    hasher.update(pk);
231    let g_arr = hasher.finalize();
232    IotaAddress::new(g_arr.digest)
233}
234
235impl From<&PublicKey> for IotaAddress {
236    fn from(pk: &PublicKey) -> Self {
237        let mut hasher = DefaultHash::default();
238        pk.scheme().update_hasher_with_flag(&mut hasher);
239        hasher.update(pk);
240        let g_arr = hasher.finalize();
241        IotaAddress::new(g_arr.digest)
242    }
243}
244
245impl From<&MultiSigPublicKey> for IotaAddress {
246    /// Derive a IotaAddress from [struct MultiSigPublicKey]. A MultiSig address
247    /// is defined as the 32-byte Blake2b hash of serializing the flag, the
248    /// threshold, concatenation of all n flag, public keys and
249    /// its weight. `flag_MultiSig || threshold || flag_1 || pk_1 || weight_1
250    /// || ... || flag_n || pk_n || weight_n`.
251    fn from(multisig_pk: &MultiSigPublicKey) -> Self {
252        let mut hasher = DefaultHash::default();
253        hasher.update([SignatureScheme::MultiSig.flag()]);
254        hasher.update(multisig_pk.threshold().to_le_bytes());
255        multisig_pk.pubkeys().iter().for_each(|(pk, w)| {
256            pk.scheme().update_hasher_with_flag(&mut hasher);
257            hasher.update(pk.as_ref());
258            hasher.update(w.to_le_bytes());
259        });
260        IotaAddress::new(hasher.finalize().digest)
261    }
262}
263
264impl TryFrom<&GenericSignature> for IotaAddress {
265    type Error = IotaError;
266    /// Derive a IotaAddress from a serialized signature in IOTA
267    /// [GenericSignature].
268    fn try_from(sig: &GenericSignature) -> IotaResult<Self> {
269        match sig {
270            GenericSignature::Signature(sig) => {
271                let scheme = sig.scheme();
272                let pub_key_bytes = sig.public_key_bytes();
273                let pub_key = PublicKey::try_from_bytes(scheme, pub_key_bytes).map_err(|_| {
274                    IotaError::InvalidSignature {
275                        error: "Cannot parse pubkey".to_string(),
276                    }
277                })?;
278                Ok(IotaAddress::from(&pub_key))
279            }
280            GenericSignature::MultiSig(ms) => Ok(ms.get_pk().into()),
281            #[allow(deprecated)]
282            GenericSignature::ZkLoginAuthenticatorDeprecated(_) => {
283                Err(IotaError::UnsupportedFeature {
284                    error: "zkLogin is not supported".to_string(),
285                })
286            }
287            GenericSignature::PasskeyAuthenticator(s) => Ok(IotaAddress::from(&s.get_pk()?)),
288            GenericSignature::MoveAuthenticator(move_authenticator) => move_authenticator.address(),
289        }
290    }
291}
292
293/// Generate a fake IotaAddress with repeated one byte.
294pub fn dbg_addr(name: u8) -> IotaAddress {
295    let addr = [name; IOTA_ADDRESS_LENGTH];
296    IotaAddress::new(addr)
297}
298
299#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash, Serialize, Deserialize, Debug)]
300pub struct ExecutionDigests {
301    pub transaction: TransactionDigest,
302    pub effects: TransactionEffectsDigest,
303}
304
305impl ExecutionDigests {
306    pub fn new(transaction: TransactionDigest, effects: TransactionEffectsDigest) -> Self {
307        Self {
308            transaction,
309            effects,
310        }
311    }
312
313    pub fn random() -> Self {
314        Self {
315            transaction: TransactionDigest::random(),
316            effects: TransactionEffectsDigest::random(),
317        }
318    }
319}
320
321#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)]
322pub struct ExecutionData {
323    pub transaction: Transaction,
324    pub effects: TransactionEffects,
325}
326
327impl ExecutionData {
328    pub fn new(transaction: Transaction, effects: TransactionEffects) -> ExecutionData {
329        debug_assert_eq!(transaction.digest(), effects.transaction_digest());
330        Self {
331            transaction,
332            effects,
333        }
334    }
335
336    pub fn digests(&self) -> ExecutionDigests {
337        self.effects.execution_digests()
338    }
339}
340
341#[derive(Clone, Eq, PartialEq, Debug)]
342pub struct VerifiedExecutionData {
343    pub transaction: VerifiedTransaction,
344    pub effects: TransactionEffects,
345}
346
347impl VerifiedExecutionData {
348    pub fn new(transaction: VerifiedTransaction, effects: TransactionEffects) -> Self {
349        debug_assert_eq!(transaction.digest(), effects.transaction_digest());
350        Self {
351            transaction,
352            effects,
353        }
354    }
355
356    pub fn new_unchecked(data: ExecutionData) -> Self {
357        Self {
358            transaction: VerifiedTransaction::new_unchecked(data.transaction),
359            effects: data.effects,
360        }
361    }
362
363    pub fn into_inner(self) -> ExecutionData {
364        ExecutionData {
365            transaction: self.transaction.into_inner(),
366            effects: self.effects,
367        }
368    }
369
370    pub fn digests(&self) -> ExecutionDigests {
371        self.effects.execution_digests()
372    }
373}
374
375pub const RESOLVED_STD_OPTION: (&AccountAddress, &IdentStr, &IdentStr) = (
376    &MOVE_STDLIB_ADDRESS,
377    ident_str!("option"),
378    ident_str!("Option"),
379);
380
381pub const RESOLVED_ASCII_STR: (&AccountAddress, &IdentStr, &IdentStr) = (
382    &MOVE_STDLIB_ADDRESS,
383    ident_str!("ascii"),
384    ident_str!("String"),
385);
386
387pub const RESOLVED_UTF8_STR: (&AccountAddress, &IdentStr, &IdentStr) = (
388    &MOVE_STDLIB_ADDRESS,
389    ident_str!("string"),
390    ident_str!("String"),
391);
392
393pub fn move_ascii_str_layout() -> A::MoveStructLayout {
394    A::MoveStructLayout {
395        type_: struct_tag_sdk_to_core(&StructTag::new_ascii_string()),
396        fields: vec![A::MoveFieldLayout::new(
397            ident_str!("bytes").into(),
398            A::MoveTypeLayout::Vector(Box::new(A::MoveTypeLayout::U8)),
399        )],
400    }
401}
402
403pub fn move_utf8_str_layout() -> A::MoveStructLayout {
404    A::MoveStructLayout {
405        type_: struct_tag_sdk_to_core(&StructTag::new_string()),
406        fields: vec![A::MoveFieldLayout::new(
407            ident_str!("bytes").into(),
408            A::MoveTypeLayout::Vector(Box::new(A::MoveTypeLayout::U8)),
409        )],
410    }
411}
412
413pub fn url_layout() -> A::MoveStructLayout {
414    A::MoveStructLayout {
415        type_: struct_tag_sdk_to_core(&StructTag::new_url()),
416        fields: vec![A::MoveFieldLayout::new(
417            ident_str!("url").to_owned(),
418            A::MoveTypeLayout::Struct(Box::new(move_ascii_str_layout())),
419        )],
420    }
421}
422
423// The Rust representation of the Move `TxContext`.
424// This struct must be kept in sync with the Move `TxContext` definition.
425// Moving forward we are going to zero all fields of the Move `TxContext`
426// and use native functions to retrieve info about the transaction.
427// However we cannot remove the Move type and so this struct is going to
428// be the Rust equivalent to the Move `TxContext` for legacy usages.
429//
430// `TxContext` in Rust (see below) is going to be purely used in Rust and can
431// evolve as needed without worrying about any compatibility with Move.
432#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
433pub struct MoveLegacyTxContext {
434    // Signer/sender of the transaction
435    sender: AccountAddress,
436    // Digest of the current transaction
437    digest: Vec<u8>,
438    // The current epoch number
439    epoch: EpochId,
440    // Timestamp that the epoch started at
441    epoch_timestamp_ms: CheckpointTimestamp,
442    // Number of `ObjectID`'s generated during execution of the current transaction
443    ids_created: u64,
444}
445
446impl From<&TxContext> for MoveLegacyTxContext {
447    fn from(tx_context: &TxContext) -> Self {
448        Self {
449            sender: tx_context.sender,
450            digest: tx_context.digest.clone(),
451            epoch: tx_context.epoch,
452            epoch_timestamp_ms: tx_context.epoch_timestamp_ms,
453            ids_created: tx_context.ids_created,
454        }
455    }
456}
457
458// Information about the transaction context.
459// This struct is not related to Move and can evolve as needed/required.
460#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
461pub struct TxContext {
462    /// Signer/sender of the transaction
463    sender: AccountAddress,
464    /// Digest of the current transaction
465    digest: Vec<u8>,
466    /// The current epoch number
467    epoch: EpochId,
468    /// Timestamp that the epoch started at
469    epoch_timestamp_ms: CheckpointTimestamp,
470    /// Number of `ObjectID`'s generated during execution of the current
471    /// transaction
472    ids_created: u64,
473    // Reference gas price
474    rgp: u64,
475    /// Gas price passed to transaction as input
476    gas_price: u64,
477    /// Gas budget passed to transaction as input
478    gas_budget: u64,
479    /// Address of the sponsor if any (gas owner != sender)
480    sponsor: Option<AccountAddress>,
481    /// Whether the `TxContext` is native or not (i.e., Move reads values via
482    /// native functions instead of struct fields).
483    is_native: bool,
484}
485
486#[derive(PartialEq, Eq, Clone, Copy)]
487pub enum TxContextKind {
488    // No TxContext
489    None,
490    // &mut TxContext
491    Mutable,
492    // &TxContext
493    Immutable,
494}
495
496impl TxContext {
497    pub fn new(
498        sender: &IotaAddress,
499        digest: &TransactionDigest,
500        epoch_data: &EpochData,
501        rgp: u64,
502        gas_price: u64,
503        gas_budget: u64,
504        sponsor: Option<IotaAddress>,
505        protocol_config: &ProtocolConfig,
506    ) -> Self {
507        Self::new_from_components(
508            sender,
509            digest,
510            &epoch_data.epoch_id(),
511            epoch_data.epoch_start_timestamp(),
512            rgp,
513            gas_price,
514            gas_budget,
515            sponsor,
516            protocol_config,
517        )
518    }
519
520    pub fn new_from_components(
521        sender: &IotaAddress,
522        digest: &TransactionDigest,
523        epoch_id: &EpochId,
524        epoch_timestamp_ms: u64,
525        rgp: u64,
526        gas_price: u64,
527        gas_budget: u64,
528        sponsor: Option<IotaAddress>,
529        protocol_config: &ProtocolConfig,
530    ) -> Self {
531        Self {
532            sender: AccountAddress::new(sender.into_bytes()),
533            digest: digest.into_inner().to_vec(),
534            epoch: *epoch_id,
535            epoch_timestamp_ms,
536            ids_created: 0,
537            rgp,
538            gas_price,
539            gas_budget,
540            sponsor: sponsor.map(|s| AccountAddress::new(s.into_bytes())),
541            is_native: protocol_config.move_native_tx_context(),
542        }
543    }
544
545    /// Returns whether the type signature is &mut TxContext, &TxContext, or
546    /// none of the above.
547    pub fn kind(view: &CompiledModule, s: &SignatureToken) -> TxContextKind {
548        use SignatureToken as S;
549        let (kind, s) = match s {
550            S::MutableReference(s) => (TxContextKind::Mutable, s),
551            S::Reference(s) => (TxContextKind::Immutable, s),
552            _ => return TxContextKind::None,
553        };
554
555        let S::Datatype(idx) = &**s else {
556            return TxContextKind::None;
557        };
558
559        let (module_addr, module_name, struct_name) = resolve_struct(view, *idx);
560        let is_tx_context_type = module_name.as_str() == Identifier::TX_CONTEXT_MODULE.as_str()
561            && module_addr.as_ref() == IotaAddress::FRAMEWORK.as_bytes()
562            && struct_name.as_str() == Identifier::TX_CONTEXT.as_str();
563
564        if is_tx_context_type {
565            kind
566        } else {
567            TxContextKind::None
568        }
569    }
570
571    pub fn epoch(&self) -> EpochId {
572        self.epoch
573    }
574
575    pub fn epoch_timestamp_ms(&self) -> u64 {
576        self.epoch_timestamp_ms
577    }
578
579    /// Return the transaction digest, to include in new objects
580    pub fn digest(&self) -> TransactionDigest {
581        TransactionDigest::new(self.digest.clone().try_into().unwrap())
582    }
583
584    pub fn sponsor(&self) -> Option<IotaAddress> {
585        self.sponsor.map(|a| IotaAddress::from(a.into_bytes()))
586    }
587
588    pub fn rgp(&self) -> u64 {
589        self.rgp
590    }
591
592    pub fn gas_price(&self) -> u64 {
593        self.gas_price
594    }
595
596    pub fn gas_budget(&self) -> u64 {
597        self.gas_budget
598    }
599
600    pub fn ids_created(&self) -> u64 {
601        self.ids_created
602    }
603
604    /// Derive a globally unique object ID by hashing self.digest |
605    /// self.ids_created
606    pub fn fresh_id(&mut self) -> ObjectID {
607        let id = ObjectID::derive_id(self.digest(), self.ids_created);
608        self.ids_created += 1;
609        id
610    }
611
612    pub fn sender(&self) -> IotaAddress {
613        IotaAddress::new(self.sender.into_bytes())
614    }
615
616    pub fn to_vec(&self) -> Vec<u8> {
617        bcs::to_bytes(&self).unwrap()
618    }
619
620    /// Serialize this context as a `MoveLegacyTxContext`. When `is_native` is
621    /// true, all fields except digest are zeroed (Move reads actual values via
622    /// native functions). When false, actual field values are used.
623    pub fn to_bcs_legacy_context(&self) -> Vec<u8> {
624        let move_context: MoveLegacyTxContext = if self.is_native {
625            let tx_context = &TxContext {
626                sender: AccountAddress::ZERO,
627                digest: vec![],
628                epoch: 0,
629                epoch_timestamp_ms: 0,
630                ids_created: 0,
631                rgp: 0,
632                gas_price: 0,
633                gas_budget: 0,
634                sponsor: None,
635                is_native: true,
636            };
637            tx_context.into()
638        } else {
639            self.into()
640        };
641        bcs::to_bytes(&move_context).unwrap()
642    }
643
644    /// Updates state of the context instance. It's intended to use
645    /// when mutable context is passed over some boundary via
646    /// serialize/deserialize and this is the reason why this method
647    /// consumes the other context.
648    pub fn update_state(&mut self, other: MoveLegacyTxContext) -> Result<(), ExecutionError> {
649        if !self.is_native {
650            if self.sender != other.sender
651                || self.digest != other.digest
652                || other.ids_created < self.ids_created
653            {
654                return Err(ExecutionError::new_with_source(
655                    ExecutionErrorKind::InvariantViolation,
656                    "Immutable fields for TxContext changed",
657                ));
658            }
659            self.ids_created = other.ids_created;
660        }
661        Ok(())
662    }
663
664    /// Replace all fields. Used by Move test-only native functions.
665    pub fn replace(
666        &mut self,
667        sender: AccountAddress,
668        tx_hash: Vec<u8>,
669        epoch: u64,
670        epoch_timestamp_ms: u64,
671        ids_created: u64,
672        rgp: u64,
673        gas_price: u64,
674        gas_budget: u64,
675        sponsor: Option<AccountAddress>,
676    ) {
677        self.sender = sender;
678        self.digest = tx_hash;
679        self.epoch = epoch;
680        self.epoch_timestamp_ms = epoch_timestamp_ms;
681        self.ids_created = ids_created;
682        self.rgp = rgp;
683        self.gas_price = gas_price;
684        self.gas_budget = gas_budget;
685        self.sponsor = sponsor;
686    }
687
688    // Generate a random TxContext for testing.
689    pub fn random_for_testing_only() -> Self {
690        Self::new(
691            &IotaAddress::random(),
692            &TransactionDigest::random(),
693            &EpochData::new_test(),
694            0,
695            0,
696            0,
697            None,
698            &ProtocolConfig::get_for_max_version_UNSAFE(),
699        )
700    }
701}
702
703/// Generate a fake ObjectID with repeated one byte.
704pub fn dbg_object_id(name: u8) -> ObjectID {
705    ObjectID::new([name; ObjectID::LENGTH])
706}
707
708#[derive(PartialEq, Eq, Clone, Debug, thiserror::Error)]
709pub enum ObjectIDParseError {
710    #[error("ObjectID hex literal must start with 0x")]
711    HexLiteralPrefixMissing,
712
713    #[error("Could not convert from bytes slice")]
714    TryFromSlice,
715}
716
717impl fmt::Display for ObjectType {
718    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
719        match self {
720            ObjectType::Package => write!(f, "{PACKAGE}"),
721            ObjectType::Struct(t) => write!(
722                f,
723                "{}",
724                to_iota_struct_tag_string(t).map_err(fmt::Error::custom)?
725            ),
726        }
727    }
728}
729
730// SizeOneVec is a wrapper around Vec<T> that enforces the size of the vec to be
731// 1. This seems pointless, but it allows us to have fields in protocol messages
732// that are current enforced to be of size 1, but might later allow other sizes,
733// and to have that constraint enforced in the serialization/deserialization
734// layer, instead of requiring manual input validation.
735#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
736#[serde(try_from = "Vec<T>")]
737pub struct SizeOneVec<T> {
738    e: T,
739}
740
741impl<T> SizeOneVec<T> {
742    pub fn new(e: T) -> Self {
743        Self { e }
744    }
745
746    pub fn element(&self) -> &T {
747        &self.e
748    }
749
750    pub fn element_mut(&mut self) -> &mut T {
751        &mut self.e
752    }
753
754    pub fn into_inner(self) -> T {
755        self.e
756    }
757
758    pub fn iter(&self) -> std::iter::Once<&T> {
759        std::iter::once(&self.e)
760    }
761}
762
763impl<T> Serialize for SizeOneVec<T>
764where
765    T: Serialize,
766{
767    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
768    where
769        S: Serializer,
770    {
771        let mut seq = serializer.serialize_seq(Some(1))?;
772        seq.serialize_element(&self.e)?;
773        seq.end()
774    }
775}
776
777impl<T> TryFrom<Vec<T>> for SizeOneVec<T> {
778    type Error = anyhow::Error;
779
780    fn try_from(mut v: Vec<T>) -> Result<Self, Self::Error> {
781        if v.len() != 1 {
782            Err(anyhow!("Expected a vec of size 1"))
783        } else {
784            Ok(SizeOneVec {
785                e: v.pop().unwrap(),
786            })
787        }
788    }
789}
790
791#[test]
792fn test_size_one_vec_is_transparent() {
793    let regular = vec![42u8];
794    let size_one = SizeOneVec::new(42u8);
795
796    // Vec -> SizeOneVec serialization is transparent
797    let regular_ser = bcs::to_bytes(&regular).unwrap();
798    let size_one_deser = bcs::from_bytes::<SizeOneVec<u8>>(&regular_ser).unwrap();
799    assert_eq!(size_one, size_one_deser);
800
801    // other direction works too
802    let size_one_ser = bcs::to_bytes(&SizeOneVec::new(43u8)).unwrap();
803    let regular_deser = bcs::from_bytes::<Vec<u8>>(&size_one_ser).unwrap();
804    assert_eq!(regular_deser, vec![43u8]);
805
806    // we get a deserialize error when deserializing a vec with size != 1
807    let empty_ser = bcs::to_bytes(&Vec::<u8>::new()).unwrap();
808    bcs::from_bytes::<SizeOneVec<u8>>(&empty_ser).unwrap_err();
809
810    let size_greater_than_one_ser = bcs::to_bytes(&vec![1u8, 2u8]).unwrap();
811    bcs::from_bytes::<SizeOneVec<u8>>(&size_greater_than_one_ser).unwrap_err();
812}