public-awesome / cw-nfts

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

Use message composition to extend contracts #94

Closed larry0x closed 1 year ago

larry0x commented 1 year ago

This is an alternative approach to #42 and #91 on how to extend the CW-721 contract.

There are three approaches on how a custom NFT contract can inherit/extend the base cw721 contract:

Comparison

In this section, we use an example to compare the three approaches.

Terminology: Let's say the contract being extended is the "parent" contract, while the contract extending it is the "child" contract.

For this example, we use cw721-base as the parent, and cw2981-royalties as the child. cw2981 wants to extend cw721-base's query message by adding a custom method, royalty_info.

42

cw721-base/src/msg.rs

pub enum QueryMsg<Q> {
    ContractInfo { .. },
    NumTokens { .. },
    // ...

    // the wildcard extension
    Extension { query: Q },
}

cw2981-royalties/src/msg.rs

// the child defines its custom query methods as an enum...
pub enum CustomQuery {
    RoyaltyInfo {
        token_id: String,
        sale_price: Uint128,
    },
}

// ...and plug it into the parent enum as a generic
pub type QueryMsg = cw721_base::msg::QueryMsg<CustomQuery>;

cw2981-royalties/src/contract.rs

pub fn Query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        // if it is the extension, dispatch to the appropriate handler function
        QueryMsg::Extension { custom_query } => {
            match custom_query {
                CustomQuery::RoyaltyInfo { token_id, sale_price } => {
                    query_royalty_info(deps, token_id, sale_price)
                }
            }
        },

        // otherwise, dispatch it to the parent
        _ => {
            let parent = Cw721Contract::<Extension, CustomMsg, CustomQuery>::default();
            parent.query(deps, env, msg)
        },
    }
}

90

cw721-base/src/msg.rs

// the parent defines a macro to "autofill" its query methods
#[proc_macro_attribute]
pub fn cw721_base_query(metadata: TokenStream, input: TokenStream) -> TokenStream {
    // ...
}

// the parent's message type
#[cw721_base_query]
#[cw_serde]
pub enum QueryMsg {}

cw2981-royalties/src/msg.rs

// the child applies the macro, and appends its custom methods below
#[cw721_base_query]
#[cw_serde]
pub enum QueryMsg {
    RoyaltyInfo {
        token_id: String,
        sale_price: Uint128,
    },
}

// the child also needs to implement a function to cast the custom message
// to the parent's one
impl TryFrom<QueryMsg> for cw721_base::QueryMsg {
    // ...
}

cw2981-royalties/src/contract.rs

pub fn Query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        // if it is the custom variant, dispatch to the appropriate handler function
        QueryMsg::RoyaltyInfo { token_id, sale_price } => {
            query_royalty_info(deps, token_id, sale_price)
        },

        // otherwise, try cast the message to the parent type, and dispatch to parent
        _ => {
            let parent = Cw721Contract::<Extension>::default();
            parent.query(deps, env, msg.try_into()?)
        },
    }
}

This PR

The parent does not provide any special arrangement for the child.

cw721-base/src/msg.rs

// the child defines its enum "nesting" the parent one
// note the use of serde "untagged" option
#[serde(untagged)]
pub enum QueryMsg {
    Parent(cw721_base::msg::QueryMsg),

    RoyaltyInfo {
        token_id: String,
        sale_price: Uint128,
    },
}

cw2981-royalties/src/contract.rs

pub fn Query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Parent(msg) => {
            let parent = Cw721Contract::<Extension>::default();
            parent.query(deps, env, msg)
        },
        QueryMsg::RoyaltyInfo { token_id, sale_price } => {
            query_royalty_info(deps, token_id, sale_price)
        },
    }
}

Overall, I feel this PR is the best solution

larry0x commented 1 year ago

It seems for some reason, using #[serde(untagged)] (the approach suggested by this PR) introduces float operators into the wasm build. ??

AmitPr commented 1 year ago

@larry0x I had this issue a while back. It's because serde includes a debug/log function that handles an f64 value in one of the match arms. You can get around it by writing a custom serializer.

larry0x commented 1 year ago

@larry0x I had this issue a while back. It's because serde includes a debug/log function that handles an f64 value in one of the match arms. You can get around it by writing a custom serializer.

Yes this is a known issue: https://github.com/CosmWasm/serde-json-wasm/issues/43