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 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 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 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 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 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}