package runtime import ( "context" "encoding/json" "errors" "fmt" "io" "cosmossdk.io/core/appmodule" appmodulev2 "cosmossdk.io/core/appmodule/v2" "cosmossdk.io/core/store" "cosmossdk.io/core/transaction" "cosmossdk.io/runtime/v2/services" "cosmossdk.io/server/v2/appmanager" "cosmossdk.io/server/v2/stf" "cosmossdk.io/server/v2/stf/branch" "cosmossdk.io/store/v2/root" ) // AppBuilder is a type that is injected into a container by the runtime/v2 module // (as *AppBuilder) which can be used to create an app which is compatible with // the existing app.go initialization conventions. type AppBuilder[T transaction.Tx] struct { app *App[T] storeBuilder root.Builder storeConfig *root.Config // the following fields are used to overwrite the default branch func(state store.ReaderMap) store.WriterMap txValidator func(ctx context.Context, tx T) error postTxExec func(ctx context.Context, tx T, success bool) error preblocker func(ctx context.Context, txs []T, mmPreblocker func() error) error } // RegisterModules registers the provided modules with the module manager. // This is the primary hook for integrating with modules which are not registered using the app config. func (a *AppBuilder[T]) RegisterModules(modules map[string]appmodulev2.AppModule) error { for name, appModule := range modules { // if a (legacy) module implements the HasName interface, check that the name matches if mod, ok := appModule.(interface{ Name() string }); ok { if name != mod.Name() { a.app.logger.Warn(fmt.Sprintf("module name %q does not match name returned by HasName: %q", name, mod.Name())) } } if _, ok := a.app.moduleManager.modules[name]; ok { return fmt.Errorf("module named %q already exists", name) } a.app.moduleManager.modules[name] = appModule if mod, ok := appModule.(appmodulev2.HasRegisterInterfaces); ok { mod.RegisterInterfaces(a.app.interfaceRegistrar) } if mod, ok := appModule.(appmodule.HasAminoCodec); ok { mod.RegisterLegacyAminoCodec(a.app.amino) } } return nil } // Build builds an *App instance. func (a *AppBuilder[T]) Build(opts ...AppBuilderOption[T]) (*App[T], error) { for _, opt := range opts { opt(a) } // default branch if a.branch == nil { a.branch = branch.DefaultNewWriterMap } // default tx validator if a.txValidator == nil { a.txValidator = a.app.moduleManager.TxValidators() } // default post tx exec if a.postTxExec == nil { a.postTxExec = func(ctx context.Context, tx T, success bool) error { return nil } } var err error a.app.db, err = a.storeBuilder.Build(a.app.logger, a.storeConfig) if err != nil { return nil, err } if err = a.app.moduleManager.RegisterServices(a.app); err != nil { return nil, err } endBlocker, valUpdate := a.app.moduleManager.EndBlock() preblockerFn := func(ctx context.Context, txs []T) error { if a.preblocker != nil { return a.preblocker(ctx, txs, func() error { return a.app.moduleManager.PreBlocker()(ctx, txs) }) } // if there is no preblocker set, call the module manager's preblocker directly return a.app.moduleManager.PreBlocker()(ctx, txs) } stf, err := stf.New[T]( a.app.logger.With("module", "stf"), a.app.msgRouterBuilder, a.app.queryRouterBuilder, preblockerFn, a.app.moduleManager.BeginBlock(), endBlocker, a.txValidator, valUpdate, a.postTxExec, a.branch, ) if err != nil { return nil, fmt.Errorf("failed to create STF: %w", err) } a.app.stf = stf a.app.AppManager = appmanager.New( appmanager.Config{ ValidateTxGasLimit: a.app.config.GasConfig.ValidateTxGasLimit, QueryGasLimit: a.app.config.GasConfig.QueryGasLimit, SimulationGasLimit: a.app.config.GasConfig.SimulationGasLimit, }, a.app.db, a.app.stf, a.initGenesis, a.exportGenesis, ) return a.app, nil } // initGenesis returns the app initialization genesis for modules func (a *AppBuilder[T]) initGenesis(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) (store.WriterMap, []appmodule.ValidatorUpdate, error) { // this implementation assumes that the state is a JSON object bz, err := io.ReadAll(src) if err != nil { return nil, nil, fmt.Errorf("failed to read import state: %w", err) } var genesisJSON map[string]json.RawMessage if err = json.Unmarshal(bz, &genesisJSON); err != nil { return nil, nil, err } v, zeroState, err := a.app.db.StateLatest() if err != nil { return nil, nil, fmt.Errorf("unable to get latest state: %w", err) } if v != 0 { // TODO: genesis state may be > 0, we need to set version on store return nil, nil, errors.New("cannot init genesis on non-zero state") } genesisCtx := services.NewGenesisContext(a.branch(zeroState)) var valUpdates []appmodulev2.ValidatorUpdate genesisState, err := genesisCtx.Mutate(ctx, func(ctx context.Context) error { valUpdates, err = a.app.moduleManager.InitGenesisJSON(ctx, genesisJSON, txHandler) if err != nil { return fmt.Errorf("failed to init genesis: %w", err) } return nil }) return genesisState, valUpdates, err } // exportGenesis returns the app export genesis logic for modules func (a *AppBuilder[T]) exportGenesis(ctx context.Context, version uint64) ([]byte, error) { state, err := a.app.db.StateAt(version) if err != nil { return nil, fmt.Errorf("unable to get state at given version: %w", err) } genesisJson, err := a.app.moduleManager.ExportGenesisForModules( ctx, func() store.WriterMap { return a.branch(state) }, ) if err != nil { return nil, fmt.Errorf("failed to export genesis: %w", err) } bz, err := json.Marshal(genesisJson) if err != nil { return nil, fmt.Errorf("failed to marshal genesis: %w", err) } return bz, nil } // AppBuilderOption is a function that can be passed to AppBuilder.Build to customize the resulting app. type AppBuilderOption[T transaction.Tx] func(*AppBuilder[T]) // AppBuilderWithBranch sets a custom branch implementation for the app. func AppBuilderWithBranch[T transaction.Tx](branch func(state store.ReaderMap) store.WriterMap) AppBuilderOption[T] { return func(a *AppBuilder[T]) { a.branch = branch } } // AppBuilderWithTxValidator sets the tx validator for the app. // It overrides all default tx validators defined by modules. func AppBuilderWithTxValidator[T transaction.Tx]( txValidators func( ctx context.Context, tx T, ) error, ) AppBuilderOption[T] { return func(a *AppBuilder[T]) { a.txValidator = txValidators } } // AppBuilderWithPostTxExec sets logic that will be executed after each transaction. // When not provided, a no-op function will be used. func AppBuilderWithPostTxExec[T transaction.Tx]( postTxExec func( ctx context.Context, tx T, success bool, ) error, ) AppBuilderOption[T] { return func(a *AppBuilder[T]) { a.postTxExec = postTxExec } } // AppBuilderWithPreblocker sets logic that will be executed before each block. // mmPreblocker can be used to call module manager's preblocker, so that it can be // called before or after depending on the app's logic. // This is especially useful when implementing vote extensions. func AppBuilderWithPreblocker[T transaction.Tx]( preblocker func( ctx context.Context, txs []T, mmPreblocker func() error, ) error, ) AppBuilderOption[T] { return func(a *AppBuilder[T]) { a.preblocker = preblocker } }