sui_graphql_rpc/types/
suins_registration.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::str::FromStr;
5
6use super::{
7    available_range::AvailableRange,
8    balance::{self, Balance},
9    base64::Base64,
10    big_int::BigInt,
11    checkpoint::Checkpoint,
12    coin::Coin,
13    cursor::Page,
14    display::DisplayEntry,
15    dynamic_field::{DynamicField, DynamicFieldName},
16    move_object::{MoveObject, MoveObjectImpl},
17    move_value::MoveValue,
18    object::{self, Object, ObjectFilter, ObjectImpl, ObjectOwner, ObjectStatus},
19    owner::OwnerImpl,
20    stake::StakedSui,
21    string_input::impl_string_input,
22    sui_address::SuiAddress,
23    transaction_block::{self, TransactionBlock, TransactionBlockFilter},
24    type_filter::ExactTypeFilter,
25    uint53::UInt53,
26};
27use crate::{
28    connection::ScanConnection,
29    consistency::{build_objects_query, View},
30    data::{Db, DbConnection, QueryExecutor},
31    error::Error,
32};
33use async_graphql::{connection::Connection, *};
34use diesel_async::scoped_futures::ScopedFutureExt;
35use move_core_types::{ident_str, identifier::IdentStr, language_storage::StructTag};
36use serde::{Deserialize, Serialize};
37use sui_indexer::models::objects::StoredHistoryObject;
38use sui_name_service::{Domain as NativeDomain, NameRecord, NameServiceConfig, NameServiceError};
39use sui_types::{base_types::SuiAddress as NativeSuiAddress, dynamic_field::Field, id::UID};
40
41const MOD_REGISTRATION: &IdentStr = ident_str!("suins_registration");
42const TYP_REGISTRATION: &IdentStr = ident_str!("SuinsRegistration");
43
44/// Represents the "core" of the name service (e.g. the on-chain registry and reverse registry). It
45/// doesn't contain any fields because we look them up based on the `NameServiceConfig`.
46pub(crate) struct NameService;
47
48/// Wrap SuiNS Domain type to expose as a string scalar in GraphQL.
49#[derive(Debug)]
50pub(crate) struct Domain(NativeDomain);
51
52#[derive(Enum, Copy, Clone, Eq, PartialEq)]
53#[graphql(remote = "sui_name_service::DomainFormat")]
54pub enum DomainFormat {
55    At,
56    Dot,
57}
58
59#[derive(Clone, Serialize, Deserialize)]
60pub(crate) struct NativeSuinsRegistration {
61    pub id: UID,
62    pub domain: NativeDomain,
63    pub domain_name: String,
64    pub expiration_timestamp_ms: u64,
65    pub image_url: String,
66}
67
68#[derive(Clone)]
69pub(crate) struct SuinsRegistration {
70    /// Representation of this SuinsRegistration as a generic Move object.
71    pub super_: MoveObject,
72
73    /// The deserialized representation of the Move object's contents.
74    pub native: NativeSuinsRegistration,
75}
76
77/// Represents the results of a query for a domain's `NameRecord` and its parent's `NameRecord`. The
78/// `expiration_timestamp_ms` on the name records are compared to the checkpoint's timestamp to
79/// check that the domain is not expired.
80pub(crate) struct DomainExpiration {
81    /// The domain's `NameRecord`.
82    pub name_record: Option<NameRecord>,
83    /// The parent's `NameRecord`, populated only if the domain is a subdomain.
84    pub parent_name_record: Option<NameRecord>,
85    /// The timestamp of the checkpoint at which the query was made. This is used to check if the
86    /// `expiration_timestamp_ms` on the name records are expired.
87    pub checkpoint_timestamp_ms: u64,
88}
89
90pub(crate) enum SuinsRegistrationDowncastError {
91    NotASuinsRegistration,
92    Bcs(bcs::Error),
93}
94
95#[Object]
96impl SuinsRegistration {
97    pub(crate) async fn address(&self) -> SuiAddress {
98        OwnerImpl::from(&self.super_.super_).address().await
99    }
100
101    /// Objects owned by this object, optionally `filter`-ed.
102    pub(crate) async fn objects(
103        &self,
104        ctx: &Context<'_>,
105        first: Option<u64>,
106        after: Option<object::Cursor>,
107        last: Option<u64>,
108        before: Option<object::Cursor>,
109        filter: Option<ObjectFilter>,
110    ) -> Result<Connection<String, MoveObject>> {
111        OwnerImpl::from(&self.super_.super_)
112            .objects(ctx, first, after, last, before, filter)
113            .await
114    }
115
116    /// Total balance of all coins with marker type owned by this object. If type is not supplied,
117    /// it defaults to `0x2::sui::SUI`.
118    pub(crate) async fn balance(
119        &self,
120        ctx: &Context<'_>,
121        type_: Option<ExactTypeFilter>,
122    ) -> Result<Option<Balance>> {
123        OwnerImpl::from(&self.super_.super_)
124            .balance(ctx, type_)
125            .await
126    }
127
128    /// The balances of all coin types owned by this object.
129    pub(crate) async fn balances(
130        &self,
131        ctx: &Context<'_>,
132        first: Option<u64>,
133        after: Option<balance::Cursor>,
134        last: Option<u64>,
135        before: Option<balance::Cursor>,
136    ) -> Result<Connection<String, Balance>> {
137        OwnerImpl::from(&self.super_.super_)
138            .balances(ctx, first, after, last, before)
139            .await
140    }
141
142    /// The coin objects for this object.
143    ///
144    ///`type` is a filter on the coin's type parameter, defaulting to `0x2::sui::SUI`.
145    pub(crate) async fn coins(
146        &self,
147        ctx: &Context<'_>,
148        first: Option<u64>,
149        after: Option<object::Cursor>,
150        last: Option<u64>,
151        before: Option<object::Cursor>,
152        type_: Option<ExactTypeFilter>,
153    ) -> Result<Connection<String, Coin>> {
154        OwnerImpl::from(&self.super_.super_)
155            .coins(ctx, first, after, last, before, type_)
156            .await
157    }
158
159    /// The `0x3::staking_pool::StakedSui` objects owned by this object.
160    pub(crate) async fn staked_suis(
161        &self,
162        ctx: &Context<'_>,
163        first: Option<u64>,
164        after: Option<object::Cursor>,
165        last: Option<u64>,
166        before: Option<object::Cursor>,
167    ) -> Result<Connection<String, StakedSui>> {
168        OwnerImpl::from(&self.super_.super_)
169            .staked_suis(ctx, first, after, last, before)
170            .await
171    }
172
173    /// The domain explicitly configured as the default domain pointing to this object.
174    pub(crate) async fn default_suins_name(
175        &self,
176        ctx: &Context<'_>,
177        format: Option<DomainFormat>,
178    ) -> Result<Option<String>> {
179        OwnerImpl::from(&self.super_.super_)
180            .default_suins_name(ctx, format)
181            .await
182    }
183
184    /// The SuinsRegistration NFTs owned by this object. These grant the owner the capability to
185    /// manage the associated domain.
186    pub(crate) async fn suins_registrations(
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, SuinsRegistration>> {
194        OwnerImpl::from(&self.super_.super_)
195            .suins_registrations(ctx, first, after, last, before)
196            .await
197    }
198
199    pub(crate) async fn version(&self) -> UInt53 {
200        ObjectImpl(&self.super_.super_).version().await
201    }
202
203    /// The current status of the object as read from the off-chain store. The possible states are:
204    /// NOT_INDEXED, the object is loaded from serialized data, such as the contents of a genesis or
205    /// system package upgrade transaction. LIVE, the version returned is the most recent for the
206    /// object, and it is not deleted or wrapped at that version. HISTORICAL, the object was
207    /// referenced at a specific version or checkpoint, so is fetched from historical tables and may
208    /// not be the latest version of the object. WRAPPED_OR_DELETED, the object is deleted or
209    /// wrapped and only partial information can be loaded."
210    pub(crate) async fn status(&self) -> ObjectStatus {
211        ObjectImpl(&self.super_.super_).status().await
212    }
213
214    /// 32-byte hash that identifies the object's contents, encoded as a Base58 string.
215    pub(crate) async fn digest(&self) -> Option<String> {
216        ObjectImpl(&self.super_.super_).digest().await
217    }
218
219    /// The owner type of this object: Immutable, Shared, Parent, Address
220    pub(crate) async fn owner(&self) -> Option<ObjectOwner> {
221        ObjectImpl(&self.super_.super_).owner().await
222    }
223
224    /// The transaction block that created this version of the object.
225    pub(crate) async fn previous_transaction_block(
226        &self,
227        ctx: &Context<'_>,
228    ) -> Result<Option<TransactionBlock>> {
229        ObjectImpl(&self.super_.super_)
230            .previous_transaction_block(ctx)
231            .await
232    }
233
234    /// The amount of SUI we would rebate if this object gets deleted or mutated. This number is
235    /// recalculated based on the present storage gas price.
236    pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
237        ObjectImpl(&self.super_.super_).storage_rebate().await
238    }
239
240    /// The transaction blocks that sent objects to this object.
241    ///
242    /// `scanLimit` restricts the number of candidate transactions scanned when gathering a page of
243    /// results. It is required for queries that apply more than two complex filters (on function,
244    /// kind, sender, recipient, input object, changed object, or ids), and can be at most
245    /// `serviceConfig.maxScanLimit`.
246    ///
247    /// When the scan limit is reached the page will be returned even if it has fewer than `first`
248    /// results when paginating forward (`last` when paginating backwards). If there are more
249    /// transactions to scan, `pageInfo.hasNextPage` (or `pageInfo.hasPreviousPage`) will be set to
250    /// `true`, and `PageInfo.endCursor` (or `PageInfo.startCursor`) will be set to the last
251    /// transaction that was scanned as opposed to the last (or first) transaction in the page.
252    ///
253    /// Requesting the next (or previous) page after this cursor will resume the search, scanning
254    /// the next `scanLimit` many transactions in the direction of pagination, and so on until all
255    /// transactions in the scanning range have been visited.
256    ///
257    /// By default, the scanning range includes all transactions known to GraphQL, but it can be
258    /// restricted by the `after` and `before` cursors, and the `beforeCheckpoint`,
259    /// `afterCheckpoint` and `atCheckpoint` filters.
260    pub(crate) async fn received_transaction_blocks(
261        &self,
262        ctx: &Context<'_>,
263        first: Option<u64>,
264        after: Option<transaction_block::Cursor>,
265        last: Option<u64>,
266        before: Option<transaction_block::Cursor>,
267        filter: Option<TransactionBlockFilter>,
268        scan_limit: Option<u64>,
269    ) -> Result<ScanConnection<String, TransactionBlock>> {
270        ObjectImpl(&self.super_.super_)
271            .received_transaction_blocks(ctx, first, after, last, before, filter, scan_limit)
272            .await
273    }
274
275    /// The Base64-encoded BCS serialization of the object's content.
276    pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
277        ObjectImpl(&self.super_.super_).bcs().await
278    }
279
280    /// Displays the contents of the Move object in a JSON string and through GraphQL types. Also
281    /// provides the flat representation of the type signature, and the BCS of the corresponding
282    /// data.
283    pub(crate) async fn contents(&self) -> Option<MoveValue> {
284        MoveObjectImpl(&self.super_).contents().await
285    }
286
287    /// Determines whether a transaction can transfer this object, using the TransferObjects
288    /// transaction command or `sui::transfer::public_transfer`, both of which require the object to
289    /// have the `key` and `store` abilities.
290    pub(crate) async fn has_public_transfer(&self, ctx: &Context<'_>) -> Result<bool> {
291        MoveObjectImpl(&self.super_).has_public_transfer(ctx).await
292    }
293
294    /// The set of named templates defined on-chain for the type of this object, to be handled
295    /// off-chain. The server substitutes data from the object into these templates to generate a
296    /// display string per template.
297    pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
298        ObjectImpl(&self.super_.super_).display(ctx).await
299    }
300
301    /// Access a dynamic field on an object using its name. Names are arbitrary Move values whose
302    /// type have `copy`, `drop`, and `store`, and are specified using their type, and their BCS
303    /// contents, Base64 encoded.
304    ///
305    /// Dynamic fields on wrapped objects can be accessed by using the same API under the Owner
306    /// type.
307    pub(crate) async fn dynamic_field(
308        &self,
309        ctx: &Context<'_>,
310        name: DynamicFieldName,
311    ) -> Result<Option<DynamicField>> {
312        OwnerImpl::from(&self.super_.super_)
313            .dynamic_field(ctx, name, Some(self.super_.root_version()))
314            .await
315    }
316
317    /// Access a dynamic object field on an object using its name. Names are arbitrary Move values
318    /// whose type have `copy`, `drop`, and `store`, and are specified using their type, and their
319    /// BCS contents, Base64 encoded. The value of a dynamic object field can also be accessed
320    /// off-chain directly via its address (e.g. using `Query.object`).
321    ///
322    /// Dynamic fields on wrapped objects can be accessed by using the same API under the Owner
323    /// type.
324    pub(crate) async fn dynamic_object_field(
325        &self,
326        ctx: &Context<'_>,
327        name: DynamicFieldName,
328    ) -> Result<Option<DynamicField>> {
329        OwnerImpl::from(&self.super_.super_)
330            .dynamic_object_field(ctx, name, Some(self.super_.root_version()))
331            .await
332    }
333
334    /// The dynamic fields and dynamic object fields on an object.
335    ///
336    /// Dynamic fields on wrapped objects can be accessed by using the same API under the Owner
337    /// type.
338    pub(crate) async fn dynamic_fields(
339        &self,
340        ctx: &Context<'_>,
341        first: Option<u64>,
342        after: Option<object::Cursor>,
343        last: Option<u64>,
344        before: Option<object::Cursor>,
345    ) -> Result<Connection<String, DynamicField>> {
346        OwnerImpl::from(&self.super_.super_)
347            .dynamic_fields(
348                ctx,
349                first,
350                after,
351                last,
352                before,
353                Some(self.super_.root_version()),
354            )
355            .await
356    }
357
358    /// Domain name of the SuinsRegistration object
359    async fn domain(&self) -> &str {
360        &self.native.domain_name
361    }
362}
363
364impl NameService {
365    /// Lookup the SuiNS NameRecord for the given `domain` name. `config` specifies where to find
366    /// the domain name registry, and its type.
367    ///
368    /// `checkpoint_viewed_at` represents the checkpoint sequence number at which this was queried
369    /// for.
370    ///
371    /// The `NameRecord` is returned only if it has not expired as of the `checkpoint_viewed_at` or
372    /// latest checkpoint's timestamp.
373    ///
374    /// For leaf domains, the `NameRecord` is returned only if its parent is valid and not expired.
375    pub(crate) async fn resolve_to_record(
376        ctx: &Context<'_>,
377        domain: &Domain,
378        checkpoint_viewed_at: u64,
379    ) -> Result<Option<NameRecord>, Error> {
380        // Query for the domain's NameRecord and parent NameRecord if applicable. The checkpoint's
381        // timestamp is also fetched. These values are used to determine if the domain is expired.
382        let Some(domain_expiration) =
383            Self::query_domain_expiration(ctx, domain, checkpoint_viewed_at).await?
384        else {
385            return Ok(None);
386        };
387
388        // Get the name_record from the query. If we didn't find it, we return as it means that the
389        // requested name is not registered.
390        let Some(name_record) = domain_expiration.name_record else {
391            return Ok(None);
392        };
393
394        // If name record is SLD, or Node subdomain, we can check the expiration and return the
395        // record if not expired.
396        if !name_record.is_leaf_record() {
397            return if !name_record.is_node_expired(domain_expiration.checkpoint_timestamp_ms) {
398                Ok(Some(name_record))
399            } else {
400                Err(Error::NameService(NameServiceError::NameExpired))
401            };
402        }
403
404        // If we cannot find the parent, then the name is expired.
405        let Some(parent_name_record) = domain_expiration.parent_name_record else {
406            return Err(Error::NameService(NameServiceError::NameExpired));
407        };
408
409        // If the parent is valid for this leaf, and not expired, then we can return the name
410        // record. Otherwise, the name is expired.
411        if parent_name_record.is_valid_leaf_parent(&name_record)
412            && !parent_name_record.is_node_expired(domain_expiration.checkpoint_timestamp_ms)
413        {
414            Ok(Some(name_record))
415        } else {
416            Err(Error::NameService(NameServiceError::NameExpired))
417        }
418    }
419
420    /// Lookup the SuiNS Domain for the given `address`. `config` specifies where to find the domain
421    /// name registry, and its type.
422    ///
423    /// `checkpoint_viewed_at` represents the checkpoint sequence number at which this was queried
424    /// for.
425    pub(crate) async fn reverse_resolve_to_name(
426        ctx: &Context<'_>,
427        address: SuiAddress,
428        checkpoint_viewed_at: u64,
429    ) -> Result<Option<NativeDomain>, Error> {
430        let config: &NameServiceConfig = ctx.data_unchecked();
431
432        let reverse_record_id = config.reverse_record_field_id(address.as_slice());
433
434        let Some(object) = MoveObject::query(
435            ctx,
436            reverse_record_id.into(),
437            Object::latest_at(checkpoint_viewed_at),
438        )
439        .await?
440        else {
441            return Ok(None);
442        };
443
444        let field: Field<NativeSuiAddress, NativeDomain> = object
445            .native
446            .to_rust()
447            .ok_or_else(|| Error::Internal("Malformed Suins Domain".to_string()))?;
448
449        let domain = Domain(field.value);
450
451        // We attempt to resolve the domain to a record, and if it fails, we return None. That way
452        // we can validate that the name has not expired and is still valid.
453        let Some(_) = Self::resolve_to_record(ctx, &domain, checkpoint_viewed_at).await? else {
454            return Ok(None);
455        };
456
457        Ok(Some(domain.0))
458    }
459
460    /// Query for a domain's NameRecord, its parent's NameRecord if supplied, and the timestamp of
461    /// the checkpoint bound.
462    async fn query_domain_expiration(
463        ctx: &Context<'_>,
464        domain: &Domain,
465        checkpoint_viewed_at: u64,
466    ) -> Result<Option<DomainExpiration>, Error> {
467        let config: &NameServiceConfig = ctx.data_unchecked();
468        let db: &Db = ctx.data_unchecked();
469        // Construct the list of `object_id`s to look up. The first element is the domain's
470        // `NameRecord`. If the domain is a subdomain, there will be a second element for the
471        // parent's `NameRecord`.
472        let mut object_ids = vec![SuiAddress::from(config.record_field_id(&domain.0))];
473        if domain.0.is_subdomain() {
474            object_ids.push(SuiAddress::from(config.record_field_id(&domain.0.parent())));
475        }
476
477        // Create a page with a bound of `object_ids` length to fetch the relevant `NameRecord`s.
478        let page: Page<object::Cursor> = Page::from_params(
479            ctx.data_unchecked(),
480            Some(object_ids.len() as u64),
481            None,
482            None,
483            None,
484        )
485        .map_err(|_| {
486            Error::Internal("Page size of 2 is incompatible with configured limits".to_string())
487        })?;
488
489        // prepare the filter for the query.
490        let filter = ObjectFilter {
491            object_ids: Some(object_ids.clone()),
492            ..Default::default()
493        };
494
495        let Some((checkpoint_timestamp_ms, results)) = db
496            .execute_repeatable(move |conn| {
497                async move {
498                    let Some(range) = AvailableRange::result(conn, checkpoint_viewed_at).await?
499                    else {
500                        return Ok::<_, diesel::result::Error>(None);
501                    };
502
503                    let timestamp_ms =
504                        Checkpoint::query_timestamp(conn, checkpoint_viewed_at).await?;
505
506                    let sql = build_objects_query(
507                        View::Consistent,
508                        range,
509                        &page,
510                        move |query| filter.apply(query),
511                        move |newer| newer,
512                    );
513
514                    let objects: Vec<StoredHistoryObject> =
515                        conn.results(move || sql.clone().into_boxed()).await?;
516
517                    Ok(Some((timestamp_ms, objects)))
518                }
519                .scope_boxed()
520            })
521            .await?
522        else {
523            return Err(Error::Client(
524                "Requested data is outside the available range".to_string(),
525            ));
526        };
527
528        let mut domain_expiration = DomainExpiration {
529            parent_name_record: None,
530            name_record: None,
531            checkpoint_timestamp_ms,
532        };
533
534        // Max size of results is 2. We loop through them, convert to objects, and then parse
535        // name_record. We then assign it to the correct field on `domain_expiration` based on the
536        // address.
537        for result in results {
538            let object =
539                Object::try_from_stored_history_object(result, checkpoint_viewed_at, None)?;
540            let move_object = MoveObject::try_from(&object).map_err(|_| {
541                Error::Internal(format!(
542                    "Expected {0} to be a NameRecord, but it's not a Move Object.",
543                    object.address
544                ))
545            })?;
546
547            let record = NameRecord::try_from(move_object.native)?;
548
549            if object.address == object_ids[0] {
550                domain_expiration.name_record = Some(record);
551            } else if Some(&object.address) == object_ids.get(1) {
552                domain_expiration.parent_name_record = Some(record);
553            }
554        }
555
556        Ok(Some(domain_expiration))
557    }
558}
559
560impl SuinsRegistration {
561    /// Query the database for a `page` of SuiNS registrations. The page uses the same cursor type
562    /// as is used for `Object`, and is further filtered to a particular `owner`. `config` specifies
563    /// where to find the domain name registry and its type.
564    ///
565    /// `checkpoint_viewed_at` represents the checkpoint sequence number at which this page was
566    /// queried for. Each entity returned in the connection will inherit this checkpoint, so that
567    /// when viewing that entity's state, it will be as if it was read at the same checkpoint.
568    pub(crate) async fn paginate(
569        db: &Db,
570        config: &NameServiceConfig,
571        page: Page<object::Cursor>,
572        owner: SuiAddress,
573        checkpoint_viewed_at: u64,
574    ) -> Result<Connection<String, SuinsRegistration>, Error> {
575        let type_ = SuinsRegistration::type_(config.package_address.into());
576
577        let filter = ObjectFilter {
578            type_: Some(type_.clone().into()),
579            owner: Some(owner),
580            ..Default::default()
581        };
582
583        Object::paginate_subtype(db, page, filter, checkpoint_viewed_at, |object| {
584            let address = object.address;
585            let move_object = MoveObject::try_from(&object).map_err(|_| {
586                Error::Internal(format!(
587                    "Expected {address} to be a SuinsRegistration, but it's not a Move Object.",
588                ))
589            })?;
590
591            SuinsRegistration::try_from(&move_object, &type_).map_err(|_| {
592                Error::Internal(format!(
593                    "Expected {address} to be a SuinsRegistration, but it is not."
594                ))
595            })
596        })
597        .await
598    }
599
600    /// Return the type representing a `SuinsRegistration` on chain. This can change from chain to
601    /// chain (mainnet, testnet, devnet etc).
602    pub(crate) fn type_(package: SuiAddress) -> StructTag {
603        StructTag {
604            address: package.into(),
605            module: MOD_REGISTRATION.to_owned(),
606            name: TYP_REGISTRATION.to_owned(),
607            type_params: vec![],
608        }
609    }
610
611    // Because the type of the SuinsRegistration object is not constant,
612    // we need to take it in as a param.
613    pub(crate) fn try_from(
614        move_object: &MoveObject,
615        tag: &StructTag,
616    ) -> Result<Self, SuinsRegistrationDowncastError> {
617        if !move_object.native.is_type(tag) {
618            return Err(SuinsRegistrationDowncastError::NotASuinsRegistration);
619        }
620
621        Ok(Self {
622            super_: move_object.clone(),
623            native: bcs::from_bytes(move_object.native.contents())
624                .map_err(SuinsRegistrationDowncastError::Bcs)?,
625        })
626    }
627}
628
629impl_string_input!(Domain);
630
631impl FromStr for Domain {
632    type Err = <NativeDomain as FromStr>::Err;
633
634    fn from_str(s: &str) -> Result<Self, Self::Err> {
635        Ok(Domain(NativeDomain::from_str(s)?))
636    }
637}