1use 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
44pub(crate) struct NameService;
47
48#[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 pub super_: MoveObject,
72
73 pub native: NativeSuinsRegistration,
75}
76
77pub(crate) struct DomainExpiration {
81 pub name_record: Option<NameRecord>,
83 pub parent_name_record: Option<NameRecord>,
85 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 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 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 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 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 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 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 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 pub(crate) async fn status(&self) -> ObjectStatus {
211 ObjectImpl(&self.super_.super_).status().await
212 }
213
214 pub(crate) async fn digest(&self) -> Option<String> {
216 ObjectImpl(&self.super_.super_).digest().await
217 }
218
219 pub(crate) async fn owner(&self) -> Option<ObjectOwner> {
221 ObjectImpl(&self.super_.super_).owner().await
222 }
223
224 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 pub(crate) async fn storage_rebate(&self) -> Option<BigInt> {
237 ObjectImpl(&self.super_.super_).storage_rebate().await
238 }
239
240 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 pub(crate) async fn bcs(&self) -> Result<Option<Base64>> {
277 ObjectImpl(&self.super_.super_).bcs().await
278 }
279
280 pub(crate) async fn contents(&self) -> Option<MoveValue> {
284 MoveObjectImpl(&self.super_).contents().await
285 }
286
287 pub(crate) async fn has_public_transfer(&self, ctx: &Context<'_>) -> Result<bool> {
291 MoveObjectImpl(&self.super_).has_public_transfer(ctx).await
292 }
293
294 pub(crate) async fn display(&self, ctx: &Context<'_>) -> Result<Option<Vec<DisplayEntry>>> {
298 ObjectImpl(&self.super_.super_).display(ctx).await
299 }
300
301 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 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 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 async fn domain(&self) -> &str {
360 &self.native.domain_name
361 }
362}
363
364impl NameService {
365 pub(crate) async fn resolve_to_record(
376 ctx: &Context<'_>,
377 domain: &Domain,
378 checkpoint_viewed_at: u64,
379 ) -> Result<Option<NameRecord>, Error> {
380 let Some(domain_expiration) =
383 Self::query_domain_expiration(ctx, domain, checkpoint_viewed_at).await?
384 else {
385 return Ok(None);
386 };
387
388 let Some(name_record) = domain_expiration.name_record else {
391 return Ok(None);
392 };
393
394 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 let Some(parent_name_record) = domain_expiration.parent_name_record else {
406 return Err(Error::NameService(NameServiceError::NameExpired));
407 };
408
409 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 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 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 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 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 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 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 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 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 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 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}