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