Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
693 lines
18 KiB
Go
693 lines
18 KiB
Go
package chain
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/otiai10/copy"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/ignite/cli/v29/ignite/config"
|
|
chainconfig "github.com/ignite/cli/v29/ignite/config/chain"
|
|
"github.com/ignite/cli/v29/ignite/config/chain/defaults"
|
|
"github.com/ignite/cli/v29/ignite/pkg/cache"
|
|
chaincmdrunner "github.com/ignite/cli/v29/ignite/pkg/chaincmd/runner"
|
|
"github.com/ignite/cli/v29/ignite/pkg/cliui/colors"
|
|
"github.com/ignite/cli/v29/ignite/pkg/cliui/icons"
|
|
"github.com/ignite/cli/v29/ignite/pkg/cliui/view/accountview"
|
|
"github.com/ignite/cli/v29/ignite/pkg/cliui/view/errorview"
|
|
"github.com/ignite/cli/v29/ignite/pkg/cosmosfaucet"
|
|
"github.com/ignite/cli/v29/ignite/pkg/dirchange"
|
|
"github.com/ignite/cli/v29/ignite/pkg/errors"
|
|
"github.com/ignite/cli/v29/ignite/pkg/events"
|
|
"github.com/ignite/cli/v29/ignite/pkg/localfs"
|
|
"github.com/ignite/cli/v29/ignite/pkg/xexec"
|
|
"github.com/ignite/cli/v29/ignite/pkg/xfilepath"
|
|
"github.com/ignite/cli/v29/ignite/pkg/xhttp"
|
|
"github.com/ignite/cli/v29/ignite/pkg/xurl"
|
|
)
|
|
|
|
const (
|
|
// EvtGroupPath is the group to use for path related events.
|
|
EvtGroupPath = "path"
|
|
|
|
// exportedGenesis is the name of the exported genesis file for a chain.
|
|
exportedGenesis = "exported_genesis.json"
|
|
|
|
// sourceChecksumKey is the cache key for the checksum to detect source modification.
|
|
sourceChecksumKey = "source_checksum"
|
|
|
|
// binaryChecksumKey is the cache key for the checksum to detect binary modification.
|
|
binaryChecksumKey = "binary_checksum"
|
|
|
|
// configChecksumKey is the cache key for containing the checksum to detect config modification.
|
|
configChecksumKey = "config_checksum"
|
|
|
|
// serveDirchangeCacheNamespace is the name of the cache namespace for detecting changes in directories.
|
|
serveDirchangeCacheNamespace = "serve.dirchange"
|
|
)
|
|
|
|
var (
|
|
// ignoredExts holds a list of ignored files from watching.
|
|
ignoredExts = []string{"pb.go", "pb.gw.go"}
|
|
|
|
// starportSavePath is the place where chain exported genesis are saved.
|
|
starportSavePath = xfilepath.Join(
|
|
config.DirPath,
|
|
xfilepath.Path("local-chains"),
|
|
)
|
|
)
|
|
|
|
type serveOptions struct {
|
|
forceReset bool
|
|
resetOnce bool
|
|
skipProto bool
|
|
skipBuild bool
|
|
quitOnFail bool
|
|
generateClients bool
|
|
buildTags []string
|
|
}
|
|
|
|
func newServeOption() serveOptions {
|
|
return serveOptions{
|
|
forceReset: false,
|
|
resetOnce: false,
|
|
}
|
|
}
|
|
|
|
// ServeOption provides options for the serve command.
|
|
type ServeOption func(*serveOptions)
|
|
|
|
// ServeForceReset allows to force reset of the state when the chain is served and on every source change.
|
|
func ServeForceReset() ServeOption {
|
|
return func(c *serveOptions) {
|
|
c.forceReset = true
|
|
}
|
|
}
|
|
|
|
// ServeResetOnce allows to reset of the state when the chain is served once.
|
|
func ServeResetOnce() ServeOption {
|
|
return func(c *serveOptions) {
|
|
c.resetOnce = true
|
|
}
|
|
}
|
|
|
|
// QuitOnFail exits the serve immediately if an error occurs.
|
|
func QuitOnFail() ServeOption {
|
|
return func(c *serveOptions) {
|
|
c.quitOnFail = true
|
|
}
|
|
}
|
|
|
|
// GenerateClients enables client code generation.
|
|
func GenerateClients() ServeOption {
|
|
return func(c *serveOptions) {
|
|
c.generateClients = true
|
|
}
|
|
}
|
|
|
|
// ServeSkipProto allows to serve the app without generate Go from proto.
|
|
func ServeSkipProto() ServeOption {
|
|
return func(c *serveOptions) {
|
|
c.skipProto = true
|
|
}
|
|
}
|
|
|
|
// ServeSkipBuild allows to serve the app without rebuilding it.
|
|
// It looks up the binary in the PATH.
|
|
func ServeSkipBuild() ServeOption {
|
|
return func(c *serveOptions) {
|
|
c.skipBuild = true
|
|
}
|
|
}
|
|
|
|
// BuildTags set the build tags for the go build.
|
|
func BuildTags(buildTags ...string) ServeOption {
|
|
return func(c *serveOptions) {
|
|
c.buildTags = buildTags
|
|
}
|
|
}
|
|
|
|
// Serve serves an app.
|
|
func (c *Chain) Serve(ctx context.Context, cacheStorage cache.Storage, options ...ServeOption) error {
|
|
serveOptions := newServeOption()
|
|
|
|
// apply the options
|
|
for _, apply := range options {
|
|
apply(&serveOptions)
|
|
}
|
|
|
|
// initial checks and setup.
|
|
if err := c.setup(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// make sure that config.yml exists
|
|
if c.options.ConfigFile != "" {
|
|
if _, err := os.Stat(c.options.ConfigFile); err != nil {
|
|
return err
|
|
}
|
|
} else if _, err := chainconfig.LocateDefault(c.app.Path); err != nil {
|
|
return err
|
|
}
|
|
|
|
// start serving components.
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
|
|
// blockchain node routine
|
|
g.Go(func() error {
|
|
c.refreshServe()
|
|
|
|
for {
|
|
if ctx.Err() != nil {
|
|
return ctx.Err()
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
|
|
case <-c.serveRefresher:
|
|
commands, err := c.Commands(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
serveCtx context.Context
|
|
buildErr *CannotBuildAppError
|
|
startErr *CannotStartAppError
|
|
validationErr *chainconfig.ValidationError
|
|
)
|
|
|
|
serveCtx, c.serveCancel = context.WithCancel(ctx)
|
|
|
|
// determine if the chain should reset the state
|
|
shouldReset := serveOptions.forceReset || serveOptions.resetOnce
|
|
|
|
// serve the app.
|
|
err = c.serve(
|
|
serveCtx,
|
|
cacheStorage,
|
|
serveOptions.buildTags,
|
|
shouldReset,
|
|
serveOptions.skipProto,
|
|
serveOptions.skipBuild,
|
|
serveOptions.generateClients,
|
|
)
|
|
serveOptions.resetOnce = false
|
|
|
|
switch {
|
|
case err == nil:
|
|
case errors.Is(err, context.Canceled):
|
|
// If the app has been served, we save the genesis state
|
|
if c.served {
|
|
c.served = false
|
|
|
|
c.ev.Send("Saving genesis state...", events.ProgressStart())
|
|
|
|
// If serve has been stopped, save the genesis state
|
|
if err := c.saveChainState(context.TODO(), commands); err != nil {
|
|
c.ev.SendError(err, events.ProgressFinish())
|
|
return err
|
|
}
|
|
|
|
genesisPath, err := c.exportedGenesisPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Inform where the genesis file is saved without using
|
|
// progress update to keep the event text in the terminal.
|
|
c.ev.Send(
|
|
fmt.Sprintf("Genesis state saved in %s", genesisPath),
|
|
events.Icon(icons.CD),
|
|
)
|
|
}
|
|
case errors.As(err, &validationErr):
|
|
if serveOptions.quitOnFail {
|
|
return err
|
|
}
|
|
|
|
// Change error message to add a link to the configuration docs
|
|
err = errors.Errorf("%w\nsee: https://github.com/ignite/cli#configure", err)
|
|
|
|
c.ev.SendView(errorview.NewError(err), events.ProgressFinish(), events.Group(events.GroupError))
|
|
case errors.As(err, &buildErr):
|
|
if serveOptions.quitOnFail {
|
|
return err
|
|
}
|
|
|
|
c.ev.SendView(errorview.NewError(err), events.ProgressFinish(), events.Group(events.GroupError))
|
|
case errors.As(err, &startErr):
|
|
// Parse returned error logs
|
|
parsedErr := startErr.ParseStartError()
|
|
|
|
// If empty, we cannot recognized the error
|
|
// Therefore, the error may be caused by a new logic that is not compatible with the old app state
|
|
// We suggest the user to eventually reset the app state
|
|
if parsedErr == "" {
|
|
info := colors.Info(
|
|
"Blockchain failed to start.\n",
|
|
"If the new code is no longer compatible with the saved\n",
|
|
"state, you can reset the database by launching:\n",
|
|
"For more verbose logging, add -v to the command.",
|
|
)
|
|
command := colors.SprintFunc(colors.White)("ignite chain serve --reset-once -v")
|
|
return errors.Errorf("cannot run %s\n\n%s\n%s", startErr.AppName, info, command)
|
|
}
|
|
|
|
// return the clear parsed error
|
|
return errors.New(parsedErr)
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// routine to watch back-end
|
|
g.Go(func() error {
|
|
return c.watchAppBackend(ctx)
|
|
})
|
|
|
|
return g.Wait()
|
|
}
|
|
|
|
func (c *Chain) setup() error {
|
|
c.ev.Sendf("Cosmos SDK's version is: %s\n", colors.Info(c.Version))
|
|
|
|
return c.checkSystem()
|
|
}
|
|
|
|
// checkSystem checks if developer's work environment comply must to have
|
|
// dependencies and pre-conditions.
|
|
func (c *Chain) checkSystem() error {
|
|
// check if Go has installed.
|
|
if !xexec.IsCommandAvailable("go") {
|
|
return errors.New("Please, check that Go language is installed correctly in $PATH. See https://golang.org/doc/install")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Chain) refreshServe() {
|
|
if c.serveCancel != nil {
|
|
c.serveCancel()
|
|
}
|
|
// send event changes detected
|
|
c.serveRefresher <- struct{}{}
|
|
}
|
|
|
|
func (c *Chain) watchAppBackend(ctx context.Context) error {
|
|
watchPaths := appBackendSourceWatchPaths(defaults.ProtoDir)
|
|
|
|
if c.ConfigPath() != "" {
|
|
conf, err := c.Config()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
watchPaths = append(appBackendSourceWatchPaths(conf.Build.Proto.Path), c.ConfigPath())
|
|
}
|
|
|
|
return localfs.Watch(
|
|
ctx,
|
|
watchPaths,
|
|
localfs.WatcherWorkdir(c.app.Path),
|
|
localfs.WatcherOnChange(c.refreshServe),
|
|
localfs.WatcherIgnoreHidden(),
|
|
localfs.WatcherIgnoreFolders(),
|
|
localfs.WatcherIgnoreExt(ignoredExts...),
|
|
)
|
|
}
|
|
|
|
// serve performs the operations to serve the blockchain: build, init and start.
|
|
// If the chain is already initialized and the file weren't changed, the app is directly started.
|
|
// If the files changed, the state is imported.
|
|
func (c *Chain) serve(
|
|
ctx context.Context,
|
|
cacheStorage cache.Storage,
|
|
buildTags []string,
|
|
forceReset, skipProto, skipBuild, generateClients bool,
|
|
) error {
|
|
conf, err := c.Config()
|
|
if err != nil {
|
|
return &CannotBuildAppError{err}
|
|
}
|
|
|
|
sourceWatchPaths := appBackendSourceWatchPaths(conf.Build.Proto.Path)
|
|
|
|
commands, err := c.Commands(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// isInit determines if the app is initialized
|
|
var isInit bool
|
|
|
|
dirCache := cache.New[[]byte](cacheStorage, serveDirchangeCacheNamespace)
|
|
|
|
// determine if the app must reset the state
|
|
// if the state must be reset, then we consider the chain as being not initialized
|
|
isInit, err = c.IsInitialized()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isInit {
|
|
configModified := false
|
|
if c.ConfigPath() != "" {
|
|
configModified, err = dirchange.HasDirChecksumChanged(dirCache, configChecksumKey, c.app.Path, c.ConfigPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if forceReset || configModified {
|
|
// if forceReset is set, we consider the app as being not initialized
|
|
c.ev.Send("Resetting the app state...", events.ProgressUpdate())
|
|
isInit = false
|
|
}
|
|
}
|
|
|
|
// check if source has been modified since last serve
|
|
// if the state must not be reset but the source has changed, we rebuild the chain and import the exported state
|
|
sourceModified, err := dirchange.HasDirChecksumChanged(dirCache, sourceChecksumKey, c.app.Path, sourceWatchPaths...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// we also consider the binary in the checksum to ensure the binary has not been changed by a third party
|
|
var binaryModified bool
|
|
binaryName, err := c.Binary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
binaryPath, err := xexec.ResolveAbsPath(binaryName)
|
|
if err != nil {
|
|
if !errors.Is(err, exec.ErrNotFound) {
|
|
return err
|
|
}
|
|
binaryModified = true
|
|
} else {
|
|
binaryModified, err = dirchange.HasDirChecksumChanged(dirCache, binaryChecksumKey, "", binaryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
appModified := sourceModified || binaryModified
|
|
|
|
// check if exported genesis exists
|
|
exportGenesisExists := true
|
|
exportedGenesisPath, err := c.exportedGenesisPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := os.Stat(exportedGenesisPath); os.IsNotExist(err) {
|
|
exportGenesisExists = false
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
// build phase
|
|
// if the app is not initialized or the source/binary has been modified
|
|
// and if the --skip-build flag is not set
|
|
if skipBuild {
|
|
c.ev.SendInfo("Skip building activated. Binary won't be rebuilt, nor refresh on changes")
|
|
}
|
|
|
|
if (!isInit || appModified) && !skipBuild {
|
|
// build the blockchain app
|
|
if err := c.build(ctx, cacheStorage, buildTags, "", skipProto, generateClients, true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// init phase
|
|
initApp := !isInit || (appModified && !exportGenesisExists)
|
|
|
|
//nolint:gocritic
|
|
if initApp {
|
|
c.ev.Send("Initializing the app...", events.ProgressUpdate())
|
|
|
|
if err := c.Init(ctx, InitArgsAll); err != nil {
|
|
return err
|
|
}
|
|
} else if appModified {
|
|
// if the chain is already initialized but the source has been modified
|
|
// we reset the chain database and import the genesis state
|
|
c.ev.Send("Existent genesis detected, restoring the database...", events.ProgressUpdate())
|
|
|
|
if err := commands.UnsafeReset(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.importChainState(); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
c.ev.Send("Restarting existing app...", events.ProgressUpdate())
|
|
}
|
|
|
|
// save checksums
|
|
if c.ConfigPath() != "" {
|
|
if err := dirchange.SaveDirChecksum(dirCache, configChecksumKey, c.app.Path, c.ConfigPath()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := dirchange.SaveDirChecksum(dirCache, sourceChecksumKey, c.app.Path, sourceWatchPaths...); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := dirchange.SaveDirChecksum(dirCache, binaryChecksumKey, "", binaryPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Display existing accounts if they were not initialized.
|
|
// Note that chain init displays accounts when the app is initialized.
|
|
if !initApp {
|
|
accounts, err := commands.ListAccounts(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var view accountview.Accounts
|
|
for _, a := range accounts {
|
|
view = view.Append(accountview.NewAccount(a.Name, a.Address))
|
|
}
|
|
|
|
c.ev.SendView(view, events.ProgressFinish())
|
|
}
|
|
|
|
// start the blockchain
|
|
return c.start(ctx, conf)
|
|
}
|
|
|
|
func (c *Chain) start(ctx context.Context, cfg *chainconfig.Config) error {
|
|
commands, err := c.Commands(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
|
|
// start the blockchain.
|
|
g.Go(func() error { return c.Start(ctx, commands, cfg) })
|
|
|
|
// start the faucet if enabled.
|
|
faucet, err := c.Faucet(ctx)
|
|
isFaucetEnabled := !errors.Is(err, ErrFaucetIsNotEnabled)
|
|
|
|
if isFaucetEnabled {
|
|
if errors.Is(err, ErrFaucetAccountDoesNotExist) {
|
|
return &CannotBuildAppError{errors.Wrap(err, "faucet account doesn't exist")}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g.Go(func() (err error) {
|
|
if err := c.runFaucetServer(ctx, faucet); err != nil {
|
|
return &CannotBuildAppError{err}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// set the app as being served
|
|
c.served = true
|
|
|
|
validator, err := chainconfig.FirstValidator(cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
servers, err := validator.GetServers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// note: address format errors are handled by the
|
|
// error group, so they can be safely ignored here
|
|
|
|
rpcAddr, _ := xurl.HTTP(servers.RPC.Address)
|
|
apiAddr, _ := xurl.HTTP(servers.API.Address)
|
|
|
|
c.ev.Send(
|
|
fmt.Sprintf("Tendermint node: %s", rpcAddr),
|
|
events.Icon(icons.Earth),
|
|
events.ProgressFinish(),
|
|
)
|
|
c.ev.Send(
|
|
fmt.Sprintf("Blockchain API: %s", apiAddr),
|
|
events.Icon(icons.Earth),
|
|
)
|
|
|
|
if isFaucetEnabled {
|
|
faucetAddr, _ := xurl.HTTP(chainconfig.FaucetHost(cfg))
|
|
|
|
c.ev.Send(
|
|
fmt.Sprintf("Token faucet: %s", faucetAddr),
|
|
events.Icon(icons.Earth),
|
|
)
|
|
}
|
|
|
|
appHome, _ := c.Home()
|
|
appBin, _ := c.AbsBinaryPath()
|
|
|
|
c.ev.Send(
|
|
fmt.Sprintf("Data directory: %s", colors.Faint(appHome)),
|
|
events.Icon(icons.Bullet),
|
|
events.Group(EvtGroupPath),
|
|
)
|
|
c.ev.Send(
|
|
fmt.Sprintf("App binary: %s", colors.Faint(appBin)),
|
|
events.Icon(icons.Bullet),
|
|
events.Group(EvtGroupPath),
|
|
)
|
|
|
|
return g.Wait()
|
|
}
|
|
|
|
func (c *Chain) runFaucetServer(ctx context.Context, faucet cosmosfaucet.Faucet) error {
|
|
cfg, err := c.Config()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return xhttp.Serve(ctx, &http.Server{
|
|
Addr: chainconfig.FaucetHost(cfg),
|
|
Handler: faucet,
|
|
ReadHeaderTimeout: 5 * time.Second, // Set a reasonable timeout
|
|
})
|
|
}
|
|
|
|
// saveChainState runs the export command of the chain and store the exported genesis in the chain saved config.
|
|
func (c *Chain) saveChainState(ctx context.Context, commands chaincmdrunner.Runner) error {
|
|
genesisPath, err := c.exportedGenesisPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return commands.Export(ctx, genesisPath)
|
|
}
|
|
|
|
// importChainState imports the saved genesis in chain config to use it as the genesis.
|
|
func (c *Chain) importChainState() error {
|
|
exportGenesisPath, err := c.exportedGenesisPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
genesisPath, err := c.GenesisPath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return copy.Copy(exportGenesisPath, genesisPath)
|
|
}
|
|
|
|
// chainSavePath returns the path where the chain state is saved.
|
|
// Creates the path if it doesn't exist.
|
|
func (c *Chain) chainSavePath() (string, error) {
|
|
savePath, err := starportSavePath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
chainID, err := c.ID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
chainSavePath := filepath.Join(savePath, chainID)
|
|
|
|
// ensure the path exists
|
|
if err := os.MkdirAll(savePath, 0o700); err != nil && !os.IsExist(err) {
|
|
return "", err
|
|
}
|
|
|
|
return chainSavePath, nil
|
|
}
|
|
|
|
// exportedGenesisPath returns the path of the exported genesis file.
|
|
func (c *Chain) exportedGenesisPath() (string, error) {
|
|
savePath, err := c.chainSavePath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(savePath, exportedGenesis), nil
|
|
}
|
|
|
|
type CannotBuildAppError struct {
|
|
Err error
|
|
}
|
|
|
|
func (e *CannotBuildAppError) Error() string {
|
|
// TODO: Find at which point the error is wrapped twice
|
|
var buildErr *CannotBuildAppError
|
|
if errors.As(e.Err, &buildErr) {
|
|
return buildErr.Error()
|
|
}
|
|
|
|
return fmt.Sprintf("cannot build app:\n\n%s", e.Err)
|
|
}
|
|
|
|
func (e *CannotBuildAppError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
type CannotStartAppError struct {
|
|
AppName string
|
|
Err error
|
|
}
|
|
|
|
func (e *CannotStartAppError) Error() string {
|
|
return fmt.Sprintf("cannot run %sd start:\n%s", e.AppName, errors.Unwrap(e.Err))
|
|
}
|
|
|
|
func (e *CannotStartAppError) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
// ParseStartError parses the error into a clear error string.
|
|
// The error logs from Cosmos SDK application are too extensive to be directly printed.
|
|
// If the error is not recognized, returns an empty string.
|
|
func (e *CannotStartAppError) ParseStartError() string {
|
|
errorLogs := errors.Unwrap(e.Err).Error()
|
|
switch {
|
|
case strings.Contains(errorLogs, "bind: address already in use"):
|
|
r := regexp.MustCompile(`listen .* bind: address already in use`)
|
|
return r.FindString(errorLogs)
|
|
case strings.Contains(errorLogs, "validator set is nil in genesis"):
|
|
return "Error: error during handshake: error on replay: validator set is nil in genesis and still empty after InitChain"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|