CosmWasmとCW20の基本的な扱い方

2週間ほど HackAtom RU に参加していました.前回の HackAtom V では作業記録を特に残しておらず初手がよろしくなかったので,今回利用した CW20 の基本的な扱い方についてまとめておこうと思います.

Cosmosブロックチェーンのクライアント

  • gaiad: for Cosmos Hub (and the blockchains built by Cosmos SDK)
  • wasmd: for CosmWasm
  • secretcli: for SecretNetwork

ブロックチェーンによって対応しているクライアントのバージョンが異なるため,開発時は利用するテストネットに合わせてバージョンを選択する必要があります.

terminal
// Install gaiad
$ git clone -b <latest-release-tag> https://github.com/cosmos/gaia.git
$ cd gaia && make install

// Install wasmd
$ git clone -b <latest-release-tag> https://github.com/cosmwasm/wasmd.git
$ cd wasmd && make install && make test

バージョンアップによって gaiacli, wasmcligaiad, wasmd に統合されましたが,secretcli は統合されていないようです.

(参考)

後々操作が楽になるように,環境変数を設定しておきます.今回は Musselnet テストネットを用いることを前提に例示しています.

terminal
$ nano ~/.cosmwasm-env
$ source ~/.cosmwasm-env

設定内容は以下の通りです.

.cosmwasm-env
export MUSSELNET_NODE=(--node https://rpc.musselnet.cosmwasm.com:443)
export MUSSELNET_TXFLAG=($MUSSELNET_NODE --chain-id musselnet-4 --gas-prices 0.025ucosm --gas auto --gas-adjustment 1.3)

アカウント操作

以下,コマンドの例です.適宜置き換えてください.

terminal
// Create a new account
$ wasmd keys add fred

// Show the address of the account named "fred"
$ wasmd keys show -a fred

// Details of the account on musselnet-4
// "q" is the shorthand of "query"
$ wasmd q account $(wasmd keys show -a fred) $MUSSELNET_NODE

// Get the balance of the native token on musselnet-4
$ wasmd q bank balances $(wasmd keys show -a fred) $MUSSELNET_NODE

Faucet

チェーンによって faucet の方法は異なり,curl で叩く場合もあれば REST が用意されている場合もあるなど様々です.

CosmWasm のテストネットである Musselnet では curl で JSON を POST すれば fee token が受け取れます.staking token は discord で頼む必要があります.

terminal
$ JSON=$(jq -n --arg addr $(wasmd keys show -a fred) '{"denom":"umayo","address":$addr}')
// "-H" is the shorthand of "--header"
$ curl -X POST -H "Content-Type: application/json" --data "$JSON" https://faucet.musselnet.cosmwasm.com/credit

コントラクトの扱い方

Compile with optimizer

コントラクトをテストネットにデプロイする際には Optimizer コンテナを用いたコンパイルが必要です.単に cargo wasm しただけでは断られます.

Optimizer はコントラクトごと用意されているものが異なっており,今回は cosmwasm-plus をベースに使わせて頂いたので,cosmwasm/workspace-optimizer を使用しました.

Docker コンテナの OS/ARCH が linux/amd64 なので M1 Mac (arm64 対応 Docker Desktop RC 2 使用) ではエラーが出て実行できませんでした(Rosetta 2 も当然不可).Intel CPU とかで動かしてください.

cosmwasm-plus の場合は以下のようなコマンドになります.

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

ルートディレクトリで実行すると,./contracts/ 以下のすべてのディレクトリについてコンパイルを実行し ./artifacts/ に .wasm ファイルを出力します.cosmwasm-examples で用いられている rust-optimizer ではコンパイル対象を記述する部分があるので特定のコントラクトのみ処理することが可能ですが,こちらのコマンドではできなさそうです.まあまあ時間がかかります.

Store

コンパイルが終了したらコントラクトをチェーン上に保存します.

terminal
$ RES=$(wasmd tx wasm store ./artifacts/CONTRACT.wasm --from fred $MUSSELNET_TXFLAG -y)
gas used: 1234567
$ CODE_ID=$(echo $RES | jq -r '.logs[0].events[0].attributes[-1].value')

CODE_ID はコントラクトのアップロード順に振られている番号です.

この時点ではまだデプロイは完了していません.コントラクトが初期化されていないため,コントラクトアドレスも割り当てられていません.

Instantiate

コントラクトを初期化し,利用可能な状態にします.初期化するコントラクトは ID (CODE_ID) で指定します.

terminal
$ INIT='{}'
$ wasmd tx wasm instantiate $CODE_ID $INIT --label 'hoge' $MUSSELNET_TXFLAG -y

INIT にはコントラクトに合わせたメッセージを入れる必要があります.メッセージが空で良い場合は上記のようになんの値も渡しません.

また,初期化時にネイティブトークンを送金する場合は --amount で指定できます.ラベルの内容は自由です.

ここに限らず,コントラクトにメッセージを渡すときには必ず JSON 形式を用います.ターミナルで JSON を文字列として入力するときにダブルクォーテーションマークを用いる場合は JSON 内部の文字列を指定するときにエスケープが必要です.シングルクォーテーションを用いる場合は(ほぼ)問題ありません(JSON では key, value の識別にダブルクォーテーションしか用いることができないため).

初期化されると以下の要領でコントラクトアドレスが取得できます.

terminal
$ CONTRACT=$(wasmd query wasm list-contract-by-code $CODE_ID $MUSSELNET_NODE -o json | jq -r '.contract_infos[0].address')

Execute

コントラクト実行には以下のコマンドを用います.

terminal
$ EXECUTE='{}'
$ wasmd tx wasm execute $CONTRACT $EXECUTE --from fred $MUSSELNET_TXFLAG -y

コントラクトの実行時メッセージに応じて EXECUTE を変化させます.ここでもネイティブトークンの必要に応じて --amount 等を併用します.

Query

クエリは以下のコマンドで実行できます.

terminal
$ QUERY='{}'
$ wasmd query wasm contract-state smart $CONTRACT $QUERY $MUSSELNET_NODE

クエリにはガスを必要としないため,MUSSELNET_TXFLAG で設定している各パラメータや,--from による実行アカウントの指定は不要です.

CW20トークン発行・操作

cw20-base を使って試しに CW20 を発行します.手順は上記の通りで,cw20-base コントラクトに合わせて各メッセージを編集します.

各メッセージに必要なパラメータは ./cw20-base/src/msg.rs で確認できます.

terminal
$ git clone https://github.com/CosmWasm/cosmwasm-plus.git && cd cosmwasm-plus

// Compile at the root directory
$ docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename '$(pwd)')_cache",target=/code/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/workspace-optimizer:0.10.7

// Store the code
$ RES=$(wasmd tx wasm store ./artifacts/cw20-base.wasm --from fred $MUSSELNET_TXFLAG -y)
gas used: 1234567
$ CODE_ID=$(echo $RES | jq -r '.logs[0].events[0].attributes[-1].value')

// Instantiate
$ INIT=$(jq -n --arg fred $(wasmd keys show -a fred) '{"name":"test","symbol":"TST","decimals":6,"initial_balances":{"address":$fred,"amount":"10000"}}')
$ wasmd tx wasm instantiate $CODE_ID $INIT --label 'test' $MUSSELNET_TXFLAG -y
$ CONTRACT=$(wasmd query wasm list-contract-by-code $CODE_ID $MUSSELNET_NODE -o json | jq -r '.contract_infos[0].address')

// Query fred's balance of the CW20 token
$ QUERY=$(jq -n --arg fred $(wasmd keys show -a fred) '{"balance":{"address":$fred}}')
$ wasmd query wasm contract-state smart $CONTRACT $QUERY $MUSSELNET_NODE

// Execute the contract
$ EXECUTE='{"transfer":{"recipient":"wasmADDRESSOFRECIPIENT","amount":"1000"}}'
$ wasmd tx wasm execute $CONTRACT $EXECUTE --from fred $MUSSELNET_TXFLAG -y

メッセージの key はキャメルケースですが,JSON で入力するときはスネークケースを用います.恐らく下記の一文のおかげです.

#[serde(rename_all = "snake_case")]

ネイティブトークンの送金には denom を指定しますが,CW20 の送金には amount のみを入力します.CW20 のコントラクトを指定しており denom が必然的に固定されているためです.

注意すべき点として,amount はコントラクト上で Uint128 型として宣言されているため,JSON で与えるときは文字列で渡す必要があります.ちなみに,数値で与えている decimals の型は u8 です.

CW20を利用したコントラクトの実行

ネイティブトークンをコントラクトに送金して何らかの処理を行う際には --amount で渡すことができます.このとき,コントラクト内では info.sent_funds として処理されています.

しかし,CW20 はそのようにはいきません.既に発行されている CW20 を別のコントラクトで扱いたい場合は,CW20 コントラクトの Send メッセージを用いて別のコントラクトに対するメッセージを投げることになります.

例えば,CW20 を IBC によって他の Cosmos ブロックチェーンに送金する CW20-ICS20 コントラクトを使用する場合は,以下のようになります.

terminal
$ MSG=$(jq -n --arg fred $(gaiad keys show -a fred) '{"channel":"channel-0","remote_address":$fred,"timeout":3600}' | base64) 
$ EXECUTE=$(jq -n --arg msg $MSG '{"send":{"contract":"wasmALTERNATIVECONTRACT","amount":"1000","msg":$msg}}')
$ wasmd tx wasm execute $CONTRACT $EXECUTE --from fred $MUSSELNET_TXFLAG -y

上記の例では,自身の gaiad で管理している "fred" のアカウントに IBC を介して 1000TST を送ろうとしています.CW20-ICS20 では Cw20ReceiveMsg を受け取ると,その内部の msg を Base64 デコードした内容を TransferMsg として解釈する仕様になっているため,MSG には Base64 エンコードした TransferMsg を,EXECUTE には Cw20ReceiveMsg を用意しています.

自分で CW20 を利用したコントラクトを作成する場合も同様です.Cw20ReceiveMsg.msg をデコードし,自身のコントラクトの HandleMsgExecuteMsg に渡せば任意の処理が実行できます.

最後に

タイトルが "CosmWasmとCW20の基本的な扱い方" にも関わらず CosmWasm コントラクトの書き方に関しては一切触れていませんが,結構執筆が疲れてきたRust 力がまだ洗練されていないのでご了承ください.

いつも Python や JavaScript のゆるい型定義に甘えている上に所有権問題も発生しないので Rust はなかなかの強敵です….

参考