Skip to main content

Write and Build Cosmwasm Contracts

In the previous example, we went through the process of Instantiate, Execute, and Query for the CW20 and CW721 contracts. Both CW20 and CW721 contracts are built using the Cosmwasm module and the Rust language. Since both contracts are standard contracts, they were already deployed on the XPLA blockchain.

ContractGithubCodeId
CW20https://github.com/xpladev/cw-plus1
CW721https://github.com/xpladev/cw-nfts3

When developing a Web3 game, you might need functionalities beyond CW20 and CW721 contracts. In this step, we will write a Cosmwasm contract in Rust and deploy it to the XPLA blockchain.

Index

We'll explore creating a Cosmwasm contract in the following steps:

  1. Prerequisite

  2. Contract Creation Process

  3. Code Provision and Build

  4. Creating and Using the Contract

Prerequisite

Cosmwasm contracts are written in the Rust programming language. Please install Rust that matches your OS. Additionally, since Docker is used for building, make sure to install Docker as well.

If you want to write contract code directly, you'll need some knowledge about Rust. We've prepared some documents that can help you learn Rust.

GuideLink
The Rust Programming Languagehttps://doc.rust-lang.org/book/ch00-00-introduction.html
The Cargo Bookhttps://doc.rust-lang.org/cargo/index.html
Cosmwasm bookhttps://book.cosmwasm.com/basics/entry-points.html
Learn X in Y minuteshttps://learnxinyminutes.com/docs/rust/
CosmWasm Starter Packhttps://github.com/CosmWasm/cw-template
XPLA Docshttps://docs.xpla.io/develop/develop/smart-contract-guide/wasm/writing-the-contract/

It's okay if you don't know Rust perfectly. Reading over and following the below content is also a good option. Feel free to proceed with learning at your own pace.

Contract Creation Process

Let's first understand how Cosmwasm contracts are created on the XPLA blockchain.

  1. Write contract code in Rust.

  2. Build the written contract code to generate a .wasm file.

  3. Deploy the created .wasm file to the XPLA blockchain. Deployment can be done through the StoreCode Method transaction. Vault makes it easy to deploy .wasm files.

  4. Once deployed, a Code ID is assigned. You can use this Code ID to create contracts using the Instantiate Method transaction. Contracts instantiated with the same Code ID share the same code and have similar functionalities. However, the specifics may differ based on the initial values provided during instantiation.

We'll follow the steps above to create a contract in this example.

Code Provision and Build

Download the contract code from GitHub. The code structure is as follows. The example contract we're creating has the functionality of recording a user's game results.

mod state;
mod execute;
mod msgs;
mod error;
mod query;

pub use crate::msgs::{InstantiateMsg, ExecuteMsg, QueryMsg};
pub use crate::state::GameDataSaveContract;
pub use crate::error::ContractError;

#[cfg(not(feature = "library"))]
pub mod entry {
  use super::*;

  use cosmwasm_std::entry_point;
  use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};

  #[entry_point]
  pub fn instantiate(
      deps: DepsMut,
      env: Env,
      info: MessageInfo,
      msg: InstantiateMsg,
  ) -> StdResult<Response> {
      let tract: GameDataSaveContract<'_> = GameDataSaveContract::default();
      tract.instantiate(deps, env, info, msg)
  }

  #[entry_point]
  pub fn execute(
      deps: DepsMut,
      env: Env,
      info: MessageInfo,
      msg: ExecuteMsg,
  ) -> Result<Response, ContractError> {
      let tract: GameDataSaveContract<'_> = GameDataSaveContract::default();
      tract.execute(deps, env, info, msg)
  }

  #[entry_point]
  pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
      let tract: GameDataSaveContract<'_> = GameDataSaveContract::default();
      tract.query(deps, env, msg)
  }

}


You can build it using the command provided below.

cargo build
cargo install cargo-run-script
cargo run-script optimize

Once the build is complete, an artifacts/game_data_save.wasm file will be generated.

During the build process, you might encounter an error like the one below.

docker: Error response from daemon: create %cd%: "%cd%" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path.
To resolve this, modify line 23 of the Cargo.toml file as shown, and then try building again.
optimize = """docker run --rm -v "$(wslpath -w $(pwd))":/code --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry cosmwasm/rust-optimizer:0.12.6"""

Creating and Using the Contract

StoreCode

Let's deploy the generated .wasm file to the blockchain using the StoreCode Method transaction. You can do this via the Vault website or using JavaScript, as shown below. In this example, the .wasm file is named game_data_save.wasm.

const { LCDClient, MnemonicKey, MsgStoreCode } = require("@xpla/xpla.js");
const fs = require('fs');

const lcd = new LCDClient({
chainID: 'cube_47-5',
URL: 'https://cube-lcd.xpla.dev'
});

async function main() {
const mk = new MnemonicKey({
mnemonic: 'myth snow ski simple century dad gun dolphin sail lawsuit fringe image toast betray frown keep harbor flash table prevent isolate panic tag vehicle',
})

const sender = lcd.wallet(mk).key.accAddress;

const signedTx = await lcd.wallet(mk).createAndSignTx({
msgs: [new MsgStoreCode(sender, fs.readFileSync('game_data_save.wasm').toString('base64'))],
});

const txResult = await lcd.tx.broadcastSync(signedTx);
console.log("Your Transaction Hash: " + txResult.txhash);
}
main()

We'll create a transaction and confirm the Code ID from the Explorer. The Code ID for the created contract in the example is 571.

Instantiate

Now, let's instantiate a contract using the code deployed through StoreCode. This can be done through Vault or using JavaScript, as shown below.

const { LCDClient, MnemonicKey, MsgInstantiateContract, Fee, SignMode } = require("@xpla/xpla.js");

const lcd = new LCDClient({
  chainID: 'cube_47-5',
  URL: 'https://cube-lcd.xpla.dev'
});

const main = async () => {
  const mk = new MnemonicKey({
    mnemonic: 'myth snow ski simple century dad gun dolphin sail lawsuit fringe image toast betray frown keep harbor flash table prevent isolate panic tag vehicle' // Replace with your mnemonic words
  })

  const wallet = lcd.wallet(mk);
  const myWalletAddress = wallet.key.accAddress;

  const init_msg = {
    owner: "xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk",
    description: "Game Data Contract",
  };

  const instantiate = new MsgInstantiateContract(
    myWalletAddress, // sender
    myWalletAddress, // admin
    571, // Example Contract Code ID
    init_msg,
    {}, 
    'My First Comswasm Contract', 
  );

  const signedTx = await lcd.wallet(mk).createAndSignTx({ 
    msgs: [instantiate]
  });

  const txResult = await lcd.tx.broadcastSync(signedTx);
  console.log(txResult.txhash);
}
main()

Let's examine how the Cosmwasm contract and the Instantiate transaction interact. When creating the Instantiate transaction, we set the init_msg as shown below.

const init_msg = {
owner: "xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk",
description: "Game Data Contract",
};

The initial value you provide here will be passed as an argument of the InstantiateMsg type in the src/lib.rs file's instantiate function. The tract.instantiate function calls the instantiate Method from the src/execute.rs file.

src/lib.rs
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let tract: GameDataSaveContract<'_> = GameDataSaveContract::default();
tract.instantiate(deps, env, info, msg)
}

Using the save Method of the Item struct from the cw-storage-plus Module, we store the initial value in the blockchain database. Once the result is sent with Ok, the Instantiate is complete. More information about DepsMut and Env struct can be found in the Cosmwasm Book and cosmwasm_std.

src/execute.rs
pub fn instantiate(
&self,
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
let config = Config {
owner: deps.api.addr_validate(&msg.owner)?,
owner_candidate: deps.api.addr_validate(&msg.owner)?,
description: msg.description,
};

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

Ok(Response::new())
}

Execute

Let's proceed with the Execute step. This can also be done through Vault or JavaScript. The JavaScript code is provided below.

const { LCDClient,  MnemonicKey, MsgExecuteContract } = require("@xpla/xpla.js");

const lcd = new LCDClient({
    chainID: 'cube_47-5',
    URL: 'https://cube-lcd.xpla.dev'
});

const main = async () => {
    const mk = new MnemonicKey({
    mnemonic: 'myth snow ski simple century dad gun dolphin sail lawsuit fringe image toast betray frown keep harbor flash table prevent isolate panic tag vehicle'
    })

    const wallet = lcd.wallet(mk);
    const myWalletAddress = wallet.key.accAddress;
    
    const contractAddress = "xpla1k6ufjtkyjnxgkmxjew96n2kssdzslpnp398ghm82hk5tt2xdls9spnufcz"; // Replace it with the address of the CW20 token created in example-4.js.

    const executeMsg = {
      save_data : {
          user : "xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk",
          last_stage : 10,
          high_score : 100,
          game_gold : 10
      }
    };
    
    const message = new MsgExecuteContract(
        myWalletAddress,
        contractAddress,
        executeMsg
    );
    
    const signedTx = await lcd.wallet(mk).createAndSignTx({ // Creating and signing the transaction
        msgs: [message]
    });

    const txResult = await lcd.tx.broadcastSync(signedTx);
    console.log(txResult.txhash);
}
main()

Let's understand how the Cosmwasm contract and the Execute transaction interact. When creating the Execute transaction, we set the executeMsg as shown below.

const executeMsg = {
save_data : {
user : "xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk",
last_stage : 10,
high_score : 100,
game_gold : 10
}
};

The executeMsg value becomes an argument of the ExecuteMsg type in the src/lib.rs file's execute function. The tract.execute function calls the execute Method from the src/execute.rs file.

src/lib.rs
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
let tract: GameDataSaveContract<'_> = GameDataSaveContract::default();
tract.execute(deps, env, info, msg)
}

In the src/execute.rs file, the execute method matches the pattern of the SaveData in the ExecuteMsg. Thus, we call the save_data Method from the same file.

src/execute.rs
pub fn execute(
&self,
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg
) -> Result<Response, ContractError> {
match msg{
ExecuteMsg::SaveData {
user,
last_stage,
high_score,
game_gold
} => self.save_data(deps, env, info, user, last_stage, high_score, game_gold),

ExecuteMsg::UpdateConfig {
owner_candidate,
description,
} => self.update_config(deps, env, info, owner_candidate, description),

ExecuteMsg::AllowOwner {

} => self.allow_owner(deps, env, info),
}
}

The code for the save_data Method is provided below. Using the update Method of the Map struct from the cw-storage-plus Module, you can see that the values from executeMsg values (last_stage, high_score, game_gold) are being updated in the blockchain database. Once the result is sent with Ok, the Execute is complete.

src/execute.rs
pub fn save_data(
&self,
deps: DepsMut,
_env: Env,
info: MessageInfo,
user: String,
last_stage: Option<u64>,
high_score: Option<u64>,
game_gold: Option<i64>,
) -> Result<Response, ContractError> {
let config: Config = self.config.load(deps.storage)?;

if info.sender != config.owner {
return Err(ContractError::Unauthorized {})
}

self.game_data.update(deps.storage, user.clone(), |res| -> StdResult<GameData> {
let mut data = res.unwrap_or_default();
if let Some(ls) = last_stage {
if ls > data.last_stage {
data.last_stage = ls;
}
}
if let Some(hs) = high_score {
if hs > data.high_score {
data.high_score = hs;
}
}
if let Some(gg) = game_gold {
data.game_gold += gg;
}
Ok(data)
})?;


Ok(Response::new()
.add_attribute("action", "save_data")
.add_attribute("user", user))

}

Query

Query can be conveniently done through the Explorer Contract Details' InitMsg tab or through Vault. In this example, we'll use JavaScript.

const { LCDClient } = require("@xpla/xpla.js");

const lcd = new LCDClient({
    chainID: 'cube_47-5',
    URL: 'https://cube-lcd.xpla.dev'
});

async function main() {
    const contractAddress = "xpla1k6ufjtkyjnxgkmxjew96n2kssdzslpnp398ghm82hk5tt2xdls9spnufcz";
    
    const queryMsg = {
        game_data : {
            user : "xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk"
        }
    }

    const gameData = await lcd.wasm.contractQuery(contractAddress, queryMsg);
    console.log(gameData);
}
main()

Since we previously executed save_data Method with the values last_stage and game_gold set to 10 and high_score set to 100, the Query result aligns with the executed inputs. The Query result shows that the game_gold value is greater than 10, which is due to the game_gold value accumulating with each Execute. Take another look at how the code in the previous section handles storing the "game_gold" value in the Execute process.

Let's take a look at how the Cosmwasm contract operates when you send a Query. First, let's examine how the queryMsg is defined as shown below.

const queryMsg = {
game_data : {
user : "xpla1cwduqw0z8y66mnfpev2mvrzzzu98tuexepmwrk"
}
}

The queryMsg value becomes an argument of the QueryMsg type in the src/lib.rs file's query function. The tract.query function calls the query Method from the src/query.rs file.

src/lib.rs
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
let tract: GameDataSaveContract<'_> = GameDataSaveContract::default();
tract.query(deps, env, msg)
}

In the src/query.rs file, the query method matches the pattern of the GameData in the QueryMsg. Therefore, it calls the game_data method within the same file.

src/query.rs
pub fn query(&self, deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Config {} => to_binary(&self.config(deps)?),
QueryMsg::GameData { user } => to_binary(&self.game_data(deps, user)?)
}
}

The code for the game_data method is as follows. It uses the may_load method of the Map struct from the cw-storage-plus Module to retrieve the user's game data value from the blockchain database. Afterward, when the result is sent as Ok, the Query process is also finished.

src/query.rs
fn game_data(
&self,
deps: Deps,
user: String,
) -> StdResult<GameData> {
let data = self.game_data.may_load(deps.storage, user)?;
if let Some(user_data) = data {
return Ok(user_data)
} else {
return Ok(GameData{
last_stage: 0,
high_score: 0,
game_gold: 0,
})
}
}

Wrapping Up

So far, we've examined the Cosmwasm contract code to understand how Instantiate, Execute, and Query processes work. Using the provided example, you can develop your own Cosmwasm contract. There are also other examples related to Cosmwasm contract development available in the Docs and CosmWasm Starter Pack. Feel free to explore and experiment with creating your own contracts!