Skip to main content

iota_graphql_rpc/types/
move_object.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::{connection::Connection, *};
6use iota_names::config::IotaNamesConfig;
7use iota_sdk_types::{MoveStruct as NativeMoveStruct, ObjectData};
8
9use crate::{
10    config::DEFAULT_PAGE_SIZE,
11    connection::ScanConnection,
12    data::Db,
13    error::Error,
14    types::{
15        balance::{self, Balance},
16        base64::Base64,
17        big_int::BigInt,
18        coin::{Coin, CoinDowncastError},
19        coin_metadata::{CoinMetadata, CoinMetadataDowncastError},
20        cursor::Page,
21        display::DisplayEntry,
22        dynamic_field::{DynamicField, DynamicFieldName},
23        iota_address::IotaAddress,
24        iota_names_registration::{NameFormat, NameRegistration, NameRegistrationDowncastError},
25        move_type::MoveType,
26        move_value::MoveValue,
27        object::{self, Object, ObjectFilter, ObjectImpl, ObjectLookup, ObjectOwner, ObjectStatus},
28        owner::OwnerImpl,
29        stake::{StakedIota, StakedIotaDowncastError},
30        transaction_block::{self, TransactionBlock, TransactionBlockFilter},
31        type_filter::ExactTypeFilter,
32        uint53::UInt53,
33    },
34};
35
36#[derive(Clone)]
37pub(crate) struct MoveObject {
38    /// Representation of this Move Object as a generic Object.
39    pub super_: Object,
40
41    /// Move-object-specific data, extracted from the native representation at
42    /// `graphql_object.native_object.data`.
43    pub native: NativeMoveStruct,
44}
45
46/// Type to implement GraphQL fields that are shared by all MoveObjects.
47pub(crate) struct MoveObjectImpl<'o>(pub &'o MoveObject);
48
49pub(crate) enum MoveObjectDowncastError {
50    NotAMoveObject,
51}
52
53/// This interface is implemented by types that represent a Move object on-chain
54/// (A Move value whose type has `key`).
55#[expect(clippy::duplicated_attributes)]
56#[derive(Interface)]
57#[graphql(
58    name = "IMoveObject",
59    field(
60        name = "contents",
61        ty = "Option<MoveValue>",
62        desc = "Displays the contents of the Move object in a JSON string and through GraphQL \
63                types. Also provides the flat representation of the type signature, and the BCS of \
64                the corresponding data."
65    ),
66    field(
67        name = "display",
68        ty = "Option<Vec<DisplayEntry>>",
69        desc = "The set of named templates defined on-chain for the type of this object, to be \
70                handled off-chain. The server substitutes data from the object into these \
71                templates to generate a display string per template."
72    ),
73    field(
74        name = "dynamic_field",
75        arg(name = "name", ty = "DynamicFieldName"),
76        ty = "Option<DynamicField>",
77        desc = "Access a dynamic field on an object using its name. Names are arbitrary Move \
78                values whose type have `copy`, `drop`, and `store`, and are specified using their \
79                type, and their BCS contents, Base64 encoded.\n\n\
80                Dynamic fields on wrapped objects can be accessed by using the same API under the \
81                Ownertype."
82    ),
83    field(
84        name = "dynamic_object_field",
85        arg(name = "name", ty = "DynamicFieldName"),
86        ty = "Option<DynamicField>",
87        desc = "Access a dynamic object field on an object using its name. Names are arbitrary \
88                Move values whose type have `copy`, `drop`, and `store`, and are specified using \
89                their type, and their BCS contents, Base64 encoded. The value of a dynamic object \
90                field can also be accessed off-chain directly via its address (e.g. using \
91                `Query.object`).\n\n\
92                Dynamic fields on wrapped objects can be accessed by using the same API under the \
93                Owner type."
94    ),
95    field(
96        name = "dynamic_fields",
97        arg(name = "first", ty = "Option<u64>"),
98        arg(name = "after", ty = "Option<object::Cursor>"),
99        arg(name = "last", ty = "Option<u64>"),
100        arg(name = "before", ty = "Option<object::Cursor>"),
101        ty = "Connection<String, DynamicField>",
102        desc = "The dynamic fields and dynamic object fields on an object.\n\n\
103                Dynamic fields on wrapped objects can be accessed by using the same API under the \
104                Owner type."
105    )
106)]
107pub(crate) enum IMoveObject {
108    MoveObject(MoveObject),
109    Coin(Coin),
110    CoinMetadata(CoinMetadata),
111    StakedIota(StakedIota),
112    NameRegistration(NameRegistration),
113}
114
115/// The representation of an object as a Move Object, which exposes additional
116/// information (content, module that governs it, version, is transferable,
117/// etc.) about this object.
118#[Object]
119impl MoveObject {
120    pub(crate) async fn address(&self) -> IotaAddress {
121        OwnerImpl::from(&self.super_).address().await
122    }
123
124    /// Objects owned by this object, optionally `filter`-ed.
125    pub(crate) async fn objects(
126        &self,
127        ctx: &Context<'_>,
128        first: Option<u64>,
129        after: Option<object::Cursor>,
130        last: Option<u64>,
131        before: Option<object::Cursor>,
132        filter: Option<ObjectFilter>,
133    ) -> Result<Connection<String, MoveObject>> {
134        OwnerImpl::from(&self.super_)
135            .objects(ctx, first, after, last, before, filter)
136            .await
137    }
138
139    /// Total balance of all coins with marker type owned by this object. If
140    /// type is not supplied, it defaults to `0x2::iota::IOTA`.
141    pub(crate) async fn balance(
142        &self,
143        ctx: &Context<'_>,
144        type_: Option<ExactTypeFilter>,
145    ) -> Result<Option<Balance>> {
146        OwnerImpl::from(&self.super_).balance(ctx, type_).await
147    }
148
149    /// The balances of all coin types owned by this object.
150    pub(crate) async fn balances(
151        &self,
152        ctx: &Context<'_>,
153        first: Option<u64>,
154        after: Option<balance::Cursor>,
155        last: Option<u64>,
156        before: Option<balance::Cursor>,
157    ) -> Result<Connection<String, Balance>> {
158        OwnerImpl::from(&self.super_)
159            .balances(ctx, first, after, last, before)
160            .await
161    }
162
163    /// The coin objects for this object.
164    ///
165    /// `type` is a filter on the coin's type parameter, defaulting to
166    /// `0x2::iota::IOTA`.
167    pub(crate) async fn coins(
168        &self,
169        ctx: &Context<'_>,
170        first: Option<u64>,
171        after: Option<object::Cursor>,
172        last: Option<u64>,
173        before: Option<object::Cursor>,
174        type_: Option<ExactTypeFilter>,
175    ) -> Result<Connection<String, Coin>> {
176        OwnerImpl::from(&self.super_)
177            .coins(ctx, first, after, last, before, type_)
178            .await
179    }
180
181    /// The `0x3::staking_pool::StakedIota` objects owned by this object.
182    pub(crate) async fn staked_iotas(
183        &self,
184        ctx: &Context<'_>,
185        first: Option<u64>,
186        after: Option<object::Cursor>,
187        last: Option<u64>,
188        before: Option<object::Cursor>,
189    ) -> Result<Connection<String, StakedIota>> {
190        OwnerImpl::from(&self.super_)
191            .staked_iotas(ctx, first, after, last, before)
192            .await
193    }
194
195    /// The name explicitly configured as the default name pointing to this
196    /// object.
197    pub(crate) async fn iota_names_default_name(
198        &self,
199        ctx: &Context<'_>,
200        format: Option<NameFormat>,
201    ) -> Result<Option<String>> {
202        OwnerImpl::from(&self.super_)
203            .iota_names_default_name(ctx, format)
204            .await
205    }
206
207    /// The NameRegistration NFTs owned by this object. These grant the
208    /// owner the capability to manage the associated name.
209    pub(crate) async fn iota_names_registrations(
210        &self,
211        ctx: &Context<'_>,
212        first: Option<u64>,
213        after: Option<object::Cursor>,
214        last: Option<u64>,
215        before: Option<object::Cursor>,
216    ) -> Result<Connection<String, NameRegistration>> {
217        OwnerImpl::from(&self.super_)
218            .iota_names_registrations(ctx, first, after, last, before)
219            .await
220    }
221
222    pub(crate) async fn version(&self) -> Result<UInt53> {
223        ObjectImpl(&self.super_).version().await
224    }
225
226    /// The current status of the object as read from the off-chain store. The
227    /// possible states are:
228    /// - NOT_INDEXED: The object is loaded from serialized data, such as the
229    ///   contents of a genesis or system package upgrade transaction.
230    /// - INDEXED: The object is retrieved from the off-chain index and
231    ///   represents the most recent or historical state of the object.
232    pub(crate) async fn status(&self) -> ObjectStatus {
233        ObjectImpl(&self.super_).status().await
234    }
235
236    /// 32-byte hash that identifies the object's contents, encoded as a Base58
237    /// string.
238    pub(crate) async fn digest(&self) -> Option<String> {
239        ObjectImpl(&self.super_).digest().await
240    }
241
242    /// The owner type of this object: Immutable, Shared, Parent, Address
243    pub(crate) async fn owner(&self, ctx: &Context<'_>) -> Result<Option<ObjectOwner>> {
244        ObjectImpl(&self.super_).owner(ctx).await
245    }
246
247    /// The transaction block that created this version of the object.
248    pub(crate) async fn previous_transaction_block(
249        &self,
250        ctx: &Context<'_>,
251    ) -> Result<Option<TransactionBlock>> {
252        ObjectImpl(&self.super_)
253            .previous_transaction_block(ctx)
254            .await
255    }
256
257    /// The amount of IOTA we would rebate if this object gets deleted or
258    /// mutated. This number is recalculated based on the present storage
259    /// gas price.
260    pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
261        ObjectImpl(&self.super_).storage_rebate().await
262    }
263
264    /// The transaction blocks that sent objects to this object.
265    ///
266    /// `scanLimit` restricts the number of candidate transactions scanned when
267    /// gathering a page of results. It is required for queries that apply two
268    /// or more complex filters (on function, affected address, recipient, input
269    /// object, changed object, or wrapped or deleted object), and can be at
270    /// most `serviceConfig.maxScanLimit`. A `kind` filter cannot be
271    /// combined with any of them.
272    ///
273    /// When the scan limit is reached the page will be returned even if it has
274    /// fewer than `first` results when paginating forward (`last` when
275    /// paginating backwards). If there are more transactions to scan,
276    /// `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
277    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set
278    /// to the last transaction that was scanned as opposed to the last (or
279    /// first) transaction in the page.
280    ///
281    /// Requesting the next (or previous) page after this cursor will resume the
282    /// search, scanning the next `scanLimit` many transactions in the
283    /// direction of pagination, and so on until all transactions in the
284    /// scanning range have been visited.
285    ///
286    /// By default, the scanning range includes all transactions known to
287    /// GraphQL, but it can be restricted by the `after` and `before`
288    /// cursors, and the `beforeCheckpoint`, `afterCheckpoint` and
289    /// `atCheckpoint` filters.
290    ///
291    /// DEPRECATION NOTICE: Support for the combination of two or more complex
292    /// filters as discussed above will stop with the v1.38 release. `scanLimit`
293    /// will thus become obsolete and will be removed as well.
294    #[graphql(
295        complexity = "first.or(last).unwrap_or(DEFAULT_PAGE_SIZE as u64) as usize * child_complexity"
296    )]
297    pub(crate) async fn received_transaction_blocks(
298        &self,
299        ctx: &Context<'_>,
300        first: Option<u64>,
301        after: Option<transaction_block::Cursor>,
302        last: Option<u64>,
303        before: Option<transaction_block::Cursor>,
304        filter: Option<TransactionBlockFilter>,
305        #[graphql(
306            deprecation = "`scanLimit` will be removed with v1.38, along with the support for combining complex filters."
307        )]
308        scan_limit: Option<u64>,
309    ) -> Result<ScanConnection<String, TransactionBlock>> {
310        ObjectImpl(&self.super_)
311            .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
312            .await
313    }
314
315    /// The Base64-encoded BCS serialization of the object's content.
316    pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
317        ObjectImpl(&self.super_).bcs().await
318    }
319
320    /// Displays the contents of the Move object in a JSON string and through
321    /// GraphQL types. Also provides the flat representation of the type
322    /// signature, and the BCS of the corresponding data.
323    pub(crate) async fn contents(&self) -> Option<MoveValue> {
324        MoveObjectImpl(self).contents().await
325    }
326
327    /// The set of named templates defined on-chain for the type of this object,
328    /// to be handled off-chain. The server substitutes data from the object
329    /// into these templates to generate a display string per template.
330    pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
331        ObjectImpl(&self.super_).display(ctx).await
332    }
333
334    /// Access a dynamic field on an object using its name. Names are arbitrary
335    /// Move values whose type have `copy`, `drop`, and `store`, and are
336    /// specified using their type, and their BCS contents, Base64 encoded.
337    ///
338    /// Dynamic fields on wrapped objects can be accessed by using the same API
339    /// under the Owner type.
340    pub(crate) async fn dynamic_field(
341        &self,
342        ctx: &Context<'_>,
343        name: DynamicFieldName,
344    ) -> Result<Option<DynamicField>> {
345        OwnerImpl::from(&self.super_)
346            .dynamic_field(ctx, name, Some(self.root_version()))
347            .await
348    }
349
350    /// Access a dynamic object field on an object using its name. Names are
351    /// arbitrary Move values whose type have `copy`, `drop`, and `store`,
352    /// and are specified using their type, and their BCS contents, Base64
353    /// encoded. The value of a dynamic object field can also be accessed
354    /// off-chain directly via its address (e.g. using `Query.object`).
355    ///
356    /// Dynamic fields on wrapped objects can be accessed by using the same API
357    /// under the Owner type.
358    pub(crate) async fn dynamic_object_field(
359        &self,
360        ctx: &Context<'_>,
361        name: DynamicFieldName,
362    ) -> Result<Option<DynamicField>> {
363        OwnerImpl::from(&self.super_)
364            .dynamic_object_field(ctx, name, Some(self.root_version()))
365            .await
366    }
367
368    /// The dynamic fields and dynamic object fields on an object.
369    ///
370    /// Dynamic fields on wrapped objects can be accessed by using the same API
371    /// under the Owner type.
372    pub(crate) async fn dynamic_fields(
373        &self,
374        ctx: &Context<'_>,
375        first: Option<u64>,
376        after: Option<object::Cursor>,
377        last: Option<u64>,
378        before: Option<object::Cursor>,
379    ) -> Result<Connection<String, DynamicField>> {
380        OwnerImpl::from(&self.super_)
381            .dynamic_fields(ctx, first, after, last, before, Some(self.root_version()))
382            .await
383    }
384
385    /// Attempts to convert the Move object into a `0x2::coin::Coin`.
386    async fn as_coin(&self) -> Result<Option<Coin>> {
387        match Coin::try_from(self) {
388            Ok(coin) => Ok(Some(coin)),
389            Err(CoinDowncastError::NotACoin) => Ok(None),
390            Err(CoinDowncastError::Bcs(e)) => {
391                Err(Error::Internal(format!("Failed to deserialize Coin: {e}"))).extend()
392            }
393        }
394    }
395
396    /// Attempts to convert the Move object into a
397    /// `0x3::staking_pool::StakedIota`.
398    async fn as_staked_iota(&self) -> Result<Option<StakedIota>> {
399        match StakedIota::try_from(self) {
400            Ok(coin) => Ok(Some(coin)),
401            Err(StakedIotaDowncastError::NotAStakedIota) => Ok(None),
402            Err(StakedIotaDowncastError::Bcs(e)) => Err(Error::Internal(format!(
403                "Failed to deserialize StakedIota: {e}"
404            )))
405            .extend(),
406        }
407    }
408
409    /// Attempts to convert the Move object into a `0x2::coin::CoinMetadata`.
410    async fn as_coin_metadata(&self) -> Result<Option<CoinMetadata>> {
411        match CoinMetadata::try_from(self) {
412            Ok(metadata) => Ok(Some(metadata)),
413            Err(CoinMetadataDowncastError::NotCoinMetadata) => Ok(None),
414            Err(CoinMetadataDowncastError::Bcs(e)) => Err(Error::Internal(format!(
415                "Failed to deserialize CoinMetadata: {e}"
416            )))
417            .extend(),
418        }
419    }
420
421    // Attempts to convert the Move object into a `NameRegistration` object.
422    async fn as_iota_names_registration(
423        &self,
424        ctx: &Context<'_>,
425    ) -> Result<Option<NameRegistration>> {
426        let cfg: &IotaNamesConfig = ctx.data_unchecked();
427        let tag = NameRegistration::struct_tag(cfg.package_address.into());
428
429        match NameRegistration::try_from(self, &tag) {
430            Ok(registration) => Ok(Some(registration)),
431            Err(NameRegistrationDowncastError::NotAnNameRegistration) => Ok(None),
432            Err(NameRegistrationDowncastError::Bcs(e)) => Err(Error::Internal(format!(
433                "Failed to deserialize
434     NameRegistration: {e}",
435            )))
436            .extend(),
437        }
438    }
439}
440
441impl MoveObjectImpl<'_> {
442    pub(crate) async fn contents(&self) -> Option<MoveValue> {
443        let type_ = self.0.native.type_tag();
444        Some(MoveValue::new(type_, self.0.native.contents().into()))
445    }
446    pub(crate) async fn has_public_transfer(&self, ctx: &Context<'_>) -> Result<bool> {
447        let type_: MoveType = self.0.native.struct_tag().clone().into();
448        let set = type_.abilities_impl(ctx.data_unchecked()).await.extend()?;
449        Ok(set.has_key() && set.has_store())
450    }
451}
452
453impl MoveObject {
454    pub(crate) async fn query(
455        ctx: &Context<'_>,
456        address: IotaAddress,
457        key: ObjectLookup,
458    ) -> Result<Option<Self>, Error> {
459        let Some(object) = Object::query(ctx, address, key).await? else {
460            return Ok(None);
461        };
462
463        match MoveObject::try_from(&object) {
464            Ok(object) => Ok(Some(object)),
465            Err(MoveObjectDowncastError::NotAMoveObject) => {
466                Err(Error::Internal(format!("{address} is not a Move object")))?
467            }
468        }
469    }
470
471    /// Query the database for a `page` of Move objects, optionally `filter`-ed.
472    ///
473    /// `checkpoint_viewed_at` represents the checkpoint sequence number at
474    /// which this page was queried for. Each entity returned in the
475    /// connection will inherit this checkpoint, so that when viewing that
476    /// entity's state, it will be as if it was read at the same checkpoint.
477    pub(crate) async fn paginate(
478        db: &Db,
479        page: Page<object::Cursor>,
480        filter: ObjectFilter,
481        checkpoint_viewed_at: u64,
482    ) -> Result<Connection<String, MoveObject>, Error> {
483        Object::paginate_subtype(db, page, filter, checkpoint_viewed_at, |object| {
484            let address = object.address;
485            MoveObject::try_from(&object).map_err(|_| {
486                Error::Internal(format!(
487                    "Expected {address} to be a Move object, but it's not."
488                ))
489            })
490        })
491        .await
492    }
493
494    /// Root parent object version for dynamic fields.
495    ///
496    /// Check [`Object::root_version`] for details.
497    pub(crate) fn root_version(&self) -> u64 {
498        self.super_.root_version()
499    }
500}
501
502impl TryFrom<&Object> for MoveObject {
503    type Error = MoveObjectDowncastError;
504
505    fn try_from(object: &Object) -> Result<Self, Self::Error> {
506        let native = object.native_impl();
507
508        if let ObjectData::Struct(move_object) = &native.data {
509            Ok(Self {
510                super_: object.clone(),
511                native: move_object.clone(),
512            })
513        } else {
514            Err(MoveObjectDowncastError::NotAMoveObject)
515        }
516    }
517}