Cosmos系列-2. Cosmos SDK

1. 前言

上次讲到 Tendermint 将网络层和共识层设计好并封装起来,提供给区块链开发者使用,在这种情况下开发一个链时只需要考虑应用层。 不过应用层中有很多功能还是可以通用的,比如账户管理/Token转账等功能。Cosmos把很多通用功能分解出来,以模块化的形式加以实现,例如:账户管理,社区治理,Staking等等,这就组成了一套新的区块链开发框架Cosmos-SDK。

Cosmos系列-2. Cosmos SDK_第1张图片

开发者使用Cosmos-SDK进行应用开发时,只需要实现自身业务中特殊的功能,其他功能可以直接调用Cosmos-SDK的功能模块。

下面的内容包括三部分:

  • Cosmos SDK 的简单描述
  • 社区治理模块的逻辑
  • 一个Module的主要构成

2. Cosmos SDK

下面是为了易于理解Cosmos的框架抽取出来的几个目录(代码全部基于 Cosmos-SDK v0.33.2版本)

cosmos-sdk
├── baseapp         // Tendermint ABCI 接口进一步封装, 开发者基于它创建App
├── cmd             // 应用程序,hub的主程序gaia
├── store           // 实现了多种类型的存储,帮助App
└── x               // 插件, 模块化的通用功能
    ├── auth            // 账户管理和签名
    ├── bank            // Token和Token转账
    ├── gov             // 社区治理
    ├── ibc             // 跨链协议IBC
    ├── staking         // staking + slashing 完成了POS相关的实现,包括:绑定/解绑/通货膨胀/费用等
    └── slashing

2.1 baseapp

baseApp 是Cosmos-SDK 的ABCI接口应用的基础实现,它自带router来路由交易到各个对应的模块,通过模块丰富App的功能。
回顾一下Tendermint提供的ABCI接口:

type Application interface {
	// Info/Query Connection
	Info(RequestInfo) ResponseInfo                // Return application info
	SetOption(RequestSetOption) ResponseSetOption // Set application option
	Query(RequestQuery) ResponseQuery             // Query for state

	// Mempool Connection
	CheckTx(RequestCheckTx) ResponseCheckTx // Validate a tx for the mempool

	// Consensus Connection
	InitChain(RequestInitChain) ResponseInitChain    // Initialize blockchain with validators and other info from TendermintCore
	BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block
	DeliverTx(RequestDeliverTx) ResponseDeliverTx    // Deliver a tx for full processing
	EndBlock(RequestEndBlock) ResponseEndBlock       // Signals the end of a block, returns changes to the validator set
	Commit() ResponseCommit                          // Commit the state and return the application Merkle root hash
}

下面看一下BaseApp的结构体

// Cosmos-SDK/baseapp/baseapp.go
// BaseApp reflects the ABCI application implementation.
type BaseApp struct {
	name        string               // application name from abci.Info
	db          dbm.DB               // common DB backend
	cms         sdk.CommitMultiStore // Main (uncached) state
	router      Router               // handle any kind of message
	queryRouter QueryRouter          // router for redirecting query calls
	txDecoder   sdk.TxDecoder        // unmarshal []byte into sdk.Tx

	// set upon LoadVersion or LoadLatestVersion.
	baseKey *sdk.KVStoreKey // Main KVStore in cms

	anteHandler    sdk.AnteHandler  // ante handler for fee and auth
	initChainer    sdk.InitChainer  // initialize state with validators and state blob
	beginBlocker   sdk.BeginBlocker // logic to run before any txs
	endBlocker     sdk.EndBlocker   // logic to run after all txs, and to determine valset changes
    ...
}

baseApp 在封装了ABCI接口的基础上,预留了几个处理函数由App实现并在适当的时候被调用。

下面是一个基于CosmosSDK开发的App简单的交易处理流程:

    1. 将从Tendermint共识引擎(DeliverTx)收到的交易进行解码
    1. 从交易中提取messages 并做基本的检查
    1. 将每个message路由到各自的模块进行处理
    1. 提交状态变化
// 处理交易的流程
func (app *BaseApp) DeliverTx(txBytes []byte) (res abci.ResponseDeliverTx) {
    var result sdk.Result
	tx, err := app.txDecoder(txBytes)
	
	...
    result = app.runTx(runTxModeDeliver, txBytes, tx)
    ...
}

func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk.Result) {
	...
	ctx := app.getContextForTx(mode, txBytes)
	ms := ctx.MultiStore()
    ...
	runMsgCtx, msCache := app.cacheTxContext(ctx, txBytes)
	result = app.runMsgs(runMsgCtx, msgs, mode)
	...
}

func (app *BaseApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode runTxMode) (result sdk.Result) {
	...
	for msgIdx, msg := range msgs {
		// match message route
		msgRoute := msg.Route()
		handler := app.router.Route(msgRoute)
		if handler == nil {
			return sdk.ErrUnknownRequest("Unrecognized Msg type: " + msgRoute).Result()
		}
		if mode != runTxModeCheck {
			msgResult = handler(ctx, msg)
		}
		...
	}
    ...
}

下面是Router的实现,是一个 path string 和对应的处理函数的map映射.

type Router interface {
	AddRoute(r string, h sdk.Handler) (rtr Router)
	Route(path string) (h sdk.Handler)
}

type router struct {
	routes map[string]sdk.Handler
}

func NewRouter() *router { // nolint: golint
	return &router{
		routes: make(map[string]sdk.Handler),
	}
}

func (rtr *router) AddRoute(path string, h sdk.Handler) Router {
    ...
	rtr.routes[path] = h
	...
}

func (rtr *router) Route(path string) sdk.Handler {
	return rtr.routes[path]
}

2.2 store

Cosmos SDK 提供了一个Multistore 持久化存储。它允许开发者创建任意个KVStores, 用来分别不同模块的state。这些KVStores只能存储[]byte类型的值,因此所有自定义的结构都要在存储之前使用Amino进行编码。

Amino 是Cosmos团队基于Google的Protobuffer开发的新的编码格式.

2.3 module

Cosmos SDK的强大之处就在于它的模块化。SDK应用程序是通过聚合一组可互操作的模块来构建的。每个模块定义各自的state和message/transaction 处理流程,SDK负责将message转发到对应的模块。

                                      +
                                      |
                                      |  Transaction relayed from Tendermint
                                      |  via DeliverTx
                                      |
                                      |
                +---------------------v--------------------------+
                |                 APPLICATION                    |
                |                                                |
                |     Using baseapp's methods: Decode the Tx,    |
                |     extract and route the message(s)           |
                |                                                |
                +---------------------+--------------------------+
                                      |
                                      |
                                      |
                                      +---------------------------+
                                                                  |
                                                                  |
                                                                  |
                                                                  |  Message routed to the correct
                                                                  |  module to be processed
                                                                  |
                                                                  |
+----------------+  +---------------+  +----------------+  +------v----------+
|                |  |               |  |                |  |                 |
|  AUTH MODULE   |  |  BANK MODULE  |  | STAKING MODULE |  |   GOV MODULE    |
|                |  |               |  |                |  |                 |
|                |  |               |  |                |  | Handles message,|
|                |  |               |  |                |  | Updates state   |
|                |  |               |  |                |  |                 |
+----------------+  +---------------+  +----------------+  +------+----------+
                                                                  |
                                                                  |
                                                                  |
                                                                  |
                                       +--------------------------+
                                       |
                                       | Return result to Tendermint
                                       | (0=Ok, 1=Err)
                                       v

3. 治理模块

Cosmos 有一个内建的治理系统,该系统允许抵押通证的持有人参与提案投票。系统现在支持3种提案类型:

  • Text Proposals: 这是最基本的一种提案类型,通常用于获得大家对某个网络治理意见的观点。
  • Parameter Proposals: 这种提案通常用于改变网络参数的设定。
  • Software Upgrade Proposal: 这个提案用于升级Hub的软件。

任何质押通证的持有人都能够提交一个提案。为了让一个提案获准公开投票,提议人必须要缴纳一定量的押金 deposit,且押金值必须大于 minDeposit 参数设定值。 提案的押金不需要提案人一次全部交付。如果早期提案人交付的 deposit 不足,那么提案进入 deposit_period 状态。 此后,任何通证持有人可以通过 depositTx 交易增加对提案的押金。
当deposit 达到 minDeposit,提案进入2周的 voting_period 。 任何质押状态的通证持有人都可以参与对这个提案的投票。投票的选项有Yes, No, NoWithVeto(行使否决权的否定) 和 Abstain(弃权)。投票的权重取决于投票人所质押的通证数量。如果通证持有人不投票,那么委托人将继承其委托的验证人的投票选项。当然,委托人也可以自己投出与所委托验证人不同的票。
当投票期结束后,获得50%(不包括投Abstain 票)以上 Yes 投票权重且少于33.33% 的NoWithVeto(不包括投Abstain 票)提案将被接受。
如果提案被接受,相应的提案内容需要实现。

上面是治理模块的业务逻辑,下面通过代码来讲述一个模块的主要构成:

x
└── gov
     ├── client                     // 接口
     │   ├── cli                    // 命令行客户端接口
     │   │    ├── query.go          // 发起Query获取信息
     │   │    └── tx.go             // 构建Tx发送相应的Msg
     │   ├── rest                   // reset 请求接口
     │   │    └── rest.go           // 包含了Query和Tx所有消息处理
     │   └── module_client.go       // ModuleClient, 提供命令和路由
     ├── codec.go                   // 模块内的编解码
     ├── keeper.go                  // 存储,并与其他模块的keeper交互
     ├── msg.go                     // 模块内用到的Msg,也就是Tx
     ├── handler.go                 // 处理Msg的函数
     └── querier.go                 // 处理Query的函数
3.0 types and params

Cosmos治理系统中的数据结构

type ProposalKind byte
const (
	ProposalTypeNil             ProposalKind = 0x00
	ProposalTypeText            ProposalKind = 0x01
	ProposalTypeParameterChange ProposalKind = 0x02
	ProposalTypeSoftwareUpgrade ProposalKind = 0x03
)

type TextProposal struct {
	ProposalID   uint64       `json:"proposal_id"`   //  ID of the proposal
	Title        string       `json:"title"`         //  Title of the proposal
	Description  string       `json:"description"`   //  Description of the proposal
	ProposalType ProposalKind `json:"proposal_type"` //  Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}

	Status           ProposalStatus `json:"proposal_status"`    //  Status of the Proposal {Pending, Active, Passed, Rejected}
	FinalTallyResult TallyResult    `json:"final_tally_result"` //  Result of Tallys

	SubmitTime     time.Time `json:"submit_time"`      //  Time of the block where TxGovSubmitProposal was included
	DepositEndTime time.Time `json:"deposit_end_time"` // Time that the Proposal would expire if deposit amount isn't met
	TotalDeposit   sdk.Coins `json:"total_deposit"`    //  Current deposit on this proposal. Initial value is set at InitialDeposit

	VotingStartTime time.Time `json:"voting_start_time"` //  Time of the block where MinDeposit was reached. -1 if MinDeposit is not reached
	VotingEndTime   time.Time `json:"voting_end_time"`   // Time that the VotingPeriod for this proposal will end and votes will be tallied
}

type ProposalStatus byte
const (
	StatusNil           ProposalStatus = 0x00
	StatusDepositPeriod ProposalStatus = 0x01
	StatusVotingPeriod  ProposalStatus = 0x02
	StatusPassed        ProposalStatus = 0x03
	StatusRejected      ProposalStatus = 0x04
)

// Vote
type Vote struct {
	Voter      sdk.AccAddress `json:"voter"`       //  address of the voter
	ProposalID uint64         `json:"proposal_id"` //  proposalID of the proposal
	Option     VoteOption     `json:"option"`      //  option from OptionSet chosen by the voter
}

// Deposit
type Deposit struct {
	Depositor  sdk.AccAddress `json:"depositor"`   //  Address of the depositor
	ProposalID uint64         `json:"proposal_id"` //  proposalID of the proposal
	Amount     sdk.Coins      `json:"amount"`      //  Deposit amount
}

type VoteOption byte
const (
	OptionEmpty      VoteOption = 0x00
	OptionYes        VoteOption = 0x01
	OptionAbstain    VoteOption = 0x02
	OptionNo         VoteOption = 0x03
	OptionNoWithVeto VoteOption = 0x04
)

治理系统中的一些参数

type DepositParams struct {
	MinDeposit       sdk.Coins     `json:"min_deposit"`        //  Minimum deposit for a proposal to enter voting period.
	MaxDepositPeriod time.Duration `json:"max_deposit_period"` //  Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months
}
type TallyParams struct {
	Quorum    sdk.Dec `json:"quorum"`    //  Minimum percentage of total stake needed to vote for a result to be considered valid
	Threshold sdk.Dec `json:"threshold"` //  Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5
	Veto      sdk.Dec `json:"veto"`      //  Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3
}
type VotingParams struct {
	VotingPeriod time.Duration `json:"voting_period"` //  Length of the voting period.
}
3.1 keeper

Keeper用来处理模块与存储的交互,包含模块大部分核心功能。
在上面的逻辑中,keeper需要处理 Proposal/Deposit/Vote 的存储/查询/删除.

下面摘取一部分代码进行分析

// Cosmos-SDK/x/gov/keeper.go

type Keeper struct {
	// The reference to the CoinKeeper to modify balances
	ck BankKeeper

	// The (unexposed) keys used to access the stores from the Context.
	storeKey sdk.StoreKey

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

gov的keeper结构体

  • BankKeeper 这是bank模块的keeper引用, 包含它可以调用bank模块的方法.例如在提案的Deposit阶段时处理验证人提交的押金
  • sdk.StoreKey 这是App层KVStore数据库的一个存储Key, 模块内通过它来操作存储的Value
  • *codec.Codec 这是一个codec的指针,提供Amino编解码接口
// Creates a NewProposal
func (keeper Keeper) NewTextProposal(ctx sdk.Context, title string, description string, proposalType ProposalKind) Proposal {
	proposalID, err := keeper.getNewProposalID(ctx)
	if err != nil {
		return nil
	}
	var proposal Proposal = &TextProposal{
		ProposalID:       proposalID,
		Title:            title,
		Description:      description,
		ProposalType:     proposalType,
		Status:           StatusDepositPeriod,
		FinalTallyResult: EmptyTallyResult(),
		TotalDeposit:     sdk.Coins{},
		SubmitTime:       ctx.BlockHeader().Time,
	}

	depositPeriod := keeper.GetDepositParams(ctx).MaxDepositPeriod
	proposal.SetDepositEndTime(proposal.GetSubmitTime().Add(depositPeriod))

	keeper.SetProposal(ctx, proposal)
	keeper.InsertInactiveProposalQueue(ctx, proposal.GetDepositEndTime(), proposalID)
	return proposal
}

// Get Proposal from store by ProposalID
func (keeper Keeper) GetProposal(ctx sdk.Context, proposalID uint64) Proposal {
	store := ctx.KVStore(keeper.storeKey)
	bz := store.Get(KeyProposal(proposalID))
	if bz == nil {
		return nil
	}
	var proposal Proposal
	keeper.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &proposal)
	return proposal
}

// Implements sdk.AccountKeeper.
func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposalID uint64) {
	store := ctx.KVStore(keeper.storeKey)
	proposal := keeper.GetProposal(ctx, proposalID)
	keeper.RemoveFromInactiveProposalQueue(ctx, proposal.GetDepositEndTime(), proposalID)
	keeper.RemoveFromActiveProposalQueue(ctx, proposal.GetVotingEndTime(), proposalID)
	store.Delete(KeyProposal(proposalID))
}

上面是有关Proposal的新建/查询/删除接口,还有Deposit和Vote相关的方法不详细展示。

3.2 msg and handler

msg 是模块内支持的通过交易发起的操做
在上面的治理逻辑中,需要定义的Msg包括 (SubmitProposal/Deposit)

  • SubmitProposal : 提交提案
  • Deposit : 为提案提供押金
  • Vote : 为提案投票
// Cosmos-SDK/x/gov/msg.go
// MsgDeposit
type MsgDeposit struct {
	ProposalID uint64         `json:"proposal_id"` // ID of the proposal
	Depositor  sdk.AccAddress `json:"depositor"`   // Address of the depositor
	Amount     sdk.Coins      `json:"amount"`      // Coins to add to the proposal's deposit
}

func NewMsgDeposit(depositor sdk.AccAddress, proposalID uint64, amount sdk.Coins) MsgDeposit {
	return MsgDeposit{
		ProposalID: proposalID,
		Depositor:  depositor,
		Amount:     amount,
	}
}

// Implements Msg.
func (msg MsgDeposit) Route() string { return RouterKey }
func (msg MsgDeposit) Type() string  { return TypeMsgDeposit }

func (msg MsgDeposit) ValidateBasic() sdk.Error {
	if msg.Depositor.Empty() {
		return sdk.ErrInvalidAddress(msg.Depositor.String())
	}
	if !msg.Amount.IsValid() {
		return sdk.ErrInvalidCoins(msg.Amount.String())
	}
	if msg.Amount.IsAnyNegative() {
		return sdk.ErrInvalidCoins(msg.Amount.String())
	}
	if msg.ProposalID < 0 {
		return ErrUnknownProposal(DefaultCodespace, msg.ProposalID)
	}
	return nil
}

func (msg MsgDeposit) String() string {
	return fmt.Sprintf("MsgDeposit{%s=>%v: %v}", msg.Depositor, msg.ProposalID, msg.Amount)
}

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

func (msg MsgDeposit) GetSigners() []sdk.AccAddress {
	return []sdk.AccAddress{msg.Depositor}
}

msg 中仅仅是定义消息类型和实现消息的一些接口,当模块收到对应的消息时,处理的函数实现在handler中.

// Cosmos-SDK/x/gov/handler.go
func NewHandler(keeper Keeper) sdk.Handler {
	return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
		switch msg := msg.(type) {
		case MsgDeposit:
			return handleMsgDeposit(ctx, keeper, msg)
		case MsgSubmitProposal:
			return handleMsgSubmitProposal(ctx, keeper, msg)
		case MsgVote:
			return handleMsgVote(ctx, keeper, msg)
		default:
			errMsg := fmt.Sprintf("Unrecognized gov msg type: %T", msg)
			return sdk.ErrUnknownRequest(errMsg).Result()
		}
	}
}

func handleMsgDeposit(ctx sdk.Context, keeper Keeper, msg MsgDeposit) sdk.Result {
	err, votingStarted := keeper.AddDeposit(ctx, msg.ProposalID, msg.Depositor, msg.Amount)
	if err != nil {
		return err.Result()
	}

	proposalIDStr := fmt.Sprintf("%d", msg.ProposalID)
	resTags := sdk.NewTags(
		tags.Depositor, []byte(msg.Depositor.String()),
		tags.ProposalID, proposalIDStr,
	)

	if votingStarted {
		resTags = resTags.AppendTag(tags.VotingPeriodStart, proposalIDStr)
	}

	return sdk.Result{
		Tags: resTags,
	}
}
3.3 codec

codec 主要是将模块内自定义的类型注册到Amino,这样它们才会支持编解码。

var msgCdc = codec.New()

// Register concrete types on codec codec
func RegisterCodec(cdc *codec.Codec) {
	cdc.RegisterConcrete(MsgSubmitProposal{}, "cosmos-sdk/MsgSubmitProposal", nil)
	cdc.RegisterConcrete(MsgDeposit{}, "cosmos-sdk/MsgDeposit", nil)
	cdc.RegisterConcrete(MsgVote{}, "cosmos-sdk/MsgVote", nil)

	cdc.RegisterInterface((*Proposal)(nil), nil)
	cdc.RegisterConcrete(&TextProposal{}, "gov/TextProposal", nil)
}

func init() {
	RegisterCodec(msgCdc)
}
3.4 querier

Querier 中定义的是模块提供给外部的可供查询的接口,
在上面的逻辑中,querier包括

  • 查询当前存在的所有提案
  • 查询提案的押金数
  • 查询提案的投票数
  • 等等

baseapp中实现的ABCI接口 Query 是用来执行查询的,不同的查询请求是根据queryRouter 路由到对应的模块的,路由的原理和交易路由一样。

// Cosmos-SDK/baseapp/baseapp.go
func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) {
	path := splitPath(req.Path)
	if len(path) == 0 {
		msg := "no query path provided"
		return sdk.ErrUnknownRequest(msg).QueryResult()
	}

	switch path[0] {
	// "/app" prefix for special application queries
	case "app":
		return handleQueryApp(app, path, req)

	case "store":
		return handleQueryStore(app, path, req)

	case "p2p":
		return handleQueryP2P(app, path, req)

	case "custom":
		return handleQueryCustom(app, path, req)
	}

	msg := "unknown query path"
	return sdk.ErrUnknownRequest(msg).QueryResult()
}

func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) {
	// path[0] should be "custom" because "/custom" prefix is required for keeper
	// queries.
	
	querier := app.queryRouter.Route(path[1])
	if querier == nil {
		return sdk.ErrUnknownRequest(fmt.Sprintf("no custom querier found for route %s", path[1])).QueryResult()
	}

	resBytes, err := querier(ctx, path[2:], req)
	if err != nil {
		return abci.ResponseQuery{
			Code:      uint32(err.Code()),
			Codespace: string(err.Codespace()),
			Log:       err.ABCILog(),
		}
	}
	...
}
3.5 client

上面的querier和handler已经完成了治理模块内请求和事务的处理流程,那么client模块就是给用户提供发起请求和事务的接口。

cli 目录是提供给App命令行操作的接口:

// Cosmos-SDK/x/gov/client/cli/query.go
// GetCmdQueryDeposit implements the query proposal deposit command.
func GetCmdQueryDeposit(queryRoute string, cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "deposit [proposal-id] [depositer-addr]",
		Args:  cobra.ExactArgs(2),
		Short: "Query details of a deposit",
		Long: strings.TrimSpace(`
Query details for a single proposal deposit on a proposal by its identifier.

Example:
$ gaiacli query gov deposit 1 cosmos1skjwj5whet0lpe65qaq4rpq03hjxlwd9nf39lk
`),
		RunE: func(cmd *cobra.Command, args []string) error {
			cliCtx := context.NewCLIContext().WithCodec(cdc)

			// validate that the proposal id is a uint
			proposalID, err := strconv.ParseUint(args[0], 10, 64)
			...
			_, err = gcutils.QueryProposalByID(proposalID, cliCtx, cdc, queryRoute)
			if err != nil {
				return fmt.Errorf("Failed to fetch proposal-id %d: %s", proposalID, err)
			}

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

			params := gov.NewQueryDepositParams(proposalID, depositorAddr)
			bz, err := cdc.MarshalJSON(params)
			if err != nil {
				return err
			}

			res, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/deposit", queryRoute), bz)
			if err != nil {
				return err
			}
            ...
		},
	}
}

// Cosmos-SDK/x/gov/client/cli/tx.go
// GetCmdDeposit implements depositing tokens for an active proposal.
func GetCmdDeposit(queryRoute string, cdc *codec.Codec) *cobra.Command {
	return &cobra.Command{
		Use:   "deposit [proposal-id] [deposit]",
		Args:  cobra.ExactArgs(2),
		Short: "Deposit tokens for activing proposal",
		Long: strings.TrimSpace(`
Submit a deposit for an acive proposal. You can find the proposal-id by running gaiacli query gov proposals:

$ gaiacli tx gov deposit 1 10stake --from mykey
`),
		RunE: func(cmd *cobra.Command, args []string) error {
			txBldr := authtxb.NewTxBuilderFromCLI().WithTxEncoder(utils.GetTxEncoder(cdc))
			cliCtx := context.NewCLIContext().
				WithCodec(cdc).
				WithAccountDecoder(cdc)

			// validate that the proposal id is a uint
			proposalID, err := strconv.ParseUint(args[0], 10, 64)
			if err != nil {
				return fmt.Errorf("proposal-id %s not a valid uint, please input a valid proposal-id", args[0])
			}
            ...
            from := cliCtx.GetFromAddress()
            ...
			// Get amount of coins
			amount, err := sdk.ParseCoins(args[1])
			...

			msg := gov.NewMsgDeposit(from, proposalID, amount)
			err = msg.ValidateBasic()
			if err != nil {
				return err
			}

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

rest 目录下是提供的REST接口

注册所有提供的请求路径和对应的执行函数

// Cosmos-SDK/x/gov/client/rest/rest.go
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router, cdc *codec.Codec) {
	r.HandleFunc("/gov/proposals", postProposalHandlerFn(cdc, cliCtx)).Methods("POST")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/deposits", RestProposalID), depositHandlerFn(cdc, cliCtx)).Methods("POST")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/votes", RestProposalID), voteHandlerFn(cdc, cliCtx)).Methods("POST")

	r.HandleFunc(
		fmt.Sprintf("/gov/parameters/{%s}", RestParamsType),
		queryParamsHandlerFn(cdc, cliCtx),
	).Methods("GET")

	r.HandleFunc("/gov/proposals", queryProposalsWithParameterFn(cdc, cliCtx)).Methods("GET")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}", RestProposalID), queryProposalHandlerFn(cdc, cliCtx)).Methods("GET")
	r.HandleFunc(
		fmt.Sprintf("/gov/proposals/{%s}/proposer", RestProposalID),
		queryProposerHandlerFn(cdc, cliCtx),
	).Methods("GET")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/deposits", RestProposalID), queryDepositsHandlerFn(cdc, cliCtx)).Methods("GET")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/deposits/{%s}", RestProposalID, RestDepositor), queryDepositHandlerFn(cdc, cliCtx)).Methods("GET")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/tally", RestProposalID), queryTallyOnProposalHandlerFn(cdc, cliCtx)).Methods("GET")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/votes", RestProposalID), queryVotesOnProposalHandlerFn(cdc, cliCtx)).Methods("GET")
	r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}/votes/{%s}", RestProposalID, RestVoter), queryVoteHandlerFn(cdc, cliCtx)).Methods("GET")
}

func depositHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		strProposalID := vars[RestProposalID]
        ...

		var req DepositReq
		if !rest.ReadRESTReq(w, r, cdc, &req) {
			return
		}

		req.BaseReq = req.BaseReq.Sanitize()
		if !req.BaseReq.ValidateBasic(w) {
			return
		}

		// create the message
		msg := gov.NewMsgDeposit(req.Depositor, proposalID, req.Amount)
		if err := msg.ValidateBasic(); err != nil {
			rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
			return
		}

		clientrest.WriteGenerateStdTxResponse(w, cdc, cliCtx, req.BaseReq, []sdk.Msg{msg})
	}
}

module client 导出该模块所有的client操作接口, 最终绑定到主程序的子命令中。

// Cosmos-SDK/x/gov/client/module_client.go
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 gov queries under a subcommand
	govQueryCmd := &cobra.Command{
		Use:   gov.ModuleName,
		Short: "Querying commands for the governance module",
	}

	govQueryCmd.AddCommand(client.GetCommands(
		govCli.GetCmdQueryProposal(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryProposals(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryVote(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryVotes(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryParam(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryParams(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryProposer(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryDeposit(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryDeposits(mc.storeKey, mc.cdc),
		govCli.GetCmdQueryTally(mc.storeKey, mc.cdc))...)

	return govQueryCmd
}

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

	govTxCmd.AddCommand(client.PostCommands(
		govCli.GetCmdDeposit(mc.storeKey, mc.cdc),
		govCli.GetCmdVote(mc.storeKey, mc.cdc),
		govCli.GetCmdSubmitProposal(mc.cdc),
	)...)

	return govTxCmd
}

4. 总结

本文分析了治理模块的代码之后,整理了Cosmos模块开发框架,对于想要编写Cosmos App 及插件的同学有较大帮助。

你可能感兴趣的:(区块链)