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