simple_nft/
query.rs

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        // Successful query
272        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        // Unsuccessful query
284        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        // Successful response
306        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        // Unsuccessful response
311        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        // Query # of tokens after initialization
328        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        // Query # of tokens after minting
336        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        // Successful query
354        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        // Unsuccessful query
360        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        // Successful query
381        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        // Mint a new token
428        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        // Approve operator
433        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        // Successful query
442        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        // Unsuccessful query
461        // * operator not approved
462        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        // Mint a new token
486        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        // Approve operator
491        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}