bs721_base/
execute.rs

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