package modulecreate import ( "bytes" "fmt" "go/ast" "go/format" "go/parser" "go/token" "io/fs" "path/filepath" "github.com/gobuffalo/genny/v2" "github.com/gobuffalo/plush/v4" "github.com/ignite/cli/v29/ignite/pkg/errors" "github.com/ignite/cli/v29/ignite/pkg/gomodulepath" "github.com/ignite/cli/v29/ignite/pkg/protoanalysis/protoutil" "github.com/ignite/cli/v29/ignite/pkg/xast" "github.com/ignite/cli/v29/ignite/pkg/xgenny" "github.com/ignite/cli/v29/ignite/pkg/xstrings" "github.com/ignite/cli/v29/ignite/templates/field/plushhelpers" "github.com/ignite/cli/v29/ignite/templates/module" ) // NewIBC returns the generator to scaffold the implementation of the IBCModule interface inside a module. func NewIBC(opts *CreateOptions) (*genny.Generator, error) { subFs, err := fs.Sub(fsIBC, "files/ibc") if err != nil { return nil, errors.Errorf("fail to generate sub: %w", err) } g := genny.New() g.RunFn(genesisModify(opts)) g.RunFn(genesisTypesModify(opts)) g.RunFn(genesisProtoModify(opts)) if err := g.OnlyFS(subFs, nil, nil); err != nil { return g, errors.Errorf("generator fs: %w", err) } appModulePath := gomodulepath.ExtractAppPath(opts.ModulePath) ctx := plush.NewContext() ctx.Set("moduleName", opts.ModuleName) ctx.Set("modulePath", opts.ModulePath) ctx.Set("appName", opts.AppName) ctx.Set("protoVer", opts.ProtoVer) ctx.Set("ibcOrdering", opts.IBCOrdering) ctx.Set("dependencies", opts.Dependencies) ctx.Set("protoPkgName", module.ProtoPackageName(appModulePath, opts.ModuleName, opts.ProtoVer)) plushhelpers.ExtendPlushContext(ctx) g.Transformer(xgenny.Transformer(ctx)) g.Transformer(genny.Replace("{{protoDir}}", opts.ProtoDir)) g.Transformer(genny.Replace("{{appName}}", opts.AppName)) g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName)) g.Transformer(genny.Replace("{{protoVer}}", opts.ProtoVer)) return g, nil } func genesisModify(opts *CreateOptions) genny.RunFn { return func(r *genny.Runner) error { path := filepath.Join("x", opts.ModuleName, "keeper/genesis.go") f, err := r.Disk.Find(path) if err != nil { return err } // Genesis init replacementModuleInit := `if err := k.Port.Set(ctx, genState.PortId); err != nil { return err }` content, err := xast.ModifyFunction( f.String(), "InitGenesis", xast.AppendFuncCode(replacementModuleInit), ) if err != nil { return err } // Genesis export replacementModuleExport := `genesis.PortId, err = k.Port.Get(ctx) if err != nil && !errors.Is(err, collections.ErrNotFound) { return nil, err }` content, err = xast.ModifyFunction( content, "ExportGenesis", xast.AppendFuncCode(replacementModuleExport), ) if err != nil { return err } newFile := genny.NewFileS(path, content) return r.File(newFile) } } func genesisTypesModify(opts *CreateOptions) genny.RunFn { return func(r *genny.Runner) error { path := filepath.Join("x", opts.ModuleName, "types/genesis.go") f, err := r.Disk.Find(path) if err != nil { return err } // Import content, err := xast.AppendImports( f.String(), xast.WithNamedImport("host", "github.com/cosmos/ibc-go/v10/modules/core/24-host"), ) if err != nil { return err } // Default genesis content, err = xast.ModifyFunction( content, "DefaultGenesis", xast.AppendFuncStruct("GenesisState", "PortId", "PortID"), ) if err != nil { return err } // Validate genesis // PlaceholderIBCGenesisTypeValidate replacementTypesValidate := `if err := host.PortIdentifierValidator(gs.PortId); err != nil { return err }` content, err = xast.ModifyFunction( content, "Validate", xast.AppendFuncCode(replacementTypesValidate), ) if err != nil { return err } newFile := genny.NewFileS(path, content) return r.File(newFile) } } // Modifies genesis.proto to add a new field. // // What it depends on: // - Existence of a message named 'GenesisState' in genesis.proto. func genesisProtoModify(opts *CreateOptions) genny.RunFn { return func(r *genny.Runner) error { path := opts.ProtoFile("genesis.proto") f, err := r.Disk.Find(path) if err != nil { return err } protoFile, err := protoutil.ParseProtoFile(f) if err != nil { return err } // Grab GenesisState and add next (always 2, I gather) available field. // TODO: typed.ProtoGenesisStateMessage exists but in subfolder, so we can't use it here, refactor? genesisState, err := protoutil.GetMessageByName(protoFile, "GenesisState") if err != nil { return errors.Errorf("couldn't find message 'GenesisState' in %s: %w", path, err) } field := protoutil.NewField("port_id", "string", protoutil.NextUniqueID(genesisState)) protoutil.Append(genesisState, field) newFile := genny.NewFileS(path, protoutil.Print(protoFile)) return r.File(newFile) } } func appIBCModify(opts *CreateOptions) genny.RunFn { return func(r *genny.Runner) error { path := module.PathIBCConfigGo f, err := r.Disk.Find(path) if err != nil { return err } // Import content, err := xast.AppendImports( f.String(), xast.WithNamedImport( fmt.Sprintf("%[1]vmodule", opts.ModuleName), fmt.Sprintf("%[1]v/x/%[2]v/module", opts.ModulePath, opts.ModuleName), ), xast.WithNamedImport( fmt.Sprintf("%[1]vmoduletypes", opts.ModuleName), fmt.Sprintf("%[1]v/x/%[2]v/types", opts.ModulePath, opts.ModuleName), ), ) if err != nil { return err } content, err = addIBCModuleRoute(content, opts.ModuleName) if err != nil { return err } newFile := genny.NewFileS(path, content) return r.File(newFile) } } func addIBCModuleRoute(content, moduleName string) (string, error) { fileSet := token.NewFileSet() file, err := parser.ParseFile(fileSet, "", content, parser.ParseComments) if err != nil { return "", err } commentMap := ast.NewCommentMap(fileSet, file, file.Comments) registerIBCModules := findFunctionByName(file, "registerIBCModules") if registerIBCModules == nil { return "", errors.New(`function "registerIBCModules" not found`) } moduleNameExprText := fmt.Sprintf("%smoduletypes.ModuleName", moduleName) moduleConstructorExprText := fmt.Sprintf( "%smodule.NewIBCModule(app.appCodec, app.%sKeeper)", moduleName, xstrings.Title(moduleName), ) hasRoute, err := hasAddRoute(registerIBCModules, moduleNameExprText, fileSet) if err != nil { return "", err } if !hasRoute { insertionIndex := -1 for i, stmt := range registerIBCModules.Body.List { assignStmt, ok := stmt.(*ast.AssignStmt) if !ok || len(assignStmt.Lhs) != 1 { continue } lhs, ok := assignStmt.Lhs[0].(*ast.Ident) if !ok || lhs.Name != "ibcv2Router" { continue } insertionIndex = i break } if insertionIndex == -1 { return "", errors.New(`assignment to "ibcv2Router" not found`) } stmtCode := fmt.Sprintf( "ibcRouter = ibcRouter.AddRoute(%s, %s)", moduleNameExprText, moduleConstructorExprText, ) statements, err := parseStatements(stmtCode) if err != nil { return "", err } if len(statements) != 1 { return "", errors.New("unexpected number of statements while creating ibc route assignment") } registerIBCModules.Body.List = append( registerIBCModules.Body.List[:insertionIndex], append([]ast.Stmt{statements[0]}, registerIBCModules.Body.List[insertionIndex:]...)..., ) } file.Comments = commentMap.Filter(file).Comments() var buf bytes.Buffer if err := format.Node(&buf, fileSet, file); err != nil { return "", err } formatted, err := format.Source(buf.Bytes()) if err != nil { return "", err } return string(formatted), nil } func hasAddRoute(funcDecl *ast.FuncDecl, moduleNameExpr string, fileSet *token.FileSet) (bool, error) { var ( found bool err error ) ast.Inspect(funcDecl, func(n ast.Node) bool { if found || err != nil { return false } callExpr, ok := n.(*ast.CallExpr) if !ok { return true } selector, ok := callExpr.Fun.(*ast.SelectorExpr) if !ok || selector.Sel.Name != "AddRoute" || len(callExpr.Args) == 0 { return true } argText, argErr := exprString(fileSet, callExpr.Args[0]) if argErr != nil { err = argErr return false } if normalizedExpr(argText) == normalizedExpr(moduleNameExpr) { found = true return false } return true }) return found, err } func parseStatements(code string) ([]ast.Stmt, error) { fileSet := token.NewFileSet() file, err := parser.ParseFile(fileSet, "", fmt.Sprintf("package p\nfunc _(){\n%s\n}", code), 0) if err != nil { return nil, err } funcDecl, ok := file.Decls[0].(*ast.FuncDecl) if !ok || funcDecl.Body == nil { return nil, errors.New("failed to parse statements") } return funcDecl.Body.List, nil } func findFunctionByName(file *ast.File, funcName string) *ast.FuncDecl { for _, decl := range file.Decls { funcDecl, ok := decl.(*ast.FuncDecl) if !ok { continue } if funcDecl.Name.Name == funcName { return funcDecl } } return nil }