neo3/neo_contract/traits/
nft.rs

1use std::{collections::HashMap, sync::Arc};
2
3use crate::{
4	builder::{AccountSigner, TransactionBuilder},
5	neo_clients::JsonRpcProvider,
6	neo_contract::{ContractError, NeoIterator, NftContract, TokenTrait},
7	neo_protocol::Account,
8	Address, Bytes, ContractParameter, NNSName, ScriptHash, ScriptHashExtension, StackItem,
9};
10use async_trait::async_trait;
11use primitive_types::H160;
12
13#[async_trait]
14pub trait NonFungibleTokenTrait<'a, P: JsonRpcProvider>: TokenTrait<'a, P> + Send {
15	const OWNER_OF: &'static str = "ownerOf";
16	const TOKENS_OF: &'static str = "tokensOf";
17	const BALANCE_OF: &'static str = "balanceOf";
18	const TRANSFER: &'static str = "transfer";
19	const TOKENS: &'static str = "tokens";
20	const PROPERTIES: &'static str = "properties";
21
22	// Token methods
23
24	async fn balance_of(&mut self, owner: H160) -> Result<i32, ContractError> {
25		self.call_function_returning_int(
26			<NftContract<P> as NonFungibleTokenTrait<P>>::BALANCE_OF,
27			vec![owner.into()],
28		)
29		.await
30	}
31
32	// NFT methods
33
34	async fn tokens_of(&mut self, owner: H160) -> Result<NeoIterator<Bytes, P>, ContractError> {
35		let mapper_fn = Arc::new(|item: StackItem| item.as_bytes().unwrap());
36		self.call_function_returning_iterator(
37			<NftContract<P> as NonFungibleTokenTrait<P>>::TOKENS_OF,
38			vec![owner.into()],
39			mapper_fn,
40		)
41		.await
42	}
43
44	// Non-divisible NFT methods
45
46	async fn transfer(
47		&mut self,
48		from: &Account,
49		to: ScriptHash,
50		token_id: Bytes,
51		data: Option<ContractParameter>,
52	) -> Result<TransactionBuilder<P>, ContractError> {
53		let mut builder = self.transfer_inner(to, token_id, data).await.unwrap();
54		&builder.set_signers(vec![AccountSigner::called_by_entry(from).unwrap().into()]);
55
56		Ok(builder)
57	}
58
59	async fn transfer_inner(
60		&mut self,
61		to: ScriptHash,
62		token_id: Bytes,
63		data: Option<ContractParameter>,
64	) -> Result<TransactionBuilder<Self::P>, ContractError> {
65		self.throw_if_divisible_nft().await.unwrap();
66		self.invoke_function(
67			<NftContract<P> as NonFungibleTokenTrait<P>>::TRANSFER,
68			vec![to.into(), token_id.into(), data.unwrap()],
69		)
70		.await
71	}
72
73	async fn transfer_from_name(
74		&mut self,
75		from: &Account,
76		to: &str,
77		token_id: Bytes,
78		data: Option<ContractParameter>,
79	) -> Result<TransactionBuilder<P>, ContractError> {
80		self.throw_if_sender_is_not_owner(&from.get_script_hash(), &token_id)
81			.await
82			.unwrap();
83
84		let mut build = self
85			.transfer_inner(ScriptHash::from_address(to).unwrap(), token_id, data)
86			.await
87			.unwrap();
88		build.set_signers(vec![AccountSigner::called_by_entry(from).unwrap().into()]);
89
90		Ok(build)
91	}
92
93	async fn transfer_to_name(
94		&mut self,
95		to: &str,
96		token_id: Bytes,
97		data: Option<ContractParameter>,
98	) -> Result<TransactionBuilder<P>, ContractError> {
99		self.throw_if_divisible_nft().await.unwrap();
100
101		self.transfer_inner(
102			self.resolve_nns_text_record(&NNSName::new(to).unwrap()).await.unwrap(),
103			token_id,
104			data,
105		)
106		.await
107	}
108
109	async fn build_non_divisible_transfer_script(
110		&mut self,
111		to: Address,
112		token_id: Bytes,
113		data: ContractParameter,
114	) -> Result<Bytes, ContractError> {
115		self.throw_if_divisible_nft().await.unwrap();
116
117		self.build_invoke_function_script(
118			<NftContract<P> as NonFungibleTokenTrait<P>>::TRANSFER,
119			vec![to.into(), token_id.into(), data],
120		)
121		.await
122	}
123
124	async fn owner_of(&mut self, token_id: Bytes) -> Result<H160, ContractError> {
125		self.throw_if_divisible_nft().await.unwrap();
126
127		self.call_function_returning_script_hash(
128			<NftContract<P> as NonFungibleTokenTrait<P>>::OWNER_OF,
129			vec![token_id.into()],
130		)
131		.await
132	}
133
134	async fn throw_if_divisible_nft(&mut self) -> Result<(), ContractError> {
135		if self.get_decimals().await.unwrap() != 0 {
136			return Err(ContractError::InvalidStateError(
137				"This method is only intended for non-divisible NFTs.".to_string(),
138			));
139		}
140
141		Ok(())
142	}
143
144	async fn throw_if_sender_is_not_owner(
145		&mut self,
146		from: &ScriptHash,
147		token_id: &Bytes,
148	) -> Result<(), ContractError> {
149		let token_owner = &self.owner_of(token_id.clone()).await.unwrap();
150		if token_owner != from {
151			return Err(ContractError::InvalidArgError(
152				"The provided from account is not the owner of this token.".to_string(),
153			));
154		}
155
156		Ok(())
157	}
158
159	// Divisible NFT methods
160
161	async fn transfer_divisible(
162		&mut self,
163		from: &Account,
164		to: &ScriptHash,
165		amount: i32,
166		token_id: Bytes,
167		data: Option<ContractParameter>,
168	) -> Result<TransactionBuilder<P>, ContractError> {
169		let mut builder = self
170			.transfer_divisible_from_hashes(&from.get_script_hash(), to, amount, token_id, data)
171			.await
172			.unwrap();
173		builder.set_signers(vec![AccountSigner::called_by_entry(from).unwrap().into()]);
174		Ok(builder)
175	}
176
177	async fn transfer_divisible_from_hashes(
178		&mut self,
179		from: &ScriptHash,
180		to: &ScriptHash,
181		amount: i32,
182		token_id: Bytes,
183		data: Option<ContractParameter>,
184	) -> Result<TransactionBuilder<P>, ContractError> {
185		self.throw_if_non_divisible_nft().await.unwrap();
186
187		self.invoke_function(
188			<NftContract<P> as NonFungibleTokenTrait<P>>::TRANSFER,
189			vec![from.into(), to.into(), amount.into(), token_id.into(), data.unwrap()],
190		)
191		.await
192	}
193
194	async fn transfer_divisible_from_name(
195		&mut self,
196		from: &Account,
197		to: &str,
198		amount: i32,
199		token_id: Bytes,
200		data: Option<ContractParameter>,
201	) -> Result<TransactionBuilder<P>, ContractError> {
202		let mut builder = self
203			.transfer_divisible_from_hashes(
204				&from.get_script_hash(),
205				&self.resolve_nns_text_record(&NNSName::new(to).unwrap()).await.unwrap(),
206				amount,
207				token_id,
208				data,
209			)
210			.await
211			.unwrap();
212		builder.set_signers(vec![AccountSigner::called_by_entry(from).unwrap().into()]);
213		Ok(builder)
214	}
215
216	async fn transfer_divisible_to_name(
217		&mut self,
218		from: &ScriptHash,
219		to: &str,
220		amount: i32,
221		token_id: Bytes,
222		data: Option<ContractParameter>,
223	) -> Result<TransactionBuilder<P>, ContractError> {
224		self.throw_if_non_divisible_nft().await.unwrap();
225
226		self.transfer_divisible_from_hashes(
227			from,
228			&self.resolve_nns_text_record(&NNSName::new(to).unwrap()).await.unwrap(),
229			amount,
230			token_id,
231			data,
232		)
233		.await
234	}
235
236	async fn build_divisible_transfer_script(
237		&self,
238		from: Address,
239		to: Address,
240		amount: i32,
241		token_id: Bytes,
242		data: Option<ContractParameter>,
243	) -> Result<Bytes, ContractError> {
244		self.build_invoke_function_script(
245			<NftContract<P> as NonFungibleTokenTrait<P>>::TRANSFER,
246			vec![from.into(), to.into(), amount.into(), token_id.into(), data.unwrap()],
247		)
248		.await
249	}
250
251	async fn owners_of(
252		&mut self,
253		token_id: Bytes,
254	) -> Result<NeoIterator<Address, P>, ContractError> {
255		self.throw_if_non_divisible_nft().await.unwrap();
256
257		self.call_function_returning_iterator(
258			<NftContract<P> as NonFungibleTokenTrait<P>>::OWNER_OF,
259			vec![token_id.into()],
260			Arc::new(|item: StackItem| item.as_address().unwrap()),
261		)
262		.await
263	}
264
265	async fn throw_if_non_divisible_nft(&mut self) -> Result<(), ContractError> {
266		if self.get_decimals().await.unwrap() == 0 {
267			return Err(ContractError::InvalidStateError(
268				"This method is only intended for divisible NFTs.".to_string(),
269			));
270		}
271
272		Ok(())
273	}
274
275	async fn balance_of_divisible(
276		&mut self,
277		owner: H160,
278		token_id: Bytes,
279	) -> Result<i32, ContractError> {
280		self.throw_if_non_divisible_nft().await.unwrap();
281
282		self.call_function_returning_int(
283			<NftContract<P> as NonFungibleTokenTrait<P>>::BALANCE_OF,
284			vec![owner.into(), token_id.into()],
285		)
286		.await
287	}
288
289	// Optional methods
290
291	async fn tokens(&mut self) -> Result<NeoIterator<Bytes, P>, ContractError> {
292		self.call_function_returning_iterator(
293			<NftContract<P> as NonFungibleTokenTrait<P>>::TOKENS,
294			vec![],
295			Arc::new(|item: StackItem| item.as_bytes().unwrap()),
296		)
297		.await
298	}
299
300	async fn properties(
301		&mut self,
302		token_id: Bytes,
303	) -> Result<HashMap<String, String>, ContractError> {
304		let invocation_result = self
305			.call_invoke_function(
306				<NftContract<P> as NonFungibleTokenTrait<P>>::PROPERTIES,
307				vec![token_id.into()],
308				vec![],
309			)
310			.await
311			.unwrap();
312
313		let stack_item = invocation_result.get_first_stack_item().unwrap();
314		let map = stack_item
315			.as_map()
316			.ok_or(ContractError::UnexpectedReturnType(
317				stack_item.to_string() + &StackItem::MAP_VALUE.to_string(),
318			))
319			.unwrap();
320
321		map.iter()
322			.map(|(k, v)| {
323				let key = k.as_string().unwrap();
324				let value = v.as_string().unwrap();
325				Ok((key, value))
326			})
327			.collect()
328	}
329
330	async fn custom_properties(
331		&mut self,
332		token_id: Bytes,
333	) -> Result<HashMap<String, StackItem>, ContractError> {
334		let invocation_result = self
335			.call_invoke_function(
336				<NftContract<P> as NonFungibleTokenTrait<P>>::PROPERTIES,
337				vec![token_id.into()],
338				vec![],
339			)
340			.await
341			.unwrap();
342
343		let stack_item = invocation_result.get_first_stack_item().unwrap();
344		let map = stack_item
345			.as_map()
346			.ok_or(ContractError::UnexpectedReturnType(
347				stack_item.to_string() + &StackItem::MAP_VALUE.to_string(),
348			))
349			.unwrap();
350
351		map.into_iter()
352			.map(|(k, v)| {
353				let key = k.as_string().unwrap();
354				Ok((key, v.clone()))
355			})
356			.collect()
357	}
358}