mukan-ignite/ignite/pkg/cosmosanalysis/module/module.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

419 lines
12 KiB
Go

package module
import (
"context"
"fmt"
"path"
"path/filepath"
"strings"
"golang.org/x/mod/semver"
"github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis"
"github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis/app"
"github.com/ignite/cli/v29/ignite/pkg/errors"
"github.com/ignite/cli/v29/ignite/pkg/gomodule"
"github.com/ignite/cli/v29/ignite/pkg/protoanalysis"
"github.com/ignite/cli/v29/ignite/pkg/xstrings"
)
// Msgs is a module import path-sdk msgs pair.
type Msgs map[string][]string
// Module keeps metadata about a Cosmos SDK module.
type Module struct {
// Name of the module.
Name string `json:"name,omitempty"`
// GoModulePath of the app where the module is defined.
GoModulePath string `json:"go_module_path,omitempty"`
// Pkg holds the proto package info.
Pkg protoanalysis.Package `json:"package,omitempty"`
// Msgs is a list of sdk.Msg implementation of the module.
Msgs []Msg `json:"messages,omitempty"`
// HTTPQueries is a list of module queries.
HTTPQueries []HTTPQuery `json:"http_queries,omitempty"`
// Types is a list of proto types that might be used by module.
Types []Type `json:"types,omitempty"`
}
// Msg keeps metadata about an sdk.Msg implementation.
type Msg struct {
// Name of the type.
Name string `json:"name,omitempty"`
// URI of the type.
URI string `json:"uri,omitempty"`
// FilePath is the path of the proto file where message is defined.
FilePath string `json:"file_path,omitempty"`
}
// HTTPQuery is an sdk Query.
type HTTPQuery struct {
// Name of the RPC func.
Name string `json:"name,omitempty"`
// RequestType is the type of the request.
RequestType string `json:"request_type,omitempty"`
// ResponseType is the type of the response.
ResponseType string `json:"response_type,omitempty"`
// FullName of the query with service name and rpc func name.
FullName string `json:"full_name,omitempty"`
// Rules keeps info about configured HTTP rules of RPC functions.
Rules []protoanalysis.HTTPRule `json:"rules,omitempty"`
// Paginated indicates that the query is using pagination.
Paginated bool `json:"paginated,omitempty"`
// FilePath is the path of the .proto file where message is defined at.
FilePath string `json:"file_path,omitempty"`
}
// Type is a proto type that might be used by module.
type Type struct {
// Name of the type.
Name string `json:"name,omitempty"`
// FilePath is the path of the .proto file where message is defined at.
FilePath string `json:"file_path,omitempty"`
}
type moduleDiscoverer struct {
sourcePath string
protoPath string
basegopath string
registeredModules []string
}
// IsCosmosSDKPackage check if a Go import path is a Cosmos SDK package.
// These type of package have the "cosmossdk.io/x" prefix or "github.com/cosmos/cosmos-sdk" prefix.
func IsCosmosSDKPackage(path string) bool {
return strings.Contains(path, "cosmossdk.io/x/") || strings.Contains(path, "github.com/cosmos/cosmos-sdk")
}
// Discover discovers and returns modules and their types that are registered in the app
// chainRoot is the root path of the chain
// sourcePath is the root path of the go module which the proto dir is from
//
// Discovery algorithm make use of registered modules and proto definitions to find relevant
// registered modules. It does so by:
// 1. Getting all the registered Go modules from the app.
// 2. Parsing the proto files to find services and messages.
// 3. Check if the proto services are implemented in any of the registered modules.
func Discover(ctx context.Context, chainRoot, sourcePath string, options ...DiscoverOption) ([]Module, error) {
var o discoverOptions
for _, apply := range options {
apply(&o)
}
// find out base Go import path of the blockchain.
gm, err := gomodule.ParseAt(sourcePath)
if err != nil {
if errors.Is(err, gomodule.ErrGoModNotFound) {
return []Module{}, nil
}
return nil, err
}
// Find all the modules registered by the app
registeredModules, err := app.FindRegisteredModules(chainRoot)
if err != nil {
return nil, err
}
// Go import path of the app module
basegopath := gm.Module.Mod.Path
// Keep the custom app's modules and filter out the third
// party ones that are not defined within the app.
appModules := make([]string, 0)
for _, m := range registeredModules {
if strings.HasPrefix(m, basegopath) {
appModules = append(appModules, m)
}
}
if len(appModules) == 0 {
return []Module{}, nil
}
// Switch the proto path for "cosmossdk.io" module packages to the official Cosmos
// SDK package because the module packages doesn't contain the proto files. These
// files are only available from the Cosmos SDK package.
var protoPath string
if o.sdkDir != "" && IsCosmosSDKPackage(sourcePath) {
protoPath = switchCosmosSDKPackagePath(sourcePath, o.sdkDir)
} else {
protoPath = filepath.Join(sourcePath, o.protoDir)
}
md := &moduleDiscoverer{
protoPath: protoPath,
sourcePath: sourcePath,
basegopath: basegopath,
registeredModules: appModules,
}
// Find proto packages that belong to modules under x/.
pkgs, err := md.findModuleProtoPkgs(ctx)
if err != nil {
return nil, err
}
if len(pkgs) == 0 {
return []Module{}, nil
}
var modules []Module
for _, pkg := range pkgs {
m, err := md.discover(pkg)
if err != nil {
return nil, err
}
if m.Name == "" {
continue
}
modules = append(modules, m)
}
return modules, nil
}
// IsRootPath checks if a Go import path is a custom app module.
// Custom app modules are defined inside the "x" directory.
func IsRootPath(path string) bool {
return filepath.Base(filepath.Dir(path)) == "x"
}
// RootPath returns the Go import path of a custom app module.
// An empty string is returned when the path doesn't belong to a custom module.
func RootPath(path string) string {
for !IsRootPath(path) {
if path = filepath.Dir(path); path == "." {
return ""
}
}
return path
}
// RootGoImportPath returns a Go import path with the version suffix removed.
func RootGoImportPath(importPath string) string {
if p, v := path.Split(importPath); semver.IsValid(v) {
return strings.TrimRight(p, "/")
}
return importPath
}
// discover discovers and sdk module by a proto pkg.
func (d *moduleDiscoverer) discover(pkg protoanalysis.Package) (Module, error) {
// Check if the proto package services are implemented
// by any of the modules registered by the app.
if ok, err := d.isPkgFromRegisteredModule(pkg); err != nil || !ok {
return Module{}, err
}
if len(pkg.Services) == 0 {
return Module{}, nil
}
m := Module{
Name: pkg.ModuleName(),
GoModulePath: d.basegopath,
Pkg: pkg,
}
// isType whether if protomsg can be added as an any Type to Module.
isType := func(protomsg protoanalysis.Message) bool {
// do not use GenesisState type.
if protomsg.Name == "GenesisState" {
return false
}
// do not use if used as a request/return type of RPC.
for _, s := range pkg.Services {
for _, q := range s.RPCFuncs {
if q.RequestType == protomsg.Name || q.ReturnsType == protomsg.Name {
return false
}
}
}
return true
}
// fill types.
for _, protomsg := range pkg.Messages {
if !isType(protomsg) {
continue
}
m.Types = append(m.Types, Type{
Name: protomsg.Name,
FilePath: protomsg.Path,
})
}
// fill queries & messages.
for _, s := range pkg.Services {
for _, q := range s.RPCFuncs {
pkgmsg, ok := pkg.FindMessageByName(q.RequestType)
if !ok {
// no msg found in the proto defs corresponds to discovered sdk message.
// if it cannot be found, nothing to worry about, this means that it is used
// only internally and not open for actual use.
continue
}
switch s.Name {
case "Msg":
m.Msgs = append(m.Msgs, Msg{
Name: q.RequestType,
URI: fmt.Sprintf("%s.%s", pkg.Name, q.RequestType),
FilePath: pkgmsg.Path,
})
case "Query", "Service":
// no http rules means this query is not exposed as a REST endpoint.
if len(q.HTTPRules) == 0 {
continue
}
// check if the query is paginated.
isPaginated := false
for _, hr := range q.HTTPRules {
if hr.IsPaginated() {
isPaginated = true
break
}
}
m.HTTPQueries = append(m.HTTPQueries, HTTPQuery{
Name: q.Name,
FullName: s.Name + q.Name,
Rules: q.HTTPRules,
Paginated: isPaginated,
FilePath: pkgmsg.Path,
RequestType: q.RequestType,
ResponseType: q.ReturnsType,
})
}
}
}
return m, nil
}
func (d *moduleDiscoverer) findModuleProtoPkgs(ctx context.Context) ([]protoanalysis.Package, error) {
// find out all proto packages inside blockchain.
allprotopkgs, err := protoanalysis.Parse(ctx, nil, d.protoPath)
if err != nil {
return nil, err
}
// Remove version suffix from the Go import path if it exists.
// Proto files might omit the version in the Go import path even
// when the app module is using versioning.
basegopath := RootGoImportPath(d.basegopath)
// filter out proto packages that do not represent x/ modules of blockchain.
var xprotopkgs []protoanalysis.Package
for _, pkg := range allprotopkgs {
if !strings.HasPrefix(pkg.GoImportPath(), basegopath) {
continue
}
xprotopkgs = append(xprotopkgs, pkg)
}
return xprotopkgs, nil
}
// Checks if the proto package is implemented by any of the modules registered by the app.
func (d moduleDiscoverer) isPkgFromRegisteredModule(pkg protoanalysis.Package) (bool, error) {
// Get the Go module import defined by the proto package
goModuleImport := pkg.GoImportPath()
// Try to get the Go import path of the custom app module that should implement
// the package RPC services. When the import path doesn't import a package
// from the standard "x" folder use the path defined by the proto package.
// Using the custom app module root path guarantees that if the RPC services
// implementation exists in the module it will always be found.
if p := RootPath(goModuleImport); p != "" {
goModuleImport = p
}
// Get a Go import path with the version suffix removed
rootGoPath := RootGoImportPath(d.basegopath)
for _, m := range d.registeredModules {
// Extract the relative module path from the Go import path
implRelPath := strings.TrimPrefix(m, d.basegopath)
// Handle the case where the Go module has a version
// suffix and the registered module doesn't.
if implRelPath == m {
implRelPath = strings.TrimPrefix(m, rootGoPath)
}
// Absolute path to the app module
implPath := filepath.Join(d.sourcePath, implRelPath)
for _, s := range pkg.Services {
// List of the RPC service method names defined by the current proto service
methods := make([]string, len(s.RPCFuncs))
for i, rpcFunc := range s.RPCFuncs {
methods[i] = rpcFunc.Name
}
// Find the Go implementation of the service defined in the proto package
found, err := cosmosanalysis.DeepFindImplementation(implPath, methods)
if err != nil {
return false, err
}
// Sometimes the registered module definition is located in a different
// directory branch from where the RPC implementation is defined. In this
// case search the RPC implementation in all custom app module files.
if len(found) == 0 && strings.HasPrefix(m, goModuleImport) {
altImplRelPath := strings.TrimPrefix(goModuleImport, d.basegopath)
if altImplRelPath == goModuleImport {
altImplRelPath = strings.TrimPrefix(goModuleImport, rootGoPath)
}
altImplPath := filepath.Join(d.sourcePath, altImplRelPath)
found, err = cosmosanalysis.DeepFindImplementation(altImplPath, methods)
if err != nil {
return false, err
}
}
if len(found) > 0 {
return true, nil
}
}
}
return false, nil
}
func switchCosmosSDKPackagePath(srcPath, sdkDir string) string {
modName := xstrings.StringBetween(srcPath, "/x/", "@")
if modName == "" {
return srcPath
}
return filepath.Join(sdkDir, "proto", "cosmos", modName)
}