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