diff --git a/changelog.md b/changelog.md index e04bd90630..a5eb196c5a 100644 --- a/changelog.md +++ b/changelog.md @@ -33,6 +33,8 @@ - Add `--skip-proto` flag to `build`, `init` and `serve` commands to build the chain without building proto files - Add `node query tx` command to query a transaction in any chain. - Add `node query bank` command to query an account's bank balance in any chain. +- Add `node tx bank send` command to send funds from one account to an other in any chain. +- Add migration system for the config file to allow config versioning - Add `node tx bank send` command to send funds from one account to another in any chain. - Implement `network profile` command - Add `generate ts-client` command to generate a stand-alone modular TypeScript client. diff --git a/ignite/chainconfig/chainconfig.go b/ignite/chainconfig/chainconfig.go new file mode 100644 index 0000000000..f38ce62e60 --- /dev/null +++ b/ignite/chainconfig/chainconfig.go @@ -0,0 +1,89 @@ +package chainconfig + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/ignite/cli/ignite/chainconfig/config" + v0 "github.com/ignite/cli/ignite/chainconfig/v0" + v1 "github.com/ignite/cli/ignite/chainconfig/v1" + "github.com/ignite/cli/ignite/pkg/xfilepath" +) + +var ( + // ConfigDirPath returns the path of configuration directory of Ignite. + ConfigDirPath = xfilepath.JoinFromHome(xfilepath.Path(".ignite")) + + // ConfigFileNames is a list of recognized names as for Ignite's config file. + ConfigFileNames = []string{"config.yml", "config.yaml"} + + // DefaultTSClientPath defines the default relative path to use when generating the TS client. + // The path is relative to the app's directory. + DefaultTSClientPath = "ts-client" + + // LatestVersion defines the latest version of the config. + LatestVersion config.Version = 1 + + // Versions holds config types for the supported versions. + Versions = map[config.Version]config.Converter{ + 0: &v0.Config{}, + 1: &v1.Config{}, + } +) + +// Config defines the latest config. +type Config = v1.Config + +// DefaultConfig returns a config for the latest version initialized with default values. +func DefaultConfig() *Config { + return v1.DefaultConfig() +} + +// FaucetHost returns the faucet host to use. +func FaucetHost(cfg *Config) string { + // We keep supporting Port option for backward compatibility + // TODO: drop this option in the future + host := cfg.Faucet.Host + if cfg.Faucet.Port != 0 { + host = fmt.Sprintf(":%d", cfg.Faucet.Port) + } + + return host +} + +// TSClientPath returns the relative path to the Typescript client directory. +// Path is relative to the app's directory. +func TSClientPath(conf *Config) string { + if path := strings.TrimSpace(conf.Client.Typescript.Path); path != "" { + return filepath.Clean(path) + } + + return DefaultTSClientPath +} + +// CreateConfigDir creates config directory if it is not created yet. +func CreateConfigDir() error { + path, err := ConfigDirPath() + if err != nil { + return err + } + + return os.MkdirAll(path, 0o755) +} + +// LocateDefault locates the default path for the config file. +// Returns ErrConfigNotFound when no config file found. +func LocateDefault(root string) (path string, err error) { + for _, name := range ConfigFileNames { + path = filepath.Join(root, name) + if _, err := os.Stat(path); err == nil { + return path, nil + } else if !os.IsNotExist(err) { + return "", err + } + } + + return "", ErrConfigNotFound +} diff --git a/ignite/chainconfig/config.go b/ignite/chainconfig/config.go deleted file mode 100644 index b2d8b61a69..0000000000 --- a/ignite/chainconfig/config.go +++ /dev/null @@ -1,293 +0,0 @@ -package chainconfig - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/goccy/go-yaml" - "github.com/imdario/mergo" - - "github.com/ignite/cli/ignite/pkg/xfilepath" -) - -const ( - // DefaultTSClientPath defines the default relative path to use when generating the TS client. - // The path is relative to the app's directory. - DefaultTSClientPath = "ts-client" -) - -var ( - // ConfigDirPath returns the path of configuration directory of Ignite. - ConfigDirPath = xfilepath.JoinFromHome(xfilepath.Path(".ignite")) - - // ConfigFileNames is a list of recognized names as for Ignite's config file. - ConfigFileNames = []string{"config.yml", "config.yaml"} -) - -// ErrCouldntLocateConfig returned when config.yml cannot be found in the source code. -var ErrCouldntLocateConfig = errors.New( - "could not locate a config.yml in your chain. please follow the link for" + - "how-to: https://github.com/ignite/cli/blob/develop/docs/configure/index.md") - -// DefaultConf holds default configuration. -var DefaultConf = Config{ - Host: Host{ - // when in Docker on MacOS, it only works with 0.0.0.0. - RPC: "0.0.0.0:26657", - P2P: "0.0.0.0:26656", - Prof: "0.0.0.0:6060", - GRPC: "0.0.0.0:9090", - GRPCWeb: "0.0.0.0:9091", - API: "0.0.0.0:1317", - }, - Build: Build{ - Proto: Proto{ - Path: "proto", - ThirdPartyPaths: []string{ - "third_party/proto", - "proto_vendor", - }, - }, - }, - Faucet: Faucet{ - Host: "0.0.0.0:4500", - }, -} - -// Config is the user given configuration to do additional setup -// during serve. -type Config struct { - Accounts []Account `yaml:"accounts"` - Validator Validator `yaml:"validator"` - Faucet Faucet `yaml:"faucet"` - Client Client `yaml:"client"` - Build Build `yaml:"build"` - Init Init `yaml:"init"` - Genesis map[string]interface{} `yaml:"genesis"` - Host Host `yaml:"host"` -} - -// AccountByName finds account by name. -func (c Config) AccountByName(name string) (acc Account, found bool) { - for _, acc := range c.Accounts { - if acc.Name == name { - return acc, true - } - } - return Account{}, false -} - -// Account holds the options related to setting up Cosmos wallets. -type Account struct { - Name string `yaml:"name"` - Coins []string `yaml:"coins,omitempty"` - Mnemonic string `yaml:"mnemonic,omitempty"` - Address string `yaml:"address,omitempty"` - CoinType string `yaml:"cointype,omitempty"` - - // The RPCAddress off the chain that account is issued at. - RPCAddress string `yaml:"rpc_address,omitempty"` -} - -// Validator holds info related to validator settings. -type Validator struct { - Name string `yaml:"name"` - Staked string `yaml:"staked"` -} - -// Build holds build configs. -type Build struct { - Main string `yaml:"main"` - Binary string `yaml:"binary"` - LDFlags []string `yaml:"ldflags"` - Proto Proto `yaml:"proto"` -} - -// Proto holds proto build configs. -type Proto struct { - // Path is the relative path of where app's proto files are located at. - Path string `yaml:"path"` - - // ThirdPartyPath is the relative path of where the third party proto files are - // located that used by the app. - ThirdPartyPaths []string `yaml:"third_party_paths"` -} - -// Client configures code generation for clients. -type Client struct { - // TSClient configures code generation for Typescript Client. - Typescript Typescript `yaml:"typescript"` - - // Vuex configures code generation for Vuex stores. - Vuex Typescript `yaml:"vuex"` - - // Dart configures client code generation for Dart. - Dart Dart `yaml:"dart"` - - // OpenAPI configures OpenAPI spec generation for API. - OpenAPI OpenAPI `yaml:"openapi"` -} - -// TSClient configures code generation for Typescript Client. -type Typescript struct { - // Path configures out location for generated Typescript Client code. - Path string `yaml:"path"` -} - -// Vuex configures code generation for Vuex stores. -type Vuex struct { - // Path configures out location for generated Vuex stores code. - Path string `yaml:"path"` -} - -// Dart configures client code generation for Dart. -type Dart struct { - // Path configures out location for generated Dart code. - Path string `yaml:"path"` -} - -// OpenAPI configures OpenAPI spec generation for API. -type OpenAPI struct { - Path string `yaml:"path"` -} - -// Faucet configuration. -type Faucet struct { - // Name is faucet account's name. - Name *string `yaml:"name"` - - // Coins holds type of coin denoms and amounts to distribute. - Coins []string `yaml:"coins"` - - // CoinsMax holds of chain denoms and their max amounts that can be transferred - // to single user. - CoinsMax []string `yaml:"coins_max"` - - // LimitRefreshTime sets the timeframe at the end of which the limit will be refreshed - RateLimitWindow string `yaml:"rate_limit_window"` - - // Host is the host of the faucet server - Host string `yaml:"host"` - - // Port number for faucet server to listen at. - Port int `yaml:"port"` -} - -// Init overwrites sdk configurations with given values. -type Init struct { - // App overwrites appd's config/app.toml configs. - App map[string]interface{} `yaml:"app"` - - // Client overwrites appd's config/client.toml configs. - Client map[string]interface{} `yaml:"client"` - - // Config overwrites appd's config/config.toml configs. - Config map[string]interface{} `yaml:"config"` - - // Home overwrites default home directory used for the app - Home string `yaml:"home"` - - // KeyringBackend is the default keyring backend to use for blockchain initialization - KeyringBackend string `yaml:"keyring-backend"` -} - -// Host keeps configuration related to started servers. -type Host struct { - RPC string `yaml:"rpc"` - P2P string `yaml:"p2p"` - Prof string `yaml:"prof"` - GRPC string `yaml:"grpc"` - GRPCWeb string `yaml:"grpc-web"` - API string `yaml:"api"` -} - -// Parse parses config.yml into UserConfig. -func Parse(r io.Reader) (Config, error) { - var conf Config - if err := yaml.NewDecoder(r).Decode(&conf); err != nil { - return conf, err - } - if err := mergo.Merge(&conf, DefaultConf); err != nil { - return Config{}, err - } - return conf, validate(conf) -} - -// ParseFile parses config.yml from the path. -func ParseFile(path string) (Config, error) { - file, err := os.Open(path) - if err != nil { - return Config{}, nil - } - defer file.Close() - return Parse(file) -} - -// validate validates user config. -func validate(conf Config) error { - if len(conf.Accounts) == 0 { - return &ValidationError{"at least 1 account is needed"} - } - if conf.Validator.Name == "" { - return &ValidationError{"validator is required"} - } - return nil -} - -// ValidationError is returned when a configuration is invalid. -type ValidationError struct { - Message string -} - -func (e *ValidationError) Error() string { - return fmt.Sprintf("config is not valid: %s", e.Message) -} - -// LocateDefault locates the default path for the config file, if no file found returns ErrCouldntLocateConfig. -func LocateDefault(root string) (path string, err error) { - for _, name := range ConfigFileNames { - path = filepath.Join(root, name) - if _, err := os.Stat(path); err == nil { - return path, nil - } else if !os.IsNotExist(err) { - return "", err - } - } - return "", ErrCouldntLocateConfig -} - -// FaucetHost returns the faucet host to use. -func FaucetHost(conf Config) string { - // We keep supporting Port option for backward compatibility - // TODO: drop this option in the future - host := conf.Faucet.Host - if conf.Faucet.Port != 0 { - host = fmt.Sprintf(":%d", conf.Faucet.Port) - } - - return host -} - -// TSClientPath returns the relative path to the Typescript client directory. -// Path is relative to the app's directory. -func TSClientPath(conf Config) string { - if path := strings.TrimSpace(conf.Client.Typescript.Path); path != "" { - return filepath.Clean(path) - } - - return DefaultTSClientPath -} - -// CreateConfigDir creates config directory if it is not created yet. -func CreateConfigDir() error { - confPath, err := ConfigDirPath() - if err != nil { - return err - } - - return os.MkdirAll(confPath, 0o755) -} diff --git a/ignite/chainconfig/config/config.go b/ignite/chainconfig/config/config.go new file mode 100644 index 0000000000..2cfc60ca3f --- /dev/null +++ b/ignite/chainconfig/config/config.go @@ -0,0 +1,186 @@ +package config + +import ( + "io" + + "github.com/imdario/mergo" + + xyaml "github.com/ignite/cli/ignite/pkg/yaml" +) + +// Version defines the type for the config version number. +type Version uint + +// Converter defines the interface required to migrate configurations to newer versions. +type Converter interface { + // Clone clones the config by returning a new copy of the current one. + Clone() (Converter, error) + + // SetDefaults assigns default values to empty config fields. + SetDefaults() error + + // GetVersion returns the config version. + GetVersion() Version + + // ConvertNext converts the config to the next version. + ConvertNext() (Converter, error) + + // Decode decodes the config file from YAML and updates it's values. + Decode(io.Reader) error +} + +// Account holds the options related to setting up Cosmos wallets. +type Account struct { + Name string `yaml:"name"` + Coins []string `yaml:"coins,omitempty"` + Mnemonic string `yaml:"mnemonic,omitempty"` + Address string `yaml:"address,omitempty"` + CoinType string `yaml:"cointype,omitempty"` + + // The RPCAddress off the chain that account is issued at. + RPCAddress string `yaml:"rpc_address,omitempty"` +} + +// Build holds build configs. +type Build struct { + Main string `yaml:"main,omitempty"` + Binary string `yaml:"binary,omitempty"` + LDFlags []string `yaml:"ldflags,omitempty"` + Proto Proto `yaml:"proto"` +} + +// Proto holds proto build configs. +type Proto struct { + // Path is the relative path of where app's proto files are located at. + Path string `yaml:"path"` + + // ThirdPartyPath is the relative path of where the third party proto files are + // located that used by the app. + ThirdPartyPaths []string `yaml:"third_party_paths"` +} + +// Client configures code generation for clients. +type Client struct { + // TSClient configures code generation for Typescript Client. + Typescript Typescript `yaml:"typescript,omitempty"` + + // Vuex configures code generation for Vuex stores. + Vuex Typescript `yaml:"vuex,omitempty"` + + // Dart configures client code generation for Dart. + Dart Dart `yaml:"dart,omitempty"` + + // OpenAPI configures OpenAPI spec generation for API. + OpenAPI OpenAPI `yaml:"openapi,omitempty"` +} + +// TSClient configures code generation for Typescript Client. +type Typescript struct { + // Path configures out location for generated Typescript Client code. + Path string `yaml:"path"` +} + +// Vuex configures code generation for Vuex stores. +type Vuex struct { + // Path configures out location for generated Vuex stores code. + Path string `yaml:"path"` +} + +// Dart configures client code generation for Dart. +type Dart struct { + // Path configures out location for generated Dart code. + Path string `yaml:"path"` +} + +// OpenAPI configures OpenAPI spec generation for API. +type OpenAPI struct { + Path string `yaml:"path"` +} + +// Faucet configuration. +type Faucet struct { + // Name is faucet account's name. + Name *string `yaml:"name"` + + // Coins holds type of coin denoms and amounts to distribute. + Coins []string `yaml:"coins"` + + // CoinsMax holds of chain denoms and their max amounts that can be transferred to single user. + CoinsMax []string `yaml:"coins_max,omitempty"` + + // LimitRefreshTime sets the timeframe at the end of which the limit will be refreshed + RateLimitWindow string `yaml:"rate_limit_window,omitempty"` + + // Host is the host of the faucet server + Host string `yaml:"host,omitempty"` + + // Port number for faucet server to listen at. + Port int `yaml:"port,omitempty"` +} + +// Init overwrites sdk configurations with given values. +type Init struct { + // App overwrites appd's config/app.toml configs. + App xyaml.Map `yaml:"app"` + + // Client overwrites appd's config/client.toml configs. + Client xyaml.Map `yaml:"client"` + + // Config overwrites appd's config/config.toml configs. + Config xyaml.Map `yaml:"config"` + + // Home overwrites default home directory used for the app + Home string `yaml:"home"` + + // KeyringBackend is the default keyring backend to use for blockchain initialization + KeyringBackend string `yaml:"keyring-backend"` +} + +// Host keeps configuration related to started servers. +type Host struct { + RPC string `yaml:"rpc"` + P2P string `yaml:"p2p"` + Prof string `yaml:"prof"` + GRPC string `yaml:"grpc"` + GRPCWeb string `yaml:"grpc-web"` + API string `yaml:"api"` +} + +// BaseConfig defines a struct with the fields that are common to all config versions. +type BaseConfig struct { + Version Version `yaml:"version"` + Build Build `yaml:"build"` + Accounts []Account `yaml:"accounts"` + Faucet Faucet `yaml:"faucet,omitempty"` + Client Client `yaml:"client,omitempty"` + Genesis xyaml.Map `yaml:"genesis,omitempty"` +} + +// GetVersion returns the config version. +func (c BaseConfig) GetVersion() Version { + return c.Version +} + +// SetDefaults assigns default values to empty config fields. +func (c *BaseConfig) SetDefaults() error { + if err := mergo.Merge(c, DefaultBaseConfig()); err != nil { + return err + } + + return nil +} + +// DefaultBaseConfig returns a base config with default values. +func DefaultBaseConfig() BaseConfig { + return BaseConfig{ + Build: Build{ + Proto: Proto{ + Path: "proto", + ThirdPartyPaths: []string{"third_party/proto", "proto_vendor"}, + }, + }, + Faucet: Faucet{ + Host: "0.0.0.0:4500", + }, + } +} diff --git a/ignite/chainconfig/config_test.go b/ignite/chainconfig/config_test.go deleted file mode 100644 index f6c19b0903..0000000000 --- a/ignite/chainconfig/config_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package chainconfig - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestParse(t *testing.T) { - confyml := ` -accounts: - - name: me - coins: ["1000token", "100000000stake"] - - name: you - coins: ["5000token"] -validator: - name: user1 - staked: "100000000stake" -` - - conf, err := Parse(strings.NewReader(confyml)) - - require.NoError(t, err) - require.Equal(t, []Account{ - { - Name: "me", - Coins: []string{"1000token", "100000000stake"}, - }, - { - Name: "you", - Coins: []string{"5000token"}, - }, - }, conf.Accounts) - require.Equal(t, Validator{ - Name: "user1", - Staked: "100000000stake", - }, conf.Validator) -} - -func TestCoinTypeParse(t *testing.T) { - confyml := ` -accounts: - - name: me - coins: ["1000token", "100000000stake"] - mnemonic: ozone unfold device pave lemon potato omit insect column wise cover hint narrow large provide kidney episode clay notable milk mention dizzy muffin crazy - cointype: 7777777 - - name: you - coins: ["5000token"] - cointype: 123456 -validator: - name: user1 - staked: "100000000stake" -` - - conf, err := Parse(strings.NewReader(confyml)) - - require.NoError(t, err) - require.Equal(t, []Account{ - { - Name: "me", - Coins: []string{"1000token", "100000000stake"}, - Mnemonic: "ozone unfold device pave lemon potato omit insect column wise cover hint narrow large provide kidney episode clay notable milk mention dizzy muffin crazy", - CoinType: "7777777", - }, - { - Name: "you", - Coins: []string{"5000token"}, - CoinType: "123456", - }, - }, conf.Accounts) - require.Equal(t, Validator{ - Name: "user1", - Staked: "100000000stake", - }, conf.Validator) -} - -func TestParseInvalid(t *testing.T) { - confyml := ` -accounts: - - name: me - coins: ["1000token", "100000000stake"] - - name: you - coins: ["5000token"] -` - - _, err := Parse(strings.NewReader(confyml)) - require.Equal(t, &ValidationError{"validator is required"}, err) -} - -func TestFaucetHost(t *testing.T) { - confyml := ` -accounts: - - name: me - coins: ["1000token", "100000000stake"] - - name: you - coins: ["5000token"] -validator: - name: user1 - staked: "100000000stake" -faucet: - host: "0.0.0.0:4600" -` - conf, err := Parse(strings.NewReader(confyml)) - require.NoError(t, err) - require.Equal(t, "0.0.0.0:4600", FaucetHost(conf)) - - confyml = ` -accounts: - - name: me - coins: ["1000token", "100000000stake"] - - name: you - coins: ["5000token"] -validator: - name: user1 - staked: "100000000stake" -faucet: - port: 4700 -` - conf, err = Parse(strings.NewReader(confyml)) - require.NoError(t, err) - require.Equal(t, ":4700", FaucetHost(conf)) - - // Port must be higher priority - confyml = ` -accounts: - - name: me - coins: ["1000token", "100000000stake"] - - name: you - coins: ["5000token"] -validator: - name: user1 - staked: "100000000stake" -faucet: - host: "0.0.0.0:4600" - port: 4700 -` - conf, err = Parse(strings.NewReader(confyml)) - require.NoError(t, err) - require.Equal(t, ":4700", FaucetHost(conf)) -} diff --git a/ignite/chainconfig/convert.go b/ignite/chainconfig/convert.go new file mode 100644 index 0000000000..4fcb15b775 --- /dev/null +++ b/ignite/chainconfig/convert.go @@ -0,0 +1,40 @@ +package chainconfig + +import ( + "io" + + "gopkg.in/yaml.v2" + + "github.com/ignite/cli/ignite/chainconfig/config" +) + +// Build time check for the latest config version type. +// This is required to be sure that conversion to latest +// doesn't break when a new config version is added without +// updating the references to the previous version. +var _ = Versions[LatestVersion].(*Config) + +// ConvertLatest converts a config to the latest version. +func ConvertLatest(c config.Converter) (_ *Config, err error) { + for c.GetVersion() < LatestVersion { + c, err = c.ConvertNext() + if err != nil { + return nil, err + } + } + + // Cast to the latest version type. + // This is safe because there is a build time check that makes sure + // the type for the latest config version is the right one here. + return c.(*Config), nil +} + +// MigrateLatest migrates a config file to the latest version. +func MigrateLatest(current io.Reader, latest io.Writer) error { + cfg, err := Parse(current) + if err != nil { + return err + } + + return yaml.NewEncoder(latest).Encode(cfg) +} diff --git a/ignite/chainconfig/convert_test.go b/ignite/chainconfig/convert_test.go new file mode 100644 index 0000000000..dbe4063cc4 --- /dev/null +++ b/ignite/chainconfig/convert_test.go @@ -0,0 +1,39 @@ +package chainconfig_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/chainconfig/testdata" + v0testdata "github.com/ignite/cli/ignite/chainconfig/v0/testdata" +) + +func TestConvertLatest(t *testing.T) { + // Arrange + cfgV0 := v0testdata.GetConfig(t) + + // Act + cfgLatest, err := chainconfig.ConvertLatest(cfgV0) + + // Assert + require.NoError(t, err) + require.Equal(t, chainconfig.LatestVersion, cfgLatest.GetVersion()) +} + +func TestMigrateLatest(t *testing.T) { + // Arrange + current := bytes.NewReader(testdata.Versions[chainconfig.LatestVersion-1]) + latest := bytes.Buffer{} + want := string(testdata.Versions[chainconfig.LatestVersion]) + + // Act + err := chainconfig.MigrateLatest(current, &latest) + + // Assert + require.NotEmpty(t, want, "testdata is missing the latest config version") + require.NoError(t, err) + require.Equal(t, want, latest.String()) +} diff --git a/ignite/chainconfig/errors.go b/ignite/chainconfig/errors.go new file mode 100644 index 0000000000..792cec9fa0 --- /dev/null +++ b/ignite/chainconfig/errors.go @@ -0,0 +1,29 @@ +package chainconfig + +import ( + "errors" + "fmt" + + "github.com/ignite/cli/ignite/chainconfig/config" +) + +// ErrConfigNotFound indicates that the config.yml can't be found. +var ErrConfigNotFound = errors.New("could not locate a config.yml in your chain") + +// ValidationError is returned when a configuration is invalid. +type ValidationError struct { + Message string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("config is not valid: %s", e.Message) +} + +// UnsupportedVersionError is returned when the version of the config is not supported. +type UnsupportedVersionError struct { + Version config.Version +} + +func (e *UnsupportedVersionError) Error() string { + return fmt.Sprintf("config version %d is not supported", e.Version) +} diff --git a/ignite/chainconfig/parser.go b/ignite/chainconfig/parser.go new file mode 100644 index 0000000000..5af60cccad --- /dev/null +++ b/ignite/chainconfig/parser.go @@ -0,0 +1,110 @@ +package chainconfig + +import ( + "bytes" + "io" + "os" + + "gopkg.in/yaml.v2" + + "github.com/ignite/cli/ignite/chainconfig/config" +) + +// Parse reads a config file. +// When the version of the file beign read is not the latest +// it is automatically migrated to the latest version. +func Parse(configFile io.Reader) (*Config, error) { + var buf bytes.Buffer + + // Read the config file version first to know how to decode it + version, err := ReadConfigVersion(io.TeeReader(configFile, &buf)) + if err != nil { + return DefaultConfig(), err + } + + // Decode the current config file version and assign default + // values for the fields that are empty + c, err := decodeConfig(&buf, version) + if err != nil { + return DefaultConfig(), err + } + + // Make sure that the empty fields contain default values + // after reading the config from the YAML file + if err = c.SetDefaults(); err != nil { + return DefaultConfig(), err + } + + // Finally make sure the config is the latest one before validating it + cfg, err := ConvertLatest(c) + if err != nil { + return DefaultConfig(), err + } + + return cfg, validateConfig(cfg) +} + +// ParseFile parses a config from a file path. +func ParseFile(path string) (*Config, error) { + file, err := os.Open(path) + if err != nil { + return DefaultConfig(), err + } + + defer file.Close() + + return Parse(file) +} + +// ReadConfigVersion reads the config version. +func ReadConfigVersion(configFile io.Reader) (config.Version, error) { + c := struct { + Version config.Version `yaml:"version"` + }{} + + err := yaml.NewDecoder(configFile).Decode(&c) + + return c.Version, err +} + +func decodeConfig(r io.Reader, version config.Version) (config.Converter, error) { + c, ok := Versions[version] + if !ok { + return nil, &UnsupportedVersionError{version} + } + + cfg, err := c.Clone() + if err != nil { + return nil, err + } + + if err = cfg.Decode(r); err != nil { + return nil, err + } + + return cfg, nil +} + +func validateConfig(c *Config) error { + if len(c.Accounts) == 0 { + return &ValidationError{"at least one account is required"} + } + + if len(c.Validators) == 0 { + return &ValidationError{"at least one validator is required"} + } + + for _, validator := range c.Validators { + if validator.Name == "" { + return &ValidationError{"validator 'name' is required"} + } + + if validator.Bonded == "" { + return &ValidationError{"validator 'bonded' is required"} + } + } + + // TODO: We should validate all of the required config fields + + return nil +} diff --git a/ignite/chainconfig/parser_test.go b/ignite/chainconfig/parser_test.go new file mode 100644 index 0000000000..96387f6d0a --- /dev/null +++ b/ignite/chainconfig/parser_test.go @@ -0,0 +1,72 @@ +package chainconfig_test + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/chainconfig/config" + "github.com/ignite/cli/ignite/chainconfig/testdata" +) + +func TestReadConfigVersion(t *testing.T) { + // Arrange + r := strings.NewReader("version: 42") + want := config.Version(42) + + // Act + version, err := chainconfig.ReadConfigVersion(r) + + // Assert + require.NoError(t, err) + require.Equal(t, want, version) +} + +func TestParse(t *testing.T) { + // Arrange: Initialize a reader with the previous version + ver := chainconfig.LatestVersion - 1 + r := bytes.NewReader(testdata.Versions[ver]) + + // Act + cfg, err := chainconfig.Parse(r) + + // Assert + require.NoError(t, err) + + // Assert: Parse must return the latest version + require.Equal(t, chainconfig.LatestVersion, cfg.Version) + require.Equal(t, testdata.GetLatestConfig(t), cfg) +} + +func TestParseWithCurrentVersion(t *testing.T) { + // Arrange + r := bytes.NewReader(testdata.Versions[chainconfig.LatestVersion]) + + // Act + cfg, err := chainconfig.Parse(r) + + // Assert + require.NoError(t, err) + require.Equal(t, chainconfig.LatestVersion, cfg.Version) + require.Equal(t, testdata.GetLatestConfig(t), cfg) +} + +func TestParseWithUnknownVersion(t *testing.T) { + // Arrange + version := config.Version(9999) + r := strings.NewReader(fmt.Sprintf("version: %d", version)) + + var want *chainconfig.UnsupportedVersionError + + // Act + _, err := chainconfig.Parse(r) + + // Assert + require.ErrorAs(t, err, &want) + require.NotNil(t, want) + require.Equal(t, want.Version, version) +} diff --git a/ignite/chainconfig/testdata/testdata.go b/ignite/chainconfig/testdata/testdata.go new file mode 100644 index 0000000000..0805793360 --- /dev/null +++ b/ignite/chainconfig/testdata/testdata.go @@ -0,0 +1,19 @@ +package testdata + +import ( + "testing" + + "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/chainconfig/config" + v0testdata "github.com/ignite/cli/ignite/chainconfig/v0/testdata" + v1testdata "github.com/ignite/cli/ignite/chainconfig/v1/testdata" +) + +var Versions = map[config.Version][]byte{ + 0: v0testdata.ConfigYAML, + 1: v1testdata.ConfigYAML, +} + +func GetLatestConfig(t *testing.T) *chainconfig.Config { + return v1testdata.GetConfig(t) +} diff --git a/ignite/chainconfig/v0/config.go b/ignite/chainconfig/v0/config.go new file mode 100644 index 0000000000..a0214b8ff6 --- /dev/null +++ b/ignite/chainconfig/v0/config.go @@ -0,0 +1,44 @@ +package v0 + +import ( + "io" + + "github.com/imdario/mergo" + "gopkg.in/yaml.v2" + + "github.com/ignite/cli/ignite/chainconfig/config" +) + +// Config is the user given configuration to do additional setup during serve. +type Config struct { + config.BaseConfig `yaml:",inline"` + + Validator Validator `yaml:"validator"` + Init config.Init `yaml:"init"` + Host config.Host `yaml:"host"` +} + +// Clone returns an identical copy of the instance. +func (c *Config) Clone() (config.Converter, error) { + copy := Config{} + if err := mergo.Merge(©, c, mergo.WithAppendSlice); err != nil { + return nil, err + } + + return ©, nil +} + +// Decode decodes the config file values from YAML. +func (c *Config) Decode(r io.Reader) error { + if err := yaml.NewDecoder(r).Decode(c); err != nil { + return err + } + + return nil +} + +// Validator holds info related to validator settings. +type Validator struct { + Name string `yaml:"name"` + Staked string `yaml:"staked"` +} diff --git a/ignite/chainconfig/v0/config_convert.go b/ignite/chainconfig/v0/config_convert.go new file mode 100644 index 0000000000..33955ae23d --- /dev/null +++ b/ignite/chainconfig/v0/config_convert.go @@ -0,0 +1,66 @@ +package v0 + +import ( + "github.com/ignite/cli/ignite/chainconfig/config" + v1 "github.com/ignite/cli/ignite/chainconfig/v1" +) + +// ConvertNext converts the current config version to the next one. +func (c *Config) ConvertNext() (config.Converter, error) { + targetCfg := v1.DefaultConfig() + + // All the fields in the base config remain the same + targetCfg.BaseConfig = c.BaseConfig + targetCfg.Version = 1 + + // There is always only one validator in version 0 + validator := v1.Validator{} + validator.Name = c.Validator.Name + validator.Bonded = c.Validator.Staked + validator.Home = c.Init.Home + validator.KeyringBackend = c.Init.KeyringBackend + validator.Client = c.Init.Client + + if c.Init.App != nil { + validator.App = c.Init.App + } + + if c.Init.Config != nil { + validator.Config = c.Init.Config + } + + // The host configuration must be defined in the validators for version 1 + servers := v1.Servers{} + + if c.Host.P2P != "" { + servers.P2P.Address = c.Host.P2P + } + + if c.Host.RPC != "" { + servers.RPC.Address = c.Host.RPC + } + + if c.Host.Prof != "" { + servers.RPC.PProfAddress = c.Host.Prof + } + + if c.Host.GRPCWeb != "" { + servers.GRPCWeb.Address = c.Host.GRPCWeb + } + + if c.Host.GRPC != "" { + servers.GRPC.Address = c.Host.GRPC + } + + if c.Host.API != "" { + servers.API.Address = c.Host.API + } + + if err := validator.SetServers(servers); err != nil { + return nil, err + } + + targetCfg.Validators = append(targetCfg.Validators, validator) + + return targetCfg, nil +} diff --git a/ignite/chainconfig/v0/config_convert_test.go b/ignite/chainconfig/v0/config_convert_test.go new file mode 100644 index 0000000000..ed5c9a2df9 --- /dev/null +++ b/ignite/chainconfig/v0/config_convert_test.go @@ -0,0 +1,61 @@ +package v0_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/chainconfig/config" + v0testdata "github.com/ignite/cli/ignite/chainconfig/v0/testdata" + v1 "github.com/ignite/cli/ignite/chainconfig/v1" +) + +func TestV0ToV1(t *testing.T) { + // Arrange + cfgV0 := v0testdata.GetConfig(t) + + // Act + c, err := cfgV0.ConvertNext() + cfgV1, _ := c.(*v1.Config) + + // Assert + require.NoError(t, err) + require.NotNilf(t, cfgV1, "expected *v1.Config, got %T", c) + require.Equal(t, config.Version(1), cfgV1.GetVersion()) + require.Equal(t, cfgV0.Build, cfgV1.Build) + require.Equal(t, cfgV0.Accounts, cfgV1.Accounts) + require.Equal(t, cfgV0.Faucet, cfgV1.Faucet) + require.Equal(t, cfgV0.Client, cfgV1.Client) + require.Equal(t, cfgV0.Genesis, cfgV1.Genesis) + require.Len(t, cfgV1.Validators, 1) +} + +func TestV0ToV1Validator(t *testing.T) { + // Arrange + cfgV0 := v0testdata.GetConfig(t) + cfgV0.Host.RPC = "127.0.0.0:1" + cfgV0.Host.P2P = "127.0.0.0:2" + cfgV0.Host.GRPC = "127.0.0.0:3" + cfgV0.Host.GRPCWeb = "127.0.0.0:4" + cfgV0.Host.Prof = "127.0.0.0:5" + cfgV0.Host.API = "127.0.0.0:6" + + // Act + c, _ := cfgV0.ConvertNext() + cfgV1, _ := c.(*v1.Config) + validator := cfgV1.Validators[0] + servers, _ := validator.GetServers() + + // Assert + require.Equal(t, cfgV0.Validator.Name, validator.Name) + require.Equal(t, cfgV0.Validator.Staked, validator.Bonded) + require.Equal(t, cfgV0.Init.Home, validator.Home) + require.Equal(t, cfgV0.Init.KeyringBackend, validator.KeyringBackend) + require.Equal(t, cfgV0.Init.Client, validator.Client) + require.Equal(t, cfgV0.Host.RPC, servers.RPC.Address) + require.Equal(t, cfgV0.Host.P2P, servers.P2P.Address) + require.Equal(t, cfgV0.Host.GRPC, servers.GRPC.Address) + require.Equal(t, cfgV0.Host.GRPCWeb, servers.GRPCWeb.Address) + require.Equal(t, cfgV0.Host.Prof, servers.RPC.PProfAddress) + require.Equal(t, cfgV0.Host.API, servers.API.Address) +} diff --git a/ignite/chainconfig/v0/config_test.go b/ignite/chainconfig/v0/config_test.go new file mode 100644 index 0000000000..eed92d178c --- /dev/null +++ b/ignite/chainconfig/v0/config_test.go @@ -0,0 +1,26 @@ +package v0_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v0 "github.com/ignite/cli/ignite/chainconfig/v0" +) + +func TestClone(t *testing.T) { + // Arrange + c := &v0.Config{ + Validator: v0.Validator{ + Name: "alice", + Staked: "100000000stake", + }, + } + + // Act + c2, err := c.Clone() + + // Assert + require.NoError(t, err) + require.Equal(t, c, c2) +} diff --git a/ignite/chainconfig/v0/testdata/config.yaml b/ignite/chainconfig/v0/testdata/config.yaml new file mode 100644 index 0000000000..9d75e02198 --- /dev/null +++ b/ignite/chainconfig/v0/testdata/config.yaml @@ -0,0 +1,43 @@ +accounts: + - name: alice + coins: ["100000000uatom", "100000000000000000000aevmos"] + mnemonic: "ozone unfold device pave lemon potato omit insect column wise cover hint narrow large provide kidney episode clay notable milk mention dizzy muffin crazy" + - name: bob + coins: ["5000000000000aevmos"] + address: "cosmos1adn9gxjmrc3hrsdx5zpc9sj2ra7kgqkmphf8yw" +validator: + name: alice + staked: "100000000000000000000aevmos" +faucet: + name: bob + coins: ["10aevmos"] + host: 0.0.0.0:4600 + port: 4600 +build: + binary: "evmosd" +init: + home: "$HOME/.evmosd" + app: + evm-rpc: + address: "0.0.0.0:8545" + ws-address: "0.0.0.0:8546" +genesis: + chain_id: "evmosd_9000-1" + app_state: + staking: + params: + bond_denom: "aevmos" + mint: + params: + mint_denom: "aevmos" + crisis: + constant_fee: + denom: "aevmos" + gov: + deposit_params: + min_deposit: + - amount: "10000000" + denom: "aevmos" + evm: + params: + evm_denom: "aevmos" diff --git a/ignite/chainconfig/v0/testdata/testdata.go b/ignite/chainconfig/v0/testdata/testdata.go new file mode 100644 index 0000000000..1e625996f5 --- /dev/null +++ b/ignite/chainconfig/v0/testdata/testdata.go @@ -0,0 +1,27 @@ +package testdata + +import ( + "bytes" + _ "embed" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" + + v0 "github.com/ignite/cli/ignite/chainconfig/v0" +) + +//go:embed config.yaml +var ConfigYAML []byte + +func GetConfig(t *testing.T) *v0.Config { + c := &v0.Config{} + + err := yaml.NewDecoder(bytes.NewReader(ConfigYAML)).Decode(c) + require.NoError(t, err) + + err = c.SetDefaults() + require.NoError(t, err) + + return c +} diff --git a/ignite/chainconfig/v1/config.go b/ignite/chainconfig/v1/config.go new file mode 100644 index 0000000000..8fc3f296a5 --- /dev/null +++ b/ignite/chainconfig/v1/config.go @@ -0,0 +1,136 @@ +package v1 + +import ( + "io" + + "github.com/imdario/mergo" + "gopkg.in/yaml.v2" + + "github.com/ignite/cli/ignite/chainconfig/config" + "github.com/ignite/cli/ignite/pkg/xnet" +) + +// DefaultConfig returns a config with default values. +func DefaultConfig() *Config { + c := Config{BaseConfig: config.DefaultBaseConfig()} + c.Version = 1 + return &c +} + +// Config is the user given configuration to do additional setup during serve. +type Config struct { + config.BaseConfig `yaml:",inline"` + + Validators []Validator `yaml:"validators"` +} + +func (c *Config) SetDefaults() error { + if err := c.BaseConfig.SetDefaults(); err != nil { + return err + } + + // Make sure that validator addresses don't chash with each other + if err := c.updateValidatorAddresses(); err != nil { + return err + } + + return nil +} + +// Clone returns an identical copy of the instance +func (c *Config) Clone() (config.Converter, error) { + copy := Config{} + if err := mergo.Merge(©, c, mergo.WithAppendSlice); err != nil { + return nil, err + } + + return ©, nil +} + +// Decode decodes the config file values from YAML. +func (c *Config) Decode(r io.Reader) error { + if err := yaml.NewDecoder(r).Decode(c); err != nil { + return err + } + + return nil +} + +func (c *Config) updateValidatorAddresses() (err error) { + // Margin to increase port numbers of the default addresses + margin := 10 + + for i := range c.Validators { + // Use default addresses for the first validator + if i == 0 { + continue + } + + validator := &c.Validators[i] + servers, err := validator.GetServers() + if err != nil { + return err + } + + servers, err = incrementDefaultServerPortsBy(servers, uint64(margin*i)) + if err != nil { + return err + } + + if err := validator.SetServers(servers); err != nil { + return err + } + } + + return nil +} + +// Returns a new server where the default addresses have their ports +// incremented by a margin to avoid port clashing. +func incrementDefaultServerPortsBy(s Servers, inc uint64) (Servers, error) { + var err error + + if s.GRPC.Address == DefaultGRPCAddress { + s.GRPC.Address, err = xnet.IncreasePortBy(DefaultGRPCAddress, inc) + if err != nil { + return Servers{}, err + } + } + + if s.GRPCWeb.Address == DefaultGRPCWebAddress { + s.GRPCWeb.Address, err = xnet.IncreasePortBy(DefaultGRPCWebAddress, inc) + if err != nil { + return Servers{}, err + } + } + + if s.API.Address == DefaultAPIAddress { + s.API.Address, err = xnet.IncreasePortBy(DefaultAPIAddress, inc) + if err != nil { + return Servers{}, err + } + } + + if s.P2P.Address == DefaultP2PAddress { + s.P2P.Address, err = xnet.IncreasePortBy(DefaultP2PAddress, inc) + if err != nil { + return Servers{}, err + } + } + + if s.RPC.Address == DefaultRPCAddress { + s.RPC.Address, err = xnet.IncreasePortBy(DefaultRPCAddress, inc) + if err != nil { + return Servers{}, err + } + } + + if s.RPC.PProfAddress == DefaultPProfAddress { + s.RPC.PProfAddress, err = xnet.IncreasePortBy(DefaultPProfAddress, inc) + if err != nil { + return Servers{}, err + } + } + + return s, nil +} diff --git a/ignite/chainconfig/v1/config_convert.go b/ignite/chainconfig/v1/config_convert.go new file mode 100644 index 0000000000..acb0cbbfa0 --- /dev/null +++ b/ignite/chainconfig/v1/config_convert.go @@ -0,0 +1,9 @@ +package v1 + +import "github.com/ignite/cli/ignite/chainconfig/config" + +// ConvertNext implements the conversion of the current config to the next version. +func (c *Config) ConvertNext() (config.Converter, error) { + // v1 is the latest version, there is no need to convert. + return c, nil +} diff --git a/ignite/chainconfig/v1/config_test.go b/ignite/chainconfig/v1/config_test.go new file mode 100644 index 0000000000..b9c1346d10 --- /dev/null +++ b/ignite/chainconfig/v1/config_test.go @@ -0,0 +1,181 @@ +package v1_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/ignite/cli/ignite/chainconfig/v1" + "github.com/ignite/cli/ignite/pkg/xnet" +) + +func TestConfigValidatorDefaultServers(t *testing.T) { + // Arrange + c := v1.Config{ + Validators: []v1.Validator{ + { + Name: "name-1", + Bonded: "100ATOM", + }, + }, + } + servers := v1.Servers{} + + // Act + err := c.SetDefaults() + if err == nil { + servers, err = c.Validators[0].GetServers() + } + + // Assert + require.NoError(t, err) + + // Assert + require.Equal(t, v1.DefaultGRPCAddress, servers.GRPC.Address) + require.Equal(t, v1.DefaultGRPCWebAddress, servers.GRPCWeb.Address) + require.Equal(t, v1.DefaultAPIAddress, servers.API.Address) + require.Equal(t, v1.DefaultRPCAddress, servers.RPC.Address) + require.Equal(t, v1.DefaultP2PAddress, servers.P2P.Address) + require.Equal(t, v1.DefaultPProfAddress, servers.RPC.PProfAddress) +} + +func TestConfigValidatorWithExistingServers(t *testing.T) { + // Arrange + rpcAddr := "127.0.0.1:1234" + apiAddr := "127.0.0.1:4321" + c := v1.Config{ + Validators: []v1.Validator{ + { + Name: "name-1", + Bonded: "100ATOM", + App: map[string]interface{}{ + // This value should not be ovewritten with the default address + "api": map[string]interface{}{"address": apiAddr}, + }, + Config: map[string]interface{}{ + // This value should not be ovewritten with the default address + "rpc": map[string]interface{}{"laddr": rpcAddr}, + }, + }, + }, + } + servers := v1.Servers{} + + // Act + err := c.SetDefaults() + if err == nil { + servers, err = c.Validators[0].GetServers() + } + + // Assert + require.NoError(t, err) + + // Assert + require.Equal(t, rpcAddr, servers.RPC.Address) + require.Equal(t, apiAddr, servers.API.Address) + require.Equal(t, v1.DefaultGRPCAddress, servers.GRPC.Address) + require.Equal(t, v1.DefaultGRPCWebAddress, servers.GRPCWeb.Address) + require.Equal(t, v1.DefaultP2PAddress, servers.P2P.Address) + require.Equal(t, v1.DefaultPProfAddress, servers.RPC.PProfAddress) +} + +func TestConfigValidatorsWithExistingServers(t *testing.T) { + // Arrange + inc := uint64(10) + rpcAddr := "127.0.0.1:1234" + apiAddr := "127.0.0.1:4321" + c := v1.Config{ + Validators: []v1.Validator{ + { + Name: "name-1", + Bonded: "100ATOM", + }, + { + Name: "name-2", + Bonded: "200ATOM", + App: map[string]interface{}{ + // This value should not be ovewritten with the default address + "api": map[string]interface{}{"address": apiAddr}, + }, + Config: map[string]interface{}{ + // This value should not be ovewritten with the default address + "rpc": map[string]interface{}{"laddr": rpcAddr}, + }, + }, + }, + } + servers := v1.Servers{} + + // Act + err := c.SetDefaults() + if err == nil { + servers, err = c.Validators[1].GetServers() + } + + // Assert + require.NoError(t, err) + + // Assert: The existing addresses should not be changed + require.Equal(t, rpcAddr, servers.RPC.Address) + require.Equal(t, apiAddr, servers.API.Address) + + // Assert: The second validator should have the ports incremented by 10 + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultGRPCAddress, inc), servers.GRPC.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultGRPCWebAddress, inc), servers.GRPCWeb.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultP2PAddress, inc), servers.P2P.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultPProfAddress, inc), servers.RPC.PProfAddress) +} + +func TestConfigValidatorsDefaultServers(t *testing.T) { + // Arrange + inc := uint64(10) + c := v1.Config{ + Validators: []v1.Validator{ + { + Name: "name-1", + Bonded: "100ATOM", + }, + { + Name: "name-2", + Bonded: "200ATOM", + }, + }, + } + servers := v1.Servers{} + + // Act + err := c.SetDefaults() + if err == nil { + servers, err = c.Validators[1].GetServers() + } + + // Assert + require.NoError(t, err) + + // Assert: The second validator should have the ports incremented by 10 + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultGRPCAddress, inc), servers.GRPC.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultGRPCWebAddress, inc), servers.GRPCWeb.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultAPIAddress, inc), servers.API.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultRPCAddress, inc), servers.RPC.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultP2PAddress, inc), servers.P2P.Address) + require.Equal(t, xnet.MustIncreasePortBy(v1.DefaultPProfAddress, inc), servers.RPC.PProfAddress) +} + +func TestClone(t *testing.T) { + // Arrange + c := &v1.Config{ + Validators: []v1.Validator{ + { + Name: "alice", + Bonded: "100000000stake", + }, + }, + } + + // Act + c2, err := c.Clone() + + // Assert + require.NoError(t, err) + require.Equal(t, c, c2) +} diff --git a/ignite/chainconfig/v1/testdata/config.yaml b/ignite/chainconfig/v1/testdata/config.yaml new file mode 100644 index 0000000000..e6fa3438bb --- /dev/null +++ b/ignite/chainconfig/v1/testdata/config.yaml @@ -0,0 +1,53 @@ +version: 1 +build: + binary: evmosd + proto: + path: proto + third_party_paths: + - third_party/proto + - proto_vendor +accounts: +- name: alice + coins: + - 100000000uatom + - 100000000000000000000aevmos + mnemonic: ozone unfold device pave lemon potato omit insect column wise cover hint + narrow large provide kidney episode clay notable milk mention dizzy muffin crazy +- name: bob + coins: + - 5000000000000aevmos + address: cosmos1adn9gxjmrc3hrsdx5zpc9sj2ra7kgqkmphf8yw +faucet: + name: bob + coins: + - 10aevmos + host: 0.0.0.0:4600 + port: 4600 +genesis: + app_state: + crisis: + constant_fee: + denom: aevmos + evm: + params: + evm_denom: aevmos + gov: + deposit_params: + min_deposit: + - amount: "10000000" + denom: aevmos + mint: + params: + mint_denom: aevmos + staking: + params: + bond_denom: aevmos + chain_id: evmosd_9000-1 +validators: +- name: alice + bonded: 100000000000000000000aevmos + app: + evm-rpc: + address: 0.0.0.0:8545 + ws-address: 0.0.0.0:8546 + home: $HOME/.evmosd diff --git a/ignite/chainconfig/v1/testdata/testdata.go b/ignite/chainconfig/v1/testdata/testdata.go new file mode 100644 index 0000000000..01750e48de --- /dev/null +++ b/ignite/chainconfig/v1/testdata/testdata.go @@ -0,0 +1,27 @@ +package testdata + +import ( + "bytes" + _ "embed" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" + + v1 "github.com/ignite/cli/ignite/chainconfig/v1" +) + +//go:embed config.yaml +var ConfigYAML []byte + +func GetConfig(t *testing.T) *v1.Config { + c := &v1.Config{} + + err := yaml.NewDecoder(bytes.NewReader(ConfigYAML)).Decode(c) + require.NoError(t, err) + + err = c.SetDefaults() + require.NoError(t, err) + + return c +} diff --git a/ignite/chainconfig/v1/validator.go b/ignite/chainconfig/v1/validator.go new file mode 100644 index 0000000000..d65d2e1c9a --- /dev/null +++ b/ignite/chainconfig/v1/validator.go @@ -0,0 +1,143 @@ +package v1 + +import ( + xyaml "github.com/ignite/cli/ignite/pkg/yaml" +) + +// Validator holds info related to validator settings. +type Validator struct { + // Name is the name of the validator. + Name string `yaml:"name"` + + // Bonded is how much the validator has staked. + Bonded string `yaml:"bonded"` + + // App overwrites appd's config/app.toml configs. + App xyaml.Map `yaml:"app,omitempty"` + + // Config overwrites appd's config/config.toml configs. + Config xyaml.Map `yaml:"config,omitempty"` + + // Client overwrites appd's config/client.toml configs. + Client xyaml.Map `yaml:"client,omitempty"` + + // Home overwrites default home directory used for the app + Home string `yaml:"home,omitempty"` + + // KeyringBackend is the default keyring backend to use for blockchain initialization + KeyringBackend string `yaml:"keyring-backend,omitempty"` + + // Gentx overwrites appd's config/gentx.toml configs. + Gentx *Gentx `yaml:"gentx,omitempty"` +} + +// Gentx holds info related to Gentx settings. +type Gentx struct { + // Amount is the amount for the current Gentx. + Amount string `yaml:"amount"` + + // Moniker is the validator's (optional) moniker. + Moniker string `yaml:"moniker"` + + // Home is directory for config and data. + Home string `yaml:"home"` + + // KeyringBackend is keyring's backend. + KeyringBackend string `yaml:"keyring-backend"` + + // ChainID is the network chain ID. + ChainID string `yaml:"chain-id"` + + // CommissionMaxChangeRate is the maximum commission change rate percentage (per day). + CommissionMaxChangeRate string `yaml:"commission-max-change-rate"` + + // CommissionMaxRate is the maximum commission rate percentage + CommissionMaxRate string `yaml:"commission-max-rate"` + + // CommissionRate is the initial commission rate percentage. + CommissionRate string `yaml:"commission-rate"` + + // Details is the validator's (optional) details. + Details string `yaml:"details"` + + // SecurityContact is the validator's (optional) security contact email. + SecurityContact string `yaml:"security-contact"` + + // Website is the validator's (optional) website. + Website string `yaml:"website"` + + // AccountNumber is the account number of the signing account (offline mode only). + AccountNumber int `yaml:"account-number"` + + // BroadcastMode is the transaction broadcasting mode (sync|async|block) (default "sync"). + BroadcastMode string `yaml:"broadcast-mode"` + + // DryRun is a boolean determining whether to ignore the --gas flag and perform a simulation of a transaction. + DryRun bool `yaml:"dry-run"` + + // FeeAccount is the fee account pays fees for the transaction instead of deducting from the signer + FeeAccount string `yaml:"fee-account"` + + // Fee is the fee to pay along with transaction; eg: 10uatom. + Fee string `yaml:"fee"` + + // From is the name or address of private key with which to sign. + From string `yaml:"from"` + + // From is the gas limit to set per-transaction; set to "auto" to calculate sufficient gas automatically (default 200000). + Gas string `yaml:"gas"` + + // GasAdjustment is the adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored (default 1). + GasAdjustment string `yaml:"gas-adjustment"` + + // GasPrices is the gas prices in decimal format to determine the transaction fee (e.g. 0.1uatom). + GasPrices string `yaml:"gas-prices"` + + // GenerateOnly is a boolean determining whether to build an unsigned transaction and write it to STDOUT. + GenerateOnly bool `yaml:"generate-only"` + + // Identity is the (optional) identity signature (ex. UPort or Keybase). + Identity string `yaml:"identity"` + + // IP is the node's public IP (default "192.168.1.64"). + IP string `yaml:"ip"` + + // KeyringDir is the client Keyring directory; if omitted, the default 'home' directory will be used. + KeyringDir string `yaml:"keyring-dir"` + + // Ledger is a boolean determining whether to use a connected Ledger device. + Ledger bool `yaml:"ledger"` + + // KeyringDir is the minimum self delegation required on the validator. + MinSelfDelegation string `yaml:"min-self-delegation"` + + // Node is : to tendermint rpc interface for this chain (default "tcp://localhost:26657"). + Node string `yaml:"node"` + + // NodeID is the node's NodeID. + NodeID string `yaml:"node-id"` + + // Note is the note to add a description to the transaction (previously --memo). + Note string `yaml:"note"` + + // Offline is a boolean determining the offline mode (does not allow any online functionality). + Offline bool `yaml:"offline"` + + // Output is the output format (text|json) (default "json"). + Output string `yaml:"output"` + + // OutputDocument writes the genesis transaction JSON document to the given file instead of the default location. + OutputDocument string `yaml:"output-document"` + + // PubKey is the validator's Protobuf JSON encoded public key. + PubKey string `yaml:"pubkey"` + + // Sequence is the sequence number of the signing account (offline mode only). + Sequence uint `yaml:"sequence"` + + // SignMode is the choose sign mode (direct|amino-json), this is an advanced feature. + SignMode string `yaml:"sign-mode"` + + // TimeoutHeight sets a block timeout height to prevent the tx from being committed past a certain height. + TimeoutHeight uint `yaml:"timeout-height"` +} diff --git a/ignite/chainconfig/v1/validator_servers.go b/ignite/chainconfig/v1/validator_servers.go new file mode 100644 index 0000000000..1f69245024 --- /dev/null +++ b/ignite/chainconfig/v1/validator_servers.go @@ -0,0 +1,169 @@ +package v1 + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +var ( + // DefaultGRPCAddress is the default GRPC address. + DefaultGRPCAddress = "0.0.0.0:9090" + + // DefaultGRPCWebAddress is the default GRPC-Web address. + DefaultGRPCWebAddress = "0.0.0.0:9091" + + // DefaultAPIAddress is the default API address. + DefaultAPIAddress = "0.0.0.0:1317" + + // DefaultRPCAddress is the default RPC address. + DefaultRPCAddress = "0.0.0.0:26657" + + // DefaultP2PAddress is the default P2P address. + DefaultP2PAddress = "0.0.0.0:26656" + + // DefaultPProfAddress is the default Prof address. + DefaultPProfAddress = "0.0.0.0:6060" +) + +func DefaultServers() Servers { + s := Servers{} + s.GRPC.Address = DefaultGRPCAddress + s.GRPCWeb.Address = DefaultGRPCWebAddress + s.API.Address = DefaultAPIAddress + s.P2P.Address = DefaultP2PAddress + s.RPC.Address = DefaultRPCAddress + s.RPC.PProfAddress = DefaultPProfAddress + + return s +} + +type Servers struct { + cosmosServers `mapstructure:",squash"` + tendermintServers `mapstructure:",squash"` +} + +type cosmosServers struct { + GRPC CosmosHost `mapstructure:"grpc"` + GRPCWeb CosmosHost `mapstructure:"grpc-web"` + API CosmosHost `mapstructure:"api"` +} + +type tendermintServers struct { + P2P TendermintHost `mapstructure:"p2p"` + RPC TendermintRPCHost `mapstructure:"rpc"` +} + +type CosmosHost struct { + Address string `mapstructure:"address,omitempty"` +} + +type TendermintHost struct { + Address string `mapstructure:"laddr,omitempty"` +} + +type TendermintRPCHost struct { + TendermintHost `mapstructure:",squash"` + + PProfAddress string `mapstructure:"pprof_laddr,omitempty"` +} + +func (v Validator) GetServers() (Servers, error) { + // Initialize servers with default addresses + s := DefaultServers() + + // Ovewrite the default Cosmos SDK addresses with the configured ones + if err := mapstructure.Decode(v.App, &s); err != nil { + return Servers{}, fmt.Errorf("error reading validator app servers: %w", err) + } + + // Ovewrite the default Tendermint addresses with the configured ones + if err := mapstructure.Decode(v.Config, &s); err != nil { + return Servers{}, fmt.Errorf("error reading tendermint validator config servers: %w", err) + } + + return s, nil +} + +func (v *Validator) SetServers(s Servers) error { + if err := v.setAppServers(s); err != nil { + return fmt.Errorf("error updating validator app servers: %w", err) + } + + if err := v.setConfigServers(s); err != nil { + return fmt.Errorf("error updating validator config servers: %w", err) + } + + return nil +} + +func (v *Validator) setAppServers(s Servers) error { + c, err := decodeServers(s.cosmosServers) + if err != nil { + return err + } + + v.App = mergeMaps(c, v.App) + + return nil +} + +func (v *Validator) setConfigServers(s Servers) error { + m, err := decodeServers(s.tendermintServers) + if err != nil { + return fmt.Errorf("error updating validator config servers: %w", err) + } + + v.Config = mergeMaps(m, v.Config) + + return nil +} + +func decodeServers(input interface{}) (output map[string]interface{}, err error) { + // Decode the input structure into a map + if err := mapstructure.Decode(input, &output); err != nil { + return nil, err + } + + // Remove keys with empty server values from the map + for k := range output { + if v, _ := output[k].(map[string]interface{}); len(v) == 0 { + delete(output, k) + } + } + + // Don't return an empty map to avoid the generation of empty + // fields when the validator is saved to a YAML config file. + if len(output) == 0 { + return nil, nil + } + + return +} + +func mergeMaps(src, dst map[string]interface{}) map[string]interface{} { + if len(src) == 0 { + return dst + } + + // Allow dst to be nil by initializing it here + if dst == nil { + dst = make(map[string]interface{}) + } + + for k, v := range src { + // When the current value is a map in both merge their values + if srcValue, ok := v.(map[string]interface{}); ok { + if dstValue, ok := dst[k].(map[string]interface{}); ok { + mergeMaps(srcValue, dstValue) + + continue + } + } + + // By default ovewrite the destination map with the source value + dst[k] = v + } + + return dst +} diff --git a/ignite/chainconfig/v1/validator_servers_test.go b/ignite/chainconfig/v1/validator_servers_test.go new file mode 100644 index 0000000000..927c604ffb --- /dev/null +++ b/ignite/chainconfig/v1/validator_servers_test.go @@ -0,0 +1,69 @@ +package v1_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/ignite/cli/ignite/chainconfig/v1" + xyaml "github.com/ignite/cli/ignite/pkg/yaml" +) + +func TestValidatorGetServers(t *testing.T) { + // Arrange + want := v1.DefaultServers() + want.RPC.Address = "127.0.0.0:1" + want.P2P.Address = "127.0.0.0:2" + want.GRPC.Address = "127.0.0.0:3" + want.GRPCWeb.Address = "127.0.0.0:4" + want.RPC.PProfAddress = "127.0.0.0:5" + want.API.Address = "127.0.0.0:6" + + v := v1.Validator{ + App: map[string]interface{}{ + "grpc": map[string]interface{}{"address": want.GRPC.Address}, + "grpc-web": map[string]interface{}{"address": want.GRPCWeb.Address}, + "api": map[string]interface{}{"address": want.API.Address}, + }, + Config: map[string]interface{}{ + "p2p": map[string]interface{}{"laddr": want.P2P.Address}, + "rpc": map[string]interface{}{ + "laddr": want.RPC.Address, + "pprof_laddr": want.RPC.PProfAddress, + }, + }, + } + + // Act + s, err := v.GetServers() + + // Assert + require.NoError(t, err) + require.Equal(t, want, s) +} + +func TestValidatorSetServers(t *testing.T) { + // Arrange + v := v1.Validator{} + s := v1.DefaultServers() + wantApp := xyaml.Map{ + "grpc": map[string]interface{}{"address": s.GRPC.Address}, + "grpc-web": map[string]interface{}{"address": s.GRPCWeb.Address}, + "api": map[string]interface{}{"address": s.API.Address}, + } + wantConfig := xyaml.Map{ + "p2p": map[string]interface{}{"laddr": s.P2P.Address}, + "rpc": map[string]interface{}{ + "laddr": s.RPC.Address, + "pprof_laddr": s.RPC.PProfAddress, + }, + } + + // Act + err := v.SetServers(s) + + // Assert + require.NoError(t, err) + require.Equal(t, wantApp, v.App, "cosmos app config is not equal") + require.Equal(t, wantConfig, v.Config, "tendermint config is not equal") +} diff --git a/ignite/cmd/chain.go b/ignite/cmd/chain.go index 95d158b035..9d6e30384d 100644 --- a/ignite/cmd/chain.go +++ b/ignite/cmd/chain.go @@ -1,6 +1,25 @@ package ignitecmd -import "github.com/spf13/cobra" +import ( + "bytes" + "errors" + "fmt" + "os" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + + "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/pkg/cliui" + "github.com/ignite/cli/ignite/pkg/cliui/colors" + "github.com/ignite/cli/ignite/pkg/cliui/icons" +) + +var ( + msgMigration = "Migrating blockchain config file from v%d to v%d..." + msgMigrationCancel = "Stopping because config version v%d is required to run the command" + msgMigrationPrompt = "Your blockchain config version is v%[1]d and the latest is v%[2]d. Would you like to upgrade your config file to v%[2]d?" +) // NewChain returns a command that groups sub commands related to compiling, serving // blockchains and so on. @@ -56,17 +75,77 @@ to send token from any other account that exists on chain. The "simulate" command helps you start a simulation testing process for your chain. `, - Aliases: []string{"c"}, - Args: cobra.ExactArgs(1), + Aliases: []string{"c"}, + Args: cobra.ExactArgs(1), + PersistentPreRunE: configMigrationPreRunHandler, } - c.AddCommand( - NewChainServe(), - NewChainBuild(), - NewChainInit(), - NewChainFaucet(), - NewChainSimulate(), - ) + // Add flags required for the configMigrationPreRunHandler + c.PersistentFlags().AddFlagSet(flagSetConfig()) + c.PersistentFlags().AddFlagSet(flagSetYes()) + + c.AddCommand(NewChainServe()) + c.AddCommand(NewChainBuild()) + c.AddCommand(NewChainInit()) + c.AddCommand(NewChainFaucet()) + c.AddCommand(NewChainSimulate()) return c } + +func configMigrationPreRunHandler(cmd *cobra.Command, args []string) (err error) { + session := cliui.New() + defer session.Cleanup() + + appPath := flagGetPath(cmd) + configPath := getConfig(cmd) + if configPath == "" { + if configPath, err = chainconfig.LocateDefault(appPath); err != nil { + return err + } + } + + rawCfg, err := os.ReadFile(configPath) + if err != nil { + return err + } + + version, err := chainconfig.ReadConfigVersion(bytes.NewReader(rawCfg)) + if err != nil { + return err + } + + // Config files with older versions must be migrated to the latest before executing the command + if version != chainconfig.LatestVersion { + if !getYes(cmd) { + // Confirm before overwritting the config file + question := fmt.Sprintf(msgMigrationPrompt, version, chainconfig.LatestVersion) + if err := session.AskConfirm(question); err != nil { + if errors.Is(err, promptui.ErrAbort) { + return fmt.Errorf(msgMigrationCancel, chainconfig.LatestVersion) + } + + return err + } + + // Confirm before migrating the config if there are uncommitted changes + if err := confirmWhenUncommittedChanges(session, appPath); err != nil { + return err + } + } else { + session.Printf("%s %s\n", icons.Info, colors.Infof(msgMigration, version, chainconfig.LatestVersion)) + } + + file, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE, 0o755) + if err != nil { + return err + } + + defer file.Close() + + // Convert the current config to the latest version and update the YAML file + return chainconfig.MigrateLatest(bytes.NewReader(rawCfg), file) + } + + return nil +} diff --git a/ignite/cmd/chain_serve.go b/ignite/cmd/chain_serve.go index 9f9512ec7a..07a985595a 100644 --- a/ignite/cmd/chain_serve.go +++ b/ignite/cmd/chain_serve.go @@ -63,7 +63,6 @@ production, you may want to run "appd start" manually. c.Flags().BoolP("verbose", "v", false, "Verbose output") c.Flags().BoolP(flagForceReset, "f", false, "Force reset of the app state on start and every source change") c.Flags().BoolP(flagResetOnce, "r", false, "Reset of the app state on first start") - c.Flags().StringP(flagConfig, "c", "", "Ignite config file (default: ./config.yml)") return c } diff --git a/ignite/cmd/cmd.go b/ignite/cmd/cmd.go index e285f2363a..1e848265b1 100644 --- a/ignite/cmd/cmd.go +++ b/ignite/cmd/cmd.go @@ -105,7 +105,7 @@ func flagSetHome() *flag.FlagSet { func flagNetworkFrom() *flag.FlagSet { fs := flag.NewFlagSet("", flag.ContinueOnError) - fs.String(flagFrom, cosmosaccount.DefaultAccount, "Account name to use for sending transactions to SPN") + fs.String(flagFrom, cosmosaccount.DefaultAccount, "account name to use for sending transactions to SPN") return fs } @@ -114,9 +114,20 @@ func getHome(cmd *cobra.Command) (home string) { return } +func flagSetConfig() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.StringP(flagConfig, "c", "", "ignite config file (default: ./config.yml)") + return fs +} + +func getConfig(cmd *cobra.Command) (config string) { + config, _ = cmd.Flags().GetString(flagConfig) + return +} + func flagSetYes() *flag.FlagSet { fs := flag.NewFlagSet("", flag.ContinueOnError) - fs.BoolP(flagYes, "y", false, "Answers interactive yes/no questions with yes") + fs.BoolP(flagYes, "y", false, "answers interactive yes/no questions with yes") return fs } diff --git a/ignite/cmd/generate.go b/ignite/cmd/generate.go index 36d17e5d03..767f7a07cc 100644 --- a/ignite/cmd/generate.go +++ b/ignite/cmd/generate.go @@ -18,11 +18,11 @@ Produced source code can be regenerated by running a command again and is not me flagSetPath(c) flagSetClearCache(c) - c.AddCommand(addGitChangesVerifier(NewGenerateGo())) - c.AddCommand(addGitChangesVerifier(NewGenerateTSClient())) - c.AddCommand(addGitChangesVerifier(NewGenerateVuex())) - c.AddCommand(addGitChangesVerifier(NewGenerateDart())) - c.AddCommand(addGitChangesVerifier(NewGenerateOpenAPI())) + c.AddCommand(NewGenerateGo()) + c.AddCommand(NewGenerateTSClient()) + c.AddCommand(NewGenerateVuex()) + c.AddCommand(NewGenerateDart()) + c.AddCommand(NewGenerateOpenAPI()) return c } diff --git a/ignite/cmd/generate_dart.go b/ignite/cmd/generate_dart.go index 334171f6a9..01cdbad22a 100644 --- a/ignite/cmd/generate_dart.go +++ b/ignite/cmd/generate_dart.go @@ -11,10 +11,14 @@ import ( func NewGenerateDart() *cobra.Command { c := &cobra.Command{ - Use: "dart", - Short: "Generate a Dart client", - RunE: generateDartHandler, + Use: "dart", + Short: "Generate a Dart client", + PreRunE: gitChangesConfirmPreRunHandler, + RunE: generateDartHandler, } + + c.Flags().AddFlagSet(flagSetYes()) + return c } diff --git a/ignite/cmd/generate_go.go b/ignite/cmd/generate_go.go index a670805d97..c7f26261ec 100644 --- a/ignite/cmd/generate_go.go +++ b/ignite/cmd/generate_go.go @@ -10,11 +10,16 @@ import ( ) func NewGenerateGo() *cobra.Command { - return &cobra.Command{ - Use: "proto-go", - Short: "Generate proto based Go code needed for the app's source code", - RunE: generateGoHandler, + c := &cobra.Command{ + Use: "proto-go", + Short: "Generate proto based Go code needed for the app's source code", + PreRunE: gitChangesConfirmPreRunHandler, + RunE: generateGoHandler, } + + c.Flags().AddFlagSet(flagSetYes()) + + return c } func generateGoHandler(cmd *cobra.Command, args []string) error { diff --git a/ignite/cmd/generate_openapi.go b/ignite/cmd/generate_openapi.go index dce9c05c2c..0f575c10c0 100644 --- a/ignite/cmd/generate_openapi.go +++ b/ignite/cmd/generate_openapi.go @@ -10,11 +10,16 @@ import ( ) func NewGenerateOpenAPI() *cobra.Command { - return &cobra.Command{ - Use: "openapi", - Short: "Generate generates an OpenAPI spec for your chain from your config.yml", - RunE: generateOpenAPIHandler, + c := &cobra.Command{ + Use: "openapi", + Short: "Generate generates an OpenAPI spec for your chain from your config.yml", + PreRunE: gitChangesConfirmPreRunHandler, + RunE: generateOpenAPIHandler, } + + c.Flags().AddFlagSet(flagSetYes()) + + return c } func generateOpenAPIHandler(cmd *cobra.Command, args []string) error { diff --git a/ignite/cmd/generate_typescript_client.go b/ignite/cmd/generate_typescript_client.go index 1843ccf0a7..167ff6807a 100644 --- a/ignite/cmd/generate_typescript_client.go +++ b/ignite/cmd/generate_typescript_client.go @@ -9,11 +9,13 @@ import ( func NewGenerateTSClient() *cobra.Command { c := &cobra.Command{ - Use: "ts-client", - Short: "Generate Typescript client for your chain's frontend", - RunE: generateTSClientHandler, + Use: "ts-client", + Short: "Generate Typescript client for your chain's frontend", + PreRunE: gitChangesConfirmPreRunHandler, + RunE: generateTSClientHandler, } + c.Flags().AddFlagSet(flagSetYes()) c.Flags().StringP(flagOutput, "o", "", "typescript client output path") return c diff --git a/ignite/cmd/generate_vuex.go b/ignite/cmd/generate_vuex.go index 26105477c3..b70588cc51 100644 --- a/ignite/cmd/generate_vuex.go +++ b/ignite/cmd/generate_vuex.go @@ -11,11 +11,15 @@ import ( func NewGenerateVuex() *cobra.Command { c := &cobra.Command{ - Use: "vuex", - Short: "Generate Typescript client and Vuex stores for your chain's frontend from your `config.yml` file", - RunE: generateVuexHandler, + Use: "vuex", + Short: "Generate Typescript client and Vuex stores for your chain's frontend from your `config.yml` file", + PreRunE: gitChangesConfirmPreRunHandler, + RunE: generateVuexHandler, } + c.Flags().AddFlagSet(flagSetProto3rdParty("")) + c.Flags().AddFlagSet(flagSetYes()) + return c } diff --git a/ignite/cmd/scaffold.go b/ignite/cmd/scaffold.go index f7ed2df48d..3df95d3650 100644 --- a/ignite/cmd/scaffold.go +++ b/ignite/cmd/scaffold.go @@ -4,10 +4,11 @@ import ( "errors" "fmt" - "github.com/AlecAivazis/survey/v2" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" flag "github.com/spf13/pflag" + "github.com/ignite/cli/ignite/pkg/cliui" "github.com/ignite/cli/ignite/pkg/cliui/clispinner" "github.com/ignite/cli/ignite/pkg/placeholder" "github.com/ignite/cli/ignite/pkg/xgit" @@ -87,17 +88,17 @@ with an "--ibc" flag. Note that the default module is not IBC-enabled. } c.AddCommand(NewScaffoldChain()) - c.AddCommand(addGitChangesVerifier(NewScaffoldModule())) - c.AddCommand(addGitChangesVerifier(NewScaffoldList())) - c.AddCommand(addGitChangesVerifier(NewScaffoldMap())) - c.AddCommand(addGitChangesVerifier(NewScaffoldSingle())) - c.AddCommand(addGitChangesVerifier(NewScaffoldType())) - c.AddCommand(addGitChangesVerifier(NewScaffoldMessage())) - c.AddCommand(addGitChangesVerifier(NewScaffoldQuery())) - c.AddCommand(addGitChangesVerifier(NewScaffoldPacket())) - c.AddCommand(addGitChangesVerifier(NewScaffoldBandchain())) - c.AddCommand(addGitChangesVerifier(NewScaffoldVue())) - c.AddCommand(addGitChangesVerifier(NewScaffoldFlutter())) + c.AddCommand(NewScaffoldModule()) + c.AddCommand(NewScaffoldList()) + c.AddCommand(NewScaffoldMap()) + c.AddCommand(NewScaffoldSingle()) + c.AddCommand(NewScaffoldType()) + c.AddCommand(NewScaffoldMessage()) + c.AddCommand(NewScaffoldQuery()) + c.AddCommand(NewScaffoldPacket()) + c.AddCommand(NewScaffoldBandchain()) + c.AddCommand(NewScaffoldVue()) + c.AddCommand(NewScaffoldFlutter()) // c.AddCommand(NewScaffoldWasm()) return c @@ -168,36 +169,38 @@ func scaffoldType( return nil } -func addGitChangesVerifier(cmd *cobra.Command) *cobra.Command { - cmd.Flags().AddFlagSet(flagSetYes()) +func gitChangesConfirmPreRunHandler(cmd *cobra.Command, args []string) error { + // Don't confirm when the "--yes" flag is present + if getYes(cmd) { + return nil + } - preRunFun := cmd.PreRunE - cmd.PreRunE = func(cmd *cobra.Command, args []string) error { - if preRunFun != nil { - if err := preRunFun(cmd, args); err != nil { - return err - } - } + appPath := flagGetPath(cmd) + session := cliui.New() - appPath := flagGetPath(cmd) + defer session.Cleanup() - changesCommitted, err := xgit.AreChangesCommitted(appPath) - if err != nil { - return err - } + return confirmWhenUncommittedChanges(session, appPath) +} - if !getYes(cmd) && !changesCommitted { - var confirmed bool - prompt := &survey.Confirm{ - Message: "Your saved project changes have not been committed. To enable reverting to your current state, commit your saved changes. Do you want to proceed with scaffolding without committing your saved changes", - } - if err := survey.AskOne(prompt, &confirmed); err != nil || !confirmed { - return errors.New("said no") +func confirmWhenUncommittedChanges(session cliui.Session, appPath string) error { + cleanState, err := xgit.AreChangesCommitted(appPath) + if err != nil { + return err + } + + if !cleanState { + question := "Your saved project changes have not been committed. To enable reverting to your current state, commit your saved changes. Do you want to proceed without committing your saved changes" + if err := session.AskConfirm(question); err != nil { + if errors.Is(err, promptui.ErrAbort) { + return errors.New("No") } + + return err } - return nil } - return cmd + + return nil } func flagSetScaffoldType() *flag.FlagSet { diff --git a/ignite/cmd/scaffold_band.go b/ignite/cmd/scaffold_band.go index 20795ba942..f34aacf9dc 100644 --- a/ignite/cmd/scaffold_band.go +++ b/ignite/cmd/scaffold_band.go @@ -14,15 +14,18 @@ import ( // NewScaffoldBandchain creates a new BandChain oracle in the module func NewScaffoldBandchain() *cobra.Command { c := &cobra.Command{ - Use: "band [queryName] --module [moduleName]", - Short: "Scaffold an IBC BandChain query oracle to request real-time data", - Long: "Scaffold an IBC BandChain query oracle to request real-time data from BandChain scripts in a specific IBC-enabled Cosmos SDK module", - Args: cobra.MinimumNArgs(1), - RunE: createBandchainHandler, + Use: "band [queryName] --module [moduleName]", + Short: "Scaffold an IBC BandChain query oracle to request real-time data", + Long: "Scaffold an IBC BandChain query oracle to request real-time data from BandChain scripts in a specific IBC-enabled Cosmos SDK module", + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: createBandchainHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().String(flagModule, "", "IBC Module to add the packet into") c.Flags().String(flagSigner, "", "Label for the message signer (default: creator)") diff --git a/ignite/cmd/scaffold_flutter.go b/ignite/cmd/scaffold_flutter.go index 1f1a211aac..3a69c1149b 100644 --- a/ignite/cmd/scaffold_flutter.go +++ b/ignite/cmd/scaffold_flutter.go @@ -12,12 +12,14 @@ import ( // NewScaffoldFlutter scaffolds a Flutter app for a chain. func NewScaffoldFlutter() *cobra.Command { c := &cobra.Command{ - Use: "flutter", - Short: "A Flutter app for your chain", - Args: cobra.NoArgs, - RunE: scaffoldFlutterHandler, + Use: "flutter", + Short: "A Flutter app for your chain", + Args: cobra.NoArgs, + PreRunE: gitChangesConfirmPreRunHandler, + RunE: scaffoldFlutterHandler, } + c.Flags().AddFlagSet(flagSetYes()) c.Flags().StringP(flagPath, "p", "./flutter", "path to scaffold content of the Flutter app") return c diff --git a/ignite/cmd/scaffold_list.go b/ignite/cmd/scaffold_list.go index 7386356fe4..9267e59148 100644 --- a/ignite/cmd/scaffold_list.go +++ b/ignite/cmd/scaffold_list.go @@ -86,12 +86,15 @@ like to scaffold messages manually. Use a flag to skip message scaffolding: The "creator" field is not generated if a list is scaffolded with the "--no-message" flag. `, - Args: cobra.MinimumNArgs(1), - RunE: scaffoldListHandler, + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: scaffoldListHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().AddFlagSet(flagSetScaffoldType()) return c diff --git a/ignite/cmd/scaffold_map.go b/ignite/cmd/scaffold_map.go index 8a79fca02c..09d2a30cd7 100644 --- a/ignite/cmd/scaffold_map.go +++ b/ignite/cmd/scaffold_map.go @@ -55,12 +55,15 @@ Since the behavior of "list" and "map" scaffolding is very similar, you can use the "--no-message", "--module", "--signer" flags as well as the colon syntax for custom types. `, - Args: cobra.MinimumNArgs(1), - RunE: scaffoldMapHandler, + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: scaffoldMapHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().AddFlagSet(flagSetScaffoldType()) c.Flags().StringSlice(FlagIndexes, []string{"index"}, "fields that index the value") diff --git a/ignite/cmd/scaffold_message.go b/ignite/cmd/scaffold_message.go index 31dcf1b6b7..02ee3cc482 100644 --- a/ignite/cmd/scaffold_message.go +++ b/ignite/cmd/scaffold_message.go @@ -61,12 +61,15 @@ Message scaffolding follows the rules as "ignite scaffold list/map/single" and supports fields with standard and custom types. See "ignite scaffold list —help" for details. `, - Args: cobra.MinimumNArgs(1), - RunE: messageHandler, + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: messageHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().String(flagModule, "", "Module to add the message into. Default: app's main module") c.Flags().StringSliceP(flagResponse, "r", []string{}, "Response fields") c.Flags().Bool(flagNoSimulation, false, "Disable CRUD simulation scaffolding") diff --git a/ignite/cmd/scaffold_module.go b/ignite/cmd/scaffold_module.go index 89dfefe271..98b5737f57 100644 --- a/ignite/cmd/scaffold_module.go +++ b/ignite/cmd/scaffold_module.go @@ -88,12 +88,15 @@ you can specify a type for each param. For example: Refer to Cosmos SDK documentation to learn more about modules, dependencies and params. `, - Args: cobra.ExactArgs(1), - RunE: scaffoldModuleHandler, + Args: cobra.ExactArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: scaffoldModuleHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().StringSlice(flagDep, []string{}, "module dependencies (e.g. --dep account,bank)") c.Flags().Bool(flagIBC, false, "scaffold an IBC module") c.Flags().String(flagIBCOrdering, "none", "channel ordering of the IBC module [none|ordered|unordered]") diff --git a/ignite/cmd/scaffold_package.go b/ignite/cmd/scaffold_package.go index c31f747ee5..b5e94cbd58 100644 --- a/ignite/cmd/scaffold_package.go +++ b/ignite/cmd/scaffold_package.go @@ -18,15 +18,18 @@ const ( // NewScaffoldPacket creates a new packet in the module func NewScaffoldPacket() *cobra.Command { c := &cobra.Command{ - Use: "packet [packetName] [field1] [field2] ... --module [moduleName]", - Short: "Message for sending an IBC packet", - Long: "Scaffold an IBC packet in a specific IBC-enabled Cosmos SDK module", - Args: cobra.MinimumNArgs(1), - RunE: createPacketHandler, + Use: "packet [packetName] [field1] [field2] ... --module [moduleName]", + Short: "Message for sending an IBC packet", + Long: "Scaffold an IBC packet in a specific IBC-enabled Cosmos SDK module", + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: createPacketHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().StringSlice(flagAck, []string{}, "Custom acknowledgment type (field1,field2,...)") c.Flags().String(flagModule, "", "IBC Module to add the packet into") c.Flags().String(flagSigner, "", "Label for the message signer (default: creator)") diff --git a/ignite/cmd/scaffold_query.go b/ignite/cmd/scaffold_query.go index b15f69b2d5..4d94735be2 100644 --- a/ignite/cmd/scaffold_query.go +++ b/ignite/cmd/scaffold_query.go @@ -16,14 +16,17 @@ const ( // NewScaffoldQuery command creates a new type command to scaffold queries func NewScaffoldQuery() *cobra.Command { c := &cobra.Command{ - Use: "query [name] [request_field1] [request_field2] ...", - Short: "Query to get data from the blockchain", - Args: cobra.MinimumNArgs(1), - RunE: queryHandler, + Use: "query [name] [request_field1] [request_field2] ...", + Short: "Query to get data from the blockchain", + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: queryHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().String(flagModule, "", "Module to add the query into. Default: app's main module") c.Flags().StringSliceP(flagResponse, "r", []string{}, "Response fields") c.Flags().StringP(flagDescription, "d", "", "Description of the command") diff --git a/ignite/cmd/scaffold_single.go b/ignite/cmd/scaffold_single.go index 51e2a9ce45..e9e0fb6988 100644 --- a/ignite/cmd/scaffold_single.go +++ b/ignite/cmd/scaffold_single.go @@ -9,14 +9,17 @@ import ( // NewScaffoldSingle returns a new command to scaffold a singleton. func NewScaffoldSingle() *cobra.Command { c := &cobra.Command{ - Use: "single NAME [field]...", - Short: "CRUD for data stored in a single location", - Args: cobra.MinimumNArgs(1), - RunE: scaffoldSingleHandler, + Use: "single NAME [field]...", + Short: "CRUD for data stored in a single location", + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: scaffoldSingleHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().AddFlagSet(flagSetScaffoldType()) return c diff --git a/ignite/cmd/scaffold_type.go b/ignite/cmd/scaffold_type.go index 46a1b61982..9327a124a5 100644 --- a/ignite/cmd/scaffold_type.go +++ b/ignite/cmd/scaffold_type.go @@ -9,14 +9,17 @@ import ( // NewScaffoldType returns a new command to scaffold a type. func NewScaffoldType() *cobra.Command { c := &cobra.Command{ - Use: "type NAME [field]...", - Short: "Scaffold only a type definition", - Args: cobra.MinimumNArgs(1), - RunE: scaffoldTypeHandler, + Use: "type NAME [field]...", + Short: "Scaffold only a type definition", + Args: cobra.MinimumNArgs(1), + PreRunE: gitChangesConfirmPreRunHandler, + RunE: scaffoldTypeHandler, } flagSetPath(c) flagSetClearCache(c) + + c.Flags().AddFlagSet(flagSetYes()) c.Flags().AddFlagSet(flagSetScaffoldType()) return c diff --git a/ignite/cmd/scaffold_vue.go b/ignite/cmd/scaffold_vue.go index 9c32645894..00cfd3b36f 100644 --- a/ignite/cmd/scaffold_vue.go +++ b/ignite/cmd/scaffold_vue.go @@ -12,12 +12,14 @@ import ( // NewScaffoldVue scaffolds a Vue.js app for a chain. func NewScaffoldVue() *cobra.Command { c := &cobra.Command{ - Use: "vue", - Short: "Vue 3 web app template", - Args: cobra.NoArgs, - RunE: scaffoldVueHandler, + Use: "vue", + Short: "Vue 3 web app template", + Args: cobra.NoArgs, + PreRunE: gitChangesConfirmPreRunHandler, + RunE: scaffoldVueHandler, } + c.Flags().AddFlagSet(flagSetYes()) c.Flags().StringP(flagPath, "p", "./vue", "path to scaffold content of the Vue.js app") return c diff --git a/ignite/pkg/cliui/colors/colors.go b/ignite/pkg/cliui/colors/colors.go index e7e1c9a1e0..0bd871ff34 100644 --- a/ignite/pkg/cliui/colors/colors.go +++ b/ignite/pkg/cliui/colors/colors.go @@ -2,4 +2,7 @@ package colors import "github.com/fatih/color" -var Info = color.New(color.FgYellow).SprintFunc() +var ( + Info = color.New(color.FgYellow).SprintFunc() + Infof = color.New(color.FgYellow).SprintfFunc() +) diff --git a/ignite/pkg/xnet/xnet.go b/ignite/pkg/xnet/xnet.go new file mode 100644 index 0000000000..62c7848463 --- /dev/null +++ b/ignite/pkg/xnet/xnet.go @@ -0,0 +1,55 @@ +package xnet + +import ( + "fmt" + "net" + "strconv" +) + +// LocalhostIPv4Address returns a localhost IPv4 address with a port +// that represents the localhost IP address listening on that port. +func LocalhostIPv4Address(port int) string { + return fmt.Sprintf("localhost:%d", port) +} + +// AnyIPv4Address returns an IPv4 meta address "0.0.0.0" with a port +// that represents any IP address listening on that port. +func AnyIPv4Address(port int) string { + return fmt.Sprintf("0.0.0.0:%d", port) +} + +// IncreasePort increases a port number by 1. +// This can be useful to generate port ranges or consecutive +// port numbers for the same address. +func IncreasePort(addr string) (string, error) { + return IncreasePortBy(addr, 1) +} + +// IncreasePortBy increases a port number by a factor of "inc". +// This can be useful to generate port ranges or consecutive +// port numbers for the same address. +func IncreasePortBy(addr string, inc uint64) (string, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return "", err + } + + v, err := strconv.ParseUint(port, 10, 0) + if err != nil { + return "", err + } + + port = strconv.FormatUint(v+inc, 10) + + return net.JoinHostPort(host, port), nil +} + +// MustIncreasePortBy calls IncreasePortBy and panics on error. +func MustIncreasePortBy(addr string, inc uint64) string { + s, err := IncreasePortBy(addr, inc) + if err != nil { + panic(err) + } + + return s +} diff --git a/ignite/pkg/xnet/xnet_test.go b/ignite/pkg/xnet/xnet_test.go new file mode 100644 index 0000000000..e6661001bc --- /dev/null +++ b/ignite/pkg/xnet/xnet_test.go @@ -0,0 +1,54 @@ +package xnet_test + +import ( + "testing" + + "github.com/ignite/cli/ignite/pkg/xnet" + "github.com/stretchr/testify/require" +) + +func TestLocalhostIPv4Address(t *testing.T) { + require.Equal(t, "localhost:42", xnet.LocalhostIPv4Address(42)) +} + +func TestAnyIPv4Address(t *testing.T) { + require.Equal(t, "0.0.0.0:42", xnet.AnyIPv4Address(42)) +} + +func TestIncreasePort(t *testing.T) { + addr, err := xnet.IncreasePort("localhost:41") + + require.NoError(t, err) + require.Equal(t, "localhost:42", addr) +} + +func TestIncreasePortWithInvalidAddress(t *testing.T) { + _, err := xnet.IncreasePort("localhost:x:41") + + require.Error(t, err) +} + +func TestIncreasePortWithInvalidPort(t *testing.T) { + _, err := xnet.IncreasePort("localhost:x") + + require.Error(t, err) +} + +func TestIncreasePortBy(t *testing.T) { + addr, err := xnet.IncreasePortBy("localhost:32", 10) + + require.NoError(t, err) + require.Equal(t, "localhost:42", addr) +} + +func TestIncreasePortByWithInvalidAddress(t *testing.T) { + _, err := xnet.IncreasePortBy("localhost:x:32", 10) + + require.Error(t, err) +} + +func TestIncreasePortByWithInvalidPort(t *testing.T) { + _, err := xnet.IncreasePortBy("localhost:x", 10) + + require.Error(t, err) +} diff --git a/ignite/pkg/yaml/map.go b/ignite/pkg/yaml/map.go new file mode 100644 index 0000000000..790de6f9c0 --- /dev/null +++ b/ignite/pkg/yaml/map.go @@ -0,0 +1,53 @@ +package yaml + +// Map defines a map type that uses strings as key value. +// The map implements the Unmarshaller interface to convert +// the unmershalled map keys type from interface{} to string. +type Map map[string]interface{} + +func (m *Map) UnmarshalYAML(unmarshal func(interface{}) error) error { + var raw map[interface{}]interface{} + + if err := unmarshal(&raw); err != nil { + return err + } + + *m = convertMapKeys(raw) + + return nil +} + +func convertSlice(raw []interface{}) []interface{} { + if len(raw) == 0 { + return raw + } + + if _, ok := raw[0].(map[interface{}]interface{}); !ok { + return raw + } + + values := make([]interface{}, len(raw)) + for i, v := range raw { + values[i] = convertMapKeys(v.(map[interface{}]interface{})) + } + + return values +} + +func convertMapKeys(raw map[interface{}]interface{}) map[string]interface{} { + m := make(map[string]interface{}) + + for k, v := range raw { + if value, _ := v.(map[interface{}]interface{}); value != nil { + // Convert map keys to string + v = convertMapKeys(value) + } else if values, _ := v.([]interface{}); values != nil { + // Make sure that maps inside slices also use strings as key + v = convertSlice(values) + } + + m[k.(string)] = v + } + + return m +} diff --git a/ignite/pkg/yaml/map_test.go b/ignite/pkg/yaml/map_test.go new file mode 100644 index 0000000000..0491d8b282 --- /dev/null +++ b/ignite/pkg/yaml/map_test.go @@ -0,0 +1,44 @@ +package yaml_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" + + xyaml "github.com/ignite/cli/ignite/pkg/yaml" +) + +func TestUnmarshalWithCustomMapType(t *testing.T) { + // Arrange + input := ` + foo: + bar: baz + ` + output := xyaml.Map{} + + // Act + err := yaml.Unmarshal([]byte(input), &output) + + // Assert + require.NoError(t, err) + require.NotNil(t, output["foo"]) + require.IsType(t, (map[string]interface{})(nil), output["foo"]) +} + +func TestUnmarshalWithNativeMapType(t *testing.T) { + // Arrange + input := ` + foo: + bar: baz + ` + output := make(map[string]interface{}) + + // Act + err := yaml.Unmarshal([]byte(input), &output) + + // Assert + require.NoError(t, err) + require.NotNil(t, output["foo"]) + require.IsType(t, (map[interface{}]interface{})(nil), output["foo"]) +} diff --git a/ignite/services/chain/chain.go b/ignite/services/chain/chain.go index 0d444accb1..9c4012e618 100644 --- a/ignite/services/chain/chain.go +++ b/ignite/services/chain/chain.go @@ -8,7 +8,6 @@ import ( "github.com/go-git/go-git/v5" "github.com/gookit/color" - "github.com/tendermint/spn/pkg/chainid" "github.com/ignite/cli/ignite/chainconfig" sperrors "github.com/ignite/cli/ignite/errors" @@ -18,6 +17,7 @@ import ( "github.com/ignite/cli/ignite/pkg/cosmosver" "github.com/ignite/cli/ignite/pkg/repoversion" "github.com/ignite/cli/ignite/pkg/xurl" + "github.com/tendermint/spn/pkg/chainid" ) var ( @@ -212,7 +212,12 @@ func (c *Chain) RPCPublicAddress() (string, error) { if err != nil { return "", err } - rpcAddress = conf.Host.RPC + validator := conf.Validators[0] + servers, err := validator.GetServers() + if err != nil { + return "", err + } + rpcAddress = servers.RPC.Address } return rpcAddress, nil } @@ -231,10 +236,10 @@ func (c *Chain) ConfigPath() string { } // Config returns the config of the chain -func (c *Chain) Config() (chainconfig.Config, error) { +func (c *Chain) Config() (*chainconfig.Config, error) { configPath := c.ConfigPath() if configPath == "" { - return chainconfig.DefaultConf, nil + return chainconfig.DefaultConfig(), nil } return chainconfig.ParseFile(configPath) } @@ -316,8 +321,9 @@ func (c *Chain) DefaultHome() (string, error) { if err != nil { return "", err } - if config.Init.Home != "" { - return config.Init.Home, nil + validator := config.Validators[0] + if validator.Home != "" { + return validator.Home, nil } return c.plugin.Home(), nil @@ -390,13 +396,14 @@ func (c *Chain) KeyringBackend() (chaincmd.KeyringBackend, error) { } // 2nd. - if config.Init.KeyringBackend != "" { - return chaincmd.KeyringBackendFromString(config.Init.KeyringBackend) + validator := config.Validators[0] + if validator.KeyringBackend != "" { + return chaincmd.KeyringBackendFromString(validator.KeyringBackend) } // 3rd. - if config.Init.Client != nil { - if backend, ok := config.Init.Client["keyring-backend"]; ok { + if validator.Client != nil { + if backend, ok := validator.Client["keyring-backend"]; ok { if backendStr, ok := backend.(string); ok { return chaincmd.KeyringBackendFromString(backendStr) } @@ -450,7 +457,13 @@ func (c *Chain) Commands(ctx context.Context) (chaincmdrunner.Runner, error) { return chaincmdrunner.Runner{}, err } - nodeAddr, err := xurl.TCP(config.Host.RPC) + validator := config.Validators[0] + servers, err := validator.GetServers() + if err != nil { + return chaincmdrunner.Runner{}, err + } + + nodeAddr, err := xurl.TCP(servers.RPC.Address) if err != nil { return chaincmdrunner.Runner{}, err } diff --git a/ignite/services/chain/faucet.go b/ignite/services/chain/faucet.go index fa4502adfe..d442c795d3 100644 --- a/ignite/services/chain/faucet.go +++ b/ignite/services/chain/faucet.go @@ -55,7 +55,13 @@ func (c *Chain) Faucet(ctx context.Context) (cosmosfaucet.Faucet, error) { } // construct faucet options. - apiAddress := conf.Host.API + validator := conf.Validators[0] + servers, err := validator.GetServers() + if err != nil { + return cosmosfaucet.Faucet{}, err + } + + apiAddress := servers.API.Address if envAPIAddress != "" { apiAddress = envAPIAddress } diff --git a/ignite/services/chain/init.go b/ignite/services/chain/init.go index f3e634f076..37360d9c44 100644 --- a/ignite/services/chain/init.go +++ b/ignite/services/chain/init.go @@ -66,9 +66,7 @@ func (c *Chain) InitChain(ctx context.Context) error { return err } - // overwrite configuration changes from Ignite CLI's config.yml to - // over app's sdk configs. - + // ovewrite app config files with the values defined in Ignite's config file if err := c.plugin.Configure(home, conf); err != nil { return err } @@ -78,54 +76,16 @@ func (c *Chain) InitChain(ctx context.Context) error { conf.Genesis["chain_id"] = chainID } - // Initilize app config - genesisPath, err := c.GenesisPath() - if err != nil { - return err - } - appTOMLPath, err := c.AppTOMLPath() - if err != nil { - return err - } - clientTOMLPath, err := c.ClientTOMLPath() - if err != nil { - return err - } - configTOMLPath, err := c.ConfigTOMLPath() - if err != nil { + // update genesis file with the genesis values defined in the config + if err := c.updateGenesisFile(conf.Genesis); err != nil { return err } - appconfigs := []struct { - ec confile.EncodingCreator - path string - changes map[string]interface{} - }{ - {confile.DefaultJSONEncodingCreator, genesisPath, conf.Genesis}, - {confile.DefaultTOMLEncodingCreator, appTOMLPath, conf.Init.App}, - {confile.DefaultTOMLEncodingCreator, clientTOMLPath, conf.Init.Client}, - {confile.DefaultTOMLEncodingCreator, configTOMLPath, conf.Init.Config}, - } - - for _, ac := range appconfigs { - cf := confile.New(ac.ec, ac.path) - var conf map[string]interface{} - if err := cf.Load(&conf); err != nil { - return err - } - if err := mergo.Merge(&conf, ac.changes, mergo.WithOverride); err != nil { - return err - } - if err := cf.Save(conf); err != nil { - return err - } - } - return nil } // InitAccounts initializes the chain accounts and creates validator gentxs -func (c *Chain) InitAccounts(ctx context.Context, conf chainconfig.Config) error { +func (c *Chain) InitAccounts(ctx context.Context, conf *chainconfig.Config) error { commands, err := c.Commands(ctx) if err != nil { return err @@ -168,10 +128,8 @@ func (c *Chain) InitAccounts(ctx context.Context, conf chainconfig.Config) error } } - _, err = c.IssueGentx(ctx, Validator{ - Name: conf.Validator.Name, - StakingAmount: conf.Validator.Staked, - }) + _, err = c.IssueGentx(ctx, createValidatorFromConfig(conf)) + return err } @@ -212,6 +170,29 @@ func (c *Chain) IsInitialized() (bool, error) { return true, nil } +func (c Chain) updateGenesisFile(data map[string]interface{}) error { + path, err := c.GenesisPath() + if err != nil { + return err + } + + genesis := make(map[string]interface{}) + cf := confile.New(confile.DefaultJSONEncodingCreator, path) + if err := cf.Load(&genesis); err != nil { + return err + } + + if err := mergo.Merge(&genesis, data, mergo.WithOverride); err != nil { + return err + } + + if err = cf.Save(genesis); err != nil { + return err + } + + return nil +} + type Validator struct { Name string Moniker string @@ -235,3 +216,47 @@ type Account struct { CoinType string Coins string } + +func createValidatorFromConfig(conf *chainconfig.Config) (validator Validator) { + // Currently, we support the config file with one valid validator. + validatorFromConfig := conf.Validators[0] + validator.Name = validatorFromConfig.Name + validator.StakingAmount = validatorFromConfig.Bonded + + if validatorFromConfig.Gentx != nil { + if validatorFromConfig.Gentx.Amount != "" { + validator.StakingAmount = validatorFromConfig.Gentx.Amount + } + if validatorFromConfig.Gentx.Moniker != "" { + validator.Moniker = validatorFromConfig.Gentx.Moniker + } + if validatorFromConfig.Gentx.CommissionRate != "" { + validator.CommissionRate = validatorFromConfig.Gentx.CommissionRate + } + if validatorFromConfig.Gentx.CommissionMaxRate != "" { + validator.CommissionMaxRate = validatorFromConfig.Gentx.CommissionMaxRate + } + if validatorFromConfig.Gentx.CommissionMaxChangeRate != "" { + validator.CommissionMaxChangeRate = validatorFromConfig.Gentx.CommissionMaxChangeRate + } + if validatorFromConfig.Gentx.GasPrices != "" { + validator.GasPrices = validatorFromConfig.Gentx.GasPrices + } + if validatorFromConfig.Gentx.Details != "" { + validator.Details = validatorFromConfig.Gentx.Details + } + if validatorFromConfig.Gentx.Identity != "" { + validator.Identity = validatorFromConfig.Gentx.Identity + } + if validatorFromConfig.Gentx.Website != "" { + validator.Website = validatorFromConfig.Gentx.Website + } + if validatorFromConfig.Gentx.SecurityContact != "" { + validator.SecurityContact = validatorFromConfig.Gentx.SecurityContact + } + if validatorFromConfig.Gentx.MinSelfDelegation != "" { + validator.MinSelfDelegation = validatorFromConfig.Gentx.MinSelfDelegation + } + } + return validator +} diff --git a/ignite/services/chain/plugin-stargate.go b/ignite/services/chain/plugin-stargate.go index 1c6dedf6db..a311f8a003 100644 --- a/ignite/services/chain/plugin-stargate.go +++ b/ignite/services/chain/plugin-stargate.go @@ -48,17 +48,17 @@ func (p *stargatePlugin) Gentx(ctx context.Context, runner chaincmdrunner.Runner ) } -func (p *stargatePlugin) Configure(homePath string, conf chainconfig.Config) error { - if err := p.appTOML(homePath, conf); err != nil { +func (p *stargatePlugin) Configure(homePath string, cfg *chainconfig.Config) error { + if err := p.appTOML(homePath, cfg); err != nil { return err } - if err := p.clientTOML(homePath); err != nil { + if err := p.clientTOML(homePath, cfg); err != nil { return err } - return p.configTOML(homePath, conf) + return p.configTOML(homePath, cfg) } -func (p *stargatePlugin) appTOML(homePath string, conf chainconfig.Config) error { +func (p *stargatePlugin) appTOML(homePath string, cfg *chainconfig.Config) error { // TODO find a better way in order to not delete comments in the toml.yml path := filepath.Join(homePath, "config/app.toml") config, err := toml.LoadFile(path) @@ -66,19 +66,29 @@ func (p *stargatePlugin) appTOML(homePath string, conf chainconfig.Config) error return err } - apiAddr, err := xurl.TCP(conf.Host.API) + validator := cfg.Validators[0] + servers, err := validator.GetServers() if err != nil { - return fmt.Errorf("invalid api address format %s: %w", conf.Host.API, err) + return err + } + + apiAddr, err := xurl.TCP(servers.API.Address) + if err != nil { + return fmt.Errorf("invalid api address format %s: %w", servers.API.Address, err) } + // Set default config values config.Set("api.enable", true) config.Set("api.enabled-unsafe-cors", true) config.Set("rpc.cors_allowed_origins", []string{"*"}) + + // Update config values with the validator's Cosmos SDK app config + updateTomlTreeValues(config, validator.App) + + // Make sure the API address have the protocol prefix config.Set("api.address", apiAddr) - config.Set("grpc.address", conf.Host.GRPC) - config.Set("grpc-web.address", conf.Host.GRPCWeb) - staked, err := sdktypes.ParseCoinNormalized(conf.Validator.Staked) + staked, err := sdktypes.ParseCoinNormalized(validator.Bonded) if err != nil { return err } @@ -95,7 +105,7 @@ func (p *stargatePlugin) appTOML(homePath string, conf chainconfig.Config) error return err } -func (p *stargatePlugin) configTOML(homePath string, conf chainconfig.Config) error { +func (p *stargatePlugin) configTOML(homePath string, cfg *chainconfig.Config) error { // TODO find a better way in order to not delete comments in the toml.yml path := filepath.Join(homePath, "config/config.toml") config, err := toml.LoadFile(path) @@ -103,23 +113,34 @@ func (p *stargatePlugin) configTOML(homePath string, conf chainconfig.Config) er return err } - rpcAddr, err := xurl.TCP(conf.Host.RPC) + validator := cfg.Validators[0] + servers, err := validator.GetServers() if err != nil { - return fmt.Errorf("invalid rpc address format %s: %w", conf.Host.RPC, err) + return err } - p2pAddr, err := xurl.TCP(conf.Host.P2P) + rpcAddr, err := xurl.TCP(servers.RPC.Address) if err != nil { - return fmt.Errorf("invalid p2p address format %s: %w", conf.Host.P2P, err) + return fmt.Errorf("invalid rpc address format %s: %w", servers.RPC.Address, err) } + p2pAddr, err := xurl.TCP(servers.P2P.Address) + if err != nil { + return fmt.Errorf("invalid p2p address format %s: %w", servers.P2P.Address, err) + } + + // Set default config values config.Set("mode", "validator") config.Set("rpc.cors_allowed_origins", []string{"*"}) config.Set("consensus.timeout_commit", "1s") config.Set("consensus.timeout_propose", "1s") + + // Update config values with the validator's Tendermint config + updateTomlTreeValues(config, validator.Config) + + // Make sure the addresses have the protocol prefix config.Set("rpc.laddr", rpcAddr) config.Set("p2p.laddr", p2pAddr) - config.Set("rpc.pprof_laddr", conf.Host.Prof) file, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC, 0o644) if err != nil { @@ -131,33 +152,43 @@ func (p *stargatePlugin) configTOML(homePath string, conf chainconfig.Config) er return err } -func (p *stargatePlugin) clientTOML(homePath string) error { +func (p *stargatePlugin) clientTOML(homePath string, cfg *chainconfig.Config) error { path := filepath.Join(homePath, "config/client.toml") config, err := toml.LoadFile(path) if os.IsNotExist(err) { return nil } + if err != nil { return err } + + // Set default config values config.Set("keyring-backend", "test") config.Set("broadcast-mode", "block") + + // Update config values with the validator's client config + updateTomlTreeValues(config, cfg.Validators[0].Client) + file, err := os.OpenFile(path, os.O_RDWR|os.O_TRUNC, 0o644) if err != nil { return err } defer file.Close() + _, err = config.WriteTo(file) return err } -func (p *stargatePlugin) Start(ctx context.Context, runner chaincmdrunner.Runner, conf chainconfig.Config) error { - err := runner.Start(ctx, - "--pruning", - "nothing", - "--grpc.address", - conf.Host.GRPC, - ) +func (p *stargatePlugin) Start(ctx context.Context, runner chaincmdrunner.Runner, cfg *chainconfig.Config) error { + validator := cfg.Validators[0] + servers, err := validator.GetServers() + if err != nil { + return err + } + + err = runner.Start(ctx, "--pruning", "nothing", "--grpc.address", servers.GRPC.Address) + return &CannotStartAppError{p.app.Name, err} } @@ -173,3 +204,21 @@ func stargateHome(app App) string { func (p *stargatePlugin) Version() cosmosver.Family { return cosmosver.Stargate } func (p *stargatePlugin) SupportsIBC() bool { return true } + +func updateTomlTreeValues(t *toml.Tree, values map[string]interface{}) { + for name, v := range values { + // Map are treated as TOML sections where the section names are the key values + if m, ok := v.(map[string]interface{}); ok { + section := name + + for name, v := range m { + path := fmt.Sprintf("%s.%s", section, name) + + t.Set(path, v) + } + } else { + // By default set top a level key/value + t.Set(name, v) + } + } +} diff --git a/ignite/services/chain/plugin.go b/ignite/services/chain/plugin.go index a67c7891b8..29b24f4b1d 100644 --- a/ignite/services/chain/plugin.go +++ b/ignite/services/chain/plugin.go @@ -17,10 +17,10 @@ type Plugin interface { Gentx(context.Context, chaincmdrunner.Runner, Validator) (path string, err error) // Configure configures config defaults. - Configure(string, chainconfig.Config) error + Configure(string, *chainconfig.Config) error // Start returns step.Exec configuration to start servers. - Start(context.Context, chaincmdrunner.Runner, chainconfig.Config) error + Start(context.Context, chaincmdrunner.Runner, *chainconfig.Config) error // Home returns the blockchain node's home dir. Home() string diff --git a/ignite/services/chain/serve.go b/ignite/services/chain/serve.go index 2986553459..3719fef0c8 100644 --- a/ignite/services/chain/serve.go +++ b/ignite/services/chain/serve.go @@ -386,7 +386,7 @@ func (c *Chain) serve(ctx context.Context, cacheStorage cache.Storage, forceRese return c.start(ctx, conf) } -func (c *Chain) start(ctx context.Context, config chainconfig.Config) error { +func (c *Chain) start(ctx context.Context, config *chainconfig.Config) error { commands, err := c.Commands(ctx) if err != nil { return err @@ -420,10 +420,18 @@ func (c *Chain) start(ctx context.Context, config chainconfig.Config) error { // set the app as being served c.served = true + // Get the first validator + validator := config.Validators[0] + 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(config.Host.RPC) - apiAddr, _ := xurl.HTTP(config.Host.API) + + rpcAddr, _ := xurl.HTTP(servers.RPC.Address) + apiAddr, _ := xurl.HTTP(servers.API.Address) // print the server addresses. fmt.Fprintf(c.stdLog().out, "🌍 Tendermint node: %s\n", rpcAddr) diff --git a/ignite/templates/app/stargate/config.yml b/ignite/templates/app/stargate/config.yml index 8d40480692..23285f47f4 100644 --- a/ignite/templates/app/stargate/config.yml +++ b/ignite/templates/app/stargate/config.yml @@ -1,11 +1,12 @@ +version: 1 accounts: - name: alice coins: ["20000token", "200000000stake"] - name: bob coins: ["10000token", "100000000stake"] -validator: - name: alice - staked: "100000000stake" +validators: + - name: alice + bonded: "100000000stake" client: openapi: path: "docs/static/openapi.yml" diff --git a/integration/app.go b/integration/app.go index fe40ff7c99..18223fa5da 100644 --- a/integration/app.go +++ b/integration/app.go @@ -12,6 +12,7 @@ import ( "gopkg.in/yaml.v2" "github.com/ignite/cli/ignite/chainconfig" + v1 "github.com/ignite/cli/ignite/chainconfig/v1" "github.com/ignite/cli/ignite/pkg/availableport" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/ignite/pkg/gocmd" @@ -23,6 +24,16 @@ const ServeTimeout = time.Minute * 15 const defaultConfigFileName = "config.yml" +// Hosts contains the "hostname:port" addresses for different service hosts. +type Hosts struct { + RPC string + P2P string + Prof string + GRPC string + GRPCWeb string + API string +} + type App struct { path string configPath string @@ -175,10 +186,10 @@ func (a App) EnableFaucet(coins, coinsMax []string) (faucetAddr string) { port, err := availableport.Find(1) require.NoError(a.env.t, err) - a.EditConfig(func(conf *chainconfig.Config) { - conf.Faucet.Port = port[0] - conf.Faucet.Coins = coins - conf.Faucet.CoinsMax = coinsMax + a.EditConfig(func(c *chainconfig.Config) { + c.Faucet.Port = port[0] + c.Faucet.Coins = coins + c.Faucet.CoinsMax = coinsMax }) addr, err := xurl.HTTP(fmt.Sprintf("0.0.0.0:%d", port[0])) @@ -189,8 +200,8 @@ func (a App) EnableFaucet(coins, coinsMax []string) (faucetAddr string) { // RandomizeServerPorts randomizes server ports for the app at path, updates // its config.yml and returns new values. -func (a App) RandomizeServerPorts() chainconfig.Host { - // generate random server ports and servers list. +func (a App) RandomizeServerPorts() Hosts { + // generate random server ports ports, err := availableport.Find(6) require.NoError(a.env.t, err) @@ -198,7 +209,7 @@ func (a App) RandomizeServerPorts() chainconfig.Host { return fmt.Sprintf("localhost:%d", port) } - servers := chainconfig.Host{ + hosts := Hosts{ RPC: genAddr(ports[0]), P2P: genAddr(ports[1]), Prof: genAddr(ports[2]), @@ -207,11 +218,21 @@ func (a App) RandomizeServerPorts() chainconfig.Host { API: genAddr(ports[5]), } - a.EditConfig(func(conf *chainconfig.Config) { - conf.Host = servers + a.EditConfig(func(c *chainconfig.Config) { + v := &c.Validators[0] + + s := v1.Servers{} + s.GRPC.Address = hosts.GRPC + s.GRPCWeb.Address = hosts.GRPCWeb + s.API.Address = hosts.API + s.P2P.Address = hosts.P2P + s.RPC.Address = hosts.RPC + s.RPC.PProfAddress = hosts.Prof + + require.NoError(a.env.t, v.SetServers(s)) }) - return servers + return hosts } // UseRandomHomeDir sets in the blockchain config files generated temporary directories for home directories @@ -219,8 +240,8 @@ func (a App) RandomizeServerPorts() chainconfig.Host { func (a App) UseRandomHomeDir() (homeDirPath string) { dir := a.env.TmpDir() - a.EditConfig(func(conf *chainconfig.Config) { - conf.Init.Home = dir + a.EditConfig(func(c *chainconfig.Config) { + c.Validators[0].Home = dir }) return dir diff --git a/integration/app/tx_test.go b/integration/app/tx_test.go index c4e4257a73..abc424ce4a 100644 --- a/integration/app/tx_test.go +++ b/integration/app/tx_test.go @@ -25,13 +25,13 @@ func TestSignTxWithDashedAppName(t *testing.T) { env = envtest.New(t) appname = "dashed-app-name" app = env.Scaffold(appname) - host = app.RandomizeServerPorts() + servers = app.RandomizeServerPorts() ctx, cancel = context.WithCancel(env.Ctx()) ) - nodeAddr, err := xurl.TCP(host.RPC) + nodeAddr, err := xurl.TCP(servers.RPC) if err != nil { - t.Fatalf("cant read nodeAddr from host.RPC %v: %v", host.RPC, err) + t.Fatalf("cant read nodeAddr from host.RPC %v: %v", servers.RPC, err) } env.Exec("scaffold a simple list", @@ -65,13 +65,13 @@ func TestSignTxWithDashedAppName(t *testing.T) { "output", "json", ), step.PreExec(func() error { - return env.IsAppServed(ctx, host) + return env.IsAppServed(ctx, servers.API) }), ), step.New( step.Stdout(output), step.PreExec(func() error { - err := env.IsAppServed(ctx, host) + err := env.IsAppServed(ctx, servers.API) return err }), step.Exec( @@ -116,7 +116,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { env = envtest.New(t) appname = randstr.Runes(10) app = env.Scaffold(fmt.Sprintf("github.com/test/%s", appname)) - host = app.RandomizeServerPorts() + servers = app.RandomizeServerPorts() ctx, cancel = context.WithCancel(env.Ctx()) ) @@ -148,7 +148,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { "output", "json", ), step.PreExec(func() error { - return env.IsAppServed(ctx, host) + return env.IsAppServed(ctx, servers.API) }), ), step.New( @@ -182,7 +182,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { return errors.New("expected alice and bob accounts to be created") } - nodeAddr, err := xurl.TCP(host.RPC) + nodeAddr, err := xurl.TCP(servers.RPC) if err != nil { return err } @@ -219,7 +219,7 @@ func TestGetTxViaGRPCGateway(t *testing.T) { return err } - apiAddr, err := xurl.HTTP(host.API) + apiAddr, err := xurl.HTTP(servers.API) if err != nil { return err } diff --git a/integration/chain/cache_test.go b/integration/chain/cache_test.go index bfd752fd8a..721ffb0818 100644 --- a/integration/chain/cache_test.go +++ b/integration/chain/cache_test.go @@ -90,7 +90,7 @@ func TestCliWithCaching(t *testing.T) { go func() { defer cancel() - isBackendAliveErr = env.IsAppServed(ctx, servers) + isBackendAliveErr = env.IsAppServed(ctx, servers.API) }() env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) diff --git a/integration/chain/cmd_serve_test.go b/integration/chain/cmd_serve_test.go index 27d0a74219..1b01714e1f 100644 --- a/integration/chain/cmd_serve_test.go +++ b/integration/chain/cmd_serve_test.go @@ -36,7 +36,7 @@ func TestServeStargateWithWasm(t *testing.T) { ) go func() { defer cancel() - isBackendAliveErr = env.IsAppServed(ctx, servers) + isBackendAliveErr = env.IsAppServed(ctx, servers.API) }() env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) @@ -56,7 +56,7 @@ func TestServeStargateWithCustomHome(t *testing.T) { ) go func() { defer cancel() - isBackendAliveErr = env.IsAppServed(ctx, servers) + isBackendAliveErr = env.IsAppServed(ctx, servers.API) }() env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) @@ -76,7 +76,7 @@ func TestServeStargateWithConfigHome(t *testing.T) { ) go func() { defer cancel() - isBackendAliveErr = env.IsAppServed(ctx, servers) + isBackendAliveErr = env.IsAppServed(ctx, servers.API) }() env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) @@ -105,7 +105,7 @@ func TestServeStargateWithCustomConfigFile(t *testing.T) { ) go func() { defer cancel() - isBackendAliveErr = env.IsAppServed(ctx, servers) + isBackendAliveErr = env.IsAppServed(ctx, servers.API) }() env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) @@ -127,7 +127,7 @@ func TestServeStargateWithName(t *testing.T) { go func() { defer cancel() - isBackendAliveErr = env.IsAppServed(ctx, servers) + isBackendAliveErr = env.IsAppServed(ctx, servers.API) }() env.Must(app.Serve("should serve with Stargate version", envtest.ExecCtx(ctx))) diff --git a/integration/chain/config_test.go b/integration/chain/config_test.go index 80b439ae64..cf232715e1 100644 --- a/integration/chain/config_test.go +++ b/integration/chain/config_test.go @@ -26,39 +26,43 @@ func TestOverwriteSDKConfigsAndChainID(t *testing.T) { isBackendAliveErr error ) - var c chainconfig.Config - + var cfg chainconfig.Config cf := confile.New(confile.DefaultYAMLEncodingCreator, filepath.Join(app.SourcePath(), "config.yml")) - require.NoError(t, cf.Load(&c)) + require.NoError(t, cf.Load(&cfg)) - c.Genesis = map[string]interface{}{"chain_id": "cosmos"} - c.Init.App = map[string]interface{}{"hello": "cosmos"} - c.Init.Config = map[string]interface{}{"fast_sync": false} + cfg.Genesis = map[string]interface{}{"chain_id": "cosmos"} + cfg.Validators[0].App["hello"] = "cosmos" + cfg.Validators[0].Config["fast_sync"] = false - require.NoError(t, cf.Save(c)) + require.NoError(t, cf.Save(cfg)) go func() { defer cancel() - isBackendAliveErr = env.IsAppServed(ctx, servers) + + isBackendAliveErr = env.IsAppServed(ctx, servers.API) }() + env.Must(app.Serve("should serve", envtest.ExecCtx(ctx))) require.NoError(t, isBackendAliveErr, "app cannot get online in time") - configs := []struct { - ec confile.EncodingCreator - relpath string - key string - expectedVal interface{} + cases := []struct { + ec confile.EncodingCreator + relpath string + key string + want interface{} }{ {confile.DefaultJSONEncodingCreator, "config/genesis.json", "chain_id", "cosmos"}, {confile.DefaultTOMLEncodingCreator, "config/app.toml", "hello", "cosmos"}, {confile.DefaultTOMLEncodingCreator, "config/config.toml", "fast_sync", false}, } - for _, c := range configs { + for _, tt := range cases { var conf map[string]interface{} - cf := confile.New(c.ec, filepath.Join(env.AppHome(appname), c.relpath)) - require.NoError(t, cf.Load(&conf)) - require.Equal(t, c.expectedVal, conf[c.key]) + + path := filepath.Join(env.AppHome(appname), tt.relpath) + c := confile.New(tt.ec, path) + + require.NoError(t, c.Load(&conf)) + require.Equalf(t, tt.want, conf[tt.key], "unexpected value for %s", tt.relpath) } } diff --git a/integration/cosmosgen/bank_module_test.go b/integration/cosmosgen/bank_module_test.go index cc2531edba..afa37a147f 100644 --- a/integration/cosmosgen/bank_module_test.go +++ b/integration/cosmosgen/bank_module_test.go @@ -9,25 +9,26 @@ import ( "github.com/stretchr/testify/require" "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/chainconfig/config" "github.com/ignite/cli/ignite/pkg/xurl" envtest "github.com/ignite/cli/integration" ) func TestBankModule(t *testing.T) { var ( - env = envtest.New(t) - app = env.Scaffold("chain", "--no-module") - host = app.RandomizeServerPorts() + env = envtest.New(t) + app = env.Scaffold("chain", "--no-module") + servers = app.RandomizeServerPorts() ) - queryAPI, err := xurl.HTTP(host.API) + queryAPI, err := xurl.HTTP(servers.API) require.NoError(t, err) - txAPI, err := xurl.TCP(host.RPC) + txAPI, err := xurl.TCP(servers.RPC) require.NoError(t, err) // Accounts to be included in the genesis - accounts := []chainconfig.Account{ + accounts := []config.Account{ { Name: "account1", Address: "cosmos1j8hw8283hj80hhq8urxaj40syrzqp77dt8qwhm", @@ -71,7 +72,7 @@ func TestBankModule(t *testing.T) { }() // Wait for the server to be up before running the client tests - err = env.IsAppServed(ctx, host) + err = env.IsAppServed(ctx, servers.API) require.NoError(t, err) testAccounts, err := json.Marshal(accounts) diff --git a/integration/env.go b/integration/env.go index 990b6b1b67..b6afbafd61 100644 --- a/integration/env.go +++ b/integration/env.go @@ -17,7 +17,6 @@ import ( "github.com/cenkalti/backoff" "github.com/stretchr/testify/require" - "github.com/ignite/cli/ignite/chainconfig" "github.com/ignite/cli/ignite/pkg/cosmosfaucet" "github.com/ignite/cli/ignite/pkg/gocmd" "github.com/ignite/cli/ignite/pkg/gomodulepath" @@ -94,11 +93,10 @@ func (e Env) Ctx() context.Context { return e.ctx } -// IsAppServed checks that app is served properly and servers are started to listening -// before ctx canceled. -func (e Env) IsAppServed(ctx context.Context, host chainconfig.Host) error { +// IsAppServed checks that app is served properly and servers are started to listening before ctx canceled. +func (e Env) IsAppServed(ctx context.Context, apiAddr string) error { checkAlive := func() error { - addr, err := xurl.HTTP(host.API) + addr, err := xurl.HTTP(apiAddr) if err != nil { return err } diff --git a/integration/faucet/faucet_test.go b/integration/faucet/faucet_test.go index b3e7fc029f..6946a88934 100644 --- a/integration/faucet/faucet_test.go +++ b/integration/faucet/faucet_test.go @@ -51,7 +51,7 @@ func TestRequestCoinsFromFaucet(t *testing.T) { // wait servers to be online defer cancel() - err := env.IsAppServed(ctx, servers) + err := env.IsAppServed(ctx, servers.API) require.NoError(t, err) err = env.IsFaucetServed(ctx, faucetClient) diff --git a/integration/network/network_test.go b/integration/network/network_test.go index 9e0de5058d..20947e4374 100644 --- a/integration/network/network_test.go +++ b/integration/network/network_test.go @@ -5,6 +5,7 @@ import ( "context" "os" "path" + "path/filepath" "strings" "testing" @@ -13,14 +14,16 @@ import ( "github.com/go-git/go-git/v5/plumbing" "github.com/stretchr/testify/require" + "github.com/ignite/cli/ignite/chainconfig" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/ignite/pkg/gomodule" envtest "github.com/ignite/cli/integration" ) const ( - spnModule = "github.com/tendermint/spn" - spnRepoURL = "https://" + spnModule + spnModule = "github.com/tendermint/spn" + spnRepoURL = "https://" + spnModule + spnConfigFile = "config_2.yml" ) func cloneSPN(t *testing.T) string { @@ -66,6 +69,26 @@ func cloneSPN(t *testing.T) string { return path } +func migrateSPNConfig(t *testing.T, spnPath string) { + configPath := filepath.Join(spnPath, spnConfigFile) + rawCfg, err := os.ReadFile(configPath) + require.NoError(t, err) + + version, err := chainconfig.ReadConfigVersion(bytes.NewReader(rawCfg)) + require.NoError(t, err) + if version != chainconfig.LatestVersion { + t.Logf("migrating spn config from v%d to v%d", version, chainconfig.LatestVersion) + + file, err := os.OpenFile(configPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) + require.NoError(t, err) + + defer file.Close() + + err = chainconfig.MigrateLatest(bytes.NewReader(rawCfg), file) + require.NoError(t, err) + } +} + func TestNetworkPublish(t *testing.T) { var ( spnPath = cloneSPN(t) @@ -73,9 +96,8 @@ func TestNetworkPublish(t *testing.T) { spn = env.App( spnPath, envtest.AppHomePath(t.TempDir()), - envtest.AppConfigPath(path.Join(spnPath, "config_2.yml")), + envtest.AppConfigPath(path.Join(spnPath, spnConfigFile)), ) - servers = spn.Config().Host ) var ( @@ -83,10 +105,17 @@ func TestNetworkPublish(t *testing.T) { isBackendAliveErr error ) + // Make sure that the SPN config file is at the latest version + migrateSPNConfig(t, spnPath) + + validator := spn.Config().Validators[0] + servers, err := validator.GetServers() + require.NoError(t, err) + go func() { defer cancel() - if isBackendAliveErr = env.IsAppServed(ctx, servers); isBackendAliveErr != nil { + if isBackendAliveErr = env.IsAppServed(ctx, servers.API.Address); isBackendAliveErr != nil { return } var b bytes.Buffer diff --git a/integration/node/cmd_query_bank_test.go b/integration/node/cmd_query_bank_test.go index 5f76c2bbec..36d5a8d46b 100644 --- a/integration/node/cmd_query_bank_test.go +++ b/integration/node/cmd_query_bank_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/chainconfig/config" "github.com/ignite/cli/ignite/pkg/cliui/entrywriter" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/ignite/pkg/cosmosaccount" @@ -64,16 +65,16 @@ func TestNodeQueryBankBalances(t *testing.T) { aliceAccount, aliceMnemonic, err := ca.Create(alice) require.NoError(t, err) - app.EditConfig(func(conf *chainconfig.Config) { - conf.Accounts = []chainconfig.Account{ + app.EditConfig(func(c *chainconfig.Config) { + c.Accounts = []config.Account{ { Name: alice, Mnemonic: aliceMnemonic, Coins: []string{"5600atoken", "1200btoken", "100000000stake"}, }, } - conf.Faucet = chainconfig.Faucet{} - conf.Init.KeyringBackend = keyring.BackendTest + c.Faucet = config.Faucet{} + c.Validators[0].KeyringBackend = keyring.BackendTest }) env.Must(env.Exec("import alice", @@ -99,7 +100,7 @@ func TestNodeQueryBankBalances(t *testing.T) { go func() { defer cancel() - if isBackendAliveErr = env.IsAppServed(ctx, servers); isBackendAliveErr != nil { + if isBackendAliveErr = env.IsAppServed(ctx, servers.API); isBackendAliveErr != nil { return } diff --git a/integration/node/cmd_query_tx_test.go b/integration/node/cmd_query_tx_test.go index feb704b2d7..069bc34a0b 100644 --- a/integration/node/cmd_query_tx_test.go +++ b/integration/node/cmd_query_tx_test.go @@ -40,7 +40,7 @@ func TestNodeQueryTx(t *testing.T) { go func() { defer cancel() - if isBackendAliveErr = env.IsAppServed(ctx, servers); isBackendAliveErr != nil { + if isBackendAliveErr = env.IsAppServed(ctx, servers.API); isBackendAliveErr != nil { return } client, err := cosmosclient.New(context.Background(), diff --git a/integration/node/cmd_tx_bank_send_test.go b/integration/node/cmd_tx_bank_send_test.go index 55f304508d..c3ab73864f 100644 --- a/integration/node/cmd_tx_bank_send_test.go +++ b/integration/node/cmd_tx_bank_send_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ignite/cli/ignite/chainconfig" + "github.com/ignite/cli/ignite/chainconfig/config" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/ignite/pkg/cosmosaccount" "github.com/ignite/cli/ignite/pkg/cosmosclient" @@ -55,8 +56,8 @@ func TestNodeTxBankSend(t *testing.T) { bobAccount, bobMnemonic, err := ca.Create(bob) require.NoError(t, err) - app.EditConfig(func(conf *chainconfig.Config) { - conf.Accounts = []chainconfig.Account{ + app.EditConfig(func(c *chainconfig.Config) { + c.Accounts = []config.Account{ { Name: alice, Mnemonic: aliceMnemonic, @@ -68,8 +69,8 @@ func TestNodeTxBankSend(t *testing.T) { Coins: []string{"10000token", "100000000stake"}, }, } - conf.Faucet = chainconfig.Faucet{} - conf.Init.KeyringBackend = keyring.BackendTest + c.Faucet = config.Faucet{} + c.Validators[0].KeyringBackend = keyring.BackendTest }) env.Must(env.Exec("import alice", step.NewSteps(step.New( @@ -107,7 +108,7 @@ func TestNodeTxBankSend(t *testing.T) { go func() { defer cancel() - if isBackendAliveErr = env.IsAppServed(ctx, servers); isBackendAliveErr != nil { + if isBackendAliveErr = env.IsAppServed(ctx, servers.API); isBackendAliveErr != nil { return } client, err := cosmosclient.New(context.Background(),