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 mainnet() -> Self {
216 const MAINNET_NS_PACKAGE_ADDRESS: &str =
217 "0xd22b24490e0bae52676651b4f56660a5ff8022a2576e0089f79b3c88d44e08f0";
218 const MAINNET_NS_REGISTRY_ID: &str =
219 "0xe64cd9db9f829c6cc405d9790bd71567ae07259855f4fba6f02c84f52298c106";
220 const MAINNET_NS_REVERSE_REGISTRY_ID: &str =
221 "0x2fd099e17a292d2bc541df474f9fafa595653848cbabb2d7a4656ec786a1969f";
222
223 let package_address = SuiAddress::from_str(MAINNET_NS_PACKAGE_ADDRESS).unwrap();
224 let registry_id = ObjectID::from_str(MAINNET_NS_REGISTRY_ID).unwrap();
225 let reverse_registry_id = ObjectID::from_str(MAINNET_NS_REVERSE_REGISTRY_ID).unwrap();
226
227 Self::new(package_address, registry_id, reverse_registry_id)
228 }
229
230 pub fn testnet() -> Self {
232 const TESTNET_NS_PACKAGE_ADDRESS: &str =
233 "0x22fa05f21b1ad71442491220bb9338f7b7095fe35000ef88d5400d28523bdd93";
234 const TESTNET_NS_REGISTRY_ID: &str =
235 "0xb120c0d55432630fce61f7854795a3463deb6e3b443cc4ae72e1282073ff56e4";
236 const TESTNET_NS_REVERSE_REGISTRY_ID: &str =
237 "0xcee9dbb070db70936c3a374439a6adb16f3ba97eac5468d2e1e6fff6ed93e465";
238
239 let package_address = SuiAddress::from_str(TESTNET_NS_PACKAGE_ADDRESS).unwrap();
240 let registry_id = ObjectID::from_str(TESTNET_NS_REGISTRY_ID).unwrap();
241 let reverse_registry_id = ObjectID::from_str(TESTNET_NS_REVERSE_REGISTRY_ID).unwrap();
242
243 Self::new(package_address, registry_id, reverse_registry_id)
244 }
245}
246
247impl NameRecord {
248 pub fn is_leaf_record(&self) -> bool {
251 self.expiration_timestamp_ms == LEAF_EXPIRATION_TIMESTAMP
252 }
253
254 pub fn is_valid_leaf_parent(&self, child: &NameRecord) -> bool {
258 self.nft_id == child.nft_id
259 }
260
261 pub fn is_node_expired(&self, checkpoint_timestamp_ms: u64) -> bool {
264 self.expiration_timestamp_ms < checkpoint_timestamp_ms
265 }
266}
267
268impl FromStr for Domain {
269 type Err = NameServiceError;
270
271 fn from_str(s: &str) -> Result<Self, Self::Err> {
272 const MAX_DOMAIN_LENGTH: usize = 200;
274
275 if s.len() > MAX_DOMAIN_LENGTH {
276 return Err(NameServiceError::ExceedsMaxLength(
277 s.len(),
278 MAX_DOMAIN_LENGTH,
279 ));
280 }
281 let separator = separator(s)?;
282
283 let formatted_string = convert_from_new_format(s, &separator)?;
284
285 let labels = formatted_string
286 .split(separator)
287 .rev()
288 .map(validate_label)
289 .collect::<Result<Vec<_>, Self::Err>>()?;
290
291 if labels.len() < 2 {
293 return Err(NameServiceError::LabelsEmpty);
294 }
295
296 let labels = labels.into_iter().map(ToOwned::to_owned).collect();
297 Ok(Domain { labels })
298 }
299}
300
301impl fmt::Display for Domain {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 let output = self.format(DomainFormat::Dot);
306 f.write_str(&output)?;
307
308 Ok(())
309 }
310}
311
312impl Default for NameServiceConfig {
313 fn default() -> Self {
314 Self::mainnet()
315 }
316}
317
318impl TryFrom<Object> for NameRecord {
319 type Error = NameServiceError;
320
321 fn try_from(object: Object) -> Result<Self, NameServiceError> {
322 object
323 .to_rust::<Field<Domain, Self>>()
324 .map(|record| record.value)
325 .ok_or_else(|| NameServiceError::MalformedObject(object.id()))
326 }
327}
328
329impl TryFrom<MoveObject> for NameRecord {
330 type Error = NameServiceError;
331
332 fn try_from(object: MoveObject) -> Result<Self, NameServiceError> {
333 object
334 .to_rust::<Field<Domain, Self>>()
335 .map(|record| record.value)
336 .ok_or_else(|| NameServiceError::MalformedObject(object.id()))
337 }
338}
339
340fn separator(s: &str) -> Result<char, NameServiceError> {
343 let mut domain_separator: Option<char> = None;
344
345 for separator in ACCEPTED_SEPARATORS.iter() {
346 if s.contains(*separator) {
347 if domain_separator.is_some() {
348 return Err(NameServiceError::InvalidSeparator);
349 }
350
351 domain_separator = Some(*separator);
352 }
353 }
354
355 match domain_separator {
356 Some(separator) => Ok(separator),
357 None => Ok(ACCEPTED_SEPARATORS[0]),
358 }
359}
360
361fn convert_from_new_format(s: &str, separator: &char) -> Result<String, NameServiceError> {
365 let mut splits = s.split(SUI_NEW_FORMAT_SEPARATOR);
366
367 let Some(before) = splits.next() else {
368 return Err(NameServiceError::InvalidSeparator);
369 };
370
371 let Some(after) = splits.next() else {
372 return Ok(before.to_string());
373 };
374
375 if splits.next().is_some() || after.contains(*separator) || after.is_empty() {
376 return Err(NameServiceError::InvalidSeparator);
377 }
378
379 let mut parts = vec![];
380
381 if !before.is_empty() {
382 parts.push(before);
383 }
384
385 parts.push(after);
386 parts.push(DEFAULT_TLD);
387
388 Ok(parts.join(&separator.to_string()))
389}
390
391pub fn validate_label(label: &str) -> Result<&str, NameServiceError> {
392 const MIN_LABEL_LENGTH: usize = 1;
393 const MAX_LABEL_LENGTH: usize = 63;
394 let bytes = label.as_bytes();
395 let len = bytes.len();
396
397 if !(MIN_LABEL_LENGTH..=MAX_LABEL_LENGTH).contains(&len) {
398 return Err(NameServiceError::InvalidLength(
399 len,
400 MIN_LABEL_LENGTH,
401 MAX_LABEL_LENGTH,
402 ));
403 }
404
405 for (i, character) in bytes.iter().enumerate() {
406 let is_valid_character = match character {
407 b'a'..=b'z' => true,
408 b'0'..=b'9' => true,
409 b'-' if i != 0 && i != len - 1 => true,
410 _ => false,
411 };
412
413 if !is_valid_character {
414 match character {
415 b'-' => return Err(NameServiceError::InvalidHyphens),
416 _ => return Err(NameServiceError::InvalidUnderscore),
417 }
418 };
419 }
420 Ok(label)
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_parent_extraction() {
429 let mut name = Domain::from_str("leaf.node.test.sui").unwrap();
430
431 assert_eq!(name.parent().to_string(), "node.test.sui");
432
433 name = Domain::from_str("node.test.sui").unwrap();
434
435 assert_eq!(name.parent().to_string(), "test.sui");
436 }
437
438 #[test]
439 fn test_expirations() {
440 let system_time: u64 = 100;
441
442 let mut name = NameRecord {
443 nft_id: sui_types::id::ID::new(ObjectID::random()),
444 data: VecMap { contents: vec![] },
445 target_address: Some(SuiAddress::random_for_testing_only()),
446 expiration_timestamp_ms: system_time + 10,
447 };
448
449 assert!(!name.is_node_expired(system_time));
450
451 name.expiration_timestamp_ms = system_time - 10;
452
453 assert!(name.is_node_expired(system_time));
454 }
455
456 #[test]
457 fn test_name_service_outputs() {
458 assert_eq!("@test".parse::<Domain>().unwrap().to_string(), "test.sui");
459 assert_eq!(
460 "test.sui".parse::<Domain>().unwrap().to_string(),
461 "test.sui"
462 );
463 assert_eq!(
464 "test@sld".parse::<Domain>().unwrap().to_string(),
465 "test.sld.sui"
466 );
467 assert_eq!(
468 "test.test@example".parse::<Domain>().unwrap().to_string(),
469 "test.test.example.sui"
470 );
471 assert_eq!(
472 "sui@sui".parse::<Domain>().unwrap().to_string(),
473 "sui.sui.sui"
474 );
475
476 assert_eq!("@sui".parse::<Domain>().unwrap().to_string(), "sui.sui");
477
478 assert_eq!(
479 "test*test@test".parse::<Domain>().unwrap().to_string(),
480 "test.test.test.sui"
481 );
482 assert_eq!(
483 "test.test.sui".parse::<Domain>().unwrap().to_string(),
484 "test.test.sui"
485 );
486 assert_eq!(
487 "test.test.test.sui".parse::<Domain>().unwrap().to_string(),
488 "test.test.test.sui"
489 );
490 }
491
492 #[test]
493 fn test_different_wildcard() {
494 assert_eq!("test.sui".parse::<Domain>(), "test*sui".parse::<Domain>(),);
495
496 assert_eq!("@test".parse::<Domain>(), "test*sui".parse::<Domain>(),);
497 }
498
499 #[test]
500 fn test_invalid_inputs() {
501 assert!("*".parse::<Domain>().is_err());
502 assert!(".".parse::<Domain>().is_err());
503 assert!("@".parse::<Domain>().is_err());
504 assert!("@inner.sui".parse::<Domain>().is_err());
505 assert!("@inner*sui".parse::<Domain>().is_err());
506 assert!("test@".parse::<Domain>().is_err());
507 assert!("sui".parse::<Domain>().is_err());
508 assert!("test.test@example.sui".parse::<Domain>().is_err());
509 assert!("test@test@example".parse::<Domain>().is_err());
510 }
511
512 #[test]
513 fn output_tests() {
514 let mut domain = "test.sui".parse::<Domain>().unwrap();
515 assert!(domain.format(DomainFormat::Dot) == "test.sui");
516 assert!(domain.format(DomainFormat::At) == "@test");
517
518 domain = "test.test.sui".parse::<Domain>().unwrap();
519 assert!(domain.format(DomainFormat::Dot) == "test.test.sui");
520 assert!(domain.format(DomainFormat::At) == "test@test");
521
522 domain = "test.test.test.sui".parse::<Domain>().unwrap();
523 assert!(domain.format(DomainFormat::Dot) == "test.test.test.sui");
524 assert!(domain.format(DomainFormat::At) == "test.test@test");
525
526 domain = "test.test.test.test.sui".parse::<Domain>().unwrap();
527 assert!(domain.format(DomainFormat::Dot) == "test.test.test.test.sui");
528 assert!(domain.format(DomainFormat::At) == "test.test.test@test");
529 }
530}