use serde::de::DeserializeOwned; use serde::Serialize; use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; use cw721::{ContractInfoResponse, CustomMsg, Cw721Execute, Cw721ReceiveMsg, Expiration}; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg}; use crate::state::{Approval, Cw721Contract, TokenInfo}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw721-base"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); impl<'a, T, C> Cw721Contract<'a, T, C> where T: Serialize + DeserializeOwned + Clone, C: CustomMsg, { pub fn instantiate( &self, deps: DepsMut, _env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> StdResult> { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; let info = ContractInfoResponse { name: msg.name, symbol: msg.symbol, }; self.contract_info.save(deps.storage, &info)?; let minter = deps.api.addr_validate(&msg.minter)?; self.minter.save(deps.storage, &minter)?; Ok(Response::default()) } pub fn execute( &self, deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result, ContractError> { match msg { ExecuteMsg::Mint(msg) => self.mint(deps, env, info, msg), ExecuteMsg::Approve { spender, token_id, expires, } => self.approve(deps, env, info, spender, token_id, expires), ExecuteMsg::Revoke { spender, token_id } => { self.revoke(deps, env, info, spender, token_id) } ExecuteMsg::ApproveAll { operator, expires } => { self.approve_all(deps, env, info, operator, expires) } ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator), ExecuteMsg::TransferNft { recipient, token_id, } => self.transfer_nft(deps, env, info, recipient, token_id), ExecuteMsg::SendNft { contract, token_id, msg, } => self.send_nft(deps, env, info, contract, token_id, msg), ExecuteMsg::Burn { token_id } => self.burn(deps, env, info, token_id), } } } // TODO pull this into some sort of trait extension?? impl<'a, T, C> Cw721Contract<'a, T, C> where T: Serialize + DeserializeOwned + Clone, C: CustomMsg, { pub fn mint( &self, deps: DepsMut, _env: Env, info: MessageInfo, msg: MintMsg, ) -> Result, ContractError> { let minter = self.minter.load(deps.storage)?; if info.sender != minter { return Err(ContractError::Unauthorized {}); } // create the token let token = TokenInfo { owner: deps.api.addr_validate(&msg.owner)?, approvals: vec![], token_uri: msg.token_uri, extension: msg.extension, }; self.tokens .update(deps.storage, &msg.token_id, |old| match old { Some(_) => Err(ContractError::Claimed {}), None => Ok(token), })?; self.increment_tokens(deps.storage)?; Ok(Response::new() .add_attribute("action", "mint") .add_attribute("minter", info.sender) .add_attribute("token_id", msg.token_id)) } } impl<'a, T, C> Cw721Execute for Cw721Contract<'a, T, C> where T: Serialize + DeserializeOwned + Clone, C: CustomMsg, { type Err = ContractError; fn transfer_nft( &self, deps: DepsMut, env: Env, info: MessageInfo, recipient: String, token_id: String, ) -> Result, ContractError> { self._transfer_nft(deps, &env, &info, &recipient, &token_id)?; Ok(Response::new() .add_attribute("action", "transfer_nft") .add_attribute("sender", info.sender) .add_attribute("recipient", recipient) .add_attribute("token_id", token_id)) } fn send_nft( &self, deps: DepsMut, env: Env, info: MessageInfo, contract: String, token_id: String, msg: Binary, ) -> Result, ContractError> { // Transfer token self._transfer_nft(deps, &env, &info, &contract, &token_id)?; let send = Cw721ReceiveMsg { sender: info.sender.to_string(), token_id: token_id.clone(), msg, }; // Send message Ok(Response::new() .add_message(send.into_cosmos_msg(contract.clone())?) .add_attribute("action", "send_nft") .add_attribute("sender", info.sender) .add_attribute("recipient", contract) .add_attribute("token_id", token_id)) } fn approve( &self, deps: DepsMut, env: Env, info: MessageInfo, spender: String, token_id: String, expires: Option, ) -> Result, ContractError> { self._update_approvals(deps, &env, &info, &spender, &token_id, true, expires)?; Ok(Response::new() .add_attribute("action", "approve") .add_attribute("sender", info.sender) .add_attribute("spender", spender) .add_attribute("token_id", token_id)) } fn revoke( &self, deps: DepsMut, env: Env, info: MessageInfo, spender: String, token_id: String, ) -> Result, ContractError> { self._update_approvals(deps, &env, &info, &spender, &token_id, false, None)?; Ok(Response::new() .add_attribute("action", "revoke") .add_attribute("sender", info.sender) .add_attribute("spender", spender) .add_attribute("token_id", token_id)) } fn approve_all( &self, deps: DepsMut, env: Env, info: MessageInfo, operator: String, expires: Option, ) -> Result, ContractError> { // reject expired data as invalid let expires = expires.unwrap_or_default(); if expires.is_expired(&env.block) { return Err(ContractError::Expired {}); } // set the operator for us let operator_addr = deps.api.addr_validate(&operator)?; self.operators .save(deps.storage, (&info.sender, &operator_addr), &expires)?; Ok(Response::new() .add_attribute("action", "approve_all") .add_attribute("sender", info.sender) .add_attribute("operator", operator)) } fn revoke_all( &self, deps: DepsMut, _env: Env, info: MessageInfo, operator: String, ) -> Result, ContractError> { let operator_addr = deps.api.addr_validate(&operator)?; self.operators .remove(deps.storage, (&info.sender, &operator_addr)); Ok(Response::new() .add_attribute("action", "revoke_all") .add_attribute("sender", info.sender) .add_attribute("operator", operator)) } fn burn( &self, deps: DepsMut, env: Env, info: MessageInfo, token_id: String, ) -> Result, ContractError> { let token = self.tokens.load(deps.storage, &token_id)?; self.check_can_send(deps.as_ref(), &env, &info, &token)?; self.tokens.remove(deps.storage, &token_id)?; self.decrement_tokens(deps.storage)?; Ok(Response::new() .add_attribute("action", "burn") .add_attribute("sender", info.sender) .add_attribute("token_id", token_id)) } } // helpers impl<'a, T, C> Cw721Contract<'a, T, C> where T: Serialize + DeserializeOwned + Clone, C: CustomMsg, { pub fn _transfer_nft( &self, deps: DepsMut, env: &Env, info: &MessageInfo, recipient: &str, token_id: &str, ) -> Result, ContractError> { let mut token = self.tokens.load(deps.storage, token_id)?; // ensure we have permissions self.check_can_send(deps.as_ref(), env, info, &token)?; // set owner and remove existing approvals token.owner = deps.api.addr_validate(recipient)?; token.approvals = vec![]; self.tokens.save(deps.storage, token_id, &token)?; Ok(token) } #[allow(clippy::too_many_arguments)] pub fn _update_approvals( &self, deps: DepsMut, env: &Env, info: &MessageInfo, spender: &str, token_id: &str, // if add == false, remove. if add == true, remove then set with this expiration add: bool, expires: Option, ) -> Result, ContractError> { let mut token = self.tokens.load(deps.storage, token_id)?; // ensure we have permissions self.check_can_approve(deps.as_ref(), env, info, &token)?; // update the approval list (remove any for the same spender before adding) let spender_addr = deps.api.addr_validate(spender)?; token.approvals.retain(|apr| apr.spender != spender_addr); // only difference between approve and revoke if add { // reject expired data as invalid let expires = expires.unwrap_or_default(); if expires.is_expired(&env.block) { return Err(ContractError::Expired {}); } let approval = Approval { spender: spender_addr, expires, }; token.approvals.push(approval); } self.tokens.save(deps.storage, token_id, &token)?; Ok(token) } /// returns true iff the sender can execute approve or reject on the contract pub fn check_can_approve( &self, deps: Deps, env: &Env, info: &MessageInfo, token: &TokenInfo, ) -> Result<(), ContractError> { // owner can approve if token.owner == info.sender { return Ok(()); } // operator can approve let op = self .operators .may_load(deps.storage, (&token.owner, &info.sender))?; match op { Some(ex) => { if ex.is_expired(&env.block) { Err(ContractError::Unauthorized {}) } else { Ok(()) } } None => Err(ContractError::Unauthorized {}), } } /// returns true iff the sender can transfer ownership of the token pub fn check_can_send( &self, deps: Deps, env: &Env, info: &MessageInfo, token: &TokenInfo, ) -> Result<(), ContractError> { // owner can send if token.owner == info.sender { return Ok(()); } // any non-expired token approval can send if token .approvals .iter() .any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block)) { return Ok(()); } // operator can send let op = self .operators .may_load(deps.storage, (&token.owner, &info.sender))?; match op { Some(ex) => { if ex.is_expired(&env.block) { Err(ContractError::Unauthorized {}) } else { Ok(()) } } None => Err(ContractError::Unauthorized {}), } } }