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
419 lines
12 KiB
Go
419 lines
12 KiB
Go
package module
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/semver"
|
|
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cosmosanalysis"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cosmosanalysis/app"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/errors"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/gomodule"
|
|
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/protoanalysis"
|
|
"git.cw.tr/mukan-network/mukan-ignite/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)
|
|
}
|