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