Skip to main content

cw721/
msg.rs

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    // NftInfo extension msg for onchain metadata.
29    TNftExtensionMsg,
30    // CollectionInfo extension msg for onchain collection attributes.
31    TCollectionExtensionMsg,
32    // Custom extension msg for custom contract logic. Default implementation is a no-op.
33    TExtensionMsg,
34> {
35    #[deprecated(since = "0.19.0", note = "Please use UpdateMinterOwnership instead")]
36    /// Deprecated: use UpdateMinterOwnership instead! Will be removed in next release!
37    UpdateOwnership(Action),
38    UpdateMinterOwnership(Action),
39    UpdateCreatorOwnership(Action),
40
41    /// The creator is the only one eligible to update `CollectionInfo`.
42    UpdateCollectionInfo {
43        collection_info: CollectionInfoMsg<TCollectionExtensionMsg>,
44    },
45    /// Transfer is a base message to move a token to another account without triggering actions
46    TransferNft {
47        recipient: String,
48        token_id: String,
49    },
50    /// Send is a base message to transfer a token to a contract and trigger an action
51    /// on the receiving contract.
52    SendNft {
53        contract: String,
54        token_id: String,
55        msg: Binary,
56    },
57    /// Allows operator to transfer / send the token from the owner's account.
58    /// If expiration is set, then this allowance has a time/height limit
59    Approve {
60        spender: String,
61        token_id: String,
62        expires: Option<Expiration>,
63    },
64    /// Remove previously granted Approval
65    Revoke {
66        spender: String,
67        token_id: String,
68    },
69    /// Allows operator to transfer / send any token from the owner's account.
70    /// If expiration is set, then this allowance has a time/height limit
71    ApproveAll {
72        operator: String,
73        expires: Option<Expiration>,
74    },
75    /// Remove previously granted ApproveAll permission
76    RevokeAll {
77        operator: String,
78    },
79
80    /// Mint a new NFT, can only be called by the contract minter
81    Mint {
82        /// Unique ID of the NFT
83        token_id: String,
84        /// The owner of the newly minter NFT
85        owner: String,
86        /// Universal resource identifier for this NFT
87        /// Should point to a JSON file that conforms to the ERC721
88        /// Metadata JSON Schema
89        token_uri: Option<String>,
90        /// Any custom extension used by this contract
91        extension: TNftExtensionMsg,
92    },
93
94    /// Burn an NFT the sender has access to
95    Burn {
96        token_id: String,
97    },
98
99    /// Custom msg execution. This is a no-op in default implementation.
100    UpdateExtension {
101        msg: TExtensionMsg,
102    },
103
104    /// The creator is the only one eligible to update NFT's token uri and onchain metadata (`NftInfo.extension`).
105    /// NOTE: approvals and owner are not affected by this call, since they belong to the NFT owner.
106    UpdateNftInfo {
107        token_id: String,
108        /// NOTE: Empty string is handled as None
109        token_uri: Option<String>,
110        extension: TNftExtensionMsg,
111    },
112
113    /// Sets address to send withdrawn fees to. Only owner can call this.
114    SetWithdrawAddress {
115        address: String,
116    },
117    /// Removes the withdraw address, so fees are sent to the contract. Only owner can call this.
118    RemoveWithdrawAddress {},
119    /// Withdraw from the contract to the given address. Anyone can call this,
120    /// which is okay since withdraw address has been set by owner.
121    WithdrawFunds {
122        amount: Coin,
123    },
124}
125
126#[cw_serde]
127pub struct Cw721InstantiateMsg<TCollectionExtensionMsg> {
128    /// Name of the NFT contract
129    pub name: String,
130    /// Symbol of the NFT contract
131    pub symbol: String,
132    /// Optional extension of the collection metadata
133    pub collection_info_extension: TCollectionExtensionMsg,
134
135    /// The minter is the only one who can create new NFTs.
136    /// This is designed for a base NFT that is controlled by an external program
137    /// or contract. You will likely replace this with custom logic in custom NFTs
138    pub minter: Option<String>,
139
140    /// Sets the creator of collection. The creator is the only one eligible to update `CollectionInfo`.
141    pub creator: Option<String>,
142
143    pub withdraw_address: Option<String>,
144}
145
146#[cw_serde]
147#[derive(QueryResponses)]
148pub enum Cw721QueryMsg<
149    // Return type of NFT metadata defined in `NftInfo` and `AllNftInfo`.
150    TNftExtension,
151    // Return type of collection extension defined in `GetCollectionInfo`.
152    TCollectionExtension,
153    // Custom query msg for custom contract logic. Default implementation returns an empty binary.
154    TExtensionQueryMsg,
155> {
156    /// Return the owner of the given token, error if token does not exist
157    #[returns(OwnerOfResponse)]
158    OwnerOf {
159        token_id: String,
160        /// unset or false will filter out expired approvals, you must set to true to see them
161        include_expired: Option<bool>,
162    },
163    /// Return operator that can access all of the owner's tokens.
164    #[returns(ApprovalResponse)]
165    Approval {
166        token_id: String,
167        spender: String,
168        include_expired: Option<bool>,
169    },
170    /// Return approvals that a token has
171    #[returns(ApprovalsResponse)]
172    Approvals {
173        token_id: String,
174        include_expired: Option<bool>,
175    },
176    /// Return approval of a given operator for all tokens of an owner, error if not set
177    #[returns(OperatorResponse)]
178    Operator {
179        owner: String,
180        operator: String,
181        include_expired: Option<bool>,
182    },
183    /// List all operators that can access all of the owner's tokens
184    #[returns(OperatorsResponse)]
185    AllOperators {
186        owner: String,
187        /// unset or false will filter out expired items, you must set to true to see them
188        include_expired: Option<bool>,
189        start_after: Option<String>,
190        limit: Option<u32>,
191    },
192    /// Total number of tokens issued
193    #[returns(NumTokensResponse)]
194    NumTokens {},
195
196    #[deprecated(
197        since = "0.19.0",
198        note = "Please use GetCollectionInfoAndExtension instead"
199    )]
200    #[returns(CollectionInfoAndExtensionResponse<TCollectionExtension>)]
201    /// Deprecated: use GetCollectionInfoAndExtension instead! Will be removed in next release!
202    ContractInfo {},
203
204    /// Returns `AllCollectionInfoResponse`
205    #[returns(ConfigResponse<TCollectionExtension>)]
206    GetConfig {},
207
208    /// Returns `CollectionInfoAndExtensionResponse`
209    #[returns(CollectionInfoAndExtensionResponse<TCollectionExtension>)]
210    GetCollectionInfoAndExtension {},
211
212    /// returns `AllInfoResponse` which contains contract, collection and nft details
213    #[returns(AllInfoResponse)]
214    GetAllInfo {},
215
216    /// Returns `CollectionExtensionAttributes`
217    #[returns(CollectionExtensionAttributes)]
218    GetCollectionExtensionAttributes {},
219
220    #[deprecated(since = "0.19.0", note = "Please use GetMinterOwnership instead")]
221    #[returns(Ownership<Addr>)]
222    /// Deprecated: use GetMinterOwnership instead! Will be removed in next release!
223    Ownership {},
224
225    /// Return the minter
226    #[deprecated(since = "0.19.0", note = "Please use GetMinterOwnership instead")]
227    #[returns(MinterResponse)]
228    /// Deprecated: use GetMinterOwnership instead! Will be removed in next release!
229    Minter {},
230
231    #[returns(Ownership<Addr>)]
232    GetMinterOwnership {},
233
234    #[returns(Ownership<Addr>)]
235    GetCreatorOwnership {},
236
237    /// With MetaData Extension.
238    /// Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema*
239    /// but directly from the contract
240    #[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    /// With MetaData Extension.
251    /// Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization
252    /// for clients
253    #[returns(AllNftInfoResponse<TNftExtension>)]
254    AllNftInfo {
255        token_id: String,
256        /// unset or false will filter out expired approvals, you must set to true to see them
257        include_expired: Option<bool>,
258    },
259
260    /// With Enumerable extension.
261    /// Returns all tokens owned by the given address, [] if unset.
262    #[returns(TokensResponse)]
263    Tokens {
264        owner: String,
265        start_after: Option<String>,
266        limit: Option<u32>,
267    },
268    /// With Enumerable extension.
269    /// Requires pagination. Lists all token_ids controlled by the contract.
270    #[returns(TokensResponse)]
271    AllTokens {
272        start_after: Option<String>,
273        limit: Option<u32>,
274    },
275
276    /// Custom msg query. Default implementation returns an empty binary.
277    #[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]
373/// NOTE: In case `info` is not provided in `create()` or `validate()` (like for migration), creator/minter assertion is skipped.
374pub 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    /// NOTE: In case `info` is not provided (like for migration), creator/minter assertion is skipped.
393    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: update existing metadata
403            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: existing royalty info for update
426                        Some(current_royalty_info) => {
427                            updated.royalty_info = Some(royalty_info_response.create(
428                                deps,
429                                env,
430                                info,
431                                Some(&current_royalty_info),
432                            )?);
433                        }
434                        // None: no royalty info, so create new
435                        None => {
436                            updated.royalty_info =
437                                Some(royalty_info_response.create(deps, env, info, None)?);
438                        }
439                    }
440                }
441                Ok(updated)
442            }
443            // None: create new metadata
444            None => {
445                let royalty_info = match &self.royalty_info {
446                    // new royalty info
447                    Some(royalty_info) => Some(royalty_info.create(deps, env, info, None)?),
448                    // current royalty is none and new royalty is none
449                    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    /// NOTE: In case `info` is not provided (like for migration), creator/minter assertion is skipped.
466    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        // start trading time can only be updated by minter
477        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        // all other props collection extension can only be updated by the creator
487        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        // check description length, must not be empty and max 512 chars
501        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        // check images are URLs
513        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        // no need to check royalty info, as it is checked during creation of RoyaltyInfo
523        Ok(())
524    }
525}
526
527#[cw_serde]
528// This is both: a query response, and incoming message during instantiation and execution.
529pub 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: update existing royalty info
547            Some(current) => {
548                let mut updated = current.clone();
549                updated.payment_address = Addr::unchecked(self.payment_address.as_str()); // no check needed, since it is already done in validate
550                updated.share = self.share;
551                Ok(updated)
552            }
553            // None: create new royalty info
554            None => {
555                let new = RoyaltyInfo {
556                    payment_address: Addr::unchecked(self.payment_address.as_str()), // no check needed, since it is already done in validate
557                    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            // check max share delta
573            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        // check max share
584        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        // validate payment address
590        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/// This is a wrapper around CollectionInfo that includes the extension.
605#[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/// This is a wrapper around CollectionInfo that includes the extension.
617#[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/// This is a wrapper around CollectionInfo that includes the extension, contract info, and number of tokens (supply).
626#[cw_serde]
627pub struct AllInfoResponse {
628    // contract details
629    pub contract_info: ContractInfoResponse,
630    // collection details
631    pub collection_info: CollectionInfo,
632    pub collection_extension: CollectionExtensionAttributes,
633    // NFT details
634    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: update existing metadata
664            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(&current_extension))?;
676                updated.extension = updated_extension;
677                Ok(updated)
678            }
679            // None: create new metadata
680            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        // make sure the name and symbol are not empty
701        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        // collection metadata can only be updated by the creator. creator assertion is skipped for these cases:
708        // - CREATOR store is empty/not initioized (like in instantiation)
709        // - info is none (like in migration)
710        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    /// Owner of the token
825    pub owner: String,
826    /// If set this address is approved to transfer/send the token as well
827    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    /// Universal resource identifier for this NFT
858    /// Should point to a JSON file that conforms to the ERC721
859    /// Metadata JSON Schema
860    pub token_uri: Option<String>,
861    /// You can add any custom metadata here when you extend cw721-base
862    pub extension: TNftExtension,
863}
864
865#[cw_serde]
866pub struct AllNftInfoResponse<TNftExtension> {
867    /// Who can transfer the token
868    pub access: OwnerOfResponse,
869    /// Data on the token itself,
870    pub info: NftInfoResponse<TNftExtension>,
871}
872
873#[cw_serde]
874pub struct TokensResponse {
875    /// Contains all token_ids in lexicographical ordering
876    /// If there are more than `limit`, use `start_after` in future queries
877    /// to achieve pagination.
878    pub tokens: Vec<String>,
879}
880
881/// Deprecated: use Cw721QueryMsg::GetMinterOwnership instead!
882/// Shows who can mint these tokens.
883#[cw_serde]
884pub struct MinterResponse {
885    pub minter: Option<String>,
886}
887
888#[cw_serde]
889pub struct NftInfoMsg<TNftExtensionMsg> {
890    /// The owner of the newly minted NFT
891    pub owner: String,
892    /// Approvals are stored here, as we clear them all upon transfer and cannot accumulate much
893    pub approvals: Vec<Approval>,
894
895    /// Universal resource identifier for this NFT
896    /// Should point to a JSON file that conforms to the ERC721
897    /// Metadata JSON Schema
898    /// NOTE: Empty string is handled as None
899    pub token_uri: Option<String>,
900
901    /// You can add any custom metadata here when you extend cw721-base
902    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: update only token uri and extension in existing NFT (but not owner and approvals)
921            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                // update extension
927                // current extension is a nested option in option, so we need to flatten it
928                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: create new NFT, note: msg is of same type, so we can clone it
933            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), // only for creation we use owner, but not for update!
938                    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            // current is none: only minter can create new NFT
956            assert_minter(deps.storage, &info.sender)?;
957        } else {
958            // current is some: only creator can update NFT
959            assert_creator(deps.storage, &info.sender)?;
960        }
961        // validate token_uri is a URL
962        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        // validate owner
967        deps.api.addr_validate(&self.owner)?;
968        Ok(())
969    }
970}
971
972#[cw_serde]
973#[derive(Default)]
974pub struct NftExtensionMsg {
975    /// NOTE: Empty string is handled as None
976    pub image: Option<String>,
977    pub image_data: Option<String>,
978    /// NOTE: Empty string is handled as None
979    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    /// NOTE: Empty string is handled as None
985    pub animation_url: Option<String>,
986    /// NOTE: Empty string is handled as None
987    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: update existing metadata
1019            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: create new metadata, note: msg is of same type as metadata, so we can clone it
1054            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        // assert here is different to NFT Info:
1075        // - creator and minter can create NFT metadata
1076        // - only creator can update NFT metadata
1077        if current.is_none() {
1078            let info = info.ok_or(Cw721ContractError::NoInfo)?;
1079            // current is none: minter and creator can create new NFT metadata
1080            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            // current is some: only creator can update NFT metadata
1088            assert_creator(deps.storage, &info.sender)?;
1089        }
1090        // check URLs
1091        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        // no need to validate simple strings: image_data, description, name, and background_color
1108        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        // no msg, so no validation needed
1129        if self.is_none() {
1130            return Ok(None);
1131        }
1132        let msg = self.clone().unwrap();
1133        // current is a nested option in option, so we need to flatten it
1134        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        // no msg, so no validation needed
1147        if self.is_none() {
1148            return Ok(());
1149        }
1150        let msg = self.clone().unwrap();
1151        // current is a nested option in option, so we need to flatten it
1152        let current = current.and_then(|c| c.as_ref());
1153        msg.validate(deps, env, info, current)
1154    }
1155}