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)]
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)]
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    let k_tag_bytes = bcs::to_bytes(key_type_tag)?;
279    tracing::trace!(
280        "Deriving dynamic field ID for parent={:?}, key={:?}, key_type_tag={:?}",
281        parent,
282        key_bytes,
283        key_type_tag,
284    );
285
286    // hash(parent || len(key) || key || key_type_tag)
287    let mut hasher = DefaultHash::default();
288    hasher.update([HashingIntentScope::ChildObjectId as u8]);
289    hasher.update(parent);
290    hasher.update(key_bytes.len().to_le_bytes());
291    hasher.update(key_bytes);
292    hasher.update(k_tag_bytes);
293    let hash = hasher.finalize();
294
295    // truncate into an ObjectID and return
296    // OK to access slice because digest should never be shorter than ObjectID::LENGTH.
297    let id = ObjectID::try_from(&hash.as_ref()[0..ObjectID::LENGTH]).unwrap();
298    tracing::trace!("derive_dynamic_field_id result: {:?}", id);
299    Ok(id)
300}
301
302pub fn serialize_dynamic_field<K, V>(id: &UID, name: &K, value: V) -> Result<Vec<u8>, SuiError>
303where
304    K: Serialize + Clone,
305    V: Serialize,
306{
307    let field = Field::<K, V> {
308        id: id.clone(),
309        name: name.clone(),
310        value,
311    };
312
313    bcs::to_bytes(&field).map_err(|err| {
314        SuiErrorKind::ObjectSerializationError {
315            error: err.to_string(),
316        }
317        .into()
318    })
319}
320
321/// Given a parent object ID (e.g. a table), and a `key`, retrieve the corresponding dynamic field object
322/// from the `object_store`. The key type `K` must implement `MoveTypeTagTrait` which has an associated
323/// function that returns the Move type tag.
324/// Note that this function returns the Field object itself, not the value in the field.
325pub fn get_dynamic_field_object_from_store<K>(
326    object_store: &dyn ObjectStore,
327    parent_id: ObjectID,
328    key: &K,
329) -> Result<Object, SuiError>
330where
331    K: MoveTypeTagTrait + Serialize + DeserializeOwned + fmt::Debug + Clone,
332{
333    Ok(DynamicFieldKey(parent_id, key.clone(), K::get_type_tag())
334        .into_unbounded_id()?
335        .expect_object(key, object_store)?
336        .as_object())
337}
338
339/// Similar to `get_dynamic_field_object_from_store`, but returns the value in the field instead of
340/// the Field object itself.
341pub fn get_dynamic_field_from_store<K, V>(
342    object_store: &dyn ObjectStore,
343    parent_id: ObjectID,
344    key: &K,
345) -> Result<V, SuiError>
346where
347    K: MoveTypeTagTrait + Serialize + DeserializeOwned + fmt::Debug + Clone,
348    V: Serialize + DeserializeOwned,
349{
350    DynamicFieldKey(parent_id, key.clone(), K::get_type_tag())
351        .into_unbounded_id()?
352        .expect_object(key, object_store)?
353        .load_value::<V>()
354}
355
356/// A chainable API for getting dynamic fields.
357///
358/// This allows you to start with either:
359/// - a parent ID + key
360/// - or pre-hashed child ID,
361///
362/// and then allows you to read the field
363/// - consistently (with a version bound)
364/// - or inconsistently (no version bound)
365///
366/// And take the object that was read from the store and:
367/// - return the raw object
368/// - or deserialize the field value from the object
369/// - or just check if it exists
370///
371/// By chaining these together, we can have all the options above without
372/// a cross-product of functions for each case.
373///
374/// DynamicFieldKey represents the inputs into a dynamic field id computation (hash).
375/// Use it to start a lookup if you know the parent and key.
376pub struct DynamicFieldKey<ParentID, K>(pub ParentID, pub K, pub TypeTag);
377
378impl<ParentID, K> DynamicFieldKey<ParentID, K>
379where
380    ParentID: Into<SuiAddress> + Into<ObjectID> + Copy,
381    K: Serialize + fmt::Debug,
382{
383    /// Get the computed ID of the dynamic field.
384    pub fn object_id(&self) -> Result<ObjectID, SuiError> {
385        derive_dynamic_field_id(self.0, &self.2, &bcs::to_bytes(&self.1).unwrap())
386            .map_err(|e| SuiErrorKind::DynamicFieldReadError(e.to_string()).into())
387    }
388
389    /// Convert the key into a UnboundedDynamicFieldID, which can be used to load the latest
390    /// version of the field object.
391    pub fn into_unbounded_id(self) -> Result<UnboundedDynamicFieldID<K>, SuiError> {
392        let id = self.object_id()?;
393        Ok(UnboundedDynamicFieldID::<K>::new(self.0.into(), id))
394    }
395
396    /// Convert the key into a BoundedDynamicFieldID, which can be used to load the field object
397    /// with a version bound for consistent reads.
398    pub fn into_id_with_bound(
399        self,
400        parent_version: SequenceNumber,
401    ) -> Result<BoundedDynamicFieldID<K>, SuiError> {
402        let id = self.object_id()?;
403        Ok(BoundedDynamicFieldID::<K>::new(
404            self.0.into(),
405            id,
406            parent_version,
407        ))
408    }
409
410    /// Convert the key into a DynamicField, which contains the `Field<K, V>` object,
411    /// that is, the value that would be stored in the field object.
412    ///
413    /// Used to compute the contents of a field object without writing and then reading it back.
414    pub fn into_field<V>(self, value: V) -> Result<DynamicField<K, V>, SuiError>
415    where
416        V: Serialize + DeserializeOwned + MoveTypeTagTrait,
417    {
418        let id = self.object_id()?;
419        let field = Field::<K, V> {
420            id: UID::new(id),
421            name: self.1,
422            value,
423        };
424        let type_tag = TypeTag::Struct(Box::new(DynamicFieldInfo::dynamic_field_type(
425            self.2,
426            V::get_type_tag(),
427        )));
428        Ok(DynamicField(field, type_tag))
429    }
430}
431
432/// A DynamicField is a `Field<K, V>` object, that is, the value that would be stored in the field object.
433pub struct DynamicField<K, V>(Field<K, V>, TypeTag);
434
435impl<K, V> DynamicField<K, V>
436where
437    K: Serialize,
438    V: Serialize,
439{
440    /// Convert the internal `Field<K, V>` object into a Move object.
441    /// Use this to create a Move object in memory without reading one from the store.
442    ///
443    /// IMPORTANT: Do not call except in tests to avoid possible conservation bugs.
444    /// (For instance, you can simply create a Move object that contains minted SUI. If
445    /// you then write this object to the db, it will break conservation.)
446    pub fn into_move_object_unsafe_for_testing(
447        self,
448        version: SequenceNumber,
449    ) -> Result<MoveObject, SuiError> {
450        let field = self.0;
451        let type_tag = self.1;
452        let TypeTag::Struct(struct_tag) = type_tag else {
453            unreachable!()
454        };
455        // TODO(address-balances): more efficient type repr
456        let move_object_type = MoveObjectType::from(*struct_tag);
457
458        let field_bytes =
459            bcs::to_bytes(&field).map_err(|e| SuiErrorKind::ObjectSerializationError {
460                error: e.to_string(),
461            })?;
462        Ok(unsafe {
463            MoveObject::new_from_execution_with_limit(
464                move_object_type,
465                false, // A dynamic field is never transferable, public or otherwise.
466                version,
467                field_bytes,
468                512,
469            )
470        }?)
471    }
472
473    /// Get the internal `Field<K, V>` object.
474    pub fn into_inner(self) -> Field<K, V> {
475        self.0
476    }
477}
478
479/// A UnboundedDynamicFieldID contains the material needed to load an a dynamic field from
480/// the store.
481///
482/// Can be obtained from a DynamicFieldKey, or created directly if you know the
483/// parent and child IDs but do not know the key.
484pub struct UnboundedDynamicFieldID<K: Serialize>(
485    pub ObjectID, // parent
486    pub ObjectID, // child
487    std::marker::PhantomData<K>,
488);
489
490impl<K> UnboundedDynamicFieldID<K>
491where
492    K: Serialize + std::fmt::Debug,
493{
494    /// Create a UnboundedDynamicFieldID from a parent and child ID.
495    pub fn new(parent: ObjectID, id: ObjectID) -> Self {
496        Self(parent, id, std::marker::PhantomData)
497    }
498
499    /// Load the field object from the store.
500    pub fn load_object(self, object_store: &dyn ObjectStore) -> Option<DynamicFieldObject<K>> {
501        object_store
502            .get_object(&self.1)
503            .map(DynamicFieldObject::<K>::new)
504    }
505
506    /// Load the field object from the store.
507    /// If the field does not exist, return an error.
508    pub fn expect_object(
509        self,
510        key: &K,
511        object_store: &dyn ObjectStore,
512    ) -> Result<DynamicFieldObject<K>, SuiError> {
513        let parent = self.0;
514        let id = self.1;
515        self.load_object(object_store).ok_or_else(|| {
516            {
517                SuiErrorKind::DynamicFieldReadError(format!(
518                    "Dynamic field with key={:?} and ID={:?} not found on parent {:?}",
519                    key, id, parent
520                ))
521            }
522            .into()
523        })
524    }
525
526    /// Check if the field object exists in the store.
527    pub fn exists(self, object_store: &dyn ObjectStore) -> bool {
528        self.load_object(object_store).is_some()
529    }
530
531    /// Convert an UnboundedDynamicFieldID into a BoundedDynamicFieldID, which can then
532    /// be used to do a consistent lookup of the field.
533    pub fn with_bound(self, parent_version: SequenceNumber) -> BoundedDynamicFieldID<K> {
534        BoundedDynamicFieldID::new(self.0, self.1, parent_version)
535    }
536
537    /// Get the child ID.
538    pub fn as_object_id(self) -> ObjectID {
539        self.1
540    }
541}
542
543/// A BoundedDynamicFieldID contains the material needed to load an a dynamic field from
544/// the store, along with a parent version bound. The returned field will be the highest
545/// version of the field object that has a version less than or equal to the bound.
546///
547/// Can be obtained from a DynamicFieldID by calling `with_bound`, or created directly
548/// if you know the parent and child IDs and the parent version bound.
549pub struct BoundedDynamicFieldID<K: Serialize>(
550    pub ObjectID,       // parent
551    pub ObjectID,       // child
552    pub SequenceNumber, // parent version
553    std::marker::PhantomData<K>,
554);
555
556impl<K> BoundedDynamicFieldID<K>
557where
558    K: Serialize,
559{
560    /// Create a BoundedDynamicFieldID from a parent and child ID and a parent version.
561    pub fn new(parent_id: ObjectID, child_id: ObjectID, parent_version: SequenceNumber) -> Self {
562        Self(
563            parent_id,
564            child_id,
565            parent_version,
566            std::marker::PhantomData,
567        )
568    }
569
570    /// Load the field object from the store.
571    /// If the field does not exist, return None.
572    pub fn load_object(
573        self,
574        child_object_resolver: &dyn ChildObjectResolver,
575    ) -> Result<Option<DynamicFieldObject<K>>, SuiError> {
576        child_object_resolver
577            .read_child_object(&self.0, &self.1, self.2)
578            .map(|r| r.map(DynamicFieldObject::<K>::new))
579    }
580
581    /// Check if the field object exists in the store.
582    pub fn exists(self, child_object_resolver: &dyn ChildObjectResolver) -> Result<bool, SuiError> {
583        self.load_object(child_object_resolver).map(|r| r.is_some())
584    }
585}
586
587/// A DynamicFieldObject is a wrapper around an Object that contains a `Field<K, V>` object.
588pub struct DynamicFieldObject<K>(pub Object, std::marker::PhantomData<K>);
589
590impl<K> DynamicFieldObject<K> {
591    /// Create a DynamicFieldObject directly from an Object.
592    pub fn new(object: Object) -> Self {
593        Self(object, std::marker::PhantomData)
594    }
595
596    /// Get the underlying Object.
597    pub fn as_object(self) -> Object {
598        self.0
599    }
600}
601
602impl<K> DynamicFieldObject<K>
603where
604    K: Serialize + DeserializeOwned,
605{
606    /// Deserialize the field value from the object. Requires that the value type is known.
607    pub fn load_value<V>(self) -> Result<V, SuiError>
608    where
609        V: Serialize + DeserializeOwned,
610    {
611        self.load_field::<V>().map(|f| f.value)
612    }
613
614    /// Deserialize the field value from the object. Requires that the value type is known.
615    pub fn load_field<V>(self) -> Result<Field<K, V>, SuiError>
616    where
617        V: Serialize + DeserializeOwned,
618    {
619        let object = self.0;
620        let move_object = object.data.try_as_move().ok_or_else(|| {
621            SuiErrorKind::DynamicFieldReadError(format!(
622                "Dynamic field {:?} is not a Move object",
623                object.id()
624            ))
625        })?;
626        bcs::from_bytes::<Field<K, V>>(move_object.contents())
627            .map_err(|err| SuiErrorKind::DynamicFieldReadError(err.to_string()).into())
628    }
629}