Cosmos SDK上でDappsを作成しよう ~Terraで深めるCosmWasmへの理解~
2020年10月16日
この記事を簡単にまとめると(AI要約)
目次
- はじめに
- 全体の流れ
- 必要な開発環境
- 必要なツール
- コントラクトを書いてみる
- コントラクトの実行
- おわりに
- 参考資料
はじめに
本レポートではTerraというCosmos SDKを使用して実装されたブロックチェーンに対し、自分で用意したスマートコントラクトをデプロイ、またそのスマートコントラクトを実行する技術的な方法を実際にコードを見ながら解説します。
Terraはステーブルコインの発行を目的としたプロジェクトです。詳細は下記レポートをご覧ください。
*レポート:Terra Moneyの概要・仕組み・考察。Eコマースでの活用を目指す韓国発の
Stablecoinプロジェクト
https://hashhub-research.com/articles/2019-07-04-terra-money-overview
Terraは、CosmWasmというCosmos SDKベースのチェーンにおいてスマートコントラクトを扱えるようにするモジュールを採用しています。第三者がスマートコントラクトを扱えるようにすることにより、Terra上でDappsやDeFiを作成することができるようになり、よりTerraの経済圏が盛り上がるようになることを意図しているようです。また、Cosmosのインターオペラビリティ実装後には、CosmWasmのモジュールを持つチェーン同士が繋がることによってお互いのコントラクトを参照して実行することができる予定であり、各チェーン特有の機能を用いながらそれらのコントラクトが相乗効果を生むことが考えられます。これはEthereumのコントラクトとは異なる特徴であり、今後どう発展していくのか注目されています。
Terraはステーブルコインの発行を目的としたプロジェクトです。詳細は下記レポートをご覧ください。
*レポート:Terra Moneyの概要・仕組み・考察。Eコマースでの活用を目指す韓国発の
Stablecoinプロジェクト
https://hashhub-research.com/articles/2019-07-04-terra-money-overview
Terraは、CosmWasmというCosmos SDKベースのチェーンにおいてスマートコントラクトを扱えるようにするモジュールを採用しています。第三者がスマートコントラクトを扱えるようにすることにより、Terra上でDappsやDeFiを作成することができるようになり、よりTerraの経済圏が盛り上がるようになることを意図しているようです。また、Cosmosのインターオペラビリティ実装後には、CosmWasmのモジュールを持つチェーン同士が繋がることによってお互いのコントラクトを参照して実行することができる予定であり、各チェーン特有の機能を用いながらそれらのコントラクトが相乗効果を生むことが考えられます。これはEthereumのコントラクトとは異なる特徴であり、今後どう発展していくのか注目されています。
今回はそのTerra上のCosmWasmに対して、コントラクトをアップロードし、コントラクトを実行してチェーン上のステートを書き換える手順を解説します。
Cosmos SDKベースのチェーンのスマートコントラクトに注目しておくことで、各チェーン上で出てくる新たなプロジェクトや、今後出てくるであろうチェーン間を跨ぐプロジェクトの理解の助けにもなるでしょう。これから独自のプロジェクトを立ち上げたい方にも参考にしていただければ幸いです。
途中で何かうまく行かない、又はわからないという場合、有料会員の方はSlackのコミュニティにてサポート致しますので、遠慮なくご質問ください。
途中で何かうまく行かない、又はわからないという場合、有料会員の方はSlackのコミュニティにてサポート致しますので、遠慮なくご質問ください。
全体の流れ
今回は以下の順番でコントラクトの作成、実行を行います。
コントラクトの作成
コントラクト自体をRustで記述し、後でTerra上のCosmWasmが読めるようにWasmにビルドします。このビルドのためにRustの環境が必要です。
ローカル用のTerraブロックチェーンを起動
LocalTerra( https://github.com/terra-project/LocalTerra )というローカル用のテストネットのTerraを立ち上げてくれるプロジェクトを使用してTerraを立ち上げます。このLocalTerraの実行のためにはDocker Composeを扱えるようにしておく必要があります。
コントラクトをTerraにアップロード
立ち上げたTerraに対してコントラクトをアップロードしたり、実行したりするためにterracliというCLIツールを用意してあげる必要があります。さらに、このツールを用意するためにGoの環境が必要です。
Terra上のコントラクトを実行
ここまでくれば、環境とツールはすべて揃っています。用意したterracliを使用して最後にスマートコントラクトを実行しましょう。
コントラクトの作成
コントラクト自体をRustで記述し、後でTerra上のCosmWasmが読めるようにWasmにビルドします。このビルドのためにRustの環境が必要です。
ローカル用のTerraブロックチェーンを起動
LocalTerra( https://github.com/terra-project/LocalTerra )というローカル用のテストネットのTerraを立ち上げてくれるプロジェクトを使用してTerraを立ち上げます。このLocalTerraの実行のためにはDocker Composeを扱えるようにしておく必要があります。
コントラクトをTerraにアップロード
立ち上げたTerraに対してコントラクトをアップロードしたり、実行したりするためにterracliというCLIツールを用意してあげる必要があります。さらに、このツールを用意するためにGoの環境が必要です。
Terra上のコントラクトを実行
ここまでくれば、環境とツールはすべて揃っています。用意したterracliを使用して最後にスマートコントラクトを実行しましょう。
必要な開発環境
全体の流れで述べたように、RustとGoの二つの言語と、Docker Composeを扱える環境が必要となります。Terra公式のドキュメントによると、Rustは最新バージョン v1.46.0 (2020/9/30 現在)を、Goは1.13.1以上のバージョンが求められています。
Rust:最新バージョン v1.46.0 (2020/9/30 現在)
以下のページを参考にインストールしてください。
https://www.rust-lang.org/tools/install
もしも過去にRustをインストール済みの場合は、”rustup update“を実行し、Rustを最新バージョンに更新しておきましょう。
参照:https://docs.terra.money/contracts/tutorial/setup.html#install-rust
Rustのインストールが完了しているかどうか確認したい場合は、以下のコマンドで確認できます。
https://www.rust-lang.org/tools/install
もしも過去にRustをインストール済みの場合は、”rustup update“を実行し、Rustを最新バージョンに更新しておきましょう。
参照:https://docs.terra.money/contracts/tutorial/setup.html#install-rust
Rustのインストールが完了しているかどうか確認したい場合は、以下のコマンドで確認できます。
$ rustc --version rustc 1.46.0 (04488afe3 2020-08-24)
インストールが完了したら、以下のようにしてコンパイルのターゲットにWasmを追加します。
$ rustup default stable $ rustup target add wasm32-unknown-unknown
次に、cargo-generateというRust用のパッケージをインストールしましょう。このパッケージを使って今回使用するコントラクトのコードを簡単に用意できるようになります。
$ cargo install cargo-generate --features vendored-openssl
Rustに関しては以上です。
Go:v1.13.1以上が必要
以下のページを参考にインストールしてください。
インストール後は、”go version“でv1.13.1以上のバージョンがインストールされていることを確認してください。
https://golang.org/doc/install
インストール後は、”go version“でv1.13.1以上のバージョンがインストールされていることを確認してください。
https://golang.org/doc/install
今回、terra-project/core( https://github.com/terra-project/core)というプロジェクトでGoを扱います。Go自体はインストールできているのにうまく行かない場合はGOPATHの設定を見直してみてください。
Docker Compose
以下のページを参考にインストールしてください。
https://docs.docker.jp/compose/install.html
https://docs.docker.jp/compose/install.html
ドキュメントにもあるように、Docker Compose はDocker Engine を必要とします。したがって Docker Engine がインストール済であることを確認する必要があります。Docker for Mac や Docker for Windowsを利用しているのであれば、すでにDocker Composeは利用可能なはずです。
必要なツール
全体の流れで述べたように、ローカル用のTerraを立ち上げるLocalTerraと、その立ち上げたTerraとやりとりをするためのterracliを用意する必要があります。
LocalTerra
リポジトリ(https://github.com/terra-project/LocalTerra)をクローンした後、Docker Composeを使用してLocalTerraを立ち上げます。
$ git clone https://github.com/terra-project/localterra $ cd localterra $ docker-compose up
立ち上げが成功すると定期的に以下のようなブロック生成されているログが出力されるはずです。これでLocalTerraは起動完了です。
// 以下のようなログが出力されるはず。 ... terrad_1 | I[2020-09-30|08:16:27.566] Executed block module=state height=387 validTxs=0 invalidTxs=0 terrad_1 | I[2020-09-30|08:16:27.570] Committed state module=state height=387 txs=0 appHash=AE3048319FA8FDCB2B7341A696024EB6E3907B318E87843D290026B374017E3E ...
以下の公式ドキュメントにはポートの設定等をする説明がありますが、デフォルトでその設定になっているはずなので、特別変更を行う必要はありません。
参考:https://docs.terra.money/contracts/tutorial/setup.html#download-localterra
ブロックチェーンを”Ctrl+C“などで停止した後は、再度”docker-compose up“を実行すれば、停止したブロック高からチェーンを再開できますし、最初からチェーンを再起動したい場合は、一度”docker-compose down“を実行してから立ち上げることができます。
参考:https://docs.terra.money/contracts/tutorial/setup.html#download-localterra
ブロックチェーンを”Ctrl+C“などで停止した後は、再度”docker-compose up“を実行すれば、停止したブロック高からチェーンを再開できますし、最初からチェーンを再起動したい場合は、一度”docker-compose down“を実行してから立ち上げることができます。
terracli
LocalTerraにトランザクションを投げたりするためのterracliを利用するためには、Terra Core( https://github.com/terra-project/core)というリポジトリをクローンして、そこからterracliをインストールする必要があります。
今回はv0.4.0を扱います。リポジトリをクローンした後、v0.4.0にチェックアウトして、現在のリポジトリがv0.4.0になっていることを確認してください。
$ git clone https://github.com/terra-project/core/ $ cd core $ git checkout -b v0.4.0 v0.4.0 $ git branch develop * v0.4.0
次に、クローンしたTerra Coreを使ってterracliをインストールします。正確にはterradというブロックチェーンを立ち上げるためのツールもインストールされますが、今回はその役目をLocalTerraが担ってくれるので無視して大丈夫です。
// クローンしたリポジトリ以下で $ make install
以下のコマンドを実行できれば、terracliはインストール完了です。
$ terracli version --long name: terra server_name: terrad client_name: terracli version: 0.4.0 commit: 0ccb97e4427007f940b611fb0b5fb67d6a7d69f5 build_tags: netgo,ledger go: go version go1.15.1 darwin/amd64
versionが0.4.0となっていることを確認してください。(2020/9/30 現在) もし、0.4.0よりもバージョンが低い場合、coreの違うブランチで”make install“を実行した可能性があります。ブランチを切り替えてやり直しましょう。
ここまでで必要なツールは揃いました。お疲れ様です。
次からスマートコントラクトを書きます!
ここまでで必要なツールは揃いました。お疲れ様です。
次からスマートコントラクトを書きます!
コントラクトを書いてみる
Terraが用意してくれているサンプル用のコントラクトを用います。コントラクトを書くと言ったものの実は以下のテンプレートを使えば、今回自分でコントラクトを書く必要はありません。このテンプレート好きな場所に展開してください。
cargo generate --git https://github.com/CosmWasm/cosmwasm-template.git --branch 0.10 --name my-first-contract cd my-first-contract
どういったコントラクトを用意するのか
実際にテンプレートで用意したコントラクトの中身を見る前に、これらがどういった機能を持っているのかを確認します。
countという整数を持つ変数を設定し、そのcountを記録しておくというとてもシンプルなものとなっています。
主な機能としてこれら4つがあります。
- コントラクト作成時に渡した引数をcountとし、countの値を初期化する機能
- countの値をインクリメントする機能
- countの値を渡した引数で更新する機能(オーナーのみ利用可能)
- 現在のcountの値を参照する機能
以下ではコントラクトの基本設定と、それら4つの機能の実装について説明していきます。
実際のコントラクト
Cosmwasmの仕様に則り、以下の3つの関数を使用して、先にあげた4つの機能を扱えるようにする必要があります。
- init() : コントラクトをインスタンス化する際に呼び出されるコンストラクタ
- handle() : ユーザーがコントラクト上でメソッドを呼び出したい場合に実行される関数
- query() : ユーザーがコントラクトからデータを取得したい場合に実行される関数
src/libs.rsを見ると、3つの関数が定義されていることがわかります。
コントラクトのステート
コントラクト上で扱うステートに関して、src/state.rs に定義されています。
src/state.rs
src/state.rs
use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{CanonicalAddr, Storage}; use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; pub static CONFIG_KEY: &[u8] = b"config"; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct State { pub count: i32, pub owner: CanonicalAddr, }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] の部分では指定した特定のトレイトを、”State“構造体(struct)が取り込んで扱えるようにしています。これにより、”State“がSerializeやDeserializeを扱えるようになるため、コントラクト上で扱うデータを、Terraブロックチェーンのノードが保持しているLevelDBの仕様に合わせてバイトの形式に変換し、永続的に保持させられるようになります。また逆に現在ブロックチェーンが保持しているデータをコントラクト側で扱う場合は、取り出してきたデータをコントラクトが読める状態に変換し直して扱う形となります。
“State“構造体はi32型のcount、CanonialAddr型のownerを持っていることがわかります。このCanonicalAddrというのは、バイト形式で表現されたアドレスであり、ウォレットで扱われるようなterra..から始まる人間に読みやすい形に変換されたHumanAddrとは違う形式のものになります。
src/state.rs 続き
pub fn config<S: Storage>(storage: &mut S) -> Singleton<S, State> { singleton(storage, CONFIG_KEY) } pub fn config_read<S: Storage>(storage: &S) -> ReadonlySingleton<S, State> { singleton_read(storage, CONFIG_KEY) }
cosmwasm_storageのsingletonを使用して、用意したState構造体に対し、シンプルなget・set関数を定義しています。
コントラクト上で扱うステートに関しての部分は以上です。
ここからは、先程までに設定したStateを、どう変更するのか、またその変更を実行するにはどういうメッセージをコントラクトに対して送れば良いのかという部分を見ていきます。
ここからは、先程までに設定したStateを、どう変更するのか、またその変更を実行するにはどういうメッセージをコントラクトに対して送れば良いのかという部分を見ていきます。
InitMsg
Terraブロックチェーンでは、Ethereumとは異なり、コントラクトのコードのアップロードとコントラクトのインスタンス化は別々のイベントとみなされます。例えば、Ethereum上ではERC20トークンを発行する際、コントラクトデプロイ時に初期発行量やトークンの名前といったパラメータを渡し、渡されたパラメータを持つコントラクトが作成されるという形でした。Terra上では、パラメータを持たないコントラクトのコードをブロックチェーン上にアップロードし、アップロードされたコントラクトのコードを使用してコントラクトのインスタンスを作成し、そのインスタンスにパラメータを渡し利用可能なコントラクトが作成されるという手順を踏みます。後者の場合、アップロードされたコントラクトのコードをベースとして様々なパラメータを持ったコントラクトを作成することが可能となるのです。
InitMsgは、ユーザーがMsgInstantiateContractを通じてコントラクトのインスタンスを作成する際にinit()関数に渡されるJSONメッセージです。
InitMsgではパラメータを以下のようなJSONメッセージで提供することになります。この場合はcountの初期値が100ということですね。
{ "count": 100 }
src/msg.rsにてコントラクトに対して送信するメッセージの型を定義します。コントラクトを実行する際は、そのMessageの型に合わせたJSONメッセージを引数として用意する必要があります。
Messageの定義
src/msg.rs
src/msg.rs
use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InitMsg { pub count: i32, }
コントラクトの初期値にcountを設定していることがわかりますね。
ロジック部分の定義
init()を定義します。
先程定義したInitMsgからcountを読み取って初期値に設定していること、またownerにMsgInstantiateContractを実行したアカウントのアドレスを設定していることが以下よりわかります。
先程定義したInitMsgからcountを読み取って初期値に設定していること、またownerにMsgInstantiateContractを実行したアカウントのアドレスを設定していることが以下よりわかります。
src/contract.rs
use cosmwasm_std::{ to_binary, Api, Binary, Env, Extern, HandleResponse, InitResponse, Querier, StdError, StdResult, Storage, }; use crate::msg::{CountResponse, HandleMsg, InitMsg, QueryMsg}; use crate::state::{config, config_read, State}; pub fn init<S: Storage, A: Api, Q: Querier>( deps: &mut Extern<S, A, Q>, env: Env, msg: InitMsg, ) -> StdResult<InitResponse> { let state = State { count: msg.count, owner: deps.api.canonical_address(&env.message.sender)?, }; config(&mut deps.storage).save(&state)?; Ok(InitResponse::default()) }
HandleMsg
HandleMsgは、MsgExecuteContractを通じてhandle()関数に渡されるJSONメッセージです。InitMsgとは異なり、HandleMsgはいくつかの異なるタイプのメッセージとして存在することができ、コントラクト側でそのメッセージのタイプごとに実行する処理を分けるハンドラロジックを保持しています。
メッセージタイプ : Increment
誰もが現在のcountの値をインクリメントすることができます。
{ "increment": {} }
メッセージタイプ : Reset
オーナーのみがこのcountを特定の数字にリセットすることができます。
{ "reset": { "count": 5 } }
Messageの定義
コントラクトが複数のタイプのメッセージを扱えるようにするためにenumを使用します。
#[serde(rename_all = "snake_case")]を設定することによって、JSONメッセージとの間でスネークケースと小文字の変換を自動的にやってくれます。ここでは、IncrementとResetとあるようにスネークケースを使用していますが、上記のJSONメッセージではincrementとresetと小文字統一の表記になっているのはそのためです。
#[serde(rename_all = "snake_case")]を設定することによって、JSONメッセージとの間でスネークケースと小文字の変換を自動的にやってくれます。ここでは、IncrementとResetとあるようにスネークケースを使用していますが、上記のJSONメッセージではincrementとresetと小文字統一の表記になっているのはそのためです。
src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleMsg { Increment {}, Reset { count: i32 }, }
ロジック部分の定義
src/contract.rs
pub fn handle<S: Storage, A: Api, Q: Querier>( deps: &mut Extern<S, A, Q>, env: Env, msg: HandleMsg, ) -> StdResult<HandleResponse> { match msg { HandleMsg::Increment {} => try_increment(deps, env), HandleMsg::Reset { count } => try_reset(deps, env, count), } }
ここでは、受信したHandleMsgの中身がIncrementなのか、ResetなのかをRustのパターンマッチによって判別し、それに応じて、適切な処理ロジックを実行するようにルーティングしています。
Incrementの場合は、try_incrementを、Resetの場合は、try_resetの処理を実行しているのがわかります。それぞれの処理は以下の通りです。
pub fn try_increment<S: Storage, A: Api, Q: Querier>( deps: &mut Extern<S, A, Q>, _env: Env, ) -> StdResult<HandleResponse> { config(&mut deps.storage).update(|mut state| { state.count += 1; Ok(state) })?; Ok(HandleResponse::default()) }
ここでは、mut stateとある通り更新可能な形でstateを参照し、state内のcountを1追加し、現在のstate.countを更新しています。
その後、HandleResponseを引数としてOkを返すことで、処理の成功を確認して、コントラクトの実行を終了します。
その後、HandleResponseを引数としてOkを返すことで、処理の成功を確認して、コントラクトの実行を終了します。
ここの例では簡単にするために、HandleResponse::default()を使用していますが、自分でHandleResponseに対して以下の情報を渡す形に書き換えることもできます。
- message: MsgSend, MsgSwapなどのメッセージのリスト。これを使用してスマートコントラクトから、スマートコントラクト以外のTerraブロックチェーン上のモジュールに対して作用することができるようになります。
- log: クライアントが購読できる形にイベントを定義するためのキーバリューペアのリスト。これを定義することで、ブロックエクスプローラやアプリケーションがコントラクトの動きを解析し、実行中に発生した重要なイベントやステートの状態を報告することができるようになります。
- data: コントラクトに記録する追加のデータです。
pub fn try_reset<S: Storage, A: Api, Q: Querier>( deps: &mut Extern<S, A, Q>, env: Env, count: i32, ) -> StdResult<HandleResponse> { let api = &deps.api; config(&mut deps.storage).update(|mut state| { if api.canonical_address(&env.message.sender)? != state.owner { return Err(StdError::unauthorized()); } state.count = count; Ok(state) })?; Ok(HandleResponse::default()) }
reset のロジックはcountの値を更新するという点において、 increment と非常に似ています。
メッセージの送信者が reset 関数の呼び出しを許可されているオーナーかどうかを確認している点が大きな違いと言えますね。
メッセージの送信者が reset 関数の呼び出しを許可されているオーナーかどうかを確認している点が大きな違いと言えますね。
QueryMsg
JSONメッセージでこのようにリクエストすると
{ "get_count": {} }
以下のようにcountの値がJSONメッセージで返ってきます。
{ "count": 5 }
Messageの定義
自分たちで作成したコントラクトのデータに対して、クエリをサポートするためには、今までのMsg同様のリクエストのメッセージとは別に、出力(ここではCountResponse)を定義してあげる必要もあります。入力と同様にJsonメッセージを使って情報をユーザーに返す形になっていることがわかります。
src/msg.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { // GetCount returns the current count as a json-encoded number GetCount {}, } // We define a custom struct for each query response #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct CountResponse { pub count: i32, }
ロジック部分の定義
query() のロジックは メッセージに合わせて処理を振り分ける、handle() と似ているのですが、ユーザーがトランザクションを作成するものではないので、envという変数を処理振り分けの引数として扱っていないという違いなどがあります。
src/contract.rs
pub fn query<S: Storage, A: Api, Q: Querier>( deps: &Extern<S, A, Q>, msg: QueryMsg, ) -> StdResult<Binary> { match msg { QueryMsg::GetCount {} => to_binary(&query_count(deps)?), } } fn query_count<S: Storage, A: Api, Q: Querier>(deps: &Extern<S, A, Q>) -> StdResult<CountResponse> { let state = config_read(&deps.storage).load()?; Ok(CountResponse { count: state.count }) }
コントラクトのビルド
コントラクトをビルドするには、以下のコマンドを実行します。
$ cargo wasm
無事にビルドが成功すると、target/wasm32-unknown-unknown/release 以下に、my_first_contract.wasmというファイルが作成されているはずです。
ビルドを最適化する
wasmバイナリを可能な限り小さくして、ブロックチェーンにアップロードするデータの容量を小さくし、コントラクトを実行する手数料を最小限に抑えるために最適化を行います。
スマートコントラクトのプロジェクトフォルダのルートディレクトリで以下のコマンドを実行してください。
artifacts 以下に別のmy_first_contract.wasmが作成されているはずです。これがブロックチェーンにアップロードするファイルになります。
スマートコントラクトのプロジェクトフォルダのルートディレクトリで以下のコマンドを実行してください。
artifacts 以下に別のmy_first_contract.wasmが作成されているはずです。これがブロックチェーンにアップロードするファイルになります。
$ 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/rust-optimizer:0.10.3
Schemasを設定する
この後、terracliからクエリを投げるために、JSONのスキーマを設定してあげる必要があります。JSONスキーマの自動生成を利用するためには、スキーマが必要なデータ構造をそれぞれ登録する必要があります。examples/schema.rsを開くと、以下のように設定されていることが確認できます。テンプレートが用意されていました。
use std::env::current_dir; use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use mtt::msg::{CountResponse, HandleMsg, InitMsg, QueryMsg}; use mtt::state::State; fn main() { let mut out_dir = current_dir().unwrap(); out_dir.push("schema"); create_dir_all(&out_dir).unwrap(); remove_schemas(&out_dir).unwrap(); export_schema(&schema_for!(InitMsg), &out_dir); export_schema(&schema_for!(HandleMsg), &out_dir); export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(State), &out_dir); export_schema(&schema_for!(CountResponse), &out_dir); }
以下を実行してschemasをビルドすることにより、schemaディレクトリ以下に各メッセージに対応したファイルを作成してくれます。今回はこれもすでにテンプレート側が用意してくれているので、特に実行する必要はありません。
// テンプレが用意してくれているので今回は実行する必要なし $ cargo schema
コントラクトの実行
コントラクトをTerraにアップロードする前に、すでに紹介したLocalTerraが本当に立ち上がっているかを確認しましょう。
$ cd localterra $ docker-compose up
立ち上がったら、ブロックが生成され続け、ログが出力され続けているはずです。そのままにして、別のターミナルを開きます。
コントラクトをアップデートするために、terracliを使って、terra上のアカウントを作成しましょう。以下のようにterracliのコマンドを実行します。これは、ニーモニックを使ってアカウントをインポートするためのコマンドです。
// test1は好きな名前で大丈夫です。 $ terracli keys add test1 --recover
すると、以下のようにmnemonicを入力しろと指示されるので、以下と同様のニーモニックを入力してください。このニーモニックは、Localterra側が勝手に設定してくれているアカウントのニーモニックで、トランザクションを実行するためのluna(terraの基軸通貨)を十分に持っています。
> Enter your bip39 mnemonic satisfy adjust timber high purchase tuition stool faith fine install that you unaware feed domain license impose boss human eager hat rent enjoy dawn
アカウントが十分なlunaを持っているかは以下のコマンドで確認することができます。test1のところに、自分が作成したアカウント名を入れましょう。以下のようにそれぞれ持っている通貨の量が表示されるはずです。
$ terracli query account $(terracli keys show test1 -a) | address: terra1dcegyrekltswvyy0xy69ydgxn9x8x32zdtapd8 coins: - denom: ukrw amount: "1000000000000000000" - denom: uluna amount: "999998989418598" - denom: umnt amount: "1000000000000000000" - denom: usdr amount: "1000000000000000" - denom: uusd amount: "10000000000000000" public_key: terrapub1addwnpepqflqjs2z2l52fku85zh65cxt8ctm4yn4nk5l9xyv80jq5hvszddpck20sd0 account_number: 0 sequence: 525
コントラクトをアップロード
アカウントの設定ができたら、コントラクトのプロジェクトフォルダのartifacts以下、my_first_contract.wasmがある場所で以下を実行します。公式チュートリアルだと--fees=100000ulunaとなっているのですが、ガス代が足りなくてコントラクトアップロードできないと言われるので、ここでは適当に倍の200000ulunaに設定しています。
// my-first-contract/artifacts で実行 $ terracli tx wasm store my_first_contract.wasm --from test1 --chain-id=localterra --gas=auto --fees=200000uluna --broadcast-mode=block
以下のように出るのでyを入力してエンターで承認
confirm transaction before signing and broadcasting [y/N]: y
コントラクトが無事にアップロードできれば、以下のようにlogsが出力されます。
height: 6 txhash: 83BB9C6FDBA1D2291E068D5CF7DDF7E0BE459C6AF547EC82652C52507CED8A9F codespace: "" code: 0 data: "" rawlog: '[{"msg_index":0,"log":"","events":[{"type":"message","attributes":[{"key":"action","value":"store_code"},{"key":"module","value":"wasm"}]},{"type":"store_code","attributes":[{"key":"sender","value":"terra1dcegyrekltswvyy0xy69ydgxn9x8x32zdtapd8"},{"key":"code_id","value":"1"}]}]}]' logs: - msgindex: 0 log: "" events: - type: message attributes: - key: action value: store_code - key: module value: wasm - type: store_code attributes: - key: sender value: terra1dcegyrekltswvyy0xy69ydgxn9x8x32zdtapd8 - key: code_id value: "1" info: "" gaswanted: 681907 gasused: 680262 tx: nulltimestamp: ""
code_idの値が”1”となっていますね。
次のコマンドで、コントラクトのコードがブロックチェーン上にアップロードされていることを確認しましょう。以下のようにcodehashとcreatorが帰ってくれば、アップロードができています。
次のコマンドで、コントラクトのコードがブロックチェーン上にアップロードされていることを確認しましょう。以下のようにcodehashとcreatorが帰ってくれば、アップロードができています。
$ terracli query wasm code 1 codeid: 1 codehash: KVR4SWuieLxuZaStlvFoUY9YXlcLLMTHYVpkubdjHEI= creator: terra1dcegyrekltswvyy0xy69ydgxn9x8x32zdtapd8
コントラクトを作成
アップロードしたコントラクトのコードに、初期パラメータを与えて新しいコントラクトをチェーン上で扱えるようにしましょう。
$ terracli tx wasm instantiate 1 '{"count":0}' --from test1 --chain-id=localterra --fees=20000uluna --gas=auto --broadcast-mode=block
code_idの1に対して、InitMsgとして扱う{“count”:0}を渡しています。ここでも公式ドキュメントとは倍のfeesを設定しています。成功すると以下のように出力されます。
height: 41 txhash: AEF6F2FA570029A5D4C0DA5ACFA4A2B614D5811E29EEE10FF59F821AFECCD399 codespace: "" code: 0 data: "" rawlog: '[{"msg_index":0,"log":"","events":[{"type":"instantiate_contract","attributes":[{"key":"owner","value":"terra1dcegyrekltswvyy0xy69ydgxn9x8x32zdtapd8"},{"key":"code_id","value":"1"},{"key":"contract_address","value":"terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"}]},{"type":"message","attributes":[{"key":"action","value":"instantiate_contract"},{"key":"module","value":"wasm"}]}]}]' logs: - msgindex: 0 log: "" events: - type: instantiate_contract attributes: - key: owner value: terra1dcegyrekltswvyy0xy69ydgxn9x8x32zdtapd8 - key: code_id value: "1" - key: contract_address value: terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 - type: message attributes: - key: action value: instantiate_contract - key: module value: wasm info: "" gaswanted: 120751 gasused: 120170 tx: null timestamp: ""
ここで出力されているcontract_addressの値は、コントラクトを実行するのに扱うので重要です。上のterra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5の部分に当たります。
コントラクトが作成されたかを確認しましょう。最後のパラメータのcontract_addressは自分が作成したコントラクトのものを渡してください。
// contract_addressは自分の環境で出力されたものを渡す。 $ terracli query wasm contract terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5
コントラクトを実行
最後の最後です。コントラクトの実行を行います。以下の手順を実行してみます。
- countの値を5にリセット
- 二回インクリメントを行う
これを実行する前のcountは先程設定した初期値の0ですが、実行後にはcountの値は7になっていると考えられます。
まず以下のクエリを実行して初期化で0と設定したはずのcountの値を確認してみましょう。countが0だと返ってくるはずです。返ってくるJSONメッセージも定義した通りの形になっています。トランザクションを実行しているわけではないのでfeesのパラメータ指定も必要ありません。
query
まず以下のクエリを実行して初期化で0と設定したはずのcountの値を確認してみましょう。countが0だと返ってくるはずです。返ってくるJSONメッセージも定義した通りの形になっています。トランザクションを実行しているわけではないのでfeesのパラメータ指定も必要ありません。
query
$ terracli query wasm contract-store terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 '{"get_count":{}}' {"count":0}
次に以下のコマンドでcountを5にリセットします。先程と同様にcontract_addressは適宜正しいものを渡してください。
Reset
$ terracli tx wasm execute terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 '{"reset":{"count":5}}' --from test1 --chain-id=localterra --fees=2000000uluna --gas=auto --broadcast-mode=block
Msgの定義をした通り、Incrementの場合はcountを指定してあげる必要はないことがわかります。以下のコマンドを二回実行しましょう。
Increment
$ terracli tx wasm execute terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5 '{"increment":{}}' --from test1 --chain-id=localterra --gas=auto --fees=2000000uluna --broadcast-mode=block
最後に、再度クエリを叩いて、コントラクト上のcountの値を参照してみましょう。おそらくcountが7と返ってくるはずです。
他の引数を与えて、Resetを実行したり、queryを実行したりして、本当にIncrementでcountが1ずつ増えているかを確認してみてください。ちゃんと動いているものを触れるのだけでも楽しいですね。
おわりに
お疲れさまでした!
Terra、CosmWasm上でDappsを開発する基礎を学び、Terraのブロックチェーン上でスマートコントラクトを作成することができるようになりました。
Terra、CosmWasm上でDappsを開発する基礎を学び、Terraのブロックチェーン上でスマートコントラクトを作成することができるようになりました。
今回のコントラクトの内容としてはとても単純なものでしたが、以下リンクに他のコントラクトの実装例などがあるので、「もっと面白いものを作りたい!」と言う方はぜひそちらを参考に次のステップへと進んでみてください。
Github: terra-project/cosmwasm-contracts
https://github.com/terra-project/cosmwasm-contracts
https://github.com/terra-project/cosmwasm-contracts
ここまで来られた方は、ぜひTwitterやHashHub Researchのコミュニティで「Terraのスマートコントラクトの実装できた!」の一言でもいいので感想やフィードバックをいただけると嬉しいです。励みになります。
参考資料
Terra 公式ドキュメント
https://docs.terra.money/contracts/tutorial/
https://docs.terra.money/contracts/tutorial/
※免責事項:本レポートは、いかなる種類の法的または財政的な助言とみなされるものではありません。