sui_types/
dynamic_field.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::base_types::{MoveObjectType, ObjectDigest, SuiAddress};
5use crate::crypto::DefaultHash;
6use crate::error::{SuiError, SuiErrorKind, SuiResult};
7use crate::id::UID;
8use crate::object::{MoveObject, Object};
9use crate::storage::{ChildObjectResolver, ObjectStore};
10use crate::sui_serde::Readable;
11use crate::sui_serde::SuiTypeTag;
12use crate::{MoveTypeTagTrait, ObjectID, SUI_FRAMEWORK_ADDRESS, SequenceNumber};
13use fastcrypto::encoding::Base64;
14use fastcrypto::hash::HashFunction;
15use move_core_types::annotated_value::{MoveStruct, MoveValue};
16use move_core_types::ident_str;
17use move_core_types::identifier::IdentStr;
18use move_core_types::language_storage::{StructTag, TypeTag};
19use schemars::JsonSchema;
20use serde::Deserialize;
21use serde::Serialize;
22use serde::de::DeserializeOwned;
23use serde_json::Value;
24use serde_with::DisplayFromStr;
25use serde_with::serde_as;
26use shared_crypto::intent::HashingIntentScope;
27use std::fmt;
28use std::fmt::{Display, Formatter};
29
30pub mod visitor;
31
32pub const DYNAMIC_FIELD_MODULE_NAME: &IdentStr = ident_str!("dynamic_field");
33pub const DYNAMIC_FIELD_FIELD_STRUCT_NAME: &IdentStr = ident_str!("Field");
34
35const DYNAMIC_OBJECT_FIELD_MODULE_NAME: &IdentStr = ident_str!("dynamic_object_field");
36const DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME: &IdentStr = ident_str!("Wrapper");
37
38/// Rust version of the Move sui::dynamic_field::Field type
39#[derive(Clone, Serialize, Deserialize, Debug)]
40pub struct Field<N, V> {
41    pub id: UID,
42    pub name: N,
43    pub value: V,
44}
45
46/// Rust version of the Move sui::dynamic_object_field::Wrapper type
47#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
48pub struct DOFWrapper<N> {
49    pub name: N,
50}
51
52impl<N> MoveTypeTagTrait for DOFWrapper<N>
53where
54    N: MoveTypeTagTrait,
55{
56    fn get_type_tag() -> TypeTag {
57        TypeTag::Struct(Box::new(DynamicFieldInfo::dynamic_object_field_wrapper(
58            N::get_type_tag(),
59        )))
60    }
61}
62
63#[serde_as]
64#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
65#[serde(rename_all = "camelCase")]
66pub struct DynamicFieldInfo {
67    pub name: DynamicFieldName,
68    #[serde_as(as = "Readable<Base64, _>")]
69    pub bcs_name: Vec<u8>,
70    pub type_: DynamicFieldType,
71    pub object_type: String,
72    pub object_id: ObjectID,
73    pub version: SequenceNumber,
74    pub digest: ObjectDigest,
75}
76
77#[serde_as]
78#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq)]
79#[serde(rename_all = "camelCase")]
80pub struct DynamicFieldName {
81    #[schemars(with = "String")]
82    #[serde_as(as = "Readable<SuiTypeTag, _>")]
83    pub type_: TypeTag,
84    // Bincode does not like serde_json::Value, rocksdb will not insert the value without serializing value as string.
85    // TODO: investigate if this can be removed after switch to BCS.
86    #[schemars(with = "Value")]
87    #[serde_as(as = "Readable<_, DisplayFromStr>")]
88    pub value: Value,
89}
90
91impl Display for DynamicFieldName {
92    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}: {}", self.type_, self.value)
94    }
95}
96
97#[derive(
98    Copy, Clone, Serialize, Deserialize, JsonSchema, Ord, PartialOrd, Eq, PartialEq, Debug,
99)]
100pub enum DynamicFieldType {
101    #[serde(rename_all = "camelCase")]
102    DynamicField,
103    DynamicObject,
104}
105
106impl Display for DynamicFieldType {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            DynamicFieldType::DynamicField => write!(f, "DynamicField"),
110            DynamicFieldType::DynamicObject => write!(f, "DynamicObject"),
111        }
112    }
113}
114
115impl DynamicFieldInfo {
116    pub fn is_dynamic_field(tag: &StructTag) -> bool {
117        tag.address == SUI_FRAMEWORK_ADDRESS
118            && tag.module.as_ident_str() == DYNAMIC_FIELD_MODULE_NAME
119            && tag.name.as_ident_str() == DYNAMIC_FIELD_FIELD_STRUCT_NAME
120    }
121
122    pub fn is_dynamic_object_field_wrapper(tag: &StructTag) -> bool {
123        tag.address == SUI_FRAMEWORK_ADDRESS
124            && tag.module.as_ident_str() == DYNAMIC_OBJECT_FIELD_MODULE_NAME
125            && tag.name.as_ident_str() == DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME
126    }
127
128    pub fn dynamic_field_type(key: TypeTag, value: TypeTag) -> StructTag {
129        StructTag {
130            address: SUI_FRAMEWORK_ADDRESS,
131            name: DYNAMIC_FIELD_FIELD_STRUCT_NAME.to_owned(),
132            module: DYNAMIC_FIELD_MODULE_NAME.to_owned(),
133            type_params: vec![key, value],
134        }
135    }
136
137    pub fn dynamic_object_field_wrapper(key: TypeTag) -> StructTag {
138        StructTag {
139            address: SUI_FRAMEWORK_ADDRESS,
140            module: DYNAMIC_OBJECT_FIELD_MODULE_NAME.to_owned(),
141            name: DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME.to_owned(),
142            type_params: vec![key],
143        }
144    }
145
146    pub fn try_extract_field_name(tag: &StructTag, type_: &DynamicFieldType) -> SuiResult<TypeTag> {
147        match (type_, tag.type_params.first()) {
148            (DynamicFieldType::DynamicField, Some(name_type)) => Ok(name_type.clone()),
149            (DynamicFieldType::DynamicObject, Some(TypeTag::Struct(s))) => Ok(s
150                .type_params
151                .first()
152                .ok_or_else(|| SuiErrorKind::ObjectDeserializationError {
153                    error: format!("Error extracting dynamic object name from object: {tag}"),
154                })?
155                .clone()),
156            _ => Err(SuiErrorKind::ObjectDeserializationError {
157                error: format!("Error extracting dynamic object name from object: {tag}"),
158            }
159            .into()),
160        }
161    }
162
163    pub fn try_extract_field_value(tag: &StructTag) -> SuiResult<TypeTag> {
164        match tag.type_params.last() {
165            Some(value_type) => Ok(value_type.clone()),
166            None => Err(SuiErrorKind::ObjectDeserializationError {
167                error: format!("Error extracting dynamic object value from object: {tag}"),
168            }
169            .into()),
170        }
171    }
172
173    pub fn parse_move_object(
174        move_struct: &MoveStruct,
175    ) -> SuiResult<(MoveValue, DynamicFieldType, ObjectID)> {
176        let name = extract_field_from_move_struct(move_struct, "name").ok_or_else(|| {
177            SuiErrorKind::ObjectDeserializationError {
178                error: "Cannot extract [name] field from sui::dynamic_field::Field".to_string(),
179            }
180        })?;
181
182        let value = extract_field_from_move_struct(move_struct, "value").ok_or_else(|| {
183            SuiErrorKind::ObjectDeserializationError {
184                error: "Cannot extract [value] field from sui::dynamic_field::Field".to_string(),
185            }
186        })?;
187
188        Ok(if is_dynamic_object(move_struct) {
189            let name = match name {
190                MoveValue::Struct(name_struct) => {
191                    extract_field_from_move_struct(name_struct, "name")
192                }
193                _ => None,
194            }
195            .ok_or_else(|| SuiErrorKind::ObjectDeserializationError {
196                error: "Cannot extract [name] field from sui::dynamic_object_field::Wrapper."
197                    .to_string(),
198            })?;
199            // ID extracted from the wrapper object
200            let object_id = extract_id_value(value).ok_or_else(|| {
201                SuiErrorKind::ObjectDeserializationError {
202                    error: format!(
203                        "Cannot extract dynamic object's object id from \
204                        sui::dynamic_field::Field, {value:?}"
205                    ),
206                }
207            })?;
208            (name.clone(), DynamicFieldType::DynamicObject, object_id)
209        } else {
210            // ID of the Field object
211            let object_id = extract_object_id(move_struct).ok_or_else(|| {
212                SuiErrorKind::ObjectDeserializationError {
213                    error: format!(
214                        "Cannot extract dynamic object's object id from \
215                        sui::dynamic_field::Field, {move_struct:?}",
216                    ),
217                }
218            })?;
219            (name.clone(), DynamicFieldType::DynamicField, object_id)
220        })
221    }
222}
223
224pub fn extract_field_from_move_struct<'a>(
225    move_struct: &'a MoveStruct,
226    field_name: &str,
227) -> Option<&'a MoveValue> {
228    move_struct.fields.iter().find_map(|(id, value)| {
229        if id.to_string() == field_name {
230            Some(value)
231        } else {
232            None
233        }
234    })
235}
236
237fn extract_object_id(value: &MoveStruct) -> Option<ObjectID> {
238    // id:UID is the first value in an object
239    let uid_value = &value.fields.first()?.1;
240
241    // id is the first value in UID
242    let id_value = match uid_value {
243        MoveValue::Struct(MoveStruct { fields, .. }) => &fields.first()?.1,
244        _ => return None,
245    };
246    extract_id_value(id_value)
247}
248
249pub fn extract_id_value(id_value: &MoveValue) -> Option<ObjectID> {
250    // the id struct has a single bytes field
251    let id_bytes_value = match id_value {
252        MoveValue::Struct(MoveStruct { fields, .. }) => &fields.first()?.1,
253        _ => return None,
254    };
255    // the bytes field should be an address
256    match id_bytes_value {
257        MoveValue::Address(addr) => Some(ObjectID::from(*addr)),
258        _ => None,
259    }
260}
261
262pub fn is_dynamic_object(move_struct: &MoveStruct) -> bool {
263    matches!(
264        &move_struct.type_.type_params[0],
265        TypeTag::Struct(tag) if DynamicFieldInfo::is_dynamic_object_field_wrapper(tag)
266    )
267}
268
269pub fn derive_dynamic_field_id<T>(
270    parent: T,
271    key_type_tag: &TypeTag,
272    key_bytes: &[u8],
273) -> Result<ObjectID, bcs::Error>
274where
275    T: Into<SuiAddress>,
276{
277    let parent: SuiAddress = parent.into();
278    tracing::trace!(
279        "Deriving dynamic field ID for parent={:?}, key={:?}, key_type_tag={}",
280        parent,
281        key_bytes,
282        key_type_tag.to_canonical_display(true),
283    );
284
285    // hash(parent || len(key) || key || key_type_tag)
286    let mut hasher = DefaultHash::default();
287    hasher.update([HashingIntentScope::ChildObjectId as u8]);
288    hasher.update(parent);
289    hasher.update(key_bytes.len().to_le_bytes());
290    hasher.update(key_bytes);
291    bcs::serialize_into(&mut hasher, key_type_tag)?;
292    let hash = hasher.finalize();
293
294    // truncate into an ObjectID and return
295    // OK to access slice because digest should never be shorter than ObjectID::LENGTH.
296    let id = ObjectID::try_from(&hash.as_ref()[0..ObjectID::LENGTH]).unwrap();
297    tracing::trace!("derive_dynamic_field_id result: {:?}", id);
298    Ok(id)
299}
300
301pub fn serialize_dynamic_field<K, V>(id: &UID, name: &K, value: V) -> Result<Vec<u8>, SuiError>
302where
303    K: Serialize + Clone,
304    V: Serialize,
305{
306    let field = Field::<K, V> {
307        id: id.clone(),
308        name: name.clone(),
309        value,
310    };
311
312    bcs::to_bytes(&field).map_err(|err| {
313        SuiErrorKind::ObjectSerializationError {
314            error: err.to_string(),
315        }
316        .into()
317    })
318}
319
320/// Given a parent object ID (e.g. a table), and a `key`, retrieve the corresponding dynamic field object
321/// from the `object_store`. The key type `K` must implement `MoveTypeTagTrait` which has an associated
322/// function that returns the Move type tag.
323/// Note that this function returns the Field object itself, not the value in the field.
324pub fn get_dynamic_field_object_from_store<K>(
325    object_store: &dyn ObjectStore,
326    parent_id: ObjectID,
327    key: &K,
328) -> Result<Object, SuiError>
329where
330    K: MoveTypeTagTrait + Serialize + DeserializeOwned + fmt::Debug + Clone,
331{
332    Ok(DynamicFieldKey(parent_id, key.clone(), K::get_type_tag())
333        .into_unbounded_id()?
334        .expect_object(key, object_store)?
335        .into_object())
336}
337
338/// Similar to `get_dynamic_field_object_from_store`, but returns the value in the field instead of
339/// the Field object itself.
340pub fn get_dynamic_field_from_store<K, V>(
341    object_store: &dyn ObjectStore,
342    parent_id: ObjectID,
343    key: &K,
344) -> Result<V, SuiError>
345where
346    K: MoveTypeTagTrait + Serialize + DeserializeOwned + fmt::Debug + Clone,
347    V: Serialize + DeserializeOwned,
348{
349    DynamicFieldKey(parent_id, key.clone(), K::get_type_tag())
350        .into_unbounded_id()?
351        .expect_object(key, object_store)?
352        .load_value::<V>()
353}
354
355/// A chainable API for getting dynamic fields.
356///
357/// This allows you to start with either:
358/// - a parent ID + key
359/// - or pre-hashed child ID,
360///
361/// and then allows you to read the field
362/// - consistently (with a version bound)
363/// - or inconsistently (no version bound)
364///
365/// And take the object that was read from the store and:
366/// - return the raw object
367/// - or deserialize the field value from the object
368/// - or just check if it exists
369///
370/// By chaining these together, we can have all the options above without
371/// a cross-product of functions for each case.
372///
373/// DynamicFieldKey represents the inputs into a dynamic field id computation (hash).
374/// Use it to start a lookup if you know the parent and key.
375pub struct DynamicFieldKey<ParentID, K>(pub ParentID, pub K, pub TypeTag);
376
377impl<ParentID, K> DynamicFieldKey<ParentID, K>
378where
379    ParentID: Into<SuiAddress> + Into<ObjectID> + Copy,
380    K: Serialize + fmt::Debug,
381{
382    /// Get the computed ID of the dynamic field.
383    pub fn object_id(&self) -> Result<ObjectID, SuiError> {
384        derive_dynamic_field_id(self.0, &self.2, &bcs::to_bytes(&self.1).unwrap())
385            .map_err(|e| SuiErrorKind::DynamicFieldReadError(e.to_string()).into())
386    }
387
388    /// Convert the key into a UnboundedDynamicFieldID, which can be used to load the latest
389    /// version of the field object.
390    pub fn into_unbounded_id(self) -> Result<UnboundedDynamicFieldID<K>, SuiError> {
391        let id = self.object_id()?;
392        Ok(UnboundedDynamicFieldID::<K>::new(self.0.into(), id))
393    }
394
395    /// Convert the key into a BoundedDynamicFieldID, which can be used to load the field object
396    /// with a version bound for consistent reads.
397    pub fn into_id_with_bound(
398        self,
399        parent_version: SequenceNumber,
400    ) -> Result<BoundedDynamicFieldID<K>, SuiError> {
401        let id = self.object_id()?;
402        Ok(BoundedDynamicFieldID::<K>::new(
403            self.0.into(),
404            id,
405            parent_version,
406        ))
407    }
408
409    /// Convert the key into a DynamicField, which contains the `Field<K, V>` object,
410    /// that is, the value that would be stored in the field object.
411    ///
412    /// Used to compute the contents of a field object without writing and then reading it back.
413    pub fn into_field<V>(self, value: V) -> Result<DynamicField<K, V>, SuiError>
414    where
415        V: Serialize + DeserializeOwned + MoveTypeTagTrait,
416    {
417        let id = self.object_id()?;
418        let field = Field::<K, V> {
419            id: UID::new(id),
420            name: self.1,
421            value,
422        };
423        let type_tag = TypeTag::Struct(Box::new(DynamicFieldInfo::dynamic_field_type(
424            self.2,
425            V::get_type_tag(),
426        )));
427        Ok(DynamicField(field, type_tag))
428    }
429}
430
431/// A DynamicField is a `Field<K, V>` object, that is, the value that would be stored in the field object.
432pub struct DynamicField<K, V>(Field<K, V>, TypeTag);
433
434impl<K, V> DynamicField<K, V>
435where
436    K: Serialize,
437    V: Serialize,
438{
439    /// Convert the internal `Field<K, V>` object into a Move object.
440    /// Use this to create a Move object in memory without reading one from the store.
441    ///
442    /// IMPORTANT: Do not call except in tests to avoid possible conservation bugs.
443    /// (For instance, you can simply create a Move object that contains minted SUI. If
444    /// you then write this object to the db, it will break conservation.)
445    pub fn into_move_object_unsafe_for_testing(
446        self,
447        version: SequenceNumber,
448    ) -> Result<MoveObject, SuiError> {
449        let field = self.0;
450        let type_tag = self.1;
451        let TypeTag::Struct(struct_tag) = type_tag else {
452            unreachable!()
453        };
454        // TODO(address-balances): more efficient type repr
455        let move_object_type = MoveObjectType::from(*struct_tag);
456
457        let field_bytes =
458            bcs::to_bytes(&field).map_err(|e| SuiErrorKind::ObjectSerializationError {
459                error: e.to_string(),
460            })?;
461        Ok(unsafe {
462            MoveObject::new_from_execution_with_limit(
463                move_object_type,
464                false, // A dynamic field is never transferable, public or otherwise.
465                version,
466                field_bytes,
467                512,
468            )
469        }?)
470    }
471
472    /// Get the internal `Field<K, V>` object.
473    pub fn into_inner(self) -> Field<K, V> {
474        self.0
475    }
476}
477
478/// A UnboundedDynamicFieldID contains the material needed to load an a dynamic field from
479/// the store.
480///
481/// Can be obtained from a DynamicFieldKey, or created directly if you know the
482/// parent and child IDs but do not know the key.
483pub struct UnboundedDynamicFieldID<K: Serialize>(
484    pub ObjectID, // parent
485    pub ObjectID, // child
486    std::marker::PhantomData<K>,
487);
488
489impl<K> UnboundedDynamicFieldID<K>
490where
491    K: Serialize + std::fmt::Debug,
492{
493    /// Create a UnboundedDynamicFieldID from a parent and child ID.
494    pub fn new(parent: ObjectID, id: ObjectID) -> Self {
495        Self(parent, id, std::marker::PhantomData)
496    }
497
498    /// Load the field object from the store.
499    pub fn load_object(self, object_store: &dyn ObjectStore) -> Option<DynamicFieldObject<K>> {
500        object_store
501            .get_object(&self.1)
502            .map(DynamicFieldObject::<K>::new)
503    }
504
505    /// Load the field object from the store.
506    /// If the field does not exist, return an error.
507    pub fn expect_object(
508        self,
509        key: &K,
510        object_store: &dyn ObjectStore,
511    ) -> Result<DynamicFieldObject<K>, SuiError> {
512        let parent = self.0;
513        let id = self.1;
514        self.load_object(object_store).ok_or_else(|| {
515            {
516                SuiErrorKind::DynamicFieldReadError(format!(
517                    "Dynamic field with key={:?} and ID={:?} not found on parent {:?}",
518                    key, id, parent
519                ))
520            }
521            .into()
522        })
523    }
524
525    /// Check if the field object exists in the store.
526    pub fn exists(self, object_store: &dyn ObjectStore) -> bool {
527        self.load_object(object_store).is_some()
528    }
529
530    /// Convert an UnboundedDynamicFieldID into a BoundedDynamicFieldID, which can then
531    /// be used to do a consistent lookup of the field.
532    pub fn with_bound(self, parent_version: SequenceNumber) -> BoundedDynamicFieldID<K> {
533        BoundedDynamicFieldID::new(self.0, self.1, parent_version)
534    }
535
536    /// Get the child ID.
537    pub fn as_object_id(self) -> ObjectID {
538        self.1
539    }
540}
541
542/// A BoundedDynamicFieldID contains the material needed to load an a dynamic field from
543/// the store, along with a parent version bound. The returned field will be the highest
544/// version of the field object that has a version less than or equal to the bound.
545///
546/// Can be obtained from a DynamicFieldID by calling `with_bound`, or created directly
547/// if you know the parent and child IDs and the parent version bound.
548pub struct BoundedDynamicFieldID<K: Serialize>(
549    pub ObjectID,       // parent
550    pub ObjectID,       // child
551    pub SequenceNumber, // parent version
552    std::marker::PhantomData<K>,
553);
554
555impl<K> BoundedDynamicFieldID<K>
556where
557    K: Serialize,
558{
559    /// Create a BoundedDynamicFieldID from a parent and child ID and a parent version.
560    pub fn new(parent_id: ObjectID, child_id: ObjectID, parent_version: SequenceNumber) -> Self {
561        Self(
562            parent_id,
563            child_id,
564            parent_version,
565            std::marker::PhantomData,
566        )
567    }
568
569    /// Load the field object from the store.
570    /// If the field does not exist, return None.
571    pub fn load_object(
572        self,
573        child_object_resolver: &dyn ChildObjectResolver,
574    ) -> Result<Option<DynamicFieldObject<K>>, SuiError> {
575        child_object_resolver
576            .read_child_object(&self.0, &self.1, self.2)
577            .map(|r| r.map(DynamicFieldObject::<K>::new))
578    }
579
580    /// Check if the field object exists in the store.
581    pub fn exists(self, child_object_resolver: &dyn ChildObjectResolver) -> Result<bool, SuiError> {
582        self.load_object(child_object_resolver).map(|r| r.is_some())
583    }
584}
585
586/// A DynamicFieldObject is a wrapper around an Object that contains a `Field<K, V>` object.
587pub struct DynamicFieldObject<K>(pub Object, std::marker::PhantomData<K>);
588
589impl<K> DynamicFieldObject<K> {
590    /// Create a DynamicFieldObject directly from an Object.
591    pub fn new(object: Object) -> Self {
592        Self(object, std::marker::PhantomData)
593    }
594
595    /// Get the underlying Object.
596    pub fn into_object(self) -> Object {
597        self.0
598    }
599
600    pub fn as_object(&self) -> &Object {
601        &self.0
602    }
603}
604
605impl<K> DynamicFieldObject<K>
606where
607    K: Serialize + DeserializeOwned,
608{
609    /// Deserialize the field value from the object. Requires that the value type is known.
610    pub fn load_value<V>(self) -> Result<V, SuiError>
611    where
612        V: Serialize + DeserializeOwned,
613    {
614        self.load_field::<V>().map(|f| f.value)
615    }
616
617    /// Deserialize the field value from the object. Requires that the value type is known.
618    pub fn load_field<V>(self) -> Result<Field<K, V>, SuiError>
619    where
620        V: Serialize + DeserializeOwned,
621    {
622        let object = self.0;
623        let move_object = object.data.try_as_move().ok_or_else(|| {
624            SuiErrorKind::DynamicFieldReadError(format!(
625                "Dynamic field {:?} is not a Move object",
626                object.id()
627            ))
628        })?;
629        bcs::from_bytes::<Field<K, V>>(move_object.contents())
630            .map_err(|err| SuiErrorKind::DynamicFieldReadError(err.to_string()).into())
631    }
632}