Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major release v0.19: add CollectionInfo, RoyaltyInfo, updatable NFTs for creator, etc. #156

Merged
merged 130 commits into from
Aug 11, 2024

Conversation

taitruong
Copy link
Collaborator

@taitruong taitruong commented Feb 27, 2024

Major refactoring for upcoming v0.19 release.

TL;DR: v0.19 release is a major refactoring, introducing distinctions between Creator (manages collection metadata like royalties) and Minter (controls minting), and extends UpdateNftInfo with extension for onchain metadata. We've also added a new CollectionInfo structure within the cw721 package, moving all core logic for easier access and implementation across contracts, thus streamlining contract architecture. This update simplifies the ecosystem by removing the need for separate cw721-metadata-onchain and cw2981-royalty contracts and clearly defining roles and functionalities.

Distinction between Creator and Minter

In previous release cw721 is only aware of minter (aka owner/minter ownership). Here we added Creator. Creator controls collection info (like royalties) whereas minter controls minting.

NEW utility: UpdateNftInfo and UpdateCollectionInfo msg

By adding a new CollectionInfoExtension store as an optional extension for CollectionInfo (name and symbol), there is also logic introduced here for updating collection metadata.

The creator is now able to do both: updating collection and NFT Info!

NEW CollectionMedata in cw721 package

#[cw_serde]
pub struct CollectionMetadata<TCollectionMetadataExtension> {
    pub name: String,
    pub symbol: String,
    pub extension: TCollectionMetadataExtension,
    pub updated_at: Timestamp,
}

#[cw_serde]
pub struct CollectionMetadataExtension<TRoyaltyInfo> {
    pub description: String,
    pub image: String,
    pub external_link: Option<String>,
    pub explicit_content: Option<bool>,
    pub start_trading_time: Option<Timestamp>,
    pub royalty_info: Option<TRoyaltyInfo>,
}

#[cw_serde]
pub struct RoyaltyInfo {
    pub payment_address: Addr,
    pub share: Decimal,
}

For making it available to all existing contracts, I had to move all core logic into cw721 package. As a result, outcome is the following:

  • there are now default implementations for the Cw721Execute and Cw721Query traits
  • all structs like CollectionMetadata, NftMetadata, RoyaltyInfo, NftInfo, OwnerOfResponse, ApprovalResponse, ... moved to package

Another upgrade is adding creator along to existing minter address. In previous release due to the introduction of cw-ownable there has been a renaming of minter to ownership - which is confusing. With upcoming v0.19 release it looks like this:

  • minter can mint (formerly aka ownership in v0.17-18)
  • there is no more ownership (deprecated)
  • creator as part of collection metadata, can update collection metadata - including royalty
    • NOTE: all props is owned by collection creator, except start_trading_time which belongs to minter
    • creator and minter ownership is checked in new trait StateFactory
  • both, minter and creator, uses cw-ownable and can be transferred to another owner

all stores are in state.rs (pls note new Cw721Config which will be used for contracts):

/// Creator owns this contract and can update collection metadata!
/// !!! Important note here: !!!
/// - creator is stored using using cw-ownable's OWNERSHIP singleton, so it is not stored here
/// - in release v0.18.0 it was used for minter (which is confusing), but now it is used for creator
pub const CREATOR: OwnershipStore = OwnershipStore::new(OWNERSHIP_KEY);
/// - minter is stored in the contract storage using cw_ownable::OwnershipStore (same as for OWNERSHIP but with different key)
pub const MINTER: OwnershipStore = OwnershipStore::new("collection_minter");

/// Default CollectionMetadataExtension using `Option<CollectionMetadataExtension<RoyaltyInfo>>`
pub type DefaultOptionCollectionMetadataExtension =
    Option<CollectionMetadataExtension<RoyaltyInfo>>;
pub type DefaultOptionCollectionMetadataExtensionMsg =
    Option<CollectionMetadataExtensionMsg<RoyaltyInfoResponse>>;
/// Default NftMetadataExtension using `Option<NftMetadata>`.
pub type DefaultOptionNftMetadataExtension = Option<NftMetadata>;
pub type DefaultOptionNftMetadataExtensionMsg = Option<NftMetadataMsg>;

// explicit type for better distinction.
pub type NftMetadataMsg = NftMetadata;
#[deprecated(since = "0.19.0", note = "Please use `NftMetadata` instead")]
pub type MetaData = NftMetadata;
#[deprecated(
    since = "0.19.0",
    note = "Please use `CollectionMetadata<DefaultOptionCollectionMetadataExtension>` instead"
)]
pub type ContractInfoResponse = CollectionMetadata<DefaultOptionCollectionMetadataExtension>;

pub struct Cw721Config<
    'a,
    // Metadata defined in NftInfo (used for mint).
    TNftMetadataExtension,
    // Message passed for updating metadata.
    TNftMetadataExtensionMsg,
    // Extension defined in CollectionMetadata.
    TCollectionMetadataExtension,
    // Message passed for updating collection metadata extension.
    TCollectionMetadataExtensionMsg,
    // Defines for `CosmosMsg::Custom<T>` in response. Barely used, so `Empty` can be used.
    TCustomResponseMsg,
> where
    TNftMetadataExtension: Cw721State,
    TNftMetadataExtensionMsg: Cw721CustomMsg,
    TCollectionMetadataExtension: Cw721State,
    TCollectionMetadataExtensionMsg: Cw721CustomMsg,
{
    /// Note: replaces deprecated/legacy key "nft_info"!
    pub collection_metadata: Item<'a, CollectionMetadata<TCollectionMetadataExtension>>,
    pub token_count: Item<'a, u64>,
    /// Stored as (granter, operator) giving operator full control over granter's account.
    /// NOTE: granter is the owner, so operator has only control for NFTs owned by granter!
    pub operators: Map<'a, (&'a Addr, &'a Addr), Expiration>,
    pub nft_info: IndexedMap<
        'a,
        &'a str,
        NftInfo<TNftMetadataExtension>,
        TokenIndexes<'a, TNftMetadataExtension>,
    >,
    pub withdraw_address: Item<'a, String>,
...
}

Essentially all boilerplate code (default implementations in Cw721Execute and Cw721Query traits) are now in cw721 package.
As a result contracts are very slim. Please note, implementations by Cw721Config, along with Cw721Execute and
Cw721Query traits are opionated - though contracts may be implement differently by overriding default implementations.

Here is how new cw721-base looks like by using cw721 package:

Define struct for contract interaction:

pub struct Cw721Contract<
    'a,
    // Metadata defined in NftInfo (used for mint).
    TNftMetadataExtension,
    // Message passed for updating metadata.
    TNftMetadataExtensionMsg,
    // Extension defined in CollectionMetadata.
    TCollectionMetadataExtension,
    TCollectionMetadataExtensionMsg,
    // Defines for `CosmosMsg::Custom<T>` in response. Barely used, so `Empty` can be used.
    TCustomResponseMsg,
> where
    TNftMetadataExtension: Cw721State,
    TNftMetadataExtensionMsg: Cw721CustomMsg,
    TCollectionMetadataExtension: Cw721State,
    TCollectionMetadataExtensionMsg: Cw721CustomMsg,
{
    pub config: Cw721Config<
        'a,
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    >,
}

impl<
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    > Default
    for Cw721Contract<
        'static,
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    >
where
    TNftMetadataExtension: Cw721State,
    TNftMetadataExtensionMsg: Cw721CustomMsg,
    TCollectionMetadataExtension: Cw721State,
    TCollectionMetadataExtensionMsg: Cw721CustomMsg,
{
    fn default() -> Self {
        Self {
            config: Cw721Config::default(),
        }
    }
}

Then implement execute and query traits and using default implementations:

// implements Cw721Execute
impl<
        'a,
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    >
    Cw721Execute<
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    >
    for Cw721Contract<
        'a,
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    >
where
    TNftMetadataExtension: Cw721State,
    TNftMetadataExtensionMsg: Cw721CustomMsg + StateFactory<TNftMetadataExtension>,
    TCollectionMetadataExtension: Cw721State,
    TCollectionMetadataExtensionMsg: Cw721CustomMsg + StateFactory<TCollectionMetadataExtension>,
    TCustomResponseMsg: CustomMsg,
{
}

// implements Cw721Query
impl<
        'a,
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    > Cw721Query<TNftMetadataExtension, TCollectionMetadataExtension>
    for Cw721Contract<
        'a,
        TNftMetadataExtension,
        TNftMetadataExtensionMsg,
        TCollectionMetadataExtension,
        TCollectionMetadataExtensionMsg,
        TCustomResponseMsg,
    >
where
    TNftMetadataExtension: Cw721State,
    TNftMetadataExtensionMsg: Cw721CustomMsg,
    TCollectionMetadataExtension: Cw721State,
    TCollectionMetadataExtensionMsg: Cw721CustomMsg,
    TCustomResponseMsg: CustomMsg,
{
}

And finally in lib.rs:

    // This makes a conscious choice on the various generics used by the contract
    #[cfg_attr(not(feature = "library"), entry_point)]
    pub fn instantiate(
        deps: DepsMut,
        env: Env,
        info: MessageInfo,
        msg: Cw721InstantiateMsg<DefaultOptionCollectionMetadataExtensionMsg>,
    ) -> Result<Response, Cw721ContractError> {
        let contract = Cw721Contract::<
            DefaultOptionNftMetadataExtension,
            DefaultOptionNftMetadataExtensionMsg,
            DefaultOptionCollectionMetadataExtension,
            DefaultOptionCollectionMetadataExtensionMsg,
            Empty,
        >::default();
        contract.instantiate(deps, &env, &info, msg, CONTRACT_NAME, CONTRACT_VERSION)
    }

    #[cfg_attr(not(feature = "library"), entry_point)]
    pub fn execute(
        deps: DepsMut,
        env: Env,
        info: MessageInfo,
        msg: Cw721ExecuteMsg<
            DefaultOptionNftMetadataExtensionMsg,
            DefaultOptionCollectionMetadataExtensionMsg,
        >,
    ) -> Result<Response, Cw721ContractError> {
        let contract = Cw721Contract::<
            DefaultOptionNftMetadataExtension,
            DefaultOptionNftMetadataExtensionMsg,
            DefaultOptionCollectionMetadataExtension,
            DefaultOptionCollectionMetadataExtensionMsg,
            Empty,
        >::default();
        contract.execute(deps, &env, &info, msg)
    }

    #[cfg_attr(not(feature = "library"), entry_point)]
    pub fn query(
        deps: Deps,
        env: Env,
        msg: Cw721QueryMsg<
            DefaultOptionNftMetadataExtension,
            DefaultOptionCollectionMetadataExtension,
        >,
    ) -> StdResult<Binary> {
        let contract = Cw721Contract::<
            DefaultOptionNftMetadataExtension,
            DefaultOptionNftMetadataExtensionMsg,
            DefaultOptionCollectionMetadataExtension,
            DefaultOptionCollectionMetadataExtensionMsg,
            Empty,
        >::default();
        contract.query(deps, &env, msg)
    }

    #[cfg_attr(not(feature = "library"), entry_point)]
    pub fn migrate(
        deps: DepsMut,
        env: Env,
        msg: Cw721MigrateMsg,
    ) -> Result<Response, Cw721ContractError> {
        let contract = Cw721Contract::<
            DefaultOptionNftMetadataExtension,
            DefaultOptionNftMetadataExtensionMsg,
            DefaultOptionCollectionMetadataExtension,
            DefaultOptionCollectionMetadataExtensionMsg,
            Empty,
        >::default();
        contract.migrate(deps, env, msg, CONTRACT_NAME, CONTRACT_VERSION)
    }

That's it!

cw721-metadata-onchain and cw2981-royalty contracts removed

Another positive side effect of this refactoring is that there's no need for cw721-metadata-onchain and cw2981-royalty contracts anymore. This is covered in cw721-base. Design hasnt changed and onchain metadata is stored in NftInfo<TNftMetadataExtension>. Before this PR, funny thing is that in cw721-base it was defined as:

TMetadataExtension = Option<Empty>, allowing to NOT store onchain data, but it doesnt make sense having None and Some(Empty).

In cw721-metadata-onchain contract it was defined as:

TMetadataExtension = Option<Metadata>, here for onchain contract it was allowed to either provide onchain data or event not!

In this PR it defines a pub type DefaultOptionNftMetadataExtension = Option<NftMetadata> which is used in cw721-base. onchain contract is obsolete and is removed.

Comment on lines 35 to 42
#[allow(deprecated)]
Cw721QueryMsg::ContractInfo {} => {
to_json_binary(&self.query_collection_info(deps, env)?)
}
Cw721QueryMsg::GetCollectionInfo {} => {
to_json_binary(&self.query_collection_info(deps, env)?)
}
Cw721QueryMsg::NftInfo { token_id } => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also ContractInfo is deprecated and is replaced by GetCollectionInfo. In next release all deprecated msgs should be removed. Here it is kept for backward compatibility, so existing frontends wont break!

Comment on lines 171 to 230
#[deprecated(since = "0.19.0", note = "Please use GetMinterOwnership instead")]
#[returns(Ownership<Addr>)]
Ownership {},

#[returns(Ownership<Addr>)]
GetMinterOwnership {},

#[returns(Ownership<Addr>)]
GetCreatorOwnership {},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ownership -> GetMinterOwnership
new GetCreatorOwnership

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments on these queries would be helpful. Good to note what they are in plain English and that minter will soon be deprecated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 213 to 215
#[deprecated(since = "0.19.0", note = "Please use GetMinterOwnership instead")]
#[returns(MinterResponse)]
Minter {},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also for backward compatibility: Minter -> GetMinterOwnership

Comment on lines 239 to 288
pub enum Cw721MigrateMsg {
WithUpdate {
minter: Option<String>,
creator: Option<String>,
},
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: new migrate msg allowing to reset creator and minter!

Comment on lines 18 to 32
pub trait Cw721Execute<T, C>
where
T: Serialize + DeserializeOwned + Clone,
C: CustomMsg,
{
type Err: ToString;

fn transfer_nft(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
recipient: String,
token_id: String,
) -> Result<Response<C>, Self::Err>;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before it e.g. Cw721Execute was rather an interface with no logic. Also note confusing generics naming like in Cw721Execute<T, C> and ...

Comment on lines 15 to 22
pub const CREATOR: OwnershipStore = OwnershipStore::new(OWNERSHIP_KEY);
/// - minter is stored in the contract storage using cw_ownable::OwnershipStore (same as for OWNERSHIP but with different key)
pub const MINTER: OwnershipStore = OwnershipStore::new("collection_minter");

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CREATOR and MINTER. Please note that singleton ownership was used before for minter, but now used for creator! new minter ownership has a new key collection_minter. This is also covered in migration (as shown later below).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay so to be clear, we now have an optional second owner which is creator. The max number of owners in the contract would now be 2 and not more than that right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay so to be clear, we now have an optional second owner which is creator. The max number of owners in the contract would now be 2 and not more than that right?

Correct. They are both optional: creator and minter. Naming convention before was very confusing - started with the introduction of cw-ownable. There it was minter = owner = ownership.

Imo, using terms creator and minter is much more clear. Ownership appears here only in terms of minter ownership and creator ownership. Both uses for 2-step for transfer as defined by cw-ownable:

  1. Owner transfers to new owner
  2. New owner accepts, on acceptance minter/creator ownership is changed

Comment on lines 702 to 667
/// Migrates only in case ownership is not present
/// !!! Important note here: !!!
/// - creator owns the contract and can update collection info
/// - minter can mint new tokens
///
/// Before v0.19.0 there were confusing naming conventions:
/// - v0.17.0: minter was replaced by cw_ownable, as a result minter is owner
/// - v0.16.0 and below: minter was stored in dedicated `minter` store (so NOT using cw_ownable at all)
pub fn migrate_legacy_minter_and_creator(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migrate_legacy_minter_and_creator: migrate legacy minter and set new creator...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@humanalgorithm here is the most important part, allowing to migrate any older version to the latest

packages/cw721/src/execute.rs Outdated Show resolved Hide resolved
contracts/cw721-base/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 7 to 19
pub fn migrate<T, C, E, Q>(deps: DepsMut) -> Result<Response<C>, ContractError>
where
T: Serialize + DeserializeOwned + Clone,
Q: CustomMsg,
E: CustomMsg,
{
// remove old minter info
let tract16 = v16::Cw721Contract::<T, C, E, Q>::default();
let minter = tract16.minter.load(deps.storage)?;
tract16.minter.remove(deps.storage);

// save new ownership info
let ownership = cw_ownable::initialize_owner(deps.storage, deps.api, Some(minter.as_str()))?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this old migrato logic of minter to ownership is error prone. Like it is e.g. possible to migrate from v0.16 to v0.18. Then minter is transferred to new addr. Keep in mind in old minter store there is still old addr. So migrating back to v0.16 and to v0.18 again, would reset minter addy. Better to always check whether new minter ownship is set - if so no migration needed.

This is an edge case, but still not good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We needed this for migration to v.01.7. I think I might have been the one that "wrote" it but I was just copying migration logic from cw base contract.

@taitruong taitruong changed the title WIP - Collection info Add new CollectionInfo, including RoyaltyInfo and creator (along with existing minter). Mar 7, 2024
Comment on lines 6 to 27
pub enum ContractError {
#[error(transparent)]
Std(#[from] StdError),

#[error(transparent)]
Ownership(#[from] OwnershipError),

#[error(transparent)]
Version(#[from] cw2::VersionError),

#[error("token_id already claimed")]
Claimed {},

#[error("Cannot set approval that is already expired")]
Expired {},

#[error("Approval not found for: {spender}")]
ApprovalNotFound { spender: String },

#[error("No withdraw address set")]
NoWithdrawAddress {},
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to cw721 package for better re-use

use schemars::JsonSchema;

#[cw_serde]
pub struct InstantiateMsg {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All msgs and response structs moved from cw721-base to cw721 for better re-use

}

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Metadata {
Copy link
Contributor

@humanalgorithm humanalgorithm Mar 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have some kind of flexible field here if we are putting all these attributes? I wonder if we would ever need more than what's listed here. Or is attributes flexible enough?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is flexible enough. There is Metadata.attributes which is a list of Traits:

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Trait {
    pub display_type: Option<String>,
    pub trait_type: String,
    pub value: String,
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it follows 100% ERC721 spec, using metadata and traits.

@humanalgorithm
Copy link
Contributor

What is "T" in this struct naming?

pub struct Cw721Config<
'a,
TMetadata,
TCustomResponseMessage,
TExtensionExecuteMsg,
TMetadataResponse,
TCollectionInfoExtension,

@taitruong
Copy link
Collaborator Author

What is "T" in this struct naming?

pub struct Cw721Config< 'a, TMetadata, TCustomResponseMessage, TExtensionExecuteMsg, TMetadataResponse, TCollectionInfoExtension,

It is a general convention for generics starting generally with "T". Has nothing to do with my name :P

Cheers, Mr T

Comment on lines 11 to 14
TMetadataExtension,
TCustomResponseMessage,
TExtensionExecuteMsg,
TCollectionInfoExtension,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much nicer with real trait names, been wanting this for a while. ❤️

Comment on lines 128 to 147
/// Deprecated: use GetCollectionInfo instead! Will be removed in next release!
ContractInfo {},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JakeHartnell, added docs for all deprecated query msgs and noting they'll be removed in next release.

Comment on lines 26 to 43
/// The creator is the only one eligible to update `CollectionInfo`.
UpdateCollectionInfo {
collection_info: CollectionInfoMsg<TCollectionInfoExtensionMsg>,
},
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JakeHartnell also note docs here

Comment on lines 80 to 146
fn proper_minter_and_creator_initialization() {
// case 1: sender is used in case minter and creator is not set
{
let mut deps = mock_dependencies();

let info_minter_and_creator = mock_info("minter_and_creator", &[]);
Cw721Contract::<
DefaultOptionMetadataExtension,
MetadataMsg,
DefaultOptionCollectionInfoExtension,
CollectionInfoExtensionMsg<RoyaltyInfo>,
Empty,
>::default()
.instantiate(
deps.as_mut(),
mock_env(),
info_minter_and_creator.clone(),
Cw721InstantiateMsg {
name: "collection_name".into(),
symbol: "collection_symbol".into(),
collection_info_extension: None,
creator: None,
minter: None,
withdraw_address: None,
},
"contract_name",
"contract_version",
)
.unwrap();

let minter = MINTER.item.load(deps.as_ref().storage).unwrap().owner;
assert_eq!(minter, Some(info_minter_and_creator.sender.clone()));
let creator = CREATOR.item.load(deps.as_ref().storage).unwrap().owner;
assert_eq!(creator, Some(info_minter_and_creator.sender));
}
// case 2: minter and creator are set
{
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit test for optional creator and minter on init

Comment on lines 91 to 97
let collection_info = CollectionInfo {
name: msg.name,
symbol: msg.symbol,
extension: msg.collection_info_extension,
updated_at: env.block.time,
};
collection_info.extension.validate()?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... collection info validation during instantiation and ...

if let Some(symbol) = msg.symbol {
collection_info.symbol = symbol;
}
collection_info.extension = collection_info.extension.update(&msg.extension)?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... update collection info during execution ...

Comment on lines 319 to 190
impl Update<CollectionInfoExtensionMsg<RoyaltyInfo>> for CollectionInfoExtension<RoyaltyInfo> {
fn update(
&self,
msg: &CollectionInfoExtensionMsg<RoyaltyInfo>,
) -> Result<Self, crate::error::Cw721ContractError> {
let mut extension = self.clone();
// validate royalty before updating
if let Some(royalty_info) = &extension.royalty_info {
royalty_info.validate(msg.royalty_info.clone())?;
}
extension.description = msg.description.clone().unwrap_or(self.description.clone());
extension.image = msg.image.clone().unwrap_or(self.image.clone());
extension.external_link = msg.external_link.clone().or(self.external_link.clone());
extension.explicit_content = msg.explicit_content.or(self.explicit_content);
extension.start_trading_time = msg.start_trading_time.or(self.start_trading_time);
extension.royalty_info = msg.royalty_info.clone().or(self.royalty_info.clone());
extension.validate()?;

Ok(extension)
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and in implementation for Update trait it does a validation. This way it is always guaranteed there is no invalid data. Custom contracts dont need to do this extra check anymore.

Comment on lines 193 to 203
impl Update<NftInfo<Metadata>> for NftInfo<Metadata> {
fn update(&self, msg: &NftInfo<Metadata>) -> Result<Self, crate::error::Cw721ContractError> {
msg.validate()?;
Ok(msg.clone())
}
}

impl<TMetadataExtension> Validate for NftInfo<TMetadataExtension>
where
TMetadataExtension: Validate,
{
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same applies for NftInfo and ...

Comment on lines 422 to 478
impl Validate for Metadata {
fn validate(&self) -> Result<(), Cw721ContractError> {
// check URLs
if let Some(image) = &self.image {
Url::parse(image)?;
}
if let Some(url) = &self.external_url {
Url::parse(url)?;
}
if let Some(animation_url) = &self.animation_url {
Url::parse(animation_url)?;
}
if let Some(youtube_url) = &self.youtube_url {
Url::parse(youtube_url)?;
}
// Strings must not be empty
if let Some(image_data) = &self.image_data {
if image_data.is_empty() {
return Err(Cw721ContractError::MetadataImageDataEmpty {});
}
}
if let Some(desc) = &self.description {
if desc.is_empty() {
return Err(Cw721ContractError::MetadataDescriptionEmpty {});
}
}
if let Some(name) = &self.name {
if name.is_empty() {
return Err(Cw721ContractError::MetadataNameEmpty {});
}
}
if let Some(background_color) = &self.background_color {
if background_color.is_empty() {
return Err(Cw721ContractError::MetadataBackgroundColorEmpty {});
}
}
// check traits
if let Some(attributes) = &self.attributes {
for attribute in attributes {
if attribute.trait_type.is_empty() {
return Err(Cw721ContractError::TraitTypeEmpty {});
}
if attribute.value.is_empty() {
return Err(Cw721ContractError::TraitValueEmpty {});
}
if let Some(display_type) = &attribute.display_type {
if display_type.is_empty() {
return Err(Cw721ContractError::TraitDisplayTypeEmpty {});
}
}
}
}
Ok(())
}
}

impl Update<MetadataMsg> for Metadata {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and Metadata.

Comment on lines 220 to 235
fn proper_collection_info_initialization() {
// case 1: extension set with proper data
{
let mut deps = mock_dependencies();
Copy link
Collaborator Author

@taitruong taitruong Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing update and validate for collection info during instantiation

Comment on lines 498 to 506
#[test]
fn proper_collection_info_update() {
// case 1: update with proper data
{
// initialize contract
let mut deps = mock_dependencies();
let env = mock_env();
let info = mock_info(CREATOR_ADDR, &[]);
let instantiated_extension = Some(CollectionInfoExtension {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing update and validate for collection info during execution

Comment on lines 1035 to 1354
#[test]
fn test_migrate() {
let mut deps = mock_dependencies();

let env = mock_env();
use cw721_base_016 as v16;
v16::entry::instantiate(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

migration test and especially how minter, creator, collection info looks like BEFORE and AFTER migration

Comment on lines 310 to 735
#[test]
fn mint_with_metadata() {
// case 1: mint with valid metadata
{
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());

let token_id = "1".to_string();
let token_uri = "ipfs://foo.bar".to_string();
let mint_msg = Cw721ExecuteMsg::Mint {
token_id: token_id.clone(),
owner: String::from("medusa"),
token_uri: Some(token_uri.clone()),
extension: Some(Metadata {
image: Some("ipfs://foo.bar/image.png".to_string()),
image_data: Some("image data".to_string()),
external_url: Some("https://github.com".to_string()),
description: Some("description".to_string()),
name: Some("name".to_string()),
attributes: Some(vec![Trait {
trait_type: "trait_type".to_string(),
value: "value".to_string(),
display_type: Some("display_type".to_string()),
}]),
background_color: Some("background_color".to_string()),
animation_url: Some("ssl://animation_url".to_string()),
youtube_url: Some("file://youtube_url".to_string()),
}),
};

let info_minter = mock_info(MINTER_ADDR, &[]);
let env = mock_env();
contract
.execute(deps.as_mut(), env.clone(), info_minter, mint_msg)
.unwrap();
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

happy path testing on contract level, where new traits Update and Validate are taken into account. So minting now checks new features and make sure e.g. URLs are correct, Strings are not empty - in case Some(string) is provided.

Comment on lines 346 to 764
// case 2: mint with invalid metadata
{
let mut deps = mock_dependencies();
let contract = setup_contract(deps.as_mut());

let token_id = "1".to_string();
let token_uri = "ipfs://foo.bar".to_string();
let info_minter = mock_info(MINTER_ADDR, &[]);
let env = mock_env();

let valid_metadata = Metadata {
image: Some("ipfs://foo.bar/image.png".to_string()),
image_data: Some("image data".to_string()),
external_url: Some("https://github.com".to_string()),
description: Some("description".to_string()),
name: Some("name".to_string()),
attributes: Some(vec![Trait {
trait_type: "trait_type".to_string(),
value: "value".to_string(),
display_type: Some("display_type".to_string()),
}]),
background_color: Some("background_color".to_string()),
animation_url: Some("ssl://animation_url".to_string()),
youtube_url: Some("file://youtube_url".to_string()),
};

// invalid image
let mut metadata = valid_metadata.clone();
metadata.image = Some("invalid".to_string());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deep happy path tests down to onchain metadata level! Like it checks whether metadata.image has an invalid URL!

@taitruong taitruong changed the title Add new CollectionInfo, including RoyaltyInfo and creator (along with existing minter). Add new CollectionMetadata, including RoyaltyInfo and creator (along with existing minter). Mar 18, 2024
Comment on lines 227 to 181
pub struct CollectionMetadata<TCollectionMetadataExtension> {
pub name: String,
pub symbol: String,
pub extension: TCollectionMetadataExtension,
pub updated_at: Timestamp,
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new CollectionInfo which replaces...

Comment on lines 120 to 122
pub struct ContractInfoResponse {
pub name: String,
pub symbol: String,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... ContractInfoResponse.

Comment on lines 21 to 45
pub trait Cw721Query<
// Metadata defined in NftInfo.
TNftMetadataExtension,
// Extension defined in CollectionMetadata.
TCollectionMetadataExtension,
> where
TNftMetadataExtension: Cw721State,
TCollectionMetadataExtension: Cw721State,
{
fn query(
&self,
deps: Deps,
env: &Env,
msg: Cw721QueryMsg<TNftMetadataExtension, TCollectionMetadataExtension>,
) -> StdResult<Binary> {
match msg {
#[allow(deprecated)]
Cw721QueryMsg::Minter {} => to_json_binary(&self.query_minter(deps.storage)?),
#[allow(deprecated)]
Cw721QueryMsg::ContractInfo {} => {
to_json_binary(&self.query_collection_metadata(deps, env)?)
}
Cw721QueryMsg::GetCollectionMetadata {} => {
to_json_binary(&self.query_collection_metadata(deps, env)?)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All logic from cw721-base move to trait in default implementations. Here with query entry point, used by contracts e.g. in lib.rs

Comment on lines 22 to 56
pub trait Cw721Execute<
// Metadata defined in NftInfo (used for mint).
TNftMetadataExtension,
// Message passed for updating metadata.
TNftMetadataExtensionMsg,
// Extension defined in CollectionMetadata.
TCollectionMetadataExtension,
// Message passed for updating collection metadata extension.
TCollectionMetadataExtensionMsg,
// Defines for `CosmosMsg::Custom<T>` in response. Barely used, so `Empty` can be used.
TCustomResponseMsg,
> where
TNftMetadataExtension: Cw721State,
TNftMetadataExtensionMsg: Cw721CustomMsg + StateFactory<TNftMetadataExtension>,
TCollectionMetadataExtension: Cw721State,
TCollectionMetadataExtensionMsg: Cw721CustomMsg + StateFactory<TCollectionMetadataExtension>,
TCustomResponseMsg: CustomMsg,
{
fn instantiate(
&self,
deps: DepsMut,
env: &Env,
info: &MessageInfo,
msg: Cw721InstantiateMsg<TCollectionMetadataExtensionMsg>,
contract_name: &str,
contract_version: &str,
) -> Result<Response<TCustomResponseMsg>, Cw721ContractError> {
cw2::set_contract_version(deps.storage, contract_name, contract_version)?;
let config = Cw721Config::<
TNftMetadataExtension,
TNftMetadataExtensionMsg,
TCollectionMetadataExtension,
TCollectionMetadataExtensionMsg,
TCustomResponseMsg,
>::default();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cw721Execute trait with entry points for instantiate, execute, query, and migrate. Also in both both traits specific calls define, like in...

Comment on lines 184 to 199
fn transfer_nft(
&self,
deps: DepsMut,
env: &Env,
info: &MessageInfo,
recipient: String,
token_id: String,
) -> Result<Response<TCustomResponseMsg>, Cw721ContractError> {
_transfer_nft::<TNftMetadataExtension>(deps, env, info, &recipient, &token_id)?;

Ok(Response::new()
.add_attribute("action", "transfer_nft")
.add_attribute("sender", info.sender.to_string())
.add_attribute("recipient", recipient)
.add_attribute("token_id", token_id))
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... transfer_nft default implementation.

Comment on lines 387 to 390
let collection_metadata = msg.create(deps.as_ref(), env, info, Some(&current))?;
config
.collection_metadata
.save(deps.storage, &collection_metadata)?;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... where in update_collection_metadata where on msg, a single line creates a new, updated state:

let collection_metadata = msg.create(deps.as_ref(), env, info, Some(&current))?;

And create() in StateFactory does a deep creation and validation: ...

Comment on lines 298 to 304
if self.name.is_some() && self.name.clone().unwrap().is_empty() {
return Err(Cw721ContractError::CollectionNameEmpty {});
}
if self.symbol.is_some() && self.symbol.clone().unwrap().is_empty() {
return Err(Cw721ContractError::CollectionSymbolEmpty {});
}
Ok(())
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... here in validate() in makes sure that name and symbol in CollectionMetadata is not empty.

Comment on lines 241 to 255
fn create(
&self,
deps: Deps,
env: &cosmwasm_std::Env,
info: &cosmwasm_std::MessageInfo,
current: Option<&CollectionMetadata<TCollectionMetadataExtension>>,
) -> Result<CollectionMetadata<TCollectionMetadataExtension>, Cw721ContractError> {
self.validate(deps, env, info, current)?;
match current {
// Some: update existing metadata
Some(current) => {
let mut updated = current.clone();
if let Some(name) = &self.name {
updated.name = name.clone();
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... then in create of StateFactory, it:

  • validates msg first,
  • uses current state and
  • update it with msg data

... then it goes one level deeper, ...

Comment on lines 259 to 264
let current_extension = current.extension.clone();
let updated_extension =
self.extension
.create(deps, env, info, Some(&current_extension))?;
updated.extension = updated_extension;
Ok(updated)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and creates CollectionMedataExtension, including validation of optional props like description, royalties, start trading time, etc.

Comment on lines 365 to 500
fn validate(
&self,
deps: Deps,
_env: &Env,
info: &MessageInfo,
_current: Option<&CollectionMetadataExtension<RoyaltyInfo>>,
) -> Result<(), Cw721ContractError> {
// start trading time can only be updated by minter
let minter_initialized = MINTER.item.may_load(deps.storage)?;
if self.start_trading_time.is_some()
&& minter_initialized.is_some()
&& MINTER.assert_owner(deps.storage, &info.sender).is_err()
{
return Err(Cw721ContractError::NotMinter {});
}
// all other props collection metadata extension can only be updated by the creator
let creator_initialized = CREATOR.item.may_load(deps.storage)?;
if (self.description.is_some()
|| self.image.is_some()
|| self.external_link.is_some()
|| self.explicit_content.is_some()
|| self.royalty_info.is_some())
&& creator_initialized.is_some()
&& CREATOR.assert_owner(deps.storage, &info.sender).is_err()
{
return Err(Cw721ContractError::NotCollectionCreator {});
}
// check description length, must not be empty and max 512 chars
if let Some(description) = &self.description {
if description.is_empty() {
return Err(Cw721ContractError::CollectionDescriptionEmpty {});
}
if description.len() > MAX_COLLECTION_DESCRIPTION_LENGTH as usize {
return Err(Cw721ContractError::CollectionDescriptionTooLong {
max_length: MAX_COLLECTION_DESCRIPTION_LENGTH,
});
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pls also note that StateFactory can also do complex validation. Like here in CollectionMetadataExtension:

  • start_trading_time can only be updated by Minter
  • all other probs belongs to and can only be updated by Creator 😎

In short: all state-based logic can be handled in StateFactory.

@jhernandezb @humanalgorithm @JakeHartnell

@taitruong taitruong merged commit 8009afb into public-awesome:main Aug 11, 2024
7 checks passed
@taitruong taitruong deleted the collection-info branch August 11, 2024 02:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants