public-awesome / cw-nfts

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

Define a macro for extending CW721 execute/query messages #90

Open larry0x opened 1 year ago

larry0x commented 1 year ago

The problem

cw721::ExecuteMsg defines a minimum set of execute methods that all CW-721 variants are expected to implement. However, projects often want to extend it by adding their own methods. For example, cw721-base adds "mint" and "burn" methods. The process for adding these methods is rather cumbersome, for example:

// cw721-base/src/msg.rs
pub enum Cw721ExecuteMsg {
    // below are all copy-pasted from cw721::ExecuteMsg
    TransferNft { .. },
    SendNft { .. },
    Approve { .. },
    Revoke { .. },
    ApproveAll { .. },

    // only the following are new methods added by cw721-base
    RevokeAll { .. },
    Burn { .. }
}

Imo, there are two drawbacks with this approach:

Proposed solution

@0xekez showed me it is possible to "autofill" an enum with some pre-defined variants by a macro, which is an approach already used by DAO DAO: https://github.com/DA0-DA0/dao-contracts/blob/main/packages/cwd-macros/src/lib.rs

I propose we create a #[cw721_execute] macro that if used the following way:

use cw721::cw721_execute;

#[cw721_execute]
enum ExecuteMsg {
    Foo,
    Bar,
}

Will transform the enum to:

#[cw_serde]
enum ExecuteMsg {
    TransferNft { .. },
    SendNft { .. },
    Approve { .. },
    Revoke { .. },
    ApproveAll { .. },
    Foo,
    Bar,
}

This prevents the developer to have to copy all the CW721 enum variants over, and enforces all variants are included. Additionally, #[cw_serde] is automatically applied.

In the cw721 crate, the execute message can simply be defined as:

#[cw721_execute]
pub enum ExecuteMsg {}

Further suggestion

It'd be even better if the following traits can be automatically implemented as well:

impl From<cw721::ExecuteMsg> for ExecuteMsg {
    fn from(msg: cw721::ExecuteMsg) -> Self {
        match msg {
            cw721::ExecuteMsg::TransferNft { .. } => Self::TransferNft { .. },
            // ...
        }
    }
}

impl TryFrom<ExecuteMsg> for cw721::ExecuteMsg {
    type Error = String; // or other error type we prefer
    fn try_from(msg: ExecuteMsg) -> Result<Self, Self::Error> {
        match msg {
            ExecuteMsg::TransferNft { .. } => Ok(cw721::ExecuteMsg::TransferNft { .. }),
            // ...
            ExecuteMsg::Foo => Err("`cw721::ExecuteMsg` does not have variant `Foo`".to_string()),
            ExecuteMsg::Bar => Err("`cw721::ExecuteMsg` does not have variant `Bar`".to_string()),
        }
    }
}

This makes it much easier to implement execute entry point:

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        // For custom execute methods, dispatch to the appropriate
        // handler functions
        ExecuteMsg::Foo => execute_foo(deps, env, info),
        ExecuteMsg::Bar => execute_bar(deps, env, info),

        // For generic cw721 execute methods,
        // instead of writing a bunch of matching statements,
        // simply cast `ExecuteMsg` into the parent message type
        // and dispatch to the parent contract.
        msg => {
            let parent = Cw721Contract::<Empty, Empty>::default();
            parent.execute(deps, env, info, msg.try_into()?)
        },
    }
}

Notes

JakeHartnell commented 1 year ago

Love this. :heart:

Everyone hates the extension pattern, would allow us to simplify a lot of stuff. :joy:

shanev commented 1 year ago

This greatly simplifies things for the developer that knows what's going on under the hood. But what about a noob dev that doesn't?

webmaster128 commented 1 year ago

Great to see this solution by @0xekez / the DAO DAO team. Some thoughts: