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:
#[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
:
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 aVec<Coin>
.msg
- TheInstantiateMsg
you define insrc/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 aWasmMsg::Instantiate
id
: is an arbitrary reply_ID chosen by the contract that will be used to handle the reply. This is typically used to matchReply
s in thereply
entry point to the submessage. Here, we useINSTANTIATE_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 inReplyOn
enum. Here, we use ReplyOn::Success to only callback ifSubMsg
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 registrycw_utils
withmsg
input to create areply
variable with it's type is aMsgInstantiateContractResponse
struct. This struct hascontract_address
field that our contract need. - In the second line, we will assign the value
reply.contract_address
to thecw721_address
field of theconfig
. There is data modification here, soconfig
is declared withmut
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 ofResponse
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!