mukan-ignite/ignite/services/chain/chain.go
Mukan Erkin Törük 26b204bd04
Some checks are pending
Docs Deploy / build_and_deploy (push) Waiting to run
Generate Docs / cli (push) Waiting to run
Generate Config Doc / cli (push) Waiting to run
Go formatting / go-formatting (push) Waiting to run
Check links / markdown-link-check (push) Waiting to run
Integration / pre-test (push) Waiting to run
Integration / test on (push) Blocked by required conditions
Integration / status (push) Blocked by required conditions
Lint / Lint Go code (push) Waiting to run
Test / test (ubuntu-latest) (push) Waiting to run
feat: fork Ignite CLI v29 as Mukan Ignite — remove cosmos-sdk restrictions
2026-05-11 03:31:37 +03:00

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 "github.com/ignite/cli/v29/ignite/config/chain"
chainconfigv1 "github.com/ignite/cli/v29/ignite/config/chain/v1"
"github.com/ignite/cli/v29/ignite/pkg/chaincmd"
chaincmdrunner "github.com/ignite/cli/v29/ignite/pkg/chaincmd/runner"
"github.com/ignite/cli/v29/ignite/pkg/cliui/colors"
uilog "github.com/ignite/cli/v29/ignite/pkg/cliui/log"
"github.com/ignite/cli/v29/ignite/pkg/confile"
"github.com/ignite/cli/v29/ignite/pkg/cosmosaccount"
"github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis"
"github.com/ignite/cli/v29/ignite/pkg/cosmosver"
"github.com/ignite/cli/v29/ignite/pkg/errors"
"github.com/ignite/cli/v29/ignite/pkg/events"
"github.com/ignite/cli/v29/ignite/pkg/repoversion"
"github.com/ignite/cli/v29/ignite/pkg/xexec"
"github.com/ignite/cli/v29/ignite/pkg/xurl"
igniteversion "github.com/ignite/cli/v29/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)
}