Cosmos-sdkのチュートリアルを触ってみる その4

Takuya Fujita
GBEC Tech Blog
Published in
31 min readJul 18, 2019

前回までは以下のリンクになります。

前回までで全体の半分ほどが終わりました。
続きのAliasから再開していきます。以下のリンクからです!長くなってますが、全体で何をやろうとしてるのかはわかってきたと思います。そうなりゃチュートリアルとしては半分ほどですが、もう9割終わってるようなもんですこっちのもんです!

今回の記事では、CLIやRESTからアプリチェーンに、前回設定したMsgやQueryを飛ばして指示させる部分を実装していきます。あれ?もはやブロックチェーン自体は関係なくねぇ?って感じるかもしれませんが、あなたが作ってるのは”アプリケーションチェーン”なわけなので、今後アプリにするためには大変重要な部分なわけです。

頑張って続きをやっていきましょう!

Alias

./x/nameservice/alias.goファイルを作っていきましょう。このファイルがある主な理由は、”import cycles”を防ぐためです。なんじゃそれと思って調べたら、簡単にまとめてある記事を見つけました。

“AがBをimportして、BがAをimportしちゃだめ”とのことです。この場合は2つのファイルによるサイクルですが、3つ、4つ、それ以上でサイクルが発生している場合もimport cyclesと呼ぶみたいです。

まず、今までの記事で僕らが作ったtypeフォルダーをimportしてみましょう。

package nameserviceimport (
"github.com/cosmos/sdk-application-tutorial/x/nameservice/types"
)

このサンプルコードだと、githubから取ってきてますが、直接自分で作ったローカルファイルのインポートでもいいはずです。

これから、僕たちはこのalias.goファイルの中で三つの種類の”型”を作っていきます。

  1. 定数です。あなたが今後変えることのない値を定義する場所です。
const (
ModuleName = types.ModuleName
RouterKey = types.RouterKey
StoreKey = types.StoreKey
)

どうやら、typesの中で出てきたものを、定数、変数、型で三つに大きく分けているようです。確かにModuleName,RouterKey,は定数ですね。というか確か全部ModuleNameを介して”nameservice”っていうstringが入っているんでしたよね。

2. 変数

var (
NewMsgBuyName = types.NewMsgBuyName
NewMsgSetName = types.NewMsgSetName
NewWhois = types.NewWhois
ModuleCdc = types.ModuleCdc
ResisterCodec = types.RegisterCodec
)

3. 型 ここにtypeフォルダであなたが作った型を定義していきます。

type (
MsgSetName = types.MsgSetName
MsgBuyName = types.MsgBuyName
QueryResResolve = types.QueryResResolve
QueryResNames = types.QueryResNames
Whois = types.Whois
)

必要な定数、変数、型のエイリアス設定が終わりました。なんか全然大したことしてないな。

でもこれでモジュールの作成へと前に進むことができます。

次は Aminoのエンコードフォーマットの中であなたのアプリチェーンに合わせた型を登録していきます!

Codec File

自分で作成した型をAminoの形式にエンコード、デコードできるようにするためには、codec.goファイルを作って設定して上げる必要があります。./x/nameservice/types/codec.goにファイルを作りましょう。

作成したインターフェイスや、インターフェイスを実装する構造体の場合は、RegisterCodec関数の中で宣言してあげる必要があります。このnameserviceモジュールでは、前回の記事その3で作った2つのMsg(SetNameとBuyName)を宣言して登録する必要があります。一方で、Whoisクエリの戻り値の型の登録は行う必要がないらしいです。後で使うことになるモジュール特定のCodecを設定していきます。

package typesimport (
"github.com/cosmos/cosmos-sdk/codec"
)
var ModuleCdc = codec.New()func init() {
RegisterCodec(ModuleCdc)
}
// RegisterCodec registers concrete types on the Amino codec
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgSetName{}, "nameservice/SetName", nil)
cdc.RegisterConcrete(MsgBuyName{}, "nameservice/BuyName", nil)
}

次はnameserviceモジュールとコマンドラインを繋いでいきます。

Nameservice Module CLI

Cosmos SDKでは、CLIとの相互作用のために、cobraというライブラリを使用します。このライブラリは各モジュールの独自のコマンドを公開するのを容易にしてくれるらしです。ちょっとよくわからない。まぁやってみます。

以下にそれぞれのファイルを用意しましょう。

./x/nameservice/client/cli/query.go

./x/nameservice/client/cli/tx.go

clientファイル,cliファイルは初めて出てきましたね。

・Queries

まずquery.goからです。ここでは各モジュールに対して、cobra.Commandsを定義します。

package cliimport (
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/sdk-application-tutorial/x/nameservice/types"
"github.com/spf13/cobra"
)
func GetQueryCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
nameserviceQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the nameservice module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
nameserviceQueryCmd.Addcommand(client.GetCommands(
GetCmdResolveName(storeKey, cdc),
GetCmdWhois(storeKey, cdc),
GetCmdNames(storeKey, cdc),
)...)
return nameserviceQueryCmd
}
// GetCmdResolveName queries information about a name
func GetCmdResolveName(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "resolve [name]",
Short: "resolve name",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := cotext.NewCLIContext().WithCodec(cdc)
name := args[0]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/resolve/%s", queryRoute, name), nil)
if err != nil {
fmt.Printf("could not resolve name - %s \n", name)
}
var out types.QueryResResolve
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdWhois queries information about a domain
func GetCmdWhois(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "whois [name]",
Short: "Query whois info of name",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
name := args[0]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/whois/%s", queryRoute, name), nil)
if err != nil {
fmt.Printf("could not resolve whois - %s \n", name)
return nil
}
var out types.Whois
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}
// GetCmdNames queries a list of all names
func GetCmdNames(queryRoute string, cdc *codec.Codec) *cobra.Commnad {
return &cobra.Command{
Use: "names",
Short: "names",
// Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/names", queryRoute), nil)
if err != nil {
fmt.Printf("could not get query names\n")
return nil
}
var out types.QueryResNames
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}

ここでもチュートリアルとして"github.com/cosmos/sdk-application-tutorial/x/nameservice/types" からのimportをしていますが、自分が作成したtypeフォルダを参照で問題ないはずです。

長いですね。コードを簡単に説明します。

CLIはCLIContextという新しい”context”を導入する。とありますね。
この場合のcontextは以下の記事が参考になると思います。

これが、CLIとの対話に必要なユーザーの入力と、アプリ構成に関するデータを運んでくれるらしいです。イメージでしか話できてないけど大丈夫かな。

「下三つの関数にあるcliCtx.QueryWithData()関数に必要なパスは、引数として渡しているクエリのルーターにある名前の場所に直接マップされます。」この説明をみて言ってることは理解できるのですが、このコードが何を意図しているのかがよくわからないな。

指定されているpathの最初の部分”custom”はこのSDKのアプリチェーンへのクエリとして、”Queriers”がクエリの型を区別するためのものらしいです。

pathの二つ目の部分、このコード内だと%sとなっていて、変数queryRouteが代入される部分ですが、QueryRouteにも変数storeKeyが代入され、storeKeyには文字列”nameservice”が入ることになっています。

nameserviceモジュールのことを指しているみたいです。このアプリチェーンに来たクエリをさらにnameserviceモジュールに渡すわけですね。

ついに、モジュールにあるQuerierが、三つ目の部分を参照して、どの処理を実行させるかを決めます。”ついに”とか言ってますし、これは前回その3で僕らが作ったQuerierのことでしょうね。

GetCmdResolveName関数と、GetCmdWhois関数のpathには4つ目のpathがありますが、ここにはクエリに渡す引数を指定してあげればいいようです。2つ以上の引数があるクエリの場合は、.QueryWithData関数を使ってあげれば上手く渡せるようになっているみたいです。(その例はここを見てね!ってリンクが貼ってあるもののリンク先が切れている…..!!)

・Transactions

クエリのCLIとの対話は定義できたので、次はトランザクションの生成をやります。
これもCLIからってことなのかな。

自分で今まで書いたnameserviceモジュールを使うから、サンプルのコードには”github.com/cosmos/sdk-application-tutorial/x/nameservice”って書いてるけど、”github.com/{.Username}/{.Project.Repo}/x/nameservice”と自分の書いたモジュール参照でもいいよ!とのことです。今までも何度か同じこと僕言いましたね。

説明が抜けてますが、場所は./x/nameservice/client/cli/tx.goにファイルを作ることになります!

package cliimport (
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/content"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/sdk-application-tutorial/x/nameservice/types"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
)
func GetTxCmd(storeKey string, cdc *codec.Codec) *cobra.Command {
nameserviceTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Nameservice transaction subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
nameserviceTxCmd.AddCommand(client.PostCommands(
GetCmdBuyName(cdc),
GetCmdSetName(cdc),
)...)
return nameserviceTxCmd
}
// GetCmdBuyName is the CLI command for sending a BuyName transaction
func GetCmdBuyName(cdc *codec.Codec) * cobra.Command {
return &cobra.Command{
Use: "buy-name [name] [amount]",
Short: "bid for existing name or claim new name",
Args: cobra.ExactArgs(2)
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))if err := cliCtx.EnsureAccountExists(); err != nil {
return err
}
coins, err := sdk.ParseCoins(args[1])
if err != nil {
return err
}
msg := types.NewMsgBuyName(args[0], coins, cliCtx.GetFromAddress())
err = msg.ValidateBasic()
if err != nil {
return err
}
cliCtx.PrintResponse = truereturn utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
// GetCmdSetName is the CLI command for sending a Setname transaction
func GetCmdSetName(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "set-name [name] [value]",
Short: "set the value associated with a name that you own",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc)
txBldr := auth.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))if err := cliCtx.EnsureAccountExists(); err != nil {
return err
}
msg := types.NewmsgSetName(args[0], args[1], cliCtx.GetFromAddress())
err := msg.ValidateBasic()
if err != nil {
return err
}
cliCtx.PrintResponse = true// return utils.CompleteAndBroadcastTxCLI(txBldr, cliCtx, msgs)
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

Queryの解説をやった後だと、このトランザクションのコードが何をやりたいのかが結構わかりますね。Useと書いてあるのがCLI上での指示の出し方で、その中の[name]などが、パラメータとしてMsgの中に入ってくるわけですね。Shortってのは各コマンドの簡単な説明で、Argsはその渡したパラメータが入ることになるわけか。引数のないQueryやTxだとArgsが必要ない理由がわかりました。

ここでは”authcmd”というパッケージを使っています。ここに使い方が乗ってるとのこと。これは、CLI上からアカウントへのアクセスを提供し、簡単に署名できるようにしてくれてます。

次は、作ったnameserviceモジュールをRESTクライアントから叩けるようにします!

Nameservice Module Rest Interface

僕らが作ったNameserviceモジュールはRESTインターフェイスを公開することができるらしいです。今まで何も考えずにREST API叩いてたので、そもそもRESTってなんなんだという復習から

”そのサービスのURIにHTTPメソッドでアクセスすることでデータの送受信を行う”的なことが書いてありますなるほど。

HTTP handkerなるものをnameserviceモジュール内に作っていくらしい。
これは今までのHandlerとかQuerierとかと同じルーティング的なものになりそうだな。

./x/nameservice/client/rest/rest.goにファイルを作っていきます。今回もnameserviceのモジュールは自分のに書き換えてOK。まずはimportsとconstから。

package restimport (
"fmt"
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/sdk-application-tutorial/x/nameservice/types"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/gorilla/mux"
)
const (
restName = "name"
)

・RegisterRoutes

まず、ResisterRoutes関数の中にRESTクライアントのインターフェイスを定義していきます。名前が他のモジュールのルートと一緒にならないように、全てのルートを自分のモジュール名で始める決まりらしいです。上のコードに追記しましょう。

// RegisterRoutes -Central function to define routes that get registered by the main application
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, storeName string) {
r.HandleFunc(fmt.Sprintf("/%s/names", storeName), namesHandler(cliCtx, storeName)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/names", storeName), buyNameHandler(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/names", storeName), setNameHandler(cliCtx)).Methods("PUT")
r.HandleFunc(fmt.Sprintf("/%s/names/{%s}", storeName, restName), resolvenameHandler(cliCtx, storeName)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/names/{%s}/Whois", storeName, restName), whoIsHandler(cliCtx, storename)).methods("GET")
}

最後にGET,POST,PUTとかあるので、あ〜本当にREST APIのやつだとなってます。
これらはQueryとTxへの処理が混ざってるので、設定したRESTのインターフェイスが叩かれた時にどう挙動するのかをそれぞれ定義していきます。

・Query Handlers

CLIの時と同じような感じでQueryを叩く処理に変換させる部分を追記します。

func resolveNameHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
paramType := vars[restName]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/resolve/%s", storeName, paramType), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func whoIsHandler(cliCtx context.CLIContext, storeName string) http.handlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
paramType := vars[restName]
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/whois/%s", storeName, paramType), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func namesHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/names", storeName), nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}

CLIからQueryの設定した時と同じで、cliCtx.QueryWithData関数を使ってますね!!あのpathの説明を詳しくしたところです!これらの機能はCLIの設定で扱ったものとほとんど同じです!

・Tx Handlers

次はbuyNameとsetNameのトランザクションのためのルートの用意です。

以下では実際には名前を購入したり設定するためのトランザクションを直接送信していないということに注意してください。httpリクエストと共に、署名するための秘密鍵を送ることなどはセキュリティ的な問題が発生しますよね。代わりに、これらのエンドポイントはそれぞれ特定のやりとりを構築して返してきます。その後、トランザクションは安全な方法で署名をすることができ、作成されたトランザクションがエンドポイントを使って、ネットワークにブロードキャストされるそうです。

RESTなりに署名の仕方に工夫があるようですね。

type buyNameReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Name string `json:"name"`
Amount string `json:"amount"`
Buyer string `json:"buyer"`
}
func buyNameHandler(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req buyNameReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request")
return
}
baseReq := req.BaseReq.Sanitize()
if !baseReq.ValidateBasic(w) {
return
}
addr, err := sdk.AccAddressFromBech32(req.Buyer)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
coins, err := sdk.ParseCoins(req.Amount)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// create the message
msg := types.NewMsgBuyName(req.Name, coins, addr)
err = msg.ValidateBasic()
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
}
}
type setNameReq struct {
BaseReq rest.BaseReq `json:"base_req"`
Name string `json:"name"`
Value string `json:"value"`
Owner string `json:"owner"`
}
func setNameHandler(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req setNameReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
rest.WriteErrorResponse(w, http.StatusBadRequest, "failed to parse request")
return
}
baseReq := req.BaseReq.Sanitize()
if !baseReq.ValidateBasic(w) {
return
}
addr, err := sdk.AccAddressFromBech32(req.Owner)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// create the message
msg := types.NewMsgSetName(req.Name, req.Value, addr)
err = msg.ValidateBasic()
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
}
}

長え

上のBaseReqには、トランザクションを作成するための基本的に必要なフィールドが含まれており、そこに埋め込まれる設計になっています。

cliCtxを使っているので、CLI向けに設定したトランザクションの操作をRESTから叩くようにしているのがわかります。

今回はここまでです!お疲れ様でした!!

なんとなく色んな機能が追加されて、終わりが見えてきた気がします。

次回は7月20日にその5を投稿します!

(追記)続きの記事はこちらです。

お知らせ

■HashHubでは入居者募集中です!
HashHubは、ブロックチェーン業界で働いている人のためのコワーキングスペースを運営しています。ご利用をご検討の方は、下記のWEBサイトからお問い合わせください。また、最新情報はTwitterで発信中です。

HashHub:https://hashhub.tokyo/
Twitter:https://twitter.com/HashHub_Tokyo

■ブロックチェーンエンジニア集中講座開講中!
HashHubではブロックチェーンエンジニアを育成するための短期集中講座を開講しています。お申込み、詳細は下記のページをご覧ください。

ブロックチェーンエンジニア集中講座:https://www.blockchain-edu.jp/

--

--