cw721_basic/
execute.rs

1use serde::de::DeserializeOwned;
2use serde::Serialize;
3
4use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
5
6use cw2::set_contract_version;
7use cw721::{ContractInfoResponse, CustomMsg, Cw721Execute, Cw721ReceiveMsg, Expiration};
8
9use crate::error::ContractError;
10use crate::msg::{ExecuteMsg, InstantiateMsg, MintMsg};
11use crate::state::{Approval, Cw721Contract, TokenInfo};
12
13// version info for migration info
14const CONTRACT_NAME: &str = "crates.io:cw721-base";
15const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
16
17impl<'a, T, C> Cw721Contract<'a, T, C>
18where
19    T: Serialize + DeserializeOwned + Clone,
20    C: CustomMsg,
21{
22    pub fn instantiate(
23        &self,
24        deps: DepsMut,
25        _env: Env,
26        _info: MessageInfo,
27        msg: InstantiateMsg,
28    ) -> StdResult<Response<C>> {
29        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
30
31        let info = ContractInfoResponse {
32            name: msg.name,
33            symbol: msg.symbol,
34        };
35        self.contract_info.save(deps.storage, &info)?;
36        let minter = deps.api.addr_validate(&msg.minter)?;
37        self.minter.save(deps.storage, &minter)?;
38        Ok(Response::default())
39    }
40
41    pub fn execute(
42        &self,
43        deps: DepsMut,
44        env: Env,
45        info: MessageInfo,
46        msg: ExecuteMsg<T>,
47    ) -> Result<Response<C>, ContractError> {
48        match msg {
49            ExecuteMsg::Mint(msg) => self.mint(deps, env, info, msg),
50            ExecuteMsg::Approve {
51                spender,
52                token_id,
53                expires,
54            } => self.approve(deps, env, info, spender, token_id, expires),
55            ExecuteMsg::Revoke { spender, token_id } => {
56                self.revoke(deps, env, info, spender, token_id)
57            }
58            ExecuteMsg::ApproveAll { operator, expires } => {
59                self.approve_all(deps, env, info, operator, expires)
60            }
61            ExecuteMsg::RevokeAll { operator } => self.revoke_all(deps, env, info, operator),
62            ExecuteMsg::TransferNft {
63                recipient,
64                token_id,
65            } => self.transfer_nft(deps, env, info, recipient, token_id),
66            ExecuteMsg::SendNft {
67                contract,
68                token_id,
69                msg,
70            } => self.send_nft(deps, env, info, contract, token_id, msg),
71        }
72    }
73}
74
75// TODO pull this into some sort of trait extension??
76impl<'a, T, C> Cw721Contract<'a, T, C>
77where
78    T: Serialize + DeserializeOwned + Clone,
79    C: CustomMsg,
80{
81    pub fn mint(
82        &self,
83        deps: DepsMut,
84        _env: Env,
85        info: MessageInfo,
86        msg: MintMsg<T>,
87    ) -> Result<Response<C>, ContractError> {
88        let minter = self.minter.load(deps.storage)?;
89
90        if info.sender != minter {
91            return Err(ContractError::Unauthorized {});
92        }
93
94        // create the token
95        let token = TokenInfo {
96            owner: deps.api.addr_validate(&msg.owner)?,
97            approvals: vec![],
98            token_uri: msg.token_uri,
99            extension: msg.extension,
100        };
101        self.tokens
102            .update(deps.storage, &msg.token_id, |old| match old {
103                Some(_) => Err(ContractError::Claimed {}),
104                None => Ok(token),
105            })?;
106
107        self.increment_tokens(deps.storage)?;
108
109        Ok(Response::new()
110            .add_attribute("action", "mint")
111            .add_attribute("minter", info.sender)
112            .add_attribute("token_id", msg.token_id))
113    }
114}
115
116impl<'a, T, C> Cw721Execute<T, C> for Cw721Contract<'a, T, C>
117where
118    T: Serialize + DeserializeOwned + Clone,
119    C: CustomMsg,
120{
121    type Err = ContractError;
122
123    fn transfer_nft(
124        &self,
125        deps: DepsMut,
126        env: Env,
127        info: MessageInfo,
128        recipient: String,
129        token_id: String,
130    ) -> Result<Response<C>, ContractError> {
131        self._transfer_nft(deps, &env, &info, &recipient, &token_id)?;
132
133        Ok(Response::new()
134            .add_attribute("action", "transfer_nft")
135            .add_attribute("sender", info.sender)
136            .add_attribute("recipient", recipient)
137            .add_attribute("token_id", token_id))
138    }
139
140    fn send_nft(
141        &self,
142        deps: DepsMut,
143        env: Env,
144        info: MessageInfo,
145        contract: String,
146        token_id: String,
147        msg: Binary,
148    ) -> Result<Response<C>, ContractError> {
149        // Transfer token
150        self._transfer_nft(deps, &env, &info, &contract, &token_id)?;
151
152        let send = Cw721ReceiveMsg {
153            sender: info.sender.to_string(),
154            token_id: token_id.clone(),
155            msg,
156        };
157
158        // Send message
159        Ok(Response::new()
160            .add_message(send.into_cosmos_msg(contract.clone())?)
161            .add_attribute("action", "send_nft")
162            .add_attribute("sender", info.sender)
163            .add_attribute("recipient", contract)
164            .add_attribute("token_id", token_id))
165    }
166
167    fn approve(
168        &self,
169        deps: DepsMut,
170        env: Env,
171        info: MessageInfo,
172        spender: String,
173        token_id: String,
174        expires: Option<Expiration>,
175    ) -> Result<Response<C>, ContractError> {
176        self._update_approvals(deps, &env, &info, &spender, &token_id, true, expires)?;
177
178        Ok(Response::new()
179            .add_attribute("action", "approve")
180            .add_attribute("sender", info.sender)
181            .add_attribute("spender", spender)
182            .add_attribute("token_id", token_id))
183    }
184
185    fn revoke(
186        &self,
187        deps: DepsMut,
188        env: Env,
189        info: MessageInfo,
190        spender: String,
191        token_id: String,
192    ) -> Result<Response<C>, ContractError> {
193        self._update_approvals(deps, &env, &info, &spender, &token_id, false, None)?;
194
195        Ok(Response::new()
196            .add_attribute("action", "revoke")
197            .add_attribute("sender", info.sender)
198            .add_attribute("spender", spender)
199            .add_attribute("token_id", token_id))
200    }
201
202    fn approve_all(
203        &self,
204        deps: DepsMut,
205        env: Env,
206        info: MessageInfo,
207        operator: String,
208        expires: Option<Expiration>,
209    ) -> Result<Response<C>, ContractError> {
210        // reject expired data as invalid
211        let expires = expires.unwrap_or_default();
212        if expires.is_expired(&env.block) {
213            return Err(ContractError::Expired {});
214        }
215
216        // set the operator for us
217        let operator_addr = deps.api.addr_validate(&operator)?;
218        self.operators
219            .save(deps.storage, (&info.sender, &operator_addr), &expires)?;
220
221        Ok(Response::new()
222            .add_attribute("action", "approve_all")
223            .add_attribute("sender", info.sender)
224            .add_attribute("operator", operator))
225    }
226
227    fn revoke_all(
228        &self,
229        deps: DepsMut,
230        _env: Env,
231        info: MessageInfo,
232        operator: String,
233    ) -> Result<Response<C>, ContractError> {
234        let operator_addr = deps.api.addr_validate(&operator)?;
235        self.operators
236            .remove(deps.storage, (&info.sender, &operator_addr));
237
238        Ok(Response::new()
239            .add_attribute("action", "revoke_all")
240            .add_attribute("sender", info.sender)
241            .add_attribute("operator", operator))
242    }
243}
244
245// helpers
246impl<'a, T, C> Cw721Contract<'a, T, C>
247where
248    T: Serialize + DeserializeOwned + Clone,
249    C: CustomMsg,
250{
251    pub fn _transfer_nft(
252        &self,
253        deps: DepsMut,
254        env: &Env,
255        info: &MessageInfo,
256        recipient: &str,
257        token_id: &str,
258    ) -> Result<TokenInfo<T>, ContractError> {
259        let mut token = self.tokens.load(deps.storage, &token_id)?;
260        // ensure we have permissions
261        self.check_can_send(deps.as_ref(), env, info, &token)?;
262        // set owner and remove existing approvals
263        token.owner = deps.api.addr_validate(recipient)?;
264        token.approvals = vec![];
265        self.tokens.save(deps.storage, &token_id, &token)?;
266        Ok(token)
267    }
268
269    #[allow(clippy::too_many_arguments)]
270    pub fn _update_approvals(
271        &self,
272        deps: DepsMut,
273        env: &Env,
274        info: &MessageInfo,
275        spender: &str,
276        token_id: &str,
277        // if add == false, remove. if add == true, remove then set with this expiration
278        add: bool,
279        expires: Option<Expiration>,
280    ) -> Result<TokenInfo<T>, ContractError> {
281        let mut token = self.tokens.load(deps.storage, &token_id)?;
282        // ensure we have permissions
283        self.check_can_approve(deps.as_ref(), env, info, &token)?;
284
285        // update the approval list (remove any for the same spender before adding)
286        let spender_addr = deps.api.addr_validate(spender)?;
287        token.approvals = token
288            .approvals
289            .into_iter()
290            .filter(|apr| apr.spender != spender_addr)
291            .collect();
292
293        // only difference between approve and revoke
294        if add {
295            // reject expired data as invalid
296            let expires = expires.unwrap_or_default();
297            if expires.is_expired(&env.block) {
298                return Err(ContractError::Expired {});
299            }
300            let approval = Approval {
301                spender: spender_addr,
302                expires,
303            };
304            token.approvals.push(approval);
305        }
306
307        self.tokens.save(deps.storage, &token_id, &token)?;
308
309        Ok(token)
310    }
311
312    /// returns true iff the sender can execute approve or reject on the contract
313    pub fn check_can_approve(
314        &self,
315        deps: Deps,
316        env: &Env,
317        info: &MessageInfo,
318        token: &TokenInfo<T>,
319    ) -> Result<(), ContractError> {
320        // owner can approve
321        if token.owner == info.sender {
322            return Ok(());
323        }
324        // operator can approve
325        let op = self
326            .operators
327            .may_load(deps.storage, (&token.owner, &info.sender))?;
328        match op {
329            Some(ex) => {
330                if ex.is_expired(&env.block) {
331                    Err(ContractError::Unauthorized {})
332                } else {
333                    Ok(())
334                }
335            }
336            None => Err(ContractError::Unauthorized {}),
337        }
338    }
339
340    /// returns true iff the sender can transfer ownership of the token
341    fn check_can_send(
342        &self,
343        deps: Deps,
344        env: &Env,
345        info: &MessageInfo,
346        token: &TokenInfo<T>,
347    ) -> Result<(), ContractError> {
348        // owner can send
349        if token.owner == info.sender {
350            return Ok(());
351        }
352
353        // any non-expired token approval can send
354        if token
355            .approvals
356            .iter()
357            .any(|apr| apr.spender == info.sender && !apr.is_expired(&env.block))
358        {
359            return Ok(());
360        }
361
362        // operator can send
363        let op = self
364            .operators
365            .may_load(deps.storage, (&token.owner, &info.sender))?;
366        match op {
367            Some(ex) => {
368                if ex.is_expired(&env.block) {
369                    Err(ContractError::Unauthorized {})
370                } else {
371                    Ok(())
372                }
373            }
374            None => Err(ContractError::Unauthorized {}),
375        }
376    }
377}