sui_rpc_api/grpc/v2/
name_service.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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        // Handling SLD names & node subdomains is the same (we handle them as `node` records)
83        // We check their expiration, and if not expired, return the target address.
84        !name_record.is_node_expired(current_timestamp_ms)
85    } else {
86        // If a record is a leaf record we need to check its parent for expiration.
87
88        // prepare the parent's field id.
89        let parent_domain = domain.parent();
90        let parent_record_id = name_service_config.record_field_id(&parent_domain);
91
92        // For a leaf record, we check that:
93        // 1. The parent is a valid parent for that leaf record
94        // 2. The parent is not expired
95        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    // If looking up the domain returns an empty result, we return an empty result.
175    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}