public-awesome / cw-nfts

Examples and helpers to build NFT contracts on CosmWasm
Apache License 2.0
186 stars 179 forks source link

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

Open taitruong opened 4 months ago

taitruong commented 4 months ago

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:

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:

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.

humanalgorithm commented 4 months ago

What is "T" in this struct naming?

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

taitruong commented 4 months ago

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

taitruong commented 3 weeks ago

cw2981-royalties are back here: https://github.com/webmaster128/cw-nfts/pull/156/files#diff-4607667b8c5481b764c0ae592e412fdd07bc312b64d4f553836148900492ee6a

no big changes, just renamings and moving Cw2981Contract to state.rs

@shanev @humanalgorithm

taitruong commented 7 hours ago

Approved. Just one minor naming comment.

I do miss the simplicity of earlier versions of cw721. I'd like to see a lite version at some point that has no extensions and no options, just token_id and a uri, so a new dev can get started without knowing about all the options.

+1! We should have a cw721-lite. To that extend we should consider using Sylvia. Like:

EDIT: maybe cw721-lite should rather be a package for devs, and cw721-base uses lite package e.g. with no nft extension/onchain metadata, but collection extensions (royalties).