1use std::{
5 collections::{HashMap, HashSet},
6 str::FromStr,
7};
8
9use move_core_types::language_storage::StructTag;
10use sui_name_service::{Domain, NameRecord, SubDomainRegistration};
11use sui_types::{
12 base_types::{ObjectID, SuiAddress},
13 dynamic_field::Field,
14 full_checkpoint_content::{CheckpointData, CheckpointTransaction},
15 object::Object,
16};
17
18use crate::models::VerifiedDomain;
19
20const REGISTRY_TABLE_ID: &str =
22 "0xe64cd9db9f829c6cc405d9790bd71567ae07259855f4fba6f02c84f52298c106";
23const NAME_RECORD_TYPE: &str = "0x2::dynamic_field::Field<0xd22b24490e0bae52676651b4f56660a5ff8022a2576e0089f79b3c88d44e08f0::domain::Domain,0xd22b24490e0bae52676651b4f56660a5ff8022a2576e0089f79b3c88d44e08f0::name_record::NameRecord>";
24const SUBDOMAIN_REGISTRATION_TYPE: &str = "0x00c2f85e07181b90c140b15c5ce27d863f93c4d9159d2a4e7bdaeb40e286d6f5::subdomain_registration::SubDomainRegistration";
25
26#[derive(Debug, Clone)]
27pub struct NameRecordChange(Field<Domain, NameRecord>);
28
29pub struct SuinsIndexer {
30 registry_table_id: SuiAddress,
31 subdomain_wrapper_type: StructTag,
32 name_record_type: StructTag,
33}
34
35impl std::default::Default for SuinsIndexer {
36 fn default() -> Self {
37 Self::new(
38 REGISTRY_TABLE_ID.to_owned(),
39 SUBDOMAIN_REGISTRATION_TYPE.to_owned(),
40 NAME_RECORD_TYPE.to_owned(),
41 )
42 }
43}
44
45impl SuinsIndexer {
46 pub fn new(registry_address: String, wrapper_type: String, record_type: String) -> Self {
49 let registry_table_id = SuiAddress::from_str(®istry_address).unwrap();
50 let name_record_type = StructTag::from_str(&record_type).unwrap();
51 let subdomain_wrapper_type = StructTag::from_str(&wrapper_type).unwrap();
52
53 Self {
54 registry_table_id,
55 name_record_type,
56 subdomain_wrapper_type,
57 }
58 }
59
60 pub fn is_subdomain_wrapper(&self, object: &Object) -> bool {
64 object
65 .struct_tag()
66 .is_some_and(|tag| tag == self.subdomain_wrapper_type)
67 }
68
69 pub fn is_name_record(&self, object: &Object) -> bool {
73 object
74 .get_single_owner()
75 .is_some_and(|owner| owner == self.registry_table_id)
76 && object
77 .struct_tag()
78 .is_some_and(|tag| tag == self.name_record_type)
79 }
80
81 pub fn process_checkpoint(&self, data: &CheckpointData) -> (Vec<VerifiedDomain>, Vec<String>) {
89 let mut checkpoint = SuinsIndexerCheckpoint::new(data.checkpoint_summary.sequence_number);
90
91 for transaction in &data.transactions {
95 checkpoint.parse_record_changes(self, &transaction.output_objects);
98
99 checkpoint.parse_record_deletions(self, transaction);
102 }
103
104 (
105 checkpoint.prepare_db_updates(),
107 checkpoint
108 .removals
109 .into_iter()
110 .map(|id| id.to_string())
111 .collect(),
112 )
113 }
114}
115
116pub struct SuinsIndexerCheckpoint {
117 name_records: HashMap<ObjectID, NameRecordChange>,
119 subdomain_wrappers: HashMap<String, String>,
121 removals: HashSet<ObjectID>,
123 checkpoint_sequence_number: u64,
125}
126
127impl SuinsIndexerCheckpoint {
128 pub fn new(checkpoint_sequence_number: u64) -> Self {
129 Self {
130 name_records: HashMap::new(),
131 subdomain_wrappers: HashMap::new(),
132 removals: HashSet::new(),
133 checkpoint_sequence_number,
134 }
135 }
136
137 pub fn parse_record_changes(&mut self, config: &SuinsIndexer, objects: &[Object]) {
142 for object in objects {
143 if config.is_name_record(object) {
145 let name_record: Field<Domain, NameRecord> = object
146 .to_rust()
147 .unwrap_or_else(|| panic!("Failed to parse name record for {:?}", object));
148
149 let id = object.id();
150
151 self.removals.remove(&id);
155
156 self.name_records.insert(id, NameRecordChange(name_record));
157 }
158 if config.is_subdomain_wrapper(object) {
163 let sub_domain: SubDomainRegistration = object.to_rust().unwrap();
164 self.subdomain_wrappers.insert(
165 sub_domain.nft.domain_name,
166 sub_domain.id.id.bytes.to_string(),
167 );
168 };
169 }
170 }
171
172 pub fn parse_record_deletions(
175 &mut self,
176 config: &SuinsIndexer,
177 transaction: &CheckpointTransaction,
178 ) {
179 let deleted_objects: HashSet<_> = transaction
181 .effects
182 .all_tombstones()
183 .into_iter()
184 .map(|(id, _)| id)
185 .collect();
186
187 for input in transaction.input_objects.iter() {
188 if config.is_name_record(input) && deleted_objects.contains(&input.id()) {
189 self.name_records.remove(&input.id());
193
194 self.removals.insert(input.id());
196 }
197 }
198 }
199
200 pub fn prepare_db_updates(&self) -> Vec<VerifiedDomain> {
203 let mut updates: Vec<VerifiedDomain> = vec![];
204
205 for (field_id, name_record_change) in self.name_records.iter() {
206 let name_record = &name_record_change.0;
207
208 let parent = name_record.name.parent().to_string();
209 let nft_id = name_record.value.nft_id.bytes.to_string();
210
211 updates.push(VerifiedDomain {
212 field_id: field_id.to_string(),
213 name: name_record.name.to_string(),
214 parent,
215 expiration_timestamp_ms: name_record.value.expiration_timestamp_ms as i64,
216 nft_id,
217 target_address: if name_record.value.target_address.is_some() {
218 Some(SuiAddress::to_string(
219 &name_record.value.target_address.unwrap(),
220 ))
221 } else {
222 None
223 },
224 data: serde_json::to_value(&name_record.value.data).unwrap(),
226 last_checkpoint_updated: self.checkpoint_sequence_number as i64,
227 subdomain_wrapper_id: self
228 .subdomain_wrappers
229 .get(&name_record.name.to_string())
230 .cloned(),
231 });
232 }
233
234 updates
235 }
236}
237
238pub fn format_update_field_query(field: &str) -> String {
245 format!(
246 "CASE WHEN excluded.last_checkpoint_updated > domains.last_checkpoint_updated THEN excluded.{field} ELSE domains.{field} END"
247 )
248}
249
250pub fn format_update_subdomain_wrapper_query() -> String {
252 "CASE WHEN excluded.subdomain_wrapper_id IS NOT NULL THEN excluded.subdomain_wrapper_id ELSE domains.subdomain_wrapper_id END".to_string()
253}