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) }