iota_types/
dynamic_field.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    fmt,
7    fmt::{Display, Formatter},
8};
9
10use fastcrypto::{encoding::Base64, hash::HashFunction};
11use move_core_types::{
12    annotated_value::{MoveStruct, MoveValue},
13    ident_str,
14    identifier::IdentStr,
15    language_storage::{StructTag, TypeTag},
16};
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize, de::DeserializeOwned};
19use serde_json::Value;
20use serde_with::{DisplayFromStr, serde_as};
21use shared_crypto::intent::HashingIntentScope;
22
23use crate::{
24    IOTA_FRAMEWORK_ADDRESS, MoveTypeTagTrait, ObjectID, SequenceNumber,
25    base_types::{IotaAddress, ObjectDigest},
26    crypto::DefaultHash,
27    error::{IotaError, IotaResult},
28    id::UID,
29    iota_serde::{IotaTypeTag, Readable},
30    object::Object,
31    storage::ObjectStore,
32};
33
34pub mod visitor;
35
36const DYNAMIC_FIELD_MODULE_NAME: &IdentStr = ident_str!("dynamic_field");
37const DYNAMIC_FIELD_FIELD_STRUCT_NAME: &IdentStr = ident_str!("Field");
38
39const DYNAMIC_OBJECT_FIELD_MODULE_NAME: &IdentStr = ident_str!("dynamic_object_field");
40const DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME: &IdentStr = ident_str!("Wrapper");
41
42/// Rust version of the Move iota::dynamic_field::Field type
43#[derive(Clone, Serialize, Deserialize, Debug)]
44pub struct Field<N, V> {
45    pub id: UID,
46    pub name: N,
47    pub value: V,
48}
49
50/// Rust version of the Move iota::dynamic_object_field::Wrapper type
51#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
52pub struct DOFWrapper<N> {
53    pub name: N,
54}
55
56impl<N> MoveTypeTagTrait for DOFWrapper<N>
57where
58    N: MoveTypeTagTrait,
59{
60    fn get_type_tag() -> TypeTag {
61        TypeTag::Struct(Box::new(DynamicFieldInfo::dynamic_object_field_wrapper(
62            N::get_type_tag(),
63        )))
64    }
65}
66
67#[serde_as]
68#[derive(Clone, Serialize, Deserialize, Debug)]
69#[serde(rename_all = "camelCase")]
70pub struct DynamicFieldInfo {
71    pub name: DynamicFieldName,
72    #[serde_as(as = "Readable<Base64, _>")]
73    pub bcs_name: Vec<u8>,
74    pub type_: DynamicFieldType,
75    pub object_type: String,
76    pub object_id: ObjectID,
77    pub version: SequenceNumber,
78    pub digest: ObjectDigest,
79}
80
81#[serde_as]
82#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
83#[serde(rename_all = "camelCase")]
84pub struct DynamicFieldName {
85    #[schemars(with = "String")]
86    #[serde_as(as = "Readable<IotaTypeTag, _>")]
87    pub type_: TypeTag,
88    // Bincode does not like serde_json::Value, rocksdb will not insert the value without
89    // serializing value as string. TODO: investigate if this can be removed after switch to
90    // BCS.
91    #[schemars(with = "Value")]
92    #[serde_as(as = "Readable<_, DisplayFromStr>")]
93    pub value: Value,
94}
95
96impl Display for DynamicFieldName {
97    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
98        write!(f, "{}: {}", self.type_, self.value)
99    }
100}
101
102#[derive(
103    Copy, Clone, Serialize, Deserialize, JsonSchema, Ord, PartialOrd, Eq, PartialEq, Debug,
104)]
105pub enum DynamicFieldType {
106    #[serde(rename_all = "camelCase")]
107    DynamicField,
108    DynamicObject,
109}
110
111impl Display for DynamicFieldType {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            DynamicFieldType::DynamicField => write!(f, "DynamicField"),
115            DynamicFieldType::DynamicObject => write!(f, "DynamicObject"),
116        }
117    }
118}
119
120impl DynamicFieldInfo {
121    pub fn is_dynamic_field(tag: &StructTag) -> bool {
122        tag.address == IOTA_FRAMEWORK_ADDRESS
123            && tag.module.as_ident_str() == DYNAMIC_FIELD_MODULE_NAME
124            && tag.name.as_ident_str() == DYNAMIC_FIELD_FIELD_STRUCT_NAME
125    }
126
127    pub fn is_dynamic_object_field_wrapper(tag: &StructTag) -> bool {
128        tag.address == IOTA_FRAMEWORK_ADDRESS
129            && tag.module.as_ident_str() == DYNAMIC_OBJECT_FIELD_MODULE_NAME
130            && tag.name.as_ident_str() == DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME
131    }
132
133    pub fn dynamic_field_type(key: TypeTag, value: TypeTag) -> StructTag {
134        StructTag {
135            address: IOTA_FRAMEWORK_ADDRESS,
136            name: DYNAMIC_FIELD_FIELD_STRUCT_NAME.to_owned(),
137            module: DYNAMIC_FIELD_MODULE_NAME.to_owned(),
138            type_params: vec![key, value],
139        }
140    }
141
142    pub fn dynamic_object_field_wrapper(key: TypeTag) -> StructTag {
143        StructTag {
144            address: IOTA_FRAMEWORK_ADDRESS,
145            module: DYNAMIC_OBJECT_FIELD_MODULE_NAME.to_owned(),
146            name: DYNAMIC_OBJECT_FIELD_WRAPPER_STRUCT_NAME.to_owned(),
147            type_params: vec![key],
148        }
149    }
150
151    pub fn try_extract_field_name(
152        tag: &StructTag,
153        type_: &DynamicFieldType,
154    ) -> IotaResult<TypeTag> {
155        match (type_, tag.type_params.first()) {
156            (DynamicFieldType::DynamicField, Some(name_type)) => Ok(name_type.clone()),
157            (DynamicFieldType::DynamicObject, Some(TypeTag::Struct(s))) => Ok(s
158                .type_params
159                .first()
160                .ok_or_else(|| IotaError::ObjectDeserialization {
161                    error: format!("Error extracting dynamic object name from object: {tag}"),
162                })?
163                .clone()),
164            _ => Err(IotaError::ObjectDeserialization {
165                error: format!("Error extracting dynamic object name from object: {tag}"),
166            }),
167        }
168    }
169
170    pub fn try_extract_field_value(tag: &StructTag) -> IotaResult<TypeTag> {
171        match tag.type_params.last() {
172            Some(value_type) => Ok(value_type.clone()),
173            None => Err(IotaError::ObjectDeserialization {
174                error: format!("Error extracting dynamic object value from object: {tag}"),
175            }),
176        }
177    }
178
179    pub fn parse_move_object(
180        move_struct: &MoveStruct,
181    ) -> IotaResult<(MoveValue, DynamicFieldType, ObjectID)> {
182        let name = extract_field_from_move_struct(move_struct, "name").ok_or_else(|| {
183            IotaError::ObjectDeserialization {
184                error: "Cannot extract [name] field from iota::dynamic_field::Field".to_string(),
185            }
186        })?;
187
188        let value = extract_field_from_move_struct(move_struct, "value").ok_or_else(|| {
189            IotaError::ObjectDeserialization {
190                error: "Cannot extract [value] field from iota::dynamic_field::Field".to_string(),
191            }
192        })?;
193
194        Ok(if is_dynamic_object(move_struct) {
195            let name = match name {
196                MoveValue::Struct(name_struct) => {
197                    extract_field_from_move_struct(name_struct, "name")
198                }
199                _ => None,
200            }
201            .ok_or_else(|| IotaError::ObjectDeserialization {
202                error: "Cannot extract [name] field from iota::dynamic_object_field::Wrapper."
203                    .to_string(),
204            })?;
205            // ID extracted from the wrapper object
206            let object_id =
207                extract_id_value(value).ok_or_else(|| IotaError::ObjectDeserialization {
208                    error: format!(
209                        "Cannot extract dynamic object's object id from \
210                        iota::dynamic_field::Field, {value:?}"
211                    ),
212                })?;
213            (name.clone(), DynamicFieldType::DynamicObject, object_id)
214        } else {
215            // ID of the Field object
216            let object_id =
217                extract_object_id(move_struct).ok_or_else(|| IotaError::ObjectDeserialization {
218                    error: format!(
219                        "Cannot extract dynamic object's object id from \
220                        iota::dynamic_field::Field, {move_struct:?}",
221                    ),
222                })?;
223            (name.clone(), DynamicFieldType::DynamicField, object_id)
224        })
225    }
226}
227
228pub fn extract_field_from_move_struct<'a>(
229    move_struct: &'a MoveStruct,
230    field_name: &str,
231) -> Option<&'a MoveValue> {
232    move_struct.fields.iter().find_map(|(id, value)| {
233        if id.to_string() == field_name {
234            Some(value)
235        } else {
236            None
237        }
238    })
239}
240
241fn extract_object_id(value: &MoveStruct) -> Option<ObjectID> {
242    // id:UID is the first value in an object
243    let uid_value = &value.fields.first()?.1;
244
245    // id is the first value in UID
246    let id_value = match uid_value {
247        MoveValue::Struct(MoveStruct { fields, .. }) => &fields.first()?.1,
248        _ => return None,
249    };
250    extract_id_value(id_value)
251}
252
253pub fn extract_id_value(id_value: &MoveValue) -> Option<ObjectID> {
254    // the id struct has a single bytes field
255    let id_bytes_value = match id_value {
256        MoveValue::Struct(MoveStruct { fields, .. }) => &fields.first()?.1,
257        _ => return None,
258    };
259    // the bytes field should be an address
260    match id_bytes_value {
261        MoveValue::Address(addr) => Some(ObjectID::from(*addr)),
262        _ => None,
263    }
264}
265
266pub fn is_dynamic_object(move_struct: &MoveStruct) -> bool {
267    matches!(
268        &move_struct.type_.type_params[0],
269        TypeTag::Struct(tag) if DynamicFieldInfo::is_dynamic_object_field_wrapper(tag)
270    )
271}
272
273pub fn derive_dynamic_field_id<T>(
274    parent: T,
275    key_type_tag: &TypeTag,
276    key_bytes: &[u8],
277) -> Result<ObjectID, bcs::Error>
278where
279    T: Into<IotaAddress>,
280{
281    let parent: IotaAddress = parent.into();
282    let k_tag_bytes = bcs::to_bytes(key_type_tag)?;
283    tracing::trace!(
284        "Deriving dynamic field ID for parent={:?}, key={:?}, key_type_tag={:?}",
285        parent,
286        key_bytes,
287        key_type_tag,
288    );
289
290    // hash(parent || len(key) || key || key_type_tag)
291    let mut hasher = DefaultHash::default();
292    hasher.update([HashingIntentScope::ChildObjectId as u8]);
293    hasher.update(parent);
294    hasher.update(key_bytes.len().to_le_bytes());
295    hasher.update(key_bytes);
296    hasher.update(k_tag_bytes);
297    let hash = hasher.finalize();
298
299    // truncate into an ObjectID and return
300    // OK to access slice because digest should never be shorter than
301    // ObjectID::LENGTH.
302    let id = ObjectID::try_from(&hash.as_ref()[0..ObjectID::LENGTH]).unwrap();
303    tracing::trace!("derive_dynamic_field_id result: {:?}", id);
304    Ok(id)
305}
306
307/// Given a parent object ID (e.g. a table), and a `key`, retrieve the
308/// corresponding dynamic field object from the `object_store`. The key type `K`
309/// must implement `MoveTypeTagTrait` which has an associated function that
310/// returns the Move type tag. Note that this function returns the Field object
311/// itself, not the value in the field.
312pub fn get_dynamic_field_object_from_store<K>(
313    object_store: &dyn ObjectStore,
314    parent_id: ObjectID,
315    key: &K,
316) -> Result<Object, IotaError>
317where
318    K: MoveTypeTagTrait + Serialize + DeserializeOwned + fmt::Debug,
319{
320    let id = derive_dynamic_field_id(parent_id, &K::get_type_tag(), &bcs::to_bytes(key).unwrap())
321        .map_err(|err| IotaError::DynamicFieldRead(err.to_string()))?;
322    let object = object_store.get_object(&id)?.ok_or_else(|| {
323        IotaError::DynamicFieldRead(format!(
324            "Dynamic field with key={:?} and ID={:?} not found on parent {:?}",
325            key, id, parent_id
326        ))
327    })?;
328    Ok(object)
329}
330
331/// Similar to `get_dynamic_field_object_from_store`, but returns the value in
332/// the field instead of the Field object itself.
333pub fn get_dynamic_field_from_store<K, V>(
334    object_store: &dyn ObjectStore,
335    parent_id: ObjectID,
336    key: &K,
337) -> Result<V, IotaError>
338where
339    K: MoveTypeTagTrait + Serialize + DeserializeOwned + fmt::Debug,
340    V: Serialize + DeserializeOwned,
341{
342    let object = get_dynamic_field_object_from_store(object_store, parent_id, key)?;
343    let move_object = object.data.try_as_move().ok_or_else(|| {
344        IotaError::DynamicFieldRead(format!(
345            "Dynamic field {:?} is not a Move object",
346            object.id()
347        ))
348    })?;
349    Ok(bcs::from_bytes::<Field<K, V>>(move_object.contents())
350        .map_err(|err| IotaError::DynamicFieldRead(err.to_string()))?
351        .value)
352}