Cosmos-sdkのチュートリアルを触ってみる その3
前回までは以下のリンクになります。
前回までで全体の3分の1ほどが終わりました。今回で半分ぐらいまで終わらせます。
続きの”Msgs and Handlers”からみていきましょう。
Msgs and Handlers | Cosmos SDK Documentation
Documentation for the Cosmos SDK and Gaia.
cosmos.network
Msgs and handlers
このページではMsgのtypeについてコードが貼ってありますが、慌てて写す必要はないです。説明のために使っているだけ。実際の実装は次のページで行います。
・Msgs
Msgsはステートの移り変わりを起こすものです。ユーザーネームの内容を変更するような指示のことがMsgと言うのであれば、前回の SetWhoisメソッドなどを実行させるものがMsgなのでしょうか。どうなんだろう。
MsgはTxs(トランザクション)に内包されています。このトランザクションって言うのはもちろん、ブロックチェーンの各クライアントからチェーンのネットワークにブロードキャストされる情報のことを指します。
Cosmos-sdkでは、MsgをTxsに”内包したり、そこから開く”らしいです。前回のモジュール間通信のためにAmino規格にエンコード、デコードしていたように、Msgをブロックチェーンネットワークに伝達するためにMsgをTxsの中に入れているという理解でいい気がします。厳密には全然違うと思いますが笑
アプリケーションチェーンの開発者としては、このMsgsのインターフェイスを定義してあげるだけでよく、Txsへの内包、ネットワークへの伝達部分はSDKがやってくれるみたいです。
Msgsは以下のインターフェイスは必ず満たす必要があります。次のページで実装を行うので、今以下のコードを書く必要はないです。ValidateBasic(),getSignBytes(), GetSigners()とバリデータの署名に関するものが定義されています。確かにこう見るとトランザクションの中身になっているのがわかりますね。
// Transactions messages must fulfill the Msg
type Msg interface {
// Return the message type.
// Must be alphanumeric or empty.
Type() string// Returns a human-readable string for the message, intended for utilization
// within tags
Route() string// ValidateBasic does a simple validation check that
// doesn’t require access to any other information.
validateBasic() Error// get the canonical byte representation of the Msg.
getSignBytes() []byte// Signers returns the addrs of signers that must sign.
// CONTRACT: All signatures must be present to be valid.
// CONTRACT: Returns addrs in some deterministic order.
GetSigners() []AccAddress
}
・Handlers
Handlersは、Msgをネットワークから受け取った際に実行するアクション(どのデータをどのようにアップデートして、それらをどういう状態で管理すべきか)を定義する場所です。
ユーザーがアプリとやりとりするために用意するMsgsには二種類あります。SetNameとBuyNameです。それぞれのMsgsに対してHandlerを用意することになります。
MsgsとHandlersについて少しは理解が深まったかと思います。まず、SetNameのMsgとHandlerを実装していきますよ!
SetName
・Msg
SetNameのためのMsgを実装するわけですが、Msgの名前の付け方として、「Msg+Actionの名前」とする習慣があるので、今回はMsgSetNameを実装していくことになります。一応、名前を確認しておかないと色々と厄介みたいなので。
MsgSetNameは、ユーザーネームの所有者にそのネームの中の情報を設定させるものとなっています。
新しいmsg.goファイルを./x/nameservice/types/msg.goに作って、以下のコードを書いていきましょう。
package typesimport (
sdk “github.com/cosmos/cosmos-sdk/types”
)const RouterKey = Modulename // this was defined in your key.go file// MsgSetName defines a Setname message
type MsgSetname struct {
Name string ‘json:”name”’
Value string ‘json:”value”’
Owner sdk.AccAddress ‘json:”owner”’
}// NewMsgSetName is a constructor function for MsgSetName
func NewMsgSetName(name string, value string, owner sdk.AccAddress) MsgSetName {
return MsgSetName{
Name: name,
Value: value,
Owner: owner,
}
}
Modulenameという変数は、key.goの中で前回設定したものです。このModulenameを用いて、KVStore内の変更等を行なっていましたね。今回もこれがMsg周りを扱う権限になっていくんでしょうか。
MsgSetNameには3つの属性があります。
name: 設定しようとしているユーザーネーム
value : 前回終わりに話をした、用途に合わせたユーザーネームの形(ここちょっと解釈に自信ない)
owner: そのネームの所有者
次に、Msgのインターフェイスを実装します。
// Route should return the name of the module
func (msg MsgSetName) Route() string { return RouterKey }// Type should return the action
func (msg MsgSetName) Type() { return “set_name” }
Route()関数は、Msgを適切なモジュールにルーティングして処理させるためのものです。あとで データベースのタグとして索引に使いやすくするために、人間に読めるものを設定しておきましょうとのことです。ユーザーネームのことかな。
// ValidateBasic runs stateless checks on the message
func (msg MsgSetName) ValidateBasic() sdk.Error {
if msg.Owner.Empty() {
return sdk.ErrInvalidAddress(msg.Owner.String())
}
if len(msg.Name) == 0 || len(msg.Value) == 0 {
return sdk.ErrUnknownRequest("Name and/or Value cannot be empty")
}
return nil
}
ValidateBasic()メソッドは、Msgの中にちゃんとそれぞれの属性があるかのチェックを行うためのものになります。存在していなかった場合は、sdk.Errorという型が返されているのがわかります。アプリ開発者が頻繁に遭遇すると考えられるエラーの型をSDK側が既に用意してくれています。問題ない場合は、nilを返していますね。
// GetSignBytes encodes the message for signing
func (msg MsgSetName) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}
GetSignBytesメソッドでは、Msgが署名のためにどのようにエンコードされるかを定義します。ほとんどの場合、JSON形式で整列させるらしいです。ここら辺の標準仕様を把握してないのでよくわからないが、そういうものらしい。GetSignBytesって名前で、返り値がbyteの配列だから、各バリデータからの署名がずらっと入ってるイメージだけど合っているかな?間違ってそうなら戻ってきて修正します。
// GetSigners defines whose signature is required
func (msg MsgSetName) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Owner}
}
GetSignersメソッドでは、そのトランザクションが妥当であるとされるために、誰からの署名が必要になるかを定義します。例えばこのMsgSetNameの場合、ユーザーネームの中の情報を書き換えようとする場合、そのネーム所有者による署名が必要というように設定するということです。
・Handler
MsgSetNameを記述したので、次はそのmessageを受け取った時にどうactionが扱われるかを定義して行きます。
handler.goを./x/nameservice/handler.goに作成しましょう。これもkeep.goと同じ階層です。Msgsとは違う場所なので気をつけてね!
package nameserviceimport (
"fmt"sdk "github.com/cosmos/cosmos-sdk/types"
)// NewHandler returns a handler for "nameservice" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgSetName:
return handleMsgSetName(ctx, keeper, msg)
default:
errMsg := fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type())
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
NewHandlerは、このnameserviceモジュールに入ってきたメッセージを正しいHandler対して送るサブルーター的な役割を持っています。現時点では、MsgとHandlerは一つずつのはずです。これからMsgの種類が増えると、それに合わせてHandlerも増える可能性があるようです。
MsgSetNameという名前のメッセージがちゃんときている時だけHandlerが正しく返されているのが確認できます。
次に、handleMsgSetName関数の中で、MsgSetNameを扱うためのロジックを定義していきます。
ちなみに、Msgの時に「Msg+Actionの名前」として名前を決めたように、handlerの名前は、「handleMsg+Actionの名前」という法則で決めることになっています。
// Handle a message to set name
func handleMsgSetName(ctx sdk.Context, keeper Keeper, msg MsgSetName) sdk.Result {
if !msg.Owner.Equals(keeper.GetOwner(ctx, msg.Name)) { // Checks if the msg sender is the same as the current owner
return sdk.ErrUnauthorized("Incorrect Owner").Result() // If not, throw an error
}
keeper.SetName(ctx, msg.Name, msg.Value) // If so, set the name to the value specified in the msg.
return sdk.Result{} // return
}
この関数では、Msgの送り主が、SetNameしようとしてるユーザーネームの所有者であるかどうかを確認しています。ちゃんと所有者であれば、前回実装した、KeeperのSetName関数を呼び出して名前の設定をすることができます。違ければ、”所有者じゃなくね?”っているエラーを返しています。
これでネームの所有者は、SetNameなどを使って設定変更を行うことが可能になりました。所有者のいないネームの処理に関してはまだなので、次のセクションで、BuyName メッセージを定義していきます!
BuyName
ではユーザネームを買うためのMsgを定義していきましょう。先ほどのSetNameと同じmsgs.goファイルに追記していきます。./x/nameservice/types/msgs.goファイルです。SetNameとかなり似たコードになります。
// MsgBuyName defines the BuyName message
type MsgBuyName struct {
Name string 'json:"name"'
Bid sdk.Coins 'json:"bid"'
Buyer sdk.AccAddress 'json:"buyer"'
}// NewMsgBuyName is the constructor function for MsgBuyName
func NewMsgBuyName(name string, bid sdk.Coins, buyer sdk.AccAddress) MsgBuyName {
return MsgBuyName{
Name: name,
Bid: bid,
Buyer: buyer,
}
}// Route should return the name of the module
func (msg MsgBuyName) Route() string { return RouterKey }// Type should return the action
func (msg MsgBuyName) Type() string { return "buy_name" }// ValidateBasic runs stateless checks on the message
func (msg MsgBuyName) ValidateBasic() sdk.Error {
if msg.Buyer.Empty() {
return sdk.ErrInvalidAddress(msg.Buyer.String())
}
if len(msg.Name) == 0 {
return sdk.ErrUnknownRequest("Name cannot be empty")
}
if !msg.Bid.IsAllPositive() {
return sdk.ErrInsufficientCoins("Bid must be positive")
}
return nil
}// GetSignBytes encodes the message for signing
func (msg MsgBuyName) GetSignBytes() []byte {
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}// GetSigners defined whose signature is required
func (msg MsgBuyName) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Buyer}
}
ほぼMsgSetNameと同じですね。OwnerがBuyerになっているのが確認できます。MsgBuynameの属性のBidがsdk.Coinsとなっており、これから買うネームの価格の情報が入っていると考えられます。Bidはつけ値という意味です。
MsgSetNameに対するHandlerを用意したのと同様に、今回もMsgBuyNameのためのHandlerを実装していきます。./x/nameservice/handler.goに追記していきましょう。
NewHandler関数は先ほど実装したので、その中を書き換える形になります。
// NewHandler returns a handler for "nameservice" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgSetName:
return handleMsgSetName(ctx, keeper, msg)
case MsgBuyName:
return handleMsgBuyName(ctx, keeper, msg)
default:
errMsg := fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type())
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
先ほども説明したように、これでnameserviceモジュールがMsgBuyNameのメッセージを受け取った時に意図した通りのアクションが実行されることになります。
この時点で、Msgに記述したValidateBasic関数が実行されているため、ちゃんとそのMsgに中身があるかどうかのチェックは住んでいますが、ValidateBasic関数からでは、アプリのステート情報を参照することはできないので、ネットワークの状態に依存する検証ロジックは、ハンドラー関数側で実装するべきらしいです。
少しわかりにくいですが、例えば、MsgBuyNameにちゃんと指定された型の値が入っているとしても、そのネームを買うメッセージを飛ばした人がちゃんと、ネームを買うのにnamecoinを必要なだけ持っているかはアプリのステート情報を参照しないと確認することができないので、その確認はハンドラー関数側で実装しましょうということです。上記のコードでMsgBuyNameのメッセージを受け取った時に実行されるHandler関数を以下のように実装します。
// Handle a message to buy name
func handleMsgBuyName(ctx sdk.Context, keeper Keeper, msg MsgBuyName) sdk.Result {
if keeper.GetPrice(ctx, msg.Name).IsAllGT(msg.Bid) { // Checks if the bid price is greater than the price paid by the current owner
return sdk.ErrInsufficientCoins("Bid not high enough").Result() // If not, throw an error
}
if keeper.HasOwner(ctx, msg.Name) {
err := keeper.coinKeeper.SendCoins(ctx, msg.Buyer, keeper.GetOwner(ctx, msg.Name), msg.Bid)
if err != nil {
return sdk.ErrInsufficientCoins("Buyer does not have enough coins").Result()
}
} else {
_, err := keeper.coinKeeper.SubtractCoins(ctx, msg.Buyer, msg.Bid) // If so, deduct the Bid amount from the sender
if err != nil {
return sdk.ErrInsufficientCoins("Buyer does not have enough coins").Result()
}
}
keeper.SetOwner(ctx, msg.Name, msg.Buyer)
keeper.setPrice(ctx, msg.name, msg.Bid)
return sdk.Result{}
}
最初に、今回のメッセージのBid、つまりネームへのつけ値が現在の価格よりも高くなっているかを確認しています。次に、そのネームが既にオーナーがいるかどうかを確認し、もしオーナーがいる場合は、オーナーが買い手からその額を受け取ることになっています。
もしまだオーナーが存在指定ないネームであれば、今実装しているnameserviceモジュールが買い手が支払ったコインを回復不可能なアドレスに送り、実質Burnを行います。
SubtractCoins(Burnするための関数と思われる)とSendCoins(送金)の関数はどちらもちゃんとエラーを返す仕様となっており、それでステートの変更はなかったことにします。エラーが起きなかった場合(ここでは買い手がちゃんと必要なだけコインを所持していた場合)は、Keeperで定義されたメソッドを呼び出して、ハンドラーはこの買い手を新しい所有者に設定し、つけ値を現在のネームの価格に設定します。
コインの送金部分には、Keeper.coinKeeperという系列のメソッドを使っていることが確認できます。keeper.goに戻ってcoinKeeperを見に行くと、coinKeeperとはbank.Keeperを使っていることがわかりました。handlerからnameserviceモジュールのKeeperに接続して、そこから別のモジュール、bankモジュールのKeeperにアクセスしてコインを動かさせてもらっているわけです。おさらいでした。
以上がMsgsとHandlersの実装でした。ここまでやれたので次はこれら設定したトランザクションからアプリの情報をクエリで扱える方法を学んでいきましょう。
Queriers
クエリでアプリの情報を参照できるように実装していきます。
DBを扱ってwebアプリ作ったりしてたので、なんとなくわかってはいるのですが、じゃあクエリってなんだと聞かれると、うまく答えられないので以下のリンク読んでました。
・Query Types
querier.goを./x/nameservice/types/querier.goに作って、ここにクエリの型を定義していきます。
package typesimport "strings"// Query Result Payload for a resolve query
type QueryResResolve struct {
Value string 'json:"value"'
}// implement fmt.Stringer
func (r QueryResResolve) String() string {
return r.Value
}// Query Result Payload for a names query
type QueryResNames []string// implement fmt.Stringer
func (n QueryResNames) String() string {
return strings.Join(n[:], "\n")
}
QueryResResolveやQueryResNamesの型の設定を行なって、その中身を参照するための関数も用意しています。
・Querier
型をどうするかの設定が終わったので、次にどのクエリがアプリのステートに対して実行されるかを定義する場所を用意してあげます。./x/nameservice/querier.goを用意します。先ほどの./x/nameservice/types/querier.goとは違うファイルになるので気をつけてください。このnameserviceモジュールでは以下の3つのクエリを用意します。
resolve: “name”を指定すると、nameserviceにあるその”value”を返すもの。この”value”はwhoisの中の一属性ですね。
whois: “name”を指定すると、そのネームの”price”、”value”、”owner”を返すもの。あなたがあるネームを買いたい場合に、そのネームがいくらか調べるために使われます。
name: 指定する値はなく、このクエリを実行させると”nameservice”にある全てのネームを返してくれます。
NewQuerier関数を定義していきますが、先ほどMsgsに対してHandlerを設定したように、それぞれのクエリに対して、それをルーティングするためのNewQuerier関数がある感じです。
Handlerがmsg.(type)という形でそれぞれのMsgを判別していましたが、クエリに関してはそのようなインターフェイスがないため、手動でその仕組みも定義していく必要があります。
package nameserviceimport (
"github.com/cosmos/cosmos-sdk/codec"sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
)// query endpoints supported by the nameservice Querier
const (
QueryResolve = "resolve"
QueryWhois = "whois"
QueryNames = "names"
)// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.ReauestQuery) (res []byte, err sdk.Error) {
swith path[0] {
case QueryResolve:
return queryResolve(ctx,path[1:], req, keeper)
case QueryWhois:
return queryWhois(ctx, path[1:], req, keeper)
case QueryNames:
return queryNames(ctx, req, keeper)
default:
return nil, sdk.ErrUnknownRequest("unkown nameservice query endpoint")
}
}
}
ルーティングの設定が終わったので、次にそれぞれのクエリに対して何が実行されるのかを定義していきます。Handlerで見た流れですね。
// nolint: unparam
func queryResolve(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
value := keeper.ResolveName(ctx, path[0]) if value == "" {
return []byte{}, sdk.ErrUnkownRequest("could not resolve name")
} res, err := codec.MarshalJSONIndent(keeper.cdc, QueryResResolve{value})
if err != nil {
panic("could not marshal result to JSON")
} return res, nil
}// nolint: unparam
func queryWhois(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) ([]byte, sdkError) {
whois := keeper.GetWhois(ctx, path[0])res, err := codec.MarshalJSONIndent(keeper.cdc, whois)
if err != nil {
panic("could not marshal result to JSON")
} return res, nil
}func queryNames(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, sdk.Error) {
var namesList QueryResNames iterator := keeper.GetNamesIterator(ctx) for ; iterator.Valid(); iterator.Next() {
nameList = append(namesList, string(itetator.Key()))
} res, err := codec.MarshalJSONIndent(keeper.cdc, namesList)
if err != nil {
panic("could not marshal result to JSON")
} return res, nil
}
上記のクエリを受けて実行される関数を簡単に説明していきます。
それぞれよく見ると、Keeperで設定したGet〜関数やSet〜関数が多用されていることがわかります。もし、このチュートリアル以外で別のアプリケーションを構築する際には、Keeperに戻って、必要なGetterやSetterの部分を自分で定義する必要があります。
慣例にならって、クエリに対する出力の型は、JSONの形式か、文字列化可能な形式であるべきです。今回これらの関数の返り値のByteは、JSONエンコードしたものになります。(API等で返されるJSON特有の規格ってことですかね。)
そのため、queryResolve関数の出力の型に関しては、valueという変数をQueryResResolveという名前の構造体の中に入れています。これは、QueryResResolveがJSONの形式にすることが可能かつ、.String()メソッドを持つためです。ここら辺の型はtypesの下のquerier.goで設定しましたね。
次にqueryWhois関数の出力値については、既にJSON形式化することが可能ですが、それに対して、.String()メソッドを追加する必要があります。
queryNames関数の出力についても同様に[]stringの段階でマーシャリングが可能なのですが、.String()メソッドを追加する必要があるので変換しています。
ちょいちょい出てくるマーシャリングという言葉の確認
これであなたはモジュールの中のステートを変化させたり、参照することができるよういなりました。今回はここまでです!お疲れ様でした。
次回の続きでは、それらをより簡単に扱うためにエイリアスを設定するところから始めます!これで全体の半分くらいですかね。
前回同様、時間が経つと飽きてて描かなくなるので、木曜日7月18日までに続きのその4を公開することにします。お楽しみに!!
(追記)次回の記事はこちらです
お知らせ
■HashHubでは入居者募集中です!
HashHubは、ブロックチェーン業界で働いている人のためのコワーキングスペースを運営しています。ご利用をご検討の方は、下記のWEBサイトからお問い合わせください。また、最新情報はTwitterで発信中です。
HashHub:https://hashhub.tokyo/
Twitter:https://twitter.com/HashHub_Tokyo
■ブロックチェーンエンジニア集中講座開講中!
HashHubではブロックチェーンエンジニアを育成するための短期集中講座を開講しています。お申込み、詳細は下記のページをご覧ください。
ブロックチェーンエンジニア集中講座:https://www.blockchain-edu.jp/