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
327 lines
8.4 KiB
Go
327 lines
8.4 KiB
Go
package scaffolder
|
|
|
|
import (
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gobuffalo/genny/v2"
|
|
|
|
appanalysis "github.com/ignite/cli/v29/ignite/pkg/cosmosanalysis/app"
|
|
"github.com/ignite/cli/v29/ignite/pkg/errors"
|
|
"github.com/ignite/cli/v29/ignite/pkg/multiformatname"
|
|
"github.com/ignite/cli/v29/ignite/templates/field"
|
|
"github.com/ignite/cli/v29/ignite/templates/module"
|
|
modulecreate "github.com/ignite/cli/v29/ignite/templates/module/create"
|
|
)
|
|
|
|
const (
|
|
moduleDir = "x"
|
|
modulePkg = "module"
|
|
)
|
|
|
|
var (
|
|
// reservedNames are either names from the default modules defined in a Cosmos-SDK app or names used in the default query and tx CLI namespace.
|
|
// A new module's name can't be equal to a reserved name.
|
|
// A map is used for direct comparing.
|
|
reservedNames = map[string]struct{}{
|
|
"account": {},
|
|
"block": {},
|
|
"broadcast": {},
|
|
"encode": {},
|
|
"multisign": {},
|
|
"sign": {},
|
|
"tx": {},
|
|
"txs": {},
|
|
"consumer": {}, // ICS consumer module
|
|
"ccvconsumer": {}, // ICS consumer module
|
|
"CCV": {}, // ICS consumer module
|
|
"capability": {},
|
|
"auth": {},
|
|
"bank": {},
|
|
"distribution": {},
|
|
"staking": {},
|
|
"slashing": {},
|
|
"gov": {},
|
|
"mint": {},
|
|
"ibc": {},
|
|
"genutil": {},
|
|
"evidence": {},
|
|
"authz": {},
|
|
"transfer": {}, // IBC transfer
|
|
"interchainaccounts": {},
|
|
"feeibc": {},
|
|
"feegrant": {},
|
|
"params": {},
|
|
"upgrade": {},
|
|
"vesting": {},
|
|
"circuit": {},
|
|
"nft": {},
|
|
"group": {},
|
|
"consensus": {},
|
|
"epochs": {},
|
|
"protocolpool": {},
|
|
}
|
|
|
|
// defaultStoreKeys are the names of the default store keys defined in a Cosmos-SDK app.
|
|
// A new module's name can't have a defined store key in its prefix because of potential store key collision.
|
|
defaultStoreKeys = []string{
|
|
"capability",
|
|
"acc", // auth module
|
|
"bank",
|
|
"distribution",
|
|
"staking",
|
|
"slashing",
|
|
"gov",
|
|
"mint",
|
|
"ibc",
|
|
"transfer", // IBC transfer
|
|
"feeibc",
|
|
"evidence",
|
|
"feegrant",
|
|
"params",
|
|
"upgrade",
|
|
"circuit",
|
|
"nft",
|
|
"group",
|
|
"consensus",
|
|
}
|
|
)
|
|
|
|
// moduleCreationOptions holds options for creating a new module.
|
|
type moduleCreationOptions struct {
|
|
// ibc true if the module is an ibc module.
|
|
ibc bool
|
|
|
|
// params list of parameters.
|
|
params []string
|
|
|
|
// moduleConfigs list of module configs.
|
|
moduleConfigs []string
|
|
|
|
// ibcChannelOrdering ibc channel ordering.
|
|
ibcChannelOrdering string
|
|
|
|
// dependencies list of module dependencies.
|
|
dependencies []modulecreate.Dependency
|
|
}
|
|
|
|
// ModuleCreationOption configures Chain.
|
|
type ModuleCreationOption func(*moduleCreationOptions)
|
|
|
|
// WithIBC scaffolds a module with IBC enabled.
|
|
func WithIBC() ModuleCreationOption {
|
|
return func(m *moduleCreationOptions) {
|
|
m.ibc = true
|
|
}
|
|
}
|
|
|
|
// WithParams scaffolds a module with params.
|
|
func WithParams(params []string) ModuleCreationOption {
|
|
return func(m *moduleCreationOptions) {
|
|
m.params = params
|
|
}
|
|
}
|
|
|
|
// WithModuleConfigs scaffolds a module with module configs.
|
|
func WithModuleConfigs(moduleConfigs []string) ModuleCreationOption {
|
|
return func(m *moduleCreationOptions) {
|
|
m.moduleConfigs = moduleConfigs
|
|
}
|
|
}
|
|
|
|
// WithIBCChannelOrdering configures channel ordering of the IBC module.
|
|
func WithIBCChannelOrdering(ordering string) ModuleCreationOption {
|
|
return func(m *moduleCreationOptions) {
|
|
switch ordering {
|
|
case "ordered":
|
|
m.ibcChannelOrdering = "ORDERED"
|
|
case "unordered":
|
|
m.ibcChannelOrdering = "UNORDERED"
|
|
default:
|
|
m.ibcChannelOrdering = "NONE"
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithDependencies specifies the name of the modules that the module depends on.
|
|
func WithDependencies(dependencies []modulecreate.Dependency) ModuleCreationOption {
|
|
return func(m *moduleCreationOptions) {
|
|
m.dependencies = dependencies
|
|
}
|
|
}
|
|
|
|
// CreateModule creates a new empty module in the scaffolded app.
|
|
func (s Scaffolder) CreateModule(
|
|
moduleName string,
|
|
options ...ModuleCreationOption,
|
|
) error {
|
|
mfName, err := multiformatname.NewName(moduleName, multiformatname.NoNumber)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
moduleName = mfName.LowerCase
|
|
|
|
// Check if the module name is valid
|
|
if err := checkModuleName(s.appPath, moduleName); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if the module already exist
|
|
ok, err := moduleExists(s.appPath, moduleName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ok {
|
|
return errors.Errorf("the module %v already exists", moduleName)
|
|
}
|
|
|
|
// Apply the options
|
|
var creationOpts moduleCreationOptions
|
|
for _, apply := range options {
|
|
apply(&creationOpts)
|
|
}
|
|
|
|
// Parse params with the associated type
|
|
params, err := field.ParseFields(creationOpts.params, checkForbiddenTypeIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse configs with the associated type
|
|
configs, err := field.ParseFields(creationOpts.moduleConfigs, checkForbiddenTypeIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check dependencies
|
|
if err := checkDependencies(creationOpts.dependencies, s.appPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
opts := &modulecreate.CreateOptions{
|
|
ModuleName: moduleName,
|
|
ModulePath: s.modpath.RawPath,
|
|
Params: params,
|
|
Configs: configs,
|
|
AppName: s.modpath.Package,
|
|
AppPath: s.appPath,
|
|
ProtoDir: s.protoDir,
|
|
ProtoVer: "v1", // TODO(@julienrbrt): possibly in the future add flag to specify custom proto version.
|
|
IsIBC: creationOpts.ibc,
|
|
IBCOrdering: creationOpts.ibcChannelOrdering,
|
|
Dependencies: creationOpts.dependencies,
|
|
}
|
|
|
|
g, err := modulecreate.NewGenerator(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gens := []*genny.Generator{g}
|
|
|
|
// Scaffold IBC module
|
|
if opts.IsIBC {
|
|
g, err = modulecreate.NewIBC(opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gens = append(gens, g)
|
|
}
|
|
gens = append(gens, modulecreate.NewAppModify(opts))
|
|
|
|
err = s.Run(gens...)
|
|
var validationErr errors.ValidationError
|
|
if err != nil && !errors.As(err, &validationErr) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// moduleExists checks if the module exists in the app.
|
|
func moduleExists(appPath string, moduleName string) (bool, error) {
|
|
absPath, err := filepath.Abs(filepath.Join(appPath, moduleDir, moduleName))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
_, err = os.Stat(absPath)
|
|
if os.IsNotExist(err) {
|
|
// The module doesn't exist
|
|
return false, nil
|
|
}
|
|
|
|
return err == nil, err
|
|
}
|
|
|
|
// checkModuleName checks if the name can be used as a module name.
|
|
func checkModuleName(appPath, moduleName string) error {
|
|
// go keyword
|
|
if token.Lookup(moduleName).IsKeyword() {
|
|
return errors.Errorf("%s is a Go keyword", moduleName)
|
|
}
|
|
|
|
// check if the name is a reserved name
|
|
if _, ok := reservedNames[moduleName]; ok {
|
|
return errors.Errorf("%s is a reserved name and can't be used as a module name", moduleName)
|
|
}
|
|
|
|
checkPrefix := func(name, prefix string) error {
|
|
if strings.HasPrefix(name, prefix) {
|
|
return errors.Errorf("the module name can't be prefixed with %s because of potential store key collision", prefix)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// check if the name can imply potential store key collision
|
|
for _, defaultStoreKey := range defaultStoreKeys {
|
|
if err := checkPrefix(moduleName, defaultStoreKey); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// check store key with user's defined modules
|
|
// we consider all user's defined modules use the module name as the store key
|
|
entries, err := os.ReadDir(filepath.Join(appPath, moduleDir))
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
if err := checkPrefix(moduleName, entry.Name()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkDependencies perform checks on the dependencies.
|
|
func checkDependencies(dependencies []modulecreate.Dependency, appPath string) error {
|
|
depMap := make(map[string]struct{})
|
|
for _, dep := range dependencies {
|
|
// check the dependency has been registered
|
|
path := filepath.Join(appPath, module.PathAppModule)
|
|
if err := appanalysis.CheckKeeper(path, dep.KeeperName()); err != nil {
|
|
return errors.Errorf(
|
|
"the module cannot have %s as a dependency: %w",
|
|
dep.Name,
|
|
err,
|
|
)
|
|
}
|
|
|
|
// check duplicated
|
|
_, ok := depMap[dep.Name]
|
|
if ok {
|
|
return errors.Errorf("%s is a duplicated dependency", dep)
|
|
}
|
|
depMap[dep.Name] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
}
|