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