2020/12/18

COSMOS SDKのTutorialに沿ってblockchainアプリを作ってみた

golangcosmos-sdktendermint

概要

Getting Started | Cosmos SDK Tutorials をやってみました。

COSMOS SDKを使うと簡単にblockchainアプリが作れるようだったので、試してみました。

install starport

https://github.com/tendermint/starport

Starport generates boilerplate code for you

「"Starport"はプロジェクトのコードテンプレートを作ってくれる便利なツール」みたいです。
早速、コードをクローンして手動で、ビルド・インストールしていきます。

% git clone https://github.com/tendermint/starport
% cd starport
  • setup goenv
% goenv install 1.15.3
% goenv local 1.15.3
% cat <<EOF > .envrc
export GOROOT=\${HOME}/.anyenv/envs/goenv/versions/1.15.3
export PATH=\${PATH}:\${GOPATH}/1.15.3/bin
export GOPATH=\${GOPATH}/1.15.3:\$(pwd)
EOF
% direnv allow
  • build & install
% make
% which starport
/Users/yourname/go/1.15.3/bin/starport  # 無事インストールされていました

さわり

A blockchain application is just a replicated deterministic state machine (opens new window). As a developer, you just have to define the state machine (i.e. what the state, a starting state and messages that trigger state transitions), and Tendermint (opens new window)will handle replication over the network for you.

「ブロックチェーンは単なる分散ステートマシン」
つまり、
「ブロックチェーンとは、入力(トランザクション)に対して出力(ブロックチェーンの状態)が一つに決めるマシーンが分散・協調して動作しているようなもの」
という感じで一般化・抽象化して説明しています。

「(ブロックチェーンを作りたければ)(Tendermintを使えば)開発者としてやることは
このステートマシンの振る舞いだけ定義すればできてしまいます。(便利でしょ)」

…ということだそうです。(確かに便利そう)

さらにCosmos SDKを使うと、その「ステートマシンの振る舞い」の構築も簡単に作れるということのようです。

start implementing nameservice

% cd ..
% mkdir -p nameservice/src/github.com/kumanote
% cd nameservice
# set up goenv
% goenv local 1.15.3
% cat <<EOF > .envrc
export GOROOT=\${HOME}/.anyenv/envs/goenv/versions/1.15.3
export PATH=\${PATH}:\${GOPATH}/1.15.3/bin
export GOPATH=\${GOPATH}/1.15.3:\$(pwd)
EOF
% direnv allow

% cd src/github.com/kumanote 
% starport app github.com/kumanote/nameservice --sdk-version="launchpad"
% cd nameservice

##これで、プロジェクトのテンプレートソースコードが出来上がります。

% tree
.
├── app
│   ├── app.go
│   ├── export.go
│   └── prefix.go
├── cmd
│   ├── nameservicecli
│   │   └── main.go
│   └── nameserviced
│       ├── genaccounts.go
│       └── main.go
├── config.yml
├── go.mod
├── go.sum
├── readme.md
├── vue
│   ├── README.md
│   ├── babel.config.js
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   │   ├── favicon.ico
│   │   └── index.html
│   ├── src
│   │   ├── App.vue
│   │   ├── main.js
│   │   ├── router
│   │   │   └── index.js
│   │   ├── store
│   │   │   └── index.js
│   │   └── views
│   │       └── Index.vue
│   └── vue.config.js
└── x
    └── nameservice
        ├── abci.go
        ├── client
        │   ├── cli
        │   │   ├── query.go
        │   │   └── tx.go
        │   └── rest
        │       └── rest.go
        ├── genesis.go
        ├── handler.go
        ├── keeper
        │   ├── keeper.go
        │   ├── params.go
        │   └── querier.go
        ├── module.go
        ├── spec
        │   └── README.md
        └── types
            ├── codec.go
            ├── errors.go
            ├── events.go
            ├── expected_keepers.go
            ├── genesis.go
            ├── key.go
            ├── msg.go
            ├── params.go
            ├── querier.go
            └── types.go

install dependencies

% cd src/github.com/kumanote/nameservice
% go mod download

実装

Types

% starport type whois value price

🎉 Created a type `whois`.

# x/nameservice/types/MsgCreateWhois.go とか MsgDeleteWhois.go とか色々作成されます
% git status
        modified:   vue/src/views/Index.vue
        modified:   x/nameservice/client/cli/query.go
        new file:   x/nameservice/client/cli/queryWhois.go
        modified:   x/nameservice/client/cli/tx.go
        new file:   x/nameservice/client/cli/txWhois.go
        new file:   x/nameservice/client/rest/queryWhois.go
        modified:   x/nameservice/client/rest/rest.go
        new file:   x/nameservice/client/rest/txWhois.go
        modified:   x/nameservice/handler.go
        new file:   x/nameservice/handlerMsgCreateWhois.go
        new file:   x/nameservice/handlerMsgDeleteWhois.go
        new file:   x/nameservice/handlerMsgSetWhois.go
        modified:   x/nameservice/keeper/querier.go
        new file:   x/nameservice/keeper/whois.go
        new file:   x/nameservice/types/MsgCreateWhois.go
        new file:   x/nameservice/types/MsgDeleteWhois.go
        new file:   x/nameservice/types/MsgSetWhois.go
        new file:   x/nameservice/types/TypeWhois.go
        modified:   x/nameservice/types/codec.go
        modified:   x/nameservice/types/key.go
        modified:   x/nameservice/types/querier.go

./x/nameservice/types/TypeWhois.go を編集します

package types

import (
	"fmt"
	"strings"

	sdk "github.com/cosmos/cosmos-sdk/types"
)

// MinNamePrice is Initial Starting Price for a name that was never previously owned
var MinNamePrice = sdk.Coins{sdk.NewInt64Coin("nametoken", 1)}

type Whois struct {
	Value string         `json:"value" yaml:"value"`
	Owner sdk.AccAddress `json:"owner" yaml:"owner"`
	Price sdk.Coins      `json:"price" yaml:"price"`
}

// NewWhois returns a new Whois with the minprice as the price
func NewWhois() Whois {
	return Whois{
		Price: MinNamePrice,
	}
}

// implement fmt.Stringer
func (w Whois) String() string {
	return strings.TrimSpace(fmt.Sprintf(`Owner: %s
Value: %s
Price: %s`, w.Owner, w.Value, w.Price))
}

Key

./x/nameservice/types/key.go を編集します

package types

const (
	// ModuleName is the name of the module
	ModuleName = "nameservice"

	// StoreKey to be used when creating the KVStore
	StoreKey = ModuleName

	// RouterKey to be used for routing msgs
	RouterKey = ModuleName

	// QuerierRoute to be used for querier msgs
	QuerierRoute = ModuleName
)

const (
	WhoisPrefix = "whois-"
)

Errors

./x/nameservice/types/errors.go を編集します

package types

import (
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

var (
	ErrNameDoesNotExist = sdkerrors.Register(ModuleName, 1, "name does not exist")
)

The Keeper

The main core of a Cosmos SDK module is a piece called the Keeper. It is what handles interaction with the data store, has references to other keepers for cross-module interactions, and contains most of the core functionality of a module.

コアな部分みたいです。

./x/nameservice/keeper/whois.go を編集します

package keeper

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

	"github.com/cosmos/cosmos-sdk/codec"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// GetWhois returns the whois information
func (k Keeper) GetWhois(ctx sdk.Context, key string) (types.Whois, error) {
	store := ctx.KVStore(k.storeKey)
	var whois types.Whois
	byteKey := []byte(types.WhoisPrefix + key)
	if err := k.cdc.UnmarshalBinaryLengthPrefixed(store.Get(byteKey), &whois); err != nil {
		return whois, err
	}
	return whois, nil
}

// SetWhois sets a whois. We modified this function to use the `name` value as the key instead of msg.ID
func (k Keeper) SetWhois(ctx sdk.Context, name string, whois types.Whois) {
	store := ctx.KVStore(k.storeKey)
	bz := k.cdc.MustMarshalBinaryLengthPrefixed(whois)
	key := []byte(types.WhoisPrefix + name)
	store.Set(key, bz)
}

// DeleteWhois deletes a whois
func (k Keeper) DeleteWhois(ctx sdk.Context, key string) {
	store := ctx.KVStore(k.storeKey)
	store.Delete([]byte(types.WhoisPrefix + key))
}

//
// Functions used by querier
//

func listWhois(ctx sdk.Context, k Keeper) ([]byte, error) {
	var whoisList []types.Whois
	store := ctx.KVStore(k.storeKey)
	iterator := sdk.KVStorePrefixIterator(store, []byte(types.WhoisPrefix))
	for ; iterator.Valid(); iterator.Next() {
		var whois types.Whois
		k.cdc.MustUnmarshalBinaryLengthPrefixed(store.Get(iterator.Key()), &whois)
		whoisList = append(whoisList, whois)
	}
	res := codec.MustMarshalJSONIndent(k.cdc, whoisList)
	return res, nil
}

func getWhois(ctx sdk.Context, path []string, k Keeper) (res []byte, sdkError error) {
	key := path[0]
	whois, err := k.GetWhois(ctx, key)
	if err != nil {
		return nil, err
	}

	res, err = codec.MarshalJSONIndent(k.cdc, whois)
	if err != nil {
		return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
	}

	return res, nil
}

// Resolves a name, returns the value
func resolveName(ctx sdk.Context, path []string, keeper Keeper) ([]byte, error) {
	value := keeper.ResolveName(ctx, path[0])

	if value == "" {
		return []byte{}, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "could not resolve name")
	}

	res, err := codec.MarshalJSONIndent(keeper.cdc, types.QueryResResolve{Value: value})
	if err != nil {
		return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
	}

	return res, nil
}

// Get owner of the item
func (k Keeper) GetOwner(ctx sdk.Context, key string) sdk.AccAddress {
	whois, _ := k.GetWhois(ctx, key)
	return whois.Owner
}

// Check if the key exists in the store
func (k Keeper) Exists(ctx sdk.Context, key string) bool {
	store := ctx.KVStore(k.storeKey)
	return store.Has([]byte(types.WhoisPrefix + key))
}

// ResolveName - returns the string that the name resolves to
func (k Keeper) ResolveName(ctx sdk.Context, name string) string {
	whois, _ := k.GetWhois(ctx, name)
	return whois.Value
}

// SetName - sets the value string that a name resolves to
func (k Keeper) SetName(ctx sdk.Context, name string, value string) {
	whois, _ := k.GetWhois(ctx, name)
	whois.Value = value
	k.SetWhois(ctx, name, whois)
}

// HasOwner - returns whether or not the name already has an owner
func (k Keeper) HasOwner(ctx sdk.Context, name string) bool {
	whois, _ := k.GetWhois(ctx, name)
	return !whois.Owner.Empty()
}

// SetOwner - sets the current owner of a name
func (k Keeper) SetOwner(ctx sdk.Context, name string, owner sdk.AccAddress) {
	whois, _ := k.GetWhois(ctx, name)
	whois.Owner = owner
	k.SetWhois(ctx, name, whois)
}

// GetPrice - gets the current price of a name
func (k Keeper) GetPrice(ctx sdk.Context, name string) sdk.Coins {
	whois, _ := k.GetWhois(ctx, name)
	return whois.Price
}

// SetPrice - sets the current price of a name
func (k Keeper) SetPrice(ctx sdk.Context, name string, price sdk.Coins) {
	whois, _ := k.GetWhois(ctx, name)
	whois.Price = price
	k.SetWhois(ctx, name, whois)
}

// Check if the name is present in the store or not
func (k Keeper) IsNamePresent(ctx sdk.Context, name string) bool {
	store := ctx.KVStore(k.storeKey)
	return store.Has([]byte(name))
}

// Get an iterator over all names in which the keys are the names and the values are the whois
func (k Keeper) GetNamesIterator(ctx sdk.Context) sdk.Iterator {
	store := ctx.KVStore(k.storeKey)
	return sdk.KVStorePrefixIterator(store, []byte(types.WhoisPrefix))
}

references

Msgs and Handlers

name description
Msgs ステートの変更を引き起こすもの = transactionの中身
Handlers Msgsを処理するもの

今回は3種類のMsgが必要で、それに対応したHandlerも定義していきます。

  • SetName
  • BuyName
  • DeleteName

SetName

% mv x/nameservice/types/MsgSetWhois.go x/nameservice/types/MsgSetName.go

x/nameservice/types/MsgSetName.go を編集します

package types

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

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

// Type should return the action
func (msg MsgSetName) Type() string { return "set_name" }

// Route should return the name of the module
func (msg MsgSetName) Route() string { return RouterKey }

// ValidateBasic runs stateless checks on the message
func (msg MsgSetName) ValidateBasic() error {
	if msg.Owner.Empty() {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Owner.String())
	}
	if len(msg.Name) == 0 || len(msg.Value) == 0 {
		return sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "Name and/or Value cannot be empty")
	}
	return nil
}

// GetSignBytes encodes the message for signing
func (msg MsgSetName) GetSignBytes() []byte {
	return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}

// GetSigners defines whose signature is required
func (msg MsgSetName) GetSigners() []sdk.AccAddress {
	return []sdk.AccAddress{msg.Owner}
}
% mv x/nameservice/handlerMsgSetWhois.go x/nameservice/handlerMsgSetName.go

x/nameservice/handlerMsgSetName.go を編集します

package nameservice

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

	"github.com/kumanote/nameservice/x/nameservice/keeper"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// Handle a message to set name
func handleMsgSetName(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgSetName) (*sdk.Result, error) {
	if !msg.Owner.Equals(keeper.GetOwner(ctx, msg.Name)) { // Checks if the the msg sender is the same as the current owner
		return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Incorrect Owner") // 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{}, nil                // return
}

x/nameservice/handler.go を編集します

package nameservice

import (
	"fmt"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/kumanote/nameservice/x/nameservice/keeper"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// NewHandler ...
func NewHandler(keeper keeper.Keeper) sdk.Handler {
	return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
		switch msg := msg.(type) {
		case types.MsgSetName:
			return handleMsgSetName(ctx, keeper, msg)
		default:
			return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type()))
		}
	}
}

BuyName

SetNameと同じ流れで、BuyNameのMsgとhandlerも追加してきます。

mv x/nameservice/types/MsgCreateWhois.go x/nameservice/types/MsgBuyName.go

x/nameservice/types/MsgBuyName.go を編集します

package types

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

// Originally, this file was named MsgCreateWhois, and has been modified using search-and-replace to our Msg needs.

// 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() error {
	if msg.Buyer.Empty() {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, msg.Buyer.String())
	}
	if len(msg.Name) == 0 {
		return sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "Name cannot be empty")
	}
	if !msg.Bid.IsAllPositive() {
		return sdkerrors.ErrInsufficientFunds
	}
	return nil
}

// GetSignBytes encodes the message for signing
func (msg MsgBuyName) GetSignBytes() []byte {
	return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}

// GetSigners defines whose signature is required
func (msg MsgBuyName) GetSigners() []sdk.AccAddress {
	return []sdk.AccAddress{msg.Buyer}
}
% mv x/nameservice/handlerMsgCreateWhois.go x/nameservice/handlerMsgBuyName.go

x/nameservice/handlerMsgBuyName.go を編集します

package nameservice

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

	"github.com/kumanote/nameservice/x/nameservice/keeper"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// Handle a message to buy name
func handleMsgBuyName(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgBuyName) (*sdk.Result, error) {
	// Checks if the the bid price is greater than the price paid by the current owner
	if keeper.GetPrice(ctx, msg.Name).IsAllGT(msg.Bid) {
		return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Bid not high enough") // 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 nil, err
		}
	} else {
		_, err := keeper.CoinKeeper.SubtractCoins(ctx, msg.Buyer, msg.Bid) // If so, deduct the Bid amount from the sender
		if err != nil {
			return nil, err
		}
	}
	keeper.SetOwner(ctx, msg.Name, msg.Buyer)
	keeper.SetPrice(ctx, msg.Name, msg.Bid)
	return &sdk.Result{}, nil
}

x/nameservice/handler.go を編集します

package nameservice

import (
	"fmt"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/kumanote/nameservice/x/nameservice/keeper"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// NewHandler ...
func NewHandler(keeper keeper.Keeper) sdk.Handler {
	return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
		switch msg := msg.(type) {
		case types.MsgSetName:
			return handleMsgSetName(ctx, keeper, msg)
		case types.MsgBuyName:
			return handleMsgBuyName(ctx, keeper, msg)
		default:
			return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type()))
		}
	}
}

DeleteName

SetName/BuyNameと同じ流れで、DeleteNameのMsgとhandlerも追加してきます。

mv x/nameservice/types/MsgDeleteWhois.go x/nameservice/types/MsgDeleteName.go

x/nameservice/types/MsgDeleteName.go を編集します

package types

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

// This Msg deletes a name. It was originally called MsgDeleteWhois, and has been modified using search-and-replace to our Msg needs.

var _ sdk.Msg = &MsgDeleteName{}

type MsgDeleteName struct {
	Name  string         `json:"name" yaml:"name"`
	Owner sdk.AccAddress `json:"owner" yaml:"owner"`
}

func NewMsgDeleteName(name string, owner sdk.AccAddress) MsgDeleteName {
	return MsgDeleteName{
		Name:  name,
		Owner: owner,
	}
}

func (msg MsgDeleteName) Route() string {
	return RouterKey
}

func (msg MsgDeleteName) Type() string {
	return "DeleteName"
}

func (msg MsgDeleteName) GetSigners() []sdk.AccAddress {
	return []sdk.AccAddress{sdk.AccAddress(msg.Owner)}
}

func (msg MsgDeleteName) GetSignBytes() []byte {
	bz := ModuleCdc.MustMarshalJSON(msg)
	return sdk.MustSortJSON(bz)
}

func (msg MsgDeleteName) ValidateBasic() error {
	if msg.Owner.Empty() {
		return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner can't be empty")
	}
	return nil
}
% mv x/nameservice/handlerMsgDeleteWhois.go x/nameservice/handlerMsgDeleteName.go

x/nameservice/handlerMsgDeleteName.go を編集します

package nameservice

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

	"github.com/kumanote/nameservice/x/nameservice/keeper"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// Handle a message to delete name
func handleMsgDeleteName(ctx sdk.Context, k keeper.Keeper, msg types.MsgDeleteName) (*sdk.Result, error) {
	if !k.Exists(ctx, msg.Name) {
		// replace with ErrKeyNotFound for 0.39+
		return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, msg.Name)
	}
	if !msg.Owner.Equals(k.GetOwner(ctx, msg.Name)) {
		return nil, sdkerrors.Wrap(sdkerrors.ErrUnauthorized, "Incorrect Owner")
	}

	k.DeleteWhois(ctx, msg.Name)
	return &sdk.Result{}, nil
}

x/nameservice/handler.go を編集します

package nameservice

import (
	"fmt"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
	"github.com/kumanote/nameservice/x/nameservice/keeper"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// NewHandler ...
func NewHandler(keeper keeper.Keeper) sdk.Handler {
	return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
		switch msg := msg.(type) {
		case types.MsgSetName:
			return handleMsgSetName(ctx, keeper, msg)
		case types.MsgBuyName:
			return handleMsgBuyName(ctx, keeper, msg)
		case types.MsgDeleteName:
			return handleMsgDeleteName(ctx, keeper, msg)
		default:
			return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type()))
		}
	}
}

Queriers

x/nameservice/types/querier.go を編集します

package types

import "strings"

const QueryListWhois = "list-whois"
const QueryGetWhois = "get-whois"
const QueryResolveName = "resolve-name"

// QueryResResolve Queries Result Payload for a resolve query
type QueryResResolve struct {
	Value string `json:"value"`
}

// implement fmt.Stringer
func (r QueryResResolve) String() string {
	return r.Value
}

// QueryResNames Queries Result Payload for a names query
type QueryResNames []string

// implement fmt.Stringer
func (n QueryResNames) String() string {
	return strings.Join(n[:], "\n")
}

x/nameservice/keeper/querier.go を編集します

package keeper

import (
	// this line is used by starport scaffolding # 1
	"github.com/kumanote/nameservice/x/nameservice/types"

	abci "github.com/tendermint/tendermint/abci/types"

	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

// NewQuerier creates a new querier for nameservice clients.
func NewQuerier(k Keeper) sdk.Querier {
	return func(ctx sdk.Context, path []string, req abci.RequestQuery) ([]byte, error) {
		switch path[0] {
		// this line is used by starport scaffolding # 2
		case types.QueryResolveName:
			return resolveName(ctx, path[1:], k)
		case types.QueryGetWhois:
			return getWhois(ctx, path[1:], k)
		case types.QueryListWhois:
			return listWhois(ctx, k)
		default:
			return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown nameservice query endpoint")
		}
	}
}

Codec File

  • Aminoという形式(jsonとかprotobuf3をより進化させたもの)でencode/decodeするための仕組みです

x/nameservice/types/codec.go を編集します

package types

import (
	"github.com/cosmos/cosmos-sdk/codec"
)

// RegisterCodec registers concrete types on codec
func RegisterCodec(cdc *codec.Codec) {
	// this line is used by starport scaffolding
	cdc.RegisterConcrete(MsgBuyName{}, "nameservice/BuyName", nil)
	cdc.RegisterConcrete(MsgSetName{}, "nameservice/SetName", nil)
	cdc.RegisterConcrete(MsgDeleteName{}, "nameservice/DeleteName", nil)
}

// ModuleCdc defines the module codec
var ModuleCdc *codec.Codec

func init() {
	ModuleCdc = codec.New()
	RegisterCodec(ModuleCdc)
	codec.RegisterCrypto(ModuleCdc)
	ModuleCdc.Seal()
}

Nameservice Module CLI

x/nameservice/client/cli/queryWhois.go を編集します

package cli

import (
	"fmt"

	"github.com/cosmos/cosmos-sdk/client/context"
	"github.com/cosmos/cosmos-sdk/codec"
	"github.com/kumanote/nameservice/x/nameservice/types"
	"github.com/spf13/cobra"
)

func GetCmdListWhois(queryRoute string, cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "list-whois",
		Short: "list all whois",
		RunE: func(cmd *cobra.Command, args []string) error {
			cliCtx := context.NewCLIContext().WithCodec(cdc)
			res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryListWhois), nil)
			if err != nil {
				fmt.Printf("could not list Whois\n%s\n", err.Error())
				return nil
			}
			var out []types.Whois
			cdc.MustUnmarshalJSON(res, &out)
			return cliCtx.PrintOutput(out)
		},
	}
}

func GetCmdGetWhois(queryRoute string, cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "get-whois [key]",
		Short: "Query a whois by key",
		Args:  cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			cliCtx := context.NewCLIContext().WithCodec(cdc)
			key := args[0]

			res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", queryRoute, types.QueryGetWhois, key), nil)
			if err != nil {
				fmt.Printf("could not resolve whois %s \n%s\n", key, err.Error())

				return nil
			}

			var out types.Whois
			cdc.MustUnmarshalJSON(res, &out)
			return cliCtx.PrintOutput(out)
		},
	}
}

// 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 := context.NewCLIContext().WithCodec(cdc)
			name := args[0]

			res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", queryRoute, types.QueryResolveName, name), nil)
			if err != nil {
				fmt.Printf("could not resolve name - %s \n", name)
				return nil
			}

			var out types.QueryResResolve
			cdc.MustUnmarshalJSON(res, &out)
			return cliCtx.PrintOutput(out)
		},
	}
}

x/nameservice/client/cli/txWhois.go を編集します

package cli

import (
	"bufio"

	"github.com/cosmos/cosmos-sdk/client/context"
	"github.com/cosmos/cosmos-sdk/codec"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/x/auth"
	"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
	"github.com/kumanote/nameservice/x/nameservice/types"
	"github.com/spf13/cobra"
)

func GetCmdBuyName(cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "buy-name [name] [price]",
		Short: "Buys a new name",
		Args:  cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			argsName := string(args[0])

			cliCtx := context.NewCLIContext().WithCodec(cdc)
			inBuf := bufio.NewReader(cmd.InOrStdin())
			txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))

			coins, err := sdk.ParseCoins(args[1])
			if err != nil {
				return err
			}

			msg := types.NewMsgBuyName(argsName, coins, cliCtx.GetFromAddress())
			err = msg.ValidateBasic()
			if err != nil {
				return err
			}
			return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
		},
	}
}

func GetCmdSetWhois(cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "set-name [value] [name]",
		Short: "Set a new name",
		Args:  cobra.ExactArgs(2),
		RunE: func(cmd *cobra.Command, args []string) error {
			argsValue := args[0]
			argsName := args[1]

			cliCtx := context.NewCLIContext().WithCodec(cdc)
			inBuf := bufio.NewReader(cmd.InOrStdin())
			txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
			msg := types.NewMsgSetName(argsName, argsValue, cliCtx.GetFromAddress())
			err := msg.ValidateBasic()
			if err != nil {
				return err
			}
			return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
		},
	}
}

func GetCmdDeleteWhois(cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "delete-name [name]",
		Short: "Delete name by name",
		Args:  cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {

			cliCtx := context.NewCLIContext().WithCodec(cdc)
			inBuf := bufio.NewReader(cmd.InOrStdin())
			txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))

			msg := types.NewMsgDeleteName(args[0], cliCtx.GetFromAddress())
			err := msg.ValidateBasic()
			if err != nil {
				return err
			}
			return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
		},
	}
}

x/nameservice/client/cli/tx.go を編集します

package cli

import (
	"fmt"

	"github.com/spf13/cobra"

	"github.com/cosmos/cosmos-sdk/client"
	"github.com/cosmos/cosmos-sdk/client/flags"
	"github.com/cosmos/cosmos-sdk/codec"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

// GetTxCmd returns the transaction commands for this module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
	nameserviceTxCmd := &cobra.Command{
		Use:                        types.ModuleName,
		Short:                      fmt.Sprintf("%s transactions subcommands", types.ModuleName),
		DisableFlagParsing:         true,
		SuggestionsMinimumDistance: 2,
		RunE:                       client.ValidateCmd,
	}

	nameserviceTxCmd.AddCommand(flags.PostCommands(
		// this line is used by starport scaffolding
		GetCmdBuyName(cdc),
		GetCmdSetWhois(cdc),
		GetCmdDeleteWhois(cdc),
	)...)

	return nameserviceTxCmd
}

NameService Module Rest Interface

x/nameservice/client/rest/queryWhois.go を編集します

package rest

import (
	"fmt"
	"net/http"

	"github.com/kumanote/nameservice/x/nameservice/types"

	"github.com/cosmos/cosmos-sdk/client/context"
	"github.com/cosmos/cosmos-sdk/types/rest"
	"github.com/gorilla/mux"
)

func listWhoisHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", storeName, types.QueryListWhois), nil)
		if err != nil {
			rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
			return
		}
		rest.PostProcessResponse(w, cliCtx, res)
	}
}

func getWhoisHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		key := vars["key"]

		res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", storeName, types.QueryGetWhois, key), nil)
		if err != nil {
			rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
			return
		}
		rest.PostProcessResponse(w, cliCtx, res)
	}
}

func resolveNameHandler(cliCtx context.CLIContext, storeName string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		paramType := vars["key"]

		res, _, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s/%s", storeName, types.QueryResolveName, paramType), nil)
		if err != nil {
			rest.WriteErrorResponse(w, http.StatusNotFound, err.Error())
			return
		}

		rest.PostProcessResponse(w, cliCtx, res)
	}
}

x/nameservice/client/rest/txWhois.go を編集します

package rest

import (
	"net/http"

	"github.com/cosmos/cosmos-sdk/client/context"
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/types/rest"
	"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
	"github.com/kumanote/nameservice/x/nameservice/types"
)

type buyNameRequest struct {
	BaseReq rest.BaseReq `json:"base_req"`
	Buyer   string       `json:"buyer"`
	Name    string       `json:"name"`
	Price   string       `json:"price"`
}

func buyNameHandler(cliCtx context.CLIContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req buyNameRequest
		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.Price)
		if err != nil {
			rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
			return
		}
		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 setWhoisRequest struct {
	BaseReq rest.BaseReq `json:"base_req"`
	Name    string       `json:"name"`
	Value   string       `json:"value"`
	Owner   string       `json:"owner"`
}

func setWhoisHandler(cliCtx context.CLIContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req setWhoisRequest
		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
		}
		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})
	}
}

type deleteWhoisRequest struct {
	BaseReq rest.BaseReq `json:"base_req"`
	Owner   string       `json:"owner"`
	Name    string       `json:"name"`
}

func deleteWhoisHandler(cliCtx context.CLIContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req deleteWhoisRequest
		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
		}
		msg := types.NewMsgDeleteName(req.Name, addr)

		err = msg.ValidateBasic()
		if err != nil {
			rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
			return
		}

		utils.WriteGenerateStdTxResponse(w, cliCtx, baseReq, []sdk.Msg{msg})
	}
}

x/nameservice/client/rest/rest.go を編集します

package rest

import (
	"github.com/gorilla/mux"

	"github.com/cosmos/cosmos-sdk/client/context"
)

// RegisterRoutes registers nameservice-related REST handlers to a router
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
	// this line is used by starport scaffolding
	r.HandleFunc("/nameservice/whois", buyNameHandler(cliCtx)).Methods("POST")
	r.HandleFunc("/nameservice/whois", listWhoisHandler(cliCtx, "nameservice")).Methods("GET")
	r.HandleFunc("/nameservice/whois/{key}", getWhoisHandler(cliCtx, "nameservice")).Methods("GET")
	r.HandleFunc("/nameservice/whois/{key}/resolve", resolveNameHandler(cliCtx, "nameservice")).Methods("GET")
	r.HandleFunc("/nameservice/whois", setWhoisHandler(cliCtx)).Methods("PUT")
	r.HandleFunc("/nameservice/whois", deleteWhoisHandler(cliCtx)).Methods("DELETE")
}

AppModule Interface

x/nameservice/module.go の中身を確認

今回はテンプレートから変更する必要がないですが

cosmos-sdk/module.go at master · cosmos/cosmos-sdk などが参考になりそうでした。

Genesis

genesis stateの定義を行います。

x/nameservice/types/genesis.go の中身を編集します

package types

import "fmt"

// GenesisState - all nameservice state that must be provided at genesis
type GenesisState struct {
	// TODO: Fill out what is needed by the module for genesis
	WhoisRecords []Whois `json:"whois_records"`
}

// NewGenesisState creates a new GenesisState object
func NewGenesisState( /* TODO: Fill out with what is needed for genesis state */ ) GenesisState {
	return GenesisState{
		// TODO: Fill out according to your genesis state
		WhoisRecords: nil,
	}
}

// DefaultGenesisState - default GenesisState used by Cosmos Hub
func DefaultGenesisState() GenesisState {
	return GenesisState{
		WhoisRecords: []Whois{},
	}
}

// ValidateGenesis validates the nameservice genesis parameters
func ValidateGenesis(data GenesisState) error {
	// TODO: Create a sanity check to make sure the state conforms to the modules needs
	for _, record := range data.WhoisRecords {
		if record.Owner == nil {
			return fmt.Errorf("invalid WhoisRecord: Owner: %s. Error: Missing Owner", record.Owner)
		}
		if record.Value == "" {
			return fmt.Errorf("invalid WhoisRecord: Value: %s. Error: Missing Value", record.Value)
		}
		if record.Price == nil {
			return fmt.Errorf("invalid WhoisRecord: Price: %s. Error: Missing Price", record.Price)
		}
	}
	return nil
}

x/nameservice/genesis.go の中身を編集します

package nameservice

import (
	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/kumanote/nameservice/x/nameservice/keeper"
	"github.com/kumanote/nameservice/x/nameservice/types"
	// abci "github.com/tendermint/tendermint/abci/types"
)

// InitGenesis initialize default parameters
// and the keeper's address to pubkey map
func InitGenesis(ctx sdk.Context, keeper keeper.Keeper, data types.GenesisState) {
	for _, record := range data.WhoisRecords {
		keeper.SetWhois(ctx, record.Value, record)
	}
}

// ExportGenesis writes the current store values
// to a genesis file, which can be imported again
// with InitGenesis
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
	var records []types.Whois
	iterator := k.GetNamesIterator(ctx)
	for ; iterator.Valid(); iterator.Next() {

		name := string(iterator.Key())
		whois, _ := k.GetWhois(ctx, name)
		records = append(records, whois)

	}
	return types.GenesisState{WhoisRecords: records}
}

Complete App

app/app.go の中身を確認

今回はテンプレートから変更する必要なし

Entry points

  • cmd/nameserviced/main.go の中身を確認
  • cmd/nameservicecli/main.go の中身を確認

今回はテンプレートから変更する必要なし

go.mod and Makefile

Makefileを作成します

PACKAGES=$(shell go list ./... | grep -v '/simulation')

VERSION := $(shell echo $(shell git describe --tags) | sed 's/^v//')
COMMIT := $(shell git log -1 --format='%H')

ldflags = -X github.com/cosmos/cosmos-sdk/version.Name=NameService \
	-X github.com/cosmos/cosmos-sdk/version.ServerName=nameserviced \
	-X github.com/cosmos/cosmos-sdk/version.ClientName=nameservicecli \
	-X github.com/cosmos/cosmos-sdk/version.Version=$(VERSION) \
	-X github.com/cosmos/cosmos-sdk/version.Commit=$(COMMIT) 

BUILD_FLAGS := -ldflags '$(ldflags)'

all: install

install: go.sum
		@echo "--> Installing nameserviced & nameservicecli"
		@go install -mod=readonly $(BUILD_FLAGS) ./cmd/nameserviced
		@go install -mod=readonly $(BUILD_FLAGS) ./cmd/nameservicecli

go.sum: go.mod
		@echo "--> Ensure dependencies have not been modified"
		GO111MODULE=on go mod verify

test:
	@go test -mod=readonly $(PACKAGES)

その後ビルドします

% make install

% nameserviced help
app Daemon (server)

Usage:
  nameserviced [command]

Available Commands:
  init                Initialize private validator, p2p, genesis, and application configuration files
  collect-gentxs      Collect genesis txs and output a genesis.json file
  migrate             Migrate genesis to a specified target version
  gentx               Generate a genesis tx carrying a self delegation
  validate-genesis    validates the genesis file at the default location or at the location passed as an arg
  add-genesis-account Add a genesis account to genesis.json
  debug               Tool for helping with debugging your application
  start               Run the full node
  unsafe-reset-all    Resets the blockchain database, removes address book files, and resets priv_validator.json to the genesis state
                      
  tendermint          Tendermint subcommands
  export              Export state to JSON
                      
  version             Print the app version
  help                Help about any command

Flags:
  -h, --help                    help for nameserviced
      --home string             directory for config and data (default "/Users/tanakahiroki/.nameserviced")
      --inv-check-period uint   Assert registered invariants every N blocks
      --log_level string        Log level (default "main:info,state:info,*:error")
      --trace                   print out full stack trace on errors

Use "nameserviced [command] --help" for more information about a command.

% nameservicecli help
Command line interface for interacting with nameserviced

Usage:
  nameservicecli [command]

Available Commands:
  status      Query remote node for status
  config      Create or query an application CLI configuration file
  query       Querying subcommands
  tx          Transactions subcommands
              
  rest-server Start LCD (light-client daemon), a local REST server
              
  keys        Add or view local private keys
              
  version     Print the app version
  help        Help about any command

Flags:
      --chain-id string   Chain ID of tendermint node
  -e, --encoding string   Binary encoding (hex|b64|btc) (default "hex")
  -h, --help              help for nameservicecli
      --home string       directory for config and data (default "/Users/tanakahiroki/.nameservicecli")
  -o, --output string     Output format (text|json) (default "text")
      --trace             print out full stack trace on errors

Use "nameservicecli [command] --help" for more information about a command.

無事ビルドできました。

Build and run the app

% starport serve
Cosmos' version is: Launchpad

📦 Installing dependencies...
🛠️  Building the app...
🙂 Created an account. Password (mnemonic): arrange close found alley buffalo live breeze between fitness element brush review crisp inmate drink judge syrup inspire proof rural raise ball business guide
🙂 Created an account. Password (mnemonic): solid nerve hazard dutch moment guilt brother learn blossom leopard alter end embark comfort car render pulse wheel biology mechanic acquire fan cook user1et
🌍 Running a Cosmos 'nameservice' app with Tendermint at http://0.0.0.0:26657.
🌍 Running a server at http://0.0.0.0:1317 (LCD)

🚀 Get started: http://localhost:12345

Init genesis

init_nameserviced.sh という名前でbash scriptを作成します

#!/usr/bin/env bash

rm -rf ~/.nameserviced
rm -rf ~/.nameservicecli

nameserviced init test --chain-id=namechain

nameservicecli config output json
nameservicecli config indent true
nameservicecli config trust-node true
nameservicecli config chain-id namechain
nameservicecli config keyring-backend test

nameservicecli keys add user1
nameservicecli keys add user2

nameserviced add-genesis-account $(nameservicecli keys show user1 -a) 1000nametoken,100000000stake
nameserviced add-genesis-account $(nameservicecli keys show user2 -a) 1000nametoken,100000000stake

nameserviced gentx --name user1 --keyring-backend test

echo "Collecting genesis txs..."
nameserviced collect-gentxs

echo "Validating genesis file..."
nameserviced validate-genesis
  • 実行します
% ./init_nameserviced.sh 
{"app_message":{"auth":{"accounts":[],"params":{"max_memo_characters":"256","sig_verify_cost_ed25519":"590","sig_verify_cost_secp256k1":"1000","tx_sig_limit":"7","tx_size_cost_per_byte":"10"}},"bank":{"send_enabled":true},"genutil":{"gentxs":[]},"nameservice":{"whois_records":[]},"params":null,"staking":{"delegations":null,"exported":false,"last_total_power":"0","last_validator_powers":null,"params":{"bond_denom":"stake","historical_entries":0,"max_entries":7,"max_validators":100,"unbonding_time":"1814400000000000"},"redelegations":null,"unbonding_delegations":null,"validators":null},"supply":{"supply":[]}},"chain_id":"namechain","gentxs_dir":"","moniker":"test","node_id":"8807f5b662e3edea3eba7148c74614e19307c394"}
/Users/yourname/.nameservicecli/config/config.toml does not exist
configuration saved to /Users/yourname/.nameservicecli/config/config.toml
configuration saved to /Users/yourname/.nameservicecli/config/config.toml
configuration saved to /Users/yourname/.nameservicecli/config/config.toml
configuration saved to /Users/yourname/.nameservicecli/config/config.toml
configuration saved to /Users/yourname/.nameservicecli/config/config.toml
{
  "name": "user1",
  "type": "local",
  "address": "cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46",
  "pubkey": "cosmospub1addwnpepqfpp2a9vj6tvlh27t6gev9wl2e3nvf6ysa6f80xmq47gv7uyc8tdcumwtlc",
  "mnemonic": "pole velvet system grace retreat eight fan cage canvas balcony together guess romance stay globe mom defense frown banner scale elder cream level demand"
}
{
  "name": "user2",
  "type": "local",
  "address": "cosmos1jtuecf9yu9xahlxvadz7dufmk59le4n8wqm3q0",
  "pubkey": "cosmospub1addwnpepqfdgnzfrrnhwy9yahm7fyrwxjd75ylwd0dhsjcl6cwj78hu2tuqscprq7n3",
  "mnemonic": "make asset casual visual already stick question arrange term chronic cotton fatal dust foil giant ladder axis disorder cargo hurt charge adjust gown leader"
}
Genesis transaction written to "/Users/yourname/.nameserviced/config/gentx/gentx-8807f5b662e3edea3eba7148c74614e19307c394.json"
Collecting genesis txs...
{"app_message":{"auth":{"accounts":[{"type":"cosmos-sdk/Account","value":{"account_number":"0","address":"cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46","coins":[{"amount":"1000","denom":"nametoken"},{"amount":"100000000","denom":"stake"}],"public_key":null,"sequence":"0"}},{"type":"cosmos-sdk/Account","value":{"account_number":"0","address":"cosmos1jtuecf9yu9xahlxvadz7dufmk59le4n8wqm3q0","coins":[{"amount":"1000","denom":"nametoken"},{"amount":"100000000","denom":"stake"}],"public_key":null,"sequence":"0"}}],"params":{"max_memo_characters":"256","sig_verify_cost_ed25519":"590","sig_verify_cost_secp256k1":"1000","tx_sig_limit":"7","tx_size_cost_per_byte":"10"}},"bank":{"send_enabled":true},"genutil":{"gentxs":[{"type":"cosmos-sdk/StdTx","value":{"fee":{"amount":[],"gas":"200000"},"memo":"[email protected]:26656","msg":[{"type":"cosmos-sdk/MsgCreateValidator","value":{"commission":{"max_change_rate":"0.010000000000000000","max_rate":"0.200000000000000000","rate":"0.100000000000000000"},"delegator_address":"cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46","description":{"details":"","identity":"","moniker":"test","security_contact":"","website":""},"min_self_delegation":"1","pubkey":"cosmosvalconspub1zcjduepqs0zwcynmp70sv7jgmst6c3tqx9wuv8h0eefv6umhwugnuayse3fsvkcjf3","validator_address":"cosmosvaloper17hjsw7njnzqnk9uj5xfc9msws9yun4y2w5z9ef","value":{"amount":"100000000","denom":"stake"}}}],"signatures":[{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"AkIVdKyWls/dXl6RlhXfVmM2J0SHdJO82wV8hnuEwdbc"},"signature":"6sESelojqEi6VzV6nHfA8KwxoSittpEQLxCAABZrMOQB1QaCjs+3XXpQ6RJSYeEMMejZu3kzEtGMTL6T5keZhw=="}]}}]},"nameservice":{"whois_records":[]},"params":null,"staking":{"delegations":null,"exported":false,"last_total_power":"0","last_validator_powers":null,"params":{"bond_denom":"stake","historical_entries":0,"max_entries":7,"max_validators":100,"unbonding_time":"1814400000000000"},"redelegations":null,"unbonding_delegations":null,"validators":null},"supply":{"supply":[]}},"chain_id":"namechain","gentxs_dir":"/Users/yourname/.nameserviced/config/gentx","moniker":"test","node_id":"8807f5b662e3edea3eba7148c74614e19307c394"}
Validating genesis file...
validating genesis file at /Users/yourname/.nameserviced/config/genesis.json
File at /Users/yourname/.nameserviced/config/genesis.json is a valid genesis file
  • その後、nodeをスタートします。
% nameserviced start
I[2020-12-18|23:31:40.027] Committed state                              module=state height=1 txs=0 appHash=C138E1C4E61F5A1A57EBC29EAB38C5FCBE608A40906854F31613113CA457B62F
I[2020-12-18|23:31:45.133] Executed block                               module=state height=2 validTxs=0 invalidTxs=0
I[2020-12-18|23:31:45.160] Committed state                              module=state height=2 txs=0 appHash=C138E1C4E61F5A1A57EBC29EAB38C5FCBE608A40906854F31613113CA457B62F
I[2020-12-18|23:31:50.297] Executed block                               module=state height=3 validTxs=0 invalidTxs=0
I[2020-12-18|23:31:50.344] Committed state                              module=state height=3 txs=0 appHash=C138E1C4E61F5A1A57EBC29EAB38C5FCBE608A40906854F31613113CA457B62F
I[2020-12-18|23:31:55.437] Executed block                               module=state height=4 validTxs=0 invalidTxs=0
I[2020-12-18|23:31:55.464] Committed state                              module=state height=4 txs=0 appHash=C138E1C4E61F5A1A57EBC29EAB38C5FCBE608A40906854F31613113CA457B62F
I[2020-12-18|23:32:00.571] Executed block                               module=state height=5 validTxs=0 invalidTxs=0

どんどんblockが積み重なっていっているのがわかります。。。

${HOME}/.nameserviced/data にデータが溜まっていきます。

% cd ~/.nameserviced
% tree
.
├── config
│   ├── addrbook.json
│   ├── app.toml
│   ├── config.toml
│   ├── genesis.json
│   ├── gentx
│   │   └── gentx-8807f5b662e3edea3eba7148c74614e19307c394.json
│   ├── node_key.json
│   └── priv_validator_key.json
└── data
    ├── application.db
    │   ├── 000001.log
    │   ├── CURRENT
    │   ├── LOCK
    │   ├── LOG
    │   └── MANIFEST-000000
    ├── blockstore.db
    │   ├── 000001.log
    │   ├── CURRENT
    │   ├── LOCK
    │   ├── LOG
    │   └── MANIFEST-000000
    ├── cs.wal
    │   └── wal
    ├── evidence.db
    │   ├── 000001.log
    │   ├── CURRENT
    │   ├── LOCK
    │   ├── LOG
    │   └── MANIFEST-000000
    ├── priv_validator_state.json
    ├── state.db
    │   ├── 000001.log
    │   ├── CURRENT
    │   ├── LOCK
    │   ├── LOG
    │   └── MANIFEST-000000
    └── tx_index.db
        ├── 000001.log
        ├── CURRENT
        ├── LOCK
        ├── LOG
        └── MANIFEST-000000

9 directories, 34 files

Run REST routes

$ nameservicecli keys show user1 --address
cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46
$ nameservicecli keys show user2 --address
cosmos1jtuecf9yu9xahlxvadz7dufmk59le4n8wqm3q0
% nameservicecli rest-server --chain-id namechain --trust-node
% curl -s http://localhost:1317/auth/accounts/$(nameservicecli keys show user1 -a)
{
  "height": "58",
  "result": {
    "type": "cosmos-sdk/Account",
    "value": {
      "address": "cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46",
      "coins": [
        {
          "denom": "nametoken",
          "amount": "1000"
        }
      ],
      "public_key": {
        "type": "tendermint/PubKeySecp256k1",
        "value": "AkIVdKyWls/dXl6RlhXfVmM2J0SHdJO82wV8hnuEwdbc"
      },
      "account_number": "2",
      "sequence": "1"
    }
  }
}

% curl -s http://localhost:1317/auth/accounts/$(nameservicecli keys show user2 -a)
{
  "height": "65",
  "result": {
    "type": "cosmos-sdk/Account",
    "value": {
      "address": "cosmos1jtuecf9yu9xahlxvadz7dufmk59le4n8wqm3q0",
      "coins": [
        {
          "denom": "nametoken",
          "amount": "1000"
        },
        {
          "denom": "stake",
          "amount": "100000000"
        }
      ],
      "public_key": null,
      "account_number": "3",
      "sequence": "0"
    }
  }
}

# create whois
% curl -X POST -s http://localhost:1317/nameservice/whois --data-binary '{"base_req":{"from":"'$(nameservicecli keys show user1 -a)'","chain_id":"namechain"},"name":"user1.id","price":"5nametoken","buyer":"'$(nameservicecli keys show user1 -a)'"}' > unsignedTx.json
% cat unsignedTx.json
{"type":"cosmos-sdk/StdTx","value":{"msg":[{"type":"nameservice/BuyName","value":{"name":"user1.id","bid":[{"denom":"nametoken","amount":"5"}],"buyer":"cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46"}}],"fee":{"amount":[],"gas":"200000"},"signatures":null,"memo":""}}
% nameservicecli tx sign unsignedTx.json --from user1 --offline --chain-id namechain --sequence 1 --account-number 2 > signedTx.json
% cat signedTx.json 
{
  "type": "cosmos-sdk/StdTx",
  "value": {
    "msg": [
      {
        "type": "nameservice/BuyName",
        "value": {
          "name": "user1.id",
          "bid": [
            {
              "denom": "nametoken",
              "amount": "5"
            }
          ],
          "buyer": "cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46"
        }
      }
    ],
    "fee": {
      "amount": [],
      "gas": "200000"
    },
    "signatures": [
      {
        "pub_key": {
          "type": "tendermint/PubKeySecp256k1",
          "value": "AkIVdKyWls/dXl6RlhXfVmM2J0SHdJO82wV8hnuEwdbc"
        },
        "signature": "J0nckehd87prs1vJ4z6CKAHFcUBgFM1/ole9F9hr0IcHLSBME264vRh4/Yh0mc2fH0G62drIsdKwLKjVepSu/g=="
      }
    ],
    "memo": ""
  }
}
% nameservicecli tx broadcast signedTx.json
{
  "height": "0",
  "txhash": "0D7607E148B2B22CFDB1C1D70CAB7929DD2F1D4F1625674905BE9923F97BC7B9",
  "raw_log": "[]"
}

# set whois
% curl -X PUT -s http://localhost:1317/nameservice/whois --data-binary '{"base_req":{"from":"'$(nameservicecli keys show user1 -a)'","chain_id":"namechain"},"name":"user1.id","value":"8.8.4.4","owner":"'$(nameservicecli keys show user1 -a)'"}' > unsignedTx.json
% nameservicecli tx sign unsignedTx.json --from user1 --offline --chain-id namechain --sequence 2 --account-number 2 > signedTx.json
% nameservicecli tx broadcast signedTx.json
{
  "height": "0",
  "txhash": "ECDCE535DAF197652B47CBA9F96AE8B77F1F55F713C9F501B82BE9A7C9995A72",
  "raw_log": "[]"
}


# Query the value for the name user1.id just set
% curl -s http://localhost:1317/nameservice/whois/user1.id/resolve
{
  "height": "0",
  "result": {
    "value": "8.8.4.4"
  }
}

# Query whois for the name user1 just bought
% curl -s http://localhost:1317/nameservice/whois/user1.id
{
  "height": "0",
  "result": {
    "value": "8.8.4.4",
    "owner": "cosmos17hjsw7njnzqnk9uj5xfc9msws9yun4y2tqks46",
    "price": [
      {
        "denom": "nametoken",
        "amount": "5"
      }
    ]
  }
}

# user2 buys name from user1
% curl -X POST -s http://localhost:1317/nameservice/whois --data-binary '{"base_req":{"from":"'$(nameservicecli keys show user2 -a)'","chain_id":"namechain"},"name":"user1.id","price":"10nametoken","buyer":"'$(nameservicecli keys show user2 -a)'"}' > unsignedTx.json
% nameservicecli tx sign unsignedTx.json --from user2 --offline --chain-id namechain --sequence 0 --account-number 3 > signedTx.json
% nameservicecli tx broadcast signedTx.json
{
  "height": "0",
  "txhash": "C0859DCBC1AE34419D3A89C03C4B876A846CDC4B9B6FB1088A64E3DCCE2BD1D6",
  "raw_log": "[]"
}
% curl -s http://localhost:1317/nameservice/whois/user1.id
{
  "height": "0",
  "result": {
    "value": "8.8.4.4",
    "owner": "cosmos1jtuecf9yu9xahlxvadz7dufmk59le4n8wqm3q0",
    "price": [
      {
        "denom": "nametoken",
        "amount": "10"
      }
    ]
  }
}

# Now, user2 no longer needs the name she bought from user1 and hence deletes it
% curl -XDELETE -s http://localhost:1317/nameservice/whois --data-binary '{"base_req":{"from":"'$(nameservicecli keys show user2 -a)'","chain_id":"namechain"},"name":"user1.id","owner":"'$(nameservicecli keys show user2 -a)'"}' > unsignedTx.json
% nameservicecli tx sign unsignedTx.json --from user2 --offline --chain-id namechain --sequence 1 --account-number 3 > signedTx.json
% nameservicecli tx broadcast signedTx.json
{
  "height": "0",
  "txhash": "0354CE219ED966748E4219E5C2EAD53F1464741512C255522AA678A2AC0ED5B6",
  "raw_log": "[]"
}

# Query whois for the name user2 just deleted
$ curl -s http://localhost:1317/nameservice/whois/user1.id
{"error":"internal"}

以上です。
思ったより簡単にブロックチェーンアプリが作ることができました。