Skip to main content

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