万字雄文讲解如何用cosmos构建自己的区块链

用cosmos构建自己的区块链——总体设计

你正在构建的应用程序的目标是让用户购买域名并为其设置解析的值。给定域名的所有者将是当前最高出价者。在本节中,你将了解如何将这些简单需求转化为程序的设计。

区块链应用程序只是一个具有确定性的复制状态机。作为开发人员,你只需定义状态机(即状态,启动状态和触发状态转变的消息),Tendermint 将为你处理通过网络进行复制。

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

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

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

  • auth : 此模块定义了账户和手续费,并为你应用程序的其余部分提供了访问这些功能的权限。
  • bank : 此模块使得应用程序能够创建和管理token及余额。
  • nameservice : 此模块目前还不存在其将处理你所构建的nameservice应用的核心逻辑。它是你构建应用程序时必须使用的主要部分。

你可能会好奇为什么没有模块来处理验证人集合的变更。实际上,Tendermint依靠一组验证人来对下一个要添加至区块链的有效交易区块达成共识。默认情况下,如果没有模块处理验证集合的变更,验证人集合将与创世文件genesis.json中定义的验证人集合保持一致。该应用程序就是这种情况。如果要允许更改应用程序的验证人集合,可以使用SDK的 staking 模块,或编写自己的模块

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

State

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

token 和帐户的 state 由authbank模块定义,这意味着你现在不必关心它。你需要做的是定义与你的nameservice模块特定相关部分state。

在 SDK 中,所有内容都存储在一个名为multistore的存储中。可以在此 multistore 中创建任意数量的键值对存储(在Cosmos SDK中称作KVStore)。在本应用中,我们将使用一个 store 记录 name 与 whois 信息,name 的 value、owner 和 price 将存储在一个结构中。

Message

message 包含在 transaction 中。它们负责触发 state 的转变。每个模块定义了一个 message 列表及如何去处理它们。下面这些 message 是你需要为你的 nameservice 应用去实现的:

  • MsgSetName: 此 message 允许域名的所有者为指定域名的nameStore设置一个值。
  • MsgBuyName: 此 message 允许账户去购买一个域名并在ownerStore中成为所有者。
    • 当有人购买一个域名时,他们需要支付币之前所有者购买价格更高的费用。如果域名还没有人购买,那么他们需要销毁最小价格(MinPrice)的代币。

当一条交易(包含在区块中)到达一个Tendermint节点时,它将通过 ABCI 传递给应用程序并被解码以得到 message。然后将message路由至对应的模块,并根据定义在Handler中的逻辑来进行处理。如果 state 需要更新,Handler会调用Keeper来执行更新。你将在后面的教程了解有关这些概念的更多信息。

现在你已经从高层视角完成了对应用程序的设计,是时候开始实现它了。

用cosmos构建自己的区块链——开始编写你的代码

首先创建一个新的文件./app.go。这个文件是确定性状态机的核心。

app.go中,你定义了应用程序在接收交易时执行的操作。但首先,它要能够以正确的顺序接收交易。这是 Tendermint共识引擎的职责。

引入必要的依赖:

package appimport (
  "github.com/tendermint/tendermint/libs/log"
  "github.com/cosmos/cosmos-sdk/x/auth"

  bam "github.com/cosmos/cosmos-sdk/baseapp"
  dbm "github.com/tendermint/tm-db")

下面是各引入模块和包的文档:

  • log: Tendermint 的日志
  • auth: Cosmos SDK 的auth模块
  • dbm: Tendermint 的数据库代码
  • baseapp: 如下

这里有几个包是tendermint包。Tendermint 通过名为 ABCI 的接口将交易从网络传递给应用程序。如果你要查看正在构建的区块链节点的架构,如下所示:

+---------------------+
|                     |
|     Application     |
|                     |
+--------+---+--------+
         ^   |
         |   | ABCI
         |   v
+--------+---+--------+
|                     |
|                     |
|     Tendermint      |
|                     |
|                     |
+---------------------+

幸运的是,你不必实现ABCI接口。Cosmos SDK以baseapp的形式提供了它的实现样板。

baseapp做了以下几点:

  • 解码从 Tendermint 共识引擎接收到的交易。
  • 从交易中提取 messages 并做基本的合理性校验。
  • 将这些 message 路由到合适的模块使其被正确处理。注意baseapp并不了解你想要使用的具体模块。你要做的就是在app.go中声明这些模块,在接下来的教程中将会看到这些工作。baseapp仅实现了适用于任意模块的核心路由逻辑。
  • 如果 ABCI 消息是DeliverTxCheckTx)的话就Commit。
  • 帮助设置BeginBlockEndBlock,这两种消息让你能定义在每个区块开始和结束时执行的逻辑。实际上,每个模块实现了各自的BeginBlockEndBlock子逻辑,app的职责是它们都聚合起来。(注意:你不会在你的应用程序中使用这些消息)
  • 帮助初始化你的 state。
  • 帮助设置 queries。

现在你需要为应用程序创建一个新的自定义类型nameServiceApp。这个类型将嵌入baseapp(在Go中的嵌入类似于其他语言中的继承),这意味着它可以访问baseapp的所有方法。

const (
    appName = "nameservice")type nameServiceApp struct {
    *bam.BaseApp}

为你的应用添加一个简单的构造函数:

func NewNameServiceApp(logger log.Logger, db dbm.DB) *nameServiceApp {

    // First define the top level codec that will be shared by the different modules. Note: Codec will be explained later
    cdc := MakeCodec()

    // BaseApp handles interactions with Tendermint through the ABCI protocol
    bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc))

    var app = &nameServiceApp{
        BaseApp: bApp,
        cdc:     cdc,
    }

    return app}

很好现在你有了应用程序的骨架;但是,仍然缺少具体功能。

baseapp不了解你要在应用程序中使用的路由或用户交互。应用程序的主要作用是定义这些路由。另一个作用是定义初始状态。这两件事都要求你向应用程序添加模块。

正如你在应用程序设计章节中看到的,你的nameservice需要三个模块:authbanknameservice。前两个已经存在了,但最后一个还没有nameservice模块将定义你的状态机的大部分内容。下一步是构建它。

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

用cosmos构建自己的区块链——定义你的数据结构

我们要做的第一件事是定义一个结构,包含域名所有元数据。依据 ICANN DNS 术语,我们之后将此结构称为 Whois。

types.go

首先创建文件 ./x/nameservice/internal/types/types.go 在其内定义模块自有类型,在 Cosmos SDK 应用中,习惯上将模块相关的代码放在 ./x/ 文件夹中。

Whois

每个域名有三个预期相关的数据:

  • Value – 域名解析出的值。这是任意字符串,但将来您可以修改它以要求它适合特定格式,例如IP地址,DNS区域文件或区块链地址。
  • Owner – 该域名当前所有者的地址。
  • Price – 你需要为购买域名支付的费用。

要开始你的 SDK 模块,在 ./x/nameservice/internal/types/types.go 文件中定义 nameservice.Whois 结构。

package nameservice

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

// Whois is a struct that contains all the metadata of a name
type Whois struct {
    Value string         `json:"value"`
    Owner sdk.AccAddress `json:"owner"`
    价格sdk.Coins      `json:"price"`
}

在设计文档中提到过,如果名称尚未有所有者,我们希望使用 MinPrice 对其进行初始化。

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

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

现在我们继续去编写 Keeper 模块的代码。

用cosmos构建自己的区块链——编写Keeper

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

首先创建文件./x/nameservice/internal/keeper/keeper.go来保存模块的keeper。在 Cosmos SDK 应用程序中,模块通常放在./x/文件夹中。

Keeper结构

开始制作你的SDK模块,请在./x/nameservice/internal/keeper/keeper.go文件中定义nameservice.Keeper

package nameservice

import (
    "github.com/cosmos/cosmos-sdk/codec"
    "github.com/cosmos/cosmos-sdk/x/bank"

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

// Keeper maintains the link to data storage and exposes getter/setter methods for the various parts of the state machine
type Keeper struct {
    coinKeeper bank.Keeper

    storeKey  sdk.StoreKey // Unexposed key to access store from sdk.Context

    cdc *codec.Codec // The wire codec for binary encoding/decoding.
}

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

  • 3个不同的cosmos-sdk包被引入:
    • codec – 提供负责Cosmos编码格式的工具——Amino。
    • bank – bank模块控制账户和转账。
    • types – 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来与存储交互的方法了。首先,添加一个函数来为指定域名设置解析字符串值:

// Sets the entire Whois metadata struct for a name
func (k Keeper) SetWhois(ctx sdk.Context, name string, whois Whois) {
    if whois.Owner.Empty() {
        return
    }
    store := ctx.KVStore(k.storeKey)
    store.Set([]byte(name), k.cdc.MustMarshalBinaryBare(whois))
}

在此方法中,首先使用Keeper中的namesStoreKey获取map[name]value的存储对象。

注意:这个函数使用sdk.Context。该对象持有访问像blockHeightchainID这样重要部分状态的函数。

接下来,你可以使用方法.Set([]byte,[]byte)向存储中插入键值对。由于存储只接受[]byte,想要把string转化成[]byte再把它们作为参数传给Set方法。

接下来,添加一个函数来解析域名(即查找域名对应的解析值):

// Gets the entire Whois metadata struct for a name
func (k Keeper) GetWhois(ctx sdk.Context, name string) Whois {
    store := ctx.KVStore(k.storeKey)
    if !store.Has([]byte(name)) {
        return NewWhois()
    }
    bz := store.Get([]byte(name))
    var whois Whois
    k.cdc.MustUnmarshalBinaryBare(bz, &whois)
    return whois
}

这里,与SetName方法一样,首先使用StoreKey访问存储。接下来,不使用使用.Get([] byte) []byte方法而不是Set方法。向函数传参,传递key值,要把name字符串转化成[]byte,并以[]byte的形式返回结果。将此转换成字符串再返回。

如果一个域名尚未在存储中,它返回一个新的 Whois 信息,包含最低价格 MinPrice。

现在,我们添加了根据名称从 store 获取特定参数的功能。我们重用了 GetWhois 和 SetWhois 函数,而不是重写 store 的 getter 和 setter。例如,要设置字段,首先我们获取整个 Whois 数据,更新我们的特定字段,然后将新版本放回 store。

// ResolveName - returns the string that the name resolves to
func (k Keeper) ResolveName(ctx sdk.Context, name string) string {
    return k.GetWhois(ctx, name).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 {
    return !k.GetWhois(ctx, name).Owner.Empty()
}

// GetOwner - get the current owner of a name
func (k Keeper) GetOwner(ctx sdk.Context, name string) sdk.AccAddress {
    return k.GetWhois(ctx, name).Owner
}

// 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.  If price doesn't exist yet, set to 1nametoken.
func (k Keeper) GetPrice(ctx sdk.Context, name string) sdk.Coins {
    return k.GetWhois(ctx, name).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)
}

SDK 还有一个特性叫 sdk.Iterator,可以返回一个迭代器用于遍历指定 store 中的所有 对。

我们增加一个函数用于获取遍历 store 中所有已知域名的迭代器。

// 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{})
}

最后需要在./x/nameservice/keeper.go文件中加上Keeper的构造函数:

// NewKeeper creates new instances of the nameservice Keeper
func NewKeeper(coinKeeper bank.Keeper, storeKey sdk.StoreKey, cdc *codec.Codec) Keeper {
    return Keeper{
        coinKeeper: coinKeeper,
        storeKey:   storeKey,
        cdc:        cdc,
    }
}

下一节我们会描述如何让用户通过 Msgs and Handlers 与刚刚建立的 store 交互。

用cosmos构建自己的区块链——编写消息和消息处理器

前面我们已经设置了Keeper,是时候构建允许用户购买域名和设置解析值的消息Msg和处理消息的Handler了。

Msg

Msg触发状态转变。Msgs被点评在客户端提交至网络的Txs中。Cosmos SDK从Txs中打包和解包来自Msgs,这就意味着,作为一个应用开发者,你只需要去定义MsgsMsgs必须要满足下面的接口(我们会在下一小节实现):

// Transactions messages must fulfill the Msg
type Msg interface {
    // Return the message type.
    // Must be alphanumeric or empty.
    Type() string

    // Returns a human-readable string for the message, intended for utilization
    // within tags
    Route() string

    // ValidateBasic does a simple validation check that
    // doesn't require access to any other information.
    ValidateBasic() Error

    // Get the canonical byte representation of the Msg.
    GetSignBytes() []byte

    // Signers returns the addrs of signers that must sign.
    // CONTRACT: All signatures must be present to be valid.
    // CONTRACT: Returns addrs in some deterministic order.
    GetSigners() []AccAddress
}

Handler

Handler定义了在接收到一个特定Msg时,需要采取的操作(哪些存储需要更新,怎样更新及要满足什么条件)。

在此模块中,你有两种类型的Msg,用户可以发送这些Msg来和应用程序状态进行交互:SetNameBuyName。它们各自同其Handler关联。

现在你已经更好地理解了 Msgs 和 Handler,下一节我们开始构建第一条消息:SetName

用cosmos构建自己的区块链——SetName

Msg

SDK中Msg的命令约束是 Msg{.Action}。要实现的第一个操作是SetName,因此命名为MsgSetName。此Msg允许域名的所有者设置该域名的解析返回值。首先在名为./x/nameservice/msgs.go的新文件中定义MsgSetName

package nameservice

import (
    "encoding/json"

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

// MsgSetName defines a SetName message
type MsgSetName struct {
    Name string
    Value  string
    Owner  sdk.AccAddress
}

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

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

  • name – 所要设置的域名
  • value – 要设置的域名解析值
  • owner – 域名的所有者

接下来,实现Msg接口:

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

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

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

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

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

// GetSignBytes encodes the message for signing
func (msg MsgSetName) GetSignBytes() []byte {
    b, err := json.Marshal(msg)
    if err != nil {
        panic(err)
    }
    return sdk.MustSortJSON(b)
}

GetSignBytes定义了如何编码Msg以进行签名(这里并没有进行签名,只是先编码成排序好的json)。在大多数情形下,要编码成排好序的JSON。不应修改输出。

// GetSigners defines whose signature is required
func (msg MsgSetName) GetSigners() []sdk.AccAddress {
    return []sdk.AccAddress{msg.Owner}
}

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

Handler

现在MsgSetName已经定义好了,下一部来定义收到此Msg时需要采取的操作。也就是handler所要做的。

在一个新文件(./x/nameservice/handler.go)先写入如下代码:

package nameservice

import (
    "fmt"

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

// NewHandler returns a handler for "nameservice" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
    return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
        switch msg := msg.(type) {
        case MsgSetName:
            return handleMsgSetName(ctx, keeper, msg)
        default:
            errMsg := fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type())
            return sdk.ErrUnknownRequest(errMsg).Result()
        }
    }
}

NewHandler本质上是一个子路由,它将进入该模块的msg路由到正确的handler做处理。目前,只有一个 Msg/Handler

现在,你需要定义在handlerMsgSetName中定义处理MsgSetName消息的实际逻辑:

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

// Handle a message to set name
func handleMsgSetName(ctx sdk.Context, keeper Keeper, msg MsgSetName) sdk.Result {
    if !msg.Owner.Equals(keeper.GetOwner(ctx, msg.Name)) { // Checks if the the msg sender is the same as the current owner
        return sdk.ErrUnauthorized("Incorrect Owner").Result() // If not, throw an error
    }
    keeper.SetName(ctx, msg.Name, msg.Value) // If so, set the name to the value specified in the msg.
    return sdk.Result{}                      // return
}

在此函数中,要检查Msg的发送者是否就是域名的所有者(keeper.GetOwner)。如果是,就能通过调用Keeper里的函数来设置域名。如果不是,则抛出错误并返回给用户。

很好,现在所有者可以 SetName,但在域名还没有出售时,你的模块需要有一个途径让用户购买域名。下一节我们来一起定义 BuyName 消息。

用cosmos构建自己的区块链——BuyName

Msg

现在来定义购买域名的Msg,并加在./x/nameservice/msgs.go文件中。代码同SetName非常相似:

// MsgBuyName defines the BuyName message
type MsgBuyName struct {
    Name string
    Bid    sdk.Coins
    Buyer  sdk.AccAddress
}

// 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 "nameservice" }

// Type should return the action
func (msg MsgBuyName) Type() string { return "buy_name" }

// ValidateBasic runs stateless checks on the message
func (msg MsgBuyName) ValidateBasic() sdk.Error {
    if msg.Buyer.Empty() {
        return sdk.ErrInvalidAddress(msg.Buyer.String())
    }
    if len(msg.Name) == 0 {
        return sdk.ErrUnknownRequest("Name cannot be empty")
    }
    if !msg.Bid.IsAllPositive() {
        return sdk.ErrInsufficientCoins("Bids must be positive")
    }
    return nil
}

// GetSignBytes encodes the message for signing
func (msg MsgBuyName) GetSignBytes() []byte {
    b, err := json.Marshal(msg)
    if err != nil {
        panic(err)
    }
    return sdk.MustSortJSON(b)
}

// GetSigners defines whose signature is required
func (msg MsgBuyName) GetSigners() []sdk.AccAddress {
    return []sdk.AccAddress{msg.Buyer}
}

接着,在./x/nameservice/handler.go文件中,把MsgBuyName加入到模块路由器中:

// NewHandler returns a handler for "nameservice" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
    return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
        switch msg := msg.(type) {
        case MsgSetName:
            return handleMsgSetName(ctx, keeper, msg)
        case MsgBuyName:
            return handleMsgBuyName(ctx, keeper, msg)
        default:
            errMsg := fmt.Sprintf("Unrecognized nameservice Msg type: %v", msg.Type())
            return sdk.ErrUnknownRequest(errMsg).Result()
        }
    }
}

最后,定义BuyNamehandler,该函数执行由msg触发的状态转换。请记住,此时msg已运行其ValidateBasic函数,因此已进行了一些输入验证。但是,ValidateBasic无法查询应用程序状态。应在handler中执行依赖于网络状态(例如帐户余额)的验证逻辑。

// Handle a message to buy name
func handleMsgBuyName(ctx sdk.Context, keeper Keeper, msg MsgBuyName) sdk.Result {
    if keeper.GetPrice(ctx, msg.Name).IsAllGT(msg.Bid) { // Checks if the the bid price is greater than the price paid by the current owner
        return sdk.ErrInsufficientCoins("Bid not high enough").Result() // If not, throw an error
    }
    if keeper.HasOwner(ctx, msg.Name) {
        _, err := keeper.coinKeeper.SendCoins(ctx, msg.Buyer, keeper.GetOwner(ctx, msg.Name), msg.Bid)
        if err != nil {
            return sdk.ErrInsufficientCoins("Buyer does not have enough coins").Result()
        }
    } else {
        _, _, err := keeper.coinKeeper.SubtractCoins(ctx, msg.Buyer, msg.Bid) // If so, deduct the Bid amount from the sender
        if err != nil {
            return sdk.ErrInsufficientCoins("Buyer does not have enough coins").Result()
        }
    }
    keeper.SetOwner(ctx, msg.Name, msg.Buyer)
    keeper.SetPrice(ctx, msg.Name, msg.Bid)
    return sdk.Result{}
}

首先确保出价高于当前价格。然后,检查域名是否已有所有者。如果有,之前的所有者将会收到Buyer的钱。

如果没有所有者,你的nameservice模块会把Buyer的资金“销毁”(即发送到不可恢复的地址)。

如果SubtractCoinsSendCoins返回一个非空错误,handler会抛出一个错误,回退状态转变。没有的话,使用之前在Keeper上定义的 getter 和 setter,handler 将买方设置为新所有者,并将新价格设置为当前出价。

注意:此handler使用coinKeeper中的函数执行货币相关操作。如果你的应用程序正在执行货币相关操作,你可能需要查看此模块的文档,以查看它提供的功能。

现在已经有了 Msgs and Handlers 定义,下一节我们来学习如何使交易中的数据能被查询到

用cosmos构建自己的区块链——查询域名

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

  • resolve : 传入一个域名返回nameservice给定的解析值。类似于DNS查询。
  • whois : 传入一个域名返回价格解析值和域名的所有者。用于确定你想要购买名称的成本。

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

package nameservice

import (
    "fmt"
    "strings"

    "github.com/cosmos/cosmos-sdk/codec"

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

// query endpoints supported by the nameservice Querier
const (
    QueryResolve = "resolve"
    QueryWhois   = "whois"
    QueryNames   = "names"
)

// NewQuerier is the module level router for state queries
func NewQuerier(keeper Keeper) sdk.Querier {
    return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err sdk.Error) {
        switch path[0] {
        case QueryResolve:
            return queryResolve(ctx, path[1:], req, keeper)
        case QueryWhois:
            return queryWhois(ctx, path[1:], req, keeper)
        case QueryNames:
            return queryNames(ctx, req, keeper)
        default:
            return nil, sdk.ErrUnknownRequest("unknown nameservice query endpoint")
        }
    }
}

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

// nolint: unparam
func queryResolve(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
    name := path[0]

    value := keeper.ResolveName(ctx, name)

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

    bz, err2 := codec.MarshalJSONIndent(keeper.cdc, QueryResResolve{value})
    if err2 != nil {
        panic("could not marshal result to JSON")
    }

    return bz, nil
}

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

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

// nolint: unparam
func queryWhois(ctx sdk.Context, path []string, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
    name := path[0]

    whois := keeper.GetWhois(ctx, name)

    bz, err2 := codec.MarshalJSONIndent(keeper.cdc, whois)
    if err2 != nil {
        panic("could not marshal result to JSON")
    }

    return bz, nil
}

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

func queryNames(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) (res []byte, err sdk.Error) {
    var namesList QueryResNames

    iterator := keeper.GetNamesIterator(ctx)

    for ; iterator.Valid(); iterator.Next() {
        name := string(iterator.Key())
        namesList = append(namesList, name)
    }

    bz, err2 := codec.MarshalJSONIndent(keeper.cdc, namesList)
    if err2 != nil {
        panic("could not marshal result to JSON")
    }

    return bz, nil
}

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

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

上述代码中要注意:

  • 在这里你的Keeper的 getter 和 setter 方法被大量使用。在构建使用此模块的任何其他应用程序时,你可能需要返回并定义更多getter/setter以访问所需的状态。
  • 按照惯例,每个输出类型都应该是 JSON marshallable 和 stringable(实现 Golang fmt.Stringer 接口)。返回的字节应该是输出结果的JSON编码。
    • 因此,对于输出类型的解析,我们将解析字符串盘点在一个名为 QueryResResolve 的结构中,该结构既是JSON marshallable 的又有.String()方法。
    • 对于 Whois 的输出,正常的 Whois 结构已经是 JSON marshallable 的,但我们需要在其上添加.String()方法。
    • 名称查询的输出也一样,[]字符串本身已经可 marshallable ,但我们需要在其上添加.String()方法。

用cosmos构建自己的区块链——注册接口,编写交互命令

经过前面的学习,基本完成了我们域名链所有功能的编写了,现在对我们前面编写的接口进行注册,然后开始编写和程序交互的命令,这样我们在启动程序后就可以通过命令来对区块链进行各种操作了。

在Amino中注册你的数据类型使得它们能够被编码/解码,有一些代码需要放在./x/nameservice/codec.go中。你创建的任何接口和实现接口的任何结构都需要在RegisterCodec函数中声明。在此模块中,需要注册两个Msg的实现(SetNameBuyName),但你的Whois查询返回的类型不需要:

package nameservice

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

// RegisterCodec registers concrete types on wire codec
func RegisterCodec(cdc *codec.Codec) {
    cdc.RegisterConcrete(MsgSetName{}, "nameservice/SetName", nil)
    cdc.RegisterConcrete(MsgBuyName{}, "nameservice/BuyName", nil)
}

接下来需要为你的模块定义 CLI 交互了。

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

  • ./x/nameservice/client/cli/query.go
  • ./x/nameservice/client/cli/tx.go
  • ./x/nameservice/client/module_client.go

## Queries

query.go文件中为你模块的每个Queryresolvewhois)定义cobra.Command:

package cli

import (
    "fmt"

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

// 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/resolve/%s", queryRoute, name), nil)
            if err != nil {
                fmt.Printf("could not resolve name - %s \n", string(name))
                return nil
            }

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

// GetCmdWhois queries information about a domain
func GetCmdWhois(queryRoute string, cdc *codec.Codec) *cobra.Command {
    return &cobra.Command{
        Use:   "whois [name]",
        Short: "Query whois info of name",
        Args:  cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            cliCtx := context.NewCLIContext().WithCodec(cdc)
            name := args[0]

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

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

// GetCmdNames queries a list of all names
func GetCmdNames(queryRoute string, cdc *codec.Codec) *cobra.Command {
    return &cobra.Command{
        Use:   "names",
        Short: "names",
        // Args:  cobra.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            cliCtx := context.NewCLIContext().WithCodec(cdc)

            res, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/names", queryRoute), nil)
            if err != nil {
                fmt.Printf("could not get query names\n")
                return nil
            }

            var out nameservice.QueryResNames
            cdc.MustUnmarshalJSON(res, &out)
            return cliCtx.PrintOutput(out)
        },
    }
}

注意上述代码中:

  • CLI 引入了一个新的context:CLIContext。它包含有关CLI交互所需的用户输入和应用程序配置的数据。
  • cliCtx.QueryWithData()函数所需的path直接从你的查询路径中映射。
    • 路径的第一部分用于区分 SDK 应用程序可能的querier类型:custom用于Querier
    • 第二部分(nameservice)是将查询路由到的模块的名称。
    • 最后是要调用模块中的特定的querier。
    • 在这个例子中,第四部分是查询。这是因为查询参数是一个简单的字符串。要启用更复杂的查询输入,你需要使用.QueryWithData()函数的第二个参数来传入data。有关此示例,请参阅 Staking 模块中的 queriers。

Transaction

现在已经定义了查询交互,是时候继续在tx.go中的交易生成了:

你的应用程序需要导入你刚编写的代码。这里导入路径设置为此存储库(github.com/cosmos/sdk-application-tutorial/x/nameservice)。如果您是在自己的仓库中进行的前面的操作,则需要更改导入路径(github.com/{.Username}/{.Project.Repo}/x/nameservice)。

package cli

import (
    "github.com/spf13/cobra"

    "github.com/cosmos/cosmos-sdk/client/context"
    "github.com/cosmos/cosmos-sdk/client/utils"
    "github.com/cosmos/cosmos-sdk/codec"
    "github.com/cosmos/sdk-application-tutorial/x/nameservice"

    sdk "github.com/cosmos/cosmos-sdk/types"
    authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder"
)

// GetCmdBuyName is the CLI command for sending a BuyName transaction
func GetCmdBuyName(cdc *codec.Codec) *cobra.Command {
    return &cobra.Command{
        Use:   "buy-name [name] [amount]",
        Short: "bid for existing name or claim new name",
        Args:  cobra.ExactArgs(2),
        RunE: func(cmd *cobra.Command, args []string) error {
            cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc)

            txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))

            if err := cliCtx.EnsureAccountExists(); err != nil {
                return err
            }

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

            msg := nameservice.NewMsgBuyName(args[0], coins, cliCtx.GetFromAddress())
            err = msg.ValidateBasic()
            if err != nil {
                return err
            }

            cliCtx.PrintResponse = true

            return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}, false)
        },
    }
}

// GetCmdSetName is the CLI command for sending a SetName transaction
func GetCmdSetName(cdc *codec.Codec) *cobra.Command {
    return &cobra.Command{
        Use:   "set-name [name] [value]",
        Short: "set the value associated with a name that you own",
        Args:  cobra.ExactArgs(2),
        RunE: func(cmd *cobra.Command, args []string) error {
            cliCtx := context.NewCLIContext().WithCodec(cdc).WithAccountDecoder(cdc)

            txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))

            if err := cliCtx.EnsureAccountExists(); err != nil {
                return err
            }

            msg := nameservice.NewMsgSetName(args[0], args[1], cliCtx.GetFromAddress())
            err := msg.ValidateBasic()
            if err != nil {
                return err
            }

            cliCtx.PrintResponse = true

            return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}, false)
        },
    }
}

注意在上述代码中:

  • 使用了authcmd包。查看文档了解更多使用信息。它提供对CLI控制的帐户的访问权限,并便于签名。

Module Client

导出此功能的最后一部分称为ModuleClient,在./x/nameservice/client/module_client.go文件中实现。Module Client 为模块提供了导出客户端功能的标准方法。

注意:你的应用程序需要导入你刚编写的代码。这里导入路径设置为此仓库(github.com/cosmos/sdk-application-tutorial/x/nameservice)。如果你是在自己项目中编写的,则需要更改导入路径成(github.com/{.Username}/ {.Project.Repo}/x/nameservice)。

package client

import (
    "github.com/cosmos/cosmos-sdk/client"
    nameservicecmd "github.com/cosmos/sdk-application-tutorial/x/nameservice/client/cli"
    "github.com/spf13/cobra"
    amino "github.com/tendermint/go-amino"
)

// ModuleClient exports all client functionality from this module
type ModuleClient struct {
    storeKey string
    cdc      *amino.Codec
}

func NewModuleClient(storeKey string, cdc *amino.Codec) ModuleClient {
    return ModuleClient{storeKey, cdc}
}

// GetQueryCmd returns the cli query commands for this module
func (mc ModuleClient) GetQueryCmd() *cobra.Command {
    // Group nameservice queries under a subcommand
    namesvcQueryCmd := &cobra.Command{
        Use:   "nameservice",
        Short: "Querying commands for the nameservice module",
    }

    namesvcQueryCmd.AddCommand(client.GetCommands(
        nameservicecmd.GetCmdResolveName(mc.storeKey, mc.cdc),
        nameservicecmd.GetCmdWhois(mc.storeKey, mc.cdc),
    )...)

    return namesvcQueryCmd
}

// GetTxCmd returns the transaction commands for this module
func (mc ModuleClient) GetTxCmd() *cobra.Command {
    namesvcTxCmd := &cobra.Command{
        Use:   "nameservice",
        Short: "Nameservice transactions subcommands",
    }

    namesvcTxCmd.AddCommand(client.PostCommands(
        nameservicecmd.GetCmdBuyName(mc.cdc),
        nameservicecmd.GetCmdSetName(mc.cdc),
    )...)

    return namesvcTxCmd
}

上述代码要注意:

  • 此抽象允许客户端以标准方式从模块导入客户端功能。当我们构建入口时,你将看到这一点。
  • 有一个未解决的问题是将其余功能(在本教程的下一部分中描述)添加到此接口。

用cosmos构建自己的区块链——引入你的模块并完成程序

经过前面的编写我们的模块已就绪,它可以和其它两个模块authbank被合并到./app.go文件中:

你的应用程序需要导入你刚编写的代码。这里导入路径设置为此存储库(github.com/cosmos/sdk-application-tutorial/x/nameservice)。如果您是在自己的仓库中进行的前面的操作,则需要更改导入路径(github.com/{.Username}/{.Project.Repo}/x/nameservice)。

package app

import (
    "encoding/json"

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

    "github.com/cosmos/cosmos-sdk/codec"
    "github.com/cosmos/cosmos-sdk/x/auth"
    "github.com/cosmos/cosmos-sdk/x/bank"
    "github.com/cosmos/cosmos-sdk/x/params"
    "github.com/cosmos/cosmos-sdk/x/staking"
    "github.com/cosmos/sdk-application-tutorial/x/nameservice"

    bam "github.com/cosmos/cosmos-sdk/baseapp"
    sdk "github.com/cosmos/cosmos-sdk/types"
    abci "github.com/tendermint/tendermint/abci/types"
    cmn "github.com/tendermint/tendermint/libs/common"
    dbm "github.com/tendermint/tm-db"
    tmtypes "github.com/tendermint/tendermint/types"
)

接下来,你需要在nameServiceApp结构体中添加存储的key和Keepers,并更新构造函数:

const (
    appName = "nameservice"
)

type nameServiceApp struct {
    *bam.BaseApp
    cdc *codec.Codec

    keyMain          *sdk.KVStoreKey
    keyAccount       *sdk.KVStoreKey
    keyNS            *sdk.KVStoreKey
    keyFeeCollection *sdk.KVStoreKey
    keyParams        *sdk.KVStoreKey
    tkeyParams       *sdk.TransientStoreKey

    accountKeeper       auth.AccountKeeper
    bankKeeper          bank.Keeper
    feeCollectionKeeper auth.FeeCollectionKeeper
    paramsKeeper        params.Keeper
    nsKeeper            nameservice.Keeper
}

// NewNameServiceApp is a constructor function for nameServiceApp
func NewNameServiceApp(logger log.Logger, db dbm.DB) *nameServiceApp {

    // First define the top level codec that will be shared by the different modules
    cdc := MakeCodec()

    // BaseApp handles interactions with Tendermint through the ABCI protocol
    bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc))

    // Here you initialize your application with the store keys it requires
    var app = &nameServiceApp{
        BaseApp: bApp,
        cdc:     cdc,

        keyMain:          sdk.NewKVStoreKey("main"),
        keyAccount:       sdk.NewKVStoreKey("acc"),
        keyNS:            sdk.NewKVStoreKey("ns"),
        keyFeeCollection: sdk.NewKVStoreKey("fee_collection"),
        keyParams:        sdk.NewKVStoreKey("params"),
        tkeyParams:       sdk.NewTransientStoreKey("transient_params"),
    }

    return app
}

此时,构造函数仍然缺少重要的逻辑。它需要:

  • 从每个所需模块中实例化所需的Keeper
  • 生成每个Keeper所需的storeKey
  • 注册每个模块的handlerbaseapp路由器的 AddRoute() 方法用来做这个。
  • 注册每个模块的querierbaseappqueryRouter中的AddRoute()方法用来做这个。
  • KVStores挂载到baseApp的multistore提供的key值。
  • 设置initChainer来定义初始应用程序状态。

你最终的构造函数应该如下所示:

// NewNameServiceApp is a constructor function for nameServiceApp
func NewNameServiceApp(logger log.Logger, db dbm.DB) *nameServiceApp {

    // First define the top level codec that will be shared by the different modules
    cdc := MakeCodec()

    // BaseApp handles interactions with Tendermint through the ABCI protocol
    bApp := bam.NewBaseApp(appName, logger, db, auth.DefaultTxDecoder(cdc))

    // Here you initialize your application with the store keys it requires
    var app = &nameServiceApp{
        BaseApp: bApp,
        cdc:     cdc,

        keyMain:          sdk.NewKVStoreKey("main"),
        keyAccount:       sdk.NewKVStoreKey("acc"),
        keyNS:            sdk.NewKVStoreKey("ns"),
        keyFeeCollection: sdk.NewKVStoreKey("fee_collection"),
        keyParams:        sdk.NewKVStoreKey("params"),
        tkeyParams:       sdk.NewTransientStoreKey("transient_params"),
    }

    // The ParamsKeeper handles parameter storage for the application
    app.paramsKeeper = params.NewKeeper(app.cdc, app.keyParams, app.tkeyParams)

    // The AccountKeeper handles address -> account lookups
    app.accountKeeper = auth.NewAccountKeeper(
        app.cdc,
        app.keyAccount,
        app.paramsKeeper.Subspace(auth.DefaultParamspace),
        auth.ProtoBaseAccount,
    )

    // The BankKeeper allows you perform sdk.Coins interactions
    app.bankKeeper = bank.NewBaseKeeper(
        app.accountKeeper,
        app.paramsKeeper.Subspace(bank.DefaultParamspace),
        bank.DefaultCodespace,
    )

    // The FeeCollectionKeeper collects transaction fees and renders them to the fee distribution module
    app.feeCollectionKeeper = auth.NewFeeCollectionKeeper(cdc, app.keyFeeCollection)

    // The NameserviceKeeper is the Keeper from the module for this tutorial
    // It handles interactions with the namestore
    app.nsKeeper = nameservice.NewKeeper(
        app.bankKeeper,
        app.keyNS,
        app.cdc,
    )

    // The AnteHandler handles signature verification and transaction pre-processing
    app.SetAnteHandler(auth.NewAnteHandler(app.accountKeeper, app.feeCollectionKeeper))

    // The app.Router is the main transaction router where each module registers its routes
    // Register the bank and nameservice routes here
    app.Router().
        AddRoute("bank", bank.NewHandler(app.bankKeeper)).
        AddRoute("nameservice", nameservice.NewHandler(app.nsKeeper))

    // The app.QueryRouter is the main query router where each module registers its routes
    app.QueryRouter().
        AddRoute("nameservice", nameservice.NewQuerier(app.nsKeeper)).
        AddRoute("acc", auth.NewQuerier(app.accountKeeper))

    // The initChainer handles translating the genesis.json file into initial state for the network
    app.SetInitChainer(app.initChainer)

    app.MountStores(
        app.keyMain,
        app.keyAccount,
        app.keyNS,
        app.keyFeeCollection,
        app.keyParams,
        app.tkeyParams,
    )

    err := app.LoadLatestVersion(app.keyMain)
    if err != nil {
        cmn.Exit(err.Error())
    }

    return app
}

注意:上面提到的 TransientStore 是 KVStore 的内存实现,用于未持久化的状态。

initChainer定义了genesis.json中的帐户如何在初始化区块链时被映射到应用程序状态。ExportAppStateAndValidators函数可帮助引导初始化应用程序的状态。你现在不需要太关心它们。

构造函数注册了initChainer函数,但尚未定义。继续创建它:

// GenesisState represents chain state at the start of the chain. Any initial state (account balances) are stored here.
type GenesisState struct {
    AuthData auth.GenesisState   `json:"auth"`
    BankData bank.GenesisState   `json:"bank"`
    Accounts []*auth.BaseAccount `json:"accounts"`
}

func (app *nameServiceApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
    stateJSON := req.AppStateBytes

    genesisState := new(GenesisState)
    err := app.cdc.UnmarshalJSON(stateJSON, genesisState)
    if err != nil {
        panic(err)
    }

    for _, acc := range genesisState.Accounts {
        acc.AccountNumber = app.accountKeeper.GetNextAccountNumber(ctx)
        app.accountKeeper.SetAccount(ctx, acc)
    }

    auth.InitGenesis(ctx, app.accountKeeper, app.feeCollectionKeeper, genesisState.AuthData)
    bank.InitGenesis(ctx, app.bankKeeper, genesisState.BankData)

    return abci.ResponseInitChain{}
}

// ExportAppStateAndValidators does the things
func (app *nameServiceApp) ExportAppStateAndValidators() (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) {
    ctx := app.NewContext(true, abci.Header{})
    accounts := []*auth.BaseAccount{}

    appendAccountsFn := func(acc auth.Account) bool {
        account := &auth.BaseAccount{
            Address: acc.GetAddress(),
            Coins:   acc.GetCoins(),
        }

        accounts = append(accounts, account)
        return false
    }

    app.accountKeeper.IterateAccounts(ctx, appendAccountsFn)

    genState := GenesisState{
        Accounts: accounts,
        AuthData: auth.DefaultGenesisState(),
        BankData: bank.DefaultGenesisState(),
    }

    appState, err = codec.MarshalJSONIndent(app.cdc, genState)
    if err != nil {
        return nil, nil, err
    }

    return appState, validators, err
}

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

// MakeCodec generates the necessary codecs for Amino
func MakeCodec() *codec.Codec {
    var cdc = codec.New()
    auth.RegisterCodec(cdc)
    bank.RegisterCodec(cdc)
    nameservice.RegisterCodec(cdc)
    staking.RegisterCodec(cdc)
    sdk.RegisterCodec(cdc)
    codec.RegisterCrypto(cdc)
    return cdc
}

现在您已经创建了一个包含模块的应用程序,下一节我们来构建入口点

原文 Wechat
提示:投资有风险,入市需谨慎,本资讯不作为投资理财建议。请理性投资,切实提高风险防范意识;如有发现的违法犯罪线索,可积极向有关部门举报反映。
你可能还喜欢