2020/12/18
COSMOS SDKのTutorialに沿ってblockchainアプリを作ってみた
概要
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"}
以上です。
思ったより簡単にブロックチェーンアプリが作ることができました。
関連する記事
tmkmsをdockerでビルドしてローカルのdocker-compose環境で利用してみる
tendermintのkey management systemであるtmkmsをsoftsignモードでテスト環境に導入してみる
[Golang]Cosmos TxのAminoエンコードされたバイト配列をデコードする
Cosmos HubのBlockを取得するRPCの結果に含まれるトランザクションのバイト配列をGolangでデコードしてみました
COSMOS SDKのTutorialに沿ってblockchainアプリを作ってみた
COSMOS SDKを使って、nameserviceのチュートリアルを触り基本的な概念を理解しました