1use cosmwasm_std::{entry_point, to_binary, Addr, Binary, Deps, Env, Order, StdError, StdResult};
2use cw721::{Expiration, OperatorsResponse};
3use cw_storage_plus::Bound;
4use cw_utils::maybe_addr;
5
6use crate::msg::{
7 AllNftInfoResponse, Approval, ApprovalResponse, ApprovalsResponse, AskingPriceResponse,
8 ContractInfoResponse, NftInfoResponse, NumTokensResponse, OwnerOfResponse, QueryMsg,
9};
10use crate::state::{State, TokenInfo, CONFIG, OPERATORS, TOKENS};
11
12const DEFAULT_LIMIT: u32 = 10;
13const MAX_LIMIT: u32 = 30;
14
15#[cfg_attr(not(feature = "library"), entry_point)]
16pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
17 match msg {
18 QueryMsg::AskingPrice { token_id } => to_binary(&query_asking_price(deps, env, token_id)?),
19
20 QueryMsg::OwnerOf {
21 token_id,
22 include_expired,
23 } => to_binary(&query_owner_of(deps, env, token_id, include_expired)?),
24
25 QueryMsg::Approval {
26 token_id,
27 operator,
28 include_expired,
29 } => to_binary(&query_approval(
30 deps,
31 env,
32 token_id,
33 operator,
34 include_expired,
35 )?),
36
37 QueryMsg::Approvals {
38 token_id,
39 include_expired,
40 } => to_binary(&query_approvals(deps, env, token_id, include_expired)?),
41
42 QueryMsg::AllOperators {
43 owner,
44 include_expired,
45 start_after,
46 limit,
47 } => to_binary(&query_approved_for_all(
48 deps,
49 env,
50 owner,
51 include_expired,
52 start_after,
53 limit,
54 )?),
55
56 QueryMsg::NumTokens {} => to_binary(&query_num_tokens(deps, env)?),
57
58 QueryMsg::ContractInfo {} => to_binary(&query_contract_info(deps, env)?),
59
60 QueryMsg::NftInfo { token_id } => to_binary(&query_nft_info(deps, env, token_id)?),
61
62 QueryMsg::AllNftInfo {
63 token_id,
64 include_expired,
65 } => to_binary(&query_all_nft_info(deps, env, token_id, include_expired)?),
66 }
67}
68
69pub fn query_asking_price(deps: Deps, _env: Env, token_id: u64) -> StdResult<AskingPriceResponse> {
70 let token_info = query_tokens(deps, token_id)?;
71 Ok(AskingPriceResponse {
72 price: token_info.base_price,
73 })
74}
75
76fn query_owner_of(
77 deps: Deps,
78 env: Env,
79 token_id: u64,
80 include_expired: Option<bool>,
81) -> StdResult<OwnerOfResponse> {
82 let token = query_tokens(deps, token_id)?;
83 let include_expired = include_expired.unwrap_or(false);
84
85 let approvals = token
86 .approvals
87 .into_iter()
88 .filter(|appr| include_expired || !appr.expires.is_expired(&env.block))
89 .collect();
90
91 Ok(OwnerOfResponse {
92 owner: token.owner.into_string(),
93 approvals,
94 })
95}
96
97fn query_approval(
98 deps: Deps,
99 env: Env,
100 token_id: u64,
101 operator: String,
102 include_expired: Option<bool>,
103) -> StdResult<ApprovalResponse> {
104 let token = query_tokens(deps, token_id)?;
105 let operator_addr = deps.api.addr_validate(operator.as_str())?;
106 let include_expired = include_expired.unwrap_or(false);
107
108 let appr: Vec<Approval> = token
109 .approvals
110 .into_iter()
111 .filter(|val| val.operator == operator_addr)
112 .collect();
113
114 if !appr.is_empty() && (include_expired || !appr[0].expires.is_expired(&env.block)) {
115 let res = ApprovalResponse {
116 approval: Approval {
117 operator: operator_addr,
118 expires: appr[0].expires,
119 },
120 };
121
122 return Ok(res);
123 };
124 Err(StdError::NotFound {
125 kind: String::from("Approval not found for given address"),
126 })
127}
128
129fn query_approvals(
130 deps: Deps,
131 env: Env,
132 token_id: u64,
133 include_expired: Option<bool>,
134) -> StdResult<ApprovalsResponse> {
135 let include_expired = include_expired.unwrap_or(false);
136 let token = query_tokens(deps, token_id)?;
137
138 let res = token
139 .approvals
140 .into_iter()
141 .filter(|appr| include_expired || !appr.expires.is_expired(&env.block))
142 .collect();
143
144 Ok(ApprovalsResponse { approvals: res })
145}
146
147fn query_approved_for_all(
148 deps: Deps,
149 env: Env,
150 owner: String,
151 include_expired: Option<bool>,
152 start_after: Option<String>,
153 limit: Option<u32>,
154) -> StdResult<OperatorsResponse> {
155 let include_expired = include_expired.unwrap_or(false);
156 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
157 let start_addr = maybe_addr(deps.api, start_after)?;
158 let start = start_addr.as_ref().map(Bound::exclusive);
159 let owner_addr = deps.api.addr_validate(&owner)?;
160
161 let res: StdResult<Vec<_>> = OPERATORS
162 .prefix(&owner_addr)
163 .range(deps.storage, start, None, Order::Ascending)
164 .filter(|r| include_expired || r.is_err() || !r.as_ref().unwrap().1.is_expired(&env.block))
165 .take(limit)
166 .map(parse_approval)
167 .collect();
168 Ok(OperatorsResponse { operators: res? })
169}
170
171fn parse_approval(item: StdResult<(Addr, Expiration)>) -> StdResult<cw721::Approval> {
172 item.map(|(spender, expires)| cw721::Approval {
173 spender: spender.to_string(),
174 expires,
175 })
176}
177
178fn query_num_tokens(deps: Deps, _env: Env) -> StdResult<NumTokensResponse> {
179 let config = query_config(deps)?;
180 Ok(NumTokensResponse {
181 tokens: config.num_tokens,
182 })
183}
184
185fn query_contract_info(deps: Deps, _env: Env) -> StdResult<ContractInfoResponse> {
186 let config = query_config(deps)?;
187 Ok(ContractInfoResponse {
188 name: config.name,
189 symbol: config.symbol,
190 })
191}
192
193fn query_nft_info(deps: Deps, _env: Env, token_id: u64) -> StdResult<NftInfoResponse> {
194 let token = query_tokens(deps, token_id)?;
195 let res = NftInfoResponse {
196 token_uri: token.token_uri.unwrap_or_else(|| "None".to_string()),
197 };
198 Ok(res)
199}
200
201fn query_all_nft_info(
202 deps: Deps,
203 env: Env,
204 token_id: u64,
205 include_expired: Option<bool>,
206) -> StdResult<AllNftInfoResponse> {
207 let owner = query_owner_of(deps, env.clone(), token_id, include_expired)?;
208 let nft = query_nft_info(deps, env, token_id)?;
209
210 let res = AllNftInfoResponse { owner, info: nft };
211 Ok(res)
212}
213
214pub fn query_config(deps: Deps) -> StdResult<State> {
215 let res = CONFIG.may_load(deps.storage)?;
216 match res {
217 Some(val) => Ok(val),
218 None => Err(StdError::GenericErr {
219 msg: String::from("Unable to load internal state"),
220 }),
221 }
222}
223
224pub fn query_tokens(deps: Deps, token_id: u64) -> StdResult<TokenInfo> {
225 let res = TOKENS.may_load(deps.storage, token_id)?;
226 match res {
227 Some(val) => Ok(val),
228 None => Err(StdError::NotFound {
229 kind: format!("Unable to load token with token_id: {}", token_id),
230 }),
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::contract::{execute, instantiate};
238 use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg};
239 use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
240 use cosmwasm_std::{coins, from_binary, Coin, Uint128};
241
242 const DENOM: &str = "ubit";
243
244 fn init_msg(name: String, symbol: String) -> InstantiateMsg {
245 InstantiateMsg { name, symbol }
246 }
247
248 fn mint_msg(owner: String) -> MintMsg {
249 MintMsg {
250 owner,
251 token_uri: None,
252 price: coins(1000, &DENOM.to_string()),
253 }
254 }
255
256 #[test]
257 fn asking_price() {
258 let mut deps = mock_dependencies();
259 let env = mock_env();
260 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
261 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
262 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
263 assert_eq!(res.messages.len(), 0);
264
265 let mint_msg = mint_msg("creator".to_string());
266 let msg = ExecuteMsg::Mint(mint_msg);
267 let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
268 assert_eq!(0, res.messages.len());
269 assert_eq!(4, res.attributes.len());
270
271 let query_msg = QueryMsg::AskingPrice { token_id: 1 };
273 let res = query(deps.as_ref(), env.clone(), query_msg).unwrap();
274 let res: AskingPriceResponse = from_binary(&res).unwrap();
275 assert_eq!(
276 res.price,
277 vec![Coin {
278 amount: Uint128::from(1000u64),
279 denom: DENOM.to_string()
280 }]
281 );
282
283 let query_msg = QueryMsg::AskingPrice { token_id: 2 };
285 let res = query(deps.as_ref(), env, query_msg).unwrap_err();
286 match res {
287 StdError::NotFound { .. } => {}
288 e => panic!("{:?}", e),
289 };
290 }
291
292 #[test]
293 fn owner_of() {
294 let mut deps = mock_dependencies();
295 let env = mock_env();
296 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
297 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
298 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
299 assert_eq!(res.messages.len(), 0);
300
301 let mint_msg = mint_msg("creator".to_string());
302 let msg = ExecuteMsg::Mint(mint_msg);
303 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
304
305 let res = query_owner_of(deps.as_ref(), env.clone(), 1u64, None).unwrap();
307 assert_eq!(res.owner, "creator");
308 assert_eq!(res.approvals, vec![]);
309
310 let res = query_owner_of(deps.as_ref(), env.clone(), 2u64, Some(true)).unwrap_err();
312 match res {
313 StdError::NotFound { .. } => {}
314 e => panic!("{:?}", e),
315 };
316 }
317
318 #[test]
319 fn num_tokens() {
320 let mut deps = mock_dependencies();
321 let env = mock_env();
322 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
323 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
324 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
325 assert_eq!(res.messages.len(), 0);
326
327 let res = query_num_tokens(deps.as_ref(), env.clone()).unwrap();
329 assert_eq!(res.tokens, 0);
330
331 let mint_msg = mint_msg("creator".to_string());
332 let msg = ExecuteMsg::Mint(mint_msg);
333 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
334
335 let res = query_num_tokens(deps.as_ref(), env.clone()).unwrap();
337 assert_eq!(res.tokens, 1);
338 }
339
340 #[test]
341 fn nft_info() {
342 let mut deps = mock_dependencies();
343 let env = mock_env();
344 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
345 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
346 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
347 assert_eq!(res.messages.len(), 0);
348
349 let mint_msg = mint_msg("creator".to_string());
350 let msg = ExecuteMsg::Mint(mint_msg);
351 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
352
353 let msg = QueryMsg::NftInfo { token_id: 1u64 };
355 let res = query(deps.as_ref(), env.clone(), msg).unwrap();
356 let result: NftInfoResponse = from_binary(&res).unwrap();
357 assert_eq!(result.token_uri, String::from("None"));
358
359 let msg = QueryMsg::NftInfo { token_id: 2u64 };
361 let res = query(deps.as_ref(), env.clone(), msg).unwrap_err();
362 match res {
363 StdError::NotFound { .. } => {}
364 e => panic!("{:?}", e),
365 };
366 }
367
368 #[test]
369 fn all_nft_info() {
370 let mut deps = mock_dependencies();
371 let env = mock_env();
372 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
373 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
374 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
375 assert_eq!(res.messages.len(), 0);
376
377 let msg = ExecuteMsg::Mint(mint_msg(String::from("owner")));
378 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
379
380 let msg = QueryMsg::AllNftInfo {
382 token_id: 1u64,
383 include_expired: Some(true),
384 };
385 let res = query(deps.as_ref(), env.clone(), msg).unwrap();
386 let result: AllNftInfoResponse = from_binary(&res).unwrap();
387 assert_eq!(
388 result.owner,
389 OwnerOfResponse {
390 owner: String::from("owner"),
391 approvals: vec![],
392 }
393 );
394 assert_eq!(
395 result.info,
396 NftInfoResponse {
397 token_uri: String::from("None")
398 }
399 );
400 }
401
402 #[test]
403 fn contract_info() {
404 let mut deps = mock_dependencies();
405 let env = mock_env();
406 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
407 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
408 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
409 assert_eq!(res.messages.len(), 0);
410
411 let msg = QueryMsg::ContractInfo {};
412 let res = query(deps.as_ref(), env.clone(), msg).unwrap();
413 let result: ContractInfoResponse = from_binary(&res).unwrap();
414 assert_eq!(result.name, String::from("TestNFT"));
415 assert_eq!(result.symbol, String::from("NFT"));
416 }
417
418 #[test]
419 fn approval() {
420 let mut deps = mock_dependencies();
421 let env = mock_env();
422 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
423 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
424 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
425 assert_eq!(res.messages.len(), 0);
426
427 let mint_msg = mint_msg("creator".to_string());
429 let msg = ExecuteMsg::Mint(mint_msg);
430 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
431
432 let msg = ExecuteMsg::Approve {
434 operator: "operator".to_string(),
435 token_id: 1,
436 expires: None,
437 };
438 let info = mock_info("creator", &coins(0u128, &DENOM.to_string()));
439 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
440
441 let res = query_approval(
443 deps.as_ref(),
444 env.clone(),
445 1,
446 String::from("operator"),
447 Some(false),
448 )
449 .unwrap();
450 assert_eq!(
451 res,
452 ApprovalResponse {
453 approval: Approval {
454 operator: Addr::unchecked("operator"),
455 expires: Expiration::Never {}
456 }
457 }
458 );
459
460 let res = query_approval(
463 deps.as_ref(),
464 env.clone(),
465 1u64,
466 String::from("unknown"),
467 None,
468 )
469 .unwrap_err();
470 match res {
471 StdError::NotFound { .. } => {}
472 e => panic!("{:?}", e),
473 };
474 }
475
476 #[test]
477 fn approvals() {
478 let mut deps = mock_dependencies();
479 let env = mock_env();
480 let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
481 let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
482 let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
483 assert_eq!(res.messages.len(), 0);
484
485 let mint_msg = mint_msg("creator".to_string());
487 let msg = ExecuteMsg::Mint(mint_msg);
488 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
489
490 let msg = ExecuteMsg::Approve {
492 operator: "operator".to_string(),
493 token_id: 1,
494 expires: None,
495 };
496 let info = mock_info("creator", &coins(0u128, &DENOM.to_string()));
497 execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
498
499 let res = query_approvals(deps.as_ref(), env.clone(), 1u64, None).unwrap();
500
501 assert_eq!(
502 res,
503 ApprovalsResponse {
504 approvals: vec![Approval {
505 operator: Addr::unchecked("operator"),
506 expires: Expiration::Never {}
507 }]
508 }
509 )
510 }
511}