iota_graphql_rpc/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 async_graphql::{
6    connection::{Connection, CursorType, Edge},
7    *,
8};
9use iota_indexer::{models::objects::StoredHistoryObject, types::OwnerType};
10use iota_types::{
11    TypeTag,
12    dynamic_field::{
13        DynamicFieldInfo, DynamicFieldType, derive_dynamic_field_id,
14        visitor::{Field, FieldVisitor},
15    },
16};
17
18use crate::{
19    consistency::{View, build_objects_query},
20    data::{Db, QueryExecutor, package_resolver::PackageResolver},
21    error::Error,
22    filter,
23    raw_query::RawQuery,
24    types::{
25        available_range::AvailableRange,
26        base64::Base64,
27        cursor::{Page, Target},
28        iota_address::IotaAddress,
29        move_object::MoveObject,
30        move_value::MoveValue,
31        object::{self, Object, ObjectKind},
32        type_filter::ExactTypeFilter,
33    },
34};
35
36pub(crate) struct DynamicField {
37    pub super_: MoveObject,
38}
39
40#[derive(Union)]
41pub(crate) enum DynamicFieldValue {
42    MoveObject(MoveObject), // DynamicObject
43    MoveValue(MoveValue),   // DynamicField
44}
45
46#[derive(InputObject)] // used as input object
47pub(crate) struct DynamicFieldName {
48    /// The string type of the DynamicField's 'name' field.
49    /// A string representation of a Move primitive like 'u64', or a struct type
50    /// like '0x2::kiosk::Listing'
51    pub type_: ExactTypeFilter,
52    /// The Base64 encoded bcs serialization of the DynamicField's 'name' field.
53    pub bcs: Base64,
54}
55
56/// Dynamic fields are heterogeneous fields that can be added or removed at
57/// runtime, and can have arbitrary user-assigned names. There are two sub-types
58/// of dynamic fields:
59///
60/// 1) Dynamic Fields can store any value that has the `store` ability, however
61///    an object stored in this kind of field will be considered wrapped and
62///    will not be accessible directly via its ID by external tools (explorers,
63///    wallets, etc) accessing storage.
64/// 2) Dynamic Object Fields values must be IOTA objects (have the `key` and
65///    `store` abilities, and id: UID as the first field), but will still be
66///    directly accessible off-chain via their object ID after being attached.
67#[Object]
68impl DynamicField {
69    /// The string type, data, and serialized value of the DynamicField's 'name'
70    /// field. This field is used to uniquely identify a child of the parent
71    /// object.
72    async fn name(&self, ctx: &Context<'_>) -> Result<Option<MoveValue>> {
73        let resolver: &PackageResolver = ctx.data_unchecked();
74
75        let type_ = TypeTag::from(self.super_.native.type_().clone());
76        let layout = resolver.type_layout(type_.clone()).await.map_err(|e| {
77            Error::Internal(format!(
78                "Error fetching layout for type {}: {e}",
79                type_.to_canonical_display(/* with_prefix */ true)
80            ))
81        })?;
82
83        let Field {
84            name_layout,
85            name_bytes,
86            ..
87        } = FieldVisitor::deserialize(self.super_.native.contents(), &layout)
88            .map_err(|e| Error::Internal(e.to_string()))
89            .extend()?;
90
91        Ok(Some(MoveValue::new(
92            name_layout.into(),
93            Base64::from(name_bytes.to_owned()),
94        )))
95    }
96
97    /// The returned dynamic field is an object if its return type is
98    /// `MoveObject`, in which case it is also accessible off-chain via its
99    /// address. Its contents will be from the latest version that is at
100    /// most equal to its parent object's version.
101    async fn value(&self, ctx: &Context<'_>) -> Result<Option<DynamicFieldValue>> {
102        let resolver: &PackageResolver = ctx.data_unchecked();
103
104        let type_ = TypeTag::from(self.super_.native.type_().clone());
105        let layout = resolver.type_layout(type_.clone()).await.map_err(|e| {
106            Error::Internal(format!(
107                "Error fetching layout for type {}: {e}",
108                type_.to_canonical_display(/* with_prefix */ true)
109            ))
110        })?;
111
112        let Field {
113            kind,
114            value_layout,
115            value_bytes,
116            ..
117        } = FieldVisitor::deserialize(self.super_.native.contents(), &layout)
118            .map_err(|e| Error::Internal(e.to_string()))
119            .extend()?;
120
121        if kind == DynamicFieldType::DynamicObject {
122            let df_object_id: IotaAddress = bcs::from_bytes(value_bytes)
123                .map_err(|e| Error::Internal(format!("Failed to deserialize object ID: {e}")))
124                .extend()?;
125
126            let obj = MoveObject::query(
127                ctx,
128                df_object_id,
129                Object::under_parent(self.root_version(), self.super_.super_.checkpoint_viewed_at),
130            )
131            .await
132            .extend()?;
133
134            Ok(obj.map(DynamicFieldValue::MoveObject))
135        } else {
136            Ok(Some(DynamicFieldValue::MoveValue(MoveValue::new(
137                value_layout.into(),
138                Base64::from(value_bytes.to_owned()),
139            ))))
140        }
141    }
142}
143
144impl DynamicField {
145    /// Fetch a single dynamic field entry from the `db`, on `parent` object,
146    /// with field name `name`, and kind `kind` (dynamic field or dynamic
147    /// object field). The dynamic field is bound by the `parent_version` if
148    /// provided - the fetched field will be the latest version at or before
149    /// the provided version. If `parent_version` is not provided, the latest
150    /// version of the field is returned as bounded by the
151    /// `checkpoint_viewed_at` parameter.
152    pub(crate) async fn query(
153        ctx: &Context<'_>,
154        parent: IotaAddress,
155        parent_version: Option<u64>,
156        name: DynamicFieldName,
157        kind: DynamicFieldType,
158        checkpoint_viewed_at: u64,
159    ) -> Result<Option<DynamicField>, Error> {
160        let type_ = match kind {
161            DynamicFieldType::DynamicField => name.type_.0,
162            DynamicFieldType::DynamicObject => {
163                DynamicFieldInfo::dynamic_object_field_wrapper(name.type_.0).into()
164            }
165        };
166
167        let field_id = derive_dynamic_field_id(parent, &type_, &name.bcs.0)
168            .map_err(|e| Error::Internal(format!("Failed to derive dynamic field id: {e}")))?;
169
170        let super_ = MoveObject::query(
171            ctx,
172            IotaAddress::from(field_id),
173            if let Some(parent_version) = parent_version {
174                Object::under_parent(parent_version, checkpoint_viewed_at)
175            } else {
176                Object::latest_at(checkpoint_viewed_at)
177            },
178        )
179        .await?;
180
181        super_.map(Self::try_from).transpose()
182    }
183
184    /// Query the `db` for a `page` of dynamic fields attached to object with ID
185    /// `parent`. The returned dynamic fields are bound by the
186    /// `parent_version` if provided - each field will be the latest version
187    /// at or before the provided version. If `parent_version` is not provided,
188    /// the latest version of each field is returned as bounded by the
189    /// `checkpoint_viewed-at` parameter.`
190    pub(crate) async fn paginate(
191        db: &Db,
192        page: Page<object::Cursor>,
193        parent: IotaAddress,
194        parent_version: Option<u64>,
195        checkpoint_viewed_at: u64,
196    ) -> Result<Connection<String, DynamicField>, Error> {
197        // If cursors are provided, defer to the `checkpoint_viewed_at` in the cursor if
198        // they are consistent. Otherwise, use the value from the parameter, or
199        // set to None. This is so that paginated queries are consistent with
200        // the previous query that created the cursor.
201        let cursor_viewed_at = page.validate_cursor_consistency()?;
202        let checkpoint_viewed_at = cursor_viewed_at.unwrap_or(checkpoint_viewed_at);
203
204        let Some((prev, next, results)) = db
205            .execute_repeatable(move |conn| {
206                let Some(range) = AvailableRange::result(conn, checkpoint_viewed_at)? else {
207                    return Ok::<_, diesel::result::Error>(None);
208                };
209
210                Ok(Some(page.paginate_raw_query::<StoredHistoryObject>(
211                    conn,
212                    checkpoint_viewed_at,
213                    dynamic_fields_query(parent, parent_version, range, &page),
214                )?))
215            })
216            .await?
217        else {
218            return Err(Error::Client(
219                "Requested data is outside the available range".to_string(),
220            ));
221        };
222
223        let mut conn: Connection<String, DynamicField> = Connection::new(prev, next);
224
225        for stored in results {
226            // To maintain consistency, the returned cursor should have the same upper-bound
227            // as the checkpoint found on the cursor.
228            let cursor = stored.cursor(checkpoint_viewed_at).encode_cursor();
229
230            let object = Object::try_from_stored_history_object(
231                stored,
232                checkpoint_viewed_at,
233                parent_version,
234            )?;
235
236            let move_ = MoveObject::try_from(&object).map_err(|_| {
237                Error::Internal(format!(
238                    "Failed to deserialize as Move object: {}",
239                    object.address
240                ))
241            })?;
242
243            let dynamic_field = DynamicField::try_from(move_)?;
244            conn.edges.push(Edge::new(cursor, dynamic_field));
245        }
246
247        Ok(conn)
248    }
249
250    pub(crate) fn root_version(&self) -> u64 {
251        self.super_.root_version()
252    }
253}
254
255impl TryFrom<MoveObject> for DynamicField {
256    type Error = Error;
257
258    fn try_from(stored: MoveObject) -> Result<Self, Error> {
259        let super_ = &stored.super_;
260
261        let native = match &super_.kind {
262            ObjectKind::NotIndexed(native) | ObjectKind::Indexed(native, _) => native,
263
264            ObjectKind::WrappedOrDeleted(_) => {
265                return Err(Error::Internal(
266                    "DynamicField is wrapped or deleted.".to_string(),
267                ));
268            }
269        };
270
271        let Some(object) = native.data.try_as_move() else {
272            return Err(Error::Internal("DynamicField is not an object".to_string()));
273        };
274
275        let Some(tag) = object.type_().other() else {
276            return Err(Error::Internal("DynamicField is not a struct".to_string()));
277        };
278
279        if !DynamicFieldInfo::is_dynamic_field(tag) {
280            return Err(Error::Internal("Wrong type for DynamicField".to_string()));
281        }
282
283        Ok(DynamicField { super_: stored })
284    }
285}
286
287/// Builds the `RawQuery` for fetching dynamic fields attached to a parent
288/// object. If `parent_version` is null, the latest version of each field within
289/// the given checkpoint range [`lhs`, `rhs`] is returned, conditioned on the
290/// fact that there is not a more recent version of the field.
291///
292/// If `parent_version` is provided, it is used to bound both the `candidates`
293/// and `newer` objects subqueries. This is because the dynamic fields of a
294/// parent at version v are dynamic fields owned by the parent whose versions
295/// are <= v. Unlike object ownership, where owned and owner objects
296/// can have arbitrary `object_version`s, dynamic fields on a parent cannot have
297/// a version greater than its parent.
298fn dynamic_fields_query(
299    parent: IotaAddress,
300    parent_version: Option<u64>,
301    range: AvailableRange,
302    page: &Page<object::Cursor>,
303) -> RawQuery {
304    build_objects_query(
305        View::Consistent,
306        range,
307        page,
308        move |query| apply_filter(query, parent, parent_version),
309        move |newer| {
310            if let Some(parent_version) = parent_version {
311                filter!(newer, format!("object_version <= {}", parent_version))
312            } else {
313                newer
314            }
315        },
316    )
317}
318
319fn apply_filter(query: RawQuery, parent: IotaAddress, parent_version: Option<u64>) -> RawQuery {
320    let query = filter!(
321        query,
322        format!(
323            "owner_id = '\\x{}'::bytea AND owner_type = {} AND df_kind IS NOT NULL",
324            hex::encode(parent.into_vec()),
325            OwnerType::Object as i16
326        )
327    );
328
329    if let Some(version) = parent_version {
330        filter!(query, format!("object_version <= {}", version))
331    } else {
332        query
333    }
334}