Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
670 lines
16 KiB
Go
670 lines
16 KiB
Go
package chain
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/spf13/cobra"
|
|
|
|
chainconfig "git.cw.tr/mukan-network/mukan-ignite/ignite/config/chain"
|
|
chainconfigv1 "git.cw.tr/mukan-network/mukan-ignite/ignite/config/chain/v1"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/chaincmd"
|
|
chaincmdrunner "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/chaincmd/runner"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/colors"
|
|
uilog "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/log"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/confile"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cosmosaccount"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cosmosanalysis"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cosmosver"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/events"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/repoversion"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/xexec"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/xurl"
|
|
igniteversion "git.cw.tr/mukan-network/mukan-ignite/ignite/version"
|
|
)
|
|
|
|
const (
|
|
flagPath = "path"
|
|
flagHome = "home"
|
|
)
|
|
|
|
type (
|
|
// Chain provides programmatic access and tools for a Cosmos SDK blockchain.
|
|
Chain struct {
|
|
// app holds info about blockchain app.
|
|
app App
|
|
|
|
options chainOptions
|
|
|
|
Version cosmosver.Version
|
|
|
|
sourceVersion version
|
|
serveCancel context.CancelFunc
|
|
serveRefresher chan struct{}
|
|
served bool
|
|
|
|
ev events.Bus
|
|
logOutputer uilog.Outputer
|
|
}
|
|
|
|
// chainOptions holds user given options that overwrites chain's defaults.
|
|
chainOptions struct {
|
|
// chainID is the chain's id.
|
|
chainID string
|
|
|
|
// homePath of the chain's config dir.
|
|
homePath string
|
|
|
|
// keyring backend used by commands if not specified in configuration
|
|
keyringBackend chaincmd.KeyringBackend
|
|
|
|
// checkDependencies checks that cached Go dependencies of the chain have not
|
|
// been modified since they were downloaded.
|
|
checkDependencies bool
|
|
|
|
// checkCosmosSDKVersion checks that the app was scaffolded with version of
|
|
// the Cosmos SDK that is supported by Ignite CLI.
|
|
checkCosmosSDKVersion bool
|
|
|
|
// printGeneratedPaths prints the output paths of the generated code
|
|
printGeneratedPaths bool
|
|
|
|
// path of a custom config file
|
|
ConfigFile string
|
|
}
|
|
|
|
version struct {
|
|
tag string
|
|
hash string
|
|
}
|
|
|
|
// Option configures Chain.
|
|
Option func(*Chain)
|
|
)
|
|
|
|
// ID replaces chain's id with given id.
|
|
func ID(id string) Option {
|
|
return func(c *Chain) {
|
|
c.options.chainID = id
|
|
}
|
|
}
|
|
|
|
// HomePath replaces chain's configuration home path with given path.
|
|
func HomePath(path string) Option {
|
|
return func(c *Chain) {
|
|
c.options.homePath = path
|
|
}
|
|
}
|
|
|
|
// KeyringBackend specifies the keyring backend to use for the chain command.
|
|
func KeyringBackend(keyringBackend chaincmd.KeyringBackend) Option {
|
|
return func(c *Chain) {
|
|
c.options.keyringBackend = keyringBackend
|
|
}
|
|
}
|
|
|
|
// ConfigFile specifies a custom config file to use.
|
|
func ConfigFile(configFile string) Option {
|
|
return func(c *Chain) {
|
|
c.options.ConfigFile = configFile
|
|
}
|
|
}
|
|
|
|
// WithOutputer sets the CLI outputer for the chain.
|
|
func WithOutputer(s uilog.Outputer) Option {
|
|
return func(c *Chain) {
|
|
c.logOutputer = s
|
|
}
|
|
}
|
|
|
|
// CollectEvents collects events from the chain.
|
|
func CollectEvents(ev events.Bus) Option {
|
|
return func(c *Chain) {
|
|
c.ev = ev
|
|
}
|
|
}
|
|
|
|
// CheckDependencies checks that cached Go dependencies of the chain have not
|
|
// been modified since they were downloaded. Dependencies are checked by
|
|
// running `go mod verify`.
|
|
func CheckDependencies() Option {
|
|
return func(c *Chain) {
|
|
c.options.checkDependencies = true
|
|
}
|
|
}
|
|
|
|
// CheckCosmosSDKVersion checks that the app was scaffolded with a version of
|
|
// the Cosmos SDK that is supported by Ignite CLI.
|
|
func CheckCosmosSDKVersion() Option {
|
|
return func(c *Chain) {
|
|
c.options.checkCosmosSDKVersion = true
|
|
}
|
|
}
|
|
|
|
// PrintGeneratedPaths prints the output paths of the generated code.
|
|
func PrintGeneratedPaths() Option {
|
|
return func(c *Chain) {
|
|
c.options.printGeneratedPaths = true
|
|
}
|
|
}
|
|
|
|
// New initializes a new Chain with options that its source lives at path.
|
|
func New(path string, options ...Option) (*Chain, error) {
|
|
app, err := NewAppAt(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &Chain{
|
|
app: app,
|
|
serveRefresher: make(chan struct{}, 1),
|
|
}
|
|
|
|
// Apply the options
|
|
for _, apply := range options {
|
|
apply(c)
|
|
}
|
|
|
|
c.sourceVersion, err = c.appVersion()
|
|
if err != nil && !errors.Is(err, git.ErrRepositoryNotExists) {
|
|
return nil, err
|
|
}
|
|
|
|
c.Version, err = cosmosver.Detect(c.app.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.options.checkCosmosSDKVersion {
|
|
if err := igniteversion.AssertSupportedCosmosSDKVersion(c.Version); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func NewWithHomeFlags(cmd *cobra.Command, chainOption ...Option) (*Chain, error) {
|
|
var (
|
|
home, _ = cmd.Flags().GetString(flagHome)
|
|
appPath, _ = cmd.Flags().GetString(flagPath)
|
|
)
|
|
|
|
absPath, err := filepath.Abs(appPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if custom home is provided
|
|
if home != "" {
|
|
chainOption = append(chainOption, HomePath(home))
|
|
}
|
|
return New(absPath, chainOption...)
|
|
}
|
|
|
|
func (c *Chain) appVersion() (v version, err error) {
|
|
ver, err := repoversion.Determine(c.app.Path)
|
|
if err != nil {
|
|
return version{}, err
|
|
}
|
|
|
|
v.hash = ver.Hash
|
|
v.tag = ver.Tag
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// RPCPublicAddress points to the public address of Tendermint RPC, this is shared by
|
|
// other chains for relayer related actions.
|
|
func (c *Chain) RPCPublicAddress() (string, error) {
|
|
conf, err := c.Config()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
validator, err := chainconfig.FirstValidator(conf)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
servers, err := validator.GetServers()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return servers.RPC.Address, nil
|
|
}
|
|
|
|
// ConfigPath returns the config path of the chain.
|
|
// Empty string means that the chain has no defined config.
|
|
func (c *Chain) ConfigPath() string {
|
|
if c.options.ConfigFile != "" {
|
|
return c.options.ConfigFile
|
|
}
|
|
path, err := chainconfig.LocateDefault(c.app.Path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return path
|
|
}
|
|
|
|
// Config returns the config of the chain.
|
|
func (c *Chain) Config() (*chainconfig.Config, error) {
|
|
configPath := c.ConfigPath()
|
|
if configPath == "" {
|
|
return chainconfig.DefaultChainConfig(), nil
|
|
}
|
|
return chainconfig.ParseFile(configPath)
|
|
}
|
|
|
|
// ID returns the chain's id.
|
|
func (c *Chain) ID() (string, error) {
|
|
// chainID in App has the most priority.
|
|
if c.options.chainID != "" {
|
|
return c.options.chainID, nil
|
|
}
|
|
|
|
// otherwise uses defined in config.yml
|
|
chainConfig, err := c.Config()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
genid, ok := chainConfig.Genesis["chain_id"]
|
|
if ok {
|
|
return genid.(string), nil
|
|
}
|
|
|
|
// use app name by default.
|
|
return c.app.N(), nil
|
|
}
|
|
|
|
// Name returns the chain's name.
|
|
func (c *Chain) Name() string {
|
|
return c.app.N()
|
|
}
|
|
|
|
// Binary returns the name of app's default (appd) binary.
|
|
func (c *Chain) Binary() (string, error) {
|
|
conf, err := c.Config()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if conf.Build.Binary != "" {
|
|
return conf.Build.Binary, nil
|
|
}
|
|
|
|
return c.app.D(), nil
|
|
}
|
|
|
|
// AbsBinaryPath returns the absolute path to the app's binary.
|
|
// Returned path includes the binary name.
|
|
func (c *Chain) AbsBinaryPath() (string, error) {
|
|
bin, err := c.Binary()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return xexec.ResolveAbsPath(bin)
|
|
}
|
|
|
|
// SetHome sets the chain home directory.
|
|
func (c *Chain) SetHome(home string) {
|
|
c.options.homePath = home
|
|
}
|
|
|
|
// Home returns the blockchain node's home dir.
|
|
func (c *Chain) Home() (string, error) {
|
|
// check if home is explicitly defined for the app
|
|
home := c.options.homePath
|
|
if home == "" {
|
|
// return default home otherwise
|
|
var err error
|
|
home, err = c.DefaultHome()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
}
|
|
|
|
// expand environment variables in home
|
|
home = os.ExpandEnv(home)
|
|
|
|
return home, nil
|
|
}
|
|
|
|
// AppPath returns the configured App's path.
|
|
func (c *Chain) AppPath() string {
|
|
return c.app.Path
|
|
}
|
|
|
|
// DefaultHome returns the blockchain node's default home dir when not specified in the app.
|
|
func (c *Chain) DefaultHome() (string, error) {
|
|
// check if home is defined in config
|
|
cfg, err := c.Config()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
validator, _ := chainconfig.FirstValidator(cfg)
|
|
if validator.Home != "" {
|
|
expandedHome, err := expandHome(validator.Home)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
validator.Home = expandedHome
|
|
return validator.Home, nil
|
|
}
|
|
|
|
return c.appHome(), nil
|
|
}
|
|
|
|
// DefaultGentxPath returns default gentx.json path of the app.
|
|
func (c *Chain) DefaultGentxPath() (string, error) {
|
|
home, err := c.Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "config/gentx/gentx.json"), nil
|
|
}
|
|
|
|
// GenesisPath returns genesis.json path of the app.
|
|
func (c *Chain) GenesisPath() (string, error) {
|
|
home, err := c.Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "config/genesis.json"), nil
|
|
}
|
|
|
|
// GentxsPath returns the directory where gentxs are stored for the app.
|
|
func (c *Chain) GentxsPath() (string, error) {
|
|
home, err := c.Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "config/gentx"), nil
|
|
}
|
|
|
|
// AppTOMLPath returns app.toml path of the app.
|
|
func (c *Chain) AppTOMLPath() (string, error) {
|
|
home, err := c.Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "config/app.toml"), nil
|
|
}
|
|
|
|
// ConfigTOMLPath returns config.toml path of the app.
|
|
func (c *Chain) ConfigTOMLPath() (string, error) {
|
|
home, err := c.Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "config/config.toml"), nil
|
|
}
|
|
|
|
// ClientTOMLPath returns client.toml path of the app.
|
|
func (c *Chain) ClientTOMLPath() (string, error) {
|
|
home, err := c.Home()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(home, "config/client.toml"), nil
|
|
}
|
|
|
|
// KeyringBackend returns the keyring backend chosen for the chain.
|
|
func (c *Chain) KeyringBackend() (chaincmd.KeyringBackend, error) {
|
|
// When keyring backend is initialized as a chain
|
|
// option it overrides any configured backends.
|
|
if c.options.keyringBackend != "" {
|
|
return c.options.keyringBackend, nil
|
|
}
|
|
|
|
// Try to get keyring backend from the first configured validator
|
|
cfg, err := c.Config()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
validator, _ := chainconfig.FirstValidator(cfg)
|
|
if validator.Client != nil {
|
|
if v, ok := validator.Client["keyring-backend"]; ok {
|
|
if backend, ok := v.(string); ok {
|
|
return chaincmd.KeyringBackendFromString(backend)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to get keyring backend from client.toml config file
|
|
configTOMLPath, err := c.ClientTOMLPath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cf := confile.New(confile.DefaultTOMLEncodingCreator, configTOMLPath)
|
|
var conf struct {
|
|
KeyringBackend string `toml:"keyring-backend"`
|
|
}
|
|
if err := cf.Load(&conf); err != nil {
|
|
return "", err
|
|
}
|
|
if conf.KeyringBackend != "" {
|
|
return chaincmd.KeyringBackendFromString(conf.KeyringBackend)
|
|
}
|
|
|
|
// Use test backend as default when none is configured
|
|
return chaincmd.KeyringBackendTest, nil
|
|
}
|
|
|
|
// Commands returns the runner execute commands on the chain's binary.
|
|
func (c *Chain) Commands(ctx context.Context) (chaincmdrunner.Runner, error) {
|
|
id, err := c.ID()
|
|
if err != nil {
|
|
return chaincmdrunner.Runner{}, err
|
|
}
|
|
|
|
home, err := c.Home()
|
|
if err != nil {
|
|
return chaincmdrunner.Runner{}, err
|
|
}
|
|
|
|
binary, err := c.Binary()
|
|
if err != nil {
|
|
return chaincmdrunner.Runner{}, err
|
|
}
|
|
|
|
// Try to make the binary path absolute. This will also
|
|
// find the binary path when the Go bin path is not part
|
|
// of the PATH environment variable.
|
|
binary = xexec.TryResolveAbsPath(binary)
|
|
|
|
backend, err := c.KeyringBackend()
|
|
if err != nil {
|
|
return chaincmdrunner.Runner{}, err
|
|
}
|
|
|
|
cfg, err := c.Config()
|
|
if err != nil {
|
|
return chaincmdrunner.Runner{}, err
|
|
}
|
|
|
|
servers := chainconfigv1.DefaultServers()
|
|
if len(cfg.Validators) > 0 {
|
|
validator, _ := chainconfig.FirstValidator(cfg)
|
|
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
|
|
}
|
|
|
|
chainCommandOptions := []chaincmd.Option{
|
|
chaincmd.WithChainID(id),
|
|
chaincmd.WithHome(home),
|
|
chaincmd.WithVersion(c.Version),
|
|
chaincmd.WithNodeAddress(nodeAddr),
|
|
chaincmd.WithKeyringBackend(backend),
|
|
}
|
|
|
|
cc := chaincmd.New(binary, chainCommandOptions...)
|
|
|
|
ccrOptions := []chaincmdrunner.Option{}
|
|
|
|
// Enable command output only when CLI verbosity is enabled
|
|
if c.logOutputer != nil && c.logOutputer.Verbosity() == uilog.VerbosityVerbose {
|
|
out := c.logOutputer.NewOutput(c.app.D(), colors.Cyan)
|
|
ccrOptions = append(
|
|
ccrOptions,
|
|
chaincmdrunner.Stdout(out.Stdout()),
|
|
chaincmdrunner.Stderr(out.Stderr()),
|
|
)
|
|
}
|
|
|
|
return chaincmdrunner.New(ctx, cc, ccrOptions...)
|
|
}
|
|
|
|
func appBackendSourceWatchPaths(protoDir string) []string {
|
|
return []string{
|
|
"app",
|
|
"cmd",
|
|
"x",
|
|
"third_party",
|
|
protoDir,
|
|
}
|
|
}
|
|
|
|
// expandHome expands a path that may start with "~" and may contain environment variables.
|
|
func expandHome(path string) (string, error) {
|
|
if strings.HasPrefix(path, "~") {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Only replace the first occurrence at the start.
|
|
path = home + strings.TrimPrefix(path, "~")
|
|
}
|
|
return os.ExpandEnv(path), nil
|
|
}
|
|
|
|
// Bech32Prefix returns the bech32 prefix of the chain.
|
|
func (c *Chain) Bech32Prefix() (string, error) {
|
|
prefix, err := c.parseAddressPrefix()
|
|
if err != nil || prefix == "" {
|
|
return cosmosaccount.AccountPrefixCosmos, err
|
|
}
|
|
|
|
return prefix, nil
|
|
}
|
|
|
|
// CoinType returns the coin type of the chain.
|
|
func (c *Chain) CoinType() (uint32, error) {
|
|
coinType, err := c.parseCoinType()
|
|
if err != nil || coinType == 0 {
|
|
return cosmosaccount.CoinTypeCosmos, err
|
|
}
|
|
|
|
return coinType, nil
|
|
}
|
|
|
|
// parseAddressPrefix parses the address prefix from the app code.
|
|
func (c *Chain) parseAddressPrefix() (string, error) {
|
|
appGoPath, err := c.findAppGoFile()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
content, err := os.ReadFile(appGoPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// try to find the AccountAddressPrefix constant
|
|
lines := strings.SplitSeq(string(content), "\n")
|
|
for line := range lines {
|
|
// match both formats:
|
|
// AccountAddressPrefix = "cosmos"
|
|
// AccountAddressPrefix string = "cosmos"
|
|
if strings.Contains(line, "AccountAddressPrefix") && strings.Contains(line, "=") {
|
|
parts := strings.Split(line, "=")
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
// extract the value within quotes
|
|
value := strings.TrimSpace(parts[1])
|
|
// remove comments if any
|
|
if idx := strings.Index(value, "//"); idx >= 0 {
|
|
value = value[:idx]
|
|
}
|
|
value = strings.TrimSpace(value)
|
|
|
|
// extract string between quotes
|
|
if start := strings.Index(value, "\""); start >= 0 {
|
|
if end := strings.Index(value[start+1:], "\""); end >= 0 {
|
|
return value[start+1 : start+1+end], nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", nil
|
|
}
|
|
|
|
// parseCoinType parses the coin type from the app code.
|
|
func (c *Chain) parseCoinType() (uint32, error) {
|
|
appGoPath, err := c.findAppGoFile()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
content, err := os.ReadFile(appGoPath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// try to find the ChainCoinType constant
|
|
lines := strings.SplitSeq(string(content), "\n")
|
|
for line := range lines {
|
|
if strings.Contains(line, "ChainCoinType") && strings.Contains(line, "=") {
|
|
parts := strings.Split(line, "=")
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
// extract the numeric value
|
|
value := strings.TrimSpace(parts[1])
|
|
// remove comments if any
|
|
if idx := strings.Index(value, "//"); idx >= 0 {
|
|
value = value[:idx]
|
|
}
|
|
value = strings.TrimSpace(value)
|
|
|
|
// parse the value as uint32
|
|
var coinType uint32
|
|
if _, err := fmt.Sscanf(value, "%d", &coinType); err == nil {
|
|
return coinType, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0, nil
|
|
}
|
|
|
|
// findAppGoFile attempts to find the app.go file in the project.
|
|
func (c *Chain) findAppGoFile() (string, error) {
|
|
// Look for the app.go file in common locations
|
|
commonPath := filepath.Join(c.app.Path, "app", "app.go")
|
|
if _, err := os.Stat(commonPath); err == nil {
|
|
return commonPath, nil
|
|
}
|
|
|
|
return cosmosanalysis.FindAppFilePath(c.app.Path)
|
|
}
|