diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b46146f326..890f4be20566 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* (cli) [#13304](https://github.com/cosmos/cosmos-sdk/pull/13304) Add `tx gov draft-proposal` command for generating proposal JSONs. * (x/authz) [#13047](https://github.com/cosmos/cosmos-sdk/pull/13047) Add a GetAuthorization function to the keeper. * (cli) [#12742](https://github.com/cosmos/cosmos-sdk/pull/12742) Add the `prune` CLI cmd to manually prune app store history versions based on the pruning options. diff --git a/client/prompts.go b/client/prompts.go new file mode 100644 index 000000000000..050d806c49a8 --- /dev/null +++ b/client/prompts.go @@ -0,0 +1,57 @@ +package client + +import ( + "fmt" + "net/url" + "unicode" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// ValidatePromptNotEmpty validates that the input is not empty. +func ValidatePromptNotEmpty(input string) error { + if input == "" { + return fmt.Errorf("input cannot be empty") + } + + return nil +} + +// ValidatePromptURL validates that the input is a valid URL. +func ValidatePromptURL(input string) error { + _, err := url.ParseRequestURI(input) + if err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + + return nil +} + +// ValidatePromptAddress validates that the input is a valid Bech32 address. +func ValidatePromptAddress(input string) error { + if _, err := sdk.AccAddressFromBech32(input); err != nil { + return fmt.Errorf("invalid address: %w", err) + } + + return nil +} + +// ValidatePromptYesNo validates that the input is valid sdk.COins +func ValidatePromptCoins(input string) error { + if _, err := sdk.ParseCoinsNormalized(input); err != nil { + return fmt.Errorf("invalid coins: %w", err) + } + + return nil +} + +// CamelCaseToString converts a camel case string to a string with spaces. +func CamelCaseToString(str string) string { + w := []rune(str) + for i := len(w) - 1; i > 1; i-- { + if unicode.IsUpper(w[i]) { + w = append(w[:i], append([]rune{' '}, w[i:]...)...) + } + } + return string(w) +} diff --git a/go.mod b/go.mod index 52ae925cc061..402deebac534 100644 --- a/go.mod +++ b/go.mod @@ -30,8 +30,8 @@ require ( github.com/jhump/protoreflect v1.12.1-0.20220721211354-060cc04fc18b github.com/lazyledger/smt v0.2.1-0.20210709230900-03ea40719554 github.com/magiconair/properties v1.8.6 + github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-isatty v0.0.16 - github.com/otiai10/copy v1.6.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.14.0 github.com/prometheus/common v0.37.0 @@ -51,8 +51,8 @@ require ( golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e google.golang.org/genproto v0.0.0-20220725144611-272f38e5d71b - google.golang.org/grpc v1.48.0 - google.golang.org/protobuf v1.28.0 + google.golang.org/grpc v1.49.0 + google.golang.org/protobuf v1.28.1 pgregory.net/rapid v0.4.7 sigs.k8s.io/yaml v1.3.0 ) @@ -71,6 +71,7 @@ require ( github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cosmos/ledger-go v0.9.2 // indirect github.com/creachadair/taskgroup v0.3.2 // indirect @@ -134,7 +135,7 @@ require ( go.opencensus.io v0.23.0 // indirect golang.org/x/net v0.0.0-20220726230323-06994584191e // indirect golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect - golang.org/x/sys v0.0.0-20220727055044-e65921a090b8 // indirect + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect diff --git a/go.sum b/go.sum index 12c371646a38..7d640d6525ab 100644 --- a/go.sum +++ b/go.sum @@ -185,8 +185,11 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= @@ -679,6 +682,8 @@ github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamh github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -1236,8 +1241,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220727055044-e65921a090b8 h1:dyU22nBWzrmTQxtNrr4dzVOvaw35nUYE279vF9UmsI8= -golang.org/x/sys v0.0.0-20220727055044-e65921a090b8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1490,8 +1495,8 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1505,10 +1510,9 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 h1:KR8+MyP7/qOlV+8Af01LtjL04bu7on42eVsxT4EyBQk= -google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index ec5f49287711..b997f5a4a8b9 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" "os" - "reflect" // #nosec + "reflect" "sort" "strconv" "strings" @@ -12,14 +12,12 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" - "cosmossdk.io/core/address" - "cosmossdk.io/x/gov/types" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/gov/types" ) const ( @@ -29,43 +27,21 @@ const ( draftMetadataFileName = "draft_metadata.json" ) -var suggestedProposalTypes = []proposalType{ - { - Name: proposalText, - MsgType: "", // no message for text proposal - }, - { - Name: "community-pool-spend", - MsgType: "/cosmos.protocolpool.v1.MsgCommunityPoolSpend", - }, - { - Name: "software-upgrade", - MsgType: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", - }, - { - Name: "cancel-software-upgrade", - MsgType: "/cosmos.upgrade.v1beta1.MsgCancelUpgrade", - }, - { - Name: "submit-budget-proposal", - MsgType: "/cosmos.protocolpool.v1.MsgSubmitBudgetProposal", - }, - { - Name: "create-continuous-fund", - MsgType: "/cosmos.protocolpool.v1.MsgCreateContinuousFund", - }, - { - Name: proposalOther, - MsgType: "", // user will input the message type - }, +// ProposalMetadata is the metadata of a proposal +// This metadata is supposed to live off-chain when submitted in a proposal +type ProposalMetadata struct { + Title string `json:"title"` + Authors string `json:"authors"` + Summary string `json:"summary"` + Details string `json:"details"` + ProposalForumUrl string `json:"proposal_forum_url"` // named 'Url' instead of 'URL' for avoiding the camel case split + VoteOptionContext string `json:"vote_option_context"` } // Prompt prompts the user for all values of the given type. // data is the struct to be filled -// namePrefix is the name to be displayed as "Enter " -// TODO: when bringing this in autocli, use proto message instead -// this will simplify the get address logic -func Prompt[T any](data T, namePrefix string, addressCodec address.Codec) (T, error) { +// namePrefix is the name to be display as "Enter " +func Prompt[T any](data T, namePrefix string) (T, error) { v := reflect.ValueOf(&data).Elem() if v.Kind() == reflect.Interface { v = reflect.ValueOf(data) @@ -75,15 +51,10 @@ func Prompt[T any](data T, namePrefix string, addressCodec address.Codec) (T, er } for i := 0; i < v.NumField(); i++ { - // if the field is a struct skip or not slice of string or int then skip - switch v.Field(i).Kind() { - case reflect.Struct: - // TODO(@julienrbrt) in the future we can add a recursive call to Prompt + if v.Field(i).Kind() == reflect.Struct || v.Field(i).Kind() == reflect.Slice { + // if the field is a struct skip + // in a future we can add a recursive call to Prompt continue - case reflect.Slice: - if v.Field(i).Type().Elem().Kind() != reflect.String && v.Field(i).Type().Elem().Kind() != reflect.Int { - continue - } } // create prompts @@ -93,18 +64,17 @@ func Prompt[T any](data T, namePrefix string, addressCodec address.Codec) (T, er } fieldName := strings.ToLower(v.Type().Field(i).Name) + // validation per field name + if strings.Contains(fieldName, "url") { + prompt.Validate = client.ValidatePromptURL + } if strings.EqualFold(fieldName, "authority") { // pre-fill with gov address - defaultAddr, err := addressCodec.BytesToString(authtypes.NewModuleAddress(types.ModuleName)) - if err != nil { - return data, err - } - prompt.Default = defaultAddr + prompt.Default = authtypes.NewModuleAddress(types.ModuleName).String() prompt.Validate = client.ValidatePromptAddress } - // TODO(@julienrbrt) use scalar annotation instead of dumb string name matching if strings.Contains(fieldName, "addr") || strings.Contains(fieldName, "sender") || strings.Contains(fieldName, "voter") || @@ -124,31 +94,11 @@ func Prompt[T any](data T, namePrefix string, addressCodec address.Codec) (T, er case reflect.String: v.Field(i).SetString(result) case reflect.Int: - resultInt, err := strconv.ParseInt(result, 10, 0) - if err != nil { - return data, fmt.Errorf("invalid value for int: %w", err) - } - // If a value was successfully parsed the ranges of: - // [minInt, maxInt] - // are within the ranges of: - // [minInt64, maxInt64] - // of which on 64-bit machines, which are most common, - // int==int64 - v.Field(i).SetInt(resultInt) - case reflect.Slice: - switch v.Field(i).Type().Elem().Kind() { - case reflect.String: - v.Field(i).Set(reflect.ValueOf([]string{result})) - case reflect.Int: - resultInt, err := strconv.ParseInt(result, 10, 0) - if err != nil { - return data, fmt.Errorf("invalid value for int: %w", err) - } - - v.Field(i).Set(reflect.ValueOf([]int{int(resultInt)})) - } + resultInt, _ := strconv.Atoi(result) + v.Field(i).SetInt(int64(resultInt)) default: - // skip any other types + // skip other types + // possibly in the future we can add more types (like slices) continue } } @@ -156,24 +106,22 @@ func Prompt[T any](data T, namePrefix string, addressCodec address.Codec) (T, er return data, nil } -type proposalType struct { - Name string +type proposalTypes struct { + Type string MsgType string Msg sdk.Msg } // Prompt the proposal type values and return the proposal and its metadata -func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec address.Codec) (*proposal, types.ProposalMetadata, error) { - metadata, err := PromptMetadata(skipMetadata, addressCodec) +func (p *proposalTypes) Prompt(cdc codec.Codec) (*proposal, ProposalMetadata, error) { + proposal := &proposal{} + + // set metadata + metadata, err := Prompt(ProposalMetadata{}, "proposal") if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) } - - proposal := &proposal{ - Metadata: "ipfs://CID", // the metadata must be saved on IPFS, set placeholder - Title: metadata.Title, - Summary: metadata.Summary, - } + proposal.Metadata = "ipfs://CID" // set deposit depositPrompt := promptui.Prompt{ @@ -190,7 +138,7 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec a } // set messages field - result, err := Prompt(p.Msg, "msg", addressCodec) + result, err := Prompt(p.Msg, "msg") if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err) } @@ -200,61 +148,63 @@ func (p *proposalType) Prompt(cdc codec.Codec, skipMetadata bool, addressCodec a return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) } proposal.Messages = append(proposal.Messages, message) - return proposal, metadata, nil } -// getProposalSuggestions suggests a list of proposal types -func getProposalSuggestions() []string { - types := make([]string, len(suggestedProposalTypes)) - for i, p := range suggestedProposalTypes { - types[i] = p.Name - } - return types +var supportedProposalTypes = []proposalTypes{ + { + Type: proposalText, + MsgType: "", // no message for text proposal + }, + { + Type: "community-pool-spend", + MsgType: "/cosmos.distribution.v1beta1.MsgCommunityPoolSpend", + }, + { + Type: "software-upgrade", + MsgType: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", + }, + { + Type: "cancel-software-upgrade", + MsgType: "/cosmos.upgrade.v1beta1.MsgCancelUpgrade", + }, + { + Type: proposalOther, + MsgType: "", // user will input the message type + }, } -// PromptMetadata prompts for proposal metadata or only title and summary if skip is true -func PromptMetadata(skip bool, addressCodec address.Codec) (types.ProposalMetadata, error) { - if !skip { - metadata, err := Prompt(types.ProposalMetadata{}, "proposal", addressCodec) - if err != nil { - return metadata, fmt.Errorf("failed to set proposal metadata: %w", err) - } - - return metadata, nil - } - - // prompt for title and summary - titlePrompt := promptui.Prompt{ - Label: "Enter proposal title", - Validate: client.ValidatePromptNotEmpty, +func getProposalTypes() []string { + types := make([]string, len(supportedProposalTypes)) + for i, p := range supportedProposalTypes { + types[i] = p.Type } + return types +} - title, err := titlePrompt.Run() +func getProposalMsg(cdc codec.Codec, input string) (sdk.Msg, error) { + var msg sdk.Msg + bz, err := json.Marshal(struct { + Type string `json:"@type"` + }{ + Type: input, + }) if err != nil { - return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal title: %w", err) - } - - summaryPrompt := promptui.Prompt{ - Label: "Enter proposal summary", - Validate: client.ValidatePromptNotEmpty, + return nil, err } - summary, err := summaryPrompt.Run() - if err != nil { - return types.ProposalMetadata{}, fmt.Errorf("failed to set proposal summary: %w", err) + if err := cdc.UnmarshalInterfaceJSON(bz, &msg); err != nil { + return nil, fmt.Errorf("failed to determined sdk.Msg from %s proposal type : %w", input, err) } - return types.ProposalMetadata{Title: title, Summary: summary}, nil + return msg, nil } // NewCmdDraftProposal let a user generate a draft proposal. func NewCmdDraftProposal() *cobra.Command { - flagSkipMetadata := "skip-metadata" - cmd := &cobra.Command{ Use: "draft-proposal", - Short: "Generate a draft proposal json file. The generated proposal json contains only one message (skeleton).", + Short: "Generate a draft proposal json file. The generated proposal json contains only one message.", SilenceUsage: true, RunE: func(cmd *cobra.Command, _ []string) error { clientCtx, err := client.GetClientTxContext(cmd) @@ -265,24 +215,24 @@ func NewCmdDraftProposal() *cobra.Command { // prompt proposal type proposalTypesPrompt := promptui.Select{ Label: "Select proposal type", - Items: getProposalSuggestions(), + Items: getProposalTypes(), } - _, selectedProposalType, err := proposalTypesPrompt.Run() + _, proposalType, err := proposalTypesPrompt.Run() if err != nil { return fmt.Errorf("failed to prompt proposal types: %w", err) } - var proposal proposalType - for _, p := range suggestedProposalTypes { - if strings.EqualFold(p.Name, selectedProposalType) { + var proposal proposalTypes + for _, p := range supportedProposalTypes { + if strings.EqualFold(p.Type, proposalType) { proposal = p break } } // create any proposal type - if proposal.Name == proposalOther { + if proposal.Type == proposalOther { // prompt proposal type msgPrompt := promptui.Select{ Label: "Select proposal message type:", @@ -302,43 +252,37 @@ func NewCmdDraftProposal() *cobra.Command { } if proposal.MsgType != "" { - proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, proposal.MsgType) + proposal.Msg, err = getProposalMsg(clientCtx.Codec, proposal.MsgType) if err != nil { // should never happen panic(err) } } - skipMetadataPrompt, _ := cmd.Flags().GetBool(flagSkipMetadata) - - result, metadata, err := proposal.Prompt(clientCtx.Codec, skipMetadataPrompt, clientCtx.AddressCodec) + prop, metadata, err := proposal.Prompt(clientCtx.Codec) if err != nil { return err } - if err := writeFile(draftProposalFileName, result); err != nil { + if err := writeFile(draftMetadataFileName, metadata); err != nil { return err } - if !skipMetadataPrompt { - if err := writeFile(draftMetadataFileName, metadata); err != nil { - return err - } + if err := writeFile(draftProposalFileName, prop); err != nil { + return err } - cmd.Println("The draft proposal has successfully been generated.\nProposals should contain off-chain metadata, please upload the metadata JSON to IPFS.\nThen, replace the generated metadata field with the IPFS CID.") + fmt.Printf("Your draft proposal has successfully been generated.\nProposals should contain off-chain metadata, please upload the metadata JSON to IPFS.\nThen, replace the generated metadata field with the IPFS CID.\n") return nil }, } flags.AddTxFlagsToCmd(cmd) - cmd.Flags().Bool(flagSkipMetadata, false, "skip metadata prompt") return cmd } -// writeFile writes the input to the file func writeFile(fileName string, input any) error { raw, err := json.MarshalIndent(input, "", " ") if err != nil { diff --git a/x/gov/client/cli/util.go b/x/gov/client/cli/util.go deleted file mode 100644 index f8628f9cffed..000000000000 --- a/x/gov/client/cli/util.go +++ /dev/null @@ -1,211 +0,0 @@ -package cli - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - govutils "cosmossdk.io/x/gov/client/utils" - govv1 "cosmossdk.io/x/gov/types/v1" - "cosmossdk.io/x/gov/types/v1beta1" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -type legacyProposal struct { - Title string - Description string - Type string - Deposit string -} - -// validate the legacyProposal -func (p legacyProposal) validate() error { - if p.Type == "" { - return errors.New("proposal type is required") - } - - if p.Title == "" { - return errors.New("proposal title is required") - } - - if p.Description == "" { - return errors.New("proposal description is required") - } - return nil -} - -// parseSubmitLegacyProposal reads and parses the legacy proposal. -func parseSubmitLegacyProposal(fs *pflag.FlagSet) (*legacyProposal, error) { - proposal := &legacyProposal{} - proposalFile, _ := fs.GetString(FlagProposal) - - if proposalFile == "" { - proposalType, _ := fs.GetString(FlagProposalType) - proposal.Title, _ = fs.GetString(FlagTitle) - proposal.Description, _ = fs.GetString(FlagDescription) - - if strings.EqualFold(proposalType, "text") { - proposal.Type = v1beta1.ProposalTypeText - } - proposal.Deposit, _ = fs.GetString(FlagDeposit) - if err := proposal.validate(); err != nil { - return nil, err - } - - return proposal, nil - } - - for _, flag := range ProposalFlags { - if v, _ := fs.GetString(flag); v != "" { - return nil, fmt.Errorf("--%s flag provided alongside --proposal, which is a noop", flag) - } - } - - contents, err := os.ReadFile(proposalFile) - if err != nil { - return nil, err - } - - err = json.Unmarshal(contents, proposal) - if err != nil { - return nil, err - } - - if err := proposal.validate(); err != nil { - return nil, err - } - - return proposal, nil -} - -// proposal defines the new Msg-based proposal. -type proposal struct { - // Msgs defines an array of sdk.Msgs proto-JSON-encoded as Anys. - Messages []json.RawMessage `json:"messages,omitempty"` - Metadata string `json:"metadata"` - Deposit string `json:"deposit"` - Title string `json:"title"` - Summary string `json:"summary"` - ProposalTypeStr string `json:"proposal_type,omitempty"` - - proposalType govv1.ProposalType -} - -// parseSubmitProposal reads and parses the proposal. -func parseSubmitProposal(cdc codec.Codec, path string) (proposal, []sdk.Msg, sdk.Coins, error) { - var proposal proposal - - contents, err := os.ReadFile(path) - if err != nil { - return proposal, nil, nil, err - } - - err = json.Unmarshal(contents, &proposal) - if err != nil { - return proposal, nil, nil, err - } - - proposalType := govv1.ProposalType_PROPOSAL_TYPE_STANDARD - if proposal.ProposalTypeStr != "" { - proposalType = govutils.NormalizeProposalType(proposal.ProposalTypeStr) - } - proposal.proposalType = proposalType - - msgs := make([]sdk.Msg, len(proposal.Messages)) - for i, anyJSON := range proposal.Messages { - var msg sdk.Msg - err := cdc.UnmarshalInterfaceJSON(anyJSON, &msg) - if err != nil { - return proposal, nil, nil, err - } - - msgs[i] = msg - } - - deposit, err := sdk.ParseCoinsNormalized(proposal.Deposit) - if err != nil { - return proposal, nil, nil, err - } - - return proposal, msgs, deposit, nil -} - -// AddGovPropFlagsToCmd adds flags for defining MsgSubmitProposal fields. -// -// See also ReadGovPropFlags. -func AddGovPropFlagsToCmd(cmd *cobra.Command) { - cmd.Flags().String(FlagDeposit, "", "The deposit to include with the governance proposal") - cmd.Flags().String(FlagMetadata, "", "The metadata to include with the governance proposal") - cmd.Flags().String(FlagTitle, "", "The title to put on the governance proposal") - cmd.Flags().String(FlagSummary, "", "The summary to include with the governance proposal") - cmd.Flags().Bool(FlagExpedited, false, "Whether to expedite the governance proposal") -} - -// ReadGovPropCmdFlags parses a MsgSubmitProposal from the provided context and flags. -// Setting the messages is up to the caller. -// -// See also AddGovPropFlagsToCmd. -func ReadGovPropCmdFlags(proposer string, flagSet *pflag.FlagSet) (*govv1.MsgSubmitProposal, error) { - rv := &govv1.MsgSubmitProposal{} - - deposit, err := flagSet.GetString(FlagDeposit) - if err != nil { - return nil, fmt.Errorf("could not read deposit: %w", err) - } - if len(deposit) > 0 { - rv.InitialDeposit, err = sdk.ParseCoinsNormalized(deposit) - if err != nil { - return nil, fmt.Errorf("invalid deposit: %w", err) - } - } - - rv.Metadata, err = flagSet.GetString(FlagMetadata) - if err != nil { - return nil, fmt.Errorf("could not read metadata: %w", err) - } - - rv.Title, err = flagSet.GetString(FlagTitle) - if err != nil { - return nil, fmt.Errorf("could not read title: %w", err) - } - - rv.Summary, err = flagSet.GetString(FlagSummary) - if err != nil { - return nil, fmt.Errorf("could not read summary: %w", err) - } - - expedited, err := flagSet.GetBool(FlagExpedited) - if err != nil { - return nil, fmt.Errorf("could not read expedited: %w", err) - } - if expedited { - rv.Expedited = true - rv.ProposalType = govv1.ProposalType_PROPOSAL_TYPE_EXPEDITED - } - - rv.Proposer = proposer - - return rv, nil -} - -// ReadGovPropFlags parses a MsgSubmitProposal from the provided context and flags. -// Setting the messages is up to the caller. -// -// See also AddGovPropFlagsToCmd. -// Deprecated: use ReadPropCmdFlags instead, as this depends on global bech32 prefixes. -func ReadGovPropFlags(clientCtx client.Context, flagSet *pflag.FlagSet) (*govv1.MsgSubmitProposal, error) { - addr, err := clientCtx.AddressCodec.BytesToString(clientCtx.GetFromAddress()) - if err != nil { - return nil, err - } - - return ReadGovPropCmdFlags(addr, flagSet) -} diff --git a/x/gov/client/cli/util_test.go b/x/gov/client/cli/util_test.go deleted file mode 100644 index 2601a526fe1b..000000000000 --- a/x/gov/client/cli/util_test.go +++ /dev/null @@ -1,716 +0,0 @@ -package cli - -import ( - "bytes" - "encoding/base64" - "fmt" - "io" - "os" - "strings" - "testing" - - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - sdkmath "cosmossdk.io/math" - banktypes "cosmossdk.io/x/bank/types" - v1 "cosmossdk.io/x/gov/types/v1" - "cosmossdk.io/x/gov/types/v1beta1" - stakingtypes "cosmossdk.io/x/staking/types" - - "github.com/cosmos/cosmos-sdk/client" - "github.com/cosmos/cosmos-sdk/codec" - codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "github.com/cosmos/cosmos-sdk/testutil" - "github.com/cosmos/cosmos-sdk/testutil/testdata" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -const ( - strPlus = ` string\s+` - startStr = `(?m:^\s+--` - strDollar = `$)` - strHelp = "help output" -) - -func TestParseSubmitLegacyProposal(t *testing.T) { - okJSON := testutil.WriteToNewTempFile(t, ` -{ - "title": "Test Proposal", - "description": "My awesome proposal", - "type": "Text", - "deposit": "1000test" -} -`) - - badJSON := testutil.WriteToNewTempFile(t, "bad json") - fs := NewCmdSubmitLegacyProposal().Flags() - - // nonexistent json - err := fs.Set(FlagProposal, "fileDoesNotExist") - require.NoError(t, err) - - _, err = parseSubmitLegacyProposal(fs) - require.Error(t, err) - - // invalid json - err = fs.Set(FlagProposal, badJSON.Name()) - require.NoError(t, err) - _, err = parseSubmitLegacyProposal(fs) - require.Error(t, err) - - // ok json - err = fs.Set(FlagProposal, okJSON.Name()) - require.NoError(t, err) - proposal1, err := parseSubmitLegacyProposal(fs) - require.Nil(t, err, "unexpected error") - require.Equal(t, "Test Proposal", proposal1.Title) - require.Equal(t, "My awesome proposal", proposal1.Description) - require.Equal(t, "Text", proposal1.Type) - require.Equal(t, "1000test", proposal1.Deposit) - - // flags that can't be used with --proposal - for _, incompatibleFlag := range ProposalFlags { - err = fs.Set(incompatibleFlag, "some value") - require.NoError(t, err) - _, err := parseSubmitLegacyProposal(fs) - require.Error(t, err) - err = fs.Set(incompatibleFlag, "") - require.NoError(t, err) - } - - // no --proposal, only flags - err = fs.Set(FlagProposal, "") - require.NoError(t, err) - flagTestCases := map[string]struct { - pTitle string - pDescription string - pType string - expErr bool - errMsg string - }{ - "valid flags": { - pTitle: proposal1.Title, - pDescription: proposal1.Description, - pType: proposal1.Type, - }, - "empty type": { - pTitle: proposal1.Title, - pDescription: proposal1.Description, - expErr: true, - errMsg: "proposal type is required", - }, - "empty title": { - pDescription: proposal1.Description, - pType: proposal1.Type, - expErr: true, - errMsg: "proposal title is required", - }, - "empty description": { - pTitle: proposal1.Title, - pType: proposal1.Type, - expErr: true, - errMsg: "proposal description is required", - }, - } - for name, tc := range flagTestCases { - t.Run(name, func(t *testing.T) { - err = fs.Set(FlagTitle, tc.pTitle) - require.NoError(t, err) - err = fs.Set(FlagDescription, tc.pDescription) - require.NoError(t, err) - err = fs.Set(FlagProposalType, tc.pType) - require.NoError(t, err) - err = fs.Set(FlagDeposit, proposal1.Deposit) - require.NoError(t, err) - proposal2, err := parseSubmitLegacyProposal(fs) - - if tc.expErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errMsg) - } else { - require.NoError(t, err) - require.Equal(t, proposal1.Title, proposal2.Title) - require.Equal(t, proposal1.Description, proposal2.Description) - require.Equal(t, proposal1.Type, proposal2.Type) - require.Equal(t, proposal1.Deposit, proposal2.Deposit) - } - }) - } - - err = okJSON.Close() - require.Nil(t, err, "unexpected error") - err = badJSON.Close() - require.Nil(t, err, "unexpected error") -} - -func TestParseSubmitProposal(t *testing.T) { - _, _, addr := testdata.KeyTestPubAddr() - addrStr, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(addr) - require.NoError(t, err) - - interfaceRegistry := codectypes.NewInterfaceRegistry() - cdc := codec.NewProtoCodec(interfaceRegistry) - banktypes.RegisterInterfaces(interfaceRegistry) - stakingtypes.RegisterInterfaces(interfaceRegistry) - v1beta1.RegisterInterfaces(interfaceRegistry) - v1.RegisterInterfaces(interfaceRegistry) - expectedMetadata := []byte{42} - - okJSON := testutil.WriteToNewTempFile(t, fmt.Sprintf(` -{ - "messages": [ - { - "@type": "/cosmos.bank.v1beta1.MsgSend", - "from_address": "%s", - "to_address": "%s", - "amount":[{"denom": "stake","amount": "10"}] - }, - { - "@type": "/cosmos.staking.v1beta1.MsgDelegate", - "delegator_address": "%s", - "validator_address": "%s", - "amount":{"denom": "stake","amount": "10"} - }, - { - "@type": "/cosmos.gov.v1.MsgExecLegacyContent", - "authority": "%s", - "content": { - "@type": "/cosmos.gov.v1beta1.TextProposal", - "title": "My awesome title", - "description": "My awesome description" - } - } - ], - "metadata": "%s", - "title": "My awesome title", - "summary": "My awesome summary", - "deposit": "1000test", - "proposal_type": "expedited" -} -`, addr, addr, addr, addr, addr, base64.StdEncoding.EncodeToString(expectedMetadata))) - - badJSON := testutil.WriteToNewTempFile(t, "bad json") - - // nonexistent json - _, _, _, err = parseSubmitProposal(cdc, "fileDoesNotExist") - require.Error(t, err) - - // invalid json - _, _, _, err = parseSubmitProposal(cdc, badJSON.Name()) - require.Error(t, err) - - // ok json - proposal, msgs, deposit, err := parseSubmitProposal(cdc, okJSON.Name()) - require.NoError(t, err, "unexpected error") - require.Equal(t, sdk.NewCoins(sdk.NewCoin("test", sdkmath.NewInt(1000))), deposit) - require.Equal(t, base64.StdEncoding.EncodeToString(expectedMetadata), proposal.Metadata) - require.Len(t, msgs, 3) - msg1, ok := msgs[0].(*banktypes.MsgSend) - require.True(t, ok) - require.Equal(t, addrStr, msg1.FromAddress) - require.Equal(t, addrStr, msg1.ToAddress) - require.Equal(t, sdk.NewCoins(sdk.NewCoin("stake", sdkmath.NewInt(10))), msg1.Amount) - msg2, ok := msgs[1].(*stakingtypes.MsgDelegate) - require.True(t, ok) - require.Equal(t, addrStr, msg2.DelegatorAddress) - require.Equal(t, addrStr, msg2.ValidatorAddress) - require.Equal(t, sdk.NewCoin("stake", sdkmath.NewInt(10)), msg2.Amount) - msg3, ok := msgs[2].(*v1.MsgExecLegacyContent) - require.True(t, ok) - require.Equal(t, addrStr, msg3.Authority) - textProp, ok := msg3.Content.GetCachedValue().(*v1beta1.TextProposal) - require.True(t, ok) - require.Equal(t, "My awesome title", textProp.Title) - require.Equal(t, "My awesome description", textProp.Description) - require.Equal(t, "My awesome title", proposal.Title) - require.Equal(t, "My awesome summary", proposal.Summary) - require.Equal(t, v1.ProposalType_PROPOSAL_TYPE_EXPEDITED, proposal.proposalType) - - err = okJSON.Close() - require.Nil(t, err, "unexpected error") - err = badJSON.Close() - require.Nil(t, err, "unexpected error") -} - -func getCommandHelp(t *testing.T, cmd *cobra.Command) string { - t.Helper() - // Create a pipe, so we can capture the help sent to stdout. - reader, writer, err := os.Pipe() - require.NoError(t, err, "creating os.Pipe()") - outChan := make(chan string) - defer func(origCmdOut io.Writer) { - cmd.SetOut(origCmdOut) - // Ignoring these errors since we're just ensuring cleanup here, - // and they will return an error if already called (which we don't care about). - _ = reader.Close() - _ = writer.Close() - close(outChan) - }(cmd.OutOrStdout()) - cmd.SetOut(writer) - - // Do the reading in a separate goroutine from the writing (a best practice). - go func() { - var b bytes.Buffer - _, buffErr := io.Copy(&b, reader) - if buffErr != nil { - // Due to complexities of goroutines and multiple channels, I'm sticking with a - // single channel and just putting the error in there (which I'll test for later). - b.WriteString("buffer error: " + buffErr.Error()) - } - outChan <- b.String() - }() - - err = cmd.Help() - require.NoError(t, err, "cmd.Help()") - require.NoError(t, writer.Close(), "pipe writer .Close()") - rv := <-outChan - require.NotContains(t, rv, "buffer error: ", "buffer output") - return rv -} - -func TestAddGovPropFlagsToCmd(t *testing.T) { - cmd := &cobra.Command{ - Short: "Just a test command that does nothing but we can add flags to it.", - Run: func(cmd *cobra.Command, args []string) { - t.Errorf("The cmd has run with the args %q, but Run shouldn't have been called.", args) - }, - } - testFunc := func() { - AddGovPropFlagsToCmd(cmd) - } - require.NotPanics(t, testFunc, "AddGovPropFlagsToCmd") - - help := getCommandHelp(t, cmd) - - expDepositDesc := "The deposit to include with the governance proposal" - expMetadataDesc := "The metadata to include with the governance proposal" - expTitleDesc := "The title to put on the governance proposal" - expSummaryDesc := "The summary to include with the governance proposal" - // Regexp notes: (?m:...) = multi-line mode so ^ and $ match the beginning and end of each line. - // Each regexp assertion checks for a line containing only a specific flag and its description. - assert.Regexp(t, startStr+FlagDeposit+strPlus+expDepositDesc+strDollar, help, strHelp) - assert.Regexp(t, startStr+FlagMetadata+strPlus+expMetadataDesc+strDollar, help, strHelp) - assert.Regexp(t, startStr+FlagTitle+strPlus+expTitleDesc+strDollar, help, strHelp) - assert.Regexp(t, startStr+FlagSummary+strPlus+expSummaryDesc+strDollar, help, strHelp) -} - -func TestReadGovPropFlags(t *testing.T) { - fromAddr := sdk.AccAddress("from_addr___________") - argDeposit := "--" + FlagDeposit - argMetadata := "--" + FlagMetadata - argTitle := "--" + FlagTitle - argSummary := "--" + FlagSummary - - fromAddrStr, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(fromAddr) - require.NoError(t, err) - // cz is a shorter way to define coins objects for these tests. - cz := func(coins string) sdk.Coins { - rv, err := sdk.ParseCoinsNormalized(coins) - require.NoError(t, err, "ParseCoinsNormalized(%q)", coins) - return rv - } - - tests := []struct { - name string - fromAddr sdk.AccAddress - args []string - exp *v1.MsgSubmitProposal - expErr []string - }{ - { - name: "no args no from", - fromAddr: nil, - args: []string{}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only from defined", - fromAddr: fromAddr, - args: []string{}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: fromAddrStr, - Metadata: "", - Title: "", - Summary: "", - }, - }, - - // only deposit tests. - { - name: "only deposit empty string", - fromAddr: nil, - args: []string{argDeposit, ""}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only deposit one coin", - fromAddr: nil, - args: []string{argDeposit, "1bigcoin"}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("1bigcoin"), - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only deposit invalid coins", - fromAddr: nil, - args: []string{argDeposit, "not really coins"}, - expErr: []string{"invalid deposit", "invalid character in denomination"}, - }, - { - name: "only deposit two coins", - fromAddr: nil, - args: []string{argDeposit, "1acoin,2bcoin"}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("1acoin,2bcoin"), - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only deposit two coins other order", - fromAddr: nil, - args: []string{argDeposit, "2bcoin,1acoin"}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("1acoin,2bcoin"), - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only deposit coin 1 of 3 bad", - fromAddr: nil, - args: []string{argDeposit, "1bad^coin,2bcoin,3ccoin"}, - expErr: []string{"invalid deposit", "invalid character in denomination"}, - }, - { - name: "only deposit coin 2 of 3 bad", - fromAddr: nil, - args: []string{argDeposit, "1acoin,2bad^coin,3ccoin"}, - expErr: []string{"invalid deposit", "invalid character in denomination"}, - }, - { - name: "only deposit coin 3 of 3 bad", - fromAddr: nil, - args: []string{argDeposit, "1acoin,2bcoin,3bad^coin"}, - expErr: []string{"invalid deposit", "invalid character in denomination"}, - }, - // As far as I can tell, there's no way to make flagSet.GetString return an error for a defined string flag. - // So I don't have a test for the "could not read deposit" error case. - - // only metadata tests. - { - name: "only metadata empty", - fromAddr: nil, - args: []string{argMetadata, ""}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only metadata simple", - fromAddr: nil, - args: []string{argMetadata, "just some metadata"}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "just some metadata", - Title: "", - Summary: "", - }, - }, - { - name: "only metadata super long", - fromAddr: nil, - args: []string{argMetadata, strings.Repeat("Long", 1_000_000)}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: strings.Repeat("Long", 1_000_000), - Title: "", - Summary: "", - }, - }, - // As far as I can tell, there's no way to make flagSet.GetString return an error for a defined string flag. - // So I don't have a test for the "could not read metadata" error case. - - // only title tests. - { - name: "only title empty", - fromAddr: nil, - args: []string{argTitle, ""}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only title simple", - fromAddr: nil, - args: []string{argTitle, "just a title"}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "just a title", - Summary: "", - }, - }, - { - name: "only title super long", - fromAddr: nil, - args: []string{argTitle, strings.Repeat("Long", 1_000_000)}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: strings.Repeat("Long", 1_000_000), - Summary: "", - }, - }, - // As far as I can tell, there's no way to make flagSet.GetString return an error for a defined string flag. - // So I don't have a test for the "could not read title" error case. - - // only summary tests. - { - name: "only summary empty", - fromAddr: nil, - args: []string{argSummary, ""}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "only summary simple", - fromAddr: nil, - args: []string{argSummary, "just a short summary"}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: "just a short summary", - }, - }, - { - name: "only summary super long", - fromAddr: nil, - args: []string{argSummary, strings.Repeat("Long", 1_000_000)}, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: strings.Repeat("Long", 1_000_000), - }, - }, - // As far as I can tell, there's no way to make flagSet.GetString return an error for a defined string flag. - // So I don't have a test for the "could not read summary" error case. - - // Combo tests. - { - name: "all together order 1", - fromAddr: fromAddr, - args: []string{ - argDeposit, "56depcoin", - argMetadata, "my proposal is cool", - argTitle, "Simple Gov Prop Title", - argSummary, "This is just a test summary on a simple gov prop.", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("56depcoin"), - Proposer: fromAddrStr, - Metadata: "my proposal is cool", - Title: "Simple Gov Prop Title", - Summary: "This is just a test summary on a simple gov prop.", - }, - }, - { - name: "all together order 2", - fromAddr: fromAddr, - args: []string{ - argTitle, "This title is a *bit* more complex.", - argSummary, "This\nis\na\ncrazy\nsummary", - argDeposit, "78coolcoin", - argMetadata, "this proposal is cooler", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("78coolcoin"), - Proposer: fromAddrStr, - Metadata: "this proposal is cooler", - Title: "This title is a *bit* more complex.", - Summary: "This\nis\na\ncrazy\nsummary", - }, - }, - { - name: "all except proposer", - fromAddr: nil, - args: []string{ - argMetadata, "https://example.com/lucky", - argDeposit, "33luckycoin", - argSummary, "This proposal will bring you luck and good fortune in the new year.", - argTitle, "Increase Luck", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("33luckycoin"), - Proposer: "", - Metadata: "https://example.com/lucky", - Title: "Increase Luck", - Summary: "This proposal will bring you luck and good fortune in the new year.", - }, - }, - { - name: "all except proposer but all empty", - fromAddr: nil, - args: []string{ - argMetadata, "", - argDeposit, "", - argSummary, "", - argTitle, "", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: "", - Metadata: "", - Title: "", - Summary: "", - }, - }, - { - name: "all except deposit", - fromAddr: fromAddr, - args: []string{ - argTitle, "This is a Title", - argSummary, "This is a useless summary", - argMetadata, "worthless metadata", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: nil, - Proposer: fromAddrStr, - Metadata: "worthless metadata", - Title: "This is a Title", - Summary: "This is a useless summary", - }, - expErr: nil, - }, - { - name: "all except metadata", - fromAddr: fromAddr, - args: []string{ - argTitle, "Bland Title", - argSummary, "Boring summary", - argDeposit, "99mdcoin", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("99mdcoin"), - Proposer: fromAddrStr, - Metadata: "", - Title: "Bland Title", - Summary: "Boring summary", - }, - }, - { - name: "all except title", - fromAddr: fromAddr, - args: []string{ - argMetadata, "this metadata does not have the title either", - argDeposit, "71whatcoin", - argSummary, "This is a summary on a titleless proposal.", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("71whatcoin"), - Proposer: fromAddrStr, - Metadata: "this metadata does not have the title either", - Title: "", - Summary: "This is a summary on a titleless proposal.", - }, - expErr: nil, - }, - { - name: "all except summary", - fromAddr: fromAddr, - args: []string{ - argMetadata, "28", - argTitle, "Now This is What I Call A Governance Proposal 28", - argDeposit, "42musiccoin", - }, - exp: &v1.MsgSubmitProposal{ - InitialDeposit: cz("42musiccoin"), - Proposer: fromAddrStr, - Metadata: "28", - Title: "Now This is What I Call A Governance Proposal 28", - Summary: "", - }, - expErr: nil, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - cmd := &cobra.Command{ - Short: tc.name, - Run: func(cmd *cobra.Command, args []string) { - t.Errorf("The cmd for %q has run with the args %q, but Run shouldn't have been called.", tc.name, args) - }, - } - AddGovPropFlagsToCmd(cmd) - err := cmd.ParseFlags(tc.args) - require.NoError(t, err, "parsing test case args using cmd: %q", tc.args) - flagSet := cmd.Flags() - - clientCtx := client.Context{ - FromAddress: tc.fromAddr, - AddressCodec: codectestutil.CodecOptions{}.GetAddressCodec(), - } - - var msg *v1.MsgSubmitProposal - testFunc := func() { - msg, err = ReadGovPropFlags(clientCtx, flagSet) - } - require.NotPanics(t, testFunc, "ReadGovPropFlags") - if len(tc.expErr) > 0 { - require.Error(t, err, "ReadGovPropFlags error") - for _, exp := range tc.expErr { - assert.ErrorContains(t, err, exp, "ReadGovPropFlags error") - } - } else { - require.NoError(t, err, "ReadGovPropFlags error") - } - assert.Equal(t, tc.exp, msg, "ReadGovPropFlags msg") - }) - } -} diff --git a/x/gov/spec/07_client.md b/x/gov/spec/07_client.md index 66cab628f2df..01681d42a2e0 100644 --- a/x/gov/spec/07_client.md +++ b/x/gov/spec/07_client.md @@ -316,6 +316,16 @@ Example: simd tx gov deposit 1 10000000stake --from cosmos1.. ``` +#### draft-proposal + +The `draft-proposal` command allows users to draft any type of proposal. +The command returns a `draft_proposal.json`, to be used by `submit-proposal` after being completed. +The `draft_metadata.json` is meant to be uploaded to [IPFS](./08_metadata.md). + +```bash +simd tx gov draft-proposal +``` + #### submit-proposal The `submit-proposal` command allows users to submit a governance proposal and to optionally include an initial deposit.