terp721_base/
contract.rs

1use cw721_base::state::TokenInfo;
2use url::Url;
3
4use cosmwasm_std::{
5    to_json_binary, Addr, Binary, ContractInfoResponse, Decimal, Deps, DepsMut, Empty, Env, Event,
6    MessageInfo, StdError, StdResult, Storage, Timestamp, WasmQuery,
7};
8
9use cw721::{ContractInfoResponse as CW721ContractInfoResponse, Cw721Execute};
10use cw_utils::nonpayable;
11use serde::{de::DeserializeOwned, Serialize};
12
13use terp721::{
14    CollectionInfo, ExecuteMsg, InstantiateMsg, ResidualInfo, ResidualInfoResponse,
15    UpdateCollectionInfoMsg,
16};
17use terp_sdk::Response;
18
19use crate::msg::{CollectionInfoResponse, NftParams, QueryMsg};
20use crate::{ContractError, Terp721Contract};
21
22use crate::entry::{CONTRACT_NAME, CONTRACT_VERSION};
23
24const MAX_DESCRIPTION_LENGTH: u32 = 512;
25const MAX_SHARE_DELTA_PCT: u64 = 2;
26const MAX_ROYALTY_SHARE_PCT: u64 = 10;
27
28impl<'a, T> Terp721Contract<'a, T>
29where
30    T: Serialize + DeserializeOwned + Clone,
31{
32    pub fn instantiate(
33        &self,
34        deps: DepsMut,
35        env: Env,
36        info: MessageInfo,
37        msg: InstantiateMsg,
38    ) -> Result<Response, ContractError> {
39        // no funds should be sent to this contract
40        nonpayable(&info)?;
41
42        // check sender is a contract
43        let req = WasmQuery::ContractInfo {
44            contract_addr: info.sender.into(),
45        }
46        .into();
47        let _res: ContractInfoResponse = deps
48            .querier
49            .query(&req)
50            .map_err(|_| ContractError::Unauthorized {})?;
51
52        // cw721 instantiation
53        let info = CW721ContractInfoResponse {
54            name: msg.name,
55            symbol: msg.symbol,
56        };
57        self.parent.contract_info.save(deps.storage, &info)?;
58        cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.minter))?;
59
60        // terp721 instantiation
61        if msg.collection_info.description.len() > MAX_DESCRIPTION_LENGTH as usize {
62            return Err(ContractError::DescriptionTooLong {});
63        }
64
65        let image = Url::parse(&msg.collection_info.image)?;
66
67        if let Some(ref external_link) = msg.collection_info.external_link {
68            Url::parse(external_link)?;
69        }
70
71        let residual_info: Option<ResidualInfo> = match msg.collection_info.residual_info {
72            Some(residual_info) => Some(ResidualInfo {
73                payment_address: deps.api.addr_validate(&residual_info.payment_address)?,
74                share: share_validate(residual_info.share)?,
75            }),
76            None => None,
77        };
78
79        deps.api.addr_validate(&msg.collection_info.creator)?;
80
81        let collection_info = CollectionInfo {
82            creator: msg.collection_info.creator,
83            description: msg.collection_info.description,
84            image: msg.collection_info.image,
85            external_link: msg.collection_info.external_link,
86            explicit_content: msg.collection_info.explicit_content,
87            start_trading_time: msg.collection_info.start_trading_time,
88            residual_info,
89        };
90
91        self.collection_info.save(deps.storage, &collection_info)?;
92
93        self.frozen_collection_info.save(deps.storage, &false)?;
94
95        self.royalty_updated_at
96            .save(deps.storage, &env.block.time)?;
97
98        Ok(Response::new()
99            .add_attribute("action", "instantiate")
100            .add_attribute("collection_name", info.name)
101            .add_attribute("collection_symbol", info.symbol)
102            .add_attribute("collection_creator", collection_info.creator)
103            .add_attribute("minter", msg.minter)
104            .add_attribute("image", image.to_string()))
105    }
106
107    pub fn execute(
108        &self,
109        deps: DepsMut,
110        env: Env,
111        info: MessageInfo,
112        msg: ExecuteMsg<T, Empty>,
113    ) -> Result<Response, ContractError> {
114        match msg {
115            ExecuteMsg::TransferNft {
116                recipient,
117                token_id,
118            } => self
119                .parent
120                .transfer_nft(deps, env, info, recipient, token_id)
121                .map_err(|e| e.into()),
122            ExecuteMsg::SendNft {
123                contract,
124                token_id,
125                msg,
126            } => self
127                .parent
128                .send_nft(deps, env, info, contract, token_id, msg)
129                .map_err(|e| e.into()),
130            ExecuteMsg::Approve {
131                spender,
132                token_id,
133                expires,
134            } => self
135                .parent
136                .approve(deps, env, info, spender, token_id, expires)
137                .map_err(|e| e.into()),
138            ExecuteMsg::Revoke { spender, token_id } => self
139                .parent
140                .revoke(deps, env, info, spender, token_id)
141                .map_err(|e| e.into()),
142            ExecuteMsg::ApproveAll { operator, expires } => self
143                .parent
144                .approve_all(deps, env, info, operator, expires)
145                .map_err(|e| e.into()),
146            ExecuteMsg::RevokeAll { operator } => self
147                .parent
148                .revoke_all(deps, env, info, operator)
149                .map_err(|e| e.into()),
150            ExecuteMsg::Burn { token_id } => self
151                .parent
152                .burn(deps, env, info, token_id)
153                .map_err(|e| e.into()),
154            ExecuteMsg::UpdateCollectionInfo { collection_info } => {
155                self.update_collection_info(deps, env, info, collection_info)
156            }
157            ExecuteMsg::UpdateStartTradingTime(start_time) => {
158                self.update_start_trading_time(deps, env, info, start_time)
159            }
160            ExecuteMsg::FreezeCollectionInfo {} => self.freeze_collection_info(deps, env, info),
161            ExecuteMsg::Mint {
162                token_id,
163                token_uri,
164                owner,
165                extension,
166            } => self.mint(
167                deps,
168                env,
169                info,
170                NftParams::NftData {
171                    token_id,
172                    owner,
173                    token_uri,
174                    extension,
175                },
176            ),
177            ExecuteMsg::Extension { msg: _ } => todo!(),
178            terp721::ExecuteMsg::UpdateOwnership(msg) => self
179                .parent
180                .execute(
181                    deps,
182                    env,
183                    info,
184                    cw721_base::ExecuteMsg::UpdateOwnership(msg),
185                )
186                .map_err(|e| ContractError::OwnershipUpdateError {
187                    error: e.to_string(),
188                }),
189        }
190    }
191
192    pub fn update_collection_info(
193        &self,
194        deps: DepsMut,
195        env: Env,
196        info: MessageInfo,
197        collection_msg: UpdateCollectionInfoMsg<ResidualInfoResponse>,
198    ) -> Result<Response, ContractError> {
199        let mut collection = self.collection_info.load(deps.storage)?;
200
201        if self.frozen_collection_info.load(deps.storage)? {
202            return Err(ContractError::CollectionInfoFrozen {});
203        }
204
205        // only creator can update collection info
206        if collection.creator != info.sender {
207            return Err(ContractError::Unauthorized {});
208        }
209
210        collection.description = collection_msg
211            .description
212            .unwrap_or_else(|| collection.description.to_string());
213        if collection.description.len() > MAX_DESCRIPTION_LENGTH as usize {
214            return Err(ContractError::DescriptionTooLong {});
215        }
216
217        collection.image = collection_msg
218            .image
219            .unwrap_or_else(|| collection.image.to_string());
220        Url::parse(&collection.image)?;
221
222        collection.external_link = collection_msg
223            .external_link
224            .unwrap_or_else(|| collection.external_link.as_ref().map(|s| s.to_string()));
225        if collection.external_link.as_ref().is_some() {
226            Url::parse(collection.external_link.as_ref().unwrap())?;
227        }
228
229        collection.explicit_content = collection_msg.explicit_content;
230
231        if let Some(Some(new_royalty_info_response)) = collection_msg.residual_info {
232            let last_royalty_update = self.royalty_updated_at.load(deps.storage)?;
233            if last_royalty_update.plus_seconds(24 * 60 * 60) > env.block.time {
234                return Err(ContractError::InvalidRoyalties(
235                    "Royalties can only be updated once per day".to_string(),
236                ));
237            }
238
239            let new_royalty_info = ResidualInfo {
240                payment_address: deps
241                    .api
242                    .addr_validate(&new_royalty_info_response.payment_address)?,
243                share: share_validate(new_royalty_info_response.share)?,
244            };
245
246            if let Some(old_royalty_info) = collection.residual_info {
247                if old_royalty_info.share < new_royalty_info.share {
248                    let share_delta = new_royalty_info.share.abs_diff(old_royalty_info.share);
249
250                    if share_delta > Decimal::percent(MAX_SHARE_DELTA_PCT) {
251                        return Err(ContractError::InvalidRoyalties(format!(
252                            "Share increase cannot be greater than {MAX_SHARE_DELTA_PCT}%"
253                        )));
254                    }
255                    if new_royalty_info.share > Decimal::percent(MAX_ROYALTY_SHARE_PCT) {
256                        return Err(ContractError::InvalidRoyalties(format!(
257                            "Share cannot be greater than {MAX_ROYALTY_SHARE_PCT}%"
258                        )));
259                    }
260                }
261            }
262
263            collection.residual_info = Some(new_royalty_info);
264            self.royalty_updated_at
265                .save(deps.storage, &env.block.time)?;
266        }
267
268        self.collection_info.save(deps.storage, &collection)?;
269
270        let event = Event::new("update_collection_info").add_attribute("sender", info.sender);
271        Ok(Response::new().add_event(event))
272    }
273
274    /// Called by the minter reply handler after custom validations on trading start time.
275    /// Minter has start_time, default offset, makes sense to execute from minter.
276    pub fn update_start_trading_time(
277        &self,
278        deps: DepsMut,
279        _env: Env,
280        info: MessageInfo,
281        start_time: Option<Timestamp>,
282    ) -> Result<Response, ContractError> {
283        assert_minter_owner(deps.storage, &info.sender)?;
284
285        let mut collection_info = self.collection_info.load(deps.storage)?;
286        collection_info.start_trading_time = start_time;
287        self.collection_info.save(deps.storage, &collection_info)?;
288
289        let event = Event::new("update_start_trading_time").add_attribute("sender", info.sender);
290        Ok(Response::new().add_event(event))
291    }
292
293    pub fn freeze_collection_info(
294        &self,
295        deps: DepsMut,
296        _env: Env,
297        info: MessageInfo,
298    ) -> Result<Response, ContractError> {
299        let collection = self.query_collection_info(deps.as_ref())?;
300        if collection.creator != info.sender {
301            return Err(ContractError::Unauthorized {});
302        }
303
304        let frozen = true;
305        self.frozen_collection_info.save(deps.storage, &frozen)?;
306        let event = Event::new("freeze_collection").add_attribute("sender", info.sender);
307        Ok(Response::new().add_event(event))
308    }
309
310    pub fn mint(
311        &self,
312        deps: DepsMut,
313        _env: Env,
314        info: MessageInfo,
315        nft_data: NftParams<T>,
316    ) -> Result<Response, ContractError> {
317        assert_minter_owner(deps.storage, &info.sender)?;
318        let (token_id, owner, token_uri, extension) = match nft_data {
319            NftParams::NftData {
320                token_id,
321                owner,
322                token_uri,
323                extension,
324            } => (token_id, owner, token_uri, extension),
325        };
326
327        // create the token
328        let token = TokenInfo {
329            owner: deps.api.addr_validate(&owner)?,
330            approvals: vec![],
331            token_uri: token_uri.clone(),
332            extension,
333        };
334        self.parent
335            .tokens
336            .update(deps.storage, &token_id, |old| match old {
337                Some(_) => Err(ContractError::Claimed {}),
338                None => Ok(token),
339            })?;
340
341        self.parent.increment_tokens(deps.storage)?;
342
343        let mut res = Response::new()
344            .add_attribute("action", "mint")
345            .add_attribute("minter", info.sender)
346            .add_attribute("owner", owner)
347            .add_attribute("token_id", token_id);
348        if let Some(token_uri) = token_uri {
349            res = res.add_attribute("token_uri", token_uri);
350        }
351        Ok(res)
352    }
353
354    pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
355        match msg {
356            QueryMsg::CollectionInfo {} => to_json_binary(&self.query_collection_info(deps)?),
357            _ => self.parent.query(deps, env, msg.into()),
358        }
359    }
360
361    pub fn query_collection_info(&self, deps: Deps) -> StdResult<CollectionInfoResponse> {
362        let info = self.collection_info.load(deps.storage)?;
363
364        let royalty_info_res: Option<ResidualInfoResponse> = match info.residual_info {
365            Some(residual_info) => Some(ResidualInfoResponse {
366                payment_address: residual_info.payment_address.to_string(),
367                share: residual_info.share,
368            }),
369            None => None,
370        };
371
372        Ok(CollectionInfoResponse {
373            creator: info.creator,
374            description: info.description,
375            image: info.image,
376            external_link: info.external_link,
377            explicit_content: info.explicit_content,
378            start_trading_time: info.start_trading_time,
379            residual_info: royalty_info_res,
380        })
381    }
382
383    pub fn migrate(mut deps: DepsMut, env: Env, _msg: Empty) -> Result<Response, ContractError> {
384        let prev_contract_version = cw2::get_contract_version(deps.storage)?;
385
386        let valid_contract_names = vec![CONTRACT_NAME.to_string()];
387        if !valid_contract_names.contains(&prev_contract_version.contract) {
388            return Err(StdError::generic_err("Invalid contract name for migration").into());
389        }
390
391        #[allow(clippy::cmp_owned)]
392        if prev_contract_version.version >= CONTRACT_VERSION.to_string() {
393            return Err(StdError::generic_err("Must upgrade contract version").into());
394        }
395
396        let mut response = Response::new();
397
398        #[allow(clippy::cmp_owned)]
399        if prev_contract_version.version < "3.0.0".to_string() {
400            response = crate::upgrades::v3_0_0::upgrade(deps.branch(), &env, response)?;
401        }
402
403        #[allow(clippy::cmp_owned)]
404        if prev_contract_version.version < "3.1.0".to_string() {
405            response = crate::upgrades::v3_1_0::upgrade(deps.branch(), &env, response)?;
406        }
407
408        cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
409
410        response = response.add_event(
411            Event::new("migrate")
412                .add_attribute("from_name", prev_contract_version.contract)
413                .add_attribute("from_version", prev_contract_version.version)
414                .add_attribute("to_name", CONTRACT_NAME)
415                .add_attribute("to_version", CONTRACT_VERSION),
416        );
417
418        Ok(response)
419    }
420}
421
422pub fn share_validate(share: Decimal) -> Result<Decimal, ContractError> {
423    if share > Decimal::one() {
424        return Err(ContractError::InvalidRoyalties(
425            "Share cannot be greater than 100%".to_string(),
426        ));
427    }
428
429    Ok(share)
430}
431
432pub fn get_owner_minter(storage: &mut dyn Storage) -> Result<Addr, ContractError> {
433    let ownership = cw_ownable::get_ownership(storage)?;
434    match ownership.owner {
435        Some(owner_value) => Ok(owner_value),
436        None => Err(ContractError::MinterNotFound {}),
437    }
438}
439
440pub fn assert_minter_owner(storage: &mut dyn Storage, sender: &Addr) -> Result<(), ContractError> {
441    let res = cw_ownable::assert_owner(storage, sender);
442    match res {
443        Ok(_) => Ok(()),
444        Err(_) => Err(ContractError::UnauthorizedOwner {}),
445    }
446}