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
14const CONTRACT_NAME: &str = "crates.io:bs721-base";
16const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
17
18const MAX_SELLER_FEE: u16 = 10000; impl<'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
83impl<'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 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 if let Some(fee) = msg.seller_fee_bps {
113 if fee > MAX_SELLER_FEE {
114 return Err(ContractError::MaxSellerFeeExceeded {});
115 }
116 }
117
118 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 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 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 let expires = expires.unwrap_or_default();
241 if expires.is_expired(&env.block) {
242 return Err(ContractError::Expired {});
243 }
244
245 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
293impl<'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 self.check_can_send(deps.as_ref(), env, info, &token)?;
312 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 add: bool,
329 expires: Option<Expiration>,
330 ) -> Result<TokenInfo<T>, ContractError> {
331 let mut token = self.tokens.load(deps.storage, token_id)?;
332 self.check_can_approve(deps.as_ref(), env, info, &token)?;
334
335 let spender_addr = deps.api.addr_validate(spender)?;
337 token.approvals.retain(|apr| apr.spender != spender_addr);
338
339 if add {
341 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 pub fn check_can_approve(
360 &self,
361 deps: Deps,
362 env: &Env,
363 info: &MessageInfo,
364 token: &TokenInfo<T>,
365 ) -> Result<(), ContractError> {
366 if token.owner == info.sender {
368 return Ok(());
369 }
370 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 pub fn check_can_send(
388 &self,
389 deps: Deps,
390 env: &Env,
391 info: &MessageInfo,
392 token: &TokenInfo<T>,
393 ) -> Result<(), ContractError> {
394 if token.owner == info.sender {
396 return Ok(());
397 }
398
399 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 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}