摘要 模块定义了 Cosmos SDK 应用程序的大部分逻辑。开发者使用 Cosmos SDK 将多个模块组合起来,以构建自定义的、面向特定应用场景的区块链。本文档概述了 SDK 模块背后的基本概念,以及如何进行模块管理。
本页讨论了在 Cosmos SDK 中构建模块时需要考虑的一些设计因素。 如需更深入地了解模块,请参阅以下页面:

模块概念

深入了解模块如何工作,包括 keeper、消息处理器、查询服务以及模块管理器。

构建模块

按照分步教程,在一个示例 Cosmos SDK 链上从零开始构建自定义模块。

设计考量

在编写任何代码之前,以下是决定模块如何行为、如何互操作以及如何演进的关键设计决策。

明确定义模块边界

一个模块应当只拥有一块单一且边界清晰的应用状态。不要因为方便,就把互不相关的功能打包进同一个模块。边界收敛的模块更容易审计、更容易在不同链之间复用,也更容易独立升级。 问自己一个问题:另一条链是否可以在不修改的情况下合理使用这个模块?如果答案取决于先删掉一半功能,那这个模块很可能做得太多了。

尽早规划状态结构

模块定义的每一个 KVStore 键都是永久性的:删除或重命名键都需要迁移。使用 Collections 库来进行结构化状态管理,并为键命名时确保其具备抗冲突能力且便于自解释。 考虑你的模块需要建立哪些索引。一个只会通过单一键查询的值比较简单;而一个需要按多个维度查询的值(例如按 owner 和按 ID)则需要二级索引,这会增加复杂度和存储开销。

设计消息与查询接口

保持 Msg 服务尽可能精简。模块接受的每一条消息都会成为公共 API 的一部分,并且必须在升级过程中持续支持。相比定义大量狭窄用途的消息,更应优先使用更少但用途更通用的消息。 查询比消息更容易在后期增加,但仍要考虑客户端从一开始就需要什么。设计不佳的查询往往会导致链上出现过多状态,而这些状态存在的唯一目的,只是为了支持某个实际上没人需要的查询。

决定如何控制特权操作

大多数模块都有应当允许治理更新的参数。使用标准的 MsgUpdateParams 模式,并带上 Authority 字段,同时在创世时将该 authority 设置为治理模块地址。这样可以确保参数变更通过链上治理完成,而不是被硬编码或依赖链升级。 如果你的模块需要调用另一个模块中的特权函数,应当在应用初始化时通过 keeper 引用建立这些权限,而不是在运行时通过动态查找来完成。

谨慎建模模块间依赖

列出你的模块需要访问的每一个其他模块。每一项依赖都会在 keeper 构造时,以 keeper 引用的形式注入。避免循环依赖:如果模块 A 需要 B,而 B 又需要 A,那么其中一个模块做得太多了。应当引入第三个模块,或者重构共享逻辑。 优先接收接口而不是具体的 keeper 类型。这样可以让你的模块在隔离环境中更易于测试,也更容易在采用不同模块实现的链之间复用。

从一开始就为升级做准备

如果你的模块定义了状态,它最终就会需要迁移。从第一个版本开始,就在 x/<module>/migrations/ 中编写迁移逻辑,即使从 v1 到 v2 是空操作也一样。尽早建立这种模式,避免把升级变成事后补救。 实现细节请参阅模块升级。

模块在 Cosmos SDK 应用中的角色

Cosmos SDK 可以被视为区块链开发领域的 Ruby on Rails。它自带一个核心,提供每个区块链应用都需要的基础功能,例如用于与底层共识引擎通信的 ABCI 样板实现、用于持久化状态的 multistore、用于组成全节点的 server,以及处理查询的接口。 在这一核心之上,Cosmos SDK 使开发者能够构建模块,以实现应用程序的业务逻辑。换句话说,SDK 模块实现了应用程序的大部分逻辑,而核心负责连线,并使模块能够被组合在一起。最终目标是构建一个强健的开源 Cosmos SDK 模块生态系统,使构建复杂区块链应用变得越来越容易。 Cosmos SDK 模块可以被看作状态机中的一个个小状态机。它们通常会在主 multistore 中,通过一个或多个 KVStore 定义部分状态,并定义一部分消息类型。这些消息会由 Cosmos SDK 核心的主要组件之一 BaseApp 路由到定义这些消息的模块 Protobuf Msg 服务。 由于这种架构,构建一个 Cosmos SDK 应用通常围绕两件事展开:编写模块以实现应用的专用逻辑,以及将这些模块与现有模块组合起来,补全整个应用。开发者通常会编写那些用于其特定场景、但现有生态中尚不存在的模块;对于质押、账户或代币管理这类更通用的功能,则会直接使用已有模块。

模块作为超级用户

模块能够执行常规用户无法执行的操作。这是因为状态机为模块授予了 sudo 权限。模块可以拒绝另一个模块执行某个函数的意图,但这类逻辑必须显式实现。一个典型示例是模块创建用于修改参数的函数:
package keeper

import (
    
	"context"
    "github.com/hashicorp/go-metrics"

	errorsmod "cosmossdk.io/errors"
    "cosmossdk.io/x/bank/types"
    "github.com/cosmos/cosmos-sdk/telemetry"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

type msgServer struct {
    Keeper
}

var _ types.MsgServer = msgServer{
}

// NewMsgServerImpl returns an implementation of the bank MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper Keeper)

types.MsgServer {
    return &msgServer{
    Keeper: keeper
}
}

func (k msgServer)

Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) {
    var (
		from, to []byte
		err      error
	)
    if base, ok := k.Keeper.(BaseKeeper); ok {
    from, err = base.ak.AddressCodec().StringToBytes(msg.FromAddress)
    if err != nil {
    return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", err)
}

to, err = base.ak.AddressCodec().StringToBytes(msg.ToAddress)
    if err != nil {
    return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", err)
}
	
}

else {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}
    if !msg.Amount.IsValid() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String())
}
    if !msg.Amount.IsAllPositive() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String())
}
    if err := k.IsSendEnabledCoins(ctx, msg.Amount...); err != nil {
    return nil, err
}
    if k.BlockedAddr(to) {
    return nil, errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", msg.ToAddress)
}

err = k.SendCoins(ctx, from, to, msg.Amount)
    if err != nil {
    return nil, err
}

defer func() {
    for _, a := range msg.Amount {
    if a.Amount.IsInt64() {
    telemetry.SetGaugeWithLabels(
					[]string{"tx", "msg", "send"
},
					float32(a.Amount.Int64()),
					[]metrics.Label{
    telemetry.NewLabel("denom", a.Denom)
},
				)
}
	
}
	
}()

return &types.MsgSendResponse{
}, nil
}

func (k msgServer)

MultiSend(ctx context.Context, msg *types.MsgMultiSend) (*types.MsgMultiSendResponse, error) {
    if len(msg.Inputs) == 0 {
    return nil, types.ErrNoInputs
}
    if len(msg.Inputs) != 1 {
    return nil, types.ErrMultipleSenders
}
    if len(msg.Outputs) == 0 {
    return nil, types.ErrNoOutputs
}
    if err := types.ValidateInputOutputs(msg.Inputs[0], msg.Outputs); err != nil {
    return nil, err
}

	// NOTE: totalIn == totalOut should already have been checked
    for _, in := range msg.Inputs {
    if err := k.IsSendEnabledCoins(ctx, in.Coins...); err != nil {
    return nil, err
}
	
}
    for _, out := range msg.Outputs {
    if base, ok := k.Keeper.(BaseKeeper); ok {
    accAddr, err := base.ak.AddressCodec().StringToBytes(out.Address)
    if err != nil {
    return nil, err
}
    if k.BlockedAddr(accAddr) {
    return nil, errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", out.Address)
}
	
}

else {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}
	
}
    err := k.InputOutputCoins(ctx, msg.Inputs[0], msg.Outputs)
    if err != nil {
    return nil, err
}

return &types.MsgMultiSendResponse{
}, nil
}

func (k msgServer)

UpdateParams(ctx context.Context, req *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) {
    if k.GetAuthority() != req.Authority {
    return nil, errorsmod.Wrapf(types.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.GetAuthority(), req.Authority)
}
    if err := req.Params.Validate(); err != nil {
    return nil, err
}
    if err := k.SetParams(ctx, req.Params); err != nil {
    return nil, err
}

return &types.MsgUpdateParamsResponse{
}, nil
}

func (k msgServer)

SetSendEnabled(ctx context.Context, msg *types.MsgSetSendEnabled) (*types.MsgSetSendEnabledResponse, error) {
    if k.GetAuthority() != msg.Authority {
    return nil, errorsmod.Wrapf(types.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.GetAuthority(), msg.Authority)
}
    seen := map[string]bool{
}
    for _, se := range msg.SendEnabled {
    if _, alreadySeen := seen[se.Denom]; alreadySeen {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("duplicate denom entries found for %q", se.Denom)
}

seen[se.Denom] = true
    if err := se.Validate(); err != nil {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid SendEnabled denom %q: %s", se.Denom, err)
}
	
}
    for _, denom := range msg.UseDefaultFor {
    if err := sdk.ValidateDenom(denom); err != nil {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid UseDefaultFor denom %q: %s", denom, err)
}
	
}
    if len(msg.SendEnabled) > 0 {
    k.SetAllSendEnabled(ctx, msg.SendEnabled)
}
    if len(msg.UseDefaultFor) > 0 {
    k.DeleteSendEnabled(ctx, msg.UseDefaultFor...)
}

return &types.MsgSetSendEnabledResponse{
}, nil
}

func (k msgServer)

Burn(goCtx context.Context, msg *types.MsgBurn) (*types.MsgBurnResponse, error) {
    var (
		from []byte
		err  error
	)

var coins sdk.Coins
    for _, coin := range msg.Amount {
    coins = coins.Add(sdk.NewCoin(coin.Denom, coin.Amount))
}
    if base, ok := k.Keeper.(BaseKeeper); ok {
    from, err = base.ak.AddressCodec().StringToBytes(msg.FromAddress)
    if err != nil {
    return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", err)
}
	
}

else {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}
    if !coins.IsValid() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, coins.String())
}
    if !coins.IsAllPositive() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, coins.String())
}

err = k.BurnCoins(goCtx, from, coins)
    if err != nil {
    return nil, err
}

return &types.MsgBurnResponse{
}, nil
}

作为开发者应如何构建模块

虽然编写模块并不存在一套绝对明确的规范,但开发者在构建模块时应牢记以下重要设计原则:
  • 可组合性:Cosmos SDK 应用几乎总是由多个模块组合而成。这意味着开发者不仅需要认真考虑其模块与 Cosmos SDK 核心的集成方式,还需要考虑它与其他模块的集成方式。前者可通过遵循这里概述的标准设计模式来实现,后者则通过经由 keeper 正确暴露模块的 store 来实现。
  • 专一性:可组合性 的直接结果之一,就是模块应当保持 专一性。开发者应仔细界定模块的职责范围,而不是把多种功能打包进同一个模块中。这种关注点分离使模块能够在其他项目中复用,并提升应用的可升级性。专一性 在 Cosmos SDK 的对象能力模型中也起着重要作用。
  • 能力:大多数模块都需要读取和/或写入其他模块的 store。然而,在开源环境中,某些模块可能是恶意的。因此,模块开发者不仅需要认真思考自己的模块如何与其他模块交互,还需要思考如何授予对模块 store 的访问权限。Cosmos SDK 在模块间安全上采用面向能力的方案。这意味着模块定义的每个 store 都通过一个 key 访问,而该 key 由模块的 keeper 持有。这个 keeper 定义了如何访问这些 store,以及在什么条件下可以访问。对模块 store 的访问是通过传递模块 keeper 的引用来完成的。

Cosmos SDK 模块的主要组成部分

按照惯例,模块定义在 ./x/ 子目录中(例如,bank 模块会定义在 ./x/bank 目录下)。它们通常共享以下核心组成部分:
  • keeper,用于访问模块的 store 并更新状态。
  • Msg service,用于在消息被 BaseApp 路由到该模块时处理消息,并触发状态转换。
  • query service,用于在用户查询被 BaseApp 路由到该模块时处理查询。
  • Interfaces,供最终用户查询该模块定义的那部分状态,并创建模块中定义的自定义类型 message。
除上述组件外,模块还会实现 AppModule 接口,以便由 module manager 进行管理。
Synopsis Modules define most of the logic of Cosmos SDK applications. Developers compose modules together using the Cosmos SDK to build their custom application-specific blockchains. This document outlines the basic concepts behind SDK modules and how to approach module management.
This page discusses some of the design considerations for building modules in the Cosmos SDK. For more in-depth information on modules, see the following pages:

Module Concepts

Deep dive into how modules work — keepers, message handlers, query services, and the module manager.

Build a Module

Follow a step-by-step tutorial to build a custom module from scratch on an example Cosmos SDK chain.

Design Considerations

Before writing any code, these are the key design decisions that shape how a module will behave, interoperate, and evolve.

Define clear module boundaries

A module should own a single, well-scoped piece of application state. Resist the temptation to bundle unrelated functionality into one module because it is convenient. Narrow modules are easier to audit, re-use across chains, and upgrade independently. Ask: could a different chain reasonably use this module without modification? If the answer depends on removing half the features, the module is probably doing too much.

Plan your state structure early

Every KVStore key your module defines is permanent: removing or renaming keys requires a migration. Use the Collections library for structured state management, and name keys to be collision-resistant and self-documenting. Consider what your module needs to index. A value that is only ever looked up by a single key is simple. A value looked up by multiple dimensions (e.g. by owner and by ID) requires secondary indexes, which add complexity and storage overhead.

Design your message and query surface

Keep the Msg service minimal. Every message your module accepts becomes part of your public API and must be handled across upgrades. Prefer fewer, general-purpose messages over many narrow ones. Queries are cheaper to add later than messages, but consider what clients need from day one. Poorly designed queries often lead to excessive on-chain state that exists solely to support a query no one else needs.

Decide how privileged operations are controlled

Most modules have parameters that governance should be able to update. Use the standard MsgUpdateParams pattern with an Authority field, and set that authority to the governance module address at genesis. This ensures parameter changes go through on-chain governance rather than being hardcoded or requiring a chain upgrade. If your module needs to call into another module’s privileged functions, establish those permissions through keeper references at app initialization — not through dynamic lookups at runtime.

Model inter-module dependencies carefully

List every other module your module needs access to. Each dependency becomes a keeper reference injected into your keeper at construction. Avoid circular dependencies: if module A needs B and B needs A, one of them is doing too much. Introduce a third module or restructure the shared logic. Prefer accepting interfaces over concrete keeper types. This makes your module testable in isolation and re-usable across chains with different module implementations.

Plan for upgrades from the start

If your module defines state, it will eventually need a migration. Write migration logic in x/<module>/migrations/ from the first version, even if v1 to v2 is a no-op. Establish the pattern early so upgrades are not an afterthought. See Module Upgrades for implementation details.

Role of Modules in a Cosmos SDK Application

The Cosmos SDK can be thought of as the Ruby-on-Rails of blockchain development. It comes with a core that provides the basic functionalities every blockchain application needs, like a boilerplate implementation of the ABCI to communicate with the underlying consensus engine, a multistore to persist state, a server to form a full-node and interfaces to handle queries. On top of this core, the Cosmos SDK enables developers to build modules that implement the business logic of their application. In other words, SDK modules implement the bulk of the logic of applications, while the core does the wiring and enables modules to be composed together. The end goal is to build a robust ecosystem of open-source Cosmos SDK modules, making it increasingly easier to build complex blockchain applications. Cosmos SDK modules can be seen as little state-machines within the state-machine. They generally define a subset of the state using one or more KVStores in the main multistore, as well as a subset of message types. These messages are routed by one of the main components of Cosmos SDK core, BaseApp, to a module Protobuf Msg service that defines them. As a result of this architecture, building a Cosmos SDK application usually revolves around writing modules to implement the specialized logic of the application and composing them with existing modules to complete the application. Developers will generally work on modules that implement logic needed for their specific use case that do not exist yet, and will use existing modules for more generic functionalities like staking, accounts, or token management.

Modules as super-users

Modules have the ability to perform actions that are not available to regular users. This is because modules are given sudo permissions by the state machine. Modules can reject another modules desire to execute a function but this logic must be explicit. Examples of this can be seen when modules create functions to modify parameters:
package keeper

import (
    
	"context"
    "github.com/hashicorp/go-metrics"

	errorsmod "cosmossdk.io/errors"
    "cosmossdk.io/x/bank/types"
    "github.com/cosmos/cosmos-sdk/telemetry"
	sdk "github.com/cosmos/cosmos-sdk/types"
	sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)

type msgServer struct {
    Keeper
}

var _ types.MsgServer = msgServer{
}

// NewMsgServerImpl returns an implementation of the bank MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper Keeper)

types.MsgServer {
    return &msgServer{
    Keeper: keeper
}
}

func (k msgServer)

Send(ctx context.Context, msg *types.MsgSend) (*types.MsgSendResponse, error) {
    var (
		from, to []byte
		err      error
	)
    if base, ok := k.Keeper.(BaseKeeper); ok {
    from, err = base.ak.AddressCodec().StringToBytes(msg.FromAddress)
    if err != nil {
    return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", err)
}

to, err = base.ak.AddressCodec().StringToBytes(msg.ToAddress)
    if err != nil {
    return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid to address: %s", err)
}
	
}

else {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}
    if !msg.Amount.IsValid() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String())
}
    if !msg.Amount.IsAllPositive() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, msg.Amount.String())
}
    if err := k.IsSendEnabledCoins(ctx, msg.Amount...); err != nil {
    return nil, err
}
    if k.BlockedAddr(to) {
    return nil, errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", msg.ToAddress)
}

err = k.SendCoins(ctx, from, to, msg.Amount)
    if err != nil {
    return nil, err
}

defer func() {
    for _, a := range msg.Amount {
    if a.Amount.IsInt64() {
    telemetry.SetGaugeWithLabels(
					[]string{"tx", "msg", "send"
},
					float32(a.Amount.Int64()),
					[]metrics.Label{
    telemetry.NewLabel("denom", a.Denom)
},
				)
}
	
}
	
}()

return &types.MsgSendResponse{
}, nil
}

func (k msgServer)

MultiSend(ctx context.Context, msg *types.MsgMultiSend) (*types.MsgMultiSendResponse, error) {
    if len(msg.Inputs) == 0 {
    return nil, types.ErrNoInputs
}
    if len(msg.Inputs) != 1 {
    return nil, types.ErrMultipleSenders
}
    if len(msg.Outputs) == 0 {
    return nil, types.ErrNoOutputs
}
    if err := types.ValidateInputOutputs(msg.Inputs[0], msg.Outputs); err != nil {
    return nil, err
}

	// NOTE: totalIn == totalOut should already have been checked
    for _, in := range msg.Inputs {
    if err := k.IsSendEnabledCoins(ctx, in.Coins...); err != nil {
    return nil, err
}
	
}
    for _, out := range msg.Outputs {
    if base, ok := k.Keeper.(BaseKeeper); ok {
    accAddr, err := base.ak.AddressCodec().StringToBytes(out.Address)
    if err != nil {
    return nil, err
}
    if k.BlockedAddr(accAddr) {
    return nil, errorsmod.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", out.Address)
}
	
}

else {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}
	
}
    err := k.InputOutputCoins(ctx, msg.Inputs[0], msg.Outputs)
    if err != nil {
    return nil, err
}

return &types.MsgMultiSendResponse{
}, nil
}

func (k msgServer)

UpdateParams(ctx context.Context, req *types.MsgUpdateParams) (*types.MsgUpdateParamsResponse, error) {
    if k.GetAuthority() != req.Authority {
    return nil, errorsmod.Wrapf(types.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.GetAuthority(), req.Authority)
}
    if err := req.Params.Validate(); err != nil {
    return nil, err
}
    if err := k.SetParams(ctx, req.Params); err != nil {
    return nil, err
}

return &types.MsgUpdateParamsResponse{
}, nil
}

func (k msgServer)

SetSendEnabled(ctx context.Context, msg *types.MsgSetSendEnabled) (*types.MsgSetSendEnabledResponse, error) {
    if k.GetAuthority() != msg.Authority {
    return nil, errorsmod.Wrapf(types.ErrInvalidSigner, "invalid authority; expected %s, got %s", k.GetAuthority(), msg.Authority)
}
    seen := map[string]bool{
}
    for _, se := range msg.SendEnabled {
    if _, alreadySeen := seen[se.Denom]; alreadySeen {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("duplicate denom entries found for %q", se.Denom)
}

seen[se.Denom] = true
    if err := se.Validate(); err != nil {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid SendEnabled denom %q: %s", se.Denom, err)
}
	
}
    for _, denom := range msg.UseDefaultFor {
    if err := sdk.ValidateDenom(denom); err != nil {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid UseDefaultFor denom %q: %s", denom, err)
}
	
}
    if len(msg.SendEnabled) > 0 {
    k.SetAllSendEnabled(ctx, msg.SendEnabled)
}
    if len(msg.UseDefaultFor) > 0 {
    k.DeleteSendEnabled(ctx, msg.UseDefaultFor...)
}

return &types.MsgSetSendEnabledResponse{
}, nil
}

func (k msgServer)

Burn(goCtx context.Context, msg *types.MsgBurn) (*types.MsgBurnResponse, error) {
    var (
		from []byte
		err  error
	)

var coins sdk.Coins
    for _, coin := range msg.Amount {
    coins = coins.Add(sdk.NewCoin(coin.Denom, coin.Amount))
}
    if base, ok := k.Keeper.(BaseKeeper); ok {
    from, err = base.ak.AddressCodec().StringToBytes(msg.FromAddress)
    if err != nil {
    return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid from address: %s", err)
}
	
}

else {
    return nil, sdkerrors.ErrInvalidRequest.Wrapf("invalid keeper type: %T", k.Keeper)
}
    if !coins.IsValid() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, coins.String())
}
    if !coins.IsAllPositive() {
    return nil, errorsmod.Wrap(sdkerrors.ErrInvalidCoins, coins.String())
}

err = k.BurnCoins(goCtx, from, coins)
    if err != nil {
    return nil, err
}

return &types.MsgBurnResponse{
}, nil
}

How to Approach Building Modules as a Developer

While there are no definitive guidelines for writing modules, here are some important design principles developers should keep in mind when building them:
  • Composability: Cosmos SDK applications are almost always composed of multiple modules. This means developers need to carefully consider the integration of their module not only with the core of the Cosmos SDK, but also with other modules. The former is achieved by following standard design patterns outlined here, while the latter is achieved by properly exposing the store(s) of the module via the keeper.
  • Specialization: A direct consequence of the composability feature is that modules should be specialized. Developers should carefully establish the scope of their module and not batch multiple functionalities into the same module. This separation of concerns enables modules to be re-used in other projects and improves the upgradability of the application. Specialization also plays an important role in the object-capabilities model of the Cosmos SDK.
  • Capabilities: Most modules need to read and/or write to the store(s) of other modules. However, in an open-source environment, it is possible for some modules to be malicious. That is why module developers need to carefully think not only about how their module interacts with other modules, but also about how to give access to the module’s store(s). The Cosmos SDK takes a capabilities-oriented approach to inter-module security. This means that each store defined by a module is accessed by a key, which is held by the module’s keeper. This keeper defines how to access the store(s) and under what conditions. Access to the module’s store(s) is done by passing a reference to the module’s keeper.

Main Components of Cosmos SDK Modules

Modules are by convention defined in the ./x/ subfolder (e.g. the bank module will be defined in the ./x/bank folder). They generally share the same core components:
  • A keeper, used to access the module’s store(s) and update the state.
  • A Msg service, used to process messages when they are routed to the module by BaseApp and trigger state-transitions.
  • A query service, used to process user queries when they are routed to the module by BaseApp.
  • Interfaces, for end users to query the subset of the state defined by the module and create messages of the custom types defined in the module.
In addition to these components, modules implement the AppModule interface in order to be managed by the module manager.