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 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 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 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 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 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}