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