From 7d7ee9436b20f4c9edc537311b743c0d515806bb Mon Sep 17 00:00:00 2001 From: Reece Williams <31943163+Reecepbcups@users.noreply.github.com> Date: Wed, 14 Feb 2024 22:46:20 -0600 Subject: [PATCH] feat: base proto & module generation (#39) * wip proto * rename tweaks (untested) * proto renames to `module` and `simapp` * get example module setup with proto & script * create new module base template cmd * fix namespace overlap for depinject module * fix straggling loggers * replace all with example -> new x/ name * simplify * cleanup * already exist TODO --- cmd/spawn/main.go | 1 + cmd/spawn/module.go | 131 ++++++++++++++++++++ simapp/Makefile | 4 +- simapp/app/app.go | 2 +- simapp/embed.go | 3 + simapp/proto/buf.gen.gogo.yaml | 8 ++ simapp/proto/buf.gen.pulsar.yaml | 20 +++ simapp/proto/buf.lock | 19 +++ simapp/proto/buf.yaml | 17 +++ simapp/proto/example/module/v1/module.proto | 13 ++ simapp/proto/example/v1/genesis.proto | 22 ++++ simapp/proto/example/v1/query.proto | 24 ++++ simapp/proto/example/v1/tx.proto | 40 ++++++ simapp/scripts/protocgen.sh | 37 ++++++ spawn/chain_config.go | 19 +-- spawn/file_content.go | 4 +- spawn/parser.go | 10 +- 17 files changed, 355 insertions(+), 19 deletions(-) create mode 100644 cmd/spawn/module.go create mode 100644 simapp/proto/buf.gen.gogo.yaml create mode 100644 simapp/proto/buf.gen.pulsar.yaml create mode 100644 simapp/proto/buf.lock create mode 100644 simapp/proto/buf.yaml create mode 100644 simapp/proto/example/module/v1/module.proto create mode 100644 simapp/proto/example/v1/genesis.proto create mode 100644 simapp/proto/example/v1/query.proto create mode 100644 simapp/proto/example/v1/tx.proto create mode 100644 simapp/scripts/protocgen.sh diff --git a/cmd/spawn/main.go b/cmd/spawn/main.go index 0decbe65..5729776e 100644 --- a/cmd/spawn/main.go +++ b/cmd/spawn/main.go @@ -22,6 +22,7 @@ func main() { rootCmd.AddCommand(newChain) rootCmd.AddCommand(LocalICCmd) rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(moduleCmd) rootCmd.PersistentFlags().String(LogLevelFlag, "info", "log level (debug, info, warn, error)") diff --git a/cmd/spawn/module.go b/cmd/spawn/module.go new file mode 100644 index 00000000..b4ef8093 --- /dev/null +++ b/cmd/spawn/module.go @@ -0,0 +1,131 @@ +package main + +import ( + "fmt" + "io/fs" + "log/slog" + "os" + "path" + "strings" + + "github.com/spf13/cobra" + "github.com/strangelove-ventures/simapp" + "gitub.com/strangelove-ventures/spawn/spawn" +) + +var moduleCmd = &cobra.Command{ + Use: "module [name]", + Short: "Create a new module scaffolding", + Example: `spawn module mymodule`, + Args: cobra.ExactArgs(1), + Aliases: []string{"m", "mod", "proto"}, + Run: func(cmd *cobra.Command, args []string) { + logger := GetLogger() + + // ext name is the x/ 'module' name. + extName := strings.ToLower(args[0]) + + specialChars := "!@#$%^&*()_+{}|-:<>?`=[]\\;',./~" + for _, char := range specialChars { + if strings.Contains(extName, string(char)) { + logger.Error("Special characters are not allowed in module names") + return + } + } + + cwd, err := os.Getwd() + if err != nil { + logger.Error("Error getting current working directory", err) + return + } + + // see if cwd/x/extName exists + if _, err := os.Stat(path.Join(cwd, "x", extName)); err == nil { + logger.Error("TODO: Module already exists in x/. (Prompt UI to perform actions? (protoc-gen, generate keeper, setup to app.go, etc?))", "module", extName) + return + } + + SetupModule(GetLogger(), extName) + + // Announce the new module & how to code gen + fmt.Printf("\n🎉 New Module '%s' generated!\n", extName) + fmt.Println("🏅Generate Go Code:") + fmt.Println(" - $ make proto-gen # convert proto -> code and depinject") + }, +} + +func SetupModule(logger *slog.Logger, extName string) error { + protoFS := simapp.ProtoModuleFS + + if err := os.MkdirAll("proto", 0755); err != nil { + panic(err) + } + + cwd, err := os.Getwd() + if err != nil { + fmt.Println("Error getting current working directory", err) + return err + } + + moduleName := readCurrentModuleName(path.Join(cwd, "go.mod")) + moduleNameProto := convertModuleNameToProto(moduleName) + + return fs.WalkDir(protoFS, ".", func(relPath string, d fs.DirEntry, e error) error { + newPath := path.Join(cwd, relPath) + fc, err := spawn.GetFileContent(logger, newPath, protoFS, relPath, d) + if err != nil { + return err + } else if fc == nil { + return nil + } + + // rename proto path for the new module + exampleProtoPath := path.Join("proto", "example") + if fc.ContainsPath(exampleProtoPath) { + newBinPath := path.Join("proto", extName) + fc.NewPath = strings.ReplaceAll(fc.NewPath, exampleProtoPath, newBinPath) + } + + // any file content that has github.com/strangelove-ventures/simapp replace to moduleName + + fc.ReplaceAll("github.com/strangelove-ventures/simapp", moduleName) + fc.ReplaceAll("strangelove_ventures.simapp", moduleNameProto) + + // replace example -> the new x/ name + fc.ReplaceAll("example", extName) + + // TODO: set the values in the keepers / msg server automatically + + return fc.Save() + }) +} + +// readCurrentModuleName reads the module name from the go.mod file on the host machine. +func readCurrentModuleName(loc string) string { + if !strings.HasSuffix(loc, "go.mod") { + loc = path.Join(loc, "go.mod") + } + + // read file from path into a []byte + var fileContent []byte + fileContent, err := os.ReadFile(loc) + if err != nil { + fmt.Println("Error reading file", err) + return "" + } + + lines := strings.Split(string(fileContent), "\n") + for _, line := range lines { + if strings.Contains(line, "module") { + return strings.Split(line, " ")[1] + } + } + + return "" +} + +func convertModuleNameToProto(moduleName string) string { + // github.com/rollchains/myproject -> rollchains.myproject + text := strings.Replace(moduleName, "github.com/", "", 1) + return strings.Replace(text, "/", ".", -1) +} diff --git a/simapp/Makefile b/simapp/Makefile index f0e69388..725699d1 100644 --- a/simapp/Makefile +++ b/simapp/Makefile @@ -10,7 +10,7 @@ SIMAPP = ./app # for dockerized protobuf tools DOCKER := $(shell which docker) -HTTPS_GIT := https://github.com/CosmWasm/wasmd.git +HTTPS_GIT := github.com/strangelove-ventures/simapp.git export GO111MODULE = on @@ -173,7 +173,7 @@ format: format-tools ############################################################################### ### Protobuf ### ############################################################################### -protoVer=0.14.0 +protoVer=0.13.2 protoImageName=ghcr.io/cosmos/proto-builder:$(protoVer) protoImage=$(DOCKER) run --rm -v $(CURDIR):/workspace --workdir /workspace $(protoImageName) diff --git a/simapp/app/app.go b/simapp/app/app.go index 5c3ea3dc..c2cd90d6 100644 --- a/simapp/app/app.go +++ b/simapp/app/app.go @@ -152,7 +152,7 @@ import ( globalfeetypes "github.com/reecepbcups/globalfee/x/globalfee/types" ) -const appName = "WasmApp" +const appName = "CosmWasmApp" var ( NodeDir = ".wasmd" diff --git a/simapp/embed.go b/simapp/embed.go index 826d3d66..3ebc49b9 100644 --- a/simapp/embed.go +++ b/simapp/embed.go @@ -13,3 +13,6 @@ var SimAppFS embed.FS //go:embed interchaintest/* var ICTestFS embed.FS + +//go:embed proto/* +var ProtoModuleFS embed.FS diff --git a/simapp/proto/buf.gen.gogo.yaml b/simapp/proto/buf.gen.gogo.yaml new file mode 100644 index 00000000..e3cd0b1a --- /dev/null +++ b/simapp/proto/buf.gen.gogo.yaml @@ -0,0 +1,8 @@ +version: v1 +plugins: + - name: gocosmos + out: .. + opt: plugins=grpc,Mgoogle/protobuf/any.proto=github.com/cosmos/cosmos-sdk/codec/types,Mcosmos/orm/v1/orm.proto=cosmossdk.io/orm + - name: grpc-gateway + out: .. + opt: logtostderr=true,allow_colon_final_segments=true diff --git a/simapp/proto/buf.gen.pulsar.yaml b/simapp/proto/buf.gen.pulsar.yaml new file mode 100644 index 00000000..022dd214 --- /dev/null +++ b/simapp/proto/buf.gen.pulsar.yaml @@ -0,0 +1,20 @@ +version: v1 +managed: + enabled: true + go_package_prefix: + default: github.com/strangelove-ventures/simapp/api + except: + - buf.build/googleapis/googleapis + - buf.build/cosmos/gogo-proto + - buf.build/cosmos/cosmos-proto + - buf.build/cosmos/cosmos-sdk +plugins: + - name: go-pulsar + out: .. + opt: paths=source_relative,Mcosmos/app/v1alpha1/module.proto=cosmossdk.io/api/cosmos/app/v1alpha1 + - name: go-grpc + out: .. + opt: paths=source_relative,Mcosmos/app/v1alpha1/module.proto=cosmossdk.io/api/cosmos/app/v1alpha1 + - name: go-cosmos-orm + out: .. + opt: paths=source_relative,Mcosmos/app/v1alpha1/module.proto=cosmossdk.io/api/cosmos/app/v1alpha1 diff --git a/simapp/proto/buf.lock b/simapp/proto/buf.lock new file mode 100644 index 00000000..4f85b692 --- /dev/null +++ b/simapp/proto/buf.lock @@ -0,0 +1,19 @@ +# Generated by buf. DO NOT EDIT. +version: v1 +deps: + - remote: buf.build + owner: cosmos + repository: cosmos-proto + commit: 1935555c206d4afb9e94615dfd0fad31 + - remote: buf.build + owner: cosmos + repository: cosmos-sdk + commit: d5661b4f6ef64bb1b5beb6cb7bd705b7 + - remote: buf.build + owner: cosmos + repository: gogo-proto + commit: 5e5b9fdd01804356895f8f79a6f1ddc1 + - remote: buf.build + owner: googleapis + repository: googleapis + commit: cc916c31859748a68fd229a3c8d7a2e8 diff --git a/simapp/proto/buf.yaml b/simapp/proto/buf.yaml new file mode 100644 index 00000000..202f3f2e --- /dev/null +++ b/simapp/proto/buf.yaml @@ -0,0 +1,17 @@ +version: v1 +deps: + - buf.build/cosmos/cosmos-sdk # pin the Cosmos SDK version + - buf.build/cosmos/cosmos-proto + - buf.build/cosmos/gogo-proto + - buf.build/googleapis/googleapis +lint: + use: + - DEFAULT + - COMMENTS + - FILE_LOWER_SNAKE_CASE + except: + - UNARY_RPC + - COMMENT_FIELD + - SERVICE_SUFFIX + - PACKAGE_VERSION_SUFFIX + - RPC_REQUEST_STANDARD_NAME diff --git a/simapp/proto/example/module/v1/module.proto b/simapp/proto/example/module/v1/module.proto new file mode 100644 index 00000000..e7cb0999 --- /dev/null +++ b/simapp/proto/example/module/v1/module.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package strangelove_ventures.simapp.example.module.v1; + +import "cosmos/app/v1alpha1/module.proto"; + +// Module is the app config object of the module. +// Learn more: https://docs.cosmos.network/main/building-modules/depinject +message Module { + option (cosmos.app.v1alpha1.module) = { + go_import : "github.com/strangelove-ventures/simapp" + }; +} \ No newline at end of file diff --git a/simapp/proto/example/v1/genesis.proto b/simapp/proto/example/v1/genesis.proto new file mode 100644 index 00000000..a5047f74 --- /dev/null +++ b/simapp/proto/example/v1/genesis.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; +package example.v1; + +import "gogoproto/gogo.proto"; +import "amino/amino.proto"; + +option go_package = "github.com/strangelove-ventures/simapp/x/example/types"; + +// GenesisState defines the module genesis state +message GenesisState { + // Params defines all the paramaters of the module. + Params params = 1 [(gogoproto.nullable) = false]; +} + +// Params defines the set of module parameters. +message Params { + option (amino.name) = "example/params"; + option (gogoproto.equal) = true; + option (gogoproto.goproto_stringer) = false; + + bool some_value = 2; +} \ No newline at end of file diff --git a/simapp/proto/example/v1/query.proto b/simapp/proto/example/v1/query.proto new file mode 100644 index 00000000..08b7aa62 --- /dev/null +++ b/simapp/proto/example/v1/query.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; +package example.v1; + +import "google/api/annotations.proto"; +import "example/v1/genesis.proto"; + +option go_package = "github.com/strangelove-ventures/simapp/x/example/types"; + +// Query provides defines the gRPC querier service. +service Query { + // Params queries all parameters of the module. + rpc Params(QueryParamsRequest) returns (QueryParamsResponse) { + option (google.api.http).get = "/example/v1/params"; + } +} + +// QueryParamsRequest is the request type for the Query/Params RPC method. +message QueryParamsRequest {} + +// QueryParamsResponse is the response type for the Query/Params RPC method. +message QueryParamsResponse { + // params defines the parameters of the module. + Params params = 1; +} diff --git a/simapp/proto/example/v1/tx.proto b/simapp/proto/example/v1/tx.proto new file mode 100644 index 00000000..b9b475f3 --- /dev/null +++ b/simapp/proto/example/v1/tx.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +package example.v1; + +import "cosmos/msg/v1/msg.proto"; +import "example/v1/genesis.proto"; +import "gogoproto/gogo.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "github.com/strangelove-ventures/simapp/x/example/types"; + +// Msg defines the Msg service. +service Msg { + option (cosmos.msg.v1.service) = true; + + // UpdateParams defines a governance operation for updating the parameters. + // + // Since: cosmos-sdk 0.47 + rpc UpdateParams(MsgUpdateParams) returns (MsgUpdateParamsResponse); +} + +// MsgUpdateParams is the Msg/UpdateParams request type. +// +// Since: cosmos-sdk 0.47 +message MsgUpdateParams { + option (cosmos.msg.v1.signer) = "authority"; + + // authority is the address of the governance account. + string authority = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // params defines the parameters to update. + // + // NOTE: All parameters must be supplied. + Params params = 2 [(gogoproto.nullable) = false]; +} + +// MsgUpdateParamsResponse defines the response structure for executing a +// MsgUpdateParams message. +// +// Since: cosmos-sdk 0.47 +message MsgUpdateParamsResponse {} diff --git a/simapp/scripts/protocgen.sh b/simapp/scripts/protocgen.sh new file mode 100644 index 00000000..18e5e3da --- /dev/null +++ b/simapp/scripts/protocgen.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -e + +echo "Generating gogo proto code" +cd proto +proto_dirs=$(find . -path -prune -o -name '*.proto' -print0 | xargs -0 -n1 dirname | sort | uniq) +for dir in $proto_dirs; do + for file in $(find "${dir}" -maxdepth 1 -name '*.proto'); do + # this regex checks if a proto file has its go_package set to github.com/strangelove-ventures/poa/... + # gogo proto files SHOULD ONLY be generated if this is false + # we don't want gogo proto to run for proto files which are natively built for google.golang.org/protobuf + if grep -q "option go_package" "$file" && grep -H -o -c 'option go_package.*github.com/strangelove-ventures/simapp/api' "$file" | grep -q ':0$'; then + buf generate --template buf.gen.gogo.yaml $file + fi + done +done + +echo "Generating pulsar proto code" +buf generate --template buf.gen.pulsar.yaml + +cd .. + +mv github.com/strangelove-ventures/simapp/* ./ +rm -rf github.com + +# Copy files over for dep injection +rm -rf api && mkdir api +custom_modules=$(find . -name 'module' -type d -not -path "./proto/*") +for module in $custom_modules; do + dirPath=`basename $(dirname $module)` + mkdir -p api/$dirPath + + mv $dirPath/* ./api/$dirPath/ + rm -rf $dirPath +done + diff --git a/spawn/chain_config.go b/spawn/chain_config.go index 5257a31f..bdfabda3 100644 --- a/spawn/chain_config.go +++ b/spawn/chain_config.go @@ -57,10 +57,11 @@ func (cfg *NewChainConfig) AnnounceSuccessfulBuild() { fmt.Printf("\n\n🎉 New blockchain '%s' generated!\n", projName) fmt.Println("🏅Getting started:") fmt.Println(" - $ cd " + projName) - fmt.Println(" - $ make testnet # build & start a testnet") - fmt.Println(" - $ make testnet-ibc # build & start an ibc testnet") - fmt.Println(" - $ make install # build the " + bin + " binary") - fmt.Println(" - $ make local-image # build docker image") + fmt.Println(" - $ make testnet # build & start a testnet") + fmt.Println(" - $ make testnet-ibc # build & start an ibc testnet") + fmt.Println(" - $ make install # build the " + bin + " binary") + fmt.Println(" - $ make local-image # build docker image") + fmt.Println(" - $ spawn module # generate a base module scaffold") } func (cfg *NewChainConfig) GithubPath() string { @@ -98,7 +99,7 @@ func (cfg *NewChainConfig) SetupMainChainApp() error { simappFS := simapp.SimAppFS return fs.WalkDir(simappFS, ".", func(relPath string, d fs.DirEntry, e error) error { newPath := path.Join(newDirName, relPath) - fc, err := cfg.getFileContent(newPath, simappFS, relPath, d) + fc, err := GetFileContent(cfg.Logger, newPath, simappFS, relPath, d) if err != nil { return err } else if fc == nil { @@ -142,7 +143,7 @@ func (cfg *NewChainConfig) SetupInterchainTest() error { newPath = strings.ReplaceAll(newPath, "go.mod_", "go.mod") } - fc, err := cfg.getFileContent(newPath, ictestFS, relPath, d) + fc, err := GetFileContent(cfg.Logger, newPath, ictestFS, relPath, d) if err != nil { return err } else if fc == nil { @@ -170,7 +171,7 @@ func (cfg *NewChainConfig) SetupInterchainTest() error { }) } -func (cfg *NewChainConfig) getFileContent(newFilePath string, fs embed.FS, relPath string, d fs.DirEntry) (*FileContent, error) { +func GetFileContent(logger *slog.Logger, newFilePath string, fs embed.FS, relPath string, d fs.DirEntry) (*FileContent, error) { if relPath == "." { return nil, nil } @@ -179,10 +180,10 @@ func (cfg *NewChainConfig) getFileContent(newFilePath string, fs embed.FS, relPa return nil, nil } - fc := NewFileContent(cfg.Logger, relPath, newFilePath) + fc := NewFileContent(logger, relPath, newFilePath) if fc.HasIgnoreFile() { - cfg.Logger.Debug("Ignoring file", "file", fc.NewPath) + logger.Debug("Ignoring file", "file", fc.NewPath) return nil, nil } diff --git a/spawn/file_content.go b/spawn/file_content.go index c6174599..b98dd6c3 100644 --- a/spawn/file_content.go +++ b/spawn/file_content.go @@ -98,7 +98,7 @@ func (fc *FileContent) ReplaceDockerFile(cfg *NewChainConfig) { func (fc *FileContent) ReplaceApp(cfg *NewChainConfig) { if fc.IsPath(path.Join("app", "app.go")) { fc.ReplaceAll(".wasmd", cfg.HomeDir) - fc.ReplaceAll(`const appName = "WasmApp"`, fmt.Sprintf(`const appName = "%s"`, cfg.ProjectName)) + fc.ReplaceAll(`CosmWasmApp`, cfg.ProjectName) fc.ReplaceAll(`Bech32Prefix = "wasm"`, fmt.Sprintf(`Bech32Prefix = "%s"`, cfg.Bech32Prefix)) } } @@ -118,7 +118,7 @@ func (fc *FileContent) ReplaceEverywhere(cfg *NewChainConfig) { func (fc *FileContent) ReplaceMakeFile(cfg *NewChainConfig) { bin := cfg.BinDaemon - fc.ReplaceAll("https://github.com/CosmWasm/wasmd.git", fmt.Sprintf("https://%s.git", cfg.GithubPath())) + fc.ReplaceAll("https://github.com/strangelove-ventures/simapp.git", fmt.Sprintf("https://%s.git", cfg.GithubPath())) fc.ReplaceAll("version.Name=wasm", fmt.Sprintf("version.Name=%s", cfg.ProjectName)) // ldflags fc.ReplaceAll("version.AppName=wasmd", fmt.Sprintf("version.AppName=%s", bin)) fc.ReplaceAll("cmd/wasmd", fmt.Sprintf("cmd/%s", bin)) diff --git a/spawn/parser.go b/spawn/parser.go index c4cdf3e8..e7176c08 100644 --- a/spawn/parser.go +++ b/spawn/parser.go @@ -80,7 +80,7 @@ func (fc *FileContent) RemoveTaggedLines(name string, deleteLine bool) { } startMultiLineDelete = true - fmt.Printf("startIdx %s: %d, %s\n", name, idx, line) + fc.Logger.Debug("startIdx", "idx", idx, "line", line) continue } @@ -127,7 +127,7 @@ func (fc *FileContent) RemoveModuleFromText(removeText string, pathSuffix ...str // if line contains //spawntag:ignore then we use that line. // useful if some text is 'wasm' as a bech32 prefix, not a variable / type we need to remove. if strings.Contains(line, fmt.Sprintf(StdFormat, "ignore")) { - fmt.Printf("Ignoring removal: %s: %d, %s\n", removeText, idx, line) + fc.Logger.Debug("ignoring removal", "idx", idx, "line", line) newContent = append(newContent, line) continue } @@ -135,7 +135,7 @@ func (fc *FileContent) RemoveModuleFromText(removeText string, pathSuffix ...str // if we are in a batch delete, then we need to continue until we find the close parenthesis or bracket // (i.e. NewKeeper in app.go is a good example fo this) if startBatchDelete { - fmt.Printf("rm %s startIdx: %d, %s\n", removeText, idx, line) + fc.Logger.Debug("rm", "idx", idx, "line", line) if strings.TrimSpace(line) == ")" || strings.TrimSpace(line) == "}" { fc.Logger.Debug("endIdx", "idx", idx, "line", line) @@ -151,11 +151,11 @@ func (fc *FileContent) RemoveModuleFromText(removeText string, pathSuffix ...str // if the line ends with an opening symbol, we start a batch delete process if DoesLineEndWithOpenSymbol(line) { startBatchDelete = true - fmt.Printf("startIdx %s: %d, %s\n", removeText, idx, line) + fc.Logger.Debug("startIdx", "idx", idx, "line", line) continue } - fmt.Printf("rm %s: %d, %s\n", removeText, idx, line) + fc.Logger.Debug("rm", "idx", idx, "line", line) continue }