1use 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
17const CONTRACT_NAME: &str = "crates.io:simple-nft";
19const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
20
21#[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 let minter = deps.api.addr_validate(info.sender.as_str())?;
43
44 let config = State {
46 name: msg.name,
47 symbol: msg.symbol,
48 minter,
49 num_tokens: 0u64,
50 };
51 CONFIG.save(deps.storage, &config)?;
53 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 if token.owner == info.sender {
134 return Ok(());
135 };
136
137 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 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 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 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 let mut config = query_config(deps.as_ref())?;
320
321 if info.sender != config.minter {
323 return Err(ContractError::Unauthorized);
324 }
325
326 if msg.price.is_empty() {
328 return Err(ContractError::CustomError {
329 val: String::from("Token price cannot be empty"),
330 });
331 } else {
332 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 let num_tokens = config.num_tokens + 1;
348 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 TOKENS.save(deps.storage, num_tokens, &token)?;
358
359 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 let mut deps = mock_dependencies();
395 let env = mock_env();
396 let info = mock_info("creator", &coins(0, &DENOM.to_string()));
397
398 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}