推荐的文件夹结构
本文概述了Cosmos SDK模块的推荐结构。这些想法旨在作为建议应用,鼓励应用程序开发人员改进并为模块结构和开发设计做出贡献。
结构
一个典型的Cosmos SDK模块的结构如下:
proto
└── {project_name}
└── {module_name}
└── {proto_version}
├── {module_name}.proto
├── event.proto
├── genesis.proto
├── query.proto
└── tx.proto
- {module_name}.proto。模块的通用消息类型定义。
- event.proto: 该模块与事件有关的消息类型定义。
- genesis.proto: 该模块的消息类型定义与创世状态有关。
- query.proto: 模块的查询服务和相关的消息类型定义。
- tx.proto: 该模块的Msg服务和相关的消息类型定义。
x/{module_name}
├── client
│ ├── cli
│ │ ├── query.go
│ │ └── tx.go
│ └── testutil
│ ├── cli_test.go
│ └── suite.go
├── exported
│ └── exported.go
├── keeper
│ ├── genesis.go
│ ├── grpc_query.go
│ ├── hooks.go
│ ├── invariants.go
│ ├── keeper.go
│ ├── keys.go
│ ├── msg_server.go
│ └── querier.go
├── module
│ └── module.go
├── simulation
│ ├── decoder.go
│ ├── genesis.go
│ ├── operations.go
│ └── params.go
├── spec
│ ├── 01_concepts.md
│ ├── 02_state.md
│ ├── 03_messages.md
│ └── 04_events.md
├── {module_name}.pb.go
├── abci.go
├── codec.go
├── errors.go
├── events.go
├── events.pb.go
├── expected_keepers.go
├── genesis.go
├── genesis.pb.go
├── keys.go
├── msgs.go
├── params.go
├── query.pb.go
└── tx.pb.go
- client/: 模块的CLI客户端功能实现和模块的集成测试套件。
- exported/: 模块的导出类型--通常是接口类型。如果一个模块依赖于另一个模块的keeper,它应该通过expected_keepers.go文件(见下文)以接口合同的形式接收keeper,以避免对实现keeper的模块的直接依赖。然而,这些接口合同可以定义一些方法,这些方法的操作和/或返回的类型是实现守门员的模块所特有的,这就是exported/发挥作用的地方。在exported/中定义的接口类型使用规范类型,允许模块通过expected_keepers.go文件接收作为接口合同的keepers。这种模式使代码保持干燥,同时也缓解了导入周期的混乱。
- keeper/:模块的Keeper和MsgServer实现。
- module/:该模块的AppModule和AppModuleBasic实现。
- simulation/:该模块的模拟包定义了区块链模拟器应用程序(simapp)所使用的功能。
- spec/:该模块的规范文件概述了重要的概念,状态存储结构,以及消息和事件类型定义。
- 根目录包括消息、事件和genesis状态的类型定义,包括由协议缓冲器生成的类型定义。
- abci.go:模块的BeginBlocker和EndBlocker实现(只有在需要定义BeginBlocker和/或EndBlocker时才需要这个文件)。
- codec.go:该模块的接口类型的注册方法。
- errors.go:该模块的哨兵错误。
- events.go:该模块的事件类型和构造函数。
- expected_keepers.go:模块的预期保持者接口。
- genesis.go。该模块的genesis状态方法和辅助函数。
- keys.go: 该模块的存储键和相关的辅助函数。
- msgs.go: 该模块的消息类型定义和相关方法。
- params.go: 该模块的参数类型定义和相关方法。
- *.pb.go: 该模块由协议缓冲区生成的类型定义(如上面各自的*.proto文件中的定义)。
错误
本文概述了Cosmos SDK模块中错误处理的推荐用法和API。
鼓励模块定义和注册它们自己的错误,以便为失败的消息或处理程序执行提供更好的context。通常,这些错误应该是常见或一般错误,可以进一步包装以提供额外的特定执行context。
登记
模块应该在x/{module}/errors.go中定义和注册它们的自定义错误。错误的注册是通过错误包处理的。
示例:
package types import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) // x/distribution module sentinel errors var ( ErrEmptyDelegatorAddr = sdkerrors.Register(ModuleName, 2, "delegator address is empty") ErrEmptyWithdrawAddr = sdkerrors.Register(ModuleName, 3, "withdraw address is empty") ErrEmptyValidatorAddr = sdkerrors.Register(ModuleName, 4, "validator address is empty") ErrEmptyDelegationDistInfo = sdkerrors.Register(ModuleName, 5, "no delegation distribution info") ErrNoValidatorDistInfo = sdkerrors.Register(ModuleName, 6, "no validator distribution info") ErrNoValidatorCommission = sdkerrors.Register(ModuleName, 7, "no validator commission to withdraw") ErrSetWithdrawAddrDisabled = sdkerrors.Register(ModuleName, 8, "set withdraw address disabled") ErrBadDistribution = sdkerrors.Register(ModuleName, 9, "community pool does not have sufficient coins to distribute") ErrInvalidProposalAmount = sdkerrors.Register(ModuleName, 10, "invalid community pool spend proposal amount") ErrEmptyProposalRecipient = sdkerrors.Register(ModuleName, 11, "invalid community pool spend proposal recipient") ErrNoValidatorExists = sdkerrors.Register(ModuleName, 12, "validator does not exist") ErrNoDelegationExists = sdkerrors.Register(ModuleName, 13, "delegation does not exist") )
每个自定义模块错误都必须提供代码空间,该代码空间通常是模块名称(例如“distribution”)并且每个模块都是唯一的,以及一个uint32代码。代码空间和代码一起提供了全球唯一的Cosmos SDK错误。通常,代码是单调递增的,但不一定必须如此。对错误代码的唯一限制如下:
- 必须大于1,因为代码值1是为内部错误保留的。
- 在模块内必须是唯一的。
请注意,Cosmos SDK 提供了一组核心常见错误。 这些错误在types/errors/errors.go中定义。
包装自定义模块错误可以作为它们的具体类型返回,因为它们已经实现了错误接口。但是,可以包装模块错误以提供进一步的context和失败执行的含义。
例子:
func (k BaseKeeper) DelegateCoins(ctx sdk.Context, delegatorAddr, moduleAccAddr sdk.AccAddress, amt sdk.Coins) error {
moduleAcc := k.ak.GetAccount(ctx, moduleAccAddr)
if moduleAcc == nil {
return sdkerrors.Wrapf(sdkerrors.ErrUnknownAddress, "module account %s does not exist", moduleAccAddr)
}
if !amt.IsValid() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, amt.String())
}
balances := sdk.NewCoins()
for _, coin := range amt {
balance := k.GetBalance(ctx, delegatorAddr, coin.GetDenom())
if balance.IsLT(coin) {
return sdkerrors.Wrapf(
sdkerrors.ErrInsufficientFunds, "failed to delegate; %s is smaller than %s", balance, amt,
)
}
balances = balances.Add(balance)
err := k.setBalance(ctx, delegatorAddr, balance.Sub(coin))
if err != nil {
return err
}
}
if err := k.trackDelegation(ctx, delegatorAddr, balances, amt); err != nil {
return sdkerrors.Wrap(err, "failed to track delegation")
}
// emit coin spent event
ctx.EventManager().EmitEvent(
types.NewCoinSpentEvent(delegatorAddr, amt),
)
err := k.addCoins(ctx, moduleAccAddr, amt)
if err != nil {
return err
}
return nil
}
无论错误是否被包装,Cosmos SDK的错误包都提供了一个函数来确定错误是否属于特定类型。
ABCI
如果注册了模块错误,则Cosmos SDK错误包允许通过ABCIInfo函数提取ABCI信息。该包还提供了ResponseCheckTx和ResponseDeliverTx作为辅助函数,以自动从错误中获取CheckTx和DeliverTx响应。
升级模块
就地存储迁移允许您的模块升级到包含重大更改的新版本。本文概述了如何构建模块以利用此功能。
- 版本必须由模块开发者硬编码。
- 初始版本必须设置为1。
协商一致的版本作为应用程序模块的突破性版本,当模块引入突破性变化时,必须递增。
注册迁移
为了注册模块升级过程中发生的功能,必须注册要发生的迁移。
迁移注册是在配置器中使用RegisterMigration方法进行的。AppModule对配置器的引用是在RegisterServices方法中。
可以注册一个或多个迁移。如果你注册了一个以上的迁移脚本,请按照递增的顺序列出迁移,并确保有足够多的迁移可以导致所需的共识版本。例如,要迁移到一个模块的第3个版本,需要分别注册版本1和版本2的迁移,如下图所示:
func (am AppModule) RegisterServices(cfg module.Configurator) {
// --snip--
cfg.RegisterMigration(types.ModuleName, 1, func(ctx sdk.Context) error {
// Perform in-place store migrations from ConsensusVersion 1 to 2.
})
cfg.RegisterMigration(types.ModuleName, 2, func(ctx sdk.Context) error {
// Perform in-place store migrations from ConsensusVersion 2 to 3.
})
}
由于这些迁移是需要访问Keeper存储的函数,因此请使用名为Migrator 的 Keeper包装器,如下例所示:
// Migrator is a struct for handling in-place store migrations.
type Migrator struct {
keeper BaseKeeper
}
// NewMigrator returns a new Migrator.
func NewMigrator(keeper BaseKeeper) Migrator {
return Migrator{keeper: keeper}
}
// Migrate1to2 migrates from version 1 to 2.
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
return v043.MigrateStore(ctx, m.keeper.storeKey, m.keeper.cdc)
}
// Migrate2to3 migrates x/bank storage from version 2 to 3.
func (m Migrator) Migrate2to3(ctx sdk.Context) error {
return v046.MigrateStore(ctx, m.keeper.storeKey, m.keeper.cdc)
}
编写迁移脚本
要定义在升级期间发生的功能,请编写迁移脚本并将函数放置在migrations/目录中。例如,要为银行模块编写迁移脚本,请将函数放在x/bank/migrations/中。对这些函数使用推荐的命名约定。例如,v2bank 是迁移包 x/bank/migrations/v2的脚本:
// Migrating bank module from version 1 to 2
func (m Migrator) Migrate1to2(ctx sdk.Context) error {
return v2bank.MigrateStore(ctx, m.keeper.storeKey) // v2bank is package `x/bank/migrations/v2`.
}
要查看在余额键迁移中实现的更改示例代码,请查看migrateBalanceKeys。 就上下文而言,此代码引入了银行存储的迁移,该迁移将地址更新为以字节长度为前缀,如 ADR-028 中所述。

模块模拟
本文详细介绍了如何定义每个模块的仿真功能,以便与应用程序SimulationManager集成。
- 模拟包
- 存储解码器
- 随机生成
- 随机参数变化
- 随机加权操作
- 随机提案内容
- 注册模拟功能
- 应用仿真器管理器
模拟包
实现Cosmos SDK模拟器的每个模块都需要有一个x/<module>/simulation包,其中包含模糊测试所需的主要功能:存储解码器、随机生成状态和参数、加权操作和提案内容。
存储解码器
注册存储解码器是AppImportExport所要求的。这允许将存储中的键值对被解码(即解读)为其相应的类型。特别是,它将键与一个具体的类型相匹配,然后将KVPair的值解读为提供的类型。
可以使用这里的例子从分布模块来实现存储解码器。
随机生成
模拟器测试不同的场景和genesis参数的值,以便充分测试特定模块的边缘情况。每个模块的模拟器包必须暴露一个RandomizedGenState函数,以从给定的种子生成初始的随机GenesisState。
一旦模块的genesis参数被随机生成(或用参数文件中定义的键和值),它们就被处理成JSON格式,并被添加到应用程序的创世JSON中,以便在模拟中使用。
可以在这里查看如何创建随机生成的例子。
随机参数变化
模拟器能够随机测试参数变化。每个模块的模拟器包必须包含一个RandomizedParams func,它将在整个模拟生命周期中模拟模块的参数变化。
可以在这里看到一个完全测试参数变化的例子。
随机加权操作
操作是Cosmos SDK模拟的关键部分之一。它们是用随机字段值模拟的交易(Msg)。操作的发件人也是随机分配的。
模拟的操作是使用暴露于BaseApp的ABCI应用的完整交易周期来模拟的。
下面显示的是权重的设置方式:
// Simulation operation weights constants
const (
OpWeightMsgCreateValidator = "op_weight_msg_create_validator"
OpWeightMsgEditValidator = "op_weight_msg_edit_validator"
OpWeightMsgDelegate = "op_weight_msg_delegate"
OpWeightMsgUndelegate = "op_weight_msg_undelegate"
OpWeightMsgBeginRedelegate = "op_weight_msg_begin_redelegate"
OpWeightMsgCancelUnbondingDelegation = "op_weight_msg_cancel_unbonding_delegation"
)
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simtypes.AppParams, cdc codec.JSONCodec, ak types.AccountKeeper,
bk types.BankKeeper, k keeper.Keeper,
) simulation.WeightedOperations {
var (
weightMsgCreateValidator int
weightMsgEditValidator int
weightMsgDelegate int
weightMsgUndelegate int
weightMsgBeginRedelegate int
weightMsgCancelUnbondingDelegation int
)
appParams.GetOrGenerate(cdc, OpWeightMsgCreateValidator, &weightMsgCreateValidator, nil,
func(_ *rand.Rand) {
weightMsgCreateValidator = simappparams.DefaultWeightMsgCreateValidator
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgEditValidator, &weightMsgEditValidator, nil,
func(_ *rand.Rand) {
weightMsgEditValidator = simappparams.DefaultWeightMsgEditValidator
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgDelegate, &weightMsgDelegate, nil,
func(_ *rand.Rand) {
weightMsgDelegate = simappparams.DefaultWeightMsgDelegate
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgUndelegate, &weightMsgUndelegate, nil,
func(_ *rand.Rand) {
weightMsgUndelegate = simappparams.DefaultWeightMsgUndelegate
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgBeginRedelegate, &weightMsgBeginRedelegate, nil,
func(_ *rand.Rand) {
weightMsgBeginRedelegate = simappparams.DefaultWeightMsgBeginRedelegate
},
)
appParams.GetOrGenerate(cdc, OpWeightMsgCancelUnbondingDelegation, &weightMsgCancelUnbondingDelegation, nil,
func(_ *rand.Rand) {
weightMsgCancelUnbondingDelegation = simappparams.DefaultWeightMsgCancelUnbondingDelegation
},
)
正如所看的到,在这种情况下,权重是预定义的。存在使用不同权重覆盖此行为的选项。一种选择是使用*rand.Rand为操作定义随机权重,或者可以注入自己的预定义权重。
以下是如何覆盖上述包simappparams:
@echo "Running non-determinism test..."
@go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \
-NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h
test-sim-custom-genesis-fast:
@echo "Running custom genesis simulation..."
@echo "By default, ${HOME}/.gaia/config/genesis.json will be used."
@go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation -Genesis=${HOME}/.gaia/config/genesis.json \
-Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h
test-sim-import-export: runsim
@echo "Running application import/export simulation. This may take several minutes..."
@$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 50 5 TestAppImportExport
对于最后一个测试,使用了一个名为runsim的工具,它用于并行化go测试实例,向Github提供信息,并通过slack集成向团队提供有关模拟运行方式的信息。
随机提案内容
Cosmos SDK模拟器也支持随机治理提案。每个模块都必须定义它们公开的治理提案内容并注册它们以在参数上使用。
注册模拟函数
现在所有必需的函数都已定义,我们需要将它们集成到module.go中的模块模式中:
package distribution
import (
"context"
"encoding/json"
"fmt"
"math/rand"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types"
sdkclient "github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
cdctypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/distribution/client/cli"
"github.com/cosmos/cosmos-sdk/x/distribution/keeper"
"github.com/cosmos/cosmos-sdk/x/distribution/simulation"
"github.com/cosmos/cosmos-sdk/x/distribution/types"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic defines the basic application module used by the distribution module.
type AppModuleBasic struct {
cdc codec.Codec
}
// Name returns the distribution module's name.
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// RegisterLegacyAminoCodec registers the distribution module's types for the given codec.
func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
types.RegisterLegacyAminoCodec(cdc)
}
// DefaultGenesis returns default genesis state as raw bytes for the distribution
// module.
func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage {
return cdc.MustMarshalJSON(types.DefaultGenesisState())
}
// ValidateGenesis performs genesis state validation for the distribution module.
func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config sdkclient.TxEncodingConfig, bz json.RawMessage) error {
var data types.GenesisState
if err := cdc.UnmarshalJSON(bz, &data); err != nil {
return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err)
}
return types.ValidateGenesis(&data)
}
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the distribution module.
func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx sdkclient.Context, mux *runtime.ServeMux) {
if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil {
panic(err)
}
}
// GetTxCmd returns the root tx command for the distribution module.
func (AppModuleBasic) GetTxCmd() *cobra.Command {
return cli.NewTxCmd()
}
// GetQueryCmd returns the root query command for the distribution module.
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
return cli.GetQueryCmd()
}
// RegisterInterfaces implements InterfaceModule
func (b AppModuleBasic) RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
types.RegisterInterfaces(registry)
}
// AppModule implements an application module for the distribution module.
type AppModule struct {
AppModuleBasic
keeper keeper.Keeper
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
stakingKeeper stakingkeeper.Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(
cdc codec.Codec, keeper keeper.Keeper, accountKeeper types.AccountKeeper,
bankKeeper types.BankKeeper, stakingKeeper stakingkeeper.Keeper,
) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{cdc: cdc},
keeper: keeper,
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
stakingKeeper: stakingKeeper,
}
}
// Name returns the distribution module's name.
func (AppModule) Name() string {
return types.ModuleName
}
// RegisterInvariants registers the distribution module invariants.
func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {
keeper.RegisterInvariants(ir, am.keeper)
}
// Deprecated: Route returns the message routing key for the distribution module.
func (am AppModule) Route() sdk.Route {
return sdk.Route{}
}
// QuerierRoute returns the distribution module's querier route name.
func (AppModule) QuerierRoute() string {
return types.QuerierRoute
}
// LegacyQuerierHandler returns the distribution module sdk.Querier.
func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier {
return keeper.NewQuerier(am.keeper, legacyQuerierCdc)
}
// RegisterServices registers module services.
func (am AppModule) RegisterServices(cfg module.Configurator) {
types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
types.RegisterQueryServer(cfg.QueryServer(), am.keeper)
m := keeper.NewMigrator(am.keeper)
cfg.RegisterMigration(types.ModuleName, 1, m.Migrate1to2)
}
// InitGenesis performs genesis initialization for the distribution module. It returns
// no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState types.GenesisState
cdc.MustUnmarshalJSON(data, &genesisState)
am.keeper.InitGenesis(ctx, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis returns the exported genesis state as raw bytes for the distribution
// module.
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
gs := am.keeper.ExportGenesis(ctx)
return cdc.MustMarshalJSON(gs)
}
// ConsensusVersion implements AppModule/ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 { return 2 }
// BeginBlock returns the begin blocker for the distribution module.
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
BeginBlocker(ctx, req, am.keeper)
}
// EndBlock returns the end blocker for the distribution module. It returns no validator
// updates.
func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
// AppModuleSimulation functions
// GenerateGenesisState creates a randomized GenState of the distribution module.
func (AppModule) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// ProposalContents returns all the distribution content functions used to
// simulate governance proposals.
func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent {
return simulation.ProposalContents(am.keeper)
}
// RandomizedParams creates randomized distribution param changes for the simulator.
func (AppModule) RandomizedParams(r *rand.Rand) []simtypes.ParamChange {
return simulation.ParamChanges(r)
}
// RegisterStoreDecoder registers a decoder for distribution module's types
func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[types.StoreKey] = simulation.NewDecodeStore(am.cdc)
}
// WeightedOperations returns the all the gov module operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation {
return simulation.WeightedOperations(
simState.AppParams, simState.Cdc, am.accountKeeper, am.bankKeeper, am.keeper, am.stakingKeeper,
)
}
应用仿真器管理器
以下步骤是在应用程序级别设置SimulatorManager。这是下一步的模拟测试文件所必需的。
type CustomApp struct {
...
sm *module.SimulationManager
}
然后在应用程序的实例化中,我们以与创建ModuleManager相同的方式创建SimulationManager实例,但这次我们只从上述AppModuleSimulation接口传递实现模拟功能的模块。
func NewCustomApp(...) {
// create the simulation manager and define the order of the modules for deterministic simulations
app.sm = module.NewSimulationManager(
auth.NewAppModule(app.accountKeeper),
bank.NewAppModule(app.bankKeeper, app.accountKeeper),
supply.NewAppModule(app.supplyKeeper, app.accountKeeper),
ov.NewAppModule(app.govKeeper, app.accountKeeper, app.supplyKeeper),
mint.NewAppModule(app.mintKeeper),
distr.NewAppModule(app.distrKeeper, app.accountKeeper, app.supplyKeeper, app.stakingKeeper),
staking.NewAppModule(app.stakingKeeper, app.accountKeeper, app.supplyKeeper),
slashing.NewAppModule(app.slashingKeeper, app.accountKeeper, app.stakingKeeper),
)
// register the store decoders for simulation tests
app.sm.RegisterStoreDecoders()
...
}
原文链接:https://docs.cosmos.network/main/building-modules/simulator.html#simulation
---煤油灯科技victorlamp.com翻译整理---