simple_nft/
contract.rs

1//! This module implements `instantiate`, `execute` and `query`.
2//! These actions are performed using *wasmd*.
3
4// #[cfg(not(feature = "library"))]
5use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response};
6
7use cw2::set_contract_version;
8use cw721::{Cw721ReceiveMsg, Expiration};
9
10use crate::query::{query_config, query_tokens};
11use crate::state::{State, TokenInfo, CONFIG, OPERATORS, TOKENS};
12use crate::{
13    msg::{Approval, ExecuteMsg, InstantiateMsg, MintMsg},
14    ContractError,
15};
16
17// version info for migration info
18const CONTRACT_NAME: &str = "crates.io:simple-nft";
19const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
20
21/// Initialise a new instance of this contract.
22#[cfg_attr(not(feature = "library"), entry_point)]
23pub fn instantiate(
24    deps: DepsMut,
25    _env: Env,
26    info: MessageInfo,
27    msg: InstantiateMsg,
28) -> Result<Response, ContractError> {
29    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
30    if msg.name.is_empty() {
31        return Err(ContractError::CustomError {
32            val: String::from("length of `name` should be greater than 1"),
33        });
34    }
35
36    if msg.symbol.is_empty() {
37        return Err(ContractError::CustomError {
38            val: String::from("length of `symbol` should be greater than 1"),
39        });
40    }
41    // sender will be the minter for the time being
42    let minter = deps.api.addr_validate(info.sender.as_str())?;
43
44    // Configure the state for storing
45    let config = State {
46        name: msg.name,
47        symbol: msg.symbol,
48        minter,
49        num_tokens: 0u64,
50    };
51    // Store
52    CONFIG.save(deps.storage, &config)?;
53    // Return an Ok() response as everything went well
54    Ok(Response::default())
55}
56
57#[cfg_attr(not(feature = "library"), entry_point)]
58pub fn execute(
59    deps: DepsMut,
60    env: Env,
61    info: MessageInfo,
62    msg: ExecuteMsg,
63) -> Result<Response, ContractError> {
64    match msg {
65        ExecuteMsg::TransferNft {
66            recipient,
67            token_id,
68        } => handle_transfer_nft(deps, env, info, recipient, token_id),
69
70        ExecuteMsg::SendNft {
71            contract,
72            token_id,
73            msg,
74        } => handle_send_nft(deps, env, info, contract, token_id, msg),
75
76        ExecuteMsg::Approve {
77            operator,
78            token_id,
79            expires,
80        } => handle_approve(deps, env, info, operator.as_str(), token_id, expires),
81
82        ExecuteMsg::ApproveAll { operator, expires } => {
83            handle_approve_all(deps, env, info, operator, expires)
84        }
85
86        ExecuteMsg::Revoke { operator, token_id } => {
87            handle_revoke(deps, env, info, operator, token_id)
88        }
89
90        ExecuteMsg::RevokeAll { operator } => handle_revoke_all(deps, env, info, operator),
91
92        ExecuteMsg::Mint(msg) => handle_mint(deps, env, info, msg),
93    }
94}
95
96pub fn authorized_to_send(
97    deps: Deps,
98    env: &Env,
99    info: &MessageInfo,
100    token_id: u64,
101) -> Result<(), ContractError> {
102    let token = query_tokens(deps, token_id)?;
103    if token.owner == info.sender {
104        return Ok(());
105    };
106
107    let token_appr = token
108        .approvals
109        .iter()
110        .any(|val| val.operator == info.sender && !val.expires.is_expired(&env.block));
111
112    if token_appr {
113        return Ok(());
114    }
115
116    let super_appr = OPERATORS.may_load(deps.storage, (&token.owner, &info.sender))?;
117    if let Some(val) = super_appr {
118        if !val.is_expired(&env.block) {
119            return Ok(());
120        }
121    };
122    Err(ContractError::Unauthorized)
123}
124
125fn authorized_to_approve(
126    deps: Deps,
127    env: &Env,
128    info: &MessageInfo,
129    token_id: u64,
130) -> Result<(), ContractError> {
131    let token = query_tokens(deps, token_id)?;
132    // Either token owner can approve
133    if token.owner == info.sender {
134        return Ok(());
135    };
136
137    // Or operator(with approve all) can approve
138    let super_appr = OPERATORS.may_load(deps.storage, (&token.owner, &info.sender))?;
139
140    if let Some(val) = super_appr {
141        if !val.is_expired(&env.block) {
142            return Ok(());
143        };
144    };
145    Err(ContractError::Unauthorized)
146}
147
148pub fn handle_transfer_nft(
149    deps: DepsMut,
150    env: Env,
151    info: MessageInfo,
152    recipient: String,
153    token_id: u64,
154) -> Result<Response, ContractError> {
155    let mut requested_token = TOKENS.load(deps.storage, token_id)?;
156
157    authorized_to_send(deps.as_ref(), &env, &info, token_id)?;
158
159    requested_token.owner = deps.api.addr_validate(&recipient)?;
160    requested_token.approvals = vec![];
161
162    TOKENS.save(deps.storage, token_id, &requested_token)?;
163
164    Ok(Response::new()
165        .add_attribute("action", "transfer_nft")
166        .add_attribute("from", info.sender)
167        .add_attribute("to", recipient)
168        .add_attribute("token_id", token_id.to_string()))
169}
170
171fn handle_send_nft(
172    deps: DepsMut,
173    env: Env,
174    info: MessageInfo,
175    contract: String,
176    token_id: u64,
177    msg: Binary,
178) -> Result<Response, ContractError> {
179    handle_transfer_nft(deps, env, info.clone(), contract.clone(), token_id)?;
180
181    let msg = Cw721ReceiveMsg {
182        sender: info.sender.to_string(),
183        token_id: token_id.to_string(),
184        msg,
185    };
186
187    Ok(Response::new()
188        .add_message(msg.into_cosmos_msg(contract.clone())?)
189        .add_attribute("action", "send_nft")
190        .add_attribute("from", info.sender)
191        .add_attribute("to", contract)
192        .add_attribute("token_id", token_id.to_string()))
193}
194
195pub fn handle_approve(
196    deps: DepsMut,
197    env: Env,
198    info: MessageInfo,
199    operator: &str,
200    token_id: u64,
201    expires: Option<Expiration>,
202) -> Result<Response, ContractError> {
203    // Load the token with given token id
204    let mut token = query_tokens(deps.as_ref(), token_id)?;
205
206    authorized_to_approve(deps.as_ref(), &env, &info, token_id)?;
207
208    let appr = Approval {
209        operator: deps.api.addr_validate(operator)?,
210        expires: match expires {
211            Some(val) => val,
212            None => Expiration::Never {},
213        },
214    };
215
216    if appr.expires.is_expired(&env.block) {
217        return Err(ContractError::Expired);
218    }
219    // Apply approval to the token
220    token.approvals.push(appr);
221
222    TOKENS.save(deps.storage, token_id, &token)?;
223
224    Ok(Response::new()
225        .add_attribute("action", "approve")
226        .add_attribute("from", info.sender)
227        .add_attribute("approved", operator)
228        .add_attribute("token_id", token_id.to_string()))
229}
230
231fn handle_approve_all(
232    deps: DepsMut,
233    env: Env,
234    info: MessageInfo,
235    operator: String,
236    expires: Option<Expiration>,
237) -> Result<Response, ContractError> {
238    let operator_addr = deps.api.addr_validate(&operator[..])?;
239    let expires = match expires {
240        Some(val) => val,
241        None => Expiration::Never {},
242    };
243
244    let appr = OPERATORS.may_load(deps.storage, (&info.sender, &operator_addr))?;
245    if let Some(val) = appr {
246        if val == expires {
247            return Err(ContractError::OperatorApproved { operator });
248        }
249    }
250
251    if expires.is_expired(&env.block) {
252        return Err(ContractError::Expired);
253    }
254
255    // Save the new/updated details
256    OPERATORS.save(deps.storage, (&info.sender, &operator_addr), &expires)?;
257
258    Ok(Response::new()
259        .add_attribute("action", "approve_all")
260        .add_attribute("from", info.sender)
261        .add_attribute("approved", operator))
262}
263
264pub fn handle_revoke(
265    deps: DepsMut,
266    env: Env,
267    info: MessageInfo,
268    operator: String,
269    token_id: u64,
270) -> Result<Response, ContractError> {
271    let mut token = query_tokens(deps.as_ref(), token_id)?;
272
273    authorized_to_approve(deps.as_ref(), &env, &info, token_id)?;
274
275    let operator_addr = deps.api.addr_validate(operator.as_str())?;
276    let revoked: Vec<Approval> = token
277        .approvals
278        .into_iter()
279        .filter(|val| val.operator != operator_addr)
280        .collect();
281
282    token.approvals = revoked;
283    TOKENS.save(deps.storage, token_id, &token)?;
284
285    Ok(Response::new()
286        .add_attribute("action", "revoke")
287        .add_attribute("from", info.sender)
288        .add_attribute("revoked", operator)
289        .add_attribute("token_id", token_id.to_string()))
290}
291
292pub fn handle_revoke_all(
293    deps: DepsMut,
294    _env: Env,
295    info: MessageInfo,
296    operator: String,
297) -> Result<Response, ContractError> {
298    let operator_addr = deps.api.addr_validate(operator.as_str())?;
299
300    if OPERATORS.has(deps.storage, (&info.sender, &operator_addr)) {
301        OPERATORS.remove(deps.storage, (&info.sender, &operator_addr));
302    } else {
303        return Err(ContractError::ApprovalNotFound { operator });
304    }
305
306    Ok(Response::new()
307        .add_attribute("action", "revoke_all")
308        .add_attribute("from", info.sender)
309        .add_attribute("revoked", operator))
310}
311
312pub fn handle_mint(
313    deps: DepsMut,
314    _env: Env,
315    info: MessageInfo,
316    msg: MintMsg,
317) -> Result<Response, ContractError> {
318    // Load current contract state
319    let mut config = query_config(deps.as_ref())?;
320
321    // sender and minter address should be same
322    if info.sender != config.minter {
323        return Err(ContractError::Unauthorized);
324    }
325
326    // price of the new NFT cannot be zero
327    if msg.price.is_empty() {
328        return Err(ContractError::CustomError {
329            val: String::from("Token price cannot be empty"),
330        });
331    } else {
332        // let res: Vec<&Coin> = msg
333        //     .price
334        //     .iter()
335        //     .filter(|val| val.amount.is_zero())
336        //     .collect();
337        for val in msg.price.iter() {
338            if val.amount.is_zero() {
339                return Err(ContractError::CustomError {
340                    val: String::from("Token price cannot be zero"),
341                });
342            }
343        }
344    }
345
346    // Increase the current amount of tokens issued
347    let num_tokens = config.num_tokens + 1;
348    // Create a new token
349    let token = TokenInfo {
350        owner: deps.api.addr_validate(&msg.owner)?,
351        approvals: vec![],
352        token_uri: msg.token_uri,
353        base_price: msg.price,
354        token_id: num_tokens,
355    };
356    // Save the new token to storage
357    TOKENS.save(deps.storage, num_tokens, &token)?;
358
359    // Increase the number of tokens issued in state
360    config.num_tokens = num_tokens;
361    CONFIG.save(deps.storage, &config)?;
362
363    Ok(Response::new()
364        .add_attribute("action", "mint")
365        .add_attribute("from", info.sender)
366        .add_attribute("owner", msg.owner)
367        .add_attribute("token_id", num_tokens.to_string()))
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
374    use cosmwasm_std::{coins, Addr, StdError};
375    use cw721::Expiration;
376
377    const DENOM: &str = "ubit";
378
379    fn init_msg(name: String, symbol: String) -> InstantiateMsg {
380        InstantiateMsg { name, symbol }
381    }
382
383    fn mint_msg(owner: String) -> MintMsg {
384        MintMsg {
385            owner,
386            token_uri: None,
387            price: coins(1000, &DENOM.to_string()),
388        }
389    }
390
391    #[test]
392    fn proper_initialization() {
393        // Create mock dependencies and environment
394        let mut deps = mock_dependencies();
395        let env = mock_env();
396        let info = mock_info("creator", &coins(0, &DENOM.to_string()));
397
398        // Successful instantiation
399        let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
400        let res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
401        assert_eq!(0, res.messages.len());
402
403        let stored_state = query_config(deps.as_ref()).unwrap();
404        assert_eq!(stored_state.name, "TestNFT");
405        assert_eq!(stored_state.symbol, "NFT");
406        assert_eq!(stored_state.num_tokens, 0u64);
407        assert_eq!(stored_state.minter, Addr::unchecked("creator"));
408
409        // Following tests are to check correct error when no value is given
410        // to either of the fields in InstantiateMsg.
411        // * Missing name
412        let msg = init_msg(String::new(), "NFT".to_string());
413        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err();
414
415        match res {
416            ContractError::CustomError { .. } => {}
417            e => panic!("{:?}", e),
418        }
419
420        // * Missing symbol
421        let msg = init_msg(String::from("TestNFT"), String::new());
422        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err();
423
424        match res {
425            ContractError::CustomError { .. } => {}
426            e => panic!("{:?}", e),
427        }
428
429        // * Missing name and symbol
430        let msg = init_msg(String::from(""), String::from(""));
431        let res = instantiate(deps.as_mut(), env, info, msg).unwrap_err();
432
433        match res {
434            ContractError::CustomError { .. } => {}
435            e => panic!("{:?}", e),
436        }
437    }
438
439    #[test]
440    fn mint() {
441        let mut deps = mock_dependencies();
442        let env = mock_env();
443
444        let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
445        let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
446        instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
447
448        // Successful token minting
449        let mint_msg = mint_msg("creator".to_string());
450        let res = handle_mint(deps.as_mut(), env.clone(), info.clone(), mint_msg).unwrap();
451        assert_eq!(0, res.messages.len());
452        assert_eq!(4, res.attributes.len());
453
454        let stored_token = query_tokens(deps.as_ref(), 1).unwrap();
455        assert_eq!(stored_token.owner, "creator");
456        assert_eq!(stored_token.token_id, 1);
457        assert_eq!(stored_token.base_price, coins(1000, DENOM.to_string()));
458        assert_eq!(stored_token.approvals, vec![]);
459        assert_eq!(stored_token.token_uri, None);
460
461        // Unsuccessful token minting
462        // * owner name is empty
463        let msg = MintMsg {
464            owner: String::new(),
465            token_uri: None,
466            price: coins(1000, DENOM.to_string()),
467        };
468
469        let res = handle_mint(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err();
470        match res {
471            ContractError::Std(StdError::GenericErr { .. }) => {}
472            e => panic!("{:?}", e),
473        };
474
475        // * amount is empty i.e., price has been provided as 0Denom
476        let msg = MintMsg {
477            owner: String::from("owner"),
478            token_uri: None,
479            price: coins(0, DENOM.to_string()),
480        };
481
482        let res = handle_mint(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err();
483        match res {
484            ContractError::CustomError { .. } => {}
485            e => panic! {"{:?}", e},
486        };
487
488        // * price is empty
489        let msg = MintMsg {
490            owner: String::from("owner"),
491            token_uri: None,
492            price: vec![],
493        };
494
495        let res = handle_mint(deps.as_mut(), env.clone(), info.clone(), msg).unwrap_err();
496        match res {
497            ContractError::CustomError { .. } => {}
498            e => panic! {"{:?}", e},
499        };
500    }
501
502    #[test]
503    fn approve() {
504        let mut deps = mock_dependencies();
505        let mut env = mock_env();
506        env.block.height = 50u64;
507        let info = mock_info("creator", &coins(0, &DENOM.to_string()));
508
509        let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
510        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
511        assert_eq!(0, res.messages.len());
512
513        let mint_msg = mint_msg("owner1".to_string());
514        handle_mint(deps.as_mut(), env.clone(), info, mint_msg).unwrap();
515
516        // Successful approval request
517        // * by owner
518        let approve_msg = ExecuteMsg::Approve {
519            operator: "operator".to_string(),
520            token_id: 1u64,
521            expires: None,
522        };
523
524        let info: MessageInfo = mock_info("owner1", &coins(0, &DENOM.to_string()));
525        let res = execute(deps.as_mut(), env.clone(), info.clone(), approve_msg).unwrap();
526        assert_eq!(res.messages.len(), 0);
527        assert_eq!(res.attributes.len(), 4);
528
529        let token = query_tokens(deps.as_ref(), 1u64).unwrap();
530        assert_eq!(token.approvals[0].operator, Addr::unchecked("operator"));
531        assert_eq!(token.approvals[0].expires, Expiration::Never {});
532
533        // * by operator
534
535        // Approve all for operator1
536        let res = handle_approve_all(
537            deps.as_mut(),
538            env.clone(),
539            info.clone(),
540            String::from("operator1"),
541            None,
542        )
543        .unwrap();
544        assert_eq!(res.messages.len(), 0);
545
546        // operator1 approves user1
547        let info = mock_info("operator1", &coins(0, &DENOM.to_string()));
548        let res = handle_approve(
549            deps.as_mut(),
550            env.clone(),
551            info.clone(),
552            "user1",
553            1u64,
554            None,
555        )
556        .unwrap();
557        assert_eq!(res.messages.len(), 0);
558        assert_eq!(res.attributes.len(), 4);
559        assert_eq!(res.attributes[1].value, String::from("operator1"));
560
561        let token = query_tokens(deps.as_ref(), 1u64).unwrap();
562        assert_eq!(token.approvals.len(), 2);
563        assert_eq!(token.approvals[1].operator, Addr::unchecked("user1"));
564
565        // Unsuccessful approval request
566        // * empty operator field
567        let approve_msg = ExecuteMsg::Approve {
568            operator: String::new(),
569            token_id: 1u64,
570            expires: None,
571        };
572        let info = mock_info("owner1", &coins(0, &DENOM.to_string()));
573        let res = execute(deps.as_mut(), env.clone(), info.clone(), approve_msg).unwrap_err();
574        match res {
575            ContractError::Std(StdError::GenericErr { .. }) => {}
576            e => panic!("{:?}", e),
577        };
578
579        // * Invalid token
580        let approve_msg = ExecuteMsg::Approve {
581            operator: "operator".to_string(),
582            token_id: 2u64,
583            expires: None,
584        };
585        let info = mock_info("owner1", &coins(0, &DENOM.to_string()));
586        execute(deps.as_mut(), env.clone(), info.clone(), approve_msg).unwrap_err();
587
588        // * expired approval
589        let approve_msg = ExecuteMsg::Approve {
590            operator: "operator".to_string(),
591            token_id: 1u64,
592            expires: Some(Expiration::AtHeight(45u64)),
593        };
594        let info = mock_info("owner1", &coins(0, &DENOM.to_string()));
595        let res = execute(deps.as_mut(), env.clone(), info.clone(), approve_msg).unwrap_err();
596        match res {
597            ContractError::Expired {} => {}
598            e => panic!("{:?}", e),
599        };
600    }
601
602    #[test]
603    fn approve_all() {
604        let mut deps = mock_dependencies();
605        let mut env = mock_env();
606        env.block.height = 25u64;
607        let info = mock_info("creator", &coins(0, &DENOM.to_string()));
608        let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
609        instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
610
611        // Successful approval for operator
612        // * new operator
613        let operator = String::from("operator");
614        let expires = None;
615
616        let res = handle_approve_all(deps.as_mut(), env.clone(), info.clone(), operator, expires)
617            .unwrap();
618        assert_eq!(res.messages.len(), 0);
619        assert_eq!(res.attributes.len(), 3);
620
621        let res = OPERATORS
622            .load(&deps.storage, (&info.sender, &Addr::unchecked("operator")))
623            .unwrap();
624        assert_eq!(res, Expiration::Never {});
625
626        // * update expiration
627        let operator = String::from("operator");
628        let expires = Some(Expiration::AtHeight(130_000));
629
630        let res = handle_approve_all(deps.as_mut(), env.clone(), info.clone(), operator, expires)
631            .unwrap();
632        assert_eq!(res.messages.len(), 0);
633        assert_eq!(res.attributes.len(), 3);
634
635        // Unsuccessful approval for operator
636        // * Operator exists
637        let operator = String::from("operator");
638        let expires = Some(Expiration::AtHeight(130_000));
639        let res = handle_approve_all(deps.as_mut(), env.clone(), info.clone(), operator, expires)
640            .unwrap_err();
641
642        match res {
643            ContractError::OperatorApproved { .. } => {}
644            e => panic!("{:?}", e),
645        };
646
647        // * Expired height
648        let operator = String::from("operator");
649        let expires = Some(Expiration::AtHeight(24u64));
650
651        let res = handle_approve_all(deps.as_mut(), env.clone(), info.clone(), operator, expires)
652            .unwrap_err();
653        match res {
654            ContractError::Expired => {}
655            e => panic!("{:?}", e),
656        };
657
658        // * Expired time
659        let operator = String::from("operator");
660        let expires = Some(Expiration::AtTime(env.block.time.minus_seconds(64u64)));
661
662        let res = handle_approve_all(deps.as_mut(), env.clone(), info.clone(), operator, expires)
663            .unwrap_err();
664        match res {
665            ContractError::Expired => {}
666            e => panic!("{:?}", e),
667        };
668    }
669
670    #[test]
671    fn revoke() {
672        let mut deps = mock_dependencies();
673        let env = mock_env();
674        let info = mock_info("creator", &coins(0, &DENOM.to_string()));
675
676        let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
677        let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
678        assert_eq!(0, res.messages.len());
679
680        // Mint a new token
681        let mint_msg = mint_msg("owner".to_string());
682        let _res = handle_mint(deps.as_mut(), env.clone(), info.clone(), mint_msg).unwrap();
683
684        // Approve operator1
685        let approve_msg = ExecuteMsg::Approve {
686            operator: "operator1".to_string(),
687            token_id: 1u64,
688            expires: None,
689        };
690        let info = mock_info("owner", &coins(0, &DENOM.to_string()));
691        execute(deps.as_mut(), env.clone(), info.clone(), approve_msg).unwrap();
692
693        // Approve operator2
694        handle_approve(
695            deps.as_mut(),
696            env.clone(),
697            info.clone(),
698            "operator2",
699            1u64,
700            None,
701        )
702        .unwrap();
703
704        let token = query_tokens(deps.as_ref(), 1u64).unwrap();
705        assert_eq!(token.approvals[0].operator, Addr::unchecked("operator1"));
706        assert_eq!(token.approvals[0].expires, Expiration::Never {});
707
708        // Successful approval revoke
709        // * by owner
710        let revoke_msg = ExecuteMsg::Revoke {
711            operator: "operator1".to_string(),
712            token_id: 1u64,
713        };
714
715        let res = execute(deps.as_mut(), env.clone(), info.clone(), revoke_msg).unwrap();
716        assert_eq!(res.messages.len(), 0);
717        assert_eq!(res.attributes.len(), 4);
718
719        let token = query_tokens(deps.as_ref(), 1u64).unwrap();
720        assert_eq!(token.approvals.len(), 1);
721
722        // * by operator
723        handle_approve_all(
724            deps.as_mut(),
725            env.clone(),
726            info.clone(),
727            String::from("operator"),
728            None,
729        )
730        .unwrap();
731
732        let info = mock_info("operator", &coins(0, &DENOM.to_string()));
733        handle_revoke(
734            deps.as_mut(),
735            env.clone(),
736            info.clone(),
737            String::from("operator2"),
738            1u64,
739        )
740        .unwrap();
741
742        let token = query_tokens(deps.as_ref(), 1u64).unwrap();
743        assert_eq!(token.approvals.len(), 0);
744
745        // Unsuccessful approval revoke
746        // * Invalid token id
747        let revoke_msg = ExecuteMsg::Revoke {
748            operator: "operator".to_string(),
749            token_id: 2u64,
750        };
751
752        let res = execute(deps.as_mut(), env.clone(), info.clone(), revoke_msg).unwrap_err();
753        match res {
754            ContractError::Std(StdError::NotFound { .. }) => {}
755            e => panic!("{:?}", e),
756        };
757
758        // * Approval being revoked for an address that isn't approved.
759        let revoke_msg = ExecuteMsg::Revoke {
760            operator: "unknown".to_string(),
761            token_id: 1u64,
762        };
763
764        let res = execute(deps.as_mut(), env.clone(), info.clone(), revoke_msg).unwrap();
765        assert_eq!(res.messages.len(), 0);
766        assert_eq!(res.attributes.len(), 4);
767
768        // * Unauthorised sender
769        let info = mock_info("owner2", &coins(0, &DENOM.to_string()));
770        let revoke_msg = ExecuteMsg::Revoke {
771            operator: "operator".to_string(),
772            token_id: 1u64,
773        };
774
775        let res = execute(deps.as_mut(), env.clone(), info.clone(), revoke_msg).unwrap_err();
776        match res {
777            ContractError::Unauthorized => {}
778            e => panic!("{:?}", e),
779        };
780    }
781
782    #[test]
783    fn revoke_all() {
784        let mut deps = mock_dependencies();
785        let env = mock_env();
786        let info = mock_info("owner", &coins(0u128, &DENOM.to_string()));
787
788        // Approve an address for all tokens
789        let res = handle_approve_all(
790            deps.as_mut(),
791            env.clone(),
792            info.clone(),
793            String::from("operator"),
794            None,
795        )
796        .unwrap();
797        assert_eq!(res.messages.len(), 0);
798        assert_eq!(res.attributes.len(), 3);
799
800        // Check that the new approval exists in correct format
801        let key = (&Addr::unchecked("owner"), &Addr::unchecked("operator"));
802        assert!(OPERATORS.has(&deps.storage, key));
803        assert_eq!(
804            OPERATORS.load(&deps.storage, key).unwrap(),
805            Expiration::Never {}
806        );
807
808        // Revoke previously provided approval
809        let res = handle_revoke_all(
810            deps.as_mut(),
811            env.clone(),
812            info.clone(),
813            String::from("operator"),
814        )
815        .unwrap();
816        assert_eq!(res.messages.len(), 0);
817        assert_eq!(res.attributes.len(), 3);
818        assert!(!OPERATORS.has(&deps.storage, key));
819
820        // Unsuccessful revoke
821        let res = handle_revoke_all(
822            deps.as_mut(),
823            env.clone(),
824            info.clone(),
825            String::from("operator"),
826        )
827        .unwrap_err();
828        match res {
829            ContractError::ApprovalNotFound { .. } => {}
830            e => panic!("{:?}", e),
831        };
832    }
833
834    #[test]
835    fn transfer_nft_by_owner() {
836        let mut deps = mock_dependencies();
837        let env = mock_env();
838
839        let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
840        let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
841        let _res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
842
843        let mint_msg = mint_msg("creator".to_string());
844
845        let msg = ExecuteMsg::Mint(mint_msg);
846        let res: Response = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
847        assert_eq!(0, res.messages.len());
848        assert_eq!(4, res.attributes.len());
849
850        let info = mock_info("creator", &coins(0u128, &DENOM.to_string()));
851        let msg = ExecuteMsg::TransferNft {
852            recipient: String::from("recipient"),
853            token_id: 1,
854        };
855        let res: Response = execute(deps.as_mut(), env, info, msg).unwrap();
856        assert_eq!(0, res.messages.len());
857        assert_eq!(4, res.attributes.len());
858    }
859
860    #[test]
861    fn transfer_nft_by_operator() {
862        // Setup the necessary environment
863        let mut deps = mock_dependencies();
864        let env = mock_env();
865
866        let info = mock_info("minter", &coins(0u128, &DENOM.to_string()));
867        let msg = init_msg("TestNFT".to_string(), "NFT".to_string());
868        let _res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
869
870        let mint_msg = mint_msg("creator".to_string());
871
872        let msg = ExecuteMsg::Mint(mint_msg);
873        let res: Response = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();
874        assert_eq!(0, res.messages.len());
875        assert_eq!(4, res.attributes.len());
876
877        // Add approval to the token
878        let mut token = query_tokens(deps.as_ref(), 1u64).unwrap();
879        token.approvals.push(Approval {
880            operator: Addr::unchecked("operator"),
881            expires: Expiration::Never {},
882        });
883        TOKENS.save(&mut deps.storage, 1u64, &token).unwrap();
884
885        // *operator* should now be capable of transferring the token
886        let info = mock_info("operator", &coins(0u128, &DENOM.to_string()));
887        let msg = ExecuteMsg::TransferNft {
888            recipient: String::from("recipient"),
889            token_id: 1,
890        };
891        let res: Response = execute(deps.as_mut(), env, info, msg).unwrap();
892        assert_eq!(0, res.messages.len());
893        assert_eq!(4, res.attributes.len());
894    }
895}