본문으로 건너뛰기

Cosmwasm 컨트랙트 작성 및 빌드하기

이전 예제에서 CW20CW721 컨트랙트를 Instantiate, Execute, Query까지 진행해보았습니다. CW20과 CW721 컨트랙트는 모두 Cosmwasm 모듈을 이용하고, Rust 언어로 이루어져 있습니다. 두 컨트랙트는 표준 컨트랙트이기 때문에 XPLA 블록체인에 이미 배포되어 있었습니다.

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

Web3 게임을 개발할 때 CW20, CW721 컨트랙트 기능만이 아닌, 다른 컨트랙트 기능이 필요할 수도 있습니다. 이번 단계에서는 직접 Cosmwasm 컨트랙트를 Rust 언어로 작성해보고, XPLA 블록체인에 배포해보겠습니다.

목차

다음과 같은 절차로 Cosmwasm 컨트랙트 작성을 알아볼 것입니다.

  1. Prerequisite

  2. 컨트랙트 생성 순서

  3. 코드 제공 및 빌드

  4. 컨트랙트 생성하고, 사용하기

Prerequisite

Coswmasm 컨트랙트는 Rust 언어로 작성합니다. 여러분의 운영체제에 맞게 Rust를 설치해주세요. 또한 빌드를 위해 Docker를 사용하니, Docker도 설치해주세요.

컨트랙트 코드를 직접 작성하려면 Rust 관련 지식이 필요합니다. 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/

Rust에 대해 완벽하게 알지 못해도 괜찮습니다. 일단 아래 내용을 읽고 따라해보는 것도 좋은 선택입니다. 여러분이 원하시는 대로 학습을 진행해주세요.

컨트랙트 생성 순서

먼저 XPLA 블록체인에서 Cosmwasm 컨트랙트가 어떻게 생성되는지부터 살펴봅시다.

  1. Rust 언어로 컨트랙트 코드를 작성합니다.

  2. 작성한 컨트랙트 코드를 빌드하여 .wasm 파일을 생성합니다.

  3. 생성한 .wasm 파일을 XPLA 블록체인에 배포합니다. 배포는 StoreCode Method 트랜잭션으로 진행할 수 있습니다. Vault를 이용하면 쉽게 .wasm 파일을 배포할 수 있습니다.

  4. .wasm 파일을 배포하면 Code ID가 부여됩니다. 해당 Code ID를 이용하여 Instantiate Method 트랜잭션으로 컨트랙트를 생성할 수 있습니다. 같은 Code ID로 Instantiate 된 컨트랙트들은 모두 같은 코드로 이루어져 있어, 기능이 비슷합니다. 다만 Instantiate할 때 입력한 초깃값에 따라 세부사항이 달라집니다.

위 순서대로 이번 예제에서 컨트랙트를 생성해 볼 것입니다.

코드 제공 및 빌드

Github에서 컨트랙트 코드를 다운받아 주세요. 코드 구성은 아래와 동일합니다. 예제에서 작성하는 컨트랙트는 유저의 게임 결과를 기록해주는 기능을 가진 컨트랙트입니다.

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)
  }

}


빌드는 아래 command로 진행할 수 있습니다.

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

빌드를 완료하면 artifacts/game_data_save.wasm 파일이 생성됩니다.

빌드 과정에서 아래와 같은 에러가 발생합니다. 어떻게 해야 하나요?

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.
Cargo.toml 파일의 23번째 줄을 다음과 같이 수정하고, 다시 빌드해보세요.
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"""

컨트랙트 생성하고, 사용하기

StoreCode

빌드 결과로 나온 .wasm 파일을 StoreCode Method 트랜잭션으로 블록체인에 배포해봅시다. Vault 웹페이지를 이용하거나, 아래와 같이 Javascript로도 가능합니다. 예제에서 .wasm 파일의 이름은 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()

트랜잭션을 생성하고, Code Id를 Explorer에서 확인하겠습니다. 예제에서 만든 컨트랙트의 Code Id는 571입니다.

Instantiate

StoreCode로 배포한 코드로 컨트랙트를 Instantiate합니다. 마찬가지로 Vault로 진행하거나, 아래처럼 Javascript로 가능합니다.

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()

Cosmwasm 컨트랙트와 Instantiate 트랜잭션이 어떻게 상호작용하는지 살펴봅시다. Instantiate 트랜잭션을 생성할 때, 아래와 같이 init_msg를 설정해주었습니다.

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

이때 넣은 초깃값이 src/lib.rs 파일 instantiate 함수의 InstantiateMsg 타입의 argument로 들어오게 됩니다. tract.instantiate 함수로 src/execute.rs 파일의 instantiate Method를 호출합니다.

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)
}

cw-storage-plus Module에서 Item struct의 save Method를 이용하여, 초깃값을 블록체인 데이터베이스에 저장했습니다. 이후 Ok로 결과를 전달하면 Instantiate가 완료됩니다. 예제에 나온 DepsMut와 Env struct에 관한 설명은 Cosmwasm Bookcosmwasm_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

Execute를 진행해봅시다. 마찬가지로 Vault와 Javascript로 가능합니다. Javascript 코드는 아래와 같습니다.

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()

Cosmwasm 컨트랙트와 Execute 트랜잭션이 어떻게 상호작용하는지 살펴봅시다. Execute 트랜잭션을 생성할 때, 아래와 같이 executeMsg를 설정해주었습니다.

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

executeMsg 값은 src/lib.rs 파일 execute 함수의 ExecuteMsg 타입의 argument로 들어오게 됩니다. tract.execute 함수로 src/execute.rs 파일의 execute Method를 호출합니다.

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)
}

src/execute.rs 파일의 execute Method에서 ExecuteMsgSaveData의 패턴과 일치합니다. 따라서 같은 파일의 save_data Method를 호출합니다.

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),
}
}

save_data Method 코드는 아래와 같습니다. cw-storage-plus Module에서 Map struct의 update Method를 이용하여, executeMsg 값들(last_stage, high_score, game_gold)이 블록체인 데이터베이스에 업데이트되는 것을 확인할 수 있습니다. 이후 Ok로 결과를 전달하면 Execute가 완료됩니다.

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는 Explorer Contract Details의 InitMsg 탭이나, Vault에서 간편하게 가능합니다. 예제에선 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()

앞서 last_stage, game_gold 값을 10으로, high_score 값을 100으로, 넣어 save_data Method를 Execute했습니다. Query 결과도 Execute 입력을 바탕으로 잘 나오는 것을 확인할 수 있습니다. 예제 코드의 Query 결과는 game_gold 값이 10보다 클텐데, 이것은 game_gold 값이 Execute를 실행할 때마다 축적되기 때문입니다. 앞서 Execute에 관한 코드에서 game_gold 값을 어떻게 저장하는지 다시 한 번 살펴보세요.

Cosmwasm 컨트랙트에 Query를 보낼 때, 컨트랙트 코드가 어떻게 동작하는지 살펴봅시다. 먼저 queryMsg는 아래와 같이 입력하였습니다.

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

queryMsg 값은 src/lib.rs 파일 query 함수의 QueryMsg 타입의 argument로 들어오게 됩니다. tract.query 함수로 src/query.rs 파일의 query Method를 호출합니다.

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)
}

src/query.rs 파일의 query Method에서 QueryMsgGameData의 패턴과 일치합니다. 따라서 같은 파일의 game_data Method를 호출합니다.

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)?)
}
}

game_data Method 코드는 아래와 같습니다. cw-storage-plus Module에서 Map struct의 may_load Method를 이용하여, 블록체인 데이터베이스의 유저 게임 데이터 값을 불러왔습니다. 이후 Ok로 결과를 전달하면 Query 과정도 완료됩니다.

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,
})
}
}

마무리

지금까지 Cosmwasm 컨트랙트 코드를 분석해보면서, Instantiate, Execute, Query가 어떻게 진행되는지 살펴보았습니다. 예제를 활용하여 여러분만의 Cosmwasm 컨트랙트를 개발해보세요. Cosmwasm 컨트랙트 개발에 관한 다른 예제는 DocsCosmWasm Starter Pack에도 있습니다.