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 sg721::{
14 CollectionInfo, ExecuteMsg, InstantiateMsg, RoyaltyInfo, RoyaltyInfoResponse,
15 UpdateCollectionInfoMsg,
16};
17use sg_std::Response;
18
19use crate::msg::{CollectionInfoResponse, NftParams, QueryMsg};
20use crate::{ContractError, Sg721Contract};
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> Sg721Contract<'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 nonpayable(&info)?;
41
42 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 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 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 royalty_info: Option<RoyaltyInfo> = match msg.collection_info.royalty_info {
72 Some(royalty_info) => Some(RoyaltyInfo {
73 payment_address: deps.api.addr_validate(&royalty_info.payment_address)?,
74 share: share_validate(royalty_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 royalty_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 sg721::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<RoyaltyInfoResponse>,
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 if collection.creator != info.sender {
207 return Err(ContractError::Unauthorized {});
208 }
209
210 if let Some(new_creator) = collection_msg.creator {
211 deps.api.addr_validate(&new_creator)?;
212 collection.creator = new_creator;
213 }
214
215 collection.description = collection_msg
216 .description
217 .unwrap_or_else(|| collection.description.to_string());
218 if collection.description.len() > MAX_DESCRIPTION_LENGTH as usize {
219 return Err(ContractError::DescriptionTooLong {});
220 }
221
222 collection.image = collection_msg
223 .image
224 .unwrap_or_else(|| collection.image.to_string());
225 Url::parse(&collection.image)?;
226
227 collection.external_link = collection_msg
228 .external_link
229 .unwrap_or_else(|| collection.external_link.as_ref().map(|s| s.to_string()));
230 if collection.external_link.as_ref().is_some() {
231 Url::parse(collection.external_link.as_ref().unwrap())?;
232 }
233
234 collection.explicit_content = collection_msg.explicit_content;
235
236 if let Some(Some(new_royalty_info_response)) = collection_msg.royalty_info {
237 let last_royalty_update = self.royalty_updated_at.load(deps.storage)?;
238 if last_royalty_update.plus_seconds(24 * 60 * 60) > env.block.time {
239 return Err(ContractError::InvalidRoyalties(
240 "Royalties can only be updated once per day".to_string(),
241 ));
242 }
243
244 let new_royalty_info = RoyaltyInfo {
245 payment_address: deps
246 .api
247 .addr_validate(&new_royalty_info_response.payment_address)?,
248 share: share_validate(new_royalty_info_response.share)?,
249 };
250
251 if let Some(old_royalty_info) = collection.royalty_info {
252 if old_royalty_info.share < new_royalty_info.share {
253 let share_delta = new_royalty_info.share.abs_diff(old_royalty_info.share);
254
255 if share_delta > Decimal::percent(MAX_SHARE_DELTA_PCT) {
256 return Err(ContractError::InvalidRoyalties(format!(
257 "Share increase cannot be greater than {MAX_SHARE_DELTA_PCT}%"
258 )));
259 }
260 if new_royalty_info.share > Decimal::percent(MAX_ROYALTY_SHARE_PCT) {
261 return Err(ContractError::InvalidRoyalties(format!(
262 "Share cannot be greater than {MAX_ROYALTY_SHARE_PCT}%"
263 )));
264 }
265 }
266 }
267
268 collection.royalty_info = Some(new_royalty_info);
269 self.royalty_updated_at
270 .save(deps.storage, &env.block.time)?;
271 }
272
273 self.collection_info.save(deps.storage, &collection)?;
274
275 let event = Event::new("update_collection_info").add_attribute("sender", info.sender);
276 Ok(Response::new().add_event(event))
277 }
278
279 pub fn update_start_trading_time(
282 &self,
283 deps: DepsMut,
284 _env: Env,
285 info: MessageInfo,
286 start_time: Option<Timestamp>,
287 ) -> Result<Response, ContractError> {
288 assert_minter_owner(deps.storage, &info.sender)?;
289
290 let mut collection_info = self.collection_info.load(deps.storage)?;
291 collection_info.start_trading_time = start_time;
292 self.collection_info.save(deps.storage, &collection_info)?;
293
294 let event = Event::new("update_start_trading_time").add_attribute("sender", info.sender);
295 Ok(Response::new().add_event(event))
296 }
297
298 pub fn freeze_collection_info(
299 &self,
300 deps: DepsMut,
301 _env: Env,
302 info: MessageInfo,
303 ) -> Result<Response, ContractError> {
304 let collection = self.query_collection_info(deps.as_ref())?;
305 if collection.creator != info.sender {
306 return Err(ContractError::Unauthorized {});
307 }
308
309 let frozen = true;
310 self.frozen_collection_info.save(deps.storage, &frozen)?;
311 let event = Event::new("freeze_collection").add_attribute("sender", info.sender);
312 Ok(Response::new().add_event(event))
313 }
314
315 pub fn mint(
316 &self,
317 deps: DepsMut,
318 _env: Env,
319 info: MessageInfo,
320 nft_data: NftParams<T>,
321 ) -> Result<Response, ContractError> {
322 assert_minter_owner(deps.storage, &info.sender)?;
323 let (token_id, owner, token_uri, extension) = match nft_data {
324 NftParams::NftData {
325 token_id,
326 owner,
327 token_uri,
328 extension,
329 } => (token_id, owner, token_uri, extension),
330 };
331
332 let token = TokenInfo {
334 owner: deps.api.addr_validate(&owner)?,
335 approvals: vec![],
336 token_uri: token_uri.clone(),
337 extension,
338 };
339 self.parent
340 .tokens
341 .update(deps.storage, &token_id, |old| match old {
342 Some(_) => Err(ContractError::Claimed {}),
343 None => Ok(token),
344 })?;
345
346 self.parent.increment_tokens(deps.storage)?;
347
348 let mut res = Response::new()
349 .add_attribute("action", "mint")
350 .add_attribute("minter", info.sender)
351 .add_attribute("owner", owner)
352 .add_attribute("token_id", token_id);
353 if let Some(token_uri) = token_uri {
354 res = res.add_attribute("token_uri", token_uri);
355 }
356 Ok(res)
357 }
358
359 pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
360 match msg {
361 QueryMsg::CollectionInfo {} => to_json_binary(&self.query_collection_info(deps)?),
362 _ => self.parent.query(deps, env, msg.into()),
363 }
364 }
365
366 pub fn query_collection_info(&self, deps: Deps) -> StdResult<CollectionInfoResponse> {
367 let info = self.collection_info.load(deps.storage)?;
368
369 let royalty_info_res: Option<RoyaltyInfoResponse> = match info.royalty_info {
370 Some(royalty_info) => Some(RoyaltyInfoResponse {
371 payment_address: royalty_info.payment_address.to_string(),
372 share: royalty_info.share,
373 }),
374 None => None,
375 };
376
377 Ok(CollectionInfoResponse {
378 creator: info.creator,
379 description: info.description,
380 image: info.image,
381 external_link: info.external_link,
382 explicit_content: info.explicit_content,
383 start_trading_time: info.start_trading_time,
384 royalty_info: royalty_info_res,
385 })
386 }
387
388 pub fn migrate(mut deps: DepsMut, env: Env, _msg: Empty) -> Result<Response, ContractError> {
389 let prev_contract_version = cw2::get_contract_version(deps.storage)?;
390
391 let valid_contract_names = [CONTRACT_NAME.to_string()];
392 if !valid_contract_names.contains(&prev_contract_version.contract) {
393 return Err(StdError::generic_err("Invalid contract name for migration").into());
394 }
395
396 #[allow(clippy::cmp_owned)]
397 if prev_contract_version.version >= CONTRACT_VERSION.to_string() {
398 return Err(StdError::generic_err("Must upgrade contract version").into());
399 }
400
401 let mut response = Response::new();
402
403 #[allow(clippy::cmp_owned)]
404 if prev_contract_version.version < "3.0.0".to_string() {
405 response = crate::upgrades::v3_0_0::upgrade(deps.branch(), &env, response)?;
406 }
407
408 #[allow(clippy::cmp_owned)]
409 if prev_contract_version.version < "3.1.0".to_string() {
410 response = crate::upgrades::v3_1_0::upgrade(deps.branch(), &env, response)?;
411 }
412
413 cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
414
415 response = response.add_event(
416 Event::new("migrate")
417 .add_attribute("from_name", prev_contract_version.contract)
418 .add_attribute("from_version", prev_contract_version.version)
419 .add_attribute("to_name", CONTRACT_NAME)
420 .add_attribute("to_version", CONTRACT_VERSION),
421 );
422
423 Ok(response)
424 }
425}
426
427pub fn share_validate(share: Decimal) -> Result<Decimal, ContractError> {
428 if share > Decimal::one() {
429 return Err(ContractError::InvalidRoyalties(
430 "Share cannot be greater than 100%".to_string(),
431 ));
432 }
433
434 Ok(share)
435}
436
437pub fn get_owner_minter(storage: &mut dyn Storage) -> Result<Addr, ContractError> {
438 let ownership = cw_ownable::get_ownership(storage)?;
439 match ownership.owner {
440 Some(owner_value) => Ok(owner_value),
441 None => Err(ContractError::MinterNotFound {}),
442 }
443}
444
445pub fn assert_minter_owner(storage: &mut dyn Storage, sender: &Addr) -> Result<(), ContractError> {
446 let res = cw_ownable::assert_owner(storage, sender);
447 match res {
448 Ok(_) => Ok(()),
449 Err(_) => Err(ContractError::UnauthorizedOwner {}),
450 }
451}