Skip to main content

Instantiating a Contract

When contracts are stored on the chain they must be instantiated. I cover storing contracts on a chain in a later section. Instantiating a contract is like creating an object in other languages, however, it is achieved by a special message. This message is an InstantiateMsg located under src/lib.rs. Let's add something to it!

The InstantiateMsg

When instantiating our NFT contract, we need to specify most of the fields in the Config struct defined in the State chapter.

Go to file src/msg.rs and modify InstantiateMsg looks like this:

src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct InstantiateMsg {
pub owner: Addr,
pub max_tokens: u32,
pub unit_price: Uint128,
pub name: String,
pub symbol: String,
pub token_code_id: u64,
pub cw20_address: Addr,
pub token_uri: String,
pub extension: Extension,
}

The mission of these fields are explained in the previous chapter. There is only one field to specify, it is token_code_id. We already know that, to be able to deploy a contract, we need to first store the code on the chain, and that code will be given a code_id. token_code_id in here will be the code_id of the cw721_base contract, which we will store on the chain to use for this contract. In above msg, we use serde attribute. It's a framework for serializing and deserializing Rust data structures efficiently and generically. We use it to rename all the fields (if this is a struct) or variants (if this is an enum) according to the given case convention. When you generate schema of project, all parameters will be write in snake_case type.

Instantiation

The instantiation code is implemented in src/contract.rs:

src/contract.rs
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)?;

if msg.unit_price == Uint128::new(0) {
return Err(ContractError::InvalidUnitPrice {});
}

if msg.max_tokens == 0 {
return Err(ContractError::InvalidMaxTokens {});
}

let config = Config {
cw721_address: None,
cw20_address: msg.cw20_address,
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_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))
}

// Reply callback triggered from cw721 contract instantiation
#[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)?;

if config.cw721_address != None {
return Err(ContractError::Cw721AlreadyLinked {});
}

if msg.id != INSTANTIATE_TOKEN_REPLY_ID {
return Err(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())
}

Alright, that's a lot of code. Let's talk you through it step by step.

You can see the instantiate has 4 arguments:

  • deps - The dependencies, this contains your contract storage, the ability to query other contracts and balances, and some API functionality.
  • env - The environment, contains contract information such as its address, block information such as current height and time, as well as some optional transaction info.
  • info - Message metadata, contains the sender of the message (Addr) and the funds sent with it a Vec<Coin>.
  • msg - The InstantiateMsg you define in src/msg.rs.

In the first line, set_contract_version function uses a standard called cw2, it allows contracts to store version and name as you look at.

set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

The next two if statements is the logic to validate the input of two InstantiateMsg fields: unit_price and max_tokens.

if msg.unit_price == Uint128::new(0) {
return Err(ContractError::InvalidUnitPrice {});
}

if msg.max_tokens == 0 {
return Err(ContractError::InvalidMaxTokens {});
}

If unit_price (the price to mint NFT) or max_tokens (the maximum number of NFTs that can be minted) equal to zero, our instantiate function will return the following 2 errors respectively: InvalidUnitPrice and InvalidMaxTokens. They are defined in src/error.rs:

pub enum ContractError {

// Previous code omitted

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

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

// Following code omitted
}

The above two if statements you will often see in other functions later. Because checking the input condition of the data is very important.

Ok, the next line creates a Config struct which was defined in the State chapter. And you can see, we have to use clone() in some fields to avoid moving values.

The line following that stores it in our CONFIG storage. (Ensure you have imported CONFIG from state.rs). It does this by calling it with deps.storage which is our contracts storage and giving it the address of our newly created config variable. It does this by preceding it with the & character.

Below the save CONFIG statement, we will define a sub_msg variable. Because there are new concepts here, I separate them into a section.

Submessage

let sub_msg: Vec<SubMsg> = vec![SubMsg {
msg: WasmMsg::Instantiate {
code_id: msg.token_code_id,
msg: to_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,
}];

As mentioned above, we use token_code_id field to call the initialization of a contract from previously uploaded Wasm code with the corresponding code_id. This can be done by sending a message of WasmMsg::Instantiate type. But we wanna get the result of the message sent from our smart contract (address of new contract), so we need to dispatch a sub message.

The variable sub_msg here is defined as a vector containing SubMsg (sub message). Of course it's possible to define it as just a single SubMsg, but maybe in the future you want to send another SubMsg, so it's better to define it as a vector of SubMsg. So what's inside a SubMsg?

  • msg: is message to be sent. In our case, it is a WasmMsg::Instantiate
  • id: is an arbitrary reply_ID chosen by the contract that will be used to handle the reply. This is typically used to match Replys in the reply entry point to the submessage. Here, we use INSTANTIATE_TOKEN_REPLY_ID is the SubMsg id.
  • gas_limit: is gas limit for the submessage.
  • reply_on: is a flag to determine when the reply should be sent. Submessages offer different options for the other contract to provide a reply. There are four reply options (Always, Error, Success, Never) you can choose and they are defined in ReplyOn enum. Here, we use ReplyOn::Success to only callback if SubMsg was successful, no callback on error case.

Next, we will learn about WasmMsg::Instantiate and its fields. WasmMsg::Instantiate is used to instantiate a new contracts from previously uploaded Wasm code.

  • sender is the actor that signed the messages, and is automatically filled with the current contract’s address.
  • code_id is the reference to the stored WASM code.
  • funds is coins amount that are transferred to the contract on instantiation.
  • admin is an optional address that can execute migrations.
  • label is optional metadata to be stored with a contract instance.

Finally, the last line of the instantiate function:

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

The final line is our return line indicated not include ;. This returns a success using the Ok and Result structure of Response type. Response is a response of a contract entry point, such as instantiate, execute, reply or migrate. Within the Ok structure, we create a response using various builder methods. We use add_submessages to add bulks explicit SubMsg structs to the list of messages to process.

Handling a reply

In order to handle the reply from the other contract, the calling contract must implement a new entry point. Here, our contract want to know cw721 contract (this contract link to our contract) address after cw721 contract instantiation.

#[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)?;

if config.cw721_address != None {
return Err(ContractError::Cw721AlreadyLinked {});
}

if msg.id != INSTANTIATE_TOKEN_REPLY_ID {
return Err(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())
}

In the first line, by using load(), we get the CONFIG Item data previously stored in the store and assign it to a config variable. Note that the mut keyword is used here because in this function we will modify the data of config.

The next two if statements is the logic to validate two fields: config.cw721_address and msg.id. First, if config.cw721_address already exists, the Cw721AlreadyLinked error is returned. Second, if msg.id is not equal to the reply_id of SubMsg, the InvalidTokenReplyId error will be returned. Let define these 2 errors in src/error.rs

pub enum ContractError {

// Previous code omitted

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

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

// Following code omitted
}

About the last four lines of reply function.

  • In the first line, we use parse_reply_instantiate_data(), function of registry cw_utils with msg input to create a reply variable with it's type is a MsgInstantiateContractResponse struct. This struct has contract_address field that our contract need.
  • In the second line, we will assign the value reply.contract_address to the cw721_address field of the config. There is data modification here, so config is declared with mut keyword.
  • In the third line, we store config in our CONFIG storage to save changes.
  • And the last line, similar to instantiate function, is our return line using the Ok structure of Response type.

Well, we have implemented the first instantiate entry point of our contract! Along with that, is reply entry point to reply callback triggered from cw721 contract instantiation. In the next section, we will implement execute entry_point for it!