neo3/neo_contract/
name_service.rs

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