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