Writing & Deploying CW721 Contract

In the previous sections, we have covered setting up the development environment, configuring your wallet, and obtaining testnet OM tokens for development purposes. We have also walked through the steps for writing and deploying CW20 contracts on MANTRA Chain. Now, we will shift our focus to writing NFT CosmWasm contracts.

Overview

Before diving into the steps of creating an NFT on MANTRA Chain, let's understand the contract structure and flow:

  1. CW-721 Base Contract: All NFT data is stored and instantiated here. This contract handles the creation and management of individual NFTs.

  2. CW-721 Factory Contract: Facilitates the creation of multiple NFT instances. Deployed once, it handles the heavy lifting for NFT creation.

Prerequisites

If you encounter any issues with your development environment setup or need to refresh your setup, follow these steps:

  1. Install Rust

First, you need to install Rust, a programming language used for developing smart contracts. You can do this by running the following command in your terminal:

curl --proto '=https' --tlsv1.2 <https://sh.rustup.rs> -sSf | sh

When prompted, choose the first option for standard installation. This will install Rust and its package manager, Cargo.

  1. Set Up Rust and Cargo

After installing Rust, set up Rust to use the stable version and verify the installation:

rustup default stable

cargo version

rustup target add wasm32-unknown-unknown

This ensures that you have the latest stable version of Rust and adds the WebAssembly target required for smart contract development.

  1. Install cargo-generate

cargo-generate is a tool that helps you to generate a new Rust project from a template. Install it using the following command:

cargo install cargo-generate --features vendored-openssl
  1. Install mantrachaind

Download and extract the pre-built mantrachaind binary:

On Linux:

# Download the CLI
curl -LO <https://github.com/MANTRA-Finance/public/raw/main/mantrachain-testnet/mantrachaind-linux-amd64.zip>

# Unzip the CLI
unzip mantrachaind-linux-amd64.zip

On MacOS:

# Download the CLI for Intel chips
curl -LO <https://github.com/MANTRA-Finance/public/raw/main/mantrachain-hongbai/mantrachaind-static-darwin-amd64.tar.gz>

# Download the CLI for Silicon chips (M1, M2...)
curl -LO <https://github.com/MANTRA-Finance/public/raw/main/mantrachain-hongbai/mantrachaind-static-darwin-arm64.tar.gz>

# Extract the CLI
tar -xzvf mantrachaind-static-darwin-*.tar.gz

If you encounter a missing libwasmvm.x86_64.so error, download the library:

sudo wget -P /usr/lib <https://github.com/CosmWasm/wasmvm/releases/download/v2.1.0/libwasmvm.x86_64.so>

For more details on setting up the development environment, check the section Install Prerequisites.

Getting Started with NFT CosmWasm Contract

Now that your development environment is set up, let's begin with a boilerplate codebase for NFT CosmWasm contracts. It contains a starting template for CW721 NFT contracts tailored for deployment on MANTRA Chain.

Use any your preferred code editor, such as Visual Studio Code. Open its terminal,then clone the following repository:

git clone <https://github.com/MANTRA-Finance/cw721_contract_sample.git>

Open the cloned project in VS Code to proceed with customizing and building your NFT contract.

Adding Dependencies

Navigate to Cargo.toml in your project and add the following dependencies under the [dependencies] section:

# ...

[dependencies]
cw721-base = { version = "0.18.0", features = ["library"] }
cw-utils = "1.0.3"
cosmwasm-std = "1.5.7"
cw-storage-plus = "1.2.0"
cw2 = "1.1.2"
schemars = "0.8.10"
serde = { version = "1.0.207", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.63" }
cosmwasm-schema = "2.1.3"
prost = "0.13.1"

# ...

State Definition (state.rs)

Navigate to the src folder where your contract's source code resides.

In your state.rs file, define the initial state structure for your NFT contract:

use cosmwasm_schema::cw_serde;

use cw721_base::Extension;

use cosmwasm_std::{Addr, Uint128};
use cw_storage_plus::Item;

#[cw_serde]
pub struct Config {
    pub owner: Addr,
    pub cw20_address: Addr,
    pub cw721_address: Option<Addr>,
    pub max_tokens: u32,
    pub unit_price: Uint128,
    pub name: String,
    pub symbol: String,
    pub token_uri: String,
    pub extension: Extension,
    pub unused_token_id: u32,
}

pub const CONFIG: Item<Config> = Item::new("config");

Instantiating Your NFT CosmWasm Contract

When deploying contracts on the chain, they must be instantiated. This step involves initializing the contract's state and linking it with a CW721 contract to handle NFTs. You will create a Config struct to store the necessary information and define the logic for submessages and handling replies. This ensures that our contract can interact seamlessly with the CW721 contract.

The following code should be added to src/contract.rs to handle the instantiation and reply logic. The instantiate function sets up the initial configuration and sends a submessage to instantiate the CW721 contract. The reply function processes the response from this submessage, linking the CW721 contract to our main contract.

Add the following code in src/contract.rs:

use std::marker::PhantomData;

use cosmwasm_std::{ensure, entry_point, to_json_binary};
use cosmwasm_std::{
    Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, ReplyOn, Response, StdResult,
    SubMsg, Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw721_base::helpers::Cw721Contract;
use cw721_base::{ExecuteMsg as Cw721ExecuteMsg, Extension, InstantiateMsg as Cw721InstantiateMsg};
use cw_utils::{must_pay, parse_reply_instantiate_data};

use crate::error::ContractError;
use crate::msg::{ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
use crate::state::{Config, CONFIG};

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:aura-nft";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");

const INSTANTIATE_TOKEN_REPLY_ID: u64 = 1;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    ensure!(
        msg.unit_price.amount > Uint128::zero(),
        ContractError::InvalidUnitPrice {}
    );
    ensure!(msg.max_tokens > 0, ContractError::InvalidMaxTokens {});

    let config = Config {
        cw721_address: None,
        unit_price: msg.unit_price,
        max_tokens: msg.max_tokens,
        owner: info.sender,
        name: msg.name.clone(),
        symbol: msg.symbol.clone(),
        token_uri: msg.token_uri.clone(),
        extension: msg.extension.clone(),
        unused_token_id: 0,
    };

    CONFIG.save(deps.storage, &config)?;

    let sub_msg: Vec<SubMsg> = vec![SubMsg {
        msg: WasmMsg::Instantiate {
            code_id: msg.token_code_id,
            msg: to_json_binary(&Cw721InstantiateMsg {
                name: msg.name.clone(),
                symbol: msg.symbol,
                minter: env.contract.address.to_string(),
            })?,
            funds: vec![],
            admin: None,
            label: String::from("Instantiate fixed price NFT contract"),
        }
        .into(),
        id: INSTANTIATE_TOKEN_REPLY_ID,
        gas_limit: None,
        reply_on: ReplyOn::Success,
    }];

    Ok(Response::new().add_submessages(sub_msg))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
    let mut config: Config = CONFIG.load(deps.storage)?;

    ensure!(
        config.cw721_address == None,
        ContractError::Cw721AlreadyLinked {}
    );
    ensure!(
        msg.id == INSTANTIATE_TOKEN_REPLY_ID,
        ContractError::InvalidTokenReplyId {}
    );

    let reply = parse_reply_instantiate_data(msg).unwrap();
    config.cw721_address = Addr::unchecked(reply.contract_address).into();
    CONFIG.save(deps.storage, &config)?;

    Ok(Response::new())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    match msg {
        //No migrate implemented
    }
}

Add the following code in src/msg.rs:

use cosmwasm_schema::{cw_serde, QueryResponses};

use cosmwasm_std::{Addr, Binary, Coin, Uint128};
use cw721_base::Extension;

#[cw_serde]
pub struct InstantiateMsg {
    pub owner: String,
    pub max_tokens: u32,
    pub unit_price: Coin,
    pub name: String,
    pub symbol: String,
    pub token_code_id: u64,
    pub token_uri: String,
    pub extension: Extension,
}

Writing the Execution Logic

Now let's define the ExecuteMsg in src/msg.rs:

#[cw_serde]
pub enum ExecuteMsg {
    Mint,
}

#[cw_serde]
pub enum MigrateMsg {}

Next, integrate the execution logic into src/contract.rs, below the handling replies contract code. This includes the execute function, which handles incoming messages, and the execute_receive function, which processes transactions and mints NFTs.


/// Handling contract execution
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Mint => execute_mint(deps, info),
    }
}

pub fn execute_mint(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    if config.cw721_address == None {
        return Err(ContractError::Uninitialized {});
    }

    if config.unused_token_id >= config.max_tokens {
        return Err(ContractError::SoldOut {});
    }

    let amount = must_pay(&info, &config.unit_price.denom)?;

    if amount != config.unit_price.amount {
        return Err(ContractError::WrongPaymentAmount {});
    }

    let mint_msg = Cw721ExecuteMsg::<Extension, Empty>::Mint {
        token_id: config.unused_token_id.to_string(),
        owner: info.sender.into_string(),
        token_uri: config.token_uri.clone().into(),
        extension: config.extension.clone(),
    };

    match config.cw721_address.clone() {
        Some(cw721) => {
            let callback =
                Cw721Contract::<Empty, Empty>(cw721, PhantomData, PhantomData).call(mint_msg)?;
            config.unused_token_id += 1;
            CONFIG.save(deps.storage, &config)?;

            Ok(Response::new().add_message(callback))
        }
        None => Err(ContractError::Cw721NotLinked {}),
    }
}

Here, the execute function dispatches incoming messages to the appropriate handler. The execute_mint function accepts a native coin, checks it against the configuration set on instantiation, and if the conditions for minting a NFT are right, sends a mint message to the linked CW721 contract. The callback used here is a CosmosMsg message to trigger the mint function on the CW721 contract linked to our contract after instantiation.

Error Handling Logic

In this step, you will delete the src/helpers.rs file from our project setup. You will also create an errors.rs file in the src folder if it doesn't already exist. This file will handle the error logic for running our contracts.

Create the src/errors.rs file and add the following code:

use cosmwasm_std::StdError;
use cw_utils::PaymentError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Custom Error val: {val:?}")]
    CustomError { val: String },

    #[error("InvalidUnitPrice")]
    InvalidUnitPrice {},

    #[error("InvalidMaxTokens")]
    InvalidMaxTokens {},

    #[error("InvalidTokenReplyId")]
    InvalidTokenReplyId {},

    #[error("Cw721AlreadyLinked")]
    Cw721AlreadyLinked {},

    #[error("SoldOut")]
    SoldOut {},

    #[error("UnauthorizedTokenContract")]
    UnauthorizedTokenContract {},

    #[error("Uninitialized")]
    Uninitialized {},

    #[error("WrongPaymentAmount")]
    WrongPaymentAmount {},

    #[error("Cw721NotLinked")]
    Cw721NotLinked {},

    #[error("{0}")]
    PaymentError(#[from] PaymentError),
}

Writing Query Logic

Querying is an essential part of contract interactions, allowing you to retrieve data stored on the blockchain. In this step, we'll add the necessary query logic to our contract.

Add the following code to msg.rs:

#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
    #[returns(ConfigResponse)]
    GetConfig {},
}

#[cw_serde]
pub struct ConfigResponse {
    pub owner: Addr,
    pub cw721_address: Option<Addr>,
    pub max_tokens: u32,
    pub unit_price: Coin,
    pub name: String,
    pub symbol: String,
    pub token_uri: String,
    pub extension: Extension,
    pub unused_token_id: u32,
}

Add the following code to contract.rs:


#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetConfig {} => to_json_binary(&query_config(deps)?),
    }
}

fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
    let config = CONFIG.load(deps.storage)?;
    Ok(ConfigResponse {
        owner: config.owner,
        cw721_address: config.cw721_address,
        max_tokens: config.max_tokens,
        unit_price: config.unit_price,
        name: config.name,
        symbol: config.symbol,
        token_uri: config.token_uri,
        extension: config.extension,
        unused_token_id: config.unused_token_id,
    })
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR};
    use cosmwasm_std::{from_json, to_json_binary, Coin, SubMsgResponse, SubMsgResult};
    use prost::Message;

    use super::*;

    const NFT_CONTRACT_ADDR: &str = "nftcontract";

    #[derive(Clone, PartialEq, Message)]
    struct MsgInstantiateContractResponse {
        #[prost(string, tag = "1")]
        pub contract_address: ::prost::alloc::string::String,
        #[prost(bytes, tag = "2")]
        pub data: ::prost::alloc::vec::Vec<u8>,
    }

    #[test]
    fn initialization() {
        let mut deps = mock_dependencies();
        let msg = InstantiateMsg {
            owner: "owner".to_string(),
            max_tokens: 1,
            unit_price: Coin {
                denom: "uom".to_string(),
                amount: Uint128::one(),
            },
            name: String::from("FirstFT"),
            symbol: String::from("FFT"),
            token_code_id: 10u64,
            token_uri: String::from("<https://ipfs.io/ipfs/Q>"),
            extension: None,
        };

        let info = mock_info("owner", &[]);
        let res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg.clone()).unwrap();

        instantiate(deps.as_mut(), mock_env(), info, msg.clone()).unwrap();

        assert_eq!(
            res.messages,
            vec![SubMsg {
                msg: WasmMsg::Instantiate {
                    code_id: msg.token_code_id,
                    msg: to_json_binary(&Cw721InstantiateMsg {
                        name: msg.name.clone(),
                        symbol: msg.symbol.clone(),
                        minter: MOCK_CONTRACT_ADDR.to_string(),
                    })
                    .unwrap(),
                    funds: vec![],
                    admin: None,
                    label: String::from("Instantiate fixed price NFT contract"),
                }
                .into(),
                id: INSTANTIATE_TOKEN_REPLY_ID,
                gas_limit: None,
                reply_on: ReplyOn::Success,
            }]
        );

        let instantiate_reply = MsgInstantiateContractResponse {
            contract_address: "nftcontract".to_string(),
            data: vec![2u8; 32769],
        };
        let mut encoded_instantiate_reply =
            Vec::<u8>::with_capacity(instantiate_reply.encoded_len());
        instantiate_reply
            .encode(&mut encoded_instantiate_reply)
            .unwrap();

        let reply_msg = Reply {
            id: INSTANTIATE_TOKEN_REPLY_ID,
            result: SubMsgResult::Ok(SubMsgResponse {
                events: vec![],
                data: Some(encoded_instantiate_reply.into()),
            }),
        };
        reply(deps.as_mut(), mock_env(), reply_msg).unwrap();

        let query_msg = QueryMsg::GetConfig {};
        let res = query(deps.as_ref(), mock_env(), query_msg).unwrap();
        let config: Config = from_json(&res).unwrap();
        assert_eq!(
            config,
            Config {
                owner: Addr::unchecked("owner"),
                cw721_address: Some(Addr::unchecked(NFT_CONTRACT_ADDR)),
                max_tokens: msg.max_tokens,
                unit_price: msg.unit_price,
                name: msg.name,
                symbol: msg.symbol,
                token_uri: msg.token_uri,
                extension: None,
                unused_token_id: 0,
            }
        );
    }
}

In this code, the query function handles incoming query messages and dispatches them to the appropriate handler. The query_config function retrieves the contract's configuration data from storage and returns it as a ConfigResponse.

You have successfully written a complete NFT CosmWasm contract . This contract can be deployed on MANTRA Chain or any other Cosmos-based chain. It includes functionalities for initialization, execution, and querying, enabling the minting and management of NFTs.

Make sure to thoroughly test your contract on a testnet before deploying it on the Hongbai Testnet. Customize the contract further to meet your specific requirements and ensure it fits your project's needs.

Step 4: Build Artifacts and Wasm file

To build the artifact, we need to run the rust-optimizer with docker:

docker run --rm -v "$(pwd)":/code \\
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \\
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \\
  cosmwasm/optimizer:0.16.0

Or if you are on MacOS:

docker run --rm -v "$(pwd)":/code \\
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \\
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \\
  cosmwasm/optimizer-arm64:0.16.0

This command generates a wasm file moni.wasm under the artifacts folder.

Step 5: Deploy the contract

We are going to deploy our contract using the mantrachaind cli:

RES=$(mantrachaind tx wasm store artifacts/moni.wasm --from <wallet> --node <https://rpc.hongbai.mantrachain.io:443> --chain-id mantra-hongbai-1 --gas-prices 0.35uom --gas auto --gas-adjustment 1.4 -y --output json)
TX_HASH=$(echo $RES | jq -r .txhash)
CODE_ID=$(mantrachaind query tx $TX_HASH --node <https://rpc.hongbai.mantrachain.io:443> -o json| jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo $CODE_ID

Once we get the code_id after storing the contract on chain, we can create an instance of the contract like this:

MSG='{
  "owner": "", // add your wallet
  "max_tokens": 10000,
  "unit_price": {
    "denom": "uom",
    "amount": "1000"
  },
  "name": "Mantra NFT",
  "symbol": "MANTRANFT",
  "token_code_id": 507, // the code id of a cw721_base contract
  "token_uri": "<https://ipfs.io/ipfs/QmZ>"
}'

mantrachaind tx wasm instantiate <code_id> "$MSG" --from <wallet> --node <https://rpc.hongbai.mantrachain.io:443> --chain-id mantra-hongbai-1 --label "MANTRAcw20" --no-admin --gas-prices 0.35uom --gas auto --gas-adjustment 1.4 -y --output json

Note that the token_code_id value on the InstantiateMsg needs to be a valid cw721_base contract code_id. Feel free to compile and deploy your own from the cw-nfts repo, but you can reuse the code_id 507 which is one we have deployed on Hongbai Testnet.

This will produce an output like the following:

gas estimate: 217659
{"height":"0","txhash":"BB3A0FEF8370B0A407D8AB93919212A4E39E8BA4D52D84836E2E27D76F707D0A","codespace":"","code":0,"data":"","raw_log":"[]","logs":[],"info":"","gas_wanted":"0","gas_used":"0","tx":null,"timestamp":"","events":[]}

We can get the contract address by exploring the transaction hash using jq:

hamantrachaind q tx BB3A0FEF8370B0A407D8AB93919212A4E39E8BA4D52D84836E2E27D76F707D0A --node <https://rpc.hongbai.mantrachain.io:443> -o json | jq -r '.logs[] | .events[] | select(.type == "instantiate") | .attributes[] | select(.key == "_contract_address") | .value'

Once deployed, you will receive the wallet address and confirmation of contract instantiation. Verify your deployment on the MANTRA Chain Explorer athttp://explorer.hongbai.mantrachain.io.

Happy Coding!

Last updated