1use std::fmt;
5use std::marker::PhantomData;
6use std::str::FromStr;
7
8use move_core_types::ident_str;
9use move_core_types::identifier::IdentStr;
10use move_core_types::language_storage::StructTag;
11use serde::{Deserialize, Serialize};
12use sui_types::TypeTag;
13use sui_types::base_types::{ObjectID, SuiAddress};
14use sui_types::collection_types::VecMap;
15use sui_types::dynamic_field::Field;
16use sui_types::id::{ID, UID};
17use sui_types::object::{MoveObject, Object};
18
19const NAME_SERVICE_DOMAIN_MODULE: &IdentStr = ident_str!("domain");
20const NAME_SERVICE_DOMAIN_STRUCT: &IdentStr = ident_str!("Domain");
21const LEAF_EXPIRATION_TIMESTAMP: u64 = 0;
22const DEFAULT_TLD: &str = "sui";
23const ACCEPTED_SEPARATORS: [char; 2] = ['.', '*'];
24const SUI_NEW_FORMAT_SEPARATOR: char = '@';
25
26#[derive(Debug, Serialize, Deserialize, Clone)]
27pub struct Registry {
28 registry: Table<Domain, NameRecord>,
31 reverse_registry: Table<SuiAddress, Domain>,
34}
35
36#[derive(Debug, Serialize, Deserialize, Clone, Eq, Hash, PartialEq)]
37pub struct Domain {
38 labels: Vec<String>,
39}
40
41#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
43pub struct NameRecord {
44 pub nft_id: ID,
52 pub expiration_timestamp_ms: u64,
54 pub target_address: Option<SuiAddress>,
56 pub data: VecMap<String, String>,
58}
59
60#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
62pub struct SuinsRegistration {
63 pub id: UID,
64 pub domain: Domain,
65 pub domain_name: String,
66 pub expiration_timestamp_ms: u64,
67 pub image_url: String,
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
72pub struct SubDomainRegistration {
73 pub id: UID,
74 pub nft: SuinsRegistration,
75}
76
77#[derive(Clone, Eq, PartialEq, Debug)]
80pub enum DomainFormat {
81 At,
82 Dot,
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
86#[serde(rename_all = "kebab-case")]
87pub struct NameServiceConfig {
88 pub package_address: SuiAddress,
89 pub registry_id: ObjectID,
90 pub reverse_registry_id: ObjectID,
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
95pub struct Table<K, V> {
96 pub id: ObjectID,
97 pub size: u64,
98
99 #[serde(skip)]
100 _key: PhantomData<K>,
101 #[serde(skip)]
102 _value: PhantomData<V>,
103}
104
105#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
106pub enum NameServiceError {
107 #[error("Name Service: String length: {0} exceeds maximum allowed length: {1}")]
108 ExceedsMaxLength(usize, usize),
109 #[error("Name Service: String length: {0} outside of valid range: [{1}, {2}]")]
110 InvalidLength(usize, usize, usize),
111 #[error("Name Service: Hyphens are not allowed as the first or last character")]
112 InvalidHyphens,
113 #[error("Name Service: Only lowercase letters, numbers, and hyphens are allowed")]
114 InvalidUnderscore,
115 #[error("Name Service: Domain must contain at least one label")]
116 LabelsEmpty,
117 #[error("Name Service: Domain must include only one separator")]
118 InvalidSeparator,
119
120 #[error("Name Service: Name has expired.")]
121 NameExpired,
122 #[error("Name Service: Malformed object for {0}")]
123 MalformedObject(ObjectID),
124}
125
126impl Domain {
127 pub fn type_(package_address: SuiAddress) -> StructTag {
128 StructTag {
129 address: package_address.into(),
130 module: NAME_SERVICE_DOMAIN_MODULE.to_owned(),
131 name: NAME_SERVICE_DOMAIN_STRUCT.to_owned(),
132 type_params: vec![],
133 }
134 }
135
136 pub fn parent(&self) -> Domain {
142 Domain {
143 labels: self.labels[0..(self.labels.len() - 1)].to_vec(),
144 }
145 }
146
147 pub fn is_subdomain(&self) -> bool {
148 self.depth() >= 3
149 }
150
151 pub fn depth(&self) -> u8 {
157 self.labels.len() as u8
158 }
159
160 pub fn format(&self, format: DomainFormat) -> String {
163 let mut labels = self.labels.clone();
164 let sep = &ACCEPTED_SEPARATORS[0].to_string();
165 labels.reverse();
166
167 if format == DomainFormat::Dot {
168 return labels.join(sep);
169 };
170
171 let _tld = labels.pop();
174 let sld = labels.pop().unwrap();
175
176 format!("{}{}{}", labels.join(sep), SUI_NEW_FORMAT_SEPARATOR, sld)
177 }
178}
179
180impl NameServiceConfig {
181 pub fn new(
182 package_address: SuiAddress,
183 registry_id: ObjectID,
184 reverse_registry_id: ObjectID,
185 ) -> Self {
186 Self {
187 package_address,
188 registry_id,
189 reverse_registry_id,
190 }
191 }
192
193 pub fn record_field_id(&self, domain: &Domain) -> ObjectID {
194 let domain_type_tag = Domain::type_(self.package_address);
195 let domain_bytes = bcs::to_bytes(domain).unwrap();
196
197 sui_types::dynamic_field::derive_dynamic_field_id(
198 self.registry_id,
199 &TypeTag::Struct(Box::new(domain_type_tag)),
200 &domain_bytes,
201 )
202 .unwrap()
203 }
204
205 pub fn reverse_record_field_id(&self, address: &[u8]) -> ObjectID {
206 sui_types::dynamic_field::derive_dynamic_field_id(
207 self.reverse_registry_id,
208 &TypeTag::Address,
209 address,
210 )
211 .unwrap()
212 }
213
214 pub fn name_record_type(&self) -> StructTag {
215 StructTag {
216 address: self.package_address.into(),
217 module: ident_str!("name_record").to_owned(),
218 name: ident_str!("NameRecord").to_owned(),
219 type_params: vec![],
220 }
221 }
222
223 pub fn mainnet() -> Self {
225 const MAINNET_NS_PACKAGE_ADDRESS: &str =
226 "0xd22b24490e0bae52676651b4f56660a5ff8022a2576e0089f79b3c88d44e08f0";
227 const MAINNET_NS_REGISTRY_ID: &str =
228 "0xe64cd9db9f829c6cc405d9790bd71567ae07259855f4fba6f02c84f52298c106";
229 const MAINNET_NS_REVERSE_REGISTRY_ID: &str =
230 "0x2fd099e17a292d2bc541df474f9fafa595653848cbabb2d7a4656ec786a1969f";
231
232 let package_address = SuiAddress::from_str(MAINNET_NS_PACKAGE_ADDRESS).unwrap();
233 let registry_id = ObjectID::from_str(MAINNET_NS_REGISTRY_ID).unwrap();
234 let reverse_registry_id = ObjectID::from_str(MAINNET_NS_REVERSE_REGISTRY_ID).unwrap();
235
236 Self::new(package_address, registry_id, reverse_registry_id)
237 }
238
239 pub fn testnet() -> Self {
241 const TESTNET_NS_PACKAGE_ADDRESS: &str =
242 "0x22fa05f21b1ad71442491220bb9338f7b7095fe35000ef88d5400d28523bdd93";
243 const TESTNET_NS_REGISTRY_ID: &str =
244 "0xb120c0d55432630fce61f7854795a3463deb6e3b443cc4ae72e1282073ff56e4";
245 const TESTNET_NS_REVERSE_REGISTRY_ID: &str =
246 "0xcee9dbb070db70936c3a374439a6adb16f3ba97eac5468d2e1e6fff6ed93e465";
247
248 let package_address = SuiAddress::from_str(TESTNET_NS_PACKAGE_ADDRESS).unwrap();
249 let registry_id = ObjectID::from_str(TESTNET_NS_REGISTRY_ID).unwrap();
250 let reverse_registry_id = ObjectID::from_str(TESTNET_NS_REVERSE_REGISTRY_ID).unwrap();
251
252 Self::new(package_address, registry_id, reverse_registry_id)
253 }
254}
255
256impl NameRecord {
257 pub fn is_leaf_record(&self) -> bool {
260 self.expiration_timestamp_ms == LEAF_EXPIRATION_TIMESTAMP
261 }
262
263 pub fn is_valid_leaf_parent(&self, child: &NameRecord) -> bool {
267 self.nft_id == child.nft_id
268 }
269
270 pub fn is_node_expired(&self, checkpoint_timestamp_ms: u64) -> bool {
273 self.expiration_timestamp_ms < checkpoint_timestamp_ms
274 }
275}
276
277impl FromStr for Domain {
278 type Err = NameServiceError;
279
280 fn from_str(s: &str) -> Result<Self, Self::Err> {
281 const MAX_DOMAIN_LENGTH: usize = 200;
283
284 if s.len() > MAX_DOMAIN_LENGTH {
285 return Err(NameServiceError::ExceedsMaxLength(
286 s.len(),
287 MAX_DOMAIN_LENGTH,
288 ));
289 }
290 let separator = separator(s)?;
291
292 let formatted_string = convert_from_new_format(s, &separator)?;
293
294 let labels = formatted_string
295 .split(separator)
296 .rev()
297 .map(validate_label)
298 .collect::<Result<Vec<_>, Self::Err>>()?;
299
300 if labels.len() < 2 {
302 return Err(NameServiceError::LabelsEmpty);
303 }
304
305 let labels = labels.into_iter().map(ToOwned::to_owned).collect();
306 Ok(Domain { labels })
307 }
308}
309
310impl fmt::Display for Domain {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 let output = self.format(DomainFormat::Dot);
315 f.write_str(&output)?;
316
317 Ok(())
318 }
319}
320
321impl Default for NameServiceConfig {
322 fn default() -> Self {
323 Self::mainnet()
324 }
325}
326
327impl TryFrom<Object> for NameRecord {
328 type Error = NameServiceError;
329
330 fn try_from(object: Object) -> Result<Self, NameServiceError> {
331 object
332 .to_rust::<Field<Domain, Self>>()
333 .map(|record| record.value)
334 .ok_or_else(|| NameServiceError::MalformedObject(object.id()))
335 }
336}
337
338impl TryFrom<MoveObject> for NameRecord {
339 type Error = NameServiceError;
340
341 fn try_from(object: MoveObject) -> Result<Self, NameServiceError> {
342 object
343 .to_rust::<Field<Domain, Self>>()
344 .map(|record| record.value)
345 .ok_or_else(|| NameServiceError::MalformedObject(object.id()))
346 }
347}
348
349fn separator(s: &str) -> Result<char, NameServiceError> {
352 let mut domain_separator: Option<char> = None;
353
354 for separator in ACCEPTED_SEPARATORS.iter() {
355 if s.contains(*separator) {
356 if domain_separator.is_some() {
357 return Err(NameServiceError::InvalidSeparator);
358 }
359
360 domain_separator = Some(*separator);
361 }
362 }
363
364 match domain_separator {
365 Some(separator) => Ok(separator),
366 None => Ok(ACCEPTED_SEPARATORS[0]),
367 }
368}
369
370fn convert_from_new_format(s: &str, separator: &char) -> Result<String, NameServiceError> {
374 let mut splits = s.split(SUI_NEW_FORMAT_SEPARATOR);
375
376 let Some(before) = splits.next() else {
377 return Err(NameServiceError::InvalidSeparator);
378 };
379
380 let Some(after) = splits.next() else {
381 return Ok(before.to_string());
382 };
383
384 if splits.next().is_some() || after.contains(*separator) || after.is_empty() {
385 return Err(NameServiceError::InvalidSeparator);
386 }
387
388 let mut parts = vec![];
389
390 if !before.is_empty() {
391 parts.push(before);
392 }
393
394 parts.push(after);
395 parts.push(DEFAULT_TLD);
396
397 Ok(parts.join(&separator.to_string()))
398}
399
400pub fn validate_label(label: &str) -> Result<&str, NameServiceError> {
401 const MIN_LABEL_LENGTH: usize = 1;
402 const MAX_LABEL_LENGTH: usize = 63;
403 let bytes = label.as_bytes();
404 let len = bytes.len();
405
406 if !(MIN_LABEL_LENGTH..=MAX_LABEL_LENGTH).contains(&len) {
407 return Err(NameServiceError::InvalidLength(
408 len,
409 MIN_LABEL_LENGTH,
410 MAX_LABEL_LENGTH,
411 ));
412 }
413
414 for (i, character) in bytes.iter().enumerate() {
415 let is_valid_character = match character {
416 b'a'..=b'z' => true,
417 b'0'..=b'9' => true,
418 b'-' if i != 0 && i != len - 1 => true,
419 _ => false,
420 };
421
422 if !is_valid_character {
423 match character {
424 b'-' => return Err(NameServiceError::InvalidHyphens),
425 _ => return Err(NameServiceError::InvalidUnderscore),
426 }
427 };
428 }
429 Ok(label)
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn test_parent_extraction() {
438 let mut name = Domain::from_str("leaf.node.test.sui").unwrap();
439
440 assert_eq!(name.parent().to_string(), "node.test.sui");
441
442 name = Domain::from_str("node.test.sui").unwrap();
443
444 assert_eq!(name.parent().to_string(), "test.sui");
445 }
446
447 #[test]
448 fn test_expirations() {
449 let system_time: u64 = 100;
450
451 let mut name = NameRecord {
452 nft_id: sui_types::id::ID::new(ObjectID::random()),
453 data: VecMap { contents: vec![] },
454 target_address: Some(SuiAddress::random_for_testing_only()),
455 expiration_timestamp_ms: system_time + 10,
456 };
457
458 assert!(!name.is_node_expired(system_time));
459
460 name.expiration_timestamp_ms = system_time - 10;
461
462 assert!(name.is_node_expired(system_time));
463 }
464
465 #[test]
466 fn test_name_service_outputs() {
467 assert_eq!("@test".parse::<Domain>().unwrap().to_string(), "test.sui");
468 assert_eq!(
469 "test.sui".parse::<Domain>().unwrap().to_string(),
470 "test.sui"
471 );
472 assert_eq!(
473 "test@sld".parse::<Domain>().unwrap().to_string(),
474 "test.sld.sui"
475 );
476 assert_eq!(
477 "test.test@example".parse::<Domain>().unwrap().to_string(),
478 "test.test.example.sui"
479 );
480 assert_eq!(
481 "sui@sui".parse::<Domain>().unwrap().to_string(),
482 "sui.sui.sui"
483 );
484
485 assert_eq!("@sui".parse::<Domain>().unwrap().to_string(), "sui.sui");
486
487 assert_eq!(
488 "test*test@test".parse::<Domain>().unwrap().to_string(),
489 "test.test.test.sui"
490 );
491 assert_eq!(
492 "test.test.sui".parse::<Domain>().unwrap().to_string(),
493 "test.test.sui"
494 );
495 assert_eq!(
496 "test.test.test.sui".parse::<Domain>().unwrap().to_string(),
497 "test.test.test.sui"
498 );
499 }
500
501 #[test]
502 fn test_different_wildcard() {
503 assert_eq!("test.sui".parse::<Domain>(), "test*sui".parse::<Domain>(),);
504
505 assert_eq!("@test".parse::<Domain>(), "test*sui".parse::<Domain>(),);
506 }
507
508 #[test]
509 fn test_invalid_inputs() {
510 assert!("*".parse::<Domain>().is_err());
511 assert!(".".parse::<Domain>().is_err());
512 assert!("@".parse::<Domain>().is_err());
513 assert!("@inner.sui".parse::<Domain>().is_err());
514 assert!("@inner*sui".parse::<Domain>().is_err());
515 assert!("test@".parse::<Domain>().is_err());
516 assert!("sui".parse::<Domain>().is_err());
517 assert!("test.test@example.sui".parse::<Domain>().is_err());
518 assert!("test@test@example".parse::<Domain>().is_err());
519 }
520
521 #[test]
522 fn output_tests() {
523 let mut domain = "test.sui".parse::<Domain>().unwrap();
524 assert!(domain.format(DomainFormat::Dot) == "test.sui");
525 assert!(domain.format(DomainFormat::At) == "@test");
526
527 domain = "test.test.sui".parse::<Domain>().unwrap();
528 assert!(domain.format(DomainFormat::Dot) == "test.test.sui");
529 assert!(domain.format(DomainFormat::At) == "test@test");
530
531 domain = "test.test.test.sui".parse::<Domain>().unwrap();
532 assert!(domain.format(DomainFormat::Dot) == "test.test.test.sui");
533 assert!(domain.format(DomainFormat::At) == "test.test@test");
534
535 domain = "test.test.test.test.sui".parse::<Domain>().unwrap();
536 assert!(domain.format(DomainFormat::Dot) == "test.test.test.test.sui");
537 assert!(domain.format(DomainFormat::At) == "test.test.test@test");
538 }
539}