neo3/neo_contract/
name_service.rs

1use std::{string::ToString, sync::Arc};
2
3use crate::{
4	builder::TransactionBuilder,
5	deserialize_script_hash, deserialize_script_hash_option,
6	neo_clients::{APITrait, JsonRpcProvider, RpcClient},
7	neo_contract::{
8		ContractError, NeoIterator, NonFungibleTokenTrait, SmartContractTrait, TokenTrait,
9	},
10	serialize_script_hash, serialize_script_hash_option, AddressOrScriptHash, ContractParameter,
11	NNSName, ScriptHash, StackItem,
12};
13use async_trait::async_trait;
14use futures::FutureExt;
15use primitive_types::H160;
16use serde::{Deserialize, Serialize};
17
18#[repr(u8)]
19enum RecordType {
20	None = 0,
21	Txt = 1,
22	A = 2,
23	Aaaa = 3,
24	Cname = 4,
25	Srv = 5,
26	Url = 6,
27	Oauth = 7,
28	Ipfs = 8,
29	Email = 9,
30	Dnssec = 10,
31	Tlsa = 11,
32	Smimea = 12,
33	Hippo = 13,
34	Http = 14,
35	Sshfp = 15,
36	Onion = 16,
37	Xmpp = 17,
38	Magnet = 18,
39	Tor = 19,
40	I2p = 20,
41	Git = 21,
42	Keybase = 22,
43	Briar = 23,
44	Zcash = 24,
45	Mini = 25,
46}
47
48// NameState struct
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct NameState {
52	pub name: String,
53	pub expiration: u32,
54	#[serde(skip_serializing_if = "Option::is_none")]
55	#[serde(deserialize_with = "deserialize_script_hash_option")]
56	#[serde(serialize_with = "serialize_script_hash_option")]
57	pub admin: Option<ScriptHash>,
58}
59
60#[derive(Serialize, Deserialize, Debug, Clone)]
61pub struct NeoNameService<'a, P: JsonRpcProvider> {
62	#[serde(deserialize_with = "deserialize_script_hash")]
63	#[serde(serialize_with = "serialize_script_hash")]
64	script_hash: ScriptHash,
65	#[serde(skip)]
66	provider: Option<&'a RpcClient<P>>,
67}
68
69impl<'a, P: JsonRpcProvider + 'static> NeoNameService<'a, P> {
70	const ADD_ROOT: &'static str = "addRoot";
71	const ROOTS: &'static str = "roots";
72	const SET_PRICE: &'static str = "setPrice";
73	const GET_PRICE: &'static str = "getPrice";
74	const IS_AVAILABLE: &'static str = "isAvailable";
75	const REGISTER: &'static str = "register";
76	const RENEW: &'static str = "renew";
77	const SET_ADMIN: &'static str = "setAdmin";
78	const SET_RECORD: &'static str = "setRecord";
79	const GET_RECORD: &'static str = "getRecord";
80	const GET_ALL_RECORDS: &'static str = "getAllRecords";
81	const DELETE_RECORD: &'static str = "deleteRecord";
82	const RESOLVE: &'static str = "resolve";
83	const PROPERTIES: &'static str = "properties";
84
85	const NAME_PROPERTY: &'static str = "name";
86	const EXPIRATION_PROPERTY: &'static str = "expiration";
87	const ADMIN_PROPERTY: &'static str = "admin";
88
89	pub fn new(provider: Option<&'a RpcClient<P>>) -> Result<Self, ContractError> {
90		let provider = provider.ok_or(ContractError::ProviderNotSet(
91			"Provider is required for NeoNameService".to_string(),
92		))?;
93		Ok(Self { script_hash: provider.nns_resolver().clone(), provider: Some(provider) })
94	}
95
96	// Implementation
97
98	async fn add_root(&self, root: &str) -> Result<TransactionBuilder<P>, ContractError> {
99		let args = vec![root.to_string().into()];
100		self.invoke_function(Self::ADD_ROOT, args).await
101	}
102
103	async fn get_roots(&self) -> Result<NeoIterator<String, P>, ContractError> {
104		let args = vec![];
105		self.call_function_returning_iterator(
106			Self::ROOTS,
107			args,
108			Arc::new(|item: StackItem| item.to_string()),
109		)
110		.await
111	}
112
113	async fn get_symbol(&self) -> Result<String, ContractError> {
114		Ok("NNS".to_string())
115	}
116
117	async fn get_decimals(&self) -> Result<u8, ContractError> {
118		Ok(0)
119	}
120
121	// Register a name
122
123	pub async fn register(
124		&self,
125		name: &str,
126		owner: H160,
127	) -> Result<TransactionBuilder<P>, ContractError> {
128		self.check_domain_name_availability(name, true).await?;
129
130		let args = vec![name.into(), owner.into()];
131		self.invoke_function(Self::REGISTER, args).await
132	}
133
134	// Set admin for a name
135
136	pub async fn set_admin(
137		&self,
138		name: &str,
139		admin: H160,
140	) -> Result<TransactionBuilder<P>, ContractError> {
141		self.check_domain_name_availability(name, true).await?;
142
143		let args = vec![name.into(), admin.into()];
144		self.invoke_function(Self::SET_ADMIN, args).await
145	}
146
147	// Set record
148
149	pub async fn set_record(
150		&self,
151		name: &str,
152		record_type: RecordType,
153		data: &str,
154	) -> Result<TransactionBuilder<P>, ContractError> {
155		let args = vec![name.into(), (record_type as u8).into(), data.into()];
156
157		self.invoke_function(Self::SET_RECORD, args).await
158	}
159
160	// Delete record
161
162	pub async fn delete_record(
163		&self,
164		name: &str,
165		record_type: RecordType,
166	) -> Result<TransactionBuilder<P>, ContractError> {
167		let args = vec![name.into(), (record_type as u8).into()];
168		self.invoke_function(Self::DELETE_RECORD, args).await
169	}
170
171	pub async fn is_available(&self, name: &str) -> Result<bool, ContractError> {
172		let args = vec![name.into()];
173		self.call_function_returning_bool(Self::IS_AVAILABLE, args).await
174	}
175	pub async fn renew(
176		&self,
177		name: &str,
178		years: u32,
179	) -> Result<TransactionBuilder<P>, ContractError> {
180		self.check_domain_name_availability(name, true).await?;
181
182		let args = vec![name.into(), years.into()];
183		self.invoke_function(Self::RENEW, args).await
184	}
185
186	async fn get_name_state(&self, name: &[u8]) -> Result<NameState, ContractError> {
187		let args = vec![name.into()];
188		let provider = self.provider.ok_or(ContractError::ProviderNotSet(
189			"Provider is required for NeoNameService".to_string(),
190		))?;
191
192		let invoke_result = provider
193			.invoke_function(&self.script_hash, Self::PROPERTIES.to_string(), args, None)
194			.await
195			.map_err(|e| {
196				ContractError::InvocationFailed(format!(
197					"Failed to invoke PROPERTIES function: {}",
198					e
199				))
200			})?;
201
202		let result = invoke_result
203			.stack
204			.get(0)
205			.ok_or(ContractError::InvalidResponse("Empty stack in response".to_string()))?
206			.clone();
207
208		// Convert result to HashMap for easier access
209		let map_hash = result
210			.as_map()
211			.ok_or(ContractError::InvalidResponse("Expected map result".to_string()))?;
212
213		// Find the name property in the map
214		let name_key = StackItem::ByteString { value: Self::NAME_PROPERTY.to_string() };
215		let name_item = map_hash.get(&name_key).ok_or(ContractError::InvalidResponse(format!(
216			"Missing {} property",
217			Self::NAME_PROPERTY
218		)))?;
219		let name = name_item.as_string().ok_or_else(|| {
220			ContractError::InvalidResponse(format!("Invalid {} property type", Self::NAME_PROPERTY))
221		})?;
222
223		// Find the expiration property in the map
224		let expiration_key = StackItem::ByteString { value: Self::EXPIRATION_PROPERTY.to_string() };
225		let expiration_item =
226			map_hash.get(&expiration_key).ok_or(ContractError::InvalidResponse(format!(
227				"Missing {} property",
228				Self::EXPIRATION_PROPERTY
229			)))?;
230		let expiration = expiration_item.as_int().ok_or_else(|| {
231			ContractError::InvalidResponse(format!(
232				"Invalid {} property type",
233				Self::EXPIRATION_PROPERTY
234			))
235		})? as u32;
236
237		// Find the admin property in the map
238		let admin_key = StackItem::ByteString { value: Self::ADMIN_PROPERTY.to_string() };
239		let admin_item = map_hash.get(&admin_key).ok_or(ContractError::InvalidResponse(
240			format!("Missing {} property", Self::ADMIN_PROPERTY),
241		))?;
242		let admin = admin_item.as_address().ok_or_else(|| {
243			ContractError::InvalidResponse(format!(
244				"Invalid {} property type",
245				Self::ADMIN_PROPERTY
246			))
247		})?;
248
249		Ok(NameState {
250			name,
251			expiration,
252			admin: Some(AddressOrScriptHash::from(admin).script_hash()),
253		})
254	}
255	async fn check_domain_name_availability(
256		&self,
257		name: &str,
258		should_be_available: bool,
259	) -> Result<(), ContractError> {
260		let is_available = self.is_available(name).await?;
261
262		if should_be_available && !is_available {
263			return Err(ContractError::DomainNameNotAvailable(
264				"Domain name already taken".to_string(),
265			));
266		} else if !should_be_available && is_available {
267			return Err(ContractError::DomainNameNotRegistered(
268				"Domain name not registered".to_string(),
269			));
270		}
271
272		Ok(())
273	}
274}
275
276#[async_trait]
277impl<'a, P: JsonRpcProvider> TokenTrait<'a, P> for NeoNameService<'a, P> {
278	fn total_supply(&self) -> Option<u64> {
279		// NNS doesn't have a traditional total supply concept
280		// Return None to indicate this is not applicable for NNS
281		None
282	}
283
284	fn set_total_supply(&mut self, _total_supply: u64) {
285		// NNS doesn't have a total supply concept
286		// This is intentionally a no-op as NNS is not a fungible token
287		eprintln!("Warning: Cannot set total supply for NNS contract - operation not supported");
288	}
289
290	fn decimals(&self) -> Option<u8> {
291		Some(0)
292	}
293
294	fn set_decimals(&mut self, _decimals: u8) {
295		// NNS doesn't have a decimals concept
296		// This is intentionally a no-op as NNS is not a fungible token
297	}
298
299	fn symbol(&self) -> Option<String> {
300		Some("NNS".to_string())
301	}
302
303	fn set_symbol(&mut self, _symbol: String) {
304		// NNS doesn't have a symbol concept
305		// This is intentionally a no-op as NNS is not a fungible token
306	}
307
308	async fn resolve_nns_text_record(&self, name: &NNSName) -> Result<H160, ContractError> {
309		let provider = self.provider().ok_or(ContractError::ProviderNotSet(
310			"Provider is required for NeoNameService".to_string(),
311		))?;
312
313		let req = provider
314			.invoke_function(
315				&self.script_hash(),
316				"resolve".to_string(),
317				vec![
318					ContractParameter::from(name.name()),
319					ContractParameter::from(RecordType::Txt as u8),
320				],
321				None,
322			)
323			.await
324			.map_err(|e| {
325				ContractError::InvocationFailed(format!("Failed to invoke resolve function: {}", e))
326			})?;
327
328		let address = req
329			.stack
330			.first()
331			.ok_or(ContractError::InvalidResponse("Empty stack in response".to_string()))?
332			.clone();
333
334		let bytes = match address {
335			StackItem::ByteString { value } => Ok(value.clone()),
336			_ => Err(ContractError::InvalidResponse(
337				"Invalid address format in response".to_string(),
338			)),
339		}?;
340
341		// Convert String to bytes
342		let bytes_vec = bytes.as_bytes().to_vec();
343		Ok(H160::from_slice(&bytes_vec))
344	}
345}
346
347impl<'a, P: JsonRpcProvider> SmartContractTrait<'a> for NeoNameService<'a, P> {
348	type P = P;
349
350	fn set_name(&mut self, _name: String) {}
351
352	fn script_hash(&self) -> H160 {
353		self.script_hash
354	}
355
356	fn set_script_hash(&mut self, script_hash: H160) {
357		self.script_hash = script_hash;
358	}
359
360	fn provider(&self) -> Option<&RpcClient<P>> {
361		self.provider
362	}
363}
364
365impl<'a, P: JsonRpcProvider> NonFungibleTokenTrait<'a, P> for NeoNameService<'a, P> {}