1use std::collections::HashMap;
2
3use cosmwasm_schema::{cw_serde, QueryResponses};
4use cosmwasm_std::{
5 to_json_binary, Addr, Binary, Coin, ContractInfoResponse, Decimal, Deps, Env, MessageInfo,
6 Timestamp,
7};
8use cw_ownable::{Action, Ownership};
9use cw_utils::Expiration;
10use serde::Serialize;
11use url::Url;
12
13use crate::error::Cw721ContractError;
14use crate::execute::{assert_creator, assert_minter};
15use crate::state::{
16 Attribute, CollectionExtension, CollectionExtensionAttributes, CollectionInfo, NftInfo, Trait,
17 ATTRIBUTE_BANNER_URL, ATTRIBUTE_DESCRIPTION, ATTRIBUTE_EXPLICIT_CONTENT,
18 ATTRIBUTE_EXTERNAL_LINK, ATTRIBUTE_IMAGE, ATTRIBUTE_ROYALTY_INFO, ATTRIBUTE_START_TRADING_TIME,
19 CREATOR, MAX_COLLECTION_DESCRIPTION_LENGTH, MAX_ROYALTY_SHARE_DELTA_PCT, MAX_ROYALTY_SHARE_PCT,
20 MINTER,
21};
22use crate::traits::{Cw721CustomMsg, Cw721State, FromAttributesState, ToAttributesState};
23use crate::NftExtension;
24use crate::{traits::StateFactory, Approval, RoyaltyInfo};
25
26#[cw_serde]
27pub enum Cw721ExecuteMsg<
28 TNftExtensionMsg,
30 TCollectionExtensionMsg,
32 TExtensionMsg,
34> {
35 #[deprecated(since = "0.19.0", note = "Please use UpdateMinterOwnership instead")]
36 UpdateOwnership(Action),
38 UpdateMinterOwnership(Action),
39 UpdateCreatorOwnership(Action),
40
41 UpdateCollectionInfo {
43 collection_info: CollectionInfoMsg<TCollectionExtensionMsg>,
44 },
45 TransferNft {
47 recipient: String,
48 token_id: String,
49 },
50 SendNft {
53 contract: String,
54 token_id: String,
55 msg: Binary,
56 },
57 Approve {
60 spender: String,
61 token_id: String,
62 expires: Option<Expiration>,
63 },
64 Revoke {
66 spender: String,
67 token_id: String,
68 },
69 ApproveAll {
72 operator: String,
73 expires: Option<Expiration>,
74 },
75 RevokeAll {
77 operator: String,
78 },
79
80 Mint {
82 token_id: String,
84 owner: String,
86 token_uri: Option<String>,
90 extension: TNftExtensionMsg,
92 },
93
94 Burn {
96 token_id: String,
97 },
98
99 UpdateExtension {
101 msg: TExtensionMsg,
102 },
103
104 UpdateNftInfo {
107 token_id: String,
108 token_uri: Option<String>,
110 extension: TNftExtensionMsg,
111 },
112
113 SetWithdrawAddress {
115 address: String,
116 },
117 RemoveWithdrawAddress {},
119 WithdrawFunds {
122 amount: Coin,
123 },
124}
125
126#[cw_serde]
127pub struct Cw721InstantiateMsg<TCollectionExtensionMsg> {
128 pub name: String,
130 pub symbol: String,
132 pub collection_info_extension: TCollectionExtensionMsg,
134
135 pub minter: Option<String>,
139
140 pub creator: Option<String>,
142
143 pub withdraw_address: Option<String>,
144}
145
146#[cw_serde]
147#[derive(QueryResponses)]
148pub enum Cw721QueryMsg<
149 TNftExtension,
151 TCollectionExtension,
153 TExtensionQueryMsg,
155> {
156 #[returns(OwnerOfResponse)]
158 OwnerOf {
159 token_id: String,
160 include_expired: Option<bool>,
162 },
163 #[returns(ApprovalResponse)]
165 Approval {
166 token_id: String,
167 spender: String,
168 include_expired: Option<bool>,
169 },
170 #[returns(ApprovalsResponse)]
172 Approvals {
173 token_id: String,
174 include_expired: Option<bool>,
175 },
176 #[returns(OperatorResponse)]
178 Operator {
179 owner: String,
180 operator: String,
181 include_expired: Option<bool>,
182 },
183 #[returns(OperatorsResponse)]
185 AllOperators {
186 owner: String,
187 include_expired: Option<bool>,
189 start_after: Option<String>,
190 limit: Option<u32>,
191 },
192 #[returns(NumTokensResponse)]
194 NumTokens {},
195
196 #[deprecated(
197 since = "0.19.0",
198 note = "Please use GetCollectionInfoAndExtension instead"
199 )]
200 #[returns(CollectionInfoAndExtensionResponse<TCollectionExtension>)]
201 ContractInfo {},
203
204 #[returns(ConfigResponse<TCollectionExtension>)]
206 GetConfig {},
207
208 #[returns(CollectionInfoAndExtensionResponse<TCollectionExtension>)]
210 GetCollectionInfoAndExtension {},
211
212 #[returns(AllInfoResponse)]
214 GetAllInfo {},
215
216 #[returns(CollectionExtensionAttributes)]
218 GetCollectionExtensionAttributes {},
219
220 #[deprecated(since = "0.19.0", note = "Please use GetMinterOwnership instead")]
221 #[returns(Ownership<Addr>)]
222 Ownership {},
224
225 #[deprecated(since = "0.19.0", note = "Please use GetMinterOwnership instead")]
227 #[returns(MinterResponse)]
228 Minter {},
230
231 #[returns(Ownership<Addr>)]
232 GetMinterOwnership {},
233
234 #[returns(Ownership<Addr>)]
235 GetCreatorOwnership {},
236
237 #[returns(NftInfoResponse<TNftExtension>)]
241 NftInfo { token_id: String },
242
243 #[returns(Option<NftInfoResponse<TNftExtension>>)]
244 GetNftByExtension {
245 extension: TNftExtension,
246 start_after: Option<String>,
247 limit: Option<u32>,
248 },
249
250 #[returns(AllNftInfoResponse<TNftExtension>)]
254 AllNftInfo {
255 token_id: String,
256 include_expired: Option<bool>,
258 },
259
260 #[returns(TokensResponse)]
263 Tokens {
264 owner: String,
265 start_after: Option<String>,
266 limit: Option<u32>,
267 },
268 #[returns(TokensResponse)]
271 AllTokens {
272 start_after: Option<String>,
273 limit: Option<u32>,
274 },
275
276 #[returns(())]
278 Extension { msg: TExtensionQueryMsg },
279
280 #[returns(())]
281 GetCollectionExtension { msg: TCollectionExtension },
282
283 #[returns(Option<String>)]
284 GetWithdrawAddress {},
285}
286
287#[cw_serde]
288pub enum Cw721MigrateMsg {
289 WithUpdate {
290 minter: Option<String>,
291 creator: Option<String>,
292 },
293}
294
295#[cw_serde]
296pub struct CollectionInfoMsg<TCollectionExtensionMsg> {
297 pub name: Option<String>,
298 pub symbol: Option<String>,
299 pub extension: TCollectionExtensionMsg,
300}
301
302#[cw_serde]
303pub struct AttributeMsg {
304 pub attr_type: AttributeType,
305 pub key: String,
306 pub value: String,
307 pub data: Option<HashMap<String, String>>,
308}
309
310impl AttributeMsg {
311 pub fn string_value(&self) -> Result<String, Cw721ContractError> {
312 Ok(self.value.clone())
313 }
314
315 pub fn u64_value(&self) -> Result<u64, Cw721ContractError> {
316 Ok(self.value.parse::<u64>()?)
317 }
318
319 pub fn bool_value(&self) -> Result<bool, Cw721ContractError> {
320 Ok(self.value.parse::<bool>()?)
321 }
322
323 pub fn decimal_value(&self) -> Result<Decimal, Cw721ContractError> {
324 Ok(self.value.parse::<Decimal>()?)
325 }
326
327 pub fn timestamp_value(&self) -> Result<Timestamp, Cw721ContractError> {
328 let nanos = self.u64_value()?;
329 Ok(Timestamp::from_nanos(nanos))
330 }
331
332 pub fn addr_value(&self) -> Result<Addr, Cw721ContractError> {
333 Ok(Addr::unchecked(self.string_value()?))
334 }
335}
336
337impl AttributeMsg {
338 pub fn from(&self) -> Result<Attribute, Cw721ContractError> {
339 let value = match self.attr_type {
340 AttributeType::String => to_json_binary(&self.string_value()?)?,
341 AttributeType::U64 => to_json_binary(&self.u64_value()?)?,
342 AttributeType::Boolean => to_json_binary(&self.bool_value()?)?,
343 AttributeType::Decimal => to_json_binary(&self.decimal_value()?)?,
344 AttributeType::Timestamp => to_json_binary(&self.timestamp_value()?)?,
345 AttributeType::Addr => to_json_binary(&self.addr_value()?)?,
346 AttributeType::Custom => {
347 return Err(Cw721ContractError::UnsupportedCustomAttributeType {
348 key: self.key.clone(),
349 value: self.value.clone(),
350 });
351 }
352 };
353 let attribute = Attribute {
354 key: self.key.clone(),
355 value,
356 };
357 Ok(attribute)
358 }
359}
360
361#[cw_serde]
362pub enum AttributeType {
363 String,
364 U64,
365 Boolean,
366 Timestamp,
367 Addr,
368 Decimal,
369 Custom,
370}
371
372#[cw_serde]
373pub struct CollectionExtensionMsg<TRoyaltyInfoResponse> {
375 pub description: Option<String>,
376 pub image: Option<String>,
377 pub external_link: Option<String>,
378 pub banner_url: Option<String>,
379 pub explicit_content: Option<bool>,
380 pub start_trading_time: Option<Timestamp>,
381 pub royalty_info: Option<TRoyaltyInfoResponse>,
382}
383
384impl<TRoyaltyInfoResponse> Cw721CustomMsg for CollectionExtensionMsg<TRoyaltyInfoResponse> where
385 TRoyaltyInfoResponse: Cw721CustomMsg
386{
387}
388
389impl StateFactory<CollectionExtension<RoyaltyInfo>>
390 for CollectionExtensionMsg<RoyaltyInfoResponse>
391{
392 fn create(
394 &self,
395 deps: Deps,
396 env: &Env,
397 info: Option<&MessageInfo>,
398 current: Option<&CollectionExtension<RoyaltyInfo>>,
399 ) -> Result<CollectionExtension<RoyaltyInfo>, Cw721ContractError> {
400 self.validate(deps, env, info, current)?;
401 match current {
402 Some(current) => {
404 let mut updated = current.clone();
405 if let Some(description) = &self.description {
406 updated.description.clone_from(description);
407 }
408 if let Some(image) = &self.image {
409 updated.image.clone_from(image)
410 }
411 if let Some(external_link) = &self.external_link {
412 updated.external_link = Some(external_link.clone());
413 }
414 if let Some(banner_url) = &self.banner_url {
415 updated.banner_url = Some(banner_url.clone());
416 }
417 if let Some(explicit_content) = self.explicit_content {
418 updated.explicit_content = Some(explicit_content);
419 }
420 if let Some(start_trading_time) = self.start_trading_time {
421 updated.start_trading_time = Some(start_trading_time);
422 }
423 if let Some(royalty_info_response) = &self.royalty_info {
424 match current.royalty_info.clone() {
425 Some(current_royalty_info) => {
427 updated.royalty_info = Some(royalty_info_response.create(
428 deps,
429 env,
430 info,
431 Some(¤t_royalty_info),
432 )?);
433 }
434 None => {
436 updated.royalty_info =
437 Some(royalty_info_response.create(deps, env, info, None)?);
438 }
439 }
440 }
441 Ok(updated)
442 }
443 None => {
445 let royalty_info = match &self.royalty_info {
446 Some(royalty_info) => Some(royalty_info.create(deps, env, info, None)?),
448 None => None,
450 };
451 let new = CollectionExtension {
452 description: self.description.clone().unwrap_or_default(),
453 image: self.image.clone().unwrap_or_default(),
454 external_link: self.external_link.clone(),
455 banner_url: self.banner_url.clone(),
456 explicit_content: self.explicit_content,
457 start_trading_time: self.start_trading_time,
458 royalty_info,
459 };
460 Ok(new)
461 }
462 }
463 }
464
465 fn validate(
467 &self,
468 deps: Deps,
469 _env: &Env,
470 info: Option<&MessageInfo>,
471 _current: Option<&CollectionExtension<RoyaltyInfo>>,
472 ) -> Result<(), Cw721ContractError> {
473 let sender = info.map(|i| &i.sender);
474
475 let minter_initialized = MINTER.item.may_load(deps.storage)?;
476 if self.start_trading_time.is_some()
478 && minter_initialized.is_some()
479 && sender.is_some()
480 && MINTER.assert_owner(deps.storage, sender.unwrap()).is_err()
481 && MINTER.item.exists(deps.storage)
482 {
483 return Err(Cw721ContractError::NotMinter {});
484 }
485
486 let creator_initialized = CREATOR.item.may_load(deps.storage)?;
488 if (self.description.is_some()
489 || self.image.is_some()
490 || self.external_link.is_some()
491 || self.banner_url.is_some()
492 || self.explicit_content.is_some())
493 && sender.is_some()
494 && creator_initialized.is_some()
495 && CREATOR.assert_owner(deps.storage, sender.unwrap()).is_err()
496 {
497 return Err(Cw721ContractError::NotCreator {});
498 }
499
500 if let Some(description) = &self.description {
502 if description.is_empty() {
503 return Err(Cw721ContractError::CollectionDescriptionEmpty {});
504 }
505 if description.len() > MAX_COLLECTION_DESCRIPTION_LENGTH as usize {
506 return Err(Cw721ContractError::CollectionDescriptionTooLong {
507 max_length: MAX_COLLECTION_DESCRIPTION_LENGTH,
508 });
509 }
510 }
511
512 if let Some(image) = &self.image {
514 Url::parse(image)?;
515 }
516 if let Some(external_link) = &self.external_link {
517 Url::parse(external_link)?;
518 }
519 if let Some(banner_url) = &self.banner_url {
520 Url::parse(banner_url)?;
521 }
522 Ok(())
524 }
525}
526
527#[cw_serde]
528pub struct RoyaltyInfoResponse {
530 pub payment_address: String,
531 pub share: Decimal,
532}
533
534impl Cw721CustomMsg for RoyaltyInfoResponse {}
535
536impl StateFactory<RoyaltyInfo> for RoyaltyInfoResponse {
537 fn create(
538 &self,
539 deps: Deps,
540 env: &Env,
541 info: Option<&MessageInfo>,
542 current: Option<&RoyaltyInfo>,
543 ) -> Result<RoyaltyInfo, Cw721ContractError> {
544 self.validate(deps, env, info, current)?;
545 match current {
546 Some(current) => {
548 let mut updated = current.clone();
549 updated.payment_address = Addr::unchecked(self.payment_address.as_str()); updated.share = self.share;
551 Ok(updated)
552 }
553 None => {
555 let new = RoyaltyInfo {
556 payment_address: Addr::unchecked(self.payment_address.as_str()), share: self.share,
558 };
559 Ok(new)
560 }
561 }
562 }
563
564 fn validate(
565 &self,
566 deps: Deps,
567 _env: &Env,
568 _info: Option<&MessageInfo>,
569 current: Option<&RoyaltyInfo>,
570 ) -> Result<(), Cw721ContractError> {
571 if let Some(current_royalty_info) = current {
572 if current_royalty_info.share < self.share {
574 let share_delta = self.share.abs_diff(current_royalty_info.share);
575
576 if share_delta > Decimal::percent(MAX_ROYALTY_SHARE_DELTA_PCT) {
577 return Err(Cw721ContractError::InvalidRoyalties(format!(
578 "Share increase cannot be greater than {MAX_ROYALTY_SHARE_DELTA_PCT}%"
579 )));
580 }
581 }
582 }
583 if self.share > Decimal::percent(MAX_ROYALTY_SHARE_PCT) {
585 return Err(Cw721ContractError::InvalidRoyalties(format!(
586 "Share cannot be greater than {MAX_ROYALTY_SHARE_PCT}%"
587 )));
588 }
589 deps.api.addr_validate(self.payment_address.as_str())?;
591 Ok(())
592 }
593}
594
595impl From<RoyaltyInfo> for RoyaltyInfoResponse {
596 fn from(royalty_info: RoyaltyInfo) -> Self {
597 Self {
598 payment_address: royalty_info.payment_address.to_string(),
599 share: royalty_info.share,
600 }
601 }
602}
603
604#[cw_serde]
606pub struct ConfigResponse<TCollectionExtension> {
607 pub num_tokens: u64,
608 pub minter_ownership: Ownership<Addr>,
609 pub creator_ownership: Ownership<Addr>,
610 pub withdraw_address: Option<String>,
611 pub collection_info: CollectionInfo,
612 pub collection_extension: TCollectionExtension,
613 pub contract_info: ContractInfoResponse,
614}
615
616#[cw_serde]
618pub struct CollectionInfoAndExtensionResponse<TCollectionExtension> {
619 pub name: String,
620 pub symbol: String,
621 pub extension: TCollectionExtension,
622 pub updated_at: Timestamp,
623}
624
625#[cw_serde]
627pub struct AllInfoResponse {
628 pub contract_info: ContractInfoResponse,
630 pub collection_info: CollectionInfo,
632 pub collection_extension: CollectionExtensionAttributes,
633 pub num_tokens: u64,
635}
636
637impl<T> From<CollectionInfoAndExtensionResponse<T>> for CollectionInfo {
638 fn from(response: CollectionInfoAndExtensionResponse<T>) -> Self {
639 CollectionInfo {
640 name: response.name,
641 symbol: response.symbol,
642 updated_at: response.updated_at,
643 }
644 }
645}
646
647impl<TCollectionExtension, TCollectionExtensionMsg>
648 StateFactory<CollectionInfoAndExtensionResponse<TCollectionExtension>>
649 for CollectionInfoMsg<TCollectionExtensionMsg>
650where
651 TCollectionExtension: Cw721State,
652 TCollectionExtensionMsg: Cw721CustomMsg + StateFactory<TCollectionExtension>,
653{
654 fn create(
655 &self,
656 deps: Deps,
657 env: &Env,
658 info: Option<&MessageInfo>,
659 current: Option<&CollectionInfoAndExtensionResponse<TCollectionExtension>>,
660 ) -> Result<CollectionInfoAndExtensionResponse<TCollectionExtension>, Cw721ContractError> {
661 self.validate(deps, env, info, current)?;
662 match current {
663 Some(current) => {
665 let mut updated = current.clone();
666 if let Some(name) = &self.name {
667 updated.name.clone_from(name);
668 }
669 if let Some(symbol) = &self.symbol {
670 updated.symbol.clone_from(symbol);
671 }
672 let current_extension = current.extension.clone();
673 let updated_extension =
674 self.extension
675 .create(deps, env, info, Some(¤t_extension))?;
676 updated.extension = updated_extension;
677 Ok(updated)
678 }
679 None => {
681 let extension = self.extension.create(deps, env, info, None)?;
682 let new = CollectionInfoAndExtensionResponse {
683 name: self.name.clone().unwrap(),
684 symbol: self.symbol.clone().unwrap(),
685 extension,
686 updated_at: env.block.time,
687 };
688 Ok(new)
689 }
690 }
691 }
692
693 fn validate(
694 &self,
695 deps: Deps,
696 _env: &Env,
697 info: Option<&MessageInfo>,
698 _current: Option<&CollectionInfoAndExtensionResponse<TCollectionExtension>>,
699 ) -> Result<(), Cw721ContractError> {
700 if self.name.is_some() && self.name.clone().unwrap().is_empty() {
702 return Err(Cw721ContractError::CollectionNameEmpty {});
703 }
704 if self.symbol.is_some() && self.symbol.clone().unwrap().is_empty() {
705 return Err(Cw721ContractError::CollectionSymbolEmpty {});
706 }
707 let creator_initialized = CREATOR.item.may_load(deps.storage)?;
711 if (self.name.is_some() || self.symbol.is_some())
712 && creator_initialized.is_some()
713 && info.is_some()
714 && CREATOR
715 .assert_owner(deps.storage, &info.unwrap().sender)
716 .is_err()
717 {
718 return Err(Cw721ContractError::NotCreator {});
719 }
720 Ok(())
721 }
722}
723
724impl<TRoyaltyInfo> ToAttributesState for CollectionExtension<TRoyaltyInfo>
725where
726 TRoyaltyInfo: Serialize,
727{
728 fn to_attributes_state(&self) -> Result<Vec<Attribute>, Cw721ContractError> {
729 let attributes = vec![
730 Attribute {
731 key: ATTRIBUTE_DESCRIPTION.to_string(),
732 value: to_json_binary(&self.description)?,
733 },
734 Attribute {
735 key: ATTRIBUTE_IMAGE.to_string(),
736 value: to_json_binary(&self.image)?,
737 },
738 Attribute {
739 key: ATTRIBUTE_EXTERNAL_LINK.to_string(),
740 value: to_json_binary(&self.external_link.clone())?,
741 },
742 Attribute {
743 key: ATTRIBUTE_BANNER_URL.to_string(),
744 value: to_json_binary(&self.banner_url.clone())?,
745 },
746 Attribute {
747 key: ATTRIBUTE_EXPLICIT_CONTENT.to_string(),
748 value: to_json_binary(&self.explicit_content)?,
749 },
750 Attribute {
751 key: ATTRIBUTE_START_TRADING_TIME.to_string(),
752 value: to_json_binary(&self.start_trading_time)?,
753 },
754 Attribute {
755 key: ATTRIBUTE_ROYALTY_INFO.to_string(),
756 value: to_json_binary(&self.royalty_info)?,
757 },
758 ];
759 Ok(attributes)
760 }
761}
762
763impl<TRoyaltyInfo> FromAttributesState for CollectionExtension<TRoyaltyInfo>
764where
765 TRoyaltyInfo: ToAttributesState + FromAttributesState,
766{
767 fn from_attributes_state(attributes: &[Attribute]) -> Result<Self, Cw721ContractError> {
768 let description = attributes
769 .iter()
770 .find(|attr| attr.key == ATTRIBUTE_DESCRIPTION)
771 .ok_or_else(|| Cw721ContractError::AttributeMissing("description".to_string()))?
772 .value::<String>()?;
773 let image = attributes
774 .iter()
775 .find(|attr| attr.key == ATTRIBUTE_IMAGE)
776 .ok_or_else(|| Cw721ContractError::AttributeMissing("image".to_string()))?
777 .value::<String>()?;
778 let external_link = attributes
779 .iter()
780 .find(|attr| attr.key == ATTRIBUTE_EXTERNAL_LINK)
781 .ok_or_else(|| Cw721ContractError::AttributeMissing("external link".to_string()))?
782 .value::<Option<String>>()?;
783 let banner_url = attributes
784 .iter()
785 .find(|attr| attr.key == ATTRIBUTE_BANNER_URL)
786 .and_then(|attr| attr.value::<Option<String>>().ok())
787 .unwrap_or(None);
788 let explicit_content = attributes
789 .iter()
790 .find(|attr| attr.key == ATTRIBUTE_EXPLICIT_CONTENT)
791 .ok_or_else(|| Cw721ContractError::AttributeMissing("explicit content".to_string()))?
792 .value::<Option<bool>>()?;
793 let start_trading_time = attributes
794 .iter()
795 .find(|attr| attr.key == ATTRIBUTE_START_TRADING_TIME)
796 .ok_or_else(|| Cw721ContractError::AttributeMissing("start trading time".to_string()))?
797 .value::<Option<Timestamp>>()?;
798
799 let royalty_info = attributes
800 .iter()
801 .find(|attr| attr.key == ATTRIBUTE_ROYALTY_INFO)
802 .ok_or_else(|| Cw721ContractError::AttributeMissing("royalty info".to_string()))?
803 .value::<Option<RoyaltyInfo>>()?;
804
805 let royalty_info = if royalty_info.is_some() {
806 Some(FromAttributesState::from_attributes_state(attributes)?)
807 } else {
808 None
809 };
810 Ok(CollectionExtension {
811 description,
812 image,
813 external_link,
814 banner_url,
815 explicit_content,
816 start_trading_time,
817 royalty_info,
818 })
819 }
820}
821
822#[cw_serde]
823pub struct OwnerOfResponse {
824 pub owner: String,
826 pub approvals: Vec<Approval>,
828}
829
830#[cw_serde]
831pub struct ApprovalResponse {
832 pub approval: Approval,
833}
834
835#[cw_serde]
836pub struct ApprovalsResponse {
837 pub approvals: Vec<Approval>,
838}
839
840#[cw_serde]
841pub struct OperatorResponse {
842 pub approval: Approval,
843}
844
845#[cw_serde]
846pub struct OperatorsResponse {
847 pub operators: Vec<Approval>,
848}
849
850#[cw_serde]
851pub struct NumTokensResponse {
852 pub count: u64,
853}
854
855#[cw_serde]
856pub struct NftInfoResponse<TNftExtension> {
857 pub token_uri: Option<String>,
861 pub extension: TNftExtension,
863}
864
865#[cw_serde]
866pub struct AllNftInfoResponse<TNftExtension> {
867 pub access: OwnerOfResponse,
869 pub info: NftInfoResponse<TNftExtension>,
871}
872
873#[cw_serde]
874pub struct TokensResponse {
875 pub tokens: Vec<String>,
879}
880
881#[cw_serde]
884pub struct MinterResponse {
885 pub minter: Option<String>,
886}
887
888#[cw_serde]
889pub struct NftInfoMsg<TNftExtensionMsg> {
890 pub owner: String,
892 pub approvals: Vec<Approval>,
894
895 pub token_uri: Option<String>,
900
901 pub extension: TNftExtensionMsg,
903}
904
905impl<TNftExtension, TNftExtensionMsg> StateFactory<NftInfo<TNftExtension>>
906 for NftInfoMsg<TNftExtensionMsg>
907where
908 TNftExtension: Cw721State,
909 TNftExtensionMsg: Cw721CustomMsg + StateFactory<TNftExtension>,
910{
911 fn create(
912 &self,
913 deps: Deps,
914 env: &Env,
915 info: Option<&MessageInfo>,
916 optional_current: Option<&NftInfo<TNftExtension>>,
917 ) -> Result<NftInfo<TNftExtension>, Cw721ContractError> {
918 self.validate(deps, env, info, optional_current)?;
919 match optional_current {
920 Some(current) => {
922 let mut updated = current.clone();
923 if self.token_uri.is_some() {
924 updated.token_uri = empty_as_none(self.token_uri.clone());
925 }
926 let current_extension = optional_current.map(|c| &c.extension);
929 updated.extension = self.extension.create(deps, env, info, current_extension)?;
930 Ok(updated)
931 }
932 None => {
934 let extension = self.extension.create(deps, env, info, None)?;
935 let token_uri = empty_as_none(self.token_uri.clone());
936 Ok(NftInfo {
937 owner: Addr::unchecked(&self.owner), approvals: vec![],
939 token_uri,
940 extension,
941 })
942 }
943 }
944 }
945
946 fn validate(
947 &self,
948 deps: Deps,
949 _env: &Env,
950 info: Option<&MessageInfo>,
951 current: Option<&NftInfo<TNftExtension>>,
952 ) -> Result<(), Cw721ContractError> {
953 let info = info.ok_or(Cw721ContractError::NoInfo)?;
954 if current.is_none() {
955 assert_minter(deps.storage, &info.sender)?;
957 } else {
958 assert_creator(deps.storage, &info.sender)?;
960 }
961 let token_uri = empty_as_none(self.token_uri.clone());
963 if let Some(token_uri) = token_uri {
964 Url::parse(token_uri.as_str())?;
965 }
966 deps.api.addr_validate(&self.owner)?;
968 Ok(())
969 }
970}
971
972#[cw_serde]
973#[derive(Default)]
974pub struct NftExtensionMsg {
975 pub image: Option<String>,
977 pub image_data: Option<String>,
978 pub external_url: Option<String>,
980 pub description: Option<String>,
981 pub name: Option<String>,
982 pub attributes: Option<Vec<Trait>>,
983 pub background_color: Option<String>,
984 pub animation_url: Option<String>,
986 pub youtube_url: Option<String>,
988}
989
990impl Cw721CustomMsg for NftExtensionMsg {}
991
992impl From<NftExtension> for NftExtensionMsg {
993 fn from(extension: NftExtension) -> Self {
994 NftExtensionMsg {
995 image: extension.image,
996 image_data: extension.image_data,
997 external_url: extension.external_url,
998 description: extension.description,
999 name: extension.name,
1000 attributes: extension.attributes,
1001 background_color: extension.background_color,
1002 animation_url: extension.animation_url,
1003 youtube_url: extension.youtube_url,
1004 }
1005 }
1006}
1007
1008impl StateFactory<NftExtension> for NftExtensionMsg {
1009 fn create(
1010 &self,
1011 deps: Deps,
1012 env: &Env,
1013 info: Option<&MessageInfo>,
1014 current: Option<&NftExtension>,
1015 ) -> Result<NftExtension, Cw721ContractError> {
1016 self.validate(deps, env, info, current)?;
1017 match current {
1018 Some(current) => {
1020 let mut updated = current.clone();
1021 if self.image.is_some() {
1022 updated.image = empty_as_none(self.image.clone());
1023 }
1024 if self.image_data.is_some() {
1025 updated.image_data = empty_as_none(self.image_data.clone());
1026 }
1027 if self.external_url.is_some() {
1028 updated.external_url = empty_as_none(self.external_url.clone());
1029 }
1030 if self.description.is_some() {
1031 updated.description = empty_as_none(self.description.clone());
1032 }
1033 if self.name.is_some() {
1034 updated.name = empty_as_none(self.name.clone());
1035 }
1036 if self.attributes.is_some() {
1037 updated.attributes = match self.attributes.clone() {
1038 Some(attributes) => Some(attributes.create(deps, env, info, None)?),
1039 None => None,
1040 };
1041 }
1042 if self.background_color.is_some() {
1043 updated.background_color = empty_as_none(self.background_color.clone())
1044 }
1045 if self.animation_url.is_some() {
1046 updated.animation_url = empty_as_none(self.animation_url.clone());
1047 }
1048 if self.youtube_url.is_some() {
1049 updated.youtube_url = empty_as_none(self.youtube_url.clone());
1050 }
1051 Ok(updated)
1052 }
1053 None => {
1055 let mut new_metadata: NftExtension = self.clone().into();
1056 if self.attributes.is_some() {
1057 new_metadata.attributes = match self.attributes.clone() {
1058 Some(attributes) => Some(attributes.create(deps, env, info, None)?),
1059 None => None,
1060 };
1061 }
1062 Ok(new_metadata)
1063 }
1064 }
1065 }
1066
1067 fn validate(
1068 &self,
1069 deps: Deps,
1070 _env: &Env,
1071 info: Option<&MessageInfo>,
1072 current: Option<&NftExtension>,
1073 ) -> Result<(), Cw721ContractError> {
1074 if current.is_none() {
1078 let info = info.ok_or(Cw721ContractError::NoInfo)?;
1079 let minter_check = assert_minter(deps.storage, &info.sender);
1081 let creator_check = assert_creator(deps.storage, &info.sender);
1082 if minter_check.is_err() && creator_check.is_err() {
1083 return Err(Cw721ContractError::NotMinterOrCreator {});
1084 }
1085 } else {
1086 let info = info.ok_or(Cw721ContractError::NoInfo)?;
1087 assert_creator(deps.storage, &info.sender)?;
1089 }
1090 let image = empty_as_none(self.image.clone());
1092 if let Some(image) = &image {
1093 Url::parse(image)?;
1094 }
1095 let external_url = empty_as_none(self.external_url.clone());
1096 if let Some(url) = &external_url {
1097 Url::parse(url)?;
1098 }
1099 let animation_url = empty_as_none(self.animation_url.clone());
1100 if let Some(animation_url) = &animation_url {
1101 Url::parse(animation_url)?;
1102 }
1103 let youtube_url = empty_as_none(self.youtube_url.clone());
1104 if let Some(youtube_url) = &youtube_url {
1105 Url::parse(youtube_url)?;
1106 }
1107 Ok(())
1109 }
1110}
1111
1112pub fn empty_as_none(value: Option<String>) -> Option<String> {
1113 value.filter(|v| !v.is_empty())
1114}
1115
1116impl<TMsg, TState> StateFactory<Option<TState>> for Option<TMsg>
1117where
1118 TState: Cw721State,
1119 TMsg: Cw721CustomMsg + StateFactory<TState>,
1120{
1121 fn create(
1122 &self,
1123 deps: Deps,
1124 env: &Env,
1125 info: Option<&MessageInfo>,
1126 current: Option<&Option<TState>>,
1127 ) -> Result<Option<TState>, Cw721ContractError> {
1128 if self.is_none() {
1130 return Ok(None);
1131 }
1132 let msg = self.clone().unwrap();
1133 let current = current.and_then(|c| c.as_ref());
1135 let created_or_updated = msg.create(deps, env, info, current)?;
1136 Ok(Some(created_or_updated))
1137 }
1138
1139 fn validate(
1140 &self,
1141 deps: Deps,
1142 env: &Env,
1143 info: Option<&MessageInfo>,
1144 current: Option<&Option<TState>>,
1145 ) -> Result<(), Cw721ContractError> {
1146 if self.is_none() {
1148 return Ok(());
1149 }
1150 let msg = self.clone().unwrap();
1151 let current = current.and_then(|c| c.as_ref());
1153 msg.validate(deps, env, info, current)
1154 }
1155}