sui_graphql_rpc/types/
dynamic_field.rs

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