sui_rpc_api/grpc/v2/
name_service.rs1use sui_name_service::Domain;
5use sui_name_service::NameRecord;
6use sui_name_service::NameServiceConfig;
7use sui_rpc::proto::timestamp_ms_to_proto;
8use sui_sdk_types::Address;
9
10use crate::ErrorReason;
11use crate::Result;
12use crate::RpcError;
13use crate::RpcService;
14use sui_rpc::proto::google::rpc::bad_request::FieldViolation;
15use sui_rpc::proto::sui::rpc::v2::LookupNameRequest;
16use sui_rpc::proto::sui::rpc::v2::LookupNameResponse;
17use sui_rpc::proto::sui::rpc::v2::ReverseLookupNameRequest;
18use sui_rpc::proto::sui::rpc::v2::ReverseLookupNameResponse;
19use sui_rpc::proto::sui::rpc::v2::name_service_server::NameService;
20
21#[tonic::async_trait]
22impl NameService for RpcService {
23 async fn lookup_name(
24 &self,
25 request: tonic::Request<LookupNameRequest>,
26 ) -> Result<tonic::Response<LookupNameResponse>, tonic::Status> {
27 lookup_name(self, request.into_inner())
28 .map(tonic::Response::new)
29 .map_err(Into::into)
30 }
31
32 async fn reverse_lookup_name(
33 &self,
34 request: tonic::Request<ReverseLookupNameRequest>,
35 ) -> Result<tonic::Response<ReverseLookupNameResponse>, tonic::Status> {
36 reverse_lookup_name(self, request.into_inner())
37 .map(tonic::Response::new)
38 .map_err(Into::into)
39 }
40}
41
42fn name_service_config(service: &RpcService) -> Result<NameServiceConfig> {
43 match service.chain_id.chain() {
44 sui_protocol_config::Chain::Mainnet => Ok(NameServiceConfig::mainnet()),
45 sui_protocol_config::Chain::Testnet => Ok(NameServiceConfig::testnet()),
46 sui_protocol_config::Chain::Unknown => Err(RpcError::new(
47 tonic::Code::Unimplemented,
48 "SuiNS not configured for this network",
49 )),
50 }
51}
52
53#[tracing::instrument(skip(service))]
54fn lookup_name(service: &RpcService, request: LookupNameRequest) -> Result<LookupNameResponse> {
55 let name_service_config = name_service_config(service)?;
56
57 let domain = request
58 .name
59 .ok_or_else(|| {
60 FieldViolation::new(LookupNameRequest::NAME_FIELD.name)
61 .with_reason(ErrorReason::FieldMissing)
62 })?
63 .parse::<Domain>()
64 .map_err(|e| {
65 FieldViolation::new(LookupNameRequest::NAME_FIELD.name)
66 .with_description(format!("invalid domain: {e}"))
67 .with_reason(ErrorReason::FieldInvalid)
68 })?;
69
70 let record_id = name_service_config.record_field_id(&domain);
71
72 let current_timestamp_ms = service.reader.inner().get_latest_checkpoint()?.timestamp_ms;
73
74 let Some(record_object) = service.reader.inner().get_object(&record_id) else {
75 return Err(RpcError::not_found());
76 };
77
78 let name_record = NameRecord::try_from(record_object)
79 .map_err(|e| RpcError::new(tonic::Code::Internal, e.to_string()))?;
80
81 let is_valid = if !name_record.is_leaf_record() {
82 !name_record.is_node_expired(current_timestamp_ms)
85 } else {
86 let parent_domain = domain.parent();
90 let parent_record_id = name_service_config.record_field_id(&parent_domain);
91
92 if let Some(parent_object) = service.reader.inner().get_object(&parent_record_id) {
96 let parent_name_record = NameRecord::try_from(parent_object)
97 .map_err(|e| RpcError::new(tonic::Code::Internal, e.to_string()))?;
98 parent_name_record.is_valid_leaf_parent(&name_record)
99 && !parent_name_record.is_node_expired(current_timestamp_ms)
100 } else {
101 false
102 }
103 };
104
105 if is_valid {
106 let mut record = sui_rpc::proto::sui::rpc::v2::NameRecord::default();
107 record.id = Some(record_id.to_canonical_string(true));
108 record.name = Some(domain.to_string());
109 record.registration_nft_id = Some(name_record.nft_id.bytes.to_canonical_string(true));
110 record.expiration_timestamp =
111 Some(timestamp_ms_to_proto(name_record.expiration_timestamp_ms));
112 record.target_address = name_record
113 .target_address
114 .map(|address| address.to_string());
115 record.data = name_record
116 .data
117 .contents
118 .into_iter()
119 .map(|entry| (entry.key, entry.value))
120 .collect();
121
122 let mut response = LookupNameResponse::default();
123 response.record = Some(record);
124 Ok(response)
125 } else {
126 Err(RpcError::new(
127 tonic::Code::ResourceExhausted,
128 "name has expired",
129 ))
130 }
131}
132
133#[tracing::instrument(skip(service))]
134fn reverse_lookup_name(
135 service: &RpcService,
136 request: ReverseLookupNameRequest,
137) -> Result<ReverseLookupNameResponse> {
138 let name_service_config = name_service_config(service)?;
139
140 let address = request
141 .address
142 .ok_or_else(|| {
143 FieldViolation::new(ReverseLookupNameRequest::ADDRESS_FIELD.name)
144 .with_reason(ErrorReason::FieldMissing)
145 })?
146 .parse::<Address>()
147 .map_err(|e| {
148 FieldViolation::new(ReverseLookupNameRequest::ADDRESS_FIELD.name)
149 .with_description(format!("invalid address: {e}"))
150 .with_reason(ErrorReason::FieldInvalid)
151 })?;
152
153 let reverse_record_id = name_service_config.reverse_record_field_id(address.as_ref());
154
155 let Some(field_reverse_record_object) = service.reader.inner().get_object(&reverse_record_id)
156 else {
157 return Err(RpcError::not_found());
158 };
159
160 let domain = field_reverse_record_object
161 .to_rust::<sui_types::dynamic_field::Field<Address, Domain>>()
162 .ok_or_else(|| {
163 RpcError::new(
164 tonic::Code::Internal,
165 format!("Malformed Object {reverse_record_id}"),
166 )
167 })?
168 .value;
169
170 let domain_name = domain.to_string();
171
172 let maybe_record = lookup_name(service, LookupNameRequest::new(&domain_name))?;
173
174 let Some(record) = maybe_record.record else {
176 return Err(RpcError::not_found());
177 };
178
179 if record.target_address() != address.to_string() {
180 return Err(RpcError::not_found());
181 }
182
183 Ok(ReverseLookupNameResponse::new(record))
184}