0%

StudyRecord-Cosmos官方NameService域名解析应用教程

前言:

找了一个旧版本的cosmos-sdk教程,目标是构建一个域名解析应用. 类似于Namecoin,ENS,Handshake这些模仿传统的DNS系统(map[domain]zonefile)的应用。用户可以购买未被使用的域名,或是出售/交易这些域名。

参考网址如下:

完整项目源码: https://github.com/jerrychan807/cosmos-nameservice

开发环境配置:

使用旧版的go:

一开始使用1.18版本的go,在后面会报该错误panic: crypto/hmac: hash generation function does not produce unique values when using ebdcli keys to add account,所以选择降级到1.15.15版本的go,就不会出现该错误了.

1
2
3
4
5
6
7
8
9
10
11
# 下载
wget https://golang.google.cn/dl/go1.15.15.linux-amd64.tar.gz
# 解压
tar -zxvf go1.15.15.linux-amd64.tar.gz -C /usr/
# 修改环境变量
vim /etc/profile

export GOROOT=/usr/go
export PATH=$GOROOT/bin:$PATH
export GOPATH=/opt/go15
export PATH=$(go env GOPATH)/bin:$PATH

通过修改环境变量的顺序,使得先读取到1.15.15版本的go

1
2
[root@localhost opt]# echo $PATH
/opt/go15/bin:/usr/go/bin:/usr/go/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/go/bin:/opt/go/BIN:/opt/go/bin/:/opt/go/bin:/root/bin:/usr/go/bin:/opt/go15/bin:/usr/go/bin:/opt/go15/bin

1.18版本go的环境变量,记录一下,方便后面恢复:

1
2
3
4
5
6
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
export GOPATH=/opt/go
export PATH=$PATH:$GOPATH/BIN
export PATH=$PATH:/opt/go/bin/
export PATH=$PATH:$(go env GOPATH)/bin

源码构建starport:

starport v0.13.1官方安装readme

下载源码:

1
2
3
4
5
# 下载starport V0.13.1源码
git clone -b v0.13.1 https://github.com/ignite/cli.git
cd cli
# 编译
make

报错,提示缺少packr2命令

1
2
/bin/sh: packr2: command not found
make: *** [build] Error 127

下载packr:

1
2
3
4
5
6
# 下载
wget https://github.com/gobuffalo/packr/releases/download/v2.8.1/packr_2.8.1_linux_amd64.tar.gz
# 解压缩
tar -xf packr_2.8.1_linux_amd64.tar.gz
# 移动可执行文件
mv packr2 /usr/bin/packr2

再次编译:

1
2
3
4
5
6
7
8
9
10
11
# 再次编译构建starport
make
# 生成在build目录下
[root@localhost build]# pwd
/blockchain/cli/build
# 确认版本
[root@localhost build]# ./starport version
starport version v0.13.1 linux/amd64 -build date: 2022-09-05T03:20:04
git object hash: b4e95f455cb9dfb35fcd9981786eb935cf9711c5
# 创建软连接
ln -s /blockchain/cli/build/starport /usr/bin/starport

程序目标:

你正在构建的应用程序的目标是让用户购买域名并为其设置解析的值,给定域名的所有者将是当前最高出价者。

区块链应用程序只是一个具有确定性的复制状态机.
状态机是一个计算机科学概念,其中一台机器可以有多个状态,但一次只能有一个状态。
它遵循状态转换过程(或一组定义的过程),这是状态从旧状态或初始状态 ( S) 更改为新状态 ( S')的唯一方式。
20220907161249
作为开发人员,你只需定义状态机(即状态,启动状态和触发状态转变的消息)Tendermint 将为你处理通过网络进行复制。

Tendermint是一个与应用程序无关的引擎,负责处理区块链的网络层共识层。实际上,这意味着Tendermint负责传播和排序交易字节。Tendermint Core依赖于拜占庭容错(BFT)算法来达成交易顺序的共识

20220907160412
Cosmos SDK旨在帮助你构建状态机,构建应用层。
SDK是一个模块化框架,意味着应用程序是通过将一组可互操作的模块集成在一起构建而成的。每个模块都包含自己的消息/交易处理器,而SDK负责将每条消息路由到其对应模块。

以下是nameservice应用程序所需的模块:

  • auth : 此模块定义了账户和手续费,并为你应用程序的其余部分提供了访问这些功能的权限。

  • bank : 此模块使得应用程序能够创建和管理token及余额。

  • nameservice : 需要我们自己写的nameservice应用的核心逻辑。

现在,看一下应用程序的两个主要部分:state(状态)message(消息)类型。

State:

state反映了特定时刻你的应用程序。它告诉了每个帐户拥有多少token,每个域名的所有者和价格,以及每个域名的解析值。

token 和帐户的 state 由authbank模块定义,这意味着你现在不必关心它。你需要做的是定义与你的nameservice模块特定相关部分state。
在 SDK 中,所有内容都存储在一个名为multistore的存储中。可以在此multistore中创建任意数量的键值对存储(在Cosmos SDK中称作KVStore)。
在本应用中,我们将使用一个 store 记录 namewhois 信息,name 的 valueownerprice 将存储在一个结构中。

Message:

message 包含在 transaction 中。它们负责触发state的转变
每个模块定义了一个message列表及如何去处理它们。

下面这些 message 是你需要为你的 nameservice 应用去实现的:

  • MsgSetName: 此 message 允许域名的所有者为指定域名的nameStore设置一个值。

  • MsgBuyName: 此 message 允许账户去购买一个域名并在ownerStore中成为所有者。

    • 当有人购买一个域名时,他们需要支付比之前所有者购买价格更高的费用。
    • 如果域名还没有人购买,那么他们需要燃烧最小价格(MinPrice)的代币。

交易的处理流程:

  • 当一条交易(包含在区块中)到达一个Tendermint节点时,它将通过 ABCI 传递给应用程序并被解码以得到 message

  • 然后将message路由至对应的模块module,并根据定义在Handler中的逻辑来进行处理。

  • 如果 state 需要更新,Handler会调用Keeper来执行更新。

开始编写你的程序:

1
2
# 生成脚手架
starport app github.com/jerrychan807/cosmos-nameservice --sdk-version="launchpad"
1
2
3
4
5
6
7
8
9
starport app github.com/jerrychan807/cosmos-nameservice --sdk-version="launchpad"

⭐️ Successfully created a Cosmos app 'cosmos-nameservice'.
👉 Get started with the following commands:

% cd cosmos-nameservice
% starport serve

NOTE: add --verbose flag for verbose (detailed) output.

其中app.go,这个文件是确定性状态机的核心。

app.go中,你定义了应用程序在接收交易时执行的操作
但首先,它要能够以正确的顺序接收交易。这是 Tendermint共识引擎的职责。
Tendermint 通过名为 ABCI 的接口将交易从网络传递给应用程序。
20220907181345
幸运的是,你不必实现ABCI接口。Cosmos SDKbaseapp的形式提供了区块链应用的雏形,我们只要基于baseapp定制化进行开发即可。
baseapp做了以下几点:

  • 解码从 Tendermint共识引擎接收到的交易

  • 从交易中提取 messages 并做基本的合理性校验。

  • 将这些 message 路由到合适的模块使其被正确处理。

  • 如果 ABCI 消息是DeliverTx(CheckTx)的话就Commit

  • 帮助设置BeginBlockEndBlock,这两种消息让你能定义在每个区块开始和结束时执行的逻辑。

  • 帮助初始化你的 state

  • 帮助设置 queries

baseapp不能够识别用户自定义模块的路由和应用程序中自定义的用户接口,我们需要将我们的cosmosnameservicetypes类型将嵌入到baseapp中。

需要修改 app.go

为了完成应用程序,你需要引入一些模块.继续开始构建你的域名服务模块.稍后会回到app.go.

类型Types:

我们要做的第一件事是定义一个结构,包含域名所有元数据。

1
2
# starport创建whois类型
starport type whois value price
1
2
3
[root@localhost cosmos-nameservice]# starport type whois value price

🎉 Created a type `whois`.

每个域名有以下相关的数据:

  • Value: 域名解析出为的值

  • Creator: 该域名当前所有者的地址

  • Price: 你需要为购买域名支付的费用

1
2
3
4
5
6
type Whois struct {
Creator sdk.AccAddress `json:"creator" yaml:"creator"`
ID string `json:"id" yaml:"id"`
Value string `json:"value" yaml:"value"`
Price sdk.Coins `json:"price"`
}

需要修改x/cosmosnameservice/types/TypeWhois.go

Keeper:

Cosmos SDK模块的主要核心是名为Keeper的部分。
它处理存储的交互,引用其他的keeper进行跨模块的交互,并包含模块的大部分核心功能。

Keeper结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package keeper

import (
"fmt"

"github.com/tendermint/tendermint/libs/log"

"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
)

// Keeper of the cosmosnameservice store
// Keeper结构体
type Keeper struct {
CoinKeeper bank.Keeper // bank模块, 底层管理token的模块
storeKey sdk.StoreKey // cosmos-sdk/types的通用存储key类型
cdc *codec.Codec // 底层编码模块
// paramspace types.ParamSubspace
}

需要修改x/cosmosnameservice/keeper/keeper.go

关于上述代码的几点说明:

3个不同的cosmos-sdk包被引入:

  • codec: 提供负责Cosmos编码格式的工具——Amino。

  • bank: 控制账户和转账。

  • types: 包含了整个SDK常用的类型。

Keeper结构体。在 keeper 中有几个关键部分:

  • bank.Keeper: 这是bank模块的Keeper引用。包括它来允许该模块中的代码调用bank模块的函数。

    • SDK使用对象能力来访问应用程序状态的各个部分。这是为了允许开发人员采用小权限准入原则,限制错误或恶意模块的去影响其不需要访问的状态的能力。
  • *codec.Codec: 这是被Amino用于编码及解码的指针。

  • sdk.StoreKey: 通过它来访问一个持久化保存你的应用程序状态的sdk.KVStore

模块有1个StoreKey:

  • storeKey: 这是 name 指向(如 map[name]Whois)Whois 结构的主存储空间

Getter 和 Setter:

现在要添加通过Keeper来与存储交互的方法了。
首先,添加一个函数来为指定域名设置解析字符串值:

1
2
3
4
5
6
7
8
9
10
11
12
// SetWhois sets a whois. We modified this function to use the `name` value as the key instead of msg.ID
// 存储Whois,我们修改这个函数,使用'name'值作为键,而不是msg.ID
func (k Keeper) SetWhois(ctx sdk.Context, name string, whois types.Whois) {
store := ctx.KVStore(k.storeKey)
// 使用cdc编码参数whois结构体返回的bz是byte切片
// MustMarshalBinaryLengthPrefixed 有Must代表不返回其错误直接Panic处理,即使有err的话,没有的话返回可能的错误
bz := k.cdc.MustMarshalBinaryLengthPrefixed(whois)
// 使用'name'值作为键,而不是msg.ID
key := []byte(types.WhoisPrefix + name)
// 存储
store.Set(key, bz)
}

查找域名对应的解析值函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// GetWhois returns the whois information
// 获取whois结构体数据, key就是域名的name
func (k Keeper) GetWhois(ctx sdk.Context, key string) (types.Whois, error) {
// 1. 使用StoreKey访问存储,获取namservice数据库
store := ctx.KVStore(k.storeKey)
var whois types.Whois
// 2. 拼接key, whois前缀 + 获取参数key
byteKey := []byte(types.WhoisPrefix + key)
// 3. 使用keeper的codec类型cdc解码, 赋值给whois结构体
err := k.cdc.UnmarshalBinaryLengthPrefixed(store.Get(byteKey), &whois)
if err != nil {
return whois, err
}
// 4. 返回
return whois, nil
}

需要修改x/cosmosnameservice/keeper/whois.go

其他函数:

继续添加其他函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 获取whois的所有数量
func (k Keeper) GetWhoisCount()
// 存储whois的总数
func (k Keeper) SetWhoisCount()
// 删除一个whois结构体
func (k Keeper) DeleteWhois()
// 获取域名的创建者
func (k Keeper) GetCreator()
// 检查当前域名key/name是否存在
func (k Keeper) Exists()
// 获取域名的解析值
func (k Keeper) ResolveName()
// 设置域名对应的解析值
func (k Keeper) SetName()
// 检查当前域名是否有创建人
func (k Keeper) HasCreator()
// 设置域名的当前拥有者
func (k Keeper) SetCreator()
// 获取域名的价格
func (k Keeper) GetPrice()
// 设置域名的价格
func (k Keeper) SetPrice()
// 检查当前的name参数是否存在于store中,注意于Exists函数的区别:没有加前缀
func (k Keeper) IsNamePresent()
// 获取固定前缀的迭代器
func (k Keeper) GetNamesIterator()
// 根据key = id获取whois的创建者, 但是可能会返回err
func (k Keeper) GetWhoisOwner()
// 根绝key查询是否存在, key是域名结构体中的id
func (k Keeper) WhoisExists()

接下来,该描述如何让用户通过MsgsHandlers与刚刚建立的store交互。

Msg 和 Handler:

现在你已经设置了Keeper,是时候构建允许用户购买域名和设置解析值的MsgHandler了。

Msg:

Msg触发状态转变。Msgs被包裹在客户端提交至网络的Txs中。
Cosmos SDK从Txs中打包和解包Msgs,这就意味着,作为一个应用开发者,你只需要去定义Msgs
Msg必须要满足以下接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package types

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

// Transactions messages must fulfill the Msg
// 实现接口必须实现所有的函数
type Msg interface {
// Return the message type.
// Must be alphanumeric or empty.
// 返回消息类型, 必须是字母或者空
Type() string

// Returns a human-readable string for the message, intended for utilization
// within tags
// 返回消息的可读string,用于在标签中使用
Route() string

// ValidateBasic does a simple validation check that
// doesn't require access to any other information.
// 做一些基本的验证, 不需要访问其他信息
ValidateBasic() error

// Get the canonical byte representation of the Msg.
// 获取Msg的规范字节表示即字节数组
GetSignBytes() []byte

// Signers returns the addrs of signers that must sign.
// CONTRACT: All signatures must be present to be valid.
// CONTRACT: Returns addrs in some deterministic order.
// 返回必须签名的签名者地址集合, 所有的签名必须在当下还有效, 返回的签名集合会以某种确定的顺序
GetSigners() []sdk.AccAddress
}

需要修改./x/cosmosnameservice/types/msg.go

Handler:

Handler定义了在接收到一个特定Msg时,需要采取的操作(哪些存储需要更新,怎样更新及要满足什么条件),可看作为controller
在此模块中,你有两种类型的Msg,用户可以发送这些Msg来和应用程序状态进行交互:SetNameBuyName。它们各自同其Handler关联。

现在你已经更好地理解了 MsgsHandler,可以开始构建你的第一条消息:SetName

MsgSetName:

SDK中Msg的命令约束是 Msg{.Action}。要实现的第一个操作是SetName,因此命名为MsgSetName
首先实现SetName, 这个消息Msg允许域名的所有者在解析器中设置该域名的返回值。

需要修改./x/cosmosnameservice/types/MsgSetName.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package types

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

// MsgSetName defines a SetName message
// 定义MsgSetName的结构
type MsgSetName struct {
Name string `json:"name"` // 目标域名
Value string `json:"value"` // 对应的值/解析值
Owner sdk.AccAddress `json:"owner"` // 拥有者
}

// NewMsgSetName is a constructor function for MsgSetName
// MsgSetName构造函数
func NewMsgSetName(name string, value string, owner sdk.AccAddress) MsgSetName {
return MsgSetName{
Name: name,
Value: value,
Owner: owner,
}
}

MsgSetName具有设置域名解析值所需的三个属性:

  • name: 所要设置的域名

  • value: 要设置的域名解析值

  • owner: 域名的所有者

接下来,实现Msg接口:

Route和Type接口:

1
2
3
4
5
6
7
// Route should return the name of the module
// 返回路由消息的键, 这里就是模块名
func (msg MsgSetName) Route() string { return RouterKey }

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

SDK使用上述函数将Msg路由至合适的模块进行处理。它们还为用于索引的数据库标签添加了可读性的名称。

ValidateBasic接口:

1
2
3
4
5
6
7
8
9
10
11
// 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
}

ValidateBasic用于对Msg有效性进行一些基本的无状态检查
在此情形下,请检查没有属性为空。
请注意这里使用sdk.Error类型,SDK提供了一组应用开发人员经常遇到的错误类型。

GetSignBytes接口:

1
2
3
4
5
6
7
// GetSignBytes encodes the message for signing
// GetSignBytes对整个MsgSetName消息本身进行编码、排序以进行后续的签名
func (msg MsgSetName) GetSignBytes() []byte {
// MustMarshalJSON序列化msg为字节切片
// MustSortJSON返回根据key排序的json
return sdk.MustSortJSON(ModuleCdc.MustMarshalJSON(msg))
}

GetSignBytes定义了如何编码Msg以进行签名。
在大多数情形下,要编码成排好序的JSON。不应修改输出。

GetSigners接口:

1
2
3
4
5
// GetSigners defines whose signature is required
// 定义该需要谁的签名
func (msg MsgSetName) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Owner}
}

GetSigners定义一个Tx上需要哪些人的签名才能使其有效。
在这种情形下,MsgSetName要求域名所有者在尝试重置域名解析值时要对该交易签名。

handlerMsgSetName:

目前MsgSetName已经规定好了, 当接收到了消息后Handler负责定义接下来的行动.
NewHandler本质上是一个子路由器,它将进入该模块的消息分配给适当的处理程序。
目前,只有一个Msg/Handler.

注意:SDK中handler的命名规范是handlerMsg{.Action}

需要修改x/cosmosnameservice/handlerMsgSetName.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cosmosnameservice

import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/keeper"
"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
)

// Handle a message to set name
// 接受到msg, 进一步的处理,相当于controller层, 操作keeper层
func handleMsgSetName(ctx sdk.Context, keeper keeper.Keeper, msg types.MsgSetName) (*sdk.Result, error) {
// 检查MsgSetName的提供者是否为想要设置域名的域名拥有者, 即验证身份
if !msg.Owner.Equals(keeper.GetWhoisOwner(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进行域名的解析值设定
keeper.SetName(ctx, msg.Name, msg.Value) // If so, set the name to the value specified in the msg.
return &sdk.Result{}, nil // return
}

handler.go是各个handlerxxxx.go总路由, 所以每一个对应的msg都需要在这里注册, 下面是修改完整的handler.go:

需要修改x/cosmosnameservice/handler.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package cosmosnameservice

import (
"fmt"

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

// NewHandler returns a handler for "nameservice" type messages.
// 返回一个操作nameservice各类消息的Handler对象
func NewHandler(keeper keeper.Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
//.(type)获取接口实例实际的类型指针, 以此调用实例所有可调用的方法,包括接口方法及自有方法。
//需要注意的是该写法必须与switch case联合使用,case中列出实现该接口的类型。
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()))
}
}
}

现在, 拥有者可以设置域名的解析了, 但是如果是一个域名不存在拥有者的情况呢?
现在模块需要提供一个途径让用户去购买域名, 下面就定义BuyName的Msg

MsgBuyName:

需要修改./x/cosmosnameservice/types/MsgBuyName.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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.
// 根据MsgCreateWhois文件改写
// 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
// 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
// 路由返回模块名nameService
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
// 返回消息的编码后格式[]byte
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}
}

handlerMsgBuyName:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package cosmosnameservice

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

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/keeper"
"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
)

// Handle a message to buy name
func handleMsgBuyName(ctx sdk.Context, k keeper.Keeper, msg types.MsgBuyName) (*sdk.Result, error) {
// Checks if the the bid price is greater than the price paid by the current owner
// 1.检查当前出价是否高于目前的价格, 注意Msg本身的检查只是简单的检查,这里需要额外数据的检查就只能在Handler中做
// GetPrice返回coin类型对象,其IsAllGT函数是比较大小(逐个字母比较,全部大于返回true)
// 当需要的价格 > bid那么就返回错误
if k.GetPrice(ctx, msg.Name).IsAllGT(msg.Bid) {
return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "Bid not high enough") // If not, throw an error
}
// 2.检查当前域名是否已经有拥有者了
// 不论是已拥有或者没有人拥有, 如果购买者支付出价出现错误,那么都会造成资金的回滚
if k.HasCreator(ctx, msg.Name) {
// 如果已经是别人拥有的,那么购买者支付对应的出价给域名原来的拥有者
// coin转移方向: msg.Buyer => Creator
// 金额: Bid
err := k.CoinKeeper.SendCoins(ctx, msg.Buyer, k.GetCreator(ctx, msg.Name), msg.Bid)
if err != nil {
return nil, err
}
} else {
// 如果没有,那么从购买者处减去出价金额, 发送给一个不可回收的地址(burns)
_, err := k.CoinKeeper.SubtractCoins(ctx, msg.Buyer, msg.Bid) // If so, deduct the Bid amount from the sender
if err != nil {
return nil, err
}
}
// 分别为域名设置新的所有者与金额
k.SetCreator(ctx, msg.Name, msg.Buyer)
k.SetPrice(ctx, msg.Name, msg.Bid)
return &sdk.Result{}, nil
}

同上,继续编写类似的MsgDeleteNamehandlerMsgDeleteName

现在已经有了MsgsHandlers定义,是时候学习如何使交易中的数据能被查询到!

Querier:

首先创建./x/cosmosnameservice/keeper/querier.go文件。在这里定义应用程序用户可以对那些状态进行查询。
你的nameservice模块会暴露3个querier:

  • resolve: 解析值

  • getWhois: 查询域名的所有相关信息

  • listWhois: 查询所有的已存在域名

首先定义NewQuerier函数,该函数充当查询此模块的子路由器(类似于NewHandler函数)。
请注意,因为querier没有类似于Msg的接口,所以需要手动定义switch语句(它们无法从query.Route()函数中删除):

需要修改x/cosmosnameservice/keeper/querier.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package keeper

import (
"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
// this line is used by starport scaffolding # 1
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.
// 创建了一个keeper层的查询对象给客户端
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
// 客户端输入的内容在types/querier.go中定义了常量作为路由
case types.QueryResolveName:
return resolveName(ctx, path[1:], k)
case types.QueryListWhois:
return listWhois(ctx, k)
case types.QueryGetWhois:
return getWhois(ctx, path[1:], k)
default:
return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "unknown nameservice query endpoint")
}
}
}

现在已定义路由器,为每个查询定义输入和响应:

需要修改x/cosmosnameservice/types/whois.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

//
// Functions used by querier 为用户查询的函数
//

// 查询whois的集合
func listWhois(ctx sdk.Context, k Keeper) ([]byte, error) {
var whoisList []types.Whois
store := ctx.KVStore(k.storeKey)
// 根据前缀创建循环迭代器, 遍历所有包含此前缀字段的key对应的value
// KVStorePrefixIterator 按升序迭代所有带有特定前缀的键
iterator := sdk.KVStorePrefixIterator(store, []byte(types.WhoisPrefix))
// 遍历
for ; iterator.Valid(); iterator.Next() {
var whois types.Whois
// 1. 迭代器获取包含特定前缀的完整key
// 2. 获取whois([]byte)进行解码
// 3. 赋值给whois
k.cdc.MustUnmarshalBinaryLengthPrefixed(store.Get(iterator.Key()), &whois)
// 添加到集合中
whoisList = append(whoisList, whois)
}
// 再将整个list解码/序列化成字节数组,
// MustMarshalJSONIndent有Must代表不返回其错误直接Panic处理, 即使有err的话,没有的话返回可能的错误
res := codec.MustMarshalJSONIndent(k.cdc, whoisList)
return res, nil
}

// Resolves a name, returns the value
// 解析域名对应的值,也就是whois中的字段value
func resolveName(ctx sdk.Context, path []string, keeper Keeper) ([]byte, error) {
// 直接调用keeper的解析函数(见下方), key是path[0]
value := keeper.ResolveName(ctx, path[0])

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

// 编码/序列化为字节数组
// QueryResResolve是types/querier文件下的函数, QueryResResolve就是解析值的一个结构体
// 因为MarshalJSONIndent解析需要一个结构体, 所以创建了这样的QueryResResolve结构体以赋值
res, err := codec.MarshalJSONIndent(keeper.cdc, types.QueryResResolve{Value: value})
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}

return res, nil
}

// 查询单个Whois
func getWhois(ctx sdk.Context, path []string, k Keeper) (res []byte, sdkError error) {
// 获取key, path的第一个参数(用户命令行输入)
key := path[0]
// 调用keeper的基本方法GetWhois(见上方)
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
}

在这里,你的Keepergettersetter被大量使用。
当构建任何其他使用此模块的应用程序时,您可能需要返回并定义更多的getter/setter来访问您需要的状态片段。
按照约定,每个输出类型都应该是JSON marshallablestringable(实现了Golang fmt接口)。返回的字节应该是输出结果的JSON编码。

  • 因此,对于resolve的输出类型,我们将解析字符串包装在一个名为QueryResResolve的结构中,该结构既是JSON marshallable的,又有.string()方法。

  • 对于Whois的输出,正常的Whois结构已经是JSON marshallable的,但是我们需要在其上添加一个.string()方法。

  • 对于names查询的输出也是一样的,[]字符串已经是本机可编组的,但是我们想在其上添加一个. string()方法。

既然你有办法改变和查看模块状态,那么现在是时候完成它了! 接下来以Amino编码格式注册类型!

Codec文件:

Amino中注册你的数据类型使得它们能够被编码/解码
你创建的任何接口和实现接口的任何结构都需要在RegisterCodec函数中声明。
在此模块中,需要注册三个Msg的实现(SetName, BuyName and DeleteName)。

需要修改x/cosmosnameservice/types/codec.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package types

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

// RegisterCodec registers concrete types on codec
// codec上注册具体的类型
func RegisterCodec(cdc *codec.Codec) {
// this line is used by starport scaffolding # 1
cdc.RegisterConcrete(MsgBuyName{}, "cosmosnameservice/BuyName", nil)
cdc.RegisterConcrete(MsgSetName{}, "cosmosnameservice/SetName", nil)
cdc.RegisterConcrete(MsgDeleteName{}, "cosmosnameservice/DeleteName", nil)
}

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

func init() {
// 创建实例codec
ModuleCdc = codec.New()
RegisterCodec(ModuleCdc)
// Register the go-crypto to the codec
// 注册加密函数信息
codec.RegisterCrypto(ModuleCdc)
// 封装
ModuleCdc.Seal()
}

Nameservice模块的CLI:

Cosmos SDK使用cobra库进行CLI交互。该库使每个模块都可以轻松地显示自己的操作命令。
要开始定义用户与模块的CLI交互,请创建以下文件:

  • ./x/cosmosnameservice/client/cli/queryWhois.go

  • ./x/cosmosnameservice/client/cli/txWhois.go

Queries:

实现查询的客户端命令逻辑:

需要修改x/cosmosnameservice/client/cli/queryWhois.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package cli

import (
"fmt"

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

// 获取所有的whois
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
// 解码结果放到out中
cdc.MustUnmarshalJSON(res, &out)
return cliCtx.PrintOutput(out)
},
}
}

// 获取一个whois
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), // 参数对应key
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
key := args[0] // 获取命令中的key
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)
},
}
}

注意上述代码中:

  • CLI 引入了一个新的context:CLIContext。它包含有关CLI交互所需的用户输入和应用程序配置的数据。

  • cliCtx.QueryWithData()函数所需的path直接从你的查询路径中映射。

    • 路径的第一部分用于区分 SDK 应用程序可能的querier类型:custom用于Querier。
    • 第二部分(nameservice)是将查询路由到的模块的名称。
    • 最后是要调用模块中的特定的querier
    • 在这个例子中,第四部分是查询。这是因为查询参数是一个简单的字符串。要启用更复杂的查询输入,你需要使用.QueryWithData()函数的第二个参数来传入data

继续在cli/query.go中添加子命令:

需要修改x/cosmosnameservice/client/cli/query.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package cli

import (
"fmt"
// "strings"

"github.com/spf13/cobra"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"

// "github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
// sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
)

// GetQueryCmd returns the cli query commands for this module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
// Group nameservice queries under a subcommand
nameserviceQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: fmt.Sprintf("Querying commands for the %s module", types.ModuleName),
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}

nameserviceQueryCmd.AddCommand(
flags.GetCommands(
// this line is used by starport scaffolding # 1
GetCmdListWhois(queryRoute, cdc),
GetCmdGetWhois(queryRoute, cdc),
// 注意一定添加这个命令, 不然的话使用命令行测试时resolve命令无法解析!!!
GetCmdResolveName(queryRoute, cdc),
)...,
)
return nameserviceQueryCmd
}

Transaction:

接下来实现交易命令的客户端。

需要修改x/cosmosnameservice/client/cli/txWhois.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package cli

import (
"bufio"

"github.com/spf13/cobra"

"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/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
)

// 购买新域名
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]) //获取购买的名字name

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
}
// 构建NewMsgBuyName实例
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 [id]",
Short: "Delete a new name by ID",
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/cosmosnameservice/client/cli/tx.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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/jerrychan807/cosmos-nameservice/x/cosmosnameservice/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,
}

// 把txWhois.go中的命令都添加
nameserviceTxCmd.AddCommand(flags.PostCommands(
// this line is used by starport scaffolding
GetCmdBuyName(cdc),
GetCmdSetWhois(cdc),
GetCmdDeleteWhois(cdc),
)...)

return nameserviceTxCmd
}

注意在上述代码中:

  • 使用了authcmd包,它提供对CLI控制的帐户的访问权限,并便于签名。

NameService模块的REST接口:

你的模块还可以公开 REST 接口,提供程序访问模块的功能。。首先创建一个文件来保存HTTP的handler

RegisterRoutes:

首先在RegisterRoutes函数中为模块定义REST客户端接口。路由都以模块名称开头,以防止命名空间与其他模块的路径冲突:

需要修改./x/cosmosnameservice/client/rest/rest.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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")
}

Query Handlers:

接下来,是时候定义上面提到的处理程序了。这些与前面定义的CLI方法非常相似。

需要修改x/cosmosnameservice/client/rest/queryWhois.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package rest

import (
"fmt"
"github.com/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
"net/http"

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

// 查询所有的whois
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)
}
}

上述代码要注意:

  • 我们使用相同的cliCtx.QueryWithData函数来获取数据

  • 这些函数与相应的CLI功能几乎相同

Tx Handlers:

现在定义 buyNamesetName 交易路由。
请注意,这些实际上并不能将交易发送进行买入和设置名称。 这需要跟交易请求一起发送发送密码,将是一个安全问题。
相反,这些端点构建并返回每个特定交易,然后可以以安全的方式对其进行签名,然后使用标准端点(如/txs)将其广播到网络。

需要修改x/cosmosnameservice/client/rest/txWhois.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package rest

import (
"fmt"
"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/jerrychan807/cosmos-nameservice/x/cosmosnameservice/types"
"net/http"
)

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
}
// AccAddressFromBech32转换string为32位地址的方法
addr, err := sdk.AccAddressFromBech32(req.Buyer)
fmt.Println(addr)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// 解析金额
// ParseCoins 将字符串转为coin
coins, err := sdk.ParseCoins(req.Price)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// 创建NewMsgBuyName对象
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"`
Creator string `json:"creator"`
}

// 设置解析值
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.Creator)
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})
}
}

注意上述代码:

  • BaseReq包含用于进行交易的基本必填字段(使用哪个密钥,如何解码,使用哪条链等等)并且如所示被设计成嵌入形式。

  • baseReq.ValidateBasicutils.CompleteAndBroadcastTxREST为你设置响应代码,因此你需担心在使用这些函数时处理错误或成功。

现在你的模块已经具备了全部功能,需要与 Cosmos SDK 应用合并。

AppModule Interface:

Cosmos SDK为模块提供了一个标准接口。这个AppModule接口要求模块提供一组ModuleBasicsManager使用的方法,以便将它们合并到你的应用程序中。

相关文件: x/cosmosnameservice/module.go

Genesis:

AppModule接口包含了许多用于初始化和导出初始化状态的区块链函数。
当启动、停止或导出链时,ModuleBasicManager在每个模块上调用这些函数。下面是一个非常基本的实现,您可以对其进行扩展。
x/cosmosnameservice/types/genesis.go。我们会定义初始状态是什么,默认的初始状态以及验证它的方法这样我们就不会遇到任何错误当我们以预先存在的状态开始链的时候。

相关文件: x/cosmosnameservice/types/genesis.go

关于上述代码的一些注意事项:

  • ValidateGenesis(): 验证提供的生成状态,以确保所期望的不变量保持不变

  • DefaultGenesisState(): 主要用于测试。这提供了一个最小的起源状态。

  • 在链启动时调用InitGenesis(),该函数将生成状态导入到keeper中。

  • ExportGenesis()在停止链后被调用,此函数将应用程序状态加载到GenesisState结构中,以便稍后导出到genesis.json以及来自其他模块的数据。

引入你的模块并完成程序:

现在你的模块已就绪,它可以和其它两个模块auth和bank被合并到./app.go文件中:

需要修改app/app.go

接下来,你需要在NewApp结构体中添加存储的keyKeepers

NewApp结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 新的app结构体
type NewApp struct {
*bam.BaseApp
cdc *codec.Codec

invCheckPeriod uint

keys map[string]*sdk.KVStoreKey
tKeys map[string]*sdk.TransientStoreKey

subspaces map[string]params.Subspace

accountKeeper auth.AccountKeeper
bankKeeper bank.Keeper
stakingKeeper staking.Keeper
supplyKeeper supply.Keeper
paramsKeeper params.Keeper
nameserviceKeeper cosmosnameservicekeeper.Keeper
// this line is used by starport scaffolding # 3
mm *module.Manager

sm *module.SimulationManager
}

var _ simapp.App = (*NewApp)(nil)

构造函数:

并更新构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 初始化app
func NewInitApp(
logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
invCheckPeriod uint, baseAppOptions ...func(*bam.BaseApp),
) *NewApp {
// 首先定义将被不同模块共享的编解码器
cdc := MakeCodec()
// BaseApp通过ABCI协议处理与Tendermint的交互
bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc), baseAppOptions...)
bApp.SetCommitMultiStoreTracer(traceStore)
bApp.SetAppVersion(version.Version)
// 所需要存储的键值
keys := sdk.NewKVStoreKeys(
bam.MainStoreKey,
auth.StoreKey,
staking.StoreKey,
supply.StoreKey,
params.StoreKey,
cosmosnameservicetypes.StoreKey,
// this line is used by starport scaffolding # 5
)

tKeys := sdk.NewTransientStoreKeys(staking.TStoreKey, params.TStoreKey)
// 初始化一个APP
var app = &NewApp{
BaseApp: bApp,
cdc: cdc,
invCheckPeriod: invCheckPeriod,
keys: keys,
tKeys: tKeys,
subspaces: make(map[string]params.Subspace),
}

// paramsKeeper 处理应用程序的参数存储
app.paramsKeeper = params.NewKeeper(app.cdc, keys[params.StoreKey], tKeys[params.TStoreKey])
app.subspaces[auth.ModuleName] = app.paramsKeeper.Subspace(auth.DefaultParamspace)
app.subspaces[bank.ModuleName] = app.paramsKeeper.Subspace(bank.DefaultParamspace)
app.subspaces[staking.ModuleName] = app.paramsKeeper.Subspace(staking.DefaultParamspace)

// AccountKeeper 处理address -> account对应关系
app.accountKeeper = auth.NewAccountKeeper(
app.cdc,
keys[auth.StoreKey],
app.subspaces[auth.ModuleName],
auth.ProtoBaseAccount,
)
// bankKeeper允许你与sdk.Coins交互
app.bankKeeper = bank.NewBaseKeeper(
app.accountKeeper,
app.subspaces[bank.ModuleName],
app.ModuleAccountAddrs(),
)

app.supplyKeeper = supply.NewKeeper(
app.cdc,
keys[supply.StoreKey],
app.accountKeeper,
app.bankKeeper,
maccPerms,
)

stakingKeeper := staking.NewKeeper(
app.cdc,
keys[staking.StoreKey],
app.supplyKeeper,
app.subspaces[staking.ModuleName],
)
// 处理Atom
app.stakingKeeper = *stakingKeeper.SetHooks(
staking.NewMultiStakingHooks(),
)
// 与我们自定义的域名服务模块交互
app.nameserviceKeeper = cosmosnameservicekeeper.NewKeeper(
app.bankKeeper,
app.cdc,
keys[cosmosnameservicetypes.StoreKey],
)

// this line is used by starport scaffolding # 4

app.mm = module.NewManager(
genutil.NewAppModule(app.accountKeeper, app.stakingKeeper, app.BaseApp.DeliverTx),
auth.NewAppModule(app.accountKeeper),
bank.NewAppModule(app.bankKeeper, app.accountKeeper),
supply.NewAppModule(app.supplyKeeper, app.accountKeeper),
cosmosnameservice.NewAppModule(app.nameserviceKeeper, app.bankKeeper),
staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper),
// this line is used by starport scaffolding # 6
)

app.mm.SetOrderEndBlockers(staking.ModuleName)

app.mm.SetOrderInitGenesis(
staking.ModuleName,
auth.ModuleName,
bank.ModuleName,
cosmosnameservicetypes.ModuleName,
supply.ModuleName,
genutil.ModuleName,
// this line is used by starport scaffolding # 7
)
// 注册路由的句柄
// 注册域名服务路由和查询路由
app.mm.RegisterRoutes(app.Router(), app.QueryRouter())
// 初始化相关参数
app.SetInitChainer(app.InitChainer)
app.SetBeginBlocker(app.BeginBlocker)
app.SetEndBlocker(app.EndBlocker)
// AnteHandler 处理签名验证和交易预处理
app.SetAnteHandler(
auth.NewAnteHandler(
app.accountKeeper,
app.supplyKeeper,
auth.DefaultSigVerificationGasConsumer,
),
)
// 从KV数据库加载相关数据
app.MountKVStores(keys)
app.MountTransientStores(tKeys)

if loadLatest {
err := app.LoadLatestVersion(app.keys[bam.MainStoreKey])
if err != nil {
tmos.Exit(err.Error())
}
}

return app
}

构造函数包含以下步骤:

  • 从每个所需模块中实例化所需的Keeper

  • 生成每个Keeper所需的storeKey

  • 注册每个模块的handler

  • 注册每个模块的querier

  • KVStores挂载到baseApp的multistore提供的key值

  • 设置initChainer来定义初始应用程序状态

注意模块的启动方式:顺序很重要!在这里,序列是Auth-> Bank-> Feecollection-> Stake-> Distribution-> Slashing,,然后为stake模块设置了钩子。这是因为其中一些模块在使用之前就依赖于其他现有模块。

辅助函数:

添加一个辅助函数来生成一个animo–*codec.Codec,它可以正确地注册你应用程序中使用的所有模块:

1
2
3
4
5
6
7
8
9
10
11
// 生成一个animo: *codec.Codec
// 正确地注册你应用程序中使用的所有模块
func MakeCodec() *codec.Codec {
var cdc = codec.New()

ModuleBasics.RegisterCodec(cdc)
sdk.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)

return cdc.Seal()
}

现在您已经创建了一个包含模块的应用程序,现在是时候构建入口点了!

程序入口点Entrypoint:

golang的规范是把编译成可执行程序的文件放在项目的./cmd文件夹中。对于你的应用程序,您要创建2个可执行程序:

  • cosmos-nameserviced : 此可执行程序类似于bitcoind或其他加密货币的daemon,因为它维护p2p连接,广播交易,处理本地存储并提供用以与网络交互的RPC接口。在这种情况下,Tendermint被用于网络层和排序交易。

  • cosmos-nameservicecli : 此可执行程序提供用户与你的应用程序交互的命令。

相关文件:

  • cmd/cosmos-nameserviced/main.go

  • cmd/cosmos-nameservicecli/main.go

cosmos-nameserviced:

相关文件:cmd/cosmos-nameserviced/main.go

注意上述代码中:

  • 上面的大部分代码都结合了来自以下包的CLI命令:

    1. Tendermint
    2. Cosmos-SDK
    3. 你的nameservice模块
  • InitCmd允许应用程序从配置中生成创世纪状态。深入了解函数调用,以了解有关区块链初始化过程的更多信息。

  • AddGenesisAccountCmd可以方便地将帐户添加到创世文件中,允许在区块链启动时就使用资产钱包。

cosmos-nameservicecli:

相关文件:cmd/cosmos-nameservicecli/main.go

注意:

  • 代码结合了来自以下包的CLI命令:Tendermint、Cosmos-SDK、你的nameservice模块

  • cobra CLI文档将有助于理解上述代码

  • 你可以在这里看到之前定义的ModuleClient

  • 注意如何将路由包含在registerRoutes函数中

现在你已经定义了二进制文件,那么就可以来处理依赖关系管理并构建应用程序。

构建你的程序:

Makefile:

通过在根目录中编写包含常用命令的./Makefile来帮助用户编译应用程序:

相关文件:./Makefile

go.mod:

Golang有一些依赖管理工具。在本教程中,你将使用Go Modules。Go Modules使用仓库根目录中的go.mod文件来定义应用程序所需的依赖项。Cosmos SDK 应用程序目前依赖于某些库的特定版本。

编译应用程序:

1
2
3
4
5
6
7
8
9
10
11
# 添加环境变量
vim /etc/profile
export PATH=$(go env GOPATH)/bin:$PATH

# Install the app into your $GOBIN
make install

# Now you should be able to run the following commands:
# 可执行文件会生成在GOPATH的bin目录下
cosmos-nameserviced help
cosmos-nameservicecli help

cosmos-nameserviced

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[root@localhost cosmos-nameservice]# cosmos-nameserviced help
app Daemon (server)

Usage:
cosmos-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 cosmos-nameserviced
--home string directory for config and data (default "/root/.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 "cosmos-nameserviced [command] --help" for more information about a command.

cosmos-nameservicecli:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@localhost cosmos-nameservice]# cosmos-nameservicecli help
Command line interface for interacting with cosmosnameserviced

Usage:
cosmos-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 cosmos-nameservicecli
--home string directory for config and data (default "/root/.nameservicecli")
-o, --output string Output format (text|json) (default "text")
--trace print out full stack trace on errors

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

恭喜,您已完成名称服务应用!尝试运行并使用吧!

运行程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 初始化配置文件和创世文件
cosmos-nameserviced init moniker --chain-id namechain

# 创建两个账号,分别为 jack 和 alice
cosmos-nameservicecli keys add jack
cosmos-nameservicecli keys add alice

# 往 genesis.json 中添加创世账号,账号需要包括地址和初始的币
cosmos-nameserviced add-genesis-account $(cosmos-nameservicecli keys show jack -a) 1000nametoken,1000000000stake
cosmos-nameserviced add-genesis-account $(cosmos-nameservicecli keys show alice -a) 1000nametoken,1000000000stake

# 检查资金余额
cosmos-nameservicecli query account $(cosmos-nameservicecli keys show jack -a)
cosmos-nameservicecli query account $(cosmos-nameservicecli keys show alice -a)

# 创建一笔创世交易, 交易生成在 /config/gentx/filename.json
cosmos-nameserviced gentx --name jack

# 将上一步生成的创世交易写入 genesis.json
cosmos-nameserviced collect-gentxs

# 校验 genesis 信息
cosmos-nameserviced validate-genesis

# Configure your CLI to eliminate need for chain-id flag
cosmos-nameservicecli config chain-id namechain
cosmos-nameservicecli config output json
cosmos-nameservicecli config indent true
cosmos-nameservicecli config trust-node true

# 启动 namachain
cosmos-nameserviced start

将看到日志开始不停输出,表示正在生成的区块:

20220908145241

使用cli测试域名解析应用:

1
2
3
# 检查资金余额
cosmos-nameservicecli query account $(cosmos-nameservicecli keys show jack -a)
cosmos-nameservicecli query account $(cosmos-nameservicecli keys show alice -a)

购买域名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# jack 花费 20nametoken 购买 namechain.com
cosmos-nameservicecli tx cosmosnameservice buy-name namechain.com 20nametoken --from jack
[root@localhost cosmos-nameservice]# cosmos-nameservicecli tx cosmosnameservice buy-name namechain.com 20nametoken --from jack
Enter keyring passphrase:
{
"chain_id": "namechain",
"account_number": "2",
"sequence": "1",
"fee": {
"amount": [],
"gas": "200000"
},
"msgs": [
{
"type": "cosmosnameservice/BuyName",
"value": {
"name": "namechain.com",
"bid": [
{
"denom": "nametoken",
"amount": "20"
}
],
"buyer": "cosmos1vwp2kpjyg7g2x8pd320rugw0m7ut8q3ha9m7u4"
}
}
],
"memo": ""
}

confirm transaction before signing and broadcasting [y/N]: y
Enter keyring passphrase:
{
"height": "0",
"txhash": "5B8C36D6DCF7334255BD8E974D095389D7BC75577024408F4BB7396B607A84F1",
"raw_log": "[]"
}

设置域名解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 设置域名的解析值
# jack 发起一笔交易,给 namechain 设置 DNS 解析为 8.8.8.8
# 注意官方写反了!
cosmos-nameservicecli tx cosmosnameservice set-name 8.8.8.8 namechain.com --from jack
[root@localhost cosmos-nameservice]# cosmos-nameservicecli tx cosmosnameservice set-name 8.8.8.8 namechain.com --from jack
Enter keyring passphrase:
{
"chain_id": "namechain",
"account_number": "2",
"sequence": "4",
"fee": {
"amount": [],
"gas": "200000"
},
"msgs": [
{
"type": "cosmosnameservice/SetName",
"value": {
"name": "namechain.com",
"value": "8.8.8.8",
"owner": "cosmos1vwp2kpjyg7g2x8pd320rugw0m7ut8q3ha9m7u4"
}
}
],
"memo": ""
}

confirm transaction before signing and broadcasting [y/N]: y
Enter keyring passphrase:
{
"height": "0",
"txhash": "B9D2D889371BD86AFCCF5C36A2F3143B0FCCF56A7B0B4BDF247301ABBBFA893B",
"raw_log": "[]"
}

查询域名解析:

1
2
3
4
5
6
7
8
# 针对您注册的名称尝试一个resolve查询
# 查询 namechain.com 的信息
cosmos-nameservicecli query cosmosnameservice resolve namechain.com

[root@localhost cosmos-nameservice]# cosmos-nameservicecli query cosmosnameservice resolve namechain.com
{
"value": "8.8.8.8"
}

whois查询:

1
2
# whois查询
cosmos-nameservicecli query cosmosnameservice get-whois namechain.com
1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost cosmos-nameservice]# cosmos-nameservicecli query cosmosnameservice get-whois namechain.com
{
"creator": "cosmos1vwp2kpjyg7g2x8pd320rugw0m7ut8q3ha9m7u4",
"id": "",
"value": "8.8.8.8",
"price": [
{
"denom": "nametoken",
"amount": "20"
}
]
}

购买他人的域名:

1
2
# alice买jack的域名
cosmos-nameservicecli tx cosmosnameservice buy-name namechain.com 20nametoken --from alice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[root@localhost cosmos-nameservice]# cosmos-nameservicecli tx cosmosnameservice buy-name namechain.com 20nametoken --from alice
Enter keyring passphrase:
{
"chain_id": "namechain",
"account_number": "3",
"sequence": "0",
"fee": {
"amount": [],
"gas": "200000"
},
"msgs": [
{
"type": "cosmosnameservice/BuyName",
"value": {
"name": "namechain.com",
"bid": [
{
"denom": "nametoken",
"amount": "20"
}
],
"buyer": "cosmos192t6nfdqa9z6n3j8djpyupdzjfdwq6mr9v87vw"
}
}
],
"memo": ""
}

confirm transaction before signing and broadcasting [y/N]: y
Enter keyring passphrase:
{
"height": "0",
"txhash": "F27BB0F6BA6D861B8C8B6FE48F54CC4D2869E313A8F630B0116C8A227C648A61",
"raw_log": "[]"
}
1
2
# 查询域名拥有者
cosmos-nameservicecli query cosmosnameservice get-whois namechain.com
1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost cosmos-nameservice]# cosmos-nameservicecli query cosmosnameservice get-whois namechain.com
{
"creator": "cosmos192t6nfdqa9z6n3j8djpyupdzjfdwq6mr9v87vw",
"id": "",
"value": "8.8.8.8",
"price": [
{
"denom": "nametoken",
"amount": "20"
}
]
}

creator已由cosmos1vwp2kpjyg7g2x8pd320rugw0m7ut8q3ha9m7u4 jack变更为cosmos192t6nfdqa9z6n3j8djpyupdzjfdwq6mr9v87vw alice

删除域名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# alice删除域名
cosmos-nameservicecli tx cosmosnameservice delete-name namechain.com --from alice

[root@localhost cosmos-nameservice]# cosmos-nameservicecli tx cosmosnameservice delete-name namechain.com --from alice
Enter keyring passphrase:
{
"chain_id": "namechain",
"account_number": "3",
"sequence": "1",
"fee": {
"amount": [],
"gas": "200000"
},
"msgs": [
{
"type": "cosmosnameservice/DeleteName",
"value": {
"id": "namechain.com",
"creator": "cosmos192t6nfdqa9z6n3j8djpyupdzjfdwq6mr9v87vw"
}
}
],
"memo": ""
}

confirm transaction before signing and broadcasting [y/N]: y
Enter keyring passphrase:
{
"height": "0",
"txhash": "D168567D166238B1D11B494BFE13CAC317A72E7D0C296EAAB3F96B817159D156",
"raw_log": "[]"
}
1
2
3
4
# 查询域名拥有者
[root@localhost cosmos-nameservice]# cosmos-nameservicecli query cosmosnameservice get-whois namechain.com
could not resolve whois namechain.com
internal

列出所有域名:

1
2
# 列出所有的nameservice域名
cosmos-nameservicecli query cosmosnameservice list-whois

恭喜,您已经构建了一个Cosmos SDK应用程序!本教程现已完成。接下来是要查看如何使用REST服务器运行相同的命令.

rest部分没测,先略过。

初始化的配置文件:

1
cosmos-nameserviced init <moniker> --chain-id=namechain

初始化配置文件和创世文件,默认目录/root/.nameserviced/config/
生成文件结构如下:

1
2
3
4
5
6
7
8
├── config
│ ├── app.toml
│ ├── config.toml
│ ├── genesis.json
│ ├── node_key.json
│ └── priv_validator_key.json
└── data
└── priv_validator_state.json

app.toml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 最小交易gas费用
minimum-gas-prices = ""

# state 存储策略
pruning = "default"

# These are applied if and only if the pruning strategy is custom.
pruning-keep-recent = "0"
pruning-keep-every = "0"
pruning-interval = "0"

# 暂停的区块高度,用于升级(升级高度)或测试:
halt-height = 0

# 暂停的区块时间,到达该时间时暂停出块,用于升级或测试:
halt-time = 0

# 启用 inter-block cache:
inter-block-cache = true

config.toml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# ABCI socket 地址:
proxy_app = "tcp://127.0.0.1:26658"
# 节点名称:
moniker = "moniker"
# 快速同步,允许并发下载区块,同时验证commit
fast_sync = true
# 数据库 (goleveldb | cleveldb | boltdb | rocksdb)
db_backend = "goleveldb"
# 数据库存放位置
db_dir = "data"
# 日志输出
log_level = "main:info,state:info,*:error"
# 日志格式化
log_format = "plain"
# 创世文件位置,包含 初始 validator 集合和其他元数据
genesis_file = "config/genesis.json"
# validator 私钥文件
priv_validator_key_file = "config/priv_validator_key.json"
# 存放 validator 最后的签名状态
priv_validator_state_file = "data/priv_validator_state.json"
# Tendermint 监听来自外部 validator 连接进程的地址
priv_validator_laddr = ""
# 在 P2P 中进行节点验证的私钥
node_key_file = "config/node_key.json"
# abci 连接机制 (socket | grpc)
abci = "socket"
# profiling server 监听地址
prof_laddr = "localhost:6060"
# 连接新节点时,是否查询 ABCI 应用
filter_peers = false
# RPC server 监听地址
laddr = "tcp://127.0.0.1:26657"
# 允许的跨域请求 origins
cors_allowed_origins = []
# 允许的跨域请求方法
cors_allowed_methods = ["HEAD", "GET", "POST", ]
# 跨域请求中允许携带的请求头
cors_allowed_headers = ["Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time", ]
# grpc 监听地址
grpc_laddr = ""
# grpc 最大并发连接数
grpc_max_open_connections = 900
# 是否允许非安全 RPC 命令
unsafe=false
# 最大并发连接数
max_open_connections = 900

项目目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
├── app
│   ├── app.go
│   ├── export.go
│   └── prefix.go
├── cmd
│   ├── cosmos-nameservicecli
│   │   └── main.go
│   └── cosmos-nameserviced
│   ├── genaccounts.go
│   └── main.go
├── config.yml
├── go.mod
├── go.sum
├── Makefile
├── readme.md
├── vue //
└── x
└── cosmosnameservice
├── abci.go // 实现在BeginBlock()和EndBlock()中需要触发的逻辑
├── client // 本模块支持的 命令行方法和REST方法
│   ├── cli
│   │   ├── query.go // 从命令行构建本模块支持的 查询请求
│   │   ├── queryWhois.go
│   │   ├── tx.go // 从命令行构建本模块支持的 交易请求
│   │   └── txWhois.go
│   └── rest
│   ├── queryWhois.go // 本模块支持的REST查询请求
│   ├── rest.go
│   └── txWhois.go // 本模块支持的REST交易
├── genesis.go // 导入模块的初始状态以及导出模块的状态
├── handler.go // 处理本模块支持的所有类型的消息
├── handlerMsgBuyName.go
├── handlerMsgDeleteName.go
├── handlerMsgSetName.go
├── keeper // 本模块的子存储空间的读写功能
│   ├── keeper.go
│   ├── params.go
│   ├── querier.go // 本模块的子存储空间的查询
│   └── whois.go
├── module.go // 实现APPModule接口
├── spec // 模块的规范性说明文档
│   └── README.md
└── types
├── codec.go // 使用Amino进行序列化的数据类型
├── errors.go // 模块执行过程中可能产生的错误
├── events.go // 本模块在处理交易或请求时要推送的事件类型
├── expected_keepers.go // 本模块以来的其他模块的接口
├── genesis.go // 本模块初始化时的相关类型和默认的初始状态
├── key.go // 本模块的子存储空间读写时对应的键
├── MsgBuyName.go
├── MsgCreateWhois.go
├── MsgDeleteName.go
├── MsgDeleteWhois.go
├── msg.go // 本模块负责处理的消息
├── MsgSetName.go
├── MsgSetWhois.go
├── params.go // 本模块的参数配置
├── querier.go // 本模块负责处理的查询请求相关类型的定义
├── types.go
└── TypeWhois.go

Refs: